From 9a058a7bbcca0d63d813fc9b72646cd6fae38435 Mon Sep 17 00:00:00 2001 From: qian Date: Wed, 20 May 2026 10:58:28 +0800 Subject: [PATCH] =?UTF-8?q?refactor(bill):=E9=87=8D=E6=9E=84=E8=AE=A1?= =?UTF-8?q?=E8=B4=B9=E5=8A=9F=E8=83=BD=EF=BC=8C=E5=8F=AA=E4=BA=A7=E7=94=9F?= =?UTF-8?q?=E4=B8=80=E6=9D=A1=E6=B6=88=E8=B4=B9=E8=AE=B0=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/logic/billing/reconciliation.go | 4 +-- internal/logic/billing/settlement.go | 31 ++++------------- internal/logic/billing/task_billing.go | 14 ++------ internal/logic/billing/wallet.go | 33 ++++++++----------- migrations/000004_billing_consume_type.sql | 7 ++++ .../src/views/tenant/TenantDetailPage.vue | 5 +-- web/tenant/src/views/finance/WalletPage.vue | 4 +-- 7 files changed, 36 insertions(+), 62 deletions(-) create mode 100644 migrations/000004_billing_consume_type.sql diff --git a/internal/logic/billing/reconciliation.go b/internal/logic/billing/reconciliation.go index 4273cc1..3c54c05 100644 --- a/internal/logic/billing/reconciliation.go +++ b/internal/logic/billing/reconciliation.go @@ -46,13 +46,13 @@ func RunDailyReconciliation(ctx context.Context) (*DailyReconciliationResult, er result.TotalSettled = settled.TotalCost result.RecordCount = settled.Count - // 2. 统计 bil_transactions 中昨日结算流水总额(负数,取绝对值) + // 2. 统计 bil_transactions 中昨日消费流水总额(负数,取绝对值) type txnRow struct { TotalDeduct float64 `json:"total_deduct"` } var txn txnRow err = dao.BilTransactions.Ctx(ctx). - Where("type", "settle"). + Where("type", "consume"). Where("created_at >= ?", yesterday+" 00:00:00"). Where("created_at < ?", yesterday+" 23:59:59"). Fields("COALESCE(SUM(ABS(amount)), 0) as total_deduct"). diff --git a/internal/logic/billing/settlement.go b/internal/logic/billing/settlement.go index 188c41c..1fd31f6 100644 --- a/internal/logic/billing/settlement.go +++ b/internal/logic/billing/settlement.go @@ -94,18 +94,9 @@ func Settle(ctx context.Context, tenantID, userID, apiKeyID, channelID int64, g.Log().Errorf(ctx, "settle: create billing record failed: %v", err) } - // 6. 记录流水 - recordTransaction(ctx, wallet.ID, tenantID, "settle", -actualCost, - fmt.Sprintf("settle: %s model=%s input=%d output=%d", requestID, modelName, inputTokens, outputTokens)) - - if refundAmt > 0 { - recordTransaction(ctx, wallet.ID, tenantID, "refund", refundAmt, - fmt.Sprintf("refund: %s", requestID)) - } - if supplementAmt > 0 { - recordTransaction(ctx, wallet.ID, tenantID, "pre_deduct", -supplementAmt, - fmt.Sprintf("supplement: %s", requestID)) - } + // 6. 记录消费流水(一条汇总) + recordTransaction(ctx, wallet.ID, tenantID, "consume", -actualCost, + fmt.Sprintf("consume: %s model=%s input=%d output=%d pre_deduct=%.4f actual=%.4f", requestID, modelName, inputTokens, outputTokens, preDeductAmount, actualCost)) return &SettlementResult{ PreDeductAmount: preDeductAmount, @@ -206,19 +197,9 @@ func SettleWithUsage(ctx context.Context, tenantID, userID, apiKeyID, channelID settlementResult.BillingSnapshot = snapshotJSON settlementResult.BillingSummary = summaryText - // 8. 记录流水 - // 9. 记录流水 - recordTransaction(ctx, wallet.ID, tenantID, "settle", -actualCost, - fmt.Sprintf("settle: %s model=%s input=%d output=%d", requestID, modelName, breakdown.InputTokens, breakdown.OutputTokens)) - - if refundAmt > 0 { - recordTransaction(ctx, wallet.ID, tenantID, "refund", refundAmt, - fmt.Sprintf("refund: %s", requestID)) - } - if supplementAmt > 0 { - recordTransaction(ctx, wallet.ID, tenantID, "pre_deduct", -supplementAmt, - fmt.Sprintf("supplement: %s", requestID)) - } + // 8. 记录消费流水(一条汇总) + recordTransaction(ctx, wallet.ID, tenantID, "consume", -actualCost, + fmt.Sprintf("consume: %s model=%s input=%d output=%d pre_deduct=%.4f actual=%.4f", requestID, modelName, breakdown.InputTokens, breakdown.OutputTokens, preDeductAmount, actualCost)) return settlementResult, nil } diff --git a/internal/logic/billing/task_billing.go b/internal/logic/billing/task_billing.go index b19ecc9..3263222 100644 --- a/internal/logic/billing/task_billing.go +++ b/internal/logic/billing/task_billing.go @@ -187,17 +187,9 @@ func (b *TaskBillingProviderImpl) SettleTaskSuccess(ctx context.Context, tenantI } } - // 6. 记录流水 - recordTransaction(ctx, wallet.ID, tenantID, "settle", -actualCost, - fmt.Sprintf("settle: %s model=%s", requestID, modelName)) - if result.RefundAmount > 0 { - recordTransaction(ctx, wallet.ID, tenantID, "refund", result.RefundAmount, - fmt.Sprintf("refund: %s", requestID)) - } - if result.SupplementAmount > 0 { - recordTransaction(ctx, wallet.ID, tenantID, "pre_deduct", -result.SupplementAmount, - fmt.Sprintf("supplement: %s", requestID)) - } + // 6. 记录消费流水(一条汇总) + recordTransaction(ctx, wallet.ID, tenantID, "consume", -actualCost, + fmt.Sprintf("consume: %s model=%s pre_deduct=%.4f actual=%.4f", requestID, modelName, preDeductAmount, actualCost)) return result, nil } diff --git a/internal/logic/billing/wallet.go b/internal/logic/billing/wallet.go index 035e472..c3affe7 100644 --- a/internal/logic/billing/wallet.go +++ b/internal/logic/billing/wallet.go @@ -188,8 +188,8 @@ return 1 return false, gerror.New("insufficient balance") } - // 异步同步到 DB - go preDeductSyncDB(tenantID, amount, requestID) + // 同步同步到 DB(确保 DB frozen_balance 在返回前已更新) + preDeductSyncDB(ctx, tenantID, amount) return true, nil } @@ -223,37 +223,31 @@ func preDeductDB(ctx context.Context, tenantID int64, amount float64, requestID // 清除缓存 walletCache.Delete(ctx, fmt.Sprintf("%d", tenantID)) - // 记录预扣流水 - recordTransaction(ctx, w.ID, tenantID, "freeze", amount, "pre-deduct: "+requestID) - return true, nil } -// preDeductSyncDB async sync pre-deduct to DB -func preDeductSyncDB(tenantID int64, amount float64, requestID string) { - bgCtx := context.Background() - +// preDeductSyncDB sync pre-deduct to DB(同步调用,确保 DB frozen_balance 在返回前已更新) +func preDeductSyncDB(ctx context.Context, tenantID int64, amount float64) { type walletRow struct { ID int64 } var w *walletRow - err := dao.BilWallets.Ctx(bgCtx). + err := dao.BilWallets.Ctx(ctx). Where("tenant_id", tenantID). Fields("id"). Scan(&w) if err != nil || w == nil { - g.Log().Errorf(bgCtx, "pre-deduct sync: wallet not found for tenant %d", tenantID) + g.Log().Errorf(ctx, "pre-deduct sync: wallet not found for tenant %d", tenantID) return } - _, err = g.DB().Exec(bgCtx, + _, err = g.DB().Exec(ctx, "UPDATE bil_wallets SET frozen_balance = frozen_balance + ?, updated_at = ? WHERE id = ? ", amount, time.Now(), w.ID) if err != nil { - g.Log().Errorf(bgCtx, "pre-deduct sync: %v", err) + g.Log().Errorf(ctx, "pre-deduct sync: %v", err) } - recordTransaction(bgCtx, w.ID, tenantID, "freeze", amount, "pre-deduct: "+requestID) } // UnfreezePreDeduct 解冻预扣金额(请求失败时调用) @@ -270,20 +264,20 @@ func UnfreezePreDeduct(ctx context.Context, tenantID int64, requestID string, am if err == nil { // 解冻 g.Redis().Do(ctx, "HINCRBYFLOAT", walletRedisKey, "frozen_balance", -amount) - go unfreezeSyncDB(tenantID, amount, requestID) + go unfreezeSyncDB(tenantID, amount) return } // Redis 失败,直接 DB 解冻 - unfreezeDB(ctx, tenantID, amount, requestID) + unfreezeDB(ctx, tenantID, amount) } -func unfreezeSyncDB(tenantID int64, amount float64, requestID string) { +func unfreezeSyncDB(tenantID int64, amount float64) { bgCtx := context.Background() - unfreezeDB(bgCtx, tenantID, amount, requestID) + unfreezeDB(bgCtx, tenantID, amount) } -func unfreezeDB(ctx context.Context, tenantID int64, amount float64, requestID string) { +func unfreezeDB(ctx context.Context, tenantID int64, amount float64) { type walletRow struct { ID int64 `json:"id"` } @@ -300,7 +294,6 @@ func unfreezeDB(ctx context.Context, tenantID int64, amount float64, requestID s "UPDATE bil_wallets SET frozen_balance = GREATEST(frozen_balance - ?, 0), updated_at = ? WHERE id = ? ", amount, time.Now(), w.ID) - recordTransaction(ctx, w.ID, tenantID, "unfreeze", amount, "unfreeze: "+requestID) walletCache.Delete(ctx, fmt.Sprintf("%d", tenantID)) } diff --git a/migrations/000004_billing_consume_type.sql b/migrations/000004_billing_consume_type.sql new file mode 100644 index 0000000..1db3e65 --- /dev/null +++ b/migrations/000004_billing_consume_type.sql @@ -0,0 +1,7 @@ +-- +goose Up +-- 更新 bil_transactions.type 注释,新增 consume(消费)类型 +-- 预扣模式优化:预扣/解冻不再写流水,结算合并为一条 consume 记录 +COMMENT ON COLUMN bil_transactions.type IS '类型:consume(消费)/ recharge(充值)/ adjust(调整)/ pre_deduct(预扣,已废弃)/ settle(结算,已废弃)/ refund(退款,已废弃)/ freeze(冻结,已废弃)/ unfreeze(解冻,已废弃)'; + +-- +goose Down +COMMENT ON COLUMN bil_transactions.type IS '类型:recharge(充值)/ pre_deduct(预扣)/ settle(结算)/ refund(退款)/ adjust(调整)/ freeze(冻结)/ unfreeze(解冻)'; diff --git a/web/admin/src/views/tenant/TenantDetailPage.vue b/web/admin/src/views/tenant/TenantDetailPage.vue index de9c72d..1019df8 100644 --- a/web/admin/src/views/tenant/TenantDetailPage.vue +++ b/web/admin/src/views/tenant/TenantDetailPage.vue @@ -312,11 +312,11 @@ const thresholdForm = reactive({ threshold: 0 }) const thresholdLoading = ref(false) const txTypeLabel: Record = { - recharge: '充值', pre_deduct: '预扣', settle: '结算', + recharge: '充值', consume: '消费', pre_deduct: '预扣', settle: '结算', refund: '退款', adjust: '调整', freeze: '冻结', unfreeze: '解冻', } const txTypeColor: Record = { - recharge: 'green', adjust: 'arcoblue', refund: 'cyan', + recharge: 'green', consume: 'orangered', adjust: 'arcoblue', refund: 'cyan', pre_deduct: 'orangered', settle: 'orange', freeze: 'red', unfreeze: 'purple', } @@ -555,6 +555,7 @@ onMounted(() => { 充值 + 消费 调整 预扣 结算 diff --git a/web/tenant/src/views/finance/WalletPage.vue b/web/tenant/src/views/finance/WalletPage.vue index a520c3e..4c7a08f 100644 --- a/web/tenant/src/views/finance/WalletPage.vue +++ b/web/tenant/src/views/finance/WalletPage.vue @@ -24,9 +24,9 @@ const presetAmounts = [10, 50, 100, 500] const txTypeLabel: Record = { recharge: '充值', + consume: '消费', pre_deduct: '预扣', settle: '结算', - consumption: '消费', refund: '退款', adjust: '调整', freeze: '冻结', @@ -36,9 +36,9 @@ const txTypeLabel: Record = { const txTypeBadgeClass: Record = { recharge: 'badge-success', refund: 'badge-success', + consume: 'badge-danger', pre_deduct: 'badge-danger', settle: 'badge-danger', - consumption: 'badge-danger', adjust: 'badge-warning', freeze: 'badge-gray', unfreeze: 'badge-gray',