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:
// 典型模式(无显式 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 的影响:
- M-AI-02-03/04 新增 FK 时应显式声明:避免依赖 Prisma 默认值(不同版本/方言可能不同),在 schema 中显式写出
onDelete: Restrict, onUpdate: Cascade 以保持全表一致
- AiUsageLog.jobId FK(M-AI-02-08):新增 FK 应显式声明,但需评估
onDelete: SetNull vs Restrict——删除 Job 时 UsageLog 应保留以维持计费审计完整性
- 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 |
deepseek-v4-pro |
35 |
deepseek-v4-flash |
16 |
AiUsageLog 成功率根因分析
107/110 的失败率(72.7%)由以下因素叠加导致:
- 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)
learning-trend(70 次,成功率 16%)是后台定时任务 → 高调用量 × minimax 到期 = 大量失败
rag-chat(13 次,成功率 92%)使用 deepseek-v4-pro → 几乎全部成功
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 重命名后需全部同步 |
test/worker-integration.worker-int-spec.ts |
Raw SQL 13 处 + 字符串引用 8 处 = 21 处引用物理表名 |
M-AI-02-09 重命名后需全部同步 |
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
关键发现:
ReviewCard 无 jobId FK — 无法溯源哪次 AI 分析生成了哪张卡片
- Subscriber 依赖进程内 EventEmitter — M-AI-05 改为 Outbox 后,此链必须保持
FocusItem 不走事件订阅 — Worker 进程内直接调 focusItems.create()(ai-analysis.worker.ts:88),不存在可靠投递保证
- Subscriber 声明的 payload 接口(
review-card.subscriber.ts:12-20)与实际 AIAnalysisCompleted event 类(ai-analysis.worker.ts:13-16)完全耦合 — 任何 event schema 变更需同步修改 subscriber
8. 关键发现总结
- 生产数据量极小:AiAnalysisJob 仅 9 行,AiAnalysisResult 仅 5 行,AiUsageLog 110 行。所有表 < 0.12 MB。Migration 锁表风险极低(毫秒级 ALTER TABLE)。
- AiRuntimeJob/AiRuntimeResult 零记录:确认 ADR 断言正确——系统 B 无生产者活动。M-AI-02 Expand 无需回填历史 RuntimeJob 数据。
- 两套系统并存:
AiAnalysisJob(生产运行中,active-recall + feynman)和 AiRuntimeJob(闲置,5 种分析类型)是两套完全独立的表,无任何 FK/逻辑关联
- AiUsageLog 与 Job 表无关联:当前
AiUsageLog 无 jobId 字段,无法追踪单次 AI 调用归属。M-AI-02-08 需要新增 jobId FK
- 无 Outbox/事件溯源:系统 A 通过 NestJS EventEmitter 发布事件(
ai.analysis.completed),系统 B 在 API 进程内同步写 Notification——无持久化事件表
- 状态值非标准化:无 enum 定义,两套状态语义不兼容(
processing vs locked/running,completed vs succeeded)
- AiRuntimeJob 字段远多于 AiAnalysisJob:31 vs 13 字段,M-AI-02 Expand 后应保持一致
- 无 raw SQL 引用 AI 表:AI 表仅通过 Prisma Client 访问
- 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 的清理列表
- sessionId/answerId 高 NULL 率:AiAnalysisJob 中 sessionId 100% NULL,answerId 78% NULL——M-AI-02-04 可评估是否保留这两个字段
- 无孤儿 FK:所有 FK 引用一致,数据完整性良好
- 全部 FK 来自 Prisma 默认值:Prisma schema 中 7 个
@relation 均未显式声明 onDelete/onUpdate,生产实际值为 RESTRICT/CASCADE(Prisma 默认)。M-AI-02-03/04 新增 FK 应显式声明以避免跨版本/跨方言歧义(见 1.6 节)
- ReviewCard 无 jobId 溯源:
ai.analysis.completed → ReviewCardSubscriber → AI Workflow → ReviewCard 的完整链路已追踪(见 7.7 节),但 ReviewCard 表无 jobId FK,无法溯源卡片生成来源
- FocusItem 不走事件系统:Worker 进程内直接同步调用
focusItems.create(),无可靠投递保证——M-AI-05 Outbox 化时需考虑
- 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 兼容