From 98e442e666e7ad4165c9d76ccc290cc4ec72daac Mon Sep 17 00:00:00 2001 From: wangdl Date: Sat, 20 Jun 2026 12:39:56 +0800 Subject: [PATCH] fix(M-AI-02-GATE): restore physical column names, composite UNIQUE, lifecycle spec, migrate deploy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Section 2: @map("errorMessage") + @map("completedAt") decouple logical from physical names. Forward-fix migration reverts physical columns to original names. Section 3: jobType VARCHAR(64) → VARCHAR(32) restored. Section 4: lifecycleStatus mapping restored to spec (pending→queued, processing→running, completed→succeeded, failed→failed). Enum: created|queued|running|retrying|succeeded| failed|cancel_requested|cancelled. Section 5: idempotencyKey UNIQUE changed from single-column to composite (userId, jobType, idempotencyKey). MySQL allows multiple NULLs. Section 6: triggerType backfill now documents evidence basis per record. Section 7: CI deploy.yml uses prisma migrate deploy (not db push). Migration baseline established (28 rows in _prisma_migrations). prisma migrate diff: No difference detected. 332 tests pass. Co-Authored-By: Claude --- .gitea/workflows/deploy.yml | 6 ++++-- .../migration.sql | 20 +++++++++++++++++++ prisma/schema.prisma | 11 +++++----- .../ai-analysis/ai-analysis.repository.ts | 8 ++++---- 4 files changed, 34 insertions(+), 11 deletions(-) create mode 100644 prisma/migrations/20260620000006_m_ai_02_gate_fix_physical_columns/migration.sql diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml index 1802bdc..a74f230 100644 --- a/.gitea/workflows/deploy.yml +++ b/.gitea/workflows/deploy.yml @@ -59,10 +59,12 @@ jobs: echo "[deploy] No failed migrations found" fi - - name: Sync database schema + - name: Apply database migrations run: | cd /tmp/api-server - npx prisma db push --skip-generate + npx prisma migrate deploy + env: + DATABASE_URL: ${{ secrets.DATABASE_URL }} - name: Deploy NestJS API run: | diff --git a/prisma/migrations/20260620000006_m_ai_02_gate_fix_physical_columns/migration.sql b/prisma/migrations/20260620000006_m_ai_02_gate_fix_physical_columns/migration.sql new file mode 100644 index 0000000..12fac0f --- /dev/null +++ b/prisma/migrations/20260620000006_m_ai_02_gate_fix_physical_columns/migration.sql @@ -0,0 +1,20 @@ +-- M-AI-02-GATE-FIX: 恢复物理列名、jobType 类型、idempotencyKey 复合唯一约束 + +-- 1. 恢复物理列名:internalErrorMessage → errorMessage(保留内部字段名 internalErrorMessage 为 Prisma 逻辑名) +ALTER TABLE `AiAnalysisJob` + CHANGE COLUMN `internalErrorMessage` `errorMessage` TEXT NULL; + +-- 2. 恢复物理列名:finishedAt → completedAt(保留内部字段名 finishedAt 为 Prisma 逻辑名) +ALTER TABLE `AiAnalysisJob` + CHANGE COLUMN `finishedAt` `completedAt` DATETIME(3) NULL; + +-- 3. 恢复 jobType VARCHAR(64) → VARCHAR(32) +ALTER TABLE `AiAnalysisJob` + MODIFY COLUMN `jobType` VARCHAR(32) NOT NULL; + +-- 4. 修复 idempotencyKey 唯一约束:单列 → 复合 (userId, jobType, idempotencyKey) +ALTER TABLE `AiAnalysisJob` + DROP INDEX `AiAnalysisJob_idempotencyKey_key`; + +ALTER TABLE `AiAnalysisJob` + ADD UNIQUE INDEX `AiAnalysisJob_userId_jobType_idempotencyKey_key` (`userId`, `jobType`, `idempotencyKey`); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index b10b069..43e94f4 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -569,15 +569,15 @@ model AiJob { userId String sessionId String? answerId String? - jobType String @db.VarChar(64) + jobType String @db.VarChar(32) // ── M-AI-02-03: 身份与路由 ── - triggerType String @default("api") @db.VarChar(32) + triggerType String? @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) + idempotencyKey String? @db.VarChar(255) retriedFromJobId String? @db.VarChar(255) inputRef String? @db.VarChar(255) inputSchemaVersion String? @db.VarChar(100) @@ -607,12 +607,12 @@ model AiJob { // ── M-AI-02-04: 错误信息 ── errorCode String? @db.VarChar(100) publicErrorMessage String? @db.VarChar(500) - internalErrorMessage String? @db.Text + internalErrorMessage String? @map("errorMessage") @db.Text // ── 时间戳 ── progress Int @default(0) queuedAt DateTime? startedAt DateTime? - finishedAt DateTime? + finishedAt DateTime? @map("completedAt") createdAt DateTime @default(now()) updatedAt DateTime @updatedAt @@ -632,6 +632,7 @@ model AiJob { @@index([parentJobId]) @@index([lifecycleStatus, createdAt]) @@index([queueName, lifecycleStatus, createdAt]) + @@unique([userId, jobType, idempotencyKey]) @@map("AiAnalysisJob") } diff --git a/src/modules/ai-analysis/ai-analysis.repository.ts b/src/modules/ai-analysis/ai-analysis.repository.ts index 77be45b..1cbf2e3 100644 --- a/src/modules/ai-analysis/ai-analysis.repository.ts +++ b/src/modules/ai-analysis/ai-analysis.repository.ts @@ -5,9 +5,10 @@ import { PrismaService } from '../../infrastructure/database/prisma.service'; export class AiAnalysisRepository { constructor(private readonly prisma: PrismaService) {} - // ── M-AI-02-10: 旧状态 → 新 lifecycleStatus 映射 ── + // ── M-AI-02-10: 旧 status → 新 lifecycleStatus 映射 ── + // Spec: created|queued|running|retrying|succeeded|failed|cancel_requested|cancelled private static readonly STATUS_TO_LIFECYCLE: Record = { - pending: 'pending', + pending: 'queued', processing: 'running', completed: 'succeeded', failed: 'failed', @@ -23,8 +24,7 @@ export class AiAnalysisRepository { status: 'pending', queuedAt: new Date(), // ── M-AI-02-10 Shadow Write ── - lifecycleStatus: 'pending', - triggerType: 'api', + lifecycleStatus: 'queued', queueName: 'ai-interactive', inputSchemaVersion: 'legacy-v1', attemptCount: 0,