feat(worker): watermark versioning with atomic history regeneration

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
浅念
2026-09-20 17:53:44 +08:00
parent 6723a0ce88
commit 243b2f8721
34 changed files with 3057 additions and 78 deletions
+77
View File
@@ -317,6 +317,83 @@ export class AlbumTransferRequestDto {
keepAsCollaborator!: boolean;
}
// ── 水印(P4a Task 3)──
/** 契约 WatermarkPosition 枚举(与 contracts WATERMARK_POSITIONS 同值)。 */
export const WATERMARK_POSITION_VALUES = [
'topLeft',
'topRight',
'bottomLeft',
'bottomRight',
'center',
] as const;
export type WatermarkPositionValue = (typeof WATERMARK_POSITION_VALUES)[number];
/** 契约 WatermarkTextConfig:content 1..64;fontSize 8..72。 */
class WatermarkTextConfigDto {
@IsOptional()
@IsString()
@IsNotEmpty()
@MaxLength(64)
content?: string;
@IsOptional()
@IsInt()
@Min(8)
@Max(72)
fontSize?: number;
}
/**
* enabled=true 时 text.content 必填(技术方案 7.2:P4a 文字水印——无内容的
* 开启配置在 API 层即 400,worker 不再解释「开启但无字」的形态)。约束挂在
* enabled 上(text 缺席时其字段级装饰器不运行,挂 text 无法拒绝缺省)。
*/
@ValidatorConstraint({ name: 'watermarkTextRequired', async: false })
class WatermarkTextRequiredConstraint implements ValidatorConstraintInterface {
validate(value: unknown, args: ValidationArguments): boolean {
if (value !== true) {
return true; // 关闭配置无文字要求。
}
const { text } = args.object as { text?: { content?: unknown } };
return (
typeof text?.content === 'string' && (text.content as string).length > 0
);
}
defaultMessage(): string {
return 'enabled=true 时必须提供 text.content(文字水印内容)';
}
}
/** 契约 WatermarkConfig(创建请求;数值边界对齐契约 min/max)。 */
export class WatermarkConfigDto {
@IsBoolean()
@Validate(WatermarkTextRequiredConstraint)
enabled!: boolean;
@IsOptional()
@IsIn(WATERMARK_POSITION_VALUES)
position?: WatermarkPositionValue;
@IsOptional()
@IsInt()
@Min(0)
@Max(100)
opacity?: number;
@IsOptional()
@IsInt()
@Min(5)
@Max(50)
scalePercent?: number;
@IsOptional()
@ValidateNested()
@Type(() => WatermarkTextConfigDto)
text?: WatermarkTextConfigDto;
}
// ── 资产(P1b Task 3)──
/** 契约 AssetKind:banner(观众页宣传图,PublicAlbum.banner 消费方);cover/logo 建链就绪,展示消费随后续阶段。 */