refactor(bill):重构计费功能,只产生一条消费记录

This commit is contained in:
qian committed 2026-05-20 10:58:28 +08:00
1 parent 2ce06a0274
commit 9a058a7bbc
7 files changed
+36 -62

No files matched your search

+2 -2
View File
@@ -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").
+6 -25
View File
@@ -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
}
+3 -11
View File
@@ -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
}
+13 -20
View File
@@ -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))
}
@@ -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(解冻)';
@@ -312,11 +312,11 @@ const thresholdForm = reactive({ threshold: 0 })
const thresholdLoading = ref(false)
const txTypeLabel: Record<string, string> = {
recharge: '充值', pre_deduct: '预扣', settle: '结算',
recharge: '充值', consume: '消费', pre_deduct: '预扣', settle: '结算',
refund: '退款', adjust: '调整', freeze: '冻结', unfreeze: '解冻',
}
const txTypeColor: Record<string, string> = {
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(() => {
<ASpace>
<ASelect v-model="txFilterType" placeholder="全部类型" allow-clear style="width: 140px" @change="() => { txPagination.current = 1; fetchWalletDetail() }">
<AOption value="recharge">充值</AOption>
<AOption value="consume">消费</AOption>
<AOption value="adjust">调整</AOption>
<AOption value="pre_deduct">预扣</AOption>
<AOption value="settle">结算</AOption>
+2 -2
View File
@@ -24,9 +24,9 @@ const presetAmounts = [10, 50, 100, 500]
const txTypeLabel: Record<string, string> = {
recharge: '充值',
consume: '消费',
pre_deduct: '预扣',
settle: '结算',
consumption: '消费',
refund: '退款',
adjust: '调整',
freeze: '冻结',
@@ -36,9 +36,9 @@ const txTypeLabel: Record<string, string> = {
const txTypeBadgeClass: Record<string, string> = {
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',