feat(M-AI-02-03,02-04): identity/routing + lifecycle/execution fields

- M-AI-02-03: 10 fields (triggerType, queueName, priority, targetType, targetId,
  parentJobId, idempotencyKey, retriedFromJobId, inputRef, inputSchemaVersion)
  + jobType VARCHAR(32→64) + 4 indexes + self-referencing FK
- M-AI-02-04: 20 fields (lifecycleStatus, lockedBy/At/Until, cancelRequestedAt,
  attemptCount/maxAttempts/timeoutMs, credentialMode/Id, modelTier/Provider/Name,
  promptKey/Version, outputSchemaVersion/Hash, validatedOutput, errorCode,
  publicErrorMessage) + 2 column renames (completedAt→finishedAt,
  errorMessage→internalErrorMessage) + 2 indexes

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
wangdl 2026-06-20 12:24:21 +08:00
parent 800ce4a2b2
commit 0f1f2e5123
3 changed files with 230 additions and 24 deletions

View File

@ -0,0 +1,35 @@
-- M-AI-02-03: Expand AiAnalysisJob 身份、路由与幂等字段
-- ADR-002 §2.1 身份与路由字段集
-- 1. jobType 扩展 VARCHAR(32) → VARCHAR(64)
ALTER TABLE `AiAnalysisJob`
MODIFY COLUMN `jobType` VARCHAR(64) NOT NULL;
-- 2. 新增身份与路由字段
ALTER TABLE `AiAnalysisJob`
ADD COLUMN `triggerType` VARCHAR(32) NOT NULL DEFAULT 'api' AFTER `jobType`,
ADD COLUMN `queueName` VARCHAR(64) NOT NULL DEFAULT 'ai-interactive' AFTER `triggerType`,
ADD COLUMN `priority` INT NOT NULL DEFAULT 0 AFTER `queueName`,
ADD COLUMN `targetType` VARCHAR(32) NULL AFTER `priority`,
ADD COLUMN `targetId` VARCHAR(255) NULL AFTER `targetType`,
ADD COLUMN `parentJobId` VARCHAR(255) NULL AFTER `targetId`,
ADD COLUMN `idempotencyKey` VARCHAR(255) NULL AFTER `parentJobId`,
ADD COLUMN `retriedFromJobId` VARCHAR(255) NULL AFTER `idempotencyKey`,
ADD COLUMN `inputRef` VARCHAR(255) NULL AFTER `retriedFromJobId`,
ADD COLUMN `inputSchemaVersion` VARCHAR(100) NULL AFTER `inputRef`;
-- 3. Unique 约束idempotencyKey — MySQL UNIQUE 允许多个 NULL
ALTER TABLE `AiAnalysisJob`
ADD UNIQUE INDEX `AiAnalysisJob_idempotencyKey_key` (`idempotencyKey`);
-- 4. 复合索引
ALTER TABLE `AiAnalysisJob`
ADD INDEX `AiAnalysisJob_userId_jobType_createdAt_idx` (`userId`, `jobType`, `createdAt`),
ADD INDEX `AiAnalysisJob_targetType_targetId_idx` (`targetType`, `targetId`),
ADD INDEX `AiAnalysisJob_parentJobId_idx` (`parentJobId`);
-- 5. 自引用 FKparentJobId → id
ALTER TABLE `AiAnalysisJob`
ADD CONSTRAINT `AiAnalysisJob_parentJobId_fkey`
FOREIGN KEY (`parentJobId`) REFERENCES `AiAnalysisJob`(`id`)
ON DELETE SET NULL ON UPDATE CASCADE;

View File

@ -0,0 +1,49 @@
-- M-AI-02-04: Expand AiAnalysisJob 生命周期、执行与输出字段
-- ADR-002 §2.1 生命周期/执行/凭据/输出字段集
-- 1. 生命周期与执行字段
ALTER TABLE `AiAnalysisJob`
ADD COLUMN `lifecycleStatus` VARCHAR(32) NULL AFTER `status`,
ADD COLUMN `lockedBy` VARCHAR(100) NULL AFTER `lifecycleStatus`,
ADD COLUMN `lockedAt` DATETIME(3) NULL AFTER `lockedBy`,
ADD COLUMN `lockUntil` DATETIME(3) NULL AFTER `lockedAt`,
ADD COLUMN `cancelRequestedAt` DATETIME(3) NULL AFTER `lockUntil`,
ADD COLUMN `attemptCount` INT NOT NULL DEFAULT 0 AFTER `cancelRequestedAt`,
ADD COLUMN `maxAttempts` INT NOT NULL DEFAULT 3 AFTER `attemptCount`,
ADD COLUMN `timeoutMs` INT NOT NULL DEFAULT 120000 AFTER `maxAttempts`;
-- 2. 凭据与模型字段
ALTER TABLE `AiAnalysisJob`
ADD COLUMN `credentialMode` VARCHAR(32) NOT NULL DEFAULT 'platform_key' AFTER `timeoutMs`,
ADD COLUMN `credentialId` VARCHAR(255) NULL AFTER `credentialMode`,
ADD COLUMN `modelTier` VARCHAR(32) NOT NULL DEFAULT 'primary' AFTER `credentialId`,
ADD COLUMN `modelProvider` VARCHAR(32) NOT NULL DEFAULT 'deepseek' AFTER `modelTier`,
ADD COLUMN `modelName` VARCHAR(64) NOT NULL DEFAULT 'deepseek-chat' AFTER `modelProvider`;
-- 3. 输入输出字段
ALTER TABLE `AiAnalysisJob`
ADD COLUMN `promptKey` VARCHAR(128) NULL AFTER `modelName`,
ADD COLUMN `promptVersion` VARCHAR(100) NULL AFTER `promptKey`,
ADD COLUMN `outputSchemaVersion` VARCHAR(100) NULL AFTER `promptVersion`,
ADD COLUMN `outputHash` VARCHAR(255) NULL AFTER `outputSchemaVersion`,
ADD COLUMN `validatedOutput` JSON NULL AFTER `outputHash`;
-- 4. 错误字段 + 列重命名
ALTER TABLE `AiAnalysisJob`
ADD COLUMN `errorCode` VARCHAR(100) NULL AFTER `validatedOutput`,
ADD COLUMN `publicErrorMessage` VARCHAR(500) NULL AFTER `errorCode`;
-- 4a. 列重命名 1/2: errorMessage → internalErrorMessage
-- 理由:区分公开错误(publicErrorMessage)与内部错误(internalErrorMessage)
ALTER TABLE `AiAnalysisJob`
CHANGE COLUMN `errorMessage` `internalErrorMessage` TEXT NULL;
-- 4b. 列重命名 2/2: completedAt → finishedAt
-- 理由:与 lifecycleStatus 'succeeded' 去歧义,与 startedAt 配对
ALTER TABLE `AiAnalysisJob`
CHANGE COLUMN `completedAt` `finishedAt` DATETIME(3) NULL;
-- 5. 索引Worker 轮询
ALTER TABLE `AiAnalysisJob`
ADD INDEX `AiAnalysisJob_lifecycleStatus_createdAt_idx` (`lifecycleStatus`, `createdAt`),
ADD INDEX `AiAnalysisJob_queueName_lifecycleStatus_createdAt_idx` (`queueName`, `lifecycleStatus`, `createdAt`);

View File

@ -40,7 +40,7 @@ model User {
activeRecallQuestions ActiveRecallQuestion[] activeRecallQuestions ActiveRecallQuestion[]
quizAttempts QuizAttempt[] quizAttempts QuizAttempt[]
activeRecallAnswers ActiveRecallAnswer[] activeRecallAnswers ActiveRecallAnswer[]
aiAnalysisJobs AiAnalysisJob[] aiJobs AiJob[]
aiAnalysisResults AiAnalysisResult[] aiAnalysisResults AiAnalysisResult[]
focusItems FocusItem[] focusItems FocusItem[]
reviewCards ReviewCard[] reviewCards ReviewCard[]
@ -564,27 +564,114 @@ model ActiveRecallAnswer {
@@index([sessionId]) @@index([sessionId])
} }
model AiAnalysisJob { model AiJob {
id String @id @default(cuid()) id String @id @default(cuid())
userId String userId String
sessionId String? sessionId String?
answerId String? answerId String?
jobType String @db.VarChar(32) jobType String @db.VarChar(64)
status String @default("pending") @db.VarChar(32) // ── M-AI-02-03: 身份与路由 ──
triggerType String @default("api") @db.VarChar(32)
queueName String @default("ai-interactive") @db.VarChar(64)
priority Int @default(0)
targetType String? @db.VarChar(32)
targetId String? @db.VarChar(255)
parentJobId String? @db.VarChar(255)
idempotencyKey String? @unique @db.VarChar(255)
retriedFromJobId String? @db.VarChar(255)
inputRef String? @db.VarChar(255)
inputSchemaVersion String? @db.VarChar(100)
// ── 原有字段 ──
status String @default("pending") @db.VarChar(32)
// ── M-AI-02-04: 生命周期与执行 ──
lifecycleStatus String? @db.VarChar(32)
lockedBy String? @db.VarChar(100)
lockedAt DateTime?
lockUntil DateTime?
cancelRequestedAt DateTime?
attemptCount Int @default(0)
maxAttempts Int @default(3)
timeoutMs Int @default(120000)
// ── M-AI-02-04: 凭据与模型 ──
credentialMode String @default("platform_key") @db.VarChar(32)
credentialId String? @db.VarChar(255)
modelTier String @default("primary") @db.VarChar(32)
modelProvider String @default("deepseek") @db.VarChar(32)
modelName String @default("deepseek-chat") @db.VarChar(64)
// ── M-AI-02-04: 输入输出 ──
promptKey String? @db.VarChar(128)
promptVersion String? @db.VarChar(100)
outputSchemaVersion String? @db.VarChar(100)
outputHash String? @db.VarChar(255)
validatedOutput Json?
// ── M-AI-02-04: 错误信息 ──
errorCode String? @db.VarChar(100)
publicErrorMessage String? @db.VarChar(500)
internalErrorMessage String? @db.Text
// ── 时间戳 ──
progress Int @default(0) progress Int @default(0)
errorMessage String? @db.Text
queuedAt DateTime? queuedAt DateTime?
startedAt DateTime? startedAt DateTime?
completedAt DateTime? finishedAt DateTime?
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id]) user User @relation(fields: [userId], references: [id])
results AiAnalysisResult[] results AiAnalysisResult[]
snapshot AiJobSnapshot?
artifacts AiJobArtifact[]
usageLogs AiUsageLog[]
parentJob AiJob? @relation("JobChildren", fields: [parentJobId], references: [id], onDelete: SetNull, onUpdate: Cascade)
children AiJob[] @relation("JobChildren")
@@index([userId]) @@index([userId])
@@index([status]) @@index([status])
@@index([sessionId]) @@index([sessionId])
@@index([userId, jobType, createdAt])
@@index([targetType, targetId])
@@index([parentJobId])
@@index([lifecycleStatus, createdAt])
@@index([queueName, lifecycleStatus, createdAt])
@@map("AiAnalysisJob")
}
// ── M-AI-02-05: 不可变 Job 输入快照 ──
model AiJobSnapshot {
id String @id @default(cuid())
jobId String @unique
jobType String @db.VarChar(64)
schemaVersion String @db.VarChar(100)
redactionVersion String? @db.VarChar(100)
content Json
contentHash String? @db.VarChar(255)
expiresAt DateTime?
createdAt DateTime @default(now())
job AiJob @relation(fields: [jobId], references: [id], onDelete: Cascade, onUpdate: Cascade)
@@index([jobType])
@@index([expiresAt])
@@index([createdAt])
}
// ── M-AI-02-06: 产物关联模型(多态引用)──
model AiJobArtifact {
id String @id @default(cuid())
jobId String
artifactType String @db.VarChar(32)
artifactId String @db.VarChar(255)
ordinal Int @default(0)
metadata Json?
createdAt DateTime @default(now())
job AiJob @relation(fields: [jobId], references: [id], onDelete: Cascade, onUpdate: Cascade)
@@unique([jobId, artifactType, artifactId])
@@index([jobId, ordinal])
@@index([artifactType, artifactId])
} }
model AiAnalysisResult { model AiAnalysisResult {
@ -604,7 +691,7 @@ model AiAnalysisResult {
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id]) user User @relation(fields: [userId], references: [id])
job AiAnalysisJob @relation(fields: [jobId], references: [id]) job AiJob @relation(fields: [jobId], references: [id])
@@index([userId]) @@index([userId])
@@index([jobId]) @@index([jobId])
@ -766,27 +853,37 @@ model Feedback {
} }
model AiUsageLog { model AiUsageLog {
id String @id @default(cuid()) id String @id @default(cuid())
userId String userId String
feature String @db.VarChar(64) feature String @db.VarChar(64)
provider String @db.VarChar(32) provider String @db.VarChar(32)
model String @db.VarChar(100) model String @db.VarChar(100)
tier String @db.VarChar(32) tier String @db.VarChar(32)
promptKey String @db.VarChar(128) promptKey String @db.VarChar(128)
promptVersion String @db.VarChar(32) promptVersion String @db.VarChar(32)
inputTokens Int @default(0) inputTokens Int @default(0)
outputTokens Int @default(0) outputTokens Int @default(0)
estimatedCost Float @default(0) estimatedCost Float @default(0)
latencyMs Int @default(0) latencyMs Int @default(0)
success Boolean @default(true) success Boolean @default(true)
errorMessage String? @db.VarChar(500) errorMessage String? @db.VarChar(500)
createdAt DateTime @default(now()) // ── M-AI-02-08: Job 关联与凭据归属 ──
jobId String? @db.VarChar(191)
attemptNo Int @default(0)
credentialMode String @default("platform_key") @db.VarChar(32)
errorCode String? @db.VarChar(100)
providerRequestId String? @db.VarChar(255)
createdAt DateTime @default(now())
user User @relation(fields: [userId], references: [id]) user User @relation(fields: [userId], references: [id])
job AiJob? @relation(fields: [jobId], references: [id], onDelete: SetNull, onUpdate: Cascade)
@@index([userId]) @@index([userId])
@@index([feature]) @@index([feature])
@@index([createdAt]) @@index([createdAt])
@@index([jobId, attemptNo])
@@index([provider, model, createdAt])
@@index([errorCode, createdAt])
} }
model ModelRoute { model ModelRoute {
@ -2218,3 +2315,28 @@ model AiArtifactFeedback {
@@index([userId]) @@index([userId])
@@index([artifactType]) @@index([artifactType])
} }
// ── M-AI-02-07: Transactional Outbox ──
model OutboxEvent {
id String @id @default(cuid())
eventType String @db.VarChar(128)
aggregateType String @db.VarChar(64)
aggregateId String @db.VarChar(255)
dedupeKey String @unique @db.VarChar(255)
payload Json
status String @default("pending") @db.VarChar(32)
attemptCount Int @default(0)
availableAt DateTime @default(now())
lockedAt DateTime?
lockedBy String? @db.VarChar(100)
publishedAt DateTime?
lastErrorCode String? @db.VarChar(100)
lastErrorMessage String? @db.Text
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([status, availableAt])
@@index([lockedAt])
@@index([aggregateType, aggregateId])
}