fix: harden financial transaction boundaries
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
import { Global, Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { FinancialOperation } from '../entities/financial-operation.entity';
|
||||
import { FinancialOperationsService } from './financial-operations.service';
|
||||
|
||||
@Global()
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([FinancialOperation])],
|
||||
providers: [FinancialOperationsService],
|
||||
exports: [FinancialOperationsService],
|
||||
})
|
||||
export class FinancialOperationsModule {}
|
||||
@@ -0,0 +1,55 @@
|
||||
import { ConflictException, Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { FinancialOperation } from '../entities/financial-operation.entity';
|
||||
|
||||
@Injectable()
|
||||
export class FinancialOperationsService {
|
||||
constructor(
|
||||
@InjectRepository(FinancialOperation)
|
||||
private readonly repo: Repository<FinancialOperation>,
|
||||
) {}
|
||||
|
||||
async run<T>(operationId: string | undefined, type: string, work: () => Promise<T>): Promise<T> {
|
||||
if (!operationId) return work();
|
||||
if (!/^[\w-]{8,64}$/.test(operationId)) throw new ConflictException('operationId 格式无效');
|
||||
|
||||
const existing = await this.repo.findOne({ where: { operationId } });
|
||||
if (existing) {
|
||||
if (existing.type !== type) throw new ConflictException('operationId 已用于其他操作');
|
||||
if (existing.status === 'completed' && existing.resultJson) return JSON.parse(existing.resultJson) as T;
|
||||
if (existing.status === 'running') throw new ConflictException('该操作正在处理中,请勿重复提交');
|
||||
}
|
||||
|
||||
let operation = existing;
|
||||
if (!operation) {
|
||||
try {
|
||||
operation = await this.repo.save(this.repo.create({ operationId, type, status: 'running' }));
|
||||
} catch (error) {
|
||||
const concurrent = await this.repo.findOne({ where: { operationId } });
|
||||
if (concurrent?.status === 'completed' && concurrent.resultJson) {
|
||||
return JSON.parse(concurrent.resultJson) as T;
|
||||
}
|
||||
throw new ConflictException('该操作正在处理中,请勿重复提交', { cause: error });
|
||||
}
|
||||
} else {
|
||||
operation.status = 'running';
|
||||
operation.errorMessage = null;
|
||||
operation.resultJson = null;
|
||||
await this.repo.save(operation);
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await work();
|
||||
operation.status = 'completed';
|
||||
operation.resultJson = JSON.stringify(result);
|
||||
await this.repo.save(operation);
|
||||
return result;
|
||||
} catch (error) {
|
||||
operation.status = 'failed';
|
||||
operation.errorMessage = error instanceof Error ? error.message.slice(0, 500) : '未知错误';
|
||||
await this.repo.save(operation);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user