api-server/src/modules/ai-analysis/ai-analysis.repository.ts
wangdl 98e442e666
Some checks failed
Deploy API Server / build-and-deploy (push) Failing after 23s
fix(M-AI-02-GATE): restore physical column names, composite UNIQUE, lifecycle spec, migrate deploy
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 <noreply@anthropic.com>
2026-06-20 12:39:56 +08:00

75 lines
2.5 KiB
TypeScript
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.

import { Injectable } from '@nestjs/common';
import { PrismaService } from '../../infrastructure/database/prisma.service';
@Injectable()
export class AiAnalysisRepository {
constructor(private readonly prisma: PrismaService) {}
// ── M-AI-02-10: 旧 status → 新 lifecycleStatus 映射 ──
// Spec: created|queued|running|retrying|succeeded|failed|cancel_requested|cancelled
private static readonly STATUS_TO_LIFECYCLE: Record<string, string> = {
pending: 'queued',
processing: 'running',
completed: 'succeeded',
failed: 'failed',
};
async createJob(userId: string, jobType: string, sessionId?: string, answerId?: string) {
return this.prisma.aiJob.create({
data: {
userId,
jobType,
sessionId: sessionId ?? null,
answerId: answerId ?? null,
status: 'pending',
queuedAt: new Date(),
// ── M-AI-02-10 Shadow Write ──
lifecycleStatus: 'queued',
queueName: 'ai-interactive',
inputSchemaVersion: 'legacy-v1',
attemptCount: 0,
},
});
}
async updateJobStatus(id: string, status: string, errorMessage?: string) {
const data: Record<string, any> = { status };
if (status === 'processing') data.startedAt = new Date();
if (status === 'completed' || status === 'failed') data.finishedAt = new Date();
if (errorMessage) data.internalErrorMessage = errorMessage;
// ── M-AI-02-10 Shadow Write映射旧 status 到新 lifecycleStatus ──
const lifecycleStatus = AiAnalysisRepository.STATUS_TO_LIFECYCLE[status];
if (lifecycleStatus) data.lifecycleStatus = lifecycleStatus;
return this.prisma.aiJob.update({ where: { id }, data });
}
async findJobById(id: string) {
return this.prisma.aiJob.findUnique({
where: { id },
include: { results: true },
});
}
async createResult(userId: string, jobId: string, result: Record<string, any>) {
return this.prisma.aiAnalysisResult.create({
data: {
userId,
jobId,
summary: result.summary ?? '',
masteryScore: result.score ?? null,
strengths: (result.strengths ?? []) as any,
weaknesses: (result.weaknesses ?? []) as any,
suggestions: (result.focusItems ?? result.suggestions ?? []) as any,
nextActions: (result.reviewSuggestion ?? result.recommendations ?? null) as any,
rawResult: result as any,
},
});
}
async findResultById(id: string) {
return this.prisma.aiAnalysisResult.findUnique({ where: { id } });
}
}