8a8ad8a18d
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>
81 lines
3.1 KiB
TypeScript
81 lines
3.1 KiB
TypeScript
// 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),
|
||
);
|
||
}
|
||
}
|