feat(worker): watermark versioning with atomic history regeneration
Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,114 @@
|
||||
// packages/contracts/src/watermark.ts —— 水印配置的跨服务共享形状与缺省值
|
||||
// (P4a Task 3)。三个消费方经同一模块引用,杜绝形状漂移:
|
||||
// · api albums 域(POST watermark-jobs:DTO 校验 → normalize 落库);
|
||||
// · worker 重生成消费者(读 watermark_configs.config Json → 本形状使用);
|
||||
// · worker 发布路径(新照片按相册当前配置即产水印变体)。
|
||||
//
|
||||
// 版本语义(本文件是权威表述,实现侧注释引用):
|
||||
// · albums.watermarkVersion = 最新「已创建」配置版本——POST 即推进,新照片
|
||||
// 发布立刻套用(契约 WatermarkJobCreated.configVersion 描述:新照片即刻套用);
|
||||
// · albums.watermarkEnabled + watermarkEffectiveVersion = 「有效」版本——仅当该
|
||||
// 版本的历史重生成任务 done(或创建时相册无已发布照片、任务即建即 done)才
|
||||
// 推进。ov 副本与公开侧的模式键/变体选择以有效版本为准:对象键
|
||||
// `variants/{photoId}/{version}/watermark.jpg` 只有 done 任务保证存在,
|
||||
// 用有效版本杜绝「同步了新版本号但对象尚未生成」的破图窗口(A20:
|
||||
// 任务完成前旧浏览图保持可用,失败照片保留旧图)。
|
||||
// · enabled=false 也是一版配置(记录关闭动作):关闭后发布的新照片无水印;
|
||||
// 关闭版重生成任务 = 逐张删除 watermark 变体行(对象保留,历史可溯)。
|
||||
export const WATERMARK_POSITIONS = [
|
||||
'topLeft',
|
||||
'topRight',
|
||||
'bottomLeft',
|
||||
'bottomRight',
|
||||
'center',
|
||||
] as const;
|
||||
export type WatermarkPosition = (typeof WATERMARK_POSITIONS)[number];
|
||||
|
||||
// ── 服务端缺省值(契约 WatermarkConfig「可选字段缺省用服务端默认值」的唯一权威)──
|
||||
export const WATERMARK_DEFAULT_POSITION: WatermarkPosition = 'bottomRight';
|
||||
export const WATERMARK_DEFAULT_OPACITY = 50;
|
||||
export const WATERMARK_DEFAULT_SCALE_PERCENT = 20;
|
||||
export const WATERMARK_DEFAULT_FONT_SIZE = 24;
|
||||
|
||||
/**
|
||||
* 归一化后的存储形状(watermark_configs.config)。API 保存时经
|
||||
* normalizeWatermarkConfig 填齐缺省值落库——worker 与 GET 读到的永远是完整
|
||||
* 配置,不再各自解释缺省。enabled=false 时 text.content 允许为空串
|
||||
* (关闭语义不绘制文字,其余字段仅作回显)。
|
||||
*/
|
||||
export interface StoredWatermarkConfig {
|
||||
enabled: boolean;
|
||||
position: WatermarkPosition;
|
||||
/** 不透明度百分比 0..100。 */
|
||||
opacity: number;
|
||||
/** 水印层宽度相对浏览图宽度的百分比 5..50。 */
|
||||
scalePercent: number;
|
||||
text: {
|
||||
content: string;
|
||||
/** 名义字号 8..72(渲染后再按 scalePercent 缩放,见 worker 实现)。 */
|
||||
fontSize: number;
|
||||
};
|
||||
}
|
||||
|
||||
/** 契约 WatermarkConfig(创建请求 / 当前配置响应)。 */
|
||||
export type WatermarkConfigInput = {
|
||||
enabled: boolean;
|
||||
position?: WatermarkPosition;
|
||||
opacity?: number;
|
||||
scalePercent?: number;
|
||||
text?: { content?: string; fontSize?: number };
|
||||
};
|
||||
|
||||
/** 缺省值填齐(不改写已给值;越界值由 API DTO 层 400 拒绝,此处不重复校验)。 */
|
||||
export function normalizeWatermarkConfig(
|
||||
raw: WatermarkConfigInput,
|
||||
): StoredWatermarkConfig {
|
||||
return {
|
||||
enabled: raw.enabled,
|
||||
position: raw.position ?? WATERMARK_DEFAULT_POSITION,
|
||||
opacity: raw.opacity ?? WATERMARK_DEFAULT_OPACITY,
|
||||
scalePercent: raw.scalePercent ?? WATERMARK_DEFAULT_SCALE_PERCENT,
|
||||
text: {
|
||||
content: raw.text?.content ?? '',
|
||||
fontSize: raw.text?.fontSize ?? WATERMARK_DEFAULT_FONT_SIZE,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取侧解析(watermark_configs.config Json → StoredWatermarkConfig):行由
|
||||
* normalizeWatermarkConfig 写入、形状可信,此解析仅对脏数据防御——缺字段回
|
||||
* 落缺省值,结构破坏(非对象 / enabled 非 boolean)返回 null(调用方按
|
||||
* 「配置不可用」处理:重生成任务整败 / 公开侧回落无水印)。
|
||||
*/
|
||||
export function parseStoredWatermarkConfig(
|
||||
value: unknown,
|
||||
): StoredWatermarkConfig | null {
|
||||
if (typeof value !== 'object' || value === null) {
|
||||
return null;
|
||||
}
|
||||
const raw = value as Partial<WatermarkConfigInput> & {
|
||||
text?: Partial<WatermarkConfigInput['text']>;
|
||||
};
|
||||
if (typeof raw.enabled !== 'boolean') {
|
||||
return null;
|
||||
}
|
||||
const num = (v: unknown): number | undefined =>
|
||||
typeof v === 'number' && Number.isFinite(v) ? v : undefined;
|
||||
const str = (v: unknown): string | undefined =>
|
||||
typeof v === 'string' ? v : undefined;
|
||||
return normalizeWatermarkConfig({
|
||||
enabled: raw.enabled,
|
||||
position:
|
||||
raw.position !== undefined &&
|
||||
(WATERMARK_POSITIONS as readonly string[]).includes(raw.position)
|
||||
? raw.position
|
||||
: undefined,
|
||||
opacity: num(raw.opacity),
|
||||
scalePercent: num(raw.scalePercent),
|
||||
text: {
|
||||
content: str(raw.text?.content) ?? '',
|
||||
fontSize: num(raw.text?.fontSize),
|
||||
},
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user