fix: 加固发布单幂等创建

- src/deploy-runs/deploy-run.repository.ts: 将幂等查重、目标环境读取和 run/steps 创建纳入同一事务
- src/deploy-runs/deploy-run.repository.ts: 并发唯一键冲突时回读已有发布单,避免重复点击冒泡为 500
This commit is contained in:
湛兮
2026-06-12 05:29:09 +08:00
parent f883568250
commit d0077636c7
+116 -57
View File
@@ -48,6 +48,7 @@ type DeployRunRecord = Prisma.DeployRunGetPayload<{
steps: true; steps: true;
}; };
}>; }>;
type DeployRunDatabaseClient = PrismaService | Prisma.TransactionClient;
type DeployRunMetadata = Prisma.InputJsonObject & { type DeployRunMetadata = Prisma.InputJsonObject & {
jenkinsBuildUrl?: string; jenkinsBuildUrl?: string;
@@ -149,61 +150,7 @@ export class DeployRunRepository {
return this.createMemoryRun(input, idempotencyKey); return this.createMemoryRun(input, idempotencyKey);
} }
const existing = await this.prisma.deployRun.findUnique({ const created = await this.createDatabaseRun(input, idempotencyKey);
where: { idempotencyKey },
include: {
project: true,
environment: true,
steps: {
orderBy: {
order: 'asc',
},
},
},
});
if (existing) {
return this.toSummary(existing);
}
const target = await this.findTargetProjectEnvironment(input);
const initialRun = this.buildInitialRun(input, idempotencyKey);
const metadata = this.inputMetadata(input);
const created = await this.prisma.$transaction(async (tx) => {
const run = await tx.deployRun.create({
data: {
projectId: target.project.id,
environmentId: target.environment.id,
jobId: target.deployJob?.id,
status: PrismaDeployRunStatus.PENDING,
trigger: triggerToPrisma[input.trigger ?? 'manual'],
ref: input.ref,
operator: input.operator,
idempotencyKey,
metadata,
steps: {
create: initialRun.steps.map((step) => ({
bpmnNodeId: step.bpmnNodeId,
name: step.name,
status: stepStatusToPrisma[step.status],
order: step.order,
})),
},
},
include: {
project: true,
environment: true,
steps: {
orderBy: {
order: 'asc',
},
},
},
});
return run;
});
return this.toSummary(created); return this.toSummary(created);
} }
@@ -469,8 +416,92 @@ export class DeployRunRepository {
}); });
} }
private async findTargetProjectEnvironment(input: CreateDeployRunInput) { private async findRecordByIdempotencyKey(
const project = await this.prisma.project.findUnique({ idempotencyKey: string,
client: DeployRunDatabaseClient = this.prisma,
): Promise<DeployRunRecord | null> {
return client.deployRun.findUnique({
where: { idempotencyKey },
include: {
project: true,
environment: true,
steps: {
orderBy: {
order: 'asc',
},
},
},
});
}
private async createDatabaseRun(
input: CreateDeployRunInput,
idempotencyKey: string,
): Promise<DeployRunRecord> {
try {
return await this.prisma.$transaction(async (tx) => {
const existing = await this.findRecordByIdempotencyKey(
idempotencyKey,
tx,
);
if (existing) {
return existing;
}
const target = await this.findTargetProjectEnvironment(input, tx);
const initialRun = this.buildInitialRun(input, idempotencyKey);
const metadata = this.inputMetadata(input);
return tx.deployRun.create({
data: {
projectId: target.project.id,
environmentId: target.environment.id,
jobId: target.deployJob?.id,
status: PrismaDeployRunStatus.PENDING,
trigger: triggerToPrisma[input.trigger ?? 'manual'],
ref: input.ref,
operator: input.operator,
idempotencyKey,
metadata,
steps: {
create: initialRun.steps.map((step) => ({
bpmnNodeId: step.bpmnNodeId,
name: step.name,
status: stepStatusToPrisma[step.status],
order: step.order,
})),
},
},
include: {
project: true,
environment: true,
steps: {
orderBy: {
order: 'asc',
},
},
},
});
});
} catch (error) {
if (this.isIdempotencyUniqueConflict(error)) {
const existing = await this.findRecordByIdempotencyKey(idempotencyKey);
if (existing) {
return existing;
}
}
throw error;
}
}
private async findTargetProjectEnvironment(
input: CreateDeployRunInput,
client: DeployRunDatabaseClient = this.prisma,
) {
const project = await client.project.findUnique({
where: { key: input.projectKey }, where: { key: input.projectKey },
include: { include: {
environments: { environments: {
@@ -503,6 +534,34 @@ export class DeployRunRepository {
}; };
} }
private isIdempotencyUniqueConflict(error: unknown): boolean {
if (!error || typeof error !== 'object') {
return false;
}
const record = error as {
code?: unknown;
meta?: {
target?: unknown;
};
};
if (record.code !== 'P2002') {
return false;
}
const target = record.meta?.target;
if (Array.isArray(target)) {
return target.includes('idempotencyKey');
}
return (
typeof target === 'string' &&
target.toLowerCase().includes('idempotencykey')
);
}
private createMemoryRun( private createMemoryRun(
input: CreateDeployRunInput, input: CreateDeployRunInput,
idempotencyKey: string, idempotencyKey: string,