243b2f8721
Co-Authored-By: Claude Code <noreply@anthropic.com>
115 lines
4.8 KiB
TypeScript
115 lines
4.8 KiB
TypeScript
// 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),
|
||
},
|
||
});
|
||
}
|