api-server/docs/architecture/m-ai-02-current-schema-audit.md
wangdl 800ce4a2b2 feat(M-AI-02): schema audit (02-01) + ADR freeze (02-02)
M-AI-02-01: Production DB audit of all 5 AI tables (9 rows, 0 orphans, 37 test refs)
M-AI-02-02: Frozen AiJob Expand ADR — 43-column schema, 6 indexes, state mapping

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

653 lines
36 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 SQLFK 由 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 SQLFK 由 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 FKM-AI-02-08**:新增 FK 应显式声明,**但需评估 `onDelete: SetNull` vs `Restrict`**——删除 Job 时 UsageLog 应保留以维持计费审计完整性
3. **OutboxEvent FKM-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 |
| 孤儿 FKjobId 不存在) | 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 Key2026-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.id<b.id AND ABS(TIMESTAMPDIFF(SECOND,a.createdAt,b.createdAt))<=1` | **1 对** |
| `(userId, sessionId, jobType)` 并发 | `GROUP BY userId,sessionId,jobType HAVING cnt>1 WHERE sessionId IS NOT NULL` | 0sessionId 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 级别,远低于 MEDIUMTEXT16MB上限
- `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` | 查询 Jobinclude 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-05Outbox 替代 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% NULLanswerId 78% NULL——M-AI-02-04 可评估是否保留这两个字段
11. **无孤儿 FK**所有 FK 引用一致数据完整性良好
12. **全部 FK 来自 Prisma 默认值**Prisma schema 7 `@relation` 均未显式声明 `onDelete`/`onUpdate`生产实际值为 RESTRICT/CASCADEPrisma 默认)。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/FlashM-AI-02 无需保留 Minimax 兼容