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
+24 -2
View File
@@ -222,6 +222,15 @@ export class PublicService {
bucket.set(row.variant, row);
}
// P4a Task 3 水印浏览图选择(cn):albums.watermarkEnabled(有效版本开关)
// 且照片有 watermark 变体行 → 现签水印对象键;否则 browse。行是对象 put
// 成功后才落库的(worker 先写对象再 upsert 行),故行存在 ⇒ 对象存在;
// 失败照片行保留旧版本键(重生成失败不覆盖行——A20「失败照片保留旧图」),
// 新发布照片行在最新版本键(WatermarkJobCreated:新照片即刻套用)——两者
// 都直接用行的 objectKey,无需版本比较。disable 版任务逐张删行:删到哪张
// 哪张回落 browse(渐进撤下,语义见 contracts watermark.ts)。
const serveWatermark = album.watermarkEnabled;
const items: PublicPhotoListResponse['items'] = [];
for (const photo of page) {
const bucket = variantsByPhoto.get(photo.id);
@@ -230,8 +239,14 @@ export class PublicService {
if (thumbRow === undefined || browseRow === undefined) {
continue;
}
const watermarkRow = serveWatermark
? bucket?.get('watermark')
: undefined;
const browseKey =
watermarkRow !== undefined ? watermarkRow.objectKey : browseRow.objectKey;
// 宽高取 browse 变体尺寸(契约「浏览图宽高,用于列表占位」);变体行缺
// 尺寸时回退 photos 主行(原始尺寸),两侧均缺 → 同样跳过(必返字段)。
// 水印图与 browse 同源同尺寸(compose 基于 browse 管线),占位一致。
const width = browseRow.width ?? photo.width;
const height = browseRow.height ?? photo.height;
if (width === null || height === null) {
@@ -242,7 +257,7 @@ export class PublicService {
width,
height,
thumb: await this.presign(thumbRow.objectKey),
browse: await this.presign(browseRow.objectKey),
browse: await this.presign(browseKey),
likeCount: photo.likeCount,
// published 行的 publishedAt 必非空(worker 发布事务写入);?? 兜底仅
// 收窄类型,不构成可观察行为。
@@ -290,6 +305,11 @@ export class PublicService {
});
const thumbRow = variants.find((v) => v.variant === 'thumb');
const browseRow = variants.find((v) => v.variant === 'browse');
// 水印浏览图选择同列表(有效开关 + 行存在 ⇒ 用行的对象键;A20 失败照片
// 保留旧图——行未被失败任务覆盖,仍指向旧版本键)。
const watermarkRow = album.watermarkEnabled
? variants.find((v) => v.variant === 'watermark')
: undefined;
const width = browseRow?.width ?? photo.width;
const height = browseRow?.height ?? photo.height;
if (
@@ -301,12 +321,14 @@ export class PublicService {
// post-publish 不变量破坏:公开面按「不可展示」处理(同列表跳过语义)。
throw photoNotPublic();
}
const browseKey =
watermarkRow !== undefined ? watermarkRow.objectKey : browseRow.objectKey;
return {
photoId: photo.id,
width,
height,
thumb: await this.presign(thumbRow.objectKey),
browse: await this.presign(browseRow.objectKey),
browse: await this.presign(browseKey),
likeCount: photo.likeCount,
publishedAt: (photo.publishedAt ?? new Date(0)).toISOString(),
categoryId: photo.categoryId,