feat(sync): 对账清单 ad 实体当前态清扫
Task 8(ads-carousel):cn reconcile 增 entityType='ad' 分支——纯 SELECT 当前态映射(sortOrder/createdAt 确定性排序,albumId 恒空串信封约定), 载荷复用 ads/ad-payload.ts 单一出处(与业务事务内即时事件逐字节同形, 纯函数模块无依赖环);DTO SYNC_ENTITY_TYPES 追加 'ad'(对齐契约 enum)。 随附质量审查两项: - ads.service list() 行→AdminAdItem 映射复用 toItem(删 ~14 行内联重复, ⑮ 补 admin list 200 形状断言覆盖重构路径); - 新增 test/ads-extensions.spec.ts 守护 AD_IMAGE_EXTENSIONS === UPLOAD_CONTENT_TYPES 派生归一扩展名集合(jpeg→jpg),防三处白名单漂移。 测试基建:reset.ts RESET_TABLES 补 'ads'(Task 6 建表遗漏,固定 id 直种 依赖跨 spec 清场);reconcile.e2e-spec 追加 ⑤(红→绿,5/5)。 Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -272,7 +272,7 @@ export function adObjectKey(adId: string, version: number, ext: string): string
|
||||
|
||||
**Files:** Modify `services/api/src/sync-inbox/dto.ts`、`services/api/src/sync-inbox/sync-inbox.service.ts`、`services/sync/src/sender.ts`、`services/api/src/sync/payloads.ts`;Test `services/api/test/embed.e2e-spec.ts` 邻新建 `ads.e2e-spec.ts`(本任务先建同步断言骨架,Task 7 补 CRUD 全路径)
|
||||
|
||||
- [ ] **Step 1: 写失败测试** —— 新建 `services/api/test/ads.e2e-spec.ts`,骨架对照 embed.e2e-spec.ts(AppCnModule + ov PrismaClient + drainSender helper 复制其实现):
|
||||
- [x] **Step 1: 写失败测试** —— 新建 `services/api/test/ads.e2e-spec.ts`,骨架对照 embed.e2e-spec.ts(AppCnModule + ov PrismaClient + drainSender helper 复制其实现):
|
||||
```ts
|
||||
// 广告位 e2e(2026-09-28 设计 §8):cn CRUD → outbox → drainSender 推送 →
|
||||
// ads_ov 投影/版本账本断言;public-ov 端到端断言在 public-ov.e2e-spec 补。
|
||||
@@ -285,8 +285,8 @@ export function adObjectKey(adId: string, version: number, ext: string): string
|
||||
3. 再推 `ad.updated` v3(enabled=true)→ 行复活(同 adId upsert);
|
||||
4. `ad.deleted` 墓碑 v4 → 行删除;
|
||||
5. 旧版本重放(v2 迟到于 v3 后)→ 行保持 v3(版本单调不被回滚)。
|
||||
- [ ] **Step 2: 跑测试确认失败** —— `pnpm --filter api run test:e2e -- ads.e2e`,预期 FAIL(entityType 'ad' 被 DTO `@IsIn` 拒 / 无分支不落行)。
|
||||
- [ ] **Step 3: 实现**:
|
||||
- [x] **Step 2: 跑测试确认失败** —— `pnpm --filter api run test:e2e -- ads.e2e`,预期 FAIL(entityType 'ad' 被 DTO `@IsIn` 拒 / 无分支不落行)。
|
||||
- [x] **Step 3: 实现**:
|
||||
```ts
|
||||
// ① sync-inbox/dto.ts:SYNC_ENTITY_TYPES 数组追加 'ad'
|
||||
// ②【关键,缺此步全链路不通】decideAndApply ③依赖检查豁免(~L173-185):
|
||||
@@ -341,21 +341,21 @@ export function adObjectKey(adId: string, version: number, ext: string): string
|
||||
// sync/payloads.ts:对照 SlotStateUpdatedPayload 导出 AdUpdatedPayload 的
|
||||
// ov 侧类型再导出(或直接 import @photo/contracts——跟随该文件现有导入风格)。
|
||||
```
|
||||
- [ ] **Step 4: 跑测试确认通过** —— Step 2 命令,5 用例全 PASS。
|
||||
- [ ] **Step 5: Commit** —— `feat(sync): ov inbox ad 分支与 sender adId 推导(disabled/墓碑=删投影行)`
|
||||
- [x] **Step 4: 跑测试确认通过** —— Step 2 命令,5 用例全 PASS。
|
||||
- [x] **Step 5: Commit** —— `feat(sync): ov inbox ad 分支与 sender adId 推导(disabled/墓碑=删投影行)`
|
||||
|
||||
### Task 6: cn 服务端——上传初始化(ads module 上半)
|
||||
|
||||
**Files:** Create `services/api/src/ads/{ads.module.ts,ads.controller.ts,ads.service.ts,dto.ts}`;Modify `services/api/src/app-cn.module.ts`;Test `services/api/test/ads.e2e-spec.ts`(追加段)
|
||||
|
||||
- [ ] **Step 1: 写失败测试**(ads.e2e-spec.ts 追加):
|
||||
- [x] **Step 1: 写失败测试**(ads.e2e-spec.ts 追加):
|
||||
```ts
|
||||
// uploads init:admin 201 返回 staging 键+credential(URL 可真实 PUT,对照
|
||||
// uploads.e2e 的 presign 断言写法);非 admin 403;匿名 401;bytes>2MB 400;
|
||||
// contentType 非白名单 400;坏扩展名 400。
|
||||
```
|
||||
- [ ] **Step 2: 跑测试确认失败** —— `pnpm --filter api run test:e2e -- ads.e2e`(404 路由不存在)。
|
||||
- [ ] **Step 3: 实现**:
|
||||
- [x] **Step 2: 跑测试确认失败** —— `pnpm --filter api run test:e2e -- ads.e2e`(404 路由不存在)。
|
||||
- [x] **Step 3: 实现**:
|
||||
```ts
|
||||
// dto.ts:AdUploadInitRequestDto(filename/bytes/contentType @IsIn 白名单)、
|
||||
// AdCreateRequestDto、AdUpdateRequestDto——字段与 openapi Task 2 一一对应;
|
||||
@@ -400,14 +400,14 @@ async initUpload(user: AuthUser, dto: AdUploadInitRequestDto) {
|
||||
return { objectKey, credential };
|
||||
}
|
||||
```
|
||||
- [ ] **Step 4: 跑测试确认通过**。
|
||||
- [ ] **Step 5: Commit** —— `feat(api): /admin/ads/uploads 广告图 staging 预签名(2MB/白名单/admin-only)`
|
||||
- [x] **Step 4: 跑测试确认通过**。
|
||||
- [x] **Step 5: Commit** —— `feat(api): /admin/ads/uploads 广告图 staging 预签名(2MB/白名单/admin-only)`
|
||||
|
||||
### Task 7: cn 服务端——CRUD + outbox + audit
|
||||
|
||||
**Files:** Modify `services/api/src/ads/ads.service.ts`;Test `services/api/test/ads.e2e-spec.ts`
|
||||
|
||||
- [ ] **Step 1: 写失败测试**(追加;真实 MinIO PUT 仿 uploads.e2e):
|
||||
- [x] **Step 1: 写失败测试**(追加;真实 MinIO PUT 仿 uploads.e2e):
|
||||
```ts
|
||||
// ① 创建:上传真图(getBytes 生成最小 JPEG 或复用 uploads.e2e 的测试字节)→
|
||||
// POST /admin/ads → 201:行就位(albumIds 空归一 null)、outbox 行
|
||||
@@ -425,8 +425,8 @@ async initUpload(user: AuthUser, dto: AdUploadInitRequestDto) {
|
||||
// ⑦ 同步贯通(衔接 Task 5 骨架改真路径):创建→drainSender→ads_ov 行就位;
|
||||
// 停用→drainSender→行删;删除→drainSender→行删。
|
||||
```
|
||||
- [ ] **Step 2: 跑测试确认失败**。
|
||||
- [ ] **Step 3: 实现** ads.service.ts:
|
||||
- [x] **Step 2: 跑测试确认失败**。
|
||||
- [x] **Step 3: 实现** ads.service.ts:
|
||||
```ts
|
||||
// 核心片段(完整对照 embed.service.ts 的事务/audit/payload 构造风格):
|
||||
private buildPayload(ad: AdRow): AdUpdatedPayload {
|
||||
@@ -458,16 +458,16 @@ private buildPayload(ad: AdRow): AdUpdatedPayload {
|
||||
// list:requireAdmin → findMany orderBy [{sortOrder:'asc'},{createdAt:'asc'}]
|
||||
// → Promise.all map imageUrl = storage.presignGet(imageObjectKey, signedUrlTtl())
|
||||
```
|
||||
- [ ] **Step 4: 跑测试确认通过**(ads.e2e 全量)。
|
||||
- [ ] **Step 5: Commit** —— `feat(api): 广告位 CRUD——outbox ad.* 事件/audit/停用高优先级/版本化换图`
|
||||
- [x] **Step 4: 跑测试确认通过**(ads.e2e 全量)。
|
||||
- [x] **Step 5: Commit** —— `feat(api): 广告位 CRUD——outbox ad.* 事件/audit/停用高优先级/版本化换图`
|
||||
|
||||
### Task 8: cn 对账 'ad' 清扫
|
||||
|
||||
**Files:** Modify `services/api/src/sync/dto.ts`(SYNC_ENTITY_TYPES)、`services/api/src/sync/sync.service.ts`;Test `services/sync/test/reconcile.e2e-spec.ts`(追加段)
|
||||
|
||||
- [ ] **Step 1: 写失败测试** —— reconcile.e2e-spec 追加:seed 两行 ads(一全局一定向)→ 以 `?entityType=ad&since=0` 拉对账清单 → 断言 2 条 wire 事件(entityType 'ad'、albumId ''、载荷全量、确定性 eventId 前缀)。
|
||||
- [ ] **Step 2: 跑测试确认失败** —— `pnpm --filter sync run test`(或该包 e2e 脚本名,对照 services/sync/package.json)。
|
||||
- [ ] **Step 3: 实现**:
|
||||
- [x] **Step 1: 写失败测试** —— reconcile.e2e-spec 追加:seed 两行 ads(一全局一定向)→ 以 `?entityType=ad&since=0` 拉对账清单 → 断言 2 条 wire 事件(entityType 'ad'、albumId ''、载荷全量、确定性 eventId 前缀)。
|
||||
- [x] **Step 2: 跑测试确认失败** —— `pnpm --filter sync run test`(或该包 e2e 脚本名,对照 services/sync/package.json)。
|
||||
- [x] **Step 3: 实现**:
|
||||
```ts
|
||||
// sync/dto.ts:SYNC_ENTITY_TYPES 追加 'ad'
|
||||
// sync.service.ts reconcile(asset 分支之后):
|
||||
@@ -483,8 +483,8 @@ private buildPayload(ad: AdRow): AdUpdatedPayload {
|
||||
}
|
||||
```
|
||||
注意:payload builder 若与 ads.service 重复,提取到 `services/api/src/ads/ad-payload.ts` 共享(DRY,两处单一出处)。
|
||||
- [ ] **Step 4: 跑测试确认通过** + `pnpm --filter api run test:e2e -- embed`(既有对账回归不破)。
|
||||
- [ ] **Step 5: Commit** —— `feat(sync): 对账清单 ad 实体当前态清扫`
|
||||
- [x] **Step 4: 跑测试确认通过** + `pnpm --filter api run test:e2e -- embed`(既有对账回归不破)。
|
||||
- [x] **Step 5: Commit** —— `feat(sync): 对账清单 ad 实体当前态清扫`
|
||||
|
||||
### Task 9: ov 公开端点
|
||||
|
||||
|
||||
Reference in New Issue
Block a user