2026-09-20 19:31:00 +08:00
|
|
|
|
// services/api/src/admin/users-lookup.controller.ts —— GET /admin/users 端点
|
|
|
|
|
|
// (P1b Task 4 建立;P4a Task 1 契约统一为 listAdminUsers 双模式;Task 6
|
|
|
|
|
|
// 收口双模式分流)。方法名与契约 operationId 一一对应(listAdminUsers)。
|
2026-09-20 15:56:38 +08:00
|
|
|
|
// 控制器级 SessionGuard:访问策略=任何已认证工作人员(P1b 决策,见服务文件
|
2026-09-20 19:31:00 +08:00
|
|
|
|
// 头——管理列表模式的仅 admin 判定在 AdminUsersService.list)。
|
|
|
|
|
|
// 双模式分流(P4a 契约):
|
|
|
|
|
|
// · 提供 accountPrefix → 前缀模式(P1b 语义保留:邀请协作者前置查询,
|
|
|
|
|
|
// 仅激活资格用户、account 升序、任何已认证工作人员可用);
|
|
|
|
|
|
// · 未提供 → 管理列表模式(全量用户、query/status 过滤、createdAt+ID 倒序
|
|
|
|
|
|
// 游标分页、仅 admin)。
|
|
|
|
|
|
// 控制器只做查询参数规整(limit/cursor 经 shared/pagination 同一 bounds),
|
|
|
|
|
|
// 语义在服务层。limit 边界已对齐统一契约:缺省 20、上下界 1..100。
|
|
|
|
|
|
// 显式空串 accountPrefix=''/query='' 是「提供了非法值」而非「未提供」→ 400
|
|
|
|
|
|
//(前者长度 <2,后者长度 <1,均契约 minLength 违例)。
|
2026-09-18 08:54:44 +08:00
|
|
|
|
import {
|
|
|
|
|
|
Controller,
|
|
|
|
|
|
Get,
|
|
|
|
|
|
HttpStatus,
|
|
|
|
|
|
Query,
|
2026-09-20 19:31:00 +08:00
|
|
|
|
Req,
|
2026-09-18 08:54:44 +08:00
|
|
|
|
UseGuards,
|
|
|
|
|
|
} from '@nestjs/common';
|
|
|
|
|
|
import { ErrorCodes } from '@photo/contracts';
|
|
|
|
|
|
import { ApiException } from '../api-exception';
|
2026-09-20 19:31:00 +08:00
|
|
|
|
import { AuthenticatedRequest, SessionGuard } from '../auth/session.guard';
|
|
|
|
|
|
import { userOf } from '../albums/albums.controller';
|
|
|
|
|
|
import { parseCursor } from '../shared/pagination';
|
2026-09-18 08:54:44 +08:00
|
|
|
|
import { validationFailed } from '../albums/support';
|
2026-09-20 19:31:00 +08:00
|
|
|
|
import {
|
|
|
|
|
|
AdminUsersService,
|
|
|
|
|
|
STATUS_FILTER_VALUES,
|
|
|
|
|
|
type AdminUserStatusFilter,
|
|
|
|
|
|
} from './admin-users.service';
|
2026-09-20 15:56:38 +08:00
|
|
|
|
import { UsersLookupService, type AdminUserListResponse } from './users-lookup.service';
|
2026-09-18 08:54:44 +08:00
|
|
|
|
|
2026-09-20 15:56:38 +08:00
|
|
|
|
/** 契约 listAdminUsers.limit:缺省 20,上下界 1..100(P4a 统一分页 bounds)。 */
|
|
|
|
|
|
export const DEFAULT_LIST_LIMIT = 20;
|
|
|
|
|
|
export const MAX_LIST_LIMIT = 100;
|
2026-09-18 08:54:44 +08:00
|
|
|
|
|
2026-09-20 15:56:38 +08:00
|
|
|
|
/** accountPrefix 查询参数(前缀模式):2..64(契约 bounds)。 */
|
2026-09-18 08:54:44 +08:00
|
|
|
|
export function parseAccountPrefix(raw: string | undefined): string {
|
2026-09-20 19:31:00 +08:00
|
|
|
|
// 双模式收口后 undefined 不再到达此函数(分流在 listAdminUsers);防御性
|
|
|
|
|
|
// 保留同一校验出口。空串/1 字符/超长 → 400。
|
|
|
|
|
|
if (raw === undefined || raw.length < 2 || raw.length > 64) {
|
2026-09-18 08:54:44 +08:00
|
|
|
|
throw validationFailed('accountPrefix 须为 2-64 字符');
|
|
|
|
|
|
}
|
|
|
|
|
|
return raw;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-20 19:31:00 +08:00
|
|
|
|
/** query 查询参数(管理列表模式):1..64(契约 bounds);未提供 → undefined。 */
|
|
|
|
|
|
export function parseAdminQuery(raw: string | undefined): string | undefined {
|
|
|
|
|
|
if (raw === undefined) {
|
|
|
|
|
|
return undefined;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (raw.length < 1 || raw.length > 64) {
|
|
|
|
|
|
throw validationFailed('query 须为 1-64 字符');
|
|
|
|
|
|
}
|
|
|
|
|
|
return raw;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** status 查询参数(管理列表模式):契约枚举单值;缺省/空串 → 不过滤。 */
|
|
|
|
|
|
export function parseStatusFilter(
|
|
|
|
|
|
raw: string | undefined,
|
|
|
|
|
|
): AdminUserStatusFilter | undefined {
|
|
|
|
|
|
if (raw === undefined || raw === '') {
|
|
|
|
|
|
return undefined;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!STATUS_FILTER_VALUES.includes(raw as AdminUserStatusFilter)) {
|
|
|
|
|
|
throw validationFailed('status 须为 active/pending/disabled/locked 之一');
|
|
|
|
|
|
}
|
|
|
|
|
|
return raw as AdminUserStatusFilter;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-20 15:56:38 +08:00
|
|
|
|
/** limit 查询参数:缺省 20,须为 1..100 的十进制整数(契约 bounds)。 */
|
|
|
|
|
|
export function parseListLimit(raw: string | undefined): number {
|
2026-09-18 08:54:44 +08:00
|
|
|
|
if (raw === undefined || raw === '') {
|
2026-09-20 15:56:38 +08:00
|
|
|
|
return DEFAULT_LIST_LIMIT;
|
2026-09-18 08:54:44 +08:00
|
|
|
|
}
|
|
|
|
|
|
if (!/^\d+$/.test(raw)) {
|
|
|
|
|
|
throw new ApiException(
|
|
|
|
|
|
ErrorCodes.VALIDATION_FAILED,
|
2026-09-20 15:56:38 +08:00
|
|
|
|
'limit 必须是 1-100 的整数',
|
2026-09-18 08:54:44 +08:00
|
|
|
|
HttpStatus.BAD_REQUEST,
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
const limit = Number.parseInt(raw, 10);
|
2026-09-20 15:56:38 +08:00
|
|
|
|
if (limit < 1 || limit > MAX_LIST_LIMIT) {
|
2026-09-18 08:54:44 +08:00
|
|
|
|
throw new ApiException(
|
|
|
|
|
|
ErrorCodes.VALIDATION_FAILED,
|
2026-09-20 15:56:38 +08:00
|
|
|
|
'limit 必须是 1-100 的整数',
|
2026-09-18 08:54:44 +08:00
|
|
|
|
HttpStatus.BAD_REQUEST,
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
return limit;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@Controller('admin/users')
|
|
|
|
|
|
@UseGuards(SessionGuard)
|
|
|
|
|
|
export class UsersLookupController {
|
2026-09-20 19:31:00 +08:00
|
|
|
|
constructor(
|
|
|
|
|
|
private readonly usersLookupService: UsersLookupService,
|
|
|
|
|
|
private readonly adminUsersService: AdminUsersService,
|
|
|
|
|
|
) {}
|
2026-09-18 08:54:44 +08:00
|
|
|
|
|
2026-09-20 19:31:00 +08:00
|
|
|
|
/** 用户查询/管理列表(双模式统一响应 AdminUserList,分流见文件头)。 */
|
2026-09-18 08:54:44 +08:00
|
|
|
|
@Get()
|
2026-09-20 15:56:38 +08:00
|
|
|
|
listAdminUsers(
|
2026-09-20 19:31:00 +08:00
|
|
|
|
@Req() req: AuthenticatedRequest,
|
2026-09-18 08:54:44 +08:00
|
|
|
|
@Query('accountPrefix') accountPrefix: string | undefined,
|
2026-09-20 19:31:00 +08:00
|
|
|
|
@Query('query') query: string | undefined,
|
|
|
|
|
|
@Query('status') status: string | undefined,
|
|
|
|
|
|
@Query('cursor') cursor: string | undefined,
|
2026-09-18 08:54:44 +08:00
|
|
|
|
@Query('limit') limit: string | undefined,
|
2026-09-20 15:56:38 +08:00
|
|
|
|
): Promise<AdminUserListResponse> {
|
2026-09-20 19:31:00 +08:00
|
|
|
|
const parsedLimit = parseListLimit(limit);
|
|
|
|
|
|
if (accountPrefix !== undefined) {
|
|
|
|
|
|
// 前缀模式(P1b 语义保留):任何已认证工作人员。
|
|
|
|
|
|
return this.usersLookupService.lookup(
|
|
|
|
|
|
parseAccountPrefix(accountPrefix),
|
|
|
|
|
|
parsedLimit,
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
// 管理列表模式(P4a Task 6):仅 admin(服务层 403)。
|
|
|
|
|
|
return this.adminUsersService.list(
|
|
|
|
|
|
userOf(req),
|
|
|
|
|
|
parseAdminQuery(query),
|
|
|
|
|
|
parseStatusFilter(status),
|
|
|
|
|
|
parseCursor(cursor),
|
|
|
|
|
|
parsedLimit,
|
2026-09-18 08:54:44 +08:00
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|