feat(gateway): 计费规则可后台配置 —— token→积分汇率(DB) + 每模型积分权重

让「扣费按规则扣」的规则真正可后台热调(此前汇率是 env、积分权重无接口):
- sundynix_setting KV 表 + GetSetting/SetSetting;TokensPerCredit 改为
  DB 设置优先 → env 回退 → 1000,SaveUsageEvent 按 ctx 读,改完即对后续任务生效。
- Pricing.credit_weight 纳入 UpsertPricing + ListPricing/SavePricing API。
- GET/PUT /admin/billing-config(token→积分汇率)。

live 验证:UI 存汇率=500 → billing-config/DB=500 → 新任务 credits_micro=214000
=107tok/500×1e6,规则→扣费联动精确成立。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Blizzard
2026-07-07 13:26:20 +08:00
parent 54de95aa53
commit e2fc2d366c
6 changed files with 102 additions and 19 deletions
+16 -3
View File
@@ -34,8 +34,21 @@ type UsageEvent struct {
func (UsageEvent) TableName() string { return "sundynix_usage_event" }
func (UsageEvent) isTenantScoped() {}
// TokensPerCredit 读 token→积分 汇率env TOKENS_PER_CREDIT,缺省/非法=1000)。设 1 即 token 直计
func TokensPerCredit() float64 {
// tokensPerCredit 读 token→积分 汇率:优先 DB 平台设置(后台可热调),回退 env,再回退 1000
// 设 1 即 token 直计。
func (p *Postgres) tokensPerCredit(ctx context.Context) float64 {
if p.db != nil {
if v := p.GetSetting(ctx, SettingTokensPerCredit); v != "" {
if n, err := strconv.ParseFloat(v, 64); err == nil && n > 0 {
return n
}
}
}
return tokensPerCreditEnv()
}
// tokensPerCreditEnv 是 DB 未设时的缺省来源(env TOKENS_PER_CREDIT,非法=1000)。
func tokensPerCreditEnv() float64 {
if v := os.Getenv("TOKENS_PER_CREDIT"); v != "" {
if n, err := strconv.ParseFloat(v, 64); err == nil && n > 0 {
return n
@@ -71,7 +84,7 @@ func (p *Postgres) SaveUsageEvent(ctx context.Context, ev *contract.UsageEvent)
costMicros = int64(cost * 1e6)
}
// credits_micro = total_tok / tokensPerCredit * weight,×10⁶ 存微积分。
creditsMicro := int64(float64(ev.TotalTok) / TokensPerCredit() * weight * 1e6)
creditsMicro := int64(float64(ev.TotalTok) / p.tokensPerCredit(ctx) * weight * 1e6)
row := &UsageEvent{
TenantID: ev.TenantID, Owner: ev.UserID, TaskID: ev.TaskID, Model: model,