Files
photo-live/services/api/src/admin/users-lookup.controller.ts
T
浅念 8a8ad8a18d feat(contracts): p4a admin governance, watermark, download and usage endpoints
P4a Task 1 契约扩展批次(36 → 51 operations,gen/redocly/枚举同步守护绿):

- photos:POST /albums/{albumId}/photos/hide-batch(photoIds 1..200,逐项
  BatchHideResult {photoId, ok, code?},部分成功语义,A16);
- admin:POST /admin/albums/{albumId}/transfer(owner 交接,历史上传人不动,
  keepAsCollaborator,A17);
- albums:POST/GET /albums/{albumId}/watermark-jobs(WatermarkConfig 版本化
  + 历史重生成任务 state/progress/total/failed,原子切换、原图永不覆盖,
  A18/A20);
- interactions:GET /public/albums/{token}/photos/{photoId}/download
  (variant=browse|original 两级开关校验;**新增错误码 DOWNLOAD_DISABLED**,
  errors.ts 与 Error enum 两侧同步,A18);
- admin 账号治理:GET /admin/users 统一为 listAdminUsers 双模式 + PATCH
  /admin/users/{userId} 与 reset-password / activate / deactivate;
- admin 用户同步:GET /admin/user-sync/{status,runs,errors} + POST runs
  手动补拉(互斥复用进行中批次,A29);
- admin 用量:GET /admin/usage/summary + PUT thresholds(null 清除、
  即刻重评估,A23)。

偏差登记①——/admin/users 路径冲突(P1b lookupUsersByAccount):P4a 决策
统一为双模式端点(前缀模式沿用 P1b 语义与任何已认证工作人员可用;管理
列表模式 query/status 过滤 + 游标 + 仅 admin),响应统一 AdminUserList
(AdminUser 字段超集,游标元信息随行)。消费方同步:admin 邀请搜索
(fetchLookupUsers/类型镜像,行为不变)与 api 过渡实现(仍仅前缀模式、
limit 对齐 1..100 缺省 20;管理列表模式与仅 admin 判定随 Task 6 落地,
users-lookup e2e 已按统一形状修订)。

偏差登记②——apps/mobile 手写镜像未随本次更新:其邀请流程仍按旧 3 字段
投影消费(AdminUserList 为超集,运行时兼容,mobile IT 不经该路径);列
为 P4a 跟进项,仅当其 e2e 破坏时处理。

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-20 15:56:38 +08:00

81 lines
3.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// services/api/src/admin/users-lookup.controller.ts —— /admin/users 端点
// (P1b Task 4 建立;P4a Task 1 契约统一为 listAdminUsers 双模式)。
// 方法名与契约 operationId 一一对应(listAdminUsers)。
// 控制器级 SessionGuard:访问策略=任何已认证工作人员(P1b 决策,见服务文件
// 头——管理列表模式的仅 admin 判定由 Task 6 补齐)。控制器只做查询参数规整,
// 语义在服务层。
// 过渡期边界(Task 6 收口):契约 P4a 起 accountPrefix 可选(缺省进入管理
// 列表模式:query/status 过滤 + 游标分页 + 仅 admin);当前实现仅前缀模式,
// 缺 accountPrefix 仍 400——管理列表模式随 Task 6 账号管理落地。
// limit 边界已对齐统一契约:缺省 20、上下界 1..100(P1b 时为 10/1..20;
// 邀请搜索调用方显式传 limit=10,行为不变)。
import {
Controller,
Get,
HttpStatus,
Query,
UseGuards,
} from '@nestjs/common';
import { ErrorCodes } from '@photo/contracts';
import { ApiException } from '../api-exception';
import { SessionGuard } from '../auth/session.guard';
import { validationFailed } from '../albums/support';
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 {
if (raw === undefined || raw === '') {
// Task 6 起此分支消失(缺省进入管理列表模式);当前实现仅前缀模式。
throw validationFailed('accountPrefix 必填(2-64 字符)');
}
if (raw.length < 2 || raw.length > 64) {
throw validationFailed('accountPrefix 须为 2-64 字符');
}
return raw;
}
/** 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) {}
/** 用户查询/管理列表(统一响应 AdminUserList;当前仅前缀模式,见文件头)。 */
@Get()
listAdminUsers(
@Query('accountPrefix') accountPrefix: string | undefined,
@Query('limit') limit: string | undefined,
): Promise<AdminUserListResponse> {
return this.usersLookupService.lookup(
parseAccountPrefix(accountPrefix),
parseListLimit(limit),
);
}
}