fix: make SMS retry side effects idempotent

This commit is contained in:
hectorzhao
2026-07-26 22:31:30 +08:00
parent 04f78695ed
commit e0f6eed0d4
11 changed files with 516 additions and 105 deletions
+42 -19
View File
@@ -19,6 +19,7 @@ export interface UpdateCreditLimitDto {
export interface CreateAccountTransactionDto {
tenantId: string;
transactionType: string;
idempotencyKey?: string;
amountCents?: number;
balanceAfter?: number;
relatedType?: string;
@@ -67,6 +68,7 @@ export interface EstimateSmsCostDto {
export interface BillingActionDto {
tenantId: string;
idempotencyKey?: string;
amountCents?: number;
relatedType?: string;
relatedId?: string;
@@ -454,25 +456,46 @@ export class BillingService {
}
private async applyAccountDelta(data: CreateAccountTransactionDto) {
const account = await this.getAccountOrCreate(data.tenantId);
const nextBalance = moneyToNumber(account.balanceCents) + (data.amountCents ?? 0);
await this.prisma.tenantAccount.update({
where: { tenantId: data.tenantId },
data: {
balanceCents: nextBalance,
},
});
return this.prisma.accountTransaction.create({
data: {
tenantId: data.tenantId,
transactionType: data.transactionType,
amountCents: data.amountCents ?? 0,
balanceAfter: nextBalance,
relatedType: data.relatedType,
relatedId: data.relatedId,
remark: data.remark,
},
const amountCents = data.amountCents ?? 0;
const idempotencyKey = data.idempotencyKey?.trim() || null;
return this.prisma.$transaction(async (tx) => {
await tx.$executeRaw`SELECT pg_advisory_xact_lock(hashtextextended(${'tenant-account:' + data.tenantId}, 0))`;
if (idempotencyKey) {
const existing = await tx.accountTransaction.findUnique({ where: { idempotencyKey } });
if (existing) {
if (
existing.tenantId !== data.tenantId
|| existing.transactionType !== data.transactionType
|| moneyToNumber(existing.amountCents) !== amountCents
|| existing.relatedType !== (data.relatedType ?? null)
|| existing.relatedId !== (data.relatedId ?? null)
) {
throw new ConflictException('账务幂等键已用于另一笔交易');
}
return existing;
}
}
await tx.tenantAccount.upsert({
where: { tenantId: data.tenantId },
update: {},
create: { tenantId: data.tenantId, balanceCents: 0, creditCents: 0, status: 'active' },
});
const account = await tx.tenantAccount.update({
where: { tenantId: data.tenantId },
data: { balanceCents: { increment: amountCents } },
});
return tx.accountTransaction.create({
data: {
tenantId: data.tenantId,
transactionType: data.transactionType,
idempotencyKey,
amountCents,
balanceAfter: account.balanceCents,
relatedType: data.relatedType,
relatedId: data.relatedId,
remark: data.remark,
},
});
});
}
}