api-server/src/modules/ai-runtime/ai-job-artifact.repository.ts
wangdl 7cc6947e41 feat(M-AI-02-06): polymorphic AiJobArtifact model
- 7 columns, (jobId,artifactType,artifactId) UNIQUE for idempotency
- ON DELETE CASCADE, (jobId,ordinal) + (artifactType,artifactId) indexes
- P2002 catch → findUnique → retry-create for CASCADE race edge case
- 12 unit tests (idempotency, batch, reverse lookup, constraint)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-20 12:24:33 +08:00

111 lines
3.4 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';
import { Prisma } from '@prisma/client';
export const ARTIFACT_TYPES = [
'analysis_result',
'focus_item',
'review_card',
'quiz',
'learning_analysis',
'recommendation',
'knowledge_import',
] as const;
export type ArtifactType = (typeof ARTIFACT_TYPES)[number];
export interface CreateArtifactInput {
jobId: string;
artifactType: ArtifactType;
artifactId: string;
ordinal?: number;
metadata?: Record<string, unknown>;
}
/**
* M-AI-02-06: Job 产物关联 Repository
*
* 多态引用artifactType + artifactId 关联业务表,不设 FK 到各类型表。
* 唯一约束 (jobId, artifactType, artifactId) 保证幂等。
*/
@Injectable()
export class AiJobArtifactRepository {
constructor(private readonly prisma: PrismaService) {}
/**
* 幂等创建:若已存在同 (jobId, artifactType, artifactId) 的记录则跳过。
* 使用 Prisma create + skip-duplicate 语义Prisma 不支持 INSERT IGNORE MySQL 语法,
* 但 Prisma 的唯一约束错误可通过 try/catch 转为幂等)。
*/
async create(input: CreateArtifactInput) {
try {
return await this.prisma.aiJobArtifact.create({
data: {
jobId: input.jobId,
artifactType: input.artifactType,
artifactId: input.artifactId,
ordinal: input.ordinal ?? 0,
metadata: input.metadata ?? undefined,
},
});
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === 'P2002') {
// 唯一约束冲突 = 已存在,幂等返回已有记录
const existing = await this.prisma.aiJobArtifact.findUnique({
where: {
jobId_artifactType_artifactId: {
jobId: input.jobId,
artifactType: input.artifactType,
artifactId: input.artifactId,
},
},
});
if (existing) return existing;
// 极边缘情况P2002 和 findUnique 之间父 Job 被 CASCADE 删除。
// 此时记录已不存在,重试 create 一次。
return this.prisma.aiJobArtifact.create({
data: {
jobId: input.jobId,
artifactType: input.artifactType,
artifactId: input.artifactId,
ordinal: input.ordinal ?? 0,
metadata: input.metadata ?? undefined,
},
});
}
throw error;
}
}
/** 批量幂等创建(逐个 try/catch小批量场景适用 */
async createMany(inputs: CreateArtifactInput[]) {
const results: Awaited<ReturnType<typeof this.create>>[] = [];
for (const input of inputs) {
results.push(await this.create(input));
}
return results;
}
/** 按 Job 查询所有产物,按 ordinal 排序 */
async findByJobId(jobId: string) {
return this.prisma.aiJobArtifact.findMany({
where: { jobId },
orderBy: { ordinal: 'asc' },
});
}
/**
* 反向查询:按产物查找关联的 Job。
* 通过 (artifactType, artifactId) 复合索引高效查询。
*/
async findJobsByArtifact(artifactType: ArtifactType, artifactId: string) {
const artifacts = await this.prisma.aiJobArtifact.findMany({
where: { artifactType, artifactId },
select: { jobId: true, ordinal: true, createdAt: true },
orderBy: { createdAt: 'asc' },
});
return artifacts;
}
}