refactor(bill):重构计费功能,只产生一条消费记录
This commit is contained in:
1 parent
2ce06a0274
commit
9a058a7bbc
7 files changed
+36
-62
No files matched your search
@@ -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").
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in new issue
Block a user