feat: 金额 decimal 精确计费、failover 按失败类型封顶、渠道小时桶预聚合、分层守护测试

- 计费:19 个金额字段 float64→decimal.Decimal,DB 列保持 numeric(20,8) 零迁移;
  API 金额仍为 JSON number(MarshalJSONWithoutQuotes),前端零改动
- failover:保留时间预算,新增 OT_PROXY_FAILOVER_CAPS_NET/SERVER/RATE/QUOTA
  按失败类型封顶换渠道次数(429/配额类瞬时失败时间预算形同虚设)
- 预聚合:新增 usage_channel_hourlies(schema v10),记账事务内同步 upsert,
  长窗口渠道统计走桶不再扫明细,spent_last_30d 改扫 usage_daily,v10 一次性回填幂等
- 守护:internal/arch_test.go 用 go/parser 强制分层依赖(fail-closed)
This commit is contained in:
Sakurasan committed 2026-11-09 00:41:48 +08:00
1 parent 5d6e5bdb04
commit b2e500ff51
25 files changed
+841 -253

No files matched your search

+23 -22
View File
@@ -9,6 +9,7 @@ import (
"github.com/gin-gonic/gin"
"github.com/openteam/server/internal/pkg/resp"
"github.com/openteam/server/internal/store"
"github.com/shopspring/decimal"
"gorm.io/gorm"
)
@@ -38,7 +39,7 @@ func (h *Handler) AdminModels(c *gin.Context) {
})
}
used := len(chs) > 0
needsPricing := used && m.InputPrice == 0 && m.OutputPrice == 0 && m.CacheReadPrice == 0
needsPricing := used && m.InputPrice.IsZero() && m.OutputPrice.IsZero() && m.CacheReadPrice.IsZero()
denied := containsStr(deny, m.Name) || (len(allow) > 0 && !containsStr(allow, m.Name))
out = append(out, gin.H{
"id": m.ID, "name": m.Name,
@@ -81,7 +82,7 @@ func (h *Handler) AdminModels(c *gin.Context) {
usedIDs[u.ModelID] = true
}
for _, m := range ms {
if usedIDs[m.ID] && m.InputPrice == 0 && m.OutputPrice == 0 && m.CacheReadPrice == 0 {
if usedIDs[m.ID] && m.InputPrice.IsZero() && m.OutputPrice.IsZero() && m.CacheReadPrice.IsZero() {
unpriced++
}
}
@@ -121,16 +122,16 @@ func containsStr(list []string, s string) bool {
// AdminCreateModel POST /api/v1/admin/models
func (h *Handler) AdminCreateModel(c *gin.Context) {
var req struct {
Name string `json:"name" binding:"required,min=1,max=128"`
InputPrice float64 `json:"input_price"`
OutputPrice float64 `json:"output_price"`
CacheReadPrice float64 `json:"cache_read_price"`
CacheWritePrice float64 `json:"cache_write_price"`
ContextLength int `json:"context_length"`
SupportsTools bool `json:"supports_tools"`
SupportsVision bool `json:"supports_vision"`
SupportsReasoning bool `json:"supports_reasoning"`
Enabled *bool `json:"enabled"`
Name string `json:"name" binding:"required,min=1,max=128"`
InputPrice decimal.Decimal `json:"input_price"`
OutputPrice decimal.Decimal `json:"output_price"`
CacheReadPrice decimal.Decimal `json:"cache_read_price"`
CacheWritePrice decimal.Decimal `json:"cache_write_price"`
ContextLength int `json:"context_length"`
SupportsTools bool `json:"supports_tools"`
SupportsVision bool `json:"supports_vision"`
SupportsReasoning bool `json:"supports_reasoning"`
Enabled *bool `json:"enabled"`
}
if err := c.ShouldBindJSON(&req); err != nil {
resp.Fail(c, http.StatusBadRequest, "invalid input: "+err.Error())
@@ -159,16 +160,16 @@ func (h *Handler) AdminUpdateModel(c *gin.Context) {
return
}
var req struct {
InputPrice *float64 `json:"input_price"`
OutputPrice *float64 `json:"output_price"`
CacheReadPrice *float64 `json:"cache_read_price"`
CacheWritePrice *float64 `json:"cache_write_price"`
ContextLength *int `json:"context_length"`
SupportsTools *bool `json:"supports_tools"`
SupportsVision *bool `json:"supports_vision"`
SupportsReasoning *bool `json:"supports_reasoning"`
Enabled *bool `json:"enabled"`
Sort *int `json:"sort"`
InputPrice *decimal.Decimal `json:"input_price"`
OutputPrice *decimal.Decimal `json:"output_price"`
CacheReadPrice *decimal.Decimal `json:"cache_read_price"`
CacheWritePrice *decimal.Decimal `json:"cache_write_price"`
ContextLength *int `json:"context_length"`
SupportsTools *bool `json:"supports_tools"`
SupportsVision *bool `json:"supports_vision"`
SupportsReasoning *bool `json:"supports_reasoning"`
Enabled *bool `json:"enabled"`
Sort *int `json:"sort"`
}
if err := c.ShouldBindJSON(&req); err != nil {
resp.Fail(c, http.StatusBadRequest, "invalid input")