refactor:统一价格显示为6位小数
This commit is contained in:
1 parent
e251787051
commit
0d56f7d292
17 files changed
+36
-37
No files matched your search
@@ -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),大幅节省存储空间
|
||||
|
||||
|
||||
@@ -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 按模型名称拉取官方模型信息(上下文长度+能力特性)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 定价阶梯行(绝对价格)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 '-'
|
||||
},
|
||||
|
||||
@@ -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) || '-' } },
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in new issue
Block a user