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>
653 lines
36 KiB
Markdown
653 lines
36 KiB
Markdown
# 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.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` | 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 兼容
|