docs: 广告位设计 404 码 ALBUM_NOT_PUBLIC 纠正入档 + 实施计划 Task 1-4 勾选

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
浅念
2026-09-28 23:03:32 +08:00
parent 539597e9f0
commit d86f452bed
2 changed files with 20 additions and 19 deletions
@@ -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)
@@ -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)。不加独立限速档(观众页单次加载调用一次,