// services/api/src/admin/users-lookup.controller.ts —— GET /admin/users 端点 // (P1b Task 4 建立;P4a Task 1 契约统一为 listAdminUsers 双模式;Task 6 // 收口双模式分流)。方法名与契约 operationId 一一对应(listAdminUsers)。 // 控制器级 SessionGuard:访问策略=任何已认证工作人员(P1b 决策,见服务文件 // 头——管理列表模式的仅 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 违例)。 import { Controller, Get, HttpStatus, Query, Req, UseGuards, } from '@nestjs/common'; import { ErrorCodes } from '@photo/contracts'; import { ApiException } from '../api-exception'; import { AuthenticatedRequest, SessionGuard } from '../auth/session.guard'; import { userOf } from '../albums/albums.controller'; import { parseCursor } from '../shared/pagination'; import { validationFailed } from '../albums/support'; import { AdminUsersService, STATUS_FILTER_VALUES, type AdminUserStatusFilter, } from './admin-users.service'; import { UsersLookupService, type AdminUserListResponse } from './users-lookup.service'; /** 契约 listAdminUsers.limit:缺省 20,上下界 1..100(P4a 统一分页 bounds)。 */ export const DEFAULT_LIST_LIMIT = 20; export const MAX_LIST_LIMIT = 100; /** accountPrefix 查询参数(前缀模式):2..64(契约 bounds)。 */ export function parseAccountPrefix(raw: string | undefined): string { // 双模式收口后 undefined 不再到达此函数(分流在 listAdminUsers);防御性 // 保留同一校验出口。空串/1 字符/超长 → 400。 if (raw === undefined || raw.length < 2 || raw.length > 64) { throw validationFailed('accountPrefix 须为 2-64 字符'); } return raw; } /** 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; } /** limit 查询参数:缺省 20,须为 1..100 的十进制整数(契约 bounds)。 */ export function parseListLimit(raw: string | undefined): number { if (raw === undefined || raw === '') { return DEFAULT_LIST_LIMIT; } if (!/^\d+$/.test(raw)) { throw new ApiException( ErrorCodes.VALIDATION_FAILED, 'limit 必须是 1-100 的整数', HttpStatus.BAD_REQUEST, ); } const limit = Number.parseInt(raw, 10); if (limit < 1 || limit > MAX_LIST_LIMIT) { throw new ApiException( ErrorCodes.VALIDATION_FAILED, 'limit 必须是 1-100 的整数', HttpStatus.BAD_REQUEST, ); } return limit; } @Controller('admin/users') @UseGuards(SessionGuard) export class UsersLookupController { constructor( private readonly usersLookupService: UsersLookupService, private readonly adminUsersService: AdminUsersService, ) {} /** 用户查询/管理列表(双模式统一响应 AdminUserList,分流见文件头)。 */ @Get() listAdminUsers( @Req() req: AuthenticatedRequest, @Query('accountPrefix') accountPrefix: string | undefined, @Query('query') query: string | undefined, @Query('status') status: string | undefined, @Query('cursor') cursor: string | undefined, @Query('limit') limit: string | undefined, ): Promise { 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, ); } }