From 0d56f7d292f9c545f41ba9f882fb720c81167d9d Mon Sep 17 00:00:00 2001 From: qian Date: Fri, 22 May 2026 00:08:11 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=E7=BB=9F=E4=B8=80=E4=BB=B7=E6=A0=BC?= =?UTF-8?q?=E6=98=BE=E7=A4=BA=E4=B8=BA6=E4=BD=8D=E5=B0=8F=E6=95=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CLAUDE.md | 1 + internal/logic/admin/model.go | 18 +++++++++--------- internal/logic/billing/alert.go | 6 +++--- internal/logic/billing/pricing.go | 2 +- internal/logic/billing/settlement.go | 4 ++-- internal/logic/billing/task_billing.go | 2 +- internal/logic/payment/fulfill.go | 2 +- internal/logic/tenant/model_comparison.go | 8 ++++---- web/admin/src/views/ai/ModelPricingPage.vue | 8 ++++---- web/admin/src/views/ai/TaskLogsPage.vue | 4 ++-- web/admin/src/views/finance/BillingPage.vue | 2 +- .../src/views/finance/RedemptionsPage.vue | 2 +- web/tenant/src/views/finance/RedeemPage.vue | 2 +- .../src/views/finance/TransactionsPage.vue | 2 +- web/tenant/src/views/service/ModelsPage.vue | 4 +--- web/tenant/src/views/service/TaskLogsPage.vue | 4 ++-- web/tenant/src/views/team/MemberDetailPage.vue | 2 +- 17 files changed, 36 insertions(+), 37 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 5b2f463..87b98fa 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -340,6 +340,7 @@ wallets 表包含 `frozen_balance` 字段,用于支付中/退款中的金额 - JSONB 字段用于灵活配置(功能权限、通知偏好等) - 字符串字段注意长度限制,不要用 `VARCHAR(255)` 一刀切 - 金额字段统一用 `NUMERIC(20,10)`(PostgreSQL 中 NUMERIC 与 DECIMAL 等价),避免累计计算精度丢失,不用 FLOAT +- **资金显示精度**:系统中所有资金相关数值(钱包余额、交易金额、计费金额、单价等)在 API 响应和前端展示时一律精确到小数点后 6 位(特殊说明除外),与 `NUMERIC(20,10)` 存储精度对齐,保证前端显示与后端计算结果一致 - 索引按实际查询场景创建,不过度索引 - **追加写表使用 BRIN 索引**:`usage_logs`、`audit_logs`、`billing_records` 等按时间追加写入的表,在 `created_at` 字段上使用 BRIN 索引(而非 B-tree),大幅节省存储空间 diff --git a/internal/logic/admin/model.go b/internal/logic/admin/model.go index 36d6019..ac75130 100644 --- a/internal/logic/admin/model.go +++ b/internal/logic/admin/model.go @@ -483,10 +483,10 @@ func fetchLiteLLMSource(ctx context.Context, modelName string) *v1.OfficialPrici source.MaxContext = entry.MaxInputTokens source.MaxOutput = entry.MaxOutputTokens source.Pricing = &v1.OfficialPricingItem{ - InputPrice: roundTo4(inputPrice), - OutputPrice: roundTo4(outputPrice), - CacheReadPrice: roundTo4(cacheReadPrice), - CacheCreationPrice: roundTo4(cacheCreationPrice), + InputPrice: roundTo2(inputPrice), + OutputPrice: roundTo2(outputPrice), + CacheReadPrice: roundTo2(cacheReadPrice), + CacheCreationPrice: roundTo2(cacheCreationPrice), BillingMode: billingMode, } @@ -517,14 +517,14 @@ func fetchModelsDevSource(ctx context.Context, modelName string) *v1.OfficialPri source.Found = true source.Provider = entry.Provider source.Pricing = &v1.OfficialPricingItem{ - InputPrice: roundTo4(entry.Input), - OutputPrice: roundTo4(func() float64 { + InputPrice: roundTo2(entry.Input), + OutputPrice: roundTo2(func() float64 { if entry.Output != nil { return *entry.Output } return 0 }()), - CacheReadPrice: roundTo4(cacheReadPrice), + CacheReadPrice: roundTo2(cacheReadPrice), BillingMode: "token", } @@ -532,8 +532,8 @@ func fetchModelsDevSource(ctx context.Context, modelName string) *v1.OfficialPri return source } -func roundTo4(v float64) float64 { - return float64(int(v*10000+0.5)) / 10000 +func roundTo2(v float64) float64 { + return float64(int(v*100+0.5)) / 100 } // FetchOfficialModelInfo 按模型名称拉取官方模型信息(上下文长度+能力特性) diff --git a/internal/logic/billing/alert.go b/internal/logic/billing/alert.go index 64215c9..bc05bd3 100644 --- a/internal/logic/billing/alert.go +++ b/internal/logic/billing/alert.go @@ -20,7 +20,7 @@ func CheckBalanceWarning(ctx context.Context, tenantID int64) (bool, float64, fl available := AvailableBalance(wallet) if wallet.WarningThreshold > 0 && available <= wallet.WarningThreshold { - g.Log().Warningf(ctx, "[BALANCE WARNING] tenant=%d available=%.4f threshold=%.4f", + g.Log().Warningf(ctx, "[BALANCE WARNING] tenant=%d available=%.6f threshold=%.6f", tenantID, available, wallet.WarningThreshold) // 发送余额预警通知(异步,失败不影响主流程) @@ -28,8 +28,8 @@ func CheckBalanceWarning(ctx context.Context, tenantID int64) (bool, float64, fl bgCtx := context.Background() engine := common.NewNotificationEngine() variables := map[string]any{ - "available": fmt.Sprintf("%.4f", available), - "threshold": fmt.Sprintf("%.4f", wallet.WarningThreshold), + "available": fmt.Sprintf("%.6f", available), + "threshold": fmt.Sprintf("%.6f", wallet.WarningThreshold), } if notifyErr := engine.SendBroadcast(bgCtx, tenantID, "balance_warning", variables, "owner,admin"); notifyErr != nil { g.Log().Errorf(bgCtx, "[BALANCE WARNING] send notification failed: tenant=%d err=%v", tenantID, notifyErr) diff --git a/internal/logic/billing/pricing.go b/internal/logic/billing/pricing.go index 0af4f3b..e9f70e9 100644 --- a/internal/logic/billing/pricing.go +++ b/internal/logic/billing/pricing.go @@ -479,7 +479,7 @@ func EstimatePreDeductAmount(ctx context.Context, tenantID int64, modelName stri return 0.001, nil } - return math.Ceil(breakdown.TotalCost*10000) / 10000, nil + return math.Ceil(breakdown.TotalCost*1000000) / 1000000, nil } // pricingTierRow 定价阶梯行(绝对价格) diff --git a/internal/logic/billing/settlement.go b/internal/logic/billing/settlement.go index dfae93d..cf01bac 100644 --- a/internal/logic/billing/settlement.go +++ b/internal/logic/billing/settlement.go @@ -125,7 +125,7 @@ func Settle(ctx context.Context, tenantID, userID, apiKeyID, channelID int64, // 7. 记录消费流水(事务外,best-effort) 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), + fmt.Sprintf("consume: %s model=%s input=%d output=%d pre_deduct=%.6f actual=%.6f", requestID, modelName, inputTokens, outputTokens, preDeductAmount, actualCost), userID, requestID, modelName, billingID, "billing_record", projectID, apiKeyID, "", balanceAfter, frozenAfter) return &SettlementResult{ @@ -262,7 +262,7 @@ func SettleWithUsage(ctx context.Context, tenantID, userID, apiKeyID, channelID // 9. 记录消费流水(事务外,best-effort) 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), + fmt.Sprintf("consume: %s model=%s input=%d output=%d pre_deduct=%.6f actual=%.6f", requestID, modelName, breakdown.InputTokens, breakdown.OutputTokens, preDeductAmount, actualCost), userID, requestID, modelName, billingID, "billing_record", txProjectID, apiKeyID, "", balanceAfter, frozenAfter) return settlementResult, nil diff --git a/internal/logic/billing/task_billing.go b/internal/logic/billing/task_billing.go index 331dd8b..6b614b8 100644 --- a/internal/logic/billing/task_billing.go +++ b/internal/logic/billing/task_billing.go @@ -222,7 +222,7 @@ func (b *TaskBillingProviderImpl) SettleTaskSuccess(ctx context.Context, tenantI // 7. 记录消费流水(事务外,best-effort) recordTransaction(ctx, wallet.ID, tenantID, "consume", -actualCost, - fmt.Sprintf("consume: %s model=%s pre_deduct=%.4f actual=%.4f", requestID, modelName, preDeductAmount, actualCost), + fmt.Sprintf("consume: %s model=%s pre_deduct=%.6f actual=%.6f", requestID, modelName, preDeductAmount, actualCost), userID, requestID, modelName, taskBillingID, "billing_record", 0, apiKeyID, taskID, balanceAfter, frozenAfter) return result, nil diff --git a/internal/logic/payment/fulfill.go b/internal/logic/payment/fulfill.go index b27028f..d552b4c 100644 --- a/internal/logic/payment/fulfill.go +++ b/internal/logic/payment/fulfill.go @@ -49,7 +49,7 @@ func FulfillOrder(ctx context.Context, orderID int64) error { case "recharge": // 充值金额为 CNY,转换为 USD 后入账钱包 usdAmount := billing.ConvertCNYToUSD(ctx, order.FinalAmount) - err = creditWallet(ctx, order.TenantID, usdAmount, fmt.Sprintf("Recharge: order #%d (CNY %.2f → USD %.4f)", orderID, order.FinalAmount, usdAmount)) + err = creditWallet(ctx, order.TenantID, usdAmount, fmt.Sprintf("Recharge: order #%d (CNY %.2f → USD %.6f)", orderID, order.FinalAmount, usdAmount)) if err != nil { return gerror.Wrapf(err, "credit wallet failed") } diff --git a/internal/logic/tenant/model_comparison.go b/internal/logic/tenant/model_comparison.go index f87ca69..7459733 100644 --- a/internal/logic/tenant/model_comparison.go +++ b/internal/logic/tenant/model_comparison.go @@ -114,8 +114,8 @@ func (s *sTenant) ModelComparison(ctx context.Context, req *v1.ModelComparisonRe SuccessRate: math.Round(successRate*100) / 100, AvgLatencyMs: math.Round(row.AvgLatency*100) / 100, P95LatencyMs: math.Round(row.P95Latency*100) / 100, - TotalCost: math.Round(row.TotalCost*100000000) / 100000000, - AvgCostPerRequest: math.Round(avgCost*100000000) / 100000000, + TotalCost: math.Round(row.TotalCost*1000000) / 1000000, + AvgCostPerRequest: math.Round(avgCost*1000000) / 1000000, InputTokens: row.InputTokens, OutputTokens: row.OutputTokens, }) @@ -137,7 +137,7 @@ func (s *sTenant) ModelComparison(ctx context.Context, req *v1.ModelComparisonRe return &v1.ModelComparisonRes{ Summary: v1.ModelComparisonSummary{ TotalRequests: totalRequests, - TotalCost: math.Round(totalCost*100000000) / 100000000, + TotalCost: math.Round(totalCost*1000000) / 1000000, Recommended: recommended, Reason: reason, }, @@ -234,7 +234,7 @@ func fetchTrends(ctx context.Context, tenantID int64, since string, models []str dayMap[day] = append(dayMap[day], v1.ModelTrendDayItem{ ModelName: r["model_name"].String(), Requests: r["requests"].Int64(), - Cost: math.Round(r["cost"].Float64()*100000000) / 100000000, + Cost: math.Round(r["cost"].Float64()*1000000) / 1000000, LatencyMs: math.Round(r["latency"].Float64()*100) / 100, }) } diff --git a/web/admin/src/views/ai/ModelPricingPage.vue b/web/admin/src/views/ai/ModelPricingPage.vue index 2a96c12..9a3d8ed 100644 --- a/web/admin/src/views/ai/ModelPricingPage.vue +++ b/web/admin/src/views/ai/ModelPricingPage.vue @@ -430,19 +430,19 @@ onMounted(() => {
输入价格 - ${{ src.pricing.input_price.toFixed(4) }} / 1M + ${{ src.pricing.input_price.toFixed(2) }} / 1M
输出价格 - ${{ src.pricing.output_price.toFixed(4) }} / 1M + ${{ src.pricing.output_price.toFixed(2) }} / 1M
缓存读取 - {{ src.pricing.cache_read_price ? '$' + src.pricing.cache_read_price.toFixed(4) + ' / 1M' : '—' }} + {{ src.pricing.cache_read_price ? '$' + src.pricing.cache_read_price.toFixed(2) + ' / 1M' : '—' }}
缓存创建 - {{ src.pricing.cache_creation_price ? '$' + src.pricing.cache_creation_price.toFixed(4) + ' / 1M' : '—' }} + {{ src.pricing.cache_creation_price ? '$' + src.pricing.cache_creation_price.toFixed(2) + ' / 1M' : '—' }}
应用此价格 diff --git a/web/admin/src/views/ai/TaskLogsPage.vue b/web/admin/src/views/ai/TaskLogsPage.vue index 3c03fab..5520d6c 100644 --- a/web/admin/src/views/ai/TaskLogsPage.vue +++ b/web/admin/src/views/ai/TaskLogsPage.vue @@ -109,10 +109,10 @@ const columns: TableColumnData[] = [ width: 100, render({ record }) { if (record.billing_settled && record.actual_cost > 0) { - return `$${record.actual_cost.toFixed(4)}` + return `$${record.actual_cost.toFixed(6)}` } if (record.pre_deduct_amount > 0) { - return `$${record.pre_deduct_amount.toFixed(4)} (预扣)` + return `$${record.pre_deduct_amount.toFixed(6)} (预扣)` } return '-' }, diff --git a/web/admin/src/views/finance/BillingPage.vue b/web/admin/src/views/finance/BillingPage.vue index 99f055d..9341775 100644 --- a/web/admin/src/views/finance/BillingPage.vue +++ b/web/admin/src/views/finance/BillingPage.vue @@ -34,7 +34,7 @@ const billColumns: TableColumnData[] = [ { title: '输出Token', dataIndex: 'output_tokens', width: 100 }, { title: '总费用', dataIndex: 'total_cost', width: 100, - render({ record }) { return `$${(record.total_cost || 0).toFixed(4)}` }, + render({ record }) { return `$${(record.total_cost || 0).toFixed(6)}` }, }, { title: '状态', dataIndex: 'status', width: 80, diff --git a/web/admin/src/views/finance/RedemptionsPage.vue b/web/admin/src/views/finance/RedemptionsPage.vue index 990342a..ec4d48d 100644 --- a/web/admin/src/views/finance/RedemptionsPage.vue +++ b/web/admin/src/views/finance/RedemptionsPage.vue @@ -86,7 +86,7 @@ const usageColumns: TableColumnData[] = [ const colors: any = { quota: 'green', plan: 'blue', duration: 'orangered' } return h(Tag, { color: colors[record.type], size: 'small' }, () => labels[record.type] || record.type) }}, - { title: '面值', dataIndex: 'value', width: 100, render({ record }) { return record.type === 'quota' ? `+${Number(record.value).toFixed(2)}` : '-' } }, + { title: '面值', dataIndex: 'value', width: 100, render({ record }) { return record.type === 'quota' ? `+${Number(record.value).toFixed(6)}` : '-' } }, { title: '组织名称', dataIndex: 'tenant_name', width: 140 }, { title: '兑换人', dataIndex: 'username', width: 120 }, { title: '时间', dataIndex: 'created_at', width: 170, render({ record }) { return record.created_at?.substring(0, 19) || '-' } }, diff --git a/web/tenant/src/views/finance/RedeemPage.vue b/web/tenant/src/views/finance/RedeemPage.vue index df1099a..b63df34 100644 --- a/web/tenant/src/views/finance/RedeemPage.vue +++ b/web/tenant/src/views/finance/RedeemPage.vue @@ -146,7 +146,7 @@ onMounted(fetchHistory)