fix: simplify balance billing and govern operation logs

This commit is contained in:
hectorzhao
2026-07-14 17:40:32 +08:00
parent f35691f185
commit 28dad93e3e
40 changed files with 740 additions and 395 deletions
+60 -46
View File
@@ -279,10 +279,9 @@ export class SendChainService implements OnModuleInit, OnModuleDestroy {
const accountCheck = await this.billing.checkAccount({
tenantId: data.tenantId,
amountCents: billing.amountCents,
smsUnits: billing.totalBillingUnits,
});
if (!accountCheck.canSend) {
throw new BadRequestException('企业账户余额、套餐余量或授信额度不足');
throw new BadRequestException('企业账户余额不足');
}
}
const task = await this.prisma.smsBatchTask.create({
@@ -305,11 +304,10 @@ export class SendChainService implements OnModuleInit, OnModuleDestroy {
createdById: data.createdById,
},
});
if (shouldReserveBalance && billing.amountCents + billing.totalBillingUnits > 0) {
if (shouldReserveBalance && billing.amountCents > 0) {
await this.billing.freeze({
tenantId: data.tenantId,
amountCents: billing.amountCents,
smsUnits: billing.totalBillingUnits,
relatedType: 'sms_batch_task',
relatedId: task.id,
remark: '发送任务创建冻结',
@@ -637,16 +635,14 @@ export class SendChainService implements OnModuleInit, OnModuleDestroy {
take: 100000,
});
const amountCents = messages.reduce((sum, message) => sum + message.amountCents, 0);
const smsUnits = messages.reduce((sum, message) => sum + message.billingUnits, 0);
const accountCheck = await this.billing.checkAccount({ tenantId: task.tenantId, amountCents, smsUnits });
const accountCheck = await this.billing.checkAccount({ tenantId: task.tenantId, amountCents });
if (!accountCheck.canSend) {
throw new BadRequestException('定时任务到点时企业账户余额、套餐余量或授信额度不足');
throw new BadRequestException('定时任务到点时企业账户余额不足');
}
if (amountCents + smsUnits > 0) {
if (amountCents > 0) {
await this.billing.freeze({
tenantId: task.tenantId,
amountCents,
smsUnits,
relatedType: 'sms_batch_task',
relatedId: task.id,
remark: '定时任务到点冻结',
@@ -1114,9 +1110,21 @@ export class SendChainService implements OnModuleInit, OnModuleDestroy {
}
const recoveryStatuses = (this.prisma as PrismaService & {
gatewayDownstreamRecoveryStatus: {
findUnique: (args: Record<string, unknown>) => Promise<any>;
upsert: (args: Record<string, unknown>) => Promise<any>;
};
}).gatewayDownstreamRecoveryStatus;
const previous = await recoveryStatuses.findUnique({
where: { account },
select: {
state: true,
gatewayInstanceId: true,
lockOwner: true,
failureCategory: true,
lastError: true,
lastSkipReason: true,
},
});
const application = await this.prisma.smsApplication.findUnique({
where: { cmppAccount: account },
select: { id: true, tenantId: true, name: true },
@@ -1167,27 +1175,30 @@ export class SendChainService implements OnModuleInit, OnModuleDestroy {
lockOwner?: string | null;
lockExpiresAt?: Date | null;
};
await this.prisma.operationLog.create({
data: {
tenantId: updated.tenantId ?? undefined,
action: 'gateway.downstream_recovery_status_sync',
resource: 'gateway_downstream_recovery_status',
resourceId: updated.id,
detail: {
account,
state: updated.state,
lockOwner: normalizedUpdated.lockOwner,
lockExpiresAt: normalizedUpdated.lockExpiresAt,
attemptCount: updated.attemptCount,
nextRetryAt: updated.nextRetryAt,
failureCategory: normalizedUpdated.failureCategory,
applicationId: updated.applicationId,
applicationName: application?.name,
lastError: updated.lastError,
lastSkipReason: updated.lastSkipReason,
if (hasRecoveryAuditStateChanged(previous, updated)) {
await this.prisma.operationLog.create({
data: {
tenantId: updated.tenantId ?? undefined,
action: 'gateway.downstream_recovery_status_changed',
resource: 'gateway_downstream_recovery_status',
resourceId: updated.id,
detail: {
account,
previousState: previous?.state ?? null,
state: updated.state,
gatewayInstanceId: updated.gatewayInstanceId,
lockOwner: normalizedUpdated.lockOwner,
attemptCount: updated.attemptCount,
nextRetryAt: updated.nextRetryAt,
failureCategory: normalizedUpdated.failureCategory,
applicationId: updated.applicationId,
applicationName: application?.name,
lastError: updated.lastError,
lastSkipReason: updated.lastSkipReason,
},
},
},
});
});
}
return updated;
}
@@ -1701,16 +1712,14 @@ export class SendChainService implements OnModuleInit, OnModuleDestroy {
const accountCheck = await this.billing.checkAccount({
tenantId: application.tenantId,
amountCents: billing.amountCents,
smsUnits: billing.totalBillingUnits,
});
if (!accountCheck.canSend) {
await reject('BALANCE', '企业账户余额、套餐余量或授信额度不足');
await reject('BALANCE', '企业账户余额不足');
} else {
if (billing.amountCents + billing.totalBillingUnits > 0) {
if (billing.amountCents > 0) {
await this.billing.freeze({
tenantId: application.tenantId,
amountCents: billing.amountCents,
smsUnits: billing.totalBillingUnits,
relatedType: 'sms_batch_task',
relatedId: task.id,
remark: 'CMPP 模板不匹配待审核短信冻结',
@@ -1764,16 +1773,14 @@ export class SendChainService implements OnModuleInit, OnModuleDestroy {
const accountCheck = await this.billing.checkAccount({
tenantId: application.tenantId,
amountCents: billing.amountCents,
smsUnits: billing.totalBillingUnits,
});
if (!accountCheck.canSend) {
await reject('BALANCE', '企业账户余额、套餐余量或授信额度不足');
await reject('BALANCE', '企业账户余额不足');
} else {
if (billing.amountCents + billing.totalBillingUnits > 0) {
if (billing.amountCents > 0) {
await this.billing.freeze({
tenantId: application.tenantId,
amountCents: billing.amountCents,
smsUnits: billing.totalBillingUnits,
relatedType: 'sms_batch_task',
relatedId: task.id,
remark: 'CMPP 入站短信冻结',
@@ -2253,16 +2260,15 @@ export class SendChainService implements OnModuleInit, OnModuleDestroy {
amountCents: number;
}) {
const amountCents = message.amountCents ?? 0;
const smsUnits = message.billingUnits ?? 0;
const billingUnits = message.billingUnits ?? 0;
const exists = await this.prisma.smsBillingRecord.findFirst({ where: { messageId: message.messageId } });
if (exists?.billingStatus === 'charged') {
return;
}
if (amountCents + smsUnits > 0) {
if (amountCents > 0) {
await this.billing.release({
tenantId: message.tenantId,
amountCents,
smsUnits,
relatedType: 'sms_batch_task',
relatedId: message.batchTaskId,
remark: `短信 ${message.messageId} 提交成功释放冻结并转扣费`,
@@ -2271,7 +2277,6 @@ export class SendChainService implements OnModuleInit, OnModuleDestroy {
const transaction = await this.billing.charge({
tenantId: message.tenantId,
amountCents,
smsUnits,
relatedType: 'sms_message_record',
relatedId: message.messageId,
remark: '提交成功扣费',
@@ -2283,7 +2288,7 @@ export class SendChainService implements OnModuleInit, OnModuleDestroy {
messageId: message.messageId,
phoneNumber: message.phoneNumber,
contentLength: [...message.content].length,
billingUnits: smsUnits,
billingUnits,
unitPrice: message.unitPrice ?? 0,
amountCents,
billingStatus: 'charged',
@@ -2300,7 +2305,7 @@ export class SendChainService implements OnModuleInit, OnModuleDestroy {
message: { tenantId: string; batchTaskId: string; messageId: string; amountCents: number; billingUnits: number },
remark: string,
) {
if ((message.amountCents ?? 0) + (message.billingUnits ?? 0) <= 0) {
if ((message.amountCents ?? 0) <= 0) {
return;
}
const charged = await this.prisma.smsBillingRecord.findFirst({ where: { messageId: message.messageId, billingStatus: 'charged' } });
@@ -2316,7 +2321,6 @@ export class SendChainService implements OnModuleInit, OnModuleDestroy {
await this.billing.release({
tenantId: message.tenantId,
amountCents: message.amountCents,
smsUnits: message.billingUnits,
relatedType: 'sms_message_record',
relatedId: message.messageId,
remark: `${remark}: ${message.messageId}`,
@@ -2327,7 +2331,7 @@ export class SendChainService implements OnModuleInit, OnModuleDestroy {
message: { tenantId: string; messageId: string; amountCents: number; billingUnits: number },
remark: string,
) {
if ((message.amountCents ?? 0) + (message.billingUnits ?? 0) <= 0) {
if ((message.amountCents ?? 0) <= 0) {
return;
}
const refunded = await this.prisma.smsBillingRecord.findFirst({ where: { messageId: message.messageId, billingStatus: 'refunded' } });
@@ -2341,7 +2345,6 @@ export class SendChainService implements OnModuleInit, OnModuleDestroy {
const transaction = await this.billing.refund({
tenantId: message.tenantId,
amountCents: message.amountCents,
smsUnits: message.billingUnits,
relatedType: 'sms_message_record',
relatedId: message.messageId,
remark,
@@ -2919,6 +2922,17 @@ function octetString(value: string, fixedLength: number) {
return value + '\0'.repeat(fixedLength - value.length);
}
function hasRecoveryAuditStateChanged(
previous: Record<string, unknown> | null,
current: Record<string, unknown>,
) {
if (!previous) {
return true;
}
return ['state', 'gatewayInstanceId', 'lockOwner', 'failureCategory', 'lastError', 'lastSkipReason']
.some((key) => (previous[key] ?? null) !== (current[key] ?? null));
}
function normalizeRecoveryFailureCategory(data: GatewayDownstreamRecoveryStatusDto) {
const explicit = String(data.failureCategory ?? '').trim();
if (explicit) {