// services/api/src/admin/admin-users.dto.ts —— 账号治理端点入参 DTO(P4a // Task 6)。契约 AdminUserUpdateRequest / AdminPasswordResetRequest。 // 校验语义要点: // · 可选字段一律 @ValidateIf((_, value) => value !== undefined) 而非 // @IsOptional:后者连 null 一并跳过,显式 null 会以 null 值渗入服务层 // (Prisma 拒绝、错误形态不契约化);ValidateIf 只放行 undefined(真缺省), // null 落到类型校验上 → 400 VALIDATION_FAILED,与「提供了非法值」同出口。 // · sourceStatus 只读(技术方案 3.1A 红线:上游状态仅由用户同步任务维护, // 本地不越权改写):DTO 显式声明该字段 + 只读约束,出现(含 null)即 // 400。全局 ValidationPipe 是 whitelist(剥离而非拒绝)——若不声明, // sourceStatus 会被静默剥离成「忽略」,与只读拒绝语义不符。 import { IsBoolean, IsIn, IsString, MaxLength, MinLength, Validate, ValidateIf, ValidationArguments, ValidatorConstraint, ValidatorConstraintInterface, } from 'class-validator'; /** 只读字段约束:仅 undefined(真缺省)放行,任何显式值(含 null)拒绝。 */ @ValidatorConstraint({ name: 'adminUserReadOnlyField', async: false }) class ReadOnlyFieldConstraint implements ValidatorConstraintInterface { validate(value: unknown): boolean { return value === undefined; } defaultMessage(args: ValidationArguments): string { return `${args.property} 为只读字段,不能经本端点修改`; } } export class AdminUserUpdateDto { @ValidateIf((_, value) => value !== undefined) @IsIn(['staff', 'admin']) role?: 'staff' | 'admin'; @ValidateIf((_, value) => value !== undefined) @IsBoolean() localLocked?: boolean; @ValidateIf((_, value) => value !== undefined) @IsString() @MinLength(1) @MaxLength(128) displayName?: string; /** sourceStatus 不在可更新集(契约 AdminUserUpdateRequest 无此字段)。 */ @ValidateIf(() => true) @Validate(ReadOnlyFieldConstraint) declare sourceStatus?: unknown; } export class AdminPasswordResetDto { /** 契约 bounds:8-128;缺省由服务端生成随机初始密码。 */ @ValidateIf((_, value) => value !== undefined) @IsString() @MinLength(8) @MaxLength(128) newPassword?: string; }