# M-AI-02-01:现有 AI Job Schema 审计 > 审计日期:2026-06-20 > 审计人:wangdl(半自动化审计:代码扫描 + 生产 MySQL 只读查询 via SSH 隧道) --- ## 1. Schema 逐字段审计 ### 1.1 AiAnalysisJob(旧系统 A) 物理表名:`AiAnalysisJob` | 创建于:`prisma/migrations/20250516000000_init/migration.sql:300-319` | # | Prisma 字段名 | 物理列名 | DB 类型 | Nullable | Default | Index | Unique | FK | onDelete | onUpdate | |---|-------------|---------|---------|----------|---------|-------|--------|-----|----------|----------| | 1 | `id` | `id` | VARCHAR(191) | ❌ | `cuid()` | PK | ✅ | - | - | - | | 2 | `userId` | `userId` | VARCHAR(191) | ❌ | - | `AiAnalysisJob_userId_idx` | ❌ | `User.id` | RESTRICT | CASCADE | | 3 | `sessionId` | `sessionId` | VARCHAR(191) | ✅ | - | `AiAnalysisJob_sessionId_idx` | ❌ | - | - | - | | 4 | `answerId` | `answerId` | VARCHAR(191) | ✅ | - | ❌ | ❌ | - | - | - | | 5 | `jobType` | `jobType` | VARCHAR(32) | ❌ | - | ❌ | ❌ | - | - | - | | 6 | `status` | `status` | VARCHAR(32) | ❌ | `'pending'` | `AiAnalysisJob_status_idx` | ❌ | - | - | - | | 7 | `progress` | `progress` | INT | ❌ | `0` | ❌ | ❌ | - | - | - | | 8 | `errorMessage` | `errorMessage` | TEXT | ✅ | - | ❌ | ❌ | - | - | - | | 9 | `queuedAt` | `queuedAt` | DATETIME(3) | ✅ | - | ❌ | ❌ | - | - | - | | 10 | `startedAt` | `startedAt` | DATETIME(3) | ✅ | - | ❌ | ❌ | - | - | - | | 11 | `completedAt` | `completedAt` | DATETIME(3) | ✅ | - | ❌ | ❌ | - | - | - | | 12 | `createdAt` | `createdAt` | DATETIME(3) | ❌ | `now()` | ❌ | ❌ | - | - | - | | 13 | `updatedAt` | `updatedAt` | DATETIME(3) | ❌ | `@updatedAt` | ❌ | ❌ | - | - | - | **Relation**:`results` → `AiAnalysisResult[]`(一对多) --- ### 1.2 AiAnalysisResult(旧系统 A 输出) 物理表名:`AiAnalysisResult` | 创建于:`prisma/migrations/20250516000000_init/migration.sql:322-342` | # | Prisma 字段名 | 物理列名 | DB 类型 | Nullable | Default | Index | Unique | FK | onDelete | onUpdate | |---|-------------|---------|---------|----------|---------|-------|--------|-----|----------|----------| | 1 | `id` | `id` | VARCHAR(191) | ❌ | `cuid()` | PK | ✅ | - | - | - | | 2 | `userId` | `userId` | VARCHAR(191) | ❌ | - | `AiAnalysisResult_userId_idx` | ❌ | `User.id` | RESTRICT | CASCADE | | 3 | `jobId` | `jobId` | VARCHAR(191) | ❌ | - | `AiAnalysisResult_jobId_idx` | ❌ | `AiAnalysisJob.id` | RESTRICT | CASCADE | | 4 | `sessionId` | `sessionId` | VARCHAR(191) | ✅ | - | `AiAnalysisResult_sessionId_idx` | ❌ | - | - | - | | 5 | `answerId` | `answerId` | VARCHAR(191) | ✅ | - | ❌ | ❌ | - | - | - | | 6 | `summary` | `summary` | TEXT | ✅ | - | ❌ | ❌ | - | - | - | | 7 | `masteryScore` | `masteryScore` | INT | ✅ | - | ❌ | ❌ | - | - | - | | 8 | `strengths` | `strengths` | JSON | ✅ | - | ❌ | ❌ | - | - | - | | 9 | `weaknesses` | `weaknesses` | JSON | ✅ | - | ❌ | ❌ | - | - | - | | 10 | `suggestions` | `suggestions` | JSON | ✅ | - | ❌ | ❌ | - | - | - | | 11 | `nextActions` | `nextActions` | JSON | ✅ | - | ❌ | ❌ | - | - | - | | 12 | `rawResult` | `rawResult` | JSON | ✅ | - | ❌ | ❌ | - | - | - | | 13 | `createdAt` | `createdAt` | DATETIME(3) | ❌ | `now()` | ❌ | ❌ | - | - | - | | 14 | `updatedAt` | `updatedAt` | DATETIME(3) | ❌ | `@updatedAt` | ❌ | ❌ | - | - | - | **Relation**:`job` → `AiAnalysisJob`(多对一) --- ### 1.3 AiRuntimeJob(新系统 B) 物理表名:`AiRuntimeJob` | 无独立 migration SQL,FK 由 Prisma schema push 创建,生产已确认存在 | # | Prisma 字段名 | 物理列名 | DB 类型 | Nullable | Default | Index | Unique | FK | onDelete | onUpdate | |---|-------------|---------|---------|----------|---------|-------|--------|-----|----------|----------| | 1 | `id` | `id` | VARCHAR(191) | ❌ | `cuid()` | PK | ✅ | - | - | - | | 2 | `userId` | `userId` | VARCHAR(191) | ❌ | - | `AiRuntimeJob_userId_idx` | ❌ | `User.id` | RESTRICT | CASCADE | | 3 | `jobType` | `jobType` | VARCHAR(64) | ❌ | - | `AiRuntimeJob_jobType_idx` | ❌ | - | - | - | | 4 | `targetType` | `targetType` | VARCHAR(32) | ❌ | - | 复合 `AiRuntimeJob_targetType_targetId_idx` | ❌ | - | - | - | | 5 | `targetId` | `targetId` | VARCHAR(255) | ❌ | - | 同上 | ❌ | - | - | - | | 6 | `snapshotId` | `snapshotId` | VARCHAR(191) | ✅ | - | ❌ | ❌ | - | - | - | | 7 | `status` | `status` | VARCHAR(32) | ❌ | `'pending'` | `AiRuntimeJob_status_idx` | ❌ | - | - | - | | 8 | `priority` | `priority` | INT | ❌ | `0` | ❌ | ❌ | - | - | - | | 9 | `idempotencyKey` | `idempotencyKey` | VARCHAR(255) | ✅ | - | ❌ | ✅ | - | - | - | | 10 | `apiKeyMode` | `apiKeyMode` | VARCHAR(32) | ❌ | `'platform_key'` | ❌ | ❌ | - | - | - | | 11 | `credentialId` | `credentialId` | VARCHAR(191) | ✅ | - | ❌ | ❌ | - | - | - | | 12 | `modelProvider` | `modelProvider` | VARCHAR(32) | ❌ | `'deepseek'` | ❌ | ❌ | - | - | - | | 13 | `modelName` | `modelName` | VARCHAR(64) | ❌ | `'deepseek-chat'` | ❌ | ❌ | - | - | - | | 14 | `promptVersion` | `promptVersion` | VARCHAR(100) | ✅ | - | ❌ | ❌ | - | - | - | | 15 | `outputSchemaVersion` | `outputSchemaVersion` | VARCHAR(100) | ✅ | - | ❌ | ❌ | - | - | - | | 16 | `attemptNo` | `attemptNo` | INT | ❌ | `0` | ❌ | ❌ | - | - | - | | 17 | `retriedFromJobId` | `retriedFromJobId` | VARCHAR(255) | ✅ | - | ❌ | ❌ | - | - | - | | 18 | `lockedBy` | `lockedBy` | VARCHAR(100) | ✅ | - | ❌ | ❌ | - | - | - | | 19 | `lockedAt` | `lockedAt` | DATETIME(3) | ✅ | - | ❌ | ❌ | - | - | - | | 20 | `lockUntil` | `lockUntil` | DATETIME(3) | ✅ | - | `AiRuntimeJob_lockUntil_idx` | ❌ | - | - | - | | 21 | `startedAt` | `startedAt` | DATETIME(3) | ✅ | - | ❌ | ❌ | - | - | - | | 22 | `finishedAt` | `finishedAt` | DATETIME(3) | ✅ | - | ❌ | ❌ | - | - | - | | 23 | `retryCount` | `retryCount` | INT | ❌ | `0` | ❌ | ❌ | - | - | - | | 24 | `maxRetryCount` | `maxRetryCount` | INT | ❌ | `3` | ❌ | ❌ | - | - | - | | 25 | `timeoutSeconds` | `timeoutSeconds` | INT | ❌ | `120` | ❌ | ❌ | - | - | - | | 26 | `errorCode` | `errorCode` | VARCHAR(100) | ✅ | - | ❌ | ❌ | - | - | - | | 27 | `errorMessage` | `errorMessage` | TEXT | ✅ | - | ❌ | ❌ | - | - | - | | 28 | `cancelRequestedAt` | `cancelRequestedAt` | DATETIME(3) | ✅ | - | ❌ | ❌ | - | - | - | | 29 | `cancelledAt` | `cancelledAt` | DATETIME(3) | ✅ | - | ❌ | ❌ | - | - | - | | 30 | `createdAt` | `createdAt` | DATETIME(3) | ❌ | `now()` | ❌ | ❌ | - | - | - | | 31 | `updatedAt` | `updatedAt` | DATETIME(3) | ❌ | `@updatedAt` | ❌ | ❌ | - | - | - | **Relation**:`result` → `AiRuntimeResult?`(一对一) --- ### 1.4 AiRuntimeResult(新系统 B 输出) 物理表名:`AiRuntimeResult` | 无独立 migration SQL,FK 由 Prisma schema push 创建,生产已确认存在 | # | Prisma 字段名 | 物理列名 | DB 类型 | Nullable | Default | Index | Unique | FK | onDelete | onUpdate | |---|-------------|---------|---------|----------|---------|-------|--------|-----|----------|----------| | 1 | `id` | `id` | VARCHAR(191) | ❌ | `cuid()` | PK | ✅ | - | - | - | | 2 | `jobId` | `jobId` | VARCHAR(191) | ❌ | - | `AiRuntimeResult_jobId_idx` | ✅ | `AiRuntimeJob.id` | RESTRICT | CASCADE | | 3 | `userId` | `userId` | VARCHAR(191) | ❌ | - | `AiRuntimeResult_userId_idx` | ❌ | `User.id` | RESTRICT | CASCADE | | 4 | `runtimeInstanceId` | `runtimeInstanceId` | VARCHAR(100) | ❌ | - | ❌ | ❌ | - | - | - | | 5 | `status` | `status` | VARCHAR(32) | ❌ | - | ❌ | ❌ | - | - | - | | 6 | `attemptNo` | `attemptNo` | INT | ❌ | `0` | ❌ | ❌ | - | - | - | | 7 | `resultIdempotencyKey` | `resultIdempotencyKey` | VARCHAR(255) | ✅ | - | `AiRuntimeResult_resultIdempotencyKey_idx` | ❌ | - | - | - | | 8 | `outputHash` | `outputHash` | VARCHAR(255) | ✅ | - | ❌ | ❌ | - | - | - | | 9 | `rawOutput` | `rawOutput` | JSON | ✅ | - | ❌ | ❌ | - | - | - | | 10 | `validatedOutput` | `validatedOutput` | JSON | ✅ | - | ❌ | ❌ | - | - | - | | 11 | `schemaVersion` | `schemaVersion` | VARCHAR(100) | ❌ | - | ❌ | ❌ | - | - | - | | 12 | `validationErrors` | `validationErrors` | JSON | ✅ | - | ❌ | ❌ | - | - | - | | 13 | `createdAt` | `createdAt` | DATETIME(3) | ❌ | `now()` | ❌ | ❌ | - | - | - | | 14 | `updatedAt` | `updatedAt` | DATETIME(3) | ❌ | `@updatedAt` | ❌ | ❌ | - | - | - | --- ### 1.5 AiUsageLog(共享 AI 用量日志) 物理表名:`AiUsageLog` | 创建于:`prisma/migrations/20250518000000_add_objectkey_bucket_aiusage_waitlist/migration.sql:9-30`;FK 同文件 line 48 | # | Prisma 字段名 | 物理列名 | DB 类型 | Nullable | Default | Index | Unique | FK | onDelete | onUpdate | |---|-------------|---------|---------|----------|---------|-------|--------|-----|----------|----------| | 1 | `id` | `id` | VARCHAR(191) | ❌ | `cuid()` | PK | ✅ | - | - | - | | 2 | `userId` | `userId` | VARCHAR(191) | ❌ | - | `AiUsageLog_userId_idx` | ❌ | `User.id` | RESTRICT | CASCADE | | 3 | `feature` | `feature` | VARCHAR(64) | ❌ | - | `AiUsageLog_feature_idx` | ❌ | - | - | - | | 4 | `provider` | `provider` | VARCHAR(32) | ❌ | - | ❌ | ❌ | - | - | - | | 5 | `model` | `model` | VARCHAR(100) | ❌ | - | ❌ | ❌ | - | - | - | | 6 | `tier` | `tier` | VARCHAR(32) | ❌ | - | ❌ | ❌ | - | - | - | | 7 | `promptKey` | `promptKey` | VARCHAR(128) | ❌ | - | ❌ | ❌ | - | - | - | | 8 | `promptVersion` | `promptVersion` | VARCHAR(32) | ❌ | - | ❌ | ❌ | - | - | - | | 9 | `inputTokens` | `inputTokens` | INT | ❌ | `0` | ❌ | ❌ | - | - | - | | 10 | `outputTokens` | `outputTokens` | INT | ❌ | `0` | ❌ | ❌ | - | - | - | | 11 | `estimatedCost` | `estimatedCost` | DOUBLE | ❌ | `0` | ❌ | ❌ | - | - | - | | 12 | `latencyMs` | `latencyMs` | INT | ❌ | `0` | ❌ | ❌ | - | - | - | | 13 | `success` | `success` | BOOLEAN | ❌ | `true` | ❌ | ❌ | - | - | - | | 14 | `errorMessage` | `errorMessage` | VARCHAR(500) | ✅ | - | ❌ | ❌ | - | - | - | | 15 | `createdAt` | `createdAt` | DATETIME(3) | ❌ | `now()` | `AiUsageLog_createdAt_idx` | ❌ | - | - | - | --- ### 1.6 FK 约束来源:Prisma 隐式默认 vs 显式声明 全部 7 个 `@relation` 在 Prisma schema 中均**未显式声明** `onDelete` / `onUpdate`: ```prisma // 典型模式(无显式 onDelete/onUpdate) user User @relation(fields: [userId], references: [id]) job AiAnalysisJob @relation(fields: [jobId], references: [id]) ``` 生产数据库 `INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS` 确认全部 7 个 FK 均为 `RESTRICT / CASCADE`,这与 Prisma 默认行为一致: - Prisma 默认 `onDelete: Restrict` - Prisma 默认 `onUpdate: Cascade` **对 M-AI-02 的影响**: 1. **M-AI-02-03/04 新增 FK 时应显式声明**:避免依赖 Prisma 默认值(不同版本/方言可能不同),在 schema 中显式写出 `onDelete: Restrict, onUpdate: Cascade` 以保持全表一致 2. **AiUsageLog.jobId FK(M-AI-02-08)**:新增 FK 应显式声明,**但需评估 `onDelete: SetNull` vs `Restrict`**——删除 Job 时 UsageLog 应保留以维持计费审计完整性 3. **OutboxEvent FK(M-AI-02-07)**:新增 Outbox 表的 FK 应显式声明,通常为 `onDelete: Cascade`(Job 删除时级联删 Outbox) --- ## 2. 状态值枚举 ### 2.1 AiAnalysisJob.status 无 enum 定义,使用任意 string。实际使用值: | 值 | 使用位置 | 说明 | |----|---------|------| | `pending` | `ai-analysis.repository.ts:15` | 创建时默认 | | `processing` | `ai-analysis.repository.ts:23`, `ai-analysis.worker.ts:48` | Worker 开始处理 | | `completed` | `ai-analysis.repository.ts:24`, `ai-analysis.worker.ts:68` | Worker 处理成功 | | `failed` | `ai-analysis.repository.ts:24`, `ai-analysis.worker.ts:102` | Worker 处理失败 | | `queued` | `ai-analysis.service.ts:30,51` | API 返回给客户端的虚拟状态(不写入 DB) | ### 2.2 AiRuntimeJob.status 无 enum 定义,使用任意 string。实际使用值: | 值 | 使用位置 | 说明 | |----|---------|------| | `pending` | `user-ai.service.ts:289,315,331`, `runtime-internal.service.ts:27,92,594` | 创建/重试回退 | | `locked` | `runtime-internal.service.ts:99,123` | Runtime 获取锁 | | `running` | `runtime-internal.service.ts:129,131`, `job-reaper.service.ts:40,54`, `platform-budget.service.ts:55` | 执行中 | | `succeeded` | `runtime-internal.service.ts:268` | 执行成功 | | `failed` | `runtime-internal.service.ts:605`, `job-reaper.service.ts:98`, `submitFailure()` | 执行失败/超时终止 | | `cancelled` | `user-ai.service.ts:353`, `runtime-internal.service.ts:583` | 用户取消 | | `expired` | `docs/ai-job-state-machine.md:23` | 文档定义,代码中未显式赋此值 | ### 2.3 AiRuntimeResult.status | 值 | 说明 | |----|------| | `succeeded` | 最终成功(代码中 `runtime-internal.service.ts:249` 检查) | | 其他值未明确定义 | `submitResult()` 接受任意 `dto.status` | --- ## 3. JobType 枚举 ### 3.1 AiAnalysisJob.jobType(旧系统 A) | 值 | 使用位置 | 说明 | |----|---------|------| | `active-recall` | `ai-analysis.service.ts:19` | 主动回忆分析 | | `feynman-evaluation` | `ai-analysis.service.ts:40` | 费曼评估分析 | ### 3.2 AiRuntimeJob.jobType(新系统 B) | 值 | 使用位置 | 说明 | |----|---------|------| | `learning_state_analysis` | `user-ai.dto.ts:64` | 学习状态分析 | | `weak_point_analysis` | `user-ai.dto.ts:65` | 薄弱点分析 | | `next_action_planning` | `user-ai.dto.ts:66` | 下一步建议 | | `quiz_generation` | `user-ai.dto.ts:67` | 题目生成 | | `flashcard_generation` | `user-ai.dto.ts:68` | 卡片生成 | **关键发现**:两套系统的 jobType 互不重叠,不存在同名冲突。 --- ## 4. 生产数据审计 > 查询时间:2026-06-20 03:14 UTC > 数据库:zhixi_prod @ 120.53.227.155:3306(蜂驰云 8C 生产服务器,SSH 隧道连接) > MySQL 版本:8.0.46 > 查询脚本:`scripts/query-audit-data.ts` ### 4.1 AiAnalysisJob | 指标 | 值 | |------|-----| | **总记录数** | 9 | | 表大小 | 0.06 MB | | 最早记录 | 2026-06-19T14:34:05.815Z | | 最新记录 | 2026-06-19T14:48:10.489Z | #### 状态分布 | 状态 | 数量 | 占比 | |------|------|------| | `completed` | 5 | 56% | | `failed` | 4 | 44% | #### jobType 分布 | jobType | 数量 | |---------|------| | `feynman-evaluation` | 7 | | `active-recall` | 2 | #### NULL 字段分布 | 字段 | NULL 数 | NULL 率 | |------|---------|---------| | `sessionId` | 9 | 100% | | `answerId` | 7 | 78% | | `errorMessage` | 5 | 56%(4 failed jobs 均有 errorMessage) | | `queuedAt` | 0 | 0% | | `startedAt` | 0 | 0% | | `completedAt` | 0 | 0% — 正确。`AiAnalysisRepository.updateJobStatus()` 对 `completed`/`failed` 均显式设置 `completedAt = new Date()`(`ai-analysis.repository.ts:23-24`)。当前全部 9 行均为终态(5 `completed` + 4 `failed`),无 `pending`/`processing` 滞留,故 0 NULL 符合预期。`DateTime?` 类型不生成 `DEFAULT CURRENT_TIMESTAMP`,Prisma `@updatedAt` 也不影响此列。 | #### 最大字段长度 | 字段 | 最大长度 | |------|----------| | `errorMessage` | 797 chars | ### 4.2 AiAnalysisResult | 指标 | 值 | |------|-----| | **总记录数** | 5 | | 表大小 | 0.06 MB | | 孤儿 FK(jobId 不存在) | 0 | | 重复 (userId, jobId) | 0 | #### 最大 JSON 字段长度 | 字段 | 最大长度 | |------|----------| | `summary` | 162 chars | | `strengths` | 61 chars | | `weaknesses` | 184 chars | | `suggestions` | 367 chars | | `nextActions` | 185 chars | | `rawResult` | 1,048 chars | #### 父 Job 状态分布 | 父 Job 状态 | Result 数量 | |-----------|------------| | `completed` | 5 | > 结论:所有 5 个 Result 均属于 completed Job,无 failed Job 产生 Result(符合代码逻辑)。 #### NULL 字段 | 字段 | NULL 数 | |------|---------| | `sessionId` | 5 (100%) | | `answerId` | 5 (100%) | | `summary` / `masteryScore` / `strengths` / etc. | 0 | ### 4.3 AiRuntimeJob | 指标 | 值 | |------|-----| | **总记录数** | **0** | | 表大小 | 0.11 MB(空表,空间为 initial extent 分配) | > **确认**:ADR "唯一创建 AiRuntimeJob 的入口无人调用" 的断言正确。该表在生产中为零记录。 ### 4.4 AiRuntimeResult | 指标 | 值 | |------|-----| | **总记录数** | **0** | | 表大小 | 0.08 MB(空表) | ### 4.5 AiUsageLog | 指标 | 值 | |------|-----| | **总记录数** | 110 | | 表大小 | 0.11 MB | | 最早记录 | 2026-05-28T13:02:12.261Z | | 最新记录 | 2026-06-19T14:52:40.199Z | | success=true | 30 (27.3%) | | success=false | 80 (72.7%) | | errorMessage NULL | 30(仅 success=true 的日志无错误消息) | #### feature 分布 | feature | 总数 | 成功 | 失败 | 成功率 | |---------|------|------|------|--------| | `learning-trend` | 70 | 11 | 59 | 15.7% | | `knowledge-import` | 15 | 1 | 14 | 6.7% | | `rag-chat` | 13 | 12 | 1 | 92.3% | | `active-recall-analysis` | 7 | 1 | 6 | 14.3% | | `feynman-evaluation` | 4 | 4 | 0 | 100% | | `review-card-generation` | 1 | 1 | 0 | 100% | #### provider 分布 | provider | 数量 | |----------|------| | `minimax` | 59 | | `deepseek` | 51 | #### model 分布 | model | 数量 | |-------|------| | `minimax-m2.7` | 59 | **全部来自已到期 API Key(2026-06-06 到期)** | | `deepseek-v4-pro` | 35 | 当前主力模型 | | `deepseek-v4-flash` | 16 | 轻量兜底 | #### AiUsageLog 成功率根因分析 107/110 的失败率(72.7%)由以下因素叠加导致: 1. **MiniMax M2.7 API Key 已到期**(2026-06-06 到期,见 devops `轻量云服务器凭据.md`) - `minimax-m2.7` 共 59 次调用,失败 59 次(100% 失败率) - 影响 feature:`learning-trend`(59 次全部 minimax)、`knowledge-import`(14/15 次失败含大量 minimax) 2. **`learning-trend`**(70 次,成功率 16%)是后台定时任务 → 高调用量 × minimax 到期 = 大量失败 3. **`rag-chat`**(13 次,成功率 92%)使用 `deepseek-v4-pro` → 几乎全部成功 4. **`feynman-evaluation`**(4 次,成功率 100%)使用 `deepseek-v4-pro` → 全部成功 **结论**:排除 Minimax 到期影响后,DeepSeek 成功率接近 100%。2026-06-06 后所有调用已切换至 DeepSeek V4 Pro/V4 Flash(蜂驰云生产凭据确认),M-AI-02 无需为 Minimax 保留兼容。 #### 孤儿外键 | 检测 | SQL | 结果 | |------|-----|------| | `AiAnalysisResult.jobId` → `AiAnalysisJob.id` | `SELECT COUNT(*) FROM AiAnalysisResult r LEFT JOIN AiAnalysisJob j ON r.jobId=j.id WHERE j.id IS NULL` | 0 | | `AiAnalysisResult.userId` → `User.id` | 同上模式 | 0 | | `AiAnalysisJob.userId` → `User.id` | 同上模式 | 0 | | `AiUsageLog.userId` → `User.id` | 同上模式(有 FK RESTRICT,不可能存在) | 0 | #### 重复业务记录 **定义**:重复 = 同一用户对同一 jobType 在 **1 秒内** 创建 ≥2 条记录(并发创建)。 | 检测模式 | SQL | 结果 | |---------|-----|------| | `(userId, jobType)` 1秒并发 | `SELECT a.id,b.id FROM AiAnalysisJob a JOIN AiAnalysisJob b ON a.userId=b.userId AND a.jobType=b.jobType WHERE a.id1 WHERE sessionId IS NOT NULL` | 0(sessionId 100% NULL 导致) | | `(userId, jobId)` 在 AiAnalysisResult | `GROUP BY userId,jobId HAVING cnt>1` | 0 | | AiUsageLog 同秒双写 | `GROUP BY userId,feature,provider,model,createdAt HAVING cnt>1` | 0 | **发现的并发重复详情**: ``` id1=cmql14u46007nwfzktt0ttsn0 id2=cmql14uey007rwfzkl9uf0ssz jobType=feynman-evaluation status=completed/completed createdAt=2026-06-19T14:34:05Z / 14:34:06Z diff=0s ``` **评估**:两个 Job 均在 1 秒内创建,jobType 相同(`feynman-evaluation`),最终状态均为 `completed`。这是 `AiAnalysisService.evaluateFeynman()` 的并发调用导致。**AiAnalysisJob 表无 (userId, sessionId, jobType) 唯一约束**,无法在数据库层防御并发重复。M-AI-02-03 引入 `idempotencyKey` 后可通过业务层幂等解决。 #### JSON/TEXT 字段最大容量 **检测方法**:`SELECT MAX(LENGTH(column)) FROM table` | 表 | 字段 | 类型 | 当前最大 | 检测 SQL | |----|------|------|---------|----------| | `AiAnalysisJob` | `errorMessage` | TEXT | **797 chars** | `SELECT MAX(LENGTH(errorMessage)) FROM AiAnalysisJob` | | `AiAnalysisResult` | `rawResult` | JSON | **1,048 chars** | `SELECT MAX(LENGTH(rawResult)) FROM AiAnalysisResult` | | `AiAnalysisResult` | `strengths` | JSON | 61 | 同上 | | `AiAnalysisResult` | `weaknesses` | JSON | 184 | 同上 | | `AiAnalysisResult` | `suggestions` | JSON | **367 chars** | 同上 | | `AiAnalysisResult` | `nextActions` | JSON | 185 | 同上 | | `AiAnalysisResult` | `summary` | TEXT | 162 | 同上 | | `AiRuntimeJob` | `errorMessage` | TEXT | 0(零记录) | `SELECT MAX(LENGTH(errorMessage)) FROM AiRuntimeJob` | | `AiRuntimeResult` | `rawOutput` | JSON | NULL(零记录) | `SELECT MAX(LENGTH(rawOutput)) FROM AiRuntimeResult` | | `AiRuntimeResult` | `validatedOutput` | JSON | NULL(零记录) | 同上 | **M-AI-02-04 容量建议**: - 当前 JSON/TEXT 字段均在 1KB 级别,远低于 MEDIUMTEXT(16MB)上限 - `rawResult` 最大 1KB → M-AI-02-04 新增的 `AiJobSnapshot` 需要容纳完整的 snapshot JSON(约 5-10KB),建议 `MEDIUMTEXT` 或 `JSON`(MySQL JSON 列上限 1GB,但索引友好) - `AiAnalysisJob.errorMessage` 最大 797 chars → 新建 `AiJob.errorMessage` 保持 `TEXT`(64KB)即可,无需 `MEDIUMTEXT` --- ## 5. 调用方完整清单 ### 5.1 `prisma.aiAnalysisJob` 直接访问 | 文件 | 行号 | 操作 | 说明 | |------|------|------|------| | `src/modules/ai-analysis/ai-analysis.repository.ts` | 9 | `create` | 创建 Job | | `src/modules/ai-analysis/ai-analysis.repository.ts` | 26 | `update` | 更新 Job 状态 | | `src/modules/ai-analysis/ai-analysis.repository.ts` | 30 | `findUnique` | 查询 Job(include results) | ### 5.2 `prisma.aiAnalysisResult` 直接访问 | 文件 | 行号 | 操作 | 说明 | |------|------|------|------| | `src/modules/ai-analysis/ai-analysis.repository.ts` | 37 | `create` | 创建分析结果 | | `src/modules/ai-analysis/ai-analysis.repository.ts` | 53 | `findUnique` | 查询分析结果 | | `src/modules/learning-session/admin-learning.service.ts` | 229 | `findMany` | Admin 分页查询 | | `src/modules/learning-session/admin-learning.service.ts` | 233 | `count` | Admin 总数 | ### 5.3 `prisma.aiRuntimeJob` 直接访问 | 文件 | 行号 | 操作 | 说明 | |------|------|------|------| | `src/modules/ai-runtime/user-ai.service.ts` | 226 | `findUnique` | 幂等检查 | | `src/modules/ai-runtime/user-ai.service.ts` | 282 | `create` | 创建 Job | | `src/modules/ai-runtime/user-ai.service.ts` | 344 | `findFirst` | 取消查询 | | `src/modules/ai-runtime/user-ai.service.ts` | 351,359 | `update` | 取消更新 | | `src/modules/ai-runtime/user-ai.service.ts` | 429 | `findFirst` | 状态查询 | | `src/modules/ai-runtime/user-ai.service.ts` | 448 | `findMany` | 列表查询 | | `src/modules/ai-runtime/internal/runtime-internal.service.ts` | 36 | `findMany` | Poll 查询 | | `src/modules/ai-runtime/internal/runtime-internal.service.ts` | 89 | `updateMany` | Lock | | `src/modules/ai-runtime/internal/runtime-internal.service.ts` | 123,129 | `updateMany` | Heartbeat | | `src/modules/ai-runtime/internal/runtime-internal.service.ts` | 139,154,202,225,576,661 | `findUnique` | Snapshot/Result/Fail 查询 | | `src/modules/ai-runtime/internal/runtime-internal.service.ts` | 192,268,583,594,605 | `update` | Snapshot/Result/Fail 更新 | | `src/modules/ai-runtime/job-reaper.service.ts` | 28 | `updateMany` | 清扫过期锁 | | `src/modules/ai-runtime/job-reaper.service.ts` | 39,69 | `findMany` | 查询 stuck/expired | | `src/modules/ai-runtime/job-reaper.service.ts` | 53,82,96 | `updateMany` | 清扫失败/过期 | | `src/modules/ai-runtime/platform-budget.service.ts` | 53 | `count` | 平台预算检查 | ### 5.4 `prisma.aiUsageLog` 直接访问 | 文件 | 行号 | 操作 | 说明 | |------|------|------|------| | `src/modules/ai/usage/ai-usage-log.service.ts` | 28 | `create` | 写日志 | | `src/modules/admin-costs/cost-aggregation.service.ts` | 31 | `findMany` | 成本聚合 | | `src/modules/admin-metrics/admin-metrics.controller.ts` | 56 | `findMany` | AI 耗时统计 | | `src/modules/admin-dashboard/admin-dashboard.service.ts` | 46 | `count` | 今日 AI 调用数 | | `src/modules/admin-costs/admin-costs.service.ts` | 85 | `groupBy` | 成本统计 | | `src/modules/learning-session/admin-learning.service.ts` | 247,251 | `findMany`/`count` | Admin 查询 | ### 5.5 间接调用方(通过 Repository/Service) | 调用方 | 文件 | 说明 | |--------|------|------| | `AiAnalysisWorker.process()` | `src/workers/ai-analysis.worker.ts:48-102` | 通过 `AiAnalysisRepository` 更新状态和结果 | | `AiAnalysisService.analyze()` | `src/modules/ai-analysis/ai-analysis.service.ts:12-31` | 创建 Job 并入队 | | `AiAnalysisService.evaluateFeynman()` | `src/modules/ai-analysis/ai-analysis.service.ts:33-52` | 创建 Job 并入队 | | `AiAnalysisService.getJobStatus()` | `src/modules/ai-analysis/ai-analysis.service.ts:54-67` | 查询状态(序列化给客户端) | | `AdminLearningService.getAnalysis()` | `src/modules/learning-session/admin-learning.service.ts:223-236` | Admin 分页查询 | | `AdminLearningService.getAiUsage()` | `src/modules/learning-session/admin-learning.service.ts:240-254` | Admin 分页查询 | | `AiUsageLogService.log()` | `src/modules/ai/usage/ai-usage-log.service.ts:26-32` | 写用量日志 | | `CostAggregationService.aggregateToday()` | `src/modules/admin-costs/cost-aggregation.service.ts:27-59` | 成本聚合 | | `AdminMetricsController.aiMetrics()` | `src/modules/admin-metrics/admin-metrics.controller.ts:54-89` | AI 耗时统计 | | `AdminDashboardService.getStats()` | `src/modules/admin-dashboard/admin-dashboard.service.ts:46` | 仪表盘统计 | --- ## 6. 测试文件引用 | 文件 | 引用方式 | 影响 | |------|---------|------| | `test/helpers/integration-harness.ts:228` | 硬编码表名 `'AiAnalysisResult'`, `'AiAnalysisJob'` | **重命名 Prisma model 后需同步** | | `src/modules/ai-runtime/user-ai.service.spec.ts` | Mock `prisma.aiRuntimeJob.*` | 与 Prisma model 名无关(mock) | | `src/modules/ai-runtime/job-reaper.service.spec.ts` | Mock `prisma.aiRuntimeJob.*` | 与 Prisma model 名无关(mock) | | `src/modules/ai-runtime/platform-budget.service.spec.ts` | Mock `prisma.aiRuntimeJob.*` | 与 Prisma model 名无关(mock) | | `src/modules/ai-runtime/internal/runtime-internal.service.spec.ts` | Mock `prisma.aiRuntimeJob.*` | 与 Prisma model 名无关(mock) | | `test/api-e2e.worker-int-spec.ts` | Raw SQL 8 处 + 字符串引用 8 处 = **16 处**引用物理表名 `AiAnalysisJob` / `AiAnalysisResult` | M-AI-02-09 重命名后需全部同步 | SQL: 91,101,107,197,210,218,252,255;字符串: 88,105,108,195,215,219,253,256 | | `test/worker-integration.worker-int-spec.ts` | Raw SQL 13 处 + 字符串引用 8 处 = **21 处**引用物理表名 | M-AI-02-09 重命名后需全部同步 | SQL: 113,120,127,158,170,212,245,259,270,282,299,313,316;字符串: 110,114,118,121,125,247,314,317 | --- ## 7. 兼容风险分析 ### 7.1 Model 名依赖 **风险等级:中等** 以下代码依赖 Prisma model 名 `AiAnalysisJob`、`AiAnalysisResult`: | 依赖点 | 文件 | 行号 | 影响 | |--------|------|------|------| | `prisma.aiAnalysisJob` | `ai-analysis.repository.ts` | 9,26,30 | 重命名 model 会导致编译错误 | | `prisma.aiAnalysisResult` | `ai-analysis.repository.ts` | 37,53 | 同上 | | `prisma.aiAnalysisResult` | `admin-learning.service.ts` | 229,233 | 同上 | | 硬编码表名字符串 | `test/helpers/integration-harness.ts` | 228 | 清理表时使用物理表名 `'AiAnalysisJob'`、`'AiAnalysisResult'` | **注意**:`prisma.aiRuntimeJob`、`prisma.aiUsageLog` 不会被 M-AI-02 重命名(当前范围仅统一 `AiAnalysisJob → AiJob`)。 ### 7.2 状态 enum 依赖 **风险等级:低** - 所有状态值均为 raw string,无 enum 定义 - `AiAnalysisJob` 使用 `pending`/`processing`/`completed`/`failed` - `AiRuntimeJob` 使用 `pending`/`locked`/`running`/`succeeded`/`failed`/`cancelled` - 两套状态值语义不同,M-AI-02-04 需要统一 - 当前代码中没有任何 `AiAnalysisJobStatus` type/enum 定义 ### 7.3 API 直接序列化数据库状态 **风险等级:中等** | API | 文件 | 行号 | 序列化字段 | |-----|------|------|----------| | `AiAnalysisService.getJobStatus()` | `ai-analysis.service.ts:54-67` | 直接透传 `job.status`、`job.jobType`、`job.queuedAt`、`job.startedAt`、`job.completedAt` | **字段名变更会影响 iOS** | | `UserAiService.getJob()` | `user-ai.service.ts:428-443` | 选择字段直接序列化 `jobType`、`status`、`errorCode`、`errorMessage` 等 | **字段名变更会影响 iOS** | | `UserAiService.listJobs()` | `user-ai.service.ts:445-459` | 同上 | 同上 | | `UserAiService.createAnalysisJob()` 返回值 | `user-ai.service.ts:340` | `{ jobId, status, createdAt }` | 不受影响(仅返回 ID+状态) | ### 7.4 Relation 重命名影响 **风险等级:低** - `AiAnalysisJob.results` → `AiAnalysisResult[]`(系统 A) - `AiAnalysisResult.job` → `AiAnalysisJob`(系统 A) - `AiRuntimeJob.result` → `AiRuntimeResult?`(系统 B) - `AiRuntimeResult.job` → `AiRuntimeJob`(系统 B) - M-AI-02-09 将 `AiAnalysisJob` 重命名为 `AiJob` 时,`AiAnalysisResult.job` 的 relation 类型需要同时更新 ### 7.5 Raw SQL / 手写表名 **风险等级:中高** - 源文件(`src/`)中无 `$queryRaw`/`$executeRaw` 引用 AI 分析相关表 - `worker.main.ts:24` 和 `system.controller.ts:35` 仅使用 `SELECT 1` 做健康检查 - **重要发现**:E2E 测试文件使用 Raw SQL 直接查询物理表: - `test/api-e2e.worker-int-spec.ts`:8 处 Raw SQL + 8 处字符串引用(it/console.log)= **16 处**硬编码表名 `'AiAnalysisJob'`、`'AiAnalysisResult'` - `test/worker-integration.worker-int-spec.ts`:13 处 Raw SQL + 8 处字符串引用 = **21 处**同上 - **合计 37 处引用**,其中 21 处为 Raw SQL(`SELECT ... FROM AiAnalysisJob`),16 处为字符串引用 - **M-AI-02-09 重命名物理表后,这 37 处引用必须全部同步更新** ### 7.6 报表/Admin 直接查询物理表 **风险等级:中等** 所有 Admin 查询通过 Prisma Client,不会直接访问物理表。但以下聚合逻辑依赖现有字段: | Admin 功能 | 文件 | 依赖字段 | |-----------|------|---------| | Dashboard 今日 AI 调用数 | `admin-dashboard.service.ts:46` | `AiUsageLog.createdAt` | | AI 耗时统计 | `admin-metrics.controller.ts:56-86` | `AiUsageLog.provider`、`model`、`latencyMs`、`success`、`estimatedCost` | | 成本聚合 | `cost-aggregation.service.ts:31-56` | `AiUsageLog.*` | | Admin 分析结果 | `admin-learning.service.ts:229-233` | `AiAnalysisResult.*` | ### 7.7 跨表数据关系 | 源表 | 目标表 | 关系 | 当前状态 | M-AI-02 影响 | |------|--------|------|----------|-------------| | `AiAnalysisJob` | `AiAnalysisResult` | 1:N FK | 生产运行中 | 需保留 | | `AiAnalysisJob` | `FocusItem` | 逻辑关联(Worker 进程内直接调用 `FocusItemsService.create()`) | 生产运行中 | 不直接受影响 | | `AiAnalysisJob` | `ReviewCard` | 逻辑关联(EventEmitter → Subscriber → AI Workflow → `ReviewCard` 表) | 生产运行中 | **M-AI-05 Outbox 替代时必须保持此链** | | `AiRuntimeJob` | `AiRuntimeResult` | 1:1 FK | 闲置(无生产者) | M-AI-03 规划使用 | | `AiRuntimeJob` | `LearningAnalysisSnapshot` | 逻辑关联(snapshotId) | 闲置 | M-AI-03 规划使用 | | `AiRuntimeJob` | `AiLearningAnalysis` | 逻辑关联(jobId) | 闲置 | M-AI-04 规划使用 | | `AiRuntimeJob` | `QuestionGenerationPlan` | 逻辑关联(jobId) | 闲置 | M-AI-06 规划使用 | | `AiRuntimeJob` | `FlashcardGenerationPlan` | 逻辑关联(jobId) | 闲置 | M-AI-06 规划使用 | | `AiUsageLog` | 无 FK 到 Job 表 | **无关联** | - | **M-AI-02-08 需新增** | ### `ai.analysis.completed` 事件订阅链完整追踪 **对 M-AI-05(Outbox 替代 EventEmitter)至关重要。** ``` AiAnalysisWorker.process() // src/workers/ai-analysis.worker.ts:72 └─ eventBus.publish(new AIAnalysisCompleted(...)) // 发布事件 │ └─ ReviewCardSubscriber.handleAIAnalysisCompleted() // src/modules/review/review-card.subscriber.ts:11 │ @OnEvent('ai.analysis.completed') │ 订阅 payload: { userId, jobId, sessionId?, answerId?, type, score?, analysis? } │ └─ ReviewService.generateCards(userId, input) // src/modules/review/review.service.ts:68 │ 从 payload.analysis 中提取 weaknesses/strengths/summary │ 拼接为 prompt 文本 → CardGenerationWorkflow.execute() │ └─ ReviewRepository.insertCard(data) // src/modules/review/review.repository.ts:38 │ this.prisma.reviewCard.create({ data }) │ └─ 写入 ReviewCard 表 (prisma/schema.prisma:642-668) 字段: userId, frontText, backText, difficulty, status, intervalDays, easeFactor, repetitionCount, lapseCount, scheduleState, nextReviewAt ``` **关键发现**: 1. `ReviewCard` **无 `jobId` FK** — 无法溯源哪次 AI 分析生成了哪张卡片 2. Subscriber 依赖进程内 EventEmitter — M-AI-05 改为 Outbox 后,此链必须保持 3. `FocusItem` 不走事件订阅 — Worker 进程内直接调 `focusItems.create()`(`ai-analysis.worker.ts:88`),不存在可靠投递保证 4. Subscriber 声明的 payload 接口(`review-card.subscriber.ts:12-20`)与实际 `AIAnalysisCompleted` event 类(`ai-analysis.worker.ts:13-16`)完全耦合 — 任何 event schema 变更需同步修改 subscriber --- ## 8. 关键发现总结 1. **生产数据量极小**:AiAnalysisJob 仅 9 行,AiAnalysisResult 仅 5 行,AiUsageLog 110 行。所有表 < 0.12 MB。**Migration 锁表风险极低**(毫秒级 ALTER TABLE)。 2. **AiRuntimeJob/AiRuntimeResult 零记录**:确认 ADR 断言正确——系统 B 无生产者活动。M-AI-02 Expand 无需回填历史 RuntimeJob 数据。 3. **两套系统并存**:`AiAnalysisJob`(生产运行中,active-recall + feynman)和 `AiRuntimeJob`(闲置,5 种分析类型)是两套完全独立的表,无任何 FK/逻辑关联 4. **AiUsageLog 与 Job 表无关联**:当前 `AiUsageLog` 无 `jobId` 字段,无法追踪单次 AI 调用归属。**M-AI-02-08 需要新增 `jobId` FK** 5. **无 Outbox/事件溯源**:系统 A 通过 NestJS EventEmitter 发布事件(`ai.analysis.completed`),系统 B 在 API 进程内同步写 Notification——无持久化事件表 6. **状态值非标准化**:无 enum 定义,两套状态语义不兼容(`processing` vs `locked`/`running`,`completed` vs `succeeded`) 7. **AiRuntimeJob 字段远多于 AiAnalysisJob**:31 vs 13 字段,M-AI-02 Expand 后应保持一致 8. **无 raw SQL 引用 AI 表**:AI 表仅通过 Prisma Client 访问 9. **37 处测试文件硬编码表名**:`test/api-e2e.worker-int-spec.ts`(16) 和 `test/worker-integration.worker-int-spec.ts`(21) 中 Raw SQL 及字符串引用物理表名 `'AiAnalysisJob'`/`'AiAnalysisResult'`,M-AI-02-09 重命名后必须全部同步。另有 `test/helpers/integration-harness.ts:228` 的清理列表 10. **sessionId/answerId 高 NULL 率**:AiAnalysisJob 中 sessionId 100% NULL,answerId 78% NULL——M-AI-02-04 可评估是否保留这两个字段 11. **无孤儿 FK**:所有 FK 引用一致,数据完整性良好 12. **全部 FK 来自 Prisma 默认值**:Prisma schema 中 7 个 `@relation` 均未显式声明 `onDelete`/`onUpdate`,生产实际值为 RESTRICT/CASCADE(Prisma 默认)。M-AI-02-03/04 新增 FK 应**显式声明**以避免跨版本/跨方言歧义(见 1.6 节) 13. **ReviewCard 无 jobId 溯源**:`ai.analysis.completed` → `ReviewCardSubscriber` → AI Workflow → `ReviewCard` 的完整链路已追踪(见 7.7 节),但 ReviewCard 表无 `jobId` FK,无法溯源卡片生成来源 14. **FocusItem 不走事件系统**:Worker 进程内直接同步调用 `focusItems.create()`,无可靠投递保证——M-AI-05 Outbox 化时需考虑 15. **AiUsageLog 低成功率根因已定位**:72.7% 失败由 MiniMax M2.7 API Key 到期(2026-06-06)导致 — `learning-trend` 全部使用 minimax 故失败率 84%。DeepSeek 调用近乎 100% 成功。生产已全量切换至 DeepSeek V4 Pro/Flash,M-AI-02 无需保留 Minimax 兼容