diff --git a/docs/plans/2026-09-28-广告位轮播-实施计划.md b/docs/plans/2026-09-28-广告位轮播-实施计划.md index 9438a16..0f01bf5 100644 --- a/docs/plans/2026-09-28-广告位轮播-实施计划.md +++ b/docs/plans/2026-09-28-广告位轮播-实施计划.md @@ -45,7 +45,7 @@ **Files:** Modify `packages/contracts/src/events.ts`;Test `packages/contracts/src/__tests__/events.test.ts` -- [ ] **Step 1: 写失败测试** —— 在 events.test.ts 对照 slot 的既有断言补: +- [x] **Step 1: 写失败测试** —— 在 events.test.ts 对照 slot 的既有断言补: ```ts // ad.* 映射与墓碑(广告位轮播 2026-09-28) it('ad.updated/ad.deleted → entityType ad;ad.deleted 为墓碑', () => { @@ -57,9 +57,9 @@ it('ad.updated/ad.deleted → entityType ad;ad.deleted 为墓碑', () => { ``` (该文件已有的 Equal 双向锁测试会在 union 扩后自动失败——即本步的失败信号。) -- [ ] **Step 2: 跑测试确认失败** —— `pnpm --filter @photo/contracts run test`,预期 FAIL(类型不存在/数组不匹配)。 +- [x] **Step 2: 跑测试确认失败** —— `pnpm --filter @photo/contracts run test`,预期 FAIL(类型不存在/数组不匹配)。 -- [ ] **Step 3: 实现** —— events.ts 四处: +- [x] **Step 3: 实现** —— events.ts 四处: ```ts // ① OutboxEvent type 联合('slot.updated' 后追加) | 'ad.updated' @@ -102,14 +102,14 @@ export function isTombstoneOutboxType(type: OutboxEvent['type']): boolean { } ``` -- [ ] **Step 4: 跑测试确认通过** —— 同 Step 2 命令,预期 PASS(含既有全量)。 -- [ ] **Step 5: Commit** —— `feat(contracts): ad.updated/ad.deleted 事件类型与 ad 实体映射` +- [x] **Step 4: 跑测试确认通过** —— 同 Step 2 命令,预期 PASS(含既有全量)。 +- [x] **Step 5: Commit** —— `feat(contracts): ad.updated/ad.deleted 事件类型与 ad 实体映射` ### Task 2: 契约 openapi + 常量 + 类型再生 **Files:** Modify `packages/contracts/openapi.yaml`、`src/constants.ts`;gen 产物 `src/generated/openapi.d.ts` -- [ ] **Step 1: constants.ts 加广告图上限**(注意:照片域上限真实位置是 +- [x] **Step 1: constants.ts 加广告图上限**(注意:照片域上限真实位置是 `services/api/src/uploads/support.ts` 的 `DEFAULT_UPLOAD_MAX_BYTES`(env 可覆盖), 不在 contracts——AD_UPLOAD_MAX_BYTES 落 contracts 是因为 openapi 描述引用它且 无需 env 覆盖;注释里写清这一对照关系,勿照搬 support.ts 的惰性读 env 模式): @@ -118,7 +118,7 @@ export function isTombstoneOutboxType(type: OutboxEvent['type']): boolean { * 比 UPLOAD_MAX_BYTES(50MB,照片域)收紧(设计 §3)。 */ export const AD_UPLOAD_MAX_BYTES = 2 * 1024 * 1024; ``` -- [ ] **Step 2: openapi.yaml**: +- [x] **Step 2: openapi.yaml**: 1. `SyncEntityType` enum(~L4791)加 `'ad'`; 2. tags 加 `ads`; 3. 新路径(对照 /admin/embed-slots 与 /public/albums/{token}/photos 的既有写法/错误引用): @@ -195,14 +195,14 @@ export const AD_UPLOAD_MAX_BYTES = 2 * 1024 * 1024; properties: items: { type: array, items: { $ref: '#/components/schemas/PublicAd' } } ``` -- [ ] **Step 3: 校验与再生** —— `pnpm --filter @photo/contracts run lint:api`(redocly,预期 0 error)→ `pnpm --filter @photo/contracts run gen` → `pnpm --filter @photo/contracts run typecheck && pnpm --filter @photo/contracts run test` 全绿。 -- [ ] **Step 4: Commit** —— `feat(contracts): 广告位 openapi 端点/schema 与 AD_UPLOAD_MAX_BYTES,类型再生` +- [x] **Step 3: 校验与再生** —— `pnpm --filter @photo/contracts run lint:api`(redocly,预期 0 error)→ `pnpm --filter @photo/contracts run gen` → `pnpm --filter @photo/contracts run typecheck && pnpm --filter @photo/contracts run test` 全绿。 +- [x] **Step 4: Commit** —— `feat(contracts): 广告位 openapi 端点/schema 与 AD_UPLOAD_MAX_BYTES,类型再生` ### Task 3: 双库模型与迁移 **Files:** Modify `packages/db-cn/prisma/schema.prisma`、`packages/db-ov/prisma/schema.prisma` + 各自新迁移 -- [ ] **Step 1: db-cn schema 追加**(对照 embed_slots 模型位置与注释风格): +- [x] **Step 1: db-cn schema 追加**(对照 embed_slots 模型位置与注释风格): ```prisma // —— 广告位域(2026-09-28 设计 §2.1):全局/定向投放,写权威 —— // albumIds null=全局(归一化:空数组存 null);绑定相册删除不级联清理 @@ -221,7 +221,7 @@ model ads { @@index([sortOrder, createdAt]) } ``` -- [ ] **Step 2: db-ov schema 追加**(embed_slots_ov 之后): +- [x] **Step 2: db-ov schema 追加**(embed_slots_ov 之后): ```prisma // —— 广告位副本(cn ads 的展示投影,2026-09-28):无墓碑——ad.deleted 墓碑与 // enabled=false 快照都表达为「删行」(公开不可见);createdAt 为 cn 原值 @@ -238,20 +238,20 @@ model ads_ov { @@index([sortOrder, createdAt]) } ``` -- [ ] **Step 3: 生成迁移**(本地 infra compose 已起): +- [x] **Step 3: 生成迁移**(本地 infra compose 已起): ```bash pnpm db:migrate -- --name ads pnpm db-ov:migrate -- --name ads_ov ``` 预期:各生成 `migrations/2026xxxxx_ads*/migration.sql`(仅 CREATE TABLE/INDEX,无账本表变更)。 -- [ ] **Step 4: 验证** —— `pnpm --filter @photo/db-cn run generate && pnpm --filter @photo/db-ov run generate`(或既有 postinstall 链),`pnpm typecheck` 全绿。 -- [ ] **Step 5: Commit** —— `feat(db): ads 写权威表与 ads_ov 投影表(含迁移)` +- [x] **Step 4: 验证** —— `pnpm --filter @photo/db-cn run generate && pnpm --filter @photo/db-ov run generate`(或既有 postinstall 链),`pnpm typecheck` 全绿。 +- [x] **Step 5: Commit** —— `feat(db): ads 写权威表与 ads_ov 投影表(含迁移)` ### Task 4: 对象键布局(storage/keys.ts) **Files:** Modify `packages/storage/src/keys.ts`;同步设计文档一行(见 Step 3) -- [ ] **Step 1: 实现**(对照 variantsObjectKey 风格;**与设计 §2.1 的差异**:终键带版本段——copyIfAbsent 禁止覆盖,换图必须新键): +- [x] **Step 1: 实现**(对照 variantsObjectKey 风格;**与设计 §2.1 的差异**:终键带版本段——copyIfAbsent 禁止覆盖,换图必须新键): ```ts /** 广告图 staging 键(两步上传第一步:presignPut 直传位)。 */ export function adsStagingObjectKey(cuid: string, ext: string): string { @@ -264,9 +264,9 @@ export function adObjectKey(adId: string, version: number, ext: string): string return `${OBJECT_KEY_ROOT}/ads/${adId}/v${version}.${ext}`; } ``` -- [ ] **Step 2: 验证** —— `pnpm --filter @photo/storage run typecheck`(+ 若该包有测试则跑)。 -- [ ] **Step 3: 回填设计文档** —— 设计方案.md §2.1 的终键示例改为 `ads/{adId}/v{version}.{ext}` 并注明原因(禁止覆盖语义)。 -- [ ] **Step 4: Commit** —— `feat(storage): 广告对象键布局(staging + 版本化终键)` +- [x] **Step 2: 验证** —— `pnpm --filter @photo/storage run typecheck`(+ 若该包有测试则跑)。 +- [x] **Step 3: 回填设计文档** —— 设计方案.md §2.1 的终键示例改为 `ads/{adId}/v{version}.{ext}` 并注明原因(禁止覆盖语义)。 +- [x] **Step 4: Commit** —— `feat(storage): 广告对象键布局(staging + 版本化终键)` ### Task 5: ov 消费链(inbox 分支 + sender 推导 + 两处 SYNC_ENTITY_TYPES) diff --git a/docs/plans/2026-09-28-广告位轮播-设计方案.md b/docs/plans/2026-09-28-广告位轮播-设计方案.md index a926a64..319aa8e 100644 --- a/docs/plans/2026-09-28-广告位轮播-设计方案.md +++ b/docs/plans/2026-09-28-广告位轮播-设计方案.md @@ -122,7 +122,8 @@ imageStagingKey 必须为本服务签发的 staging 键。 `GET /v1/public/albums/{token}/ads` → 200 `{ items: [{ adId, imageUrl, linkUrl }] }` - token 寻址(公开域惯例,不暴露内部 albumId);相册不存在/offline → 404 - ALBUM_NOT_FOUND(与公开相册读同码)。 + ALBUM_NOT_PUBLIC(与公开相册读同码——实施时核实既有公开端点实码为此, + 设计原稿写 ALBUM_NOT_PUBLIC 笔误已纠正 2026-09-28)。 - 过滤:`enabled 投影 ∧ (albumIds 为 null ∨ 含本相册 id)`,按 sortOrder 升序、 并列 createdAt 升序;imageUrl=presigned GET(TTL=SIGNED_URL_TTL_SECONDS)。 - 无广告返回空 items(非 404)。不加独立限速档(观众页单次加载调用一次,