docs(M-AI-02-GATE): update ADR-002 lifecycle spec + @map strategy
Some checks failed
Deploy API Server / build-and-deploy (push) Failing after 24s

- 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>
This commit is contained in:
wangdl 2026-06-20 12:40:26 +08:00
parent 98e442e666
commit 4c2b8d02d7

View File

@ -31,6 +31,7 @@ M-AI-02-01 审计确认:
| AiRuntimeJob 表 | **不删除、不修改、不迁移** | 0 行生产数据M-AI-08 退场时一并清理 | | AiRuntimeJob 表 | **不删除、不修改、不迁移** | 0 行生产数据M-AI-08 退场时一并清理 |
| AiRuntimeResult 表 | **不修改** | M-AI-03 统一 Job Engine 后用新的 Result 模型 | | AiRuntimeResult 表 | **不修改** | M-AI-03 统一 Job Engine 后用新的 Result 模型 |
| AiAnalysisResult 表 | **不修改** | 保留旧系统 A 的结果存储M-AI-04 迁移后评估删除 | | AiAnalysisResult 表 | **不修改** | 保留旧系统 A 的结果存储M-AI-04 迁移后评估删除 |
| 列重命名策略 | **Prisma 逻辑名用新名,物理列用 `@map` 保持旧名** | `internalErrorMessage @map("errorMessage")``finishedAt @map("completedAt")`。旧版本应用通过物理列名兼容,新代码通过逻辑名访问 |
### 2. 统一 AiJob Schema ### 2. 统一 AiJob Schema
@ -188,30 +189,42 @@ model AiJob {
| `sessionId` | `sessionId` | VARCHAR(191) | ✅ | - | 100% (9/9) | 保留但废弃(旧系统 AM-AI-05 删除 | | `sessionId` | `sessionId` | VARCHAR(191) | ✅ | - | 100% (9/9) | 保留但废弃(旧系统 AM-AI-05 删除 |
| `answerId` | `answerId` | VARCHAR(191) | ✅ | - | 78% (7/9) | 保留但废弃(旧系统 AM-AI-05 删除 | | `answerId` | `answerId` | VARCHAR(191) | ✅ | - | 78% (7/9) | 保留但废弃(旧系统 AM-AI-05 删除 |
### 3. 状态映射表 ### 3. 统一生命周期状态
两套旧状态统一为单一 `lifecycleStatus` 新 lifecycleStatus 枚举8 个值)
| 旧 AiAnalysisJob.status | 旧 AiRuntimeJob.status | 新 lifecycleStatus | 含义 | 进入条件 | 退出条件 | | 状态 | 含义 |
|------------------------|----------------------|-------------------|------|---------|---------| |------|------|
| `pending` | `pending` | **`pending`** | 等待消费 | API 创建 / retryable fail 回退 | 被 Runtime lock / BullMQ claim | | `created` | Job 记录已创建,尚未入队 |
| - | `locked` | **`locked`** | 已被 Runtime 获取,等待执行确认 | Runtime POST /lock 成功 | heartbeat → running / lockUntil 超时 | | `queued` | 已入队,等待 Worker 消费 |
| `processing` | `running` | **`running`** | 正在执行 | heartbeat 确认(新)/ Worker process() 开始(旧) | 完成 → succeeded / 失败 → failed | | `running` | Worker 正在执行 |
| `completed` | `succeeded` | **`succeeded`** | 执行成功 | API POST /result | 终态 | | `retrying` | 执行失败,等待重试 |
| `failed` | `failed` | **`failed`** | 执行失败 | 不可重试错误 / 超过 maxAttempts | 终态Admin 可重跑) | | `succeeded` | 执行成功(终态) |
| - | `cancelled` | **`cancelled`** | 已取消 | 用户/Admin 取消 pending job / cancelRequested 后 Runtime 确认 | 终态 | | `failed` | 执行失败,超过最大重试(终态) |
| - | `expired` | **取消**(合并入 retry 逻辑) | - | lockUntil 超时 → back to `pending`Reaper 处理) | - | | `cancel_requested` | 用户/Admin 请求取消,等待 Worker 确认 |
| `cancelled` | 已取消(终态) |
> **为什么 `processing` → `running` 而非 `locked`**:旧系统的 `processing` 表示 Worker 正在执行(`ai-analysis.worker.ts:48`),语义等同于新系统 heartbeat 确认后的 `running`。新系统的 `locked` 是「已被 claim 但尚未确认执行开始」的中间态,在旧系统中由 BullMQ 内部管理,无显式状态对应。 **Legacy → 统一映射**Shadow Write + 回填):
>
> **Reaper 安全影响**:若映射为 `locked`JobReaperService 会将旧 Worker 正在执行的任务视为「无 heartbeat 的 locked job」而在 `lockUntil` 过期后错误清扫回 `pending`,导致重复执行。映射为 `running` 可避免此问题Reaper 对 `running` 的清扫阈值远长于 `locked`)。 | 旧 AiAnalysisJob.status | 旧 AiRuntimeJob.status | 新 lifecycleStatus |
|------------------------|----------------------|-------------------|
| `pending` | `pending` | **`queued`** |
| `processing` | `running` | **`running`** |
| `completed` | `succeeded` | **`succeeded`** |
| `failed` | `failed` | **`failed`** |
| - | `locked` | N/A新系统内部状态旧系统无对应 |
| - | `cancelled` | **`cancelled`** |
> **为什么 `pending` → `queued`**:旧系统的 `pending` 表示「已创建且在 BullMQ 队列中等待」,语义更接近新系统的 `queued`。新系统的 `created` 预留给未来「创建记录」和「入队」分离的场景。
> **`lifecycleStatus` 过渡期 nullable**:旧代码写入时通过 Shadow Write 同步填充M-AI-02-10 回填历史 NULLM-AI-05 切换完成后改为 NOT NULL。
**迁移回填 SQL**(旧 `status` → 新 `lifecycleStatus` **迁移回填 SQL**(旧 `status` → 新 `lifecycleStatus`
```sql ```sql
UPDATE `AiAnalysisJob` UPDATE `AiAnalysisJob`
SET `lifecycleStatus` = CASE `status` SET `lifecycleStatus` = CASE `status`
WHEN 'pending' THEN 'pending' WHEN 'pending' THEN 'queued'
WHEN 'processing' THEN 'running' WHEN 'processing' THEN 'running'
WHEN 'completed' THEN 'succeeded' WHEN 'completed' THEN 'succeeded'
WHEN 'failed' THEN 'failed' WHEN 'failed' THEN 'failed'