refactor:统一价格显示为6位小数

This commit is contained in:
qian committed 2026-05-22 00:08:11 +08:00
1 parent e251787051
commit 0d56f7d292
17 files changed
+36 -37

No files matched your search

+1
View File
@@ -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),大幅节省存储空间
+9 -9
View File
@@ -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 按模型名称拉取官方模型信息(上下文长度+能力特性)
+3 -3
View File
@@ -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)
+1 -1
View File
@@ -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 定价阶梯行(绝对价格)
+2 -2
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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")
}
+4 -4
View File
@@ -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,
})
}
+4 -4
View File
@@ -430,19 +430,19 @@ onMounted(() => {
<div class="official-prices mt-2">
<div class="official-price-row">
<span class="official-price-label">输入价格</span>
<span class="official-price-value">${{ src.pricing.input_price.toFixed(4) }} / 1M</span>
<span class="official-price-value">${{ src.pricing.input_price.toFixed(2) }} / 1M</span>
</div>
<div class="official-price-row">
<span class="official-price-label">输出价格</span>
<span class="official-price-value">${{ src.pricing.output_price.toFixed(4) }} / 1M</span>
<span class="official-price-value">${{ src.pricing.output_price.toFixed(2) }} / 1M</span>
</div>
<div class="official-price-row">
<span class="official-price-label">缓存读取</span>
<span class="official-price-value">{{ src.pricing.cache_read_price ? '$' + src.pricing.cache_read_price.toFixed(4) + ' / 1M' : '—' }}</span>
<span class="official-price-value">{{ src.pricing.cache_read_price ? '$' + src.pricing.cache_read_price.toFixed(2) + ' / 1M' : '—' }}</span>
</div>
<div class="official-price-row">
<span class="official-price-label">缓存创建</span>
<span class="official-price-value">{{ src.pricing.cache_creation_price ? '$' + src.pricing.cache_creation_price.toFixed(4) + ' / 1M' : '—' }}</span>
<span class="official-price-value">{{ src.pricing.cache_creation_price ? '$' + src.pricing.cache_creation_price.toFixed(2) + ' / 1M' : '—' }}</span>
</div>
</div>
<AButton type="primary" size="small" class="mt-2" @click="applyOfficialPricing(src.pricing)">应用此价格</AButton>
+2 -2
View File
@@ -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 '-'
},
+1 -1
View File
@@ -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,
@@ -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) || '-' } },
+1 -1
View File
@@ -146,7 +146,7 @@ onMounted(fetchHistory)
</td>
<td class="font-mono">
<template v-if="item.type === 'quota'">
+{{ Number(item.value).toFixed(2) }}
+{{ Number(item.value).toFixed(6) }}
</template>
<template v-else>
-
@@ -132,7 +132,7 @@ onMounted(fetchTransactions)
<td :class="tx.amount >= 0 ? 'text-emerald-600 font-semibold' : 'text-red-600 font-semibold'">
{{ formatAmount(tx.amount) }}
</td>
<td class="text-gray-700">${{ tx.balance_after?.toFixed(2) ?? '--' }}</td>
<td class="text-gray-700">${{ tx.balance_after?.toFixed(6) ?? '--' }}</td>
<td class="text-gray-700 text-sm">{{ tx.username || '--' }}</td>
<td class="text-gray-500 text-xs font-mono">{{ tx.request_id || '--' }}</td>
<td class="text-gray-700 text-sm">{{ tx.model_name || '--' }}</td>
+1 -3
View File
@@ -148,9 +148,7 @@ function formatTokens(n: number): string {
function formatPrice(n: number | null): string {
if (n == null || n === 0) return '-'
if (n >= 1) return '$' + n.toFixed(2)
if (n >= 0.01) return '$' + n.toFixed(3)
return '$' + n.toFixed(4)
return '$' + n.toFixed(2)
}
function formatTokenRange(min: number, max: number | null): string {
@@ -65,8 +65,8 @@ const platformLabel: Record<string, string> = {
}
function formatCost(n: number | undefined): string {
if (!n) return '$0.0000'
return '$' + n.toFixed(4)
if (!n) return '$0.000000'
return '$' + n.toFixed(6)
}
function formatTime(s: string | undefined): string {
@@ -603,7 +603,7 @@ onMounted(() => {
<template v-if="quota.quota_type === 'periodic'"> · {{ periodLabel[quota.period] }}</template>
</span>
<span class="text-sm text-gray-500">
${{ quota.quota_used.toFixed(4) }} / ${{ quota.quota_limit.toFixed(2) }}
${{ quota.quota_used.toFixed(6) }} / ${{ quota.quota_limit.toFixed(6) }}
</span>
</div>
<div class="h-2 bg-gray-100 rounded-full overflow-hidden">