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
+33 -9
View File
@@ -217,32 +217,56 @@ func (h *Handler) ListPricing(c *gin.Context) {
out := make([]gin.H, 0, len(rows))
for _, p := range rows {
out = append(out, gin.H{
"model_id": p.ModelID, "input_per_1k": p.InputPer1K, "output_per_1k": p.OutputPer1K, "currency": p.Currency,
"model_id": p.ModelID, "input_per_1k": p.InputPer1K, "output_per_1k": p.OutputPer1K,
"credit_weight": p.CreditWeight, "currency": p.Currency,
})
}
c.JSON(http.StatusOK, gin.H{"pricing": out})
}
// SavePricing: PUT /api/v1/admin/pricing —— 设置某模型的输入/输出单价(每 1K token)。
// SavePricing: PUT /api/v1/admin/pricing —— 设置某模型的输入/输出单价(每 1K token)+ 积分权重
func (h *Handler) SavePricing(c *gin.Context) {
var b struct {
ModelID string `json:"model_id"`
InputPer1K float64 `json:"input_per_1k"`
OutputPer1K float64 `json:"output_per_1k"`
Currency string `json:"currency"`
ModelID string `json:"model_id"`
InputPer1K float64 `json:"input_per_1k"`
OutputPer1K float64 `json:"output_per_1k"`
CreditWeight float64 `json:"credit_weight"`
Currency string `json:"currency"`
}
if err := c.ShouldBindJSON(&b); err != nil || b.ModelID == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "model_id required"})
return
}
if b.InputPer1K < 0 || b.OutputPer1K < 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "单价不能为负"})
if b.InputPer1K < 0 || b.OutputPer1K < 0 || b.CreditWeight < 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "单价/权重不能为负"})
return
}
if b.Currency == "" {
b.Currency = "CNY"
}
if err := h.db.UpsertPricing(c.Request.Context(), b.ModelID, b.InputPer1K, b.OutputPer1K, b.Currency); err != nil {
if err := h.db.UpsertPricing(c.Request.Context(), b.ModelID, b.InputPer1K, b.OutputPer1K, b.CreditWeight, b.Currency); err != nil {
c.JSON(http.StatusBadGateway, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"status": "ok"})
}
// BillingConfig: GET /api/v1/admin/billing-config —— 全局计费规则(当前仅 token→积分汇率)。
func (h *Handler) BillingConfig(c *gin.Context) {
tpc := h.db.GetSetting(c.Request.Context(), store.SettingTokensPerCredit)
c.JSON(http.StatusOK, gin.H{"tokens_per_credit": tpc}) // 空串=未设,前端回退默认
}
// SaveBillingConfig: PUT /api/v1/admin/billing-config —— 设 token→积分汇率(>0)。
func (h *Handler) SaveBillingConfig(c *gin.Context) {
var b struct {
TokensPerCredit float64 `json:"tokens_per_credit"`
}
if err := c.ShouldBindJSON(&b); err != nil || b.TokensPerCredit <= 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "tokens_per_credit 必须 > 0"})
return
}
if err := h.db.SetSetting(c.Request.Context(), store.SettingTokensPerCredit, strconv.FormatFloat(b.TokensPerCredit, 'f', -1, 64)); err != nil {
c.JSON(http.StatusBadGateway, gin.H{"error": err.Error()})
return
}