- 计费: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)
65 lines
2.3 KiB
Go
65 lines
2.3 KiB
Go
package store
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/shopspring/decimal"
|
|
)
|
|
|
|
// v10 回填:存量 usage_logs 按 (channel_id, 小时) 聚合进桶;标记幂等(二次执行不重复累计)。
|
|
func TestBackfillChannelHourly(t *testing.T) {
|
|
db, err := Open("sqlite", "/tmp/store_backfill_test_"+time.Now().Format("150405.000000000")+".db")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(func() {
|
|
sqlDB, _ := db.DB()
|
|
sqlDB.Close()
|
|
})
|
|
// Open 已跑过一次空回填并打标;清掉标记与空桶,注入历史明细后重放
|
|
db.Where("key = ?", "channel_hourly_backfill_v10").Delete(&SystemConfig{})
|
|
db.Exec("DELETE FROM usage_channel_hourlies")
|
|
|
|
at := time.Date(2026, 11, 9, 14, 30, 0, 0, time.UTC)
|
|
atPrev := at.Add(-2 * time.Hour)
|
|
rows := []UsageLog{
|
|
{TraceID: "b1", UserID: 1, ChannelID: 5, Status: UsageStatusSuccess, LatencyMS: 300,
|
|
InputTokens: 10, OutputTokens: 5, Cost: decimal.NewFromFloat(0.01), CreatedAt: at},
|
|
{TraceID: "b2", UserID: 1, ChannelID: 5, Status: UsageStatusError, ErrorCode: strp("upstream_http_502"),
|
|
CreatedAt: at},
|
|
{TraceID: "b3", UserID: 1, ChannelID: 5, Status: UsageStatusSuccess, LatencyMS: 100,
|
|
InputTokens: 20, OutputTokens: 10, Cost: decimal.NewFromFloat(0.02), CreatedAt: atPrev},
|
|
{TraceID: "b4", UserID: 1, ChannelID: 0, Status: UsageStatusSuccess, CreatedAt: at}, // channel 0 不入桶
|
|
}
|
|
if err := db.Create(&rows).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
backfillChannelHourly(db)
|
|
|
|
var buckets []UsageChannelHourly
|
|
db.Where("channel_id = ?", 5).Order("hour DESC").Find(&buckets)
|
|
if len(buckets) != 2 {
|
|
t.Fatalf("应有两个小时桶,实际 %d", len(buckets))
|
|
}
|
|
cur, prev := buckets[0], buckets[1]
|
|
if cur.Requests != 2 || cur.Success != 1 || cur.Errors != 1 || cur.LatencyMax != 300 {
|
|
t.Fatalf("当前桶 = %+v", cur)
|
|
}
|
|
if prev.Requests != 1 || prev.Cost.String() != "0.02" {
|
|
t.Fatalf("上一桶 = %+v", prev)
|
|
}
|
|
|
|
// 幂等:清标记重放,计数不应翻倍
|
|
db.Where("key = ?", "channel_hourly_backfill_v10").Delete(&SystemConfig{})
|
|
backfillChannelHourly(db)
|
|
var total int64
|
|
db.Model(&UsageChannelHourly{}).Where("channel_id = ?", 5).Select("COALESCE(SUM(requests),0)").Scan(&total)
|
|
if total != 3 {
|
|
t.Fatalf("回填应幂等,requests 总计 = %d,期望 3", total)
|
|
}
|
|
}
|
|
|
|
func strp(s string) *string { return &s }
|