From d0077636c79727c8cf24580a5094fa8b18a41bd7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B9=9B=E5=85=AE?= Date: Fri, 12 Jun 2026 05:29:09 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E5=8A=A0=E5=9B=BA=E5=8F=91=E5=B8=83?= =?UTF-8?q?=E5=8D=95=E5=B9=82=E7=AD=89=E5=88=9B=E5=BB=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - src/deploy-runs/deploy-run.repository.ts: 将幂等查重、目标环境读取和 run/steps 创建纳入同一事务 - src/deploy-runs/deploy-run.repository.ts: 并发唯一键冲突时回读已有发布单,避免重复点击冒泡为 500 --- src/deploy-runs/deploy-run.repository.ts | 173 +++++++++++++++-------- 1 file changed, 116 insertions(+), 57 deletions(-) diff --git a/src/deploy-runs/deploy-run.repository.ts b/src/deploy-runs/deploy-run.repository.ts index 7a3137e..ae360fb 100644 --- a/src/deploy-runs/deploy-run.repository.ts +++ b/src/deploy-runs/deploy-run.repository.ts @@ -48,6 +48,7 @@ type DeployRunRecord = Prisma.DeployRunGetPayload<{ steps: true; }; }>; +type DeployRunDatabaseClient = PrismaService | Prisma.TransactionClient; type DeployRunMetadata = Prisma.InputJsonObject & { jenkinsBuildUrl?: string; @@ -149,61 +150,7 @@ export class DeployRunRepository { return this.createMemoryRun(input, idempotencyKey); } - const existing = await this.prisma.deployRun.findUnique({ - 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; - }); + const created = await this.createDatabaseRun(input, idempotencyKey); return this.toSummary(created); } @@ -469,8 +416,92 @@ export class DeployRunRepository { }); } - private async findTargetProjectEnvironment(input: CreateDeployRunInput) { - const project = await this.prisma.project.findUnique({ + private async findRecordByIdempotencyKey( + idempotencyKey: string, + client: DeployRunDatabaseClient = this.prisma, + ): Promise { + return client.deployRun.findUnique({ + where: { idempotencyKey }, + include: { + project: true, + environment: true, + steps: { + orderBy: { + order: 'asc', + }, + }, + }, + }); + } + + private async createDatabaseRun( + input: CreateDeployRunInput, + idempotencyKey: string, + ): Promise { + 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 }, include: { 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( input: CreateDeployRunInput, idempotencyKey: string,