api-server/docs/architecture/adr-002-ai-job-database-expand.md
wangdl 4c2b8d02d7
Some checks failed
Deploy API Server / build-and-deploy (push) Failing after 24s
docs(M-AI-02-GATE): update ADR-002 lifecycle spec + @map strategy
- lifecycleStatus enum: created|queued|running|retrying|succeeded|failed|cancel_requested|cancelled
- Legacy mapping: pending→queued, processing→running, completed→succeeded, failed→failed
- @map approach documented for physical column compatibility

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-20 12:40:26 +08:00

27 KiB
Raw Permalink Blame History

ADR-002AiJob 数据库 Expand — Schema 冻结

状态

已接受2026-06-20

背景

M-AI-02-01 审计确认:

  • AiAnalysisJob(系统 A13 字段9 行生产数据):承载 active-recall 和 feynman-evaluation 分析
  • AiRuntimeJob(系统 B31 字段0 行生产数据):设计支持 5 种分析类型但无生产者
  • 两表无 FK 关联jobType 不重叠,状态语义不兼容
  • 生产数据量极小(< 0.12 MB / 表Online DDL 毫秒级完成
  • Minimax API Key 已到期2026-06-06生产已全量切换 DeepSeek V4 Pro/Flash

目标

在现有 AiAnalysisJob 物理表上 Expand-only不 drop 列、不 rename 表名、不重建表。允许 2 处列重命名(completedAtfinishedAterrorMessageinternalErrorMessage),理由见 §7 DDL 注释。将两套系统字段统一为单一 AiJob Prisma model后续 Issue 只按本 ADR 实现。


决策

1. 物理表策略

项目 决策 依据
物理表名 保持 AiAnalysisJob M-AI-02-01 §1.1:现有生产数据在此表
Prisma model 名 AiJob@@map("AiAnalysisJob") 统一命名;@@map 避免物理表 rename
AiRuntimeJob 表 不删除、不修改、不迁移 0 行生产数据M-AI-08 退场时一并清理
AiRuntimeResult 表 不修改 M-AI-03 统一 Job Engine 后用新的 Result 模型
AiAnalysisResult 表 不修改 保留旧系统 A 的结果存储M-AI-04 迁移后评估删除
列重命名策略 Prisma 逻辑名用新名,物理列用 @map 保持旧名 internalErrorMessage @map("errorMessage")finishedAt @map("completedAt")。旧版本应用通过物理列名兼容,新代码通过逻辑名访问

2. 统一 AiJob Schema

2.1 Prisma 草图

model AiJob {
  id                   String    @id @default(cuid())
  userId               String
  // ── M-AI-02-03: 身份与路由 ──
  jobType              String    @db.VarChar(64)       // 来源: AiAnalysisJob.jobType (expand 32→64)
  triggerType          String    @default("api") @db.VarChar(32)
  queueName            String    @default("ai-interactive") @db.VarChar(64)
  targetType           String?   @db.VarChar(32)       // 来源: AiRuntimeJob.targetType
  targetId             String?   @db.VarChar(255)      // 来源: AiRuntimeJob.targetId
  parentJobId          String?   @db.VarChar(255)
  idempotencyKey       String?   @unique @db.VarChar(255) // 来源: AiRuntimeJob.idempotencyKey
  retriedFromJobId     String?   @db.VarChar(255)      // 来源: AiRuntimeJob.retriedFromJobId

  // ── M-AI-02-04: 生命周期与执行 ──
  lifecycleStatus      String    @default("pending") @db.VarChar(32) // 来源: AiAnalysisJob.status + AiRuntimeJob.status
  priority             Int       @default(0)          // 来源: AiRuntimeJob.priority
  progress             Int       @default(0)          // 来源: AiAnalysisJob.progress
  lockedBy             String?   @db.VarChar(100)     // 来源: AiRuntimeJob.lockedBy
  lockedAt             DateTime?
  lockUntil            DateTime?
  startedAt            DateTime?                      // 来源: AiAnalysisJob.startedAt + AiRuntimeJob.startedAt
  finishedAt           DateTime?                      // 来源: AiAnalysisJob.completedAt + AiRuntimeJob.finishedAt
  queuedAt             DateTime?                      // 来源: AiAnalysisJob.queuedAt
  cancelRequestedAt    DateTime?                      // 来源: AiRuntimeJob.cancelRequestedAt
  attemptCount         Int       @default(0)          // 来源: AiRuntimeJob.retryCount (重命名)
  maxAttempts          Int       @default(3)          // 来源: AiRuntimeJob.maxRetryCount (重命名)
  timeoutMs            Int       @default(120000)      // 来源: AiRuntimeJob.timeoutSeconds × 1000

  // ── M-AI-02-04: 输入输出 ──
  inputRef             String?   @db.VarChar(255)     // 新增
  inputSchemaVersion   String?   @db.VarChar(100)
  snapshotId           String?   @db.VarChar(255)     // 来源: AiRuntimeJob.snapshotId
  promptKey            String?   @db.VarChar(128)     // 新增
  promptVersion        String?   @db.VarChar(100)     // 来源: AiRuntimeJob.promptVersion
  outputSchemaVersion  String?   @db.VarChar(100)     // 来源: AiRuntimeJob.outputSchemaVersion

  // ── M-AI-02-04: 凭据与模型 ──
  credentialMode       String    @default("platform_key") @db.VarChar(32) // 来源: AiRuntimeJob.apiKeyMode
  credentialId         String?   @db.VarChar(255)     // 来源: AiRuntimeJob.credentialId
  modelTier            String    @default("primary") @db.VarChar(32)
  modelProvider        String    @default("deepseek") @db.VarChar(32) // 来源: AiRuntimeJob.modelProvider
  modelName            String    @default("deepseek-chat") @db.VarChar(64) // 来源: AiRuntimeJob.modelName

  // ── M-AI-02-04: 错误与输出 ──
  errorCode            String?   @db.VarChar(100)     // 来源: AiRuntimeJob.errorCode
  publicErrorMessage   String?   @db.VarChar(500)     // 新增:面向用户的错误
  internalErrorMessage String?   @db.Text             // 来源: AiAnalysisJob.errorMessage + AiRuntimeJob.errorMessage
  outputHash           String?   @db.VarChar(255)     // 来源: AiRuntimeResult.outputHash
  validatedOutput      Json?                          // 来源: AiRuntimeResult.validatedOutput脱敏后可缓存

  // ── 向后兼容字段(标记待删除)──
  sessionId            String?   @db.VarChar(191)     // 旧系统 A100% NULL in prod
  answerId             String?   @db.VarChar(191)     // 旧系统 A78% NULL in prod
  status               String?   @db.VarChar(32)      // 🔴 @deprecated旧系统 A 状态列。过渡期 Shadow Write 使用M-AI-05 移除

  // ── 时间戳 ──
  createdAt            DateTime  @default(now())
  updatedAt            DateTime  @updatedAt

  // ── 关联 ──
  user                 User      @relation(fields: [userId], references: [id], onDelete: Restrict, onUpdate: Cascade)
  parentJob            AiJob?    @relation("JobChildren", fields: [parentJobId], references: [id], onDelete: SetNull, onUpdate: Cascade)
  children             AiJob[]   @relation("JobChildren")
  snapshots            AiJobSnapshot[]
  artifacts            AiJobArtifact[]
  usageLogs            AiUsageLog[]

  @@map("AiAnalysisJob")
  @@index([userId, jobType, createdAt])
  @@index([lifecycleStatus, createdAt])
  @@index([queueName, lifecycleStatus, createdAt])
  @@index([targetType, targetId])
  @@index([parentJobId])
  @@index([userId, jobType, idempotencyKey])
}

过渡期 status 列说明status 是物理表 AiAnalysisJob 中已存在 13 个月之久的列(VARCHAR(32) NOT NULL DEFAULT 'pending'),旧代码 AiAnalysisRepository.updateJobStatus() 直接读写此列。M-AI-02-09 将 Prisma model 重命名为 AiJob 后,必须在过渡期模型中保留 status 字段(标记 @deprecated),否则 Shadow Write 机制§4缺少 Prisma 层面的实现路径。status 列在 M-AI-05Active Recall + Feynman 全部迁移完成后)通过 migration 删除。

2.2 字段逐一定义

身份与路由M-AI-02-03
Prisma 字段 物理列名 类型 Nullable Default Index 写入来源 旧字段映射 未来删除阶段
jobType jobType VARCHAR(64) - 复合 API → Controller → Service AiAnalysisJob.jobTypeexpand 32→64 -
triggerType triggerType VARCHAR(32) 'api' Service 层根据调用方设定 新增 -
queueName queueName VARCHAR(64) 'ai-interactive' 复合 Service 层根据 jobType 路由 新增 -
targetType targetType VARCHAR(32) - 复合 API DTO AiRuntimeJob.targetType -
targetId targetId VARCHAR(255) - 复合 API DTO AiRuntimeJob.targetId -
parentJobId parentJobId VARCHAR(255) - 单列 Service → 子 Job 创建时赋值 新增 -
idempotencyKey idempotencyKey VARCHAR(255) - UNIQUE API DTOiOS 提供 UUID AiRuntimeJob.idempotencyKey -
retriedFromJobId retriedFromJobId VARCHAR(255) - Worker → 重试时赋值 AiRuntimeJob.retriedFromJobId M-AI-08
生命周期与执行M-AI-02-04
Prisma 字段 物理列名 类型 Nullable Default Index 写入来源 旧字段映射 未来删除阶段
lifecycleStatus lifecycleStatus VARCHAR(32) 'pending' 复合 Worker/Reaper/User cancel AiAnalysisJob.status + AiRuntimeJob.status(统一状态枚举) -
priority priority INT 0 PriorityRulesService AiRuntimeJob.priority -
progress progress INT 0 Worker heartbeat AiAnalysisJob.progress -
lockedBy lockedBy VARCHAR(100) - Runtime lock AiRuntimeJob.lockedBy -
lockedAt lockedAt DATETIME(3) - Runtime lock AiRuntimeJob.lockedAt -
lockUntil lockUntil DATETIME(3) - Runtime heartbeat AiRuntimeJob.lockUntil -
startedAt startedAt DATETIME(3) - Worker/Runtime → 首次 heartbeat AiAnalysisJob.startedAt + AiRuntimeJob.startedAt -
finishedAt finishedAt DATETIME(3) - Worker/Runtime → 终态 AiAnalysisJob.completedAt + AiRuntimeJob.finishedAt -
queuedAt queuedAt DATETIME(3) - Service → create AiAnalysisJob.queuedAt -
cancelRequestedAt cancelRequestedAt DATETIME(3) - User/Admin cancel AiRuntimeJob.cancelRequestedAt -
attemptCount attemptCount INT 0 Worker → 重试时递增 AiRuntimeJob.retryCount(重命名) -
maxAttempts maxAttempts INT 3 JobType config AiRuntimeJob.maxRetryCount(重命名) -
timeoutMs timeoutMs INT 120000 JobType config AiRuntimeJob.timeoutSeconds × 1000(单位变更) -
输入输出M-AI-02-04
Prisma 字段 物理列名 类型 Nullable Default Index 写入来源 旧字段映射 未来删除阶段
inputRef inputRef VARCHAR(255) - Service → create 新增 -
inputSchemaVersion inputSchemaVersion VARCHAR(100) - Service → create 新增 -
snapshotId snapshotId VARCHAR(255) - SnapshotBuilderService AiRuntimeJob.snapshotId -
promptKey promptKey VARCHAR(128) - Service → create 新增(AiUsageLog.promptKey 对应) -
promptVersion promptVersion VARCHAR(100) - Service → create AiRuntimeJob.promptVersion -
outputSchemaVersion outputSchemaVersion VARCHAR(100) - Service → create AiRuntimeJob.outputSchemaVersion -
凭据与模型M-AI-02-04
Prisma 字段 物理列名 类型 Nullable Default Index 写入来源 旧字段映射
credentialMode credentialMode VARCHAR(32) 'platform_key' Settings → Service AiRuntimeJob.apiKeyMode(重命名)
credentialId credentialId VARCHAR(255) - Settings → Service AiRuntimeJob.credentialId
modelTier modelTier VARCHAR(32) 'primary' ModelRoute config 新增
modelProvider modelProvider VARCHAR(32) 'deepseek' ModelRoute config AiRuntimeJob.modelProvider
modelName modelName VARCHAR(64) 'deepseek-chat' ModelRoute config AiRuntimeJob.modelName
错误与输出M-AI-02-04
Prisma 字段 物理列名 类型 Nullable Default Index 写入来源 旧字段映射
errorCode errorCode VARCHAR(100) - Worker → fail AiRuntimeJob.errorCode
publicErrorMessage publicErrorMessage VARCHAR(500) - Worker → fail脱敏后 新增
internalErrorMessage internalErrorMessage TEXT - Worker → fail完整堆栈 AiAnalysisJob.errorMessage + AiRuntimeJob.errorMessage
outputHash outputHash VARCHAR(255) - Worker → success AiRuntimeResult.outputHash
validatedOutput validatedOutput JSON - Worker → success脱敏后缓存 AiRuntimeResult.validatedOutput
向后兼容字段
Prisma 字段 物理列名 类型 Nullable Default 当前 NULL 率 计划
status status VARCHAR(32) 'pending' 0% 🔴 过渡期保留Shadow Write 必需)。旧代码 AiAnalysisRepository.updateJobStatus() 直接读写。M-AI-05 移除
sessionId sessionId VARCHAR(191) - 100% (9/9) 保留但废弃(旧系统 AM-AI-05 删除
answerId answerId VARCHAR(191) - 78% (7/9) 保留但废弃(旧系统 AM-AI-05 删除

3. 统一生命周期状态

新 lifecycleStatus 枚举8 个值):

状态 含义
created Job 记录已创建,尚未入队
queued 已入队,等待 Worker 消费
running Worker 正在执行
retrying 执行失败,等待重试
succeeded 执行成功(终态)
failed 执行失败,超过最大重试(终态)
cancel_requested 用户/Admin 请求取消,等待 Worker 确认
cancelled 已取消(终态)

Legacy → 统一映射Shadow Write + 回填):

旧 AiAnalysisJob.status 旧 AiRuntimeJob.status 新 lifecycleStatus
pending pending queued
processing running running
completed succeeded succeeded
failed failed failed
- locked N/A新系统内部状态旧系统无对应
- cancelled cancelled

为什么 pendingqueued:旧系统的 pending 表示「已创建且在 BullMQ 队列中等待」,语义更接近新系统的 queued。新系统的 created 预留给未来「创建记录」和「入队」分离的场景。

lifecycleStatus 过渡期 nullable:旧代码写入时通过 Shadow Write 同步填充M-AI-02-10 回填历史 NULLM-AI-05 切换完成后改为 NOT NULL。

迁移回填 SQL(旧 status → 新 lifecycleStatus

UPDATE `AiAnalysisJob`
SET `lifecycleStatus` = CASE `status`
  WHEN 'pending'    THEN 'queued'
  WHEN 'processing' THEN 'running'
  WHEN 'completed'  THEN 'succeeded'
  WHEN 'failed'     THEN 'failed'
  ELSE `status`
END
WHERE `lifecycleStatus` IS NULL;

4. 新旧字段读写矩阵

场景 读取旧字段 写入新字段 Shadow Write 说明
旧 ProduceriOS active-recall status, jobType - 改写到 lifecycleStatus M-AI-04 迁移前,旧 Producer 仍写 status
旧 WorkerAiAnalysisWorker status - 改写到 lifecycleStatus M-AI-04 迁移前,旧 Worker 仍读 status
新 ProducerUserAiService lifecycleStatus 全部新字段 同步 status 写新字段 + Shadow Write status
新 WorkerAiJob Worker 全部新字段 全部新字段 同步 status M-AI-03 实现
Admin 查询Dashboard/Metrics AiUsageLog(不变) - 不涉及 Job 表
Admin 查询Learning AiAnalysisResult(不变) - 不涉及 Job 表
iOS Job 列表 jobType, lifecycleStatus, createdAt - 新字段直接通过 API 返回

Shadow Write 规则M-AI-02-10 实现):

// 旧 → 新reverseShadowMap旧代码写 status 时,同步写 lifecycleStatus
const reverseShadowMap: Record<string, string> = {
  'pending':    'pending',
  'processing': 'running',   // 旧 processing = 正在执行 → 新 running
  'completed':  'succeeded',
  'failed':     'failed',
};

// 新 → 旧shadowMap新代码写 lifecycleStatus 时,同步写 status
const shadowMap: Record<string, string | null> = {
  'pending':   'pending',
  'locked':    null,         // 旧系统无对应,不写 status
  'running':   'processing', // 新 running = 正在执行 → 旧 processing
  'succeeded': 'completed',
  'failed':    'failed',
  'cancelled': 'failed',     // 旧系统无 cancelled映射为 failed终态
};
  1. 新代码写 lifecycleStatus 时,若 shadowMap[lifecycleStatus] 非 null同步写 status
  2. 旧代码写 status 时,同步写 lifecycleStatus = reverseShadowMap[status]
  3. status 废弃时机M-AI-05Active Recall + Feynman 全部迁移后)

5. 索引评估

索引 类型 用途 评估
(userId, jobType, createdAt) 复合 iOS 用户端 Job 列表(最频繁查询) 必须:当前 AiAnalysisJob 仅单列 userId 索引
(lifecycleStatus, createdAt) 复合 Worker pollWHERE status='pending' ORDER BY createdAt 必须Runtime/Worker 轮询核心查询
(queueName, lifecycleStatus, createdAt) 复合 按队列轮询ai-interactive / ai-background 分队列消费) 重要M-AI-03 队列分离
(targetType, targetId) 复合 按学习对象查 Job如「该 KnowledgeBase 的所有分析」) 保留:从 AiRuntimeJob 迁移
(parentJobId) 单列 查子 Job如 Quiz Generation 的拆分任务) 必要:子 Job 关联
idempotencyKey UNIQUE单列 幂等保证 必要key 由客户端提供(如 iOS UUID复用 key 即表示重复请求应被拒绝。若未来需要 (userId, idempotencyKey) 复合唯一(防跨用户 key 碰撞),可新增复合索引并 drop 此单列索引

6. 删除策略

关系 当前 FK 变更 M-AI-02 后 FK 说明
Job → Snapshot 无 FK 新增 FKAiJobSnapshot.jobId → AiJob.id ON DELETE CASCADE M-AI-02-05 实现。CASCADE 确保 Job 删除时无孤儿 Snapshot独立过期清理通过 deleteExpired() 在 CASCADE 前主动回收
Job → Artifact M-AI-02-06 新建) 新增 FKAiJobArtifact.jobId → AiJob.id ON DELETE CASCADE Artifact 随 Job 级联删除
Job → UsageLog M-AI-02-08 新建) 新增 FKAiUsageLog.jobId → AiJob.id ON DELETE SET NULL UsageLog 是审计记录,删除 Job 时保留
Job → Child Job M-AI-02-03 新增 parentJobId 新增 FKAiJob.parentJobId → AiJob.id ON DELETE SET NULL 父 Job 删除时不级联删子 Job
User → Job ON DELETE RESTRICT 不变 保持 RESTRICT 删除用户前必须清理 Job

7. DDL 草图

-- M-AI-02-03: 身份与路由字段
ALTER TABLE `AiAnalysisJob`
  ADD COLUMN `triggerType` VARCHAR(32) NOT NULL DEFAULT 'api' AFTER `jobType`,
  ADD COLUMN `queueName` VARCHAR(64) NOT NULL DEFAULT 'ai-interactive' AFTER `triggerType`,
  ADD COLUMN `targetType` VARCHAR(32) NULL AFTER `sessionId`,
  ADD COLUMN `targetId` VARCHAR(255) NULL AFTER `targetType`,
  ADD COLUMN `parentJobId` VARCHAR(255) NULL AFTER `targetId`,
  ADD COLUMN `idempotencyKey` VARCHAR(255) NULL AFTER `parentJobId`,
  ADD COLUMN `retriedFromJobId` VARCHAR(255) NULL AFTER `idempotencyKey`;

-- M-AI-02-04: 生命周期字段
ALTER TABLE `AiAnalysisJob`
  ADD COLUMN `lifecycleStatus` VARCHAR(32) NOT NULL DEFAULT 'pending' AFTER `retriedFromJobId`,
  ADD COLUMN `priority` INT NOT NULL DEFAULT 0 AFTER `lifecycleStatus`,
  ADD COLUMN `lockedBy` VARCHAR(100) NULL AFTER `priority`,
  ADD COLUMN `lockedAt` DATETIME(3) NULL AFTER `lockedBy`,
  ADD COLUMN `lockUntil` DATETIME(3) NULL AFTER `lockedAt`,
  ADD COLUMN `cancelRequestedAt` DATETIME(3) NULL AFTER `lockUntil`,
  ADD COLUMN `attemptCount` INT NOT NULL DEFAULT 0 AFTER `progress`,
  ADD COLUMN `maxAttempts` INT NOT NULL DEFAULT 3 AFTER `attemptCount`,
  ADD COLUMN `timeoutMs` INT NOT NULL DEFAULT 120000 AFTER `maxAttempts`;

-- M-AI-02-04: 列重命名 1/2 — completedAt → finishedAt
-- 理由:与 startedAt 配对统一("started/finished"),旧名 "completed" 与新 lifecycleStatus 'succeeded' 语义冲突
-- 安全M-AI-02-01 审计确认测试文件 Raw SQL 不引用此列名,无破坏风险
ALTER TABLE `AiAnalysisJob`
  CHANGE COLUMN `completedAt` `finishedAt` DATETIME(3) NULL;

-- M-AI-02-04: 输入输出字段
ALTER TABLE `AiAnalysisJob`
  ADD COLUMN `inputRef` VARCHAR(255) NULL AFTER `timeoutMs`,
  ADD COLUMN `inputSchemaVersion` VARCHAR(100) NULL AFTER `inputRef`,
  ADD COLUMN `snapshotId` VARCHAR(255) NULL AFTER `inputSchemaVersion`,
  ADD COLUMN `promptKey` VARCHAR(128) NULL AFTER `snapshotId`,
  ADD COLUMN `promptVersion` VARCHAR(100) NULL AFTER `promptKey`,
  ADD COLUMN `outputSchemaVersion` VARCHAR(100) NULL AFTER `promptVersion`;

-- M-AI-02-04: 凭据与模型字段
ALTER TABLE `AiAnalysisJob`
  ADD COLUMN `credentialMode` VARCHAR(32) NOT NULL DEFAULT 'platform_key' AFTER `outputSchemaVersion`,
  ADD COLUMN `credentialId` VARCHAR(255) NULL AFTER `credentialMode`,
  ADD COLUMN `modelTier` VARCHAR(32) NOT NULL DEFAULT 'primary' AFTER `credentialId`,
  ADD COLUMN `modelProvider` VARCHAR(32) NOT NULL DEFAULT 'deepseek' AFTER `modelTier`,
  ADD COLUMN `modelName` VARCHAR(64) NOT NULL DEFAULT 'deepseek-chat' AFTER `modelProvider`;

-- M-AI-02-04: 错误与输出字段
ALTER TABLE `AiAnalysisJob`
  ADD COLUMN `errorCode` VARCHAR(100) NULL AFTER `errorMessage`,
  ADD COLUMN `publicErrorMessage` VARCHAR(500) NULL AFTER `errorCode`,
  ADD COLUMN `outputHash` VARCHAR(255) NULL AFTER `publicErrorMessage`,
  ADD COLUMN `validatedOutput` JSON NULL AFTER `outputHash`;

-- M-AI-02-04: 列重命名 2/2 — errorMessage → internalErrorMessage
-- 理由:新增 publicErrorMessage 列(面向用户),需区分内部/对外错误信息
-- 安全M-AI-02-01 审计确认测试文件 Raw SQL 不引用此列名,无破坏风险
ALTER TABLE `AiAnalysisJob`
  CHANGE COLUMN `errorMessage` `internalErrorMessage` TEXT NULL;

-- 索引
ALTER TABLE `AiAnalysisJob`
  ADD INDEX `idx_userId_jobType_createdAt` (`userId`, `jobType`, `createdAt`),
  ADD INDEX `idx_lifecycleStatus_createdAt` (`lifecycleStatus`, `createdAt`),
  ADD INDEX `idx_queueName_lifecycleStatus_createdAt` (`queueName`, `lifecycleStatus`, `createdAt`),
  ADD INDEX `idx_targetType_targetId` (`targetType`, `targetId`),
  ADD INDEX `idx_parentJobId` (`parentJobId`),
  ADD UNIQUE INDEX `idx_userId_jobType_idempotencyKey` (`userId`, `jobType`, `idempotencyKey`);

8. 零停机发布流程

阶段 操作 影响 时长
1. Pre-deploy 执行 DDL所有新列 nullable 或有 default 无锁表InnoDB Online DDL0.12 MB 表 < 10ms < 1 秒
2. Deploy V1 部署包含 @@map("AiAnalysisJob") 的新 Prisma client 旧代码仍读 AiAnalysisJob,新代码读 AiJob,物理表不变 正常滚动部署
3. Backfill UPDATE AiAnalysisJob SET lifecycleStatus = CASE status ... 9 行数据,瞬时完成 < 1 秒
4. Verify 运行 E2E smoke test 确认新旧 Producer 均正常读写 5 分钟
5. Shadow Write 启用双向 Shadow Writestatus ↔ lifecycleStatus 新旧代码互不阻塞 运行至 M-AI-05
6. Cleanup M-AI-05 删除旧 status 列、sessionIdanswerId 确认无旧代码读取后执行 M-AI-05 批次

9. 回滚流程

场景 操作
阶段 1-2 回滚 DDL 新增列全部 nullable旧代码不感知。无需 DDL 回滚,仅回滚代码部署。
阶段 3 回滚 Backfill 仅写了 lifecycleStatus,不影响 status 列。回滚代码即可。
阶段 4-5 回滚 Shadow Write 双向同步,两端状态一致。回滚代码后旧代码继续工作。
紧急回滚到 V0 无 DDL 回滚需求。所有新列为 nullable 或有 default。回滚代码部署即可。
completedAtfinishedAt 回滚 CHANGE COLUMN finishedAt completedAt DATETIME(3) NULL — 仅改名,数据无损

10. 实体关系图

┌──────────┐       ┌──────────────────┐       ┌──────────────────┐
│   User   │──1:N──│      AiJob       │──1:N──│  AiJobArtifact   │
│          │       │  (AiAnalysisJob) │       │  (M-AI-02-06)    │
└──────────┘       └────────┬─────────┘       └──────────────────┘
                            │
            ┌───────────────┼───────────────┐
            │               │               │
           1:N             1:1             N:1
    ┌──────────────┐ ┌──────────────┐ ┌──────────────┐
    │ AiJobSnapshot│ │  AiUsageLog  │ │    AiJob     │
    │(M-AI-02-05)  │ │(M-AI-02-08)  │ │ (parentJob)  │
    └──────────────┘ └──────────────┘ └──────────────┘
            │
            N:1 (logical, no FK)
    ┌──────────────┐
    │ AiAnalysis-  │  旧系统 A保留至 M-AI-04
    │ Result       │
    └──────────────┘

非目标

  • 不创建新通用 Job 主表
  • 不修改 AiRuntimeJob / AiRuntimeResult0 行M-AI-08 退场)
  • 不修改 AiAnalysisResult(保留至 M-AI-04
  • 不删除任何现有列(仅 rename completedAtfinishedAterrorMessageinternalErrorMessage
  • 不迁移历史数据9 行回填 trivial

验收

  • 包含最终 Prisma 草图§2.1
  • 包含 DDL 草图§7
  • 包含状态映射表§3
  • 包含新旧字段读写矩阵§4
  • 包含实体关系图§10
  • 包含零停机发布流程§8
  • 包含回滚流程§9
  • 索引评估§5
  • 删除策略§6
  • 基于 M-AI-02-01 审计结论MiniMax 已到期、数据量 9 行、FK 全 RESTRICT/CASCADE