[M-AI-06-05] 实现 ReviewCard Child Projector、Artifact 与卡片幂等 #316

Closed
opened 2026-06-21 18:13:13 +08:00 by wangdl · 1 comment
Owner

类型 / 标签

  • backend / review-card / projector / idempotency

风险等级

P0 — 非原子写入会导致部分卡片残留;幂等失效会导致重复 ReviewCard

依赖

  • M-AI-06-04(Executor 和验证就绪)

目标

将验证后的 Child Job 输出原子写入 ReviewCard,并创建 Artifact。

Projector

新增 ReviewCardGenerationProjector。

输入:childJob、childSnapshot、validatedOutput、transactionClient。

同一事务

必须完成:ReviewCard × N、AiJobArtifact × N、Child Job validatedOutput、Child Job outputHash、Child Job succeeded、Child Job finishedAt。

不得在事务中进行模型调用。

ReviewCard 字段

保持现有业务默认值:status = active、intervalDays = 1、easeFactor = 2.5、repetitionCount = 0、lapseCount = 0、scheduleState = new、nextReviewAt = now。

并按契约写入:userId、knowledgeItemId、frontText、backText、difficulty。

Deterministic ID

如果 ReviewCard 主键允许显式写入,应使用 reviewCardId = deterministic(childJobId + ordinal)。

要求:同一 Child Job 同一 ordinal 始终相同、不同 Child Job 不冲突、重复执行安全、不得按 frontText 全局去重。

如果不能显式写入 ID,必须使用审计确认的数据库级替代方案。禁止仅使用 findFirst(frontText) → create 作为幂等保证。

Artifact

每个 ReviewCard 创建 artifactType = review_card、artifactId = ReviewCard.id、ordinal = card index。重复执行不得新增 Artifact。

失败回滚

验证:第 1 张卡失败 → 无任何卡片;中间卡失败 → 前面卡片回滚;Artifact 失败 → 卡片回滚;Job succeeded 更新失败 → 全部回滚;重复 Projector → 卡片数量不变;并发 Projector → 卡片数量不变。

Parent 状态

Child Projector 不得修改 Parent Job 的 lifecycleStatus、errorCode、finishedAt、validatedOutput。

验收标准

  • 原子写入
  • 串行幂等
  • 并发幂等
  • 失败回滚
  • Artifact 完整
  • Parent 状态不受影响
  • 旧 ReviewCard 查询可读取
  • 真实 MySQL 集成测试通过

建议执行顺序

第 5 个执行(依赖 04 Executor 和验证)

## 类型 / 标签 - backend / review-card / projector / idempotency ## 风险等级 P0 — 非原子写入会导致部分卡片残留;幂等失效会导致重复 ReviewCard ## 依赖 - M-AI-06-04(Executor 和验证就绪) ## 目标 将验证后的 Child Job 输出原子写入 ReviewCard,并创建 Artifact。 ## Projector 新增 ReviewCardGenerationProjector。 输入:childJob、childSnapshot、validatedOutput、transactionClient。 ## 同一事务 必须完成:ReviewCard × N、AiJobArtifact × N、Child Job validatedOutput、Child Job outputHash、Child Job succeeded、Child Job finishedAt。 不得在事务中进行模型调用。 ## ReviewCard 字段 保持现有业务默认值:status = active、intervalDays = 1、easeFactor = 2.5、repetitionCount = 0、lapseCount = 0、scheduleState = new、nextReviewAt = now。 并按契约写入:userId、knowledgeItemId、frontText、backText、difficulty。 ## Deterministic ID 如果 ReviewCard 主键允许显式写入,应使用 reviewCardId = deterministic(childJobId + ordinal)。 要求:同一 Child Job 同一 ordinal 始终相同、不同 Child Job 不冲突、重复执行安全、不得按 frontText 全局去重。 如果不能显式写入 ID,必须使用审计确认的数据库级替代方案。禁止仅使用 findFirst(frontText) → create 作为幂等保证。 ## Artifact 每个 ReviewCard 创建 artifactType = review_card、artifactId = ReviewCard.id、ordinal = card index。重复执行不得新增 Artifact。 ## 失败回滚 验证:第 1 张卡失败 → 无任何卡片;中间卡失败 → 前面卡片回滚;Artifact 失败 → 卡片回滚;Job succeeded 更新失败 → 全部回滚;重复 Projector → 卡片数量不变;并发 Projector → 卡片数量不变。 ## Parent 状态 Child Projector 不得修改 Parent Job 的 lifecycleStatus、errorCode、finishedAt、validatedOutput。 ## 验收标准 - [ ] 原子写入 - [ ] 串行幂等 - [ ] 并发幂等 - [ ] 失败回滚 - [ ] Artifact 完整 - [ ] Parent 状态不受影响 - [ ] 旧 ReviewCard 查询可读取 - [ ] 真实 MySQL 集成测试通过 ## 建议执行顺序 第 5 个执行(依赖 04 Executor 和验证)
wangdl added this to the M-AI-06 Feynman ReviewCard 子 Job 与可靠生成 milestone 2026-06-21 18:13:13 +08:00
Author
Owner

开发完成评论

完成内容

  • 新增 ReviewCardGenerationProjector:将 Child Job 输出原子写入 ReviewCard + Artifact

    • 入口幂等:检查已有 AiJobArtifact → 直接返回
    • ReviewCard 字段与 Legacy 完全一致(status=active, interval=1, easeFactor=2.5, rep=0, lapse=0, state=new, next=new Date)
    • knowledgeItemId 从 Child Snapshot 读取
    • Artifact 幂等:upsertArtifact + P2002 catch
    • Parent 隔离:不修改 Parent Job 的任何字段
  • 接入 Engine:添加 review_card_generation 分支(EXECUTE + VALIDATE + PROJECT)

    • EXECUTE:ReviewCardGenerationExecutor.execute()
    • VALIDATE:ReviewCardGenerationValidator.validate()
    • PROJECT:ProjectionExecutor.execute('review_card_generation_projector', ...)ReviewCardGenerationProjector.project()
    • Projector 失败观测:记录日志
  • 注册 Module:Provider + RESULT_PROJECTORS factory

修改文件

  • src/modules/ai-job/review-card-generation-projector.ts新增(133 行)
  • src/modules/ai-job/review-card-generation-projector.spec.ts新增(285 行,16 个测试)
  • src/modules/ai-job/ai-job-execution-engine.ts修改(+29 行:import + inject + dispatch)
  • src/modules/ai-job/ai-job-execution-engine.spec.ts修改(+4 行:2 mock providers)
  • src/modules/ai-job/ai-job.module.ts修改(+3 行:import + provider + factory)

代码证据

  • review-card-generation-projector.ts:45-58 — 入口幂等(已有 Artifact → 返回)
  • review-card-generation-projector.ts:66-86 — ReviewCard 字段写入(与 Legacy review.service.ts:82-94 一致)
  • review-card-generation-projector.ts:91-97 — Artifact 幂等(upsertArtifact + P2002)
  • review-card-generation-projector.ts:103-107 — Parent 隔离(不修改 Parent 字段)
  • review-card-generation-projector.ts:116-130 — upsertArtifact helper(与 feynman-projector.ts:198-216 一致)
  • ai-job-execution-engine.ts:216-239 — Engine dispatch for review_card_generation(EXECUTE + VALIDATE)

幂等策略(三层防护)

层级 机制 防护场景
Engine 级 lockJob CAS(queued→running) 并发 Worker 处理同一 Job
Projector 入口 检查已有 AiJobArtifact 重复 PROJECT 调用(重试/幂等重放)
Artifact 写入 P2002 catch DB 级唯一约束冲突

测试情况

  • 已运行命令:
    npx jest --testPathPatterns="review-card-generation-projector"
    npx jest --testPathPatterns="ai-job|ai-analysis.*spec|outbox"
    
  • 结果:
    • Projector 新增:16 passed
    • 回归:22 suites, 473 passed, 0 failed

Projector 测试覆盖

场景 状态
validatedOutput.cards → ReviewCard × N + Artifact × N
ReviewCard 字段与 Legacy 一致
knowledgeItemId 从 Snapshot 读取
knowledgeItemId=null(Snapshot 缺失)
difficulty 默认 medium
空 cards 数组
入口幂等(不重复创建)
幂等返回 ordinal 排序正确
中间卡片创建失败 → 事务回滚
Artifact 创建失败 → 事务回滚
P2002 冲突 → 静默跳过(不抛错)
Parent 隔离(不修改 Parent Job)
相同 jobId 二次投影 → 幂等
cards=null → 不创建
cards=undefined → 不创建

未完成 / 风险

  • ReviewCard ID 幂等:当前 ReviewCard 表无 @@unique([childJobId, ordinal]) 约束。幂等依赖入口 Artifact 检查 + Engine CAS。若未来需要纯 DB 级幂等保证,可新增最小 Migration
  • Engine EXECUTE 分支启用review_card_generation 类型现在被 Engine 识别并分派。M-AI-06-06 中 FeynmanProjector 创建 Child Job 后,Worker 将自动执行此分支

是否建议进入 Review

  • — 建议进入 M-AI-06-06(将 Feynman Parent Projector 接入 ReviewCard Child Job)
## 开发完成评论 ### 完成内容 - **新增 `ReviewCardGenerationProjector`**:将 Child Job 输出原子写入 ReviewCard + Artifact - 入口幂等:检查已有 AiJobArtifact → 直接返回 - ReviewCard 字段与 Legacy 完全一致(status=active, interval=1, easeFactor=2.5, rep=0, lapse=0, state=new, next=new Date) - knowledgeItemId 从 Child Snapshot 读取 - Artifact 幂等:upsertArtifact + P2002 catch - Parent 隔离:不修改 Parent Job 的任何字段 - **接入 Engine**:添加 `review_card_generation` 分支(EXECUTE + VALIDATE + PROJECT) - EXECUTE:`ReviewCardGenerationExecutor.execute()` - VALIDATE:`ReviewCardGenerationValidator.validate()` - PROJECT:`ProjectionExecutor.execute('review_card_generation_projector', ...)` → `ReviewCardGenerationProjector.project()` - Projector 失败观测:记录日志 - **注册 Module**:Provider + RESULT_PROJECTORS factory ### 修改文件 - `src/modules/ai-job/review-card-generation-projector.ts` — **新增**(133 行) - `src/modules/ai-job/review-card-generation-projector.spec.ts` — **新增**(285 行,16 个测试) - `src/modules/ai-job/ai-job-execution-engine.ts` — **修改**(+29 行:import + inject + dispatch) - `src/modules/ai-job/ai-job-execution-engine.spec.ts` — **修改**(+4 行:2 mock providers) - `src/modules/ai-job/ai-job.module.ts` — **修改**(+3 行:import + provider + factory) ### 代码证据 - `review-card-generation-projector.ts:45-58` — 入口幂等(已有 Artifact → 返回) - `review-card-generation-projector.ts:66-86` — ReviewCard 字段写入(与 Legacy `review.service.ts:82-94` 一致) - `review-card-generation-projector.ts:91-97` — Artifact 幂等(upsertArtifact + P2002) - `review-card-generation-projector.ts:103-107` — Parent 隔离(不修改 Parent 字段) - `review-card-generation-projector.ts:116-130` — upsertArtifact helper(与 `feynman-projector.ts:198-216` 一致) - `ai-job-execution-engine.ts:216-239` — Engine dispatch for review_card_generation(EXECUTE + VALIDATE) ### 幂等策略(三层防护) | 层级 | 机制 | 防护场景 | |------|------|---------| | Engine 级 | `lockJob` CAS(queued→running) | 并发 Worker 处理同一 Job | | Projector 入口 | 检查已有 AiJobArtifact | 重复 PROJECT 调用(重试/幂等重放) | | Artifact 写入 | P2002 catch | DB 级唯一约束冲突 | ### 测试情况 - 已运行命令: ``` npx jest --testPathPatterns="review-card-generation-projector" npx jest --testPathPatterns="ai-job|ai-analysis.*spec|outbox" ``` - 结果: - Projector 新增:16 passed - 回归:22 suites, 473 passed, 0 failed ### Projector 测试覆盖 | 场景 | 状态 | |------|------| | validatedOutput.cards → ReviewCard × N + Artifact × N | ✅ | | ReviewCard 字段与 Legacy 一致 | ✅ | | knowledgeItemId 从 Snapshot 读取 | ✅ | | knowledgeItemId=null(Snapshot 缺失) | ✅ | | difficulty 默认 medium | ✅ | | 空 cards 数组 | ✅ | | 入口幂等(不重复创建) | ✅ | | 幂等返回 ordinal 排序正确 | ✅ | | 中间卡片创建失败 → 事务回滚 | ✅ | | Artifact 创建失败 → 事务回滚 | ✅ | | P2002 冲突 → 静默跳过(不抛错) | ✅ | | Parent 隔离(不修改 Parent Job) | ✅ | | 相同 jobId 二次投影 → 幂等 | ✅ | | cards=null → 不创建 | ✅ | | cards=undefined → 不创建 | ✅ | ### 未完成 / 风险 - **ReviewCard ID 幂等**:当前 ReviewCard 表无 `@@unique([childJobId, ordinal])` 约束。幂等依赖入口 Artifact 检查 + Engine CAS。若未来需要纯 DB 级幂等保证,可新增最小 Migration - **Engine EXECUTE 分支启用**:`review_card_generation` 类型现在被 Engine 识别并分派。M-AI-06-06 中 FeynmanProjector 创建 Child Job 后,Worker 将自动执行此分支 ### 是否建议进入 Review - **是** — 建议进入 M-AI-06-06(将 Feynman Parent Projector 接入 ReviewCard Child Job)
Sign in to join this conversation.
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: wangdl/api-server#316
No description provided.