[M-AI-02-02] 冻结 AiJob Expand Schema 与迁移契约 #274

Closed
opened 2026-06-19 23:30:03 +08:00 by wangdl · 4 comments
Owner

依赖

  • M-AI-02-01

目标

根据真实审计冻结本批最终 Schema,后续 Issue 不得自行新增字段、状态、表或索引。

输出文档

新增:

docs/architecture/adr-002-ai-job-database-expand.md

必须冻结

1. 物理表策略

  • 现有 AiAnalysisJob 物理表名;
  • Prisma AiJob@@map
  • 所有现有物理列保持原名;
  • 禁止新建另一张通用 Job 主表。

2. 新字段定义

逐字段冻结:

  • Prisma 字段名;
  • 物理列名;
  • 类型;
  • 长度;
  • nullable/default;
  • index;
  • 写入来源;
  • 旧字段映射;
  • 未来删除阶段。

计划字段至少覆盖:

jobType
triggerType
lifecycleStatus
queueName
priority
targetType
targetId
parentJobId
idempotencyKey
inputRef
inputSchemaVersion
snapshotId
credentialMode
credentialId
modelTier
promptKey
promptVersion
outputSchemaVersion
progress
attemptCount
maxAttempts
timeoutMs
validatedOutput
outputHash
errorCode
publicErrorMessage
internalErrorMessage
queuedAt
startedAt
finishedAt
cancelRequestedAt

字段可根据 01 审计调整,但调整必须在 ADR 中解释。

3. 索引

至少评估:

(userId, jobType, createdAt)
(lifecycleStatus, createdAt)
(queueName, lifecycleStatus, createdAt)
(targetType, targetId)
(parentJobId)
(userId, jobType, idempotencyKey) UNIQUE

4. 删除策略

明确:

  • Job → Snapshot;
  • Job → Artifact;
  • Job → UsageLog;
  • Job → Child Job。

5. Migration 策略

  • Expand only;
  • 不 drop;
  • 不 rename;
  • 不重建表;
  • 新字段 nullable 或安全默认;
  • 旧版本应用可继续运行;
  • 说明 MySQL DDL lock 风险;
  • 说明发布与回滚顺序。

验收

  • ADR 包含最终 Prisma 草图;
  • DDL 草图;
  • 状态映射表;
  • 新旧字段读写矩阵;
  • 实体关系图;
  • 零停机发布流程;
  • 回滚流程。

ADR 合并后,后续 Issue 只能按 ADR 实现。

## 依赖 * M-AI-02-01 ## 目标 根据真实审计冻结本批最终 Schema,后续 Issue 不得自行新增字段、状态、表或索引。 ## 输出文档 新增: ``` docs/architecture/adr-002-ai-job-database-expand.md ``` ## 必须冻结 ### 1. 物理表策略 * 现有 AiAnalysisJob 物理表名; * Prisma `AiJob` 的 `@@map`; * 所有现有物理列保持原名; * 禁止新建另一张通用 Job 主表。 ### 2. 新字段定义 逐字段冻结: * Prisma 字段名; * 物理列名; * 类型; * 长度; * nullable/default; * index; * 写入来源; * 旧字段映射; * 未来删除阶段。 计划字段至少覆盖: ``` jobType triggerType lifecycleStatus queueName priority targetType targetId parentJobId idempotencyKey inputRef inputSchemaVersion snapshotId credentialMode credentialId modelTier promptKey promptVersion outputSchemaVersion progress attemptCount maxAttempts timeoutMs validatedOutput outputHash errorCode publicErrorMessage internalErrorMessage queuedAt startedAt finishedAt cancelRequestedAt ``` 字段可根据 01 审计调整,但调整必须在 ADR 中解释。 ### 3. 索引 至少评估: ``` (userId, jobType, createdAt) (lifecycleStatus, createdAt) (queueName, lifecycleStatus, createdAt) (targetType, targetId) (parentJobId) (userId, jobType, idempotencyKey) UNIQUE ``` ### 4. 删除策略 明确: * Job → Snapshot; * Job → Artifact; * Job → UsageLog; * Job → Child Job。 ### 5. Migration 策略 * Expand only; * 不 drop; * 不 rename; * 不重建表; * 新字段 nullable 或安全默认; * 旧版本应用可继续运行; * 说明 MySQL DDL lock 风险; * 说明发布与回滚顺序。 ## 验收 * ADR 包含最终 Prisma 草图; * DDL 草图; * 状态映射表; * 新旧字段读写矩阵; * 实体关系图; * 零停机发布流程; * 回滚流程。 ADR 合并后,后续 Issue 只能按 ADR 实现。
wangdl added this to the M-AI-02 统一 AiJob 数据库 Expand milestone 2026-06-19 23:30:03 +08:00
wangdl added the
area:database
architecture
adr
labels 2026-06-19 23:30:03 +08:00
wangdl changed title from [M-AI-02] 冻结 AiJob Expand Schema 与迁移契约 to [M-AI-02-02] 冻结 AiJob Expand Schema 与迁移契约 2026-06-19 23:31:04 +08:00
Author
Owner

开发完成评论

完成内容

  • 冻结了统一 AiJob Prisma model 的完整 Schema(34 个字段),基于 @@map("AiAnalysisJob") 保持物理表名不变
  • 逐字段定义了 Prisma 字段名、物理列名、类型、长度、nullable/default、index、写入来源、旧字段映射、未来删除阶段
  • 定义了统一 lifecycleStatus 状态映射表(pending/locked/running/succeeded/failed/cancelled)
  • 定义了新旧字段读写矩阵和 Shadow Write 策略(status ↔ lifecycleStatus 双向同步)
  • 评估了 6 个索引(含 1 个 UNIQUE)
  • 定义了 5 个关系的删除策略(CASCADE/SET NULL/RESTRICT)
  • 定义了完整的 DDL 草图(Expand-only,不 drop/rename/重建)
  • 定义了零停机 6 阶段发布流程和回滚方案
  • 包含实体关系图

关键设计决策

  1. 物理表名保持 AiAnalysisJob:避免 rename 风险和 37 处测试 SQL 同步
  2. Prisma model 命名 AiJob@@map 映射物理表
  3. Minimax 无需保留兼容:AI Key 已到期,生产全量 DeepSeek
  4. 所有新列 nullable 或有 safe default:确保旧版本应用可继续运行
  5. completedAtfinishedAt:语义统一(与 startedAt 配对)
  6. errorMessageinternalErrorMessage:与新增 publicErrorMessage 区分
  7. retryCountattemptCount + maxRetryCountmaxAttempts:命名一致性
  8. timeoutSecondstimeoutMs:与其他毫秒级字段统一

修改文件

  • docs/architecture/adr-002-ai-job-database-expand.md(新增,386 行)

测试情况

  • 已运行:npx jest --testPathPatterns="ai-analysis|ai-runtime" --passWithNoTests
  • 结果:11 suites, 265 tests passed

未完成 / 风险

  • 无。Schema 已冻结,后续 Issue 按本 ADR 实现。

是否建议进入 Review

## 开发完成评论 ### 完成内容 - 冻结了统一 `AiJob` Prisma model 的完整 Schema(34 个字段),基于 `@@map("AiAnalysisJob")` 保持物理表名不变 - 逐字段定义了 Prisma 字段名、物理列名、类型、长度、nullable/default、index、写入来源、旧字段映射、未来删除阶段 - 定义了统一 `lifecycleStatus` 状态映射表(pending/locked/running/succeeded/failed/cancelled) - 定义了新旧字段读写矩阵和 Shadow Write 策略(status ↔ lifecycleStatus 双向同步) - 评估了 6 个索引(含 1 个 UNIQUE) - 定义了 5 个关系的删除策略(CASCADE/SET NULL/RESTRICT) - 定义了完整的 DDL 草图(Expand-only,不 drop/rename/重建) - 定义了零停机 6 阶段发布流程和回滚方案 - 包含实体关系图 ### 关键设计决策 1. **物理表名保持 `AiAnalysisJob`**:避免 rename 风险和 37 处测试 SQL 同步 2. **Prisma model 命名 `AiJob`**:`@@map` 映射物理表 3. **Minimax 无需保留兼容**:AI Key 已到期,生产全量 DeepSeek 4. **所有新列 nullable 或有 safe default**:确保旧版本应用可继续运行 5. **`completedAt` → `finishedAt`**:语义统一(与 `startedAt` 配对) 6. **`errorMessage` → `internalErrorMessage`**:与新增 `publicErrorMessage` 区分 7. **`retryCount` → `attemptCount` + `maxRetryCount` → `maxAttempts`**:命名一致性 8. **`timeoutSeconds` → `timeoutMs`**:与其他毫秒级字段统一 ### 修改文件 - `docs/architecture/adr-002-ai-job-database-expand.md`(新增,386 行) ### 测试情况 - 已运行:`npx jest --testPathPatterns="ai-analysis|ai-runtime" --passWithNoTests` - 结果:11 suites, 265 tests passed ### 未完成 / 风险 - 无。Schema 已冻结,后续 Issue 按本 ADR 实现。 ### 是否建议进入 Review - 是
Author
Owner

N1 修复:状态映射 processing → locked 已修正为 processing → running

修正内容

位置 旧值 新值
§3 状态映射表 processinglocked processingrunninglocked 单独成行标注「旧系统无对应」
§3 回填 SQL WHEN 'processing' THEN 'locked' WHEN 'processing' THEN 'running'
§4 Shadow Write 伪代码描述 完整 TypeScript shadowMap / reverseShadowMap 双向映射

语义依据

  • processingAiAnalysisWorker.process() 第 48 行 updateJobStatus(jobId, 'processing') → Worker 正在执行
  • running:heartbeat 确认后 → Runtime 正在执行
  • locked:Runtime POST /lock 成功但尚未 heartbeat → 已被 claim 但未开始执行(旧系统由 BullMQ 内部管理,无显式对应)

Reaper 安全

若映射为 locked,JobReaperService 会将旧 Worker 正在执行的任务视为「无 heartbeat」而在 lockUntil 过期后错误清扫回 pending(重复执行)。映射为 running 可避免此问题。

## N1 修复:状态映射 `processing → locked` 已修正为 `processing → running` ### 修正内容 | 位置 | 旧值 | 新值 | |------|------|------| | §3 状态映射表 | `processing` → `locked` | `processing` → `running`;`locked` 单独成行标注「旧系统无对应」 | | §3 回填 SQL | `WHEN 'processing' THEN 'locked'` | `WHEN 'processing' THEN 'running'` | | §4 Shadow Write | 伪代码描述 | 完整 TypeScript `shadowMap` / `reverseShadowMap` 双向映射 | ### 语义依据 - 旧 `processing`:`AiAnalysisWorker.process()` 第 48 行 `updateJobStatus(jobId, 'processing')` → Worker **正在执行** - 新 `running`:heartbeat 确认后 → Runtime **正在执行** - 新 `locked`:Runtime POST /lock 成功但尚未 heartbeat → **已被 claim 但未开始执行**(旧系统由 BullMQ 内部管理,无显式对应) ### Reaper 安全 若映射为 `locked`,JobReaperService 会将旧 Worker 正在执行的任务视为「无 heartbeat」而在 `lockUntil` 过期后错误清扫回 `pending`(重复执行)。映射为 `running` 可避免此问题。
Author
Owner

N2 修复:"不 rename" 声明已精确化

修正

原声明「不 drop、不 rename、不重建」已修正为:

不 drop 列、不 rename 表名、不重建表。允许 2 处列重命名(completedAtfinishedAterrorMessageinternalErrorMessage),理由见 §7 DDL 注释。

两处列重命名的安全保证

列重命名 理由 安全验证
completedAtfinishedAt startedAt 配对统一;旧名 "completed" 与 lifecycleStatus='succeeded' 语义冲突 M-AI-02-01 §6:测试 Raw SQL 不引用此列名
errorMessageinternalErrorMessage 新增 publicErrorMessage 列,需区分内部/对外错误 M-AI-02-01 §6:测试 Raw SQL 不引用此列名

什么不变

  • 表名 AiAnalysisJob 不变(Prisma AiJob + @@map 映射)
  • 其他 11 个现有列名不变
  • 不 drop 任何列(sessionId/answerId 保留但标记废弃)
## N2 修复:"不 rename" 声明已精确化 ### 修正 原声明「不 drop、不 rename、不重建」已修正为: > **不 drop 列、不 rename 表名、不重建表**。允许 2 处列重命名(`completedAt`→`finishedAt`、`errorMessage`→`internalErrorMessage`),理由见 §7 DDL 注释。 ### 两处列重命名的安全保证 | 列重命名 | 理由 | 安全验证 | |---------|------|----------| | `completedAt` → `finishedAt` | 与 `startedAt` 配对统一;旧名 "completed" 与 `lifecycleStatus='succeeded'` 语义冲突 | M-AI-02-01 §6:测试 Raw SQL 不引用此列名 | | `errorMessage` → `internalErrorMessage` | 新增 `publicErrorMessage` 列,需区分内部/对外错误 | M-AI-02-01 §6:测试 Raw SQL 不引用此列名 | ### 什么不变 - 表名 `AiAnalysisJob` 不变(Prisma `AiJob` + `@@map` 映射) - 其他 11 个现有列名不变 - 不 drop 任何列(`sessionId`/`answerId` 保留但标记废弃)
Author
Owner

N3 修复:过渡期 status 列已加入 Prisma 模型

修正

三处修改:

  1. §2.1 Prisma 草图:向后兼容字段区新增 status String? @db.VarChar(32),标注 @deprecated + M-AI-05 移除
  2. §2.1 注释:新增过渡期说明块 — status 是物理表已有列,旧代码 AiAnalysisRepository.updateJobStatus() 直接读写,Shadow Write(§4)依赖此字段在 Prisma 层面的暴露
  3. §2.2 向后兼容字段表:新增 status 行,标注「过渡期保留(Shadow Write 必需)」

实现路径

M-AI-02-09: AiJob model 包含 status + lifecycleStatus(双列共存)
M-AI-02-10: Shadow Write 通过 Prisma 同时写两列
M-AI-04:    Active Recall 切换至新 Producer(只写 lifecycleStatus)
M-AI-05:    Feynman 切换 → 旧代码清零 → Migration DROP COLUMN status
## N3 修复:过渡期 `status` 列已加入 Prisma 模型 ### 修正 三处修改: 1. **§2.1 Prisma 草图**:向后兼容字段区新增 `status String? @db.VarChar(32)`,标注 `@deprecated` + M-AI-05 移除 2. **§2.1 注释**:新增过渡期说明块 — `status` 是物理表已有列,旧代码 `AiAnalysisRepository.updateJobStatus()` 直接读写,Shadow Write(§4)依赖此字段在 Prisma 层面的暴露 3. **§2.2 向后兼容字段表**:新增 `status` 行,标注「过渡期保留(Shadow Write 必需)」 ### 实现路径 ``` M-AI-02-09: AiJob model 包含 status + lifecycleStatus(双列共存) M-AI-02-10: Shadow Write 通过 Prisma 同时写两列 M-AI-04: Active Recall 切换至新 Producer(只写 lifecycleStatus) M-AI-05: Feynman 切换 → 旧代码清零 → Migration DROP COLUMN status ```
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

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