- 计费: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)
80 lines
2.1 KiB
Go
80 lines
2.1 KiB
Go
// Package cli 命令行入口。
|
||
package cli
|
||
|
||
import (
|
||
"context"
|
||
"errors"
|
||
"log"
|
||
"net/http"
|
||
"os"
|
||
"os/signal"
|
||
"strconv"
|
||
"syscall"
|
||
"time"
|
||
|
||
"github.com/openteam/server/internal/api"
|
||
"github.com/openteam/server/internal/app"
|
||
"github.com/openteam/server/internal/config"
|
||
"github.com/openteam/server/internal/proxy"
|
||
"github.com/shopspring/decimal"
|
||
)
|
||
|
||
// serve 启动 HTTP 服务(默认子命令)。
|
||
func serve() error {
|
||
// 金额序列化为 JSON number 而非字符串:前端 types(balance/cost 等均为 number)零改动
|
||
decimal.MarshalJSONWithoutQuotes = true
|
||
cfg, err := config.Load()
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
a, err := app.New(cfg)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
defer a.Shutdown(context.Background())
|
||
|
||
gw := proxy.NewGateway(a.DB, a.Enc, a.Usage, a.Limit, proxy.Options{
|
||
UserRPS: cfg.RateLimit.UserRPS,
|
||
LogRaw: cfg.Proxy.LogRaw,
|
||
GWTimeout: cfg.Proxy.Timeout,
|
||
MaxBodyMB: cfg.Proxy.MaxBodyMB,
|
||
FailoverBudget: cfg.Proxy.FailoverBudget,
|
||
FailoverSwitchCaps: cfg.Proxy.FailoverSwitchCaps,
|
||
StreamHeartbeat: cfg.Proxy.StreamHeartbeat,
|
||
FirstTokenWait: cfg.Proxy.FirstTokenWait,
|
||
AffinityTTL: cfg.Proxy.AffinityTTL,
|
||
ChannelAcquireWait: cfg.Proxy.ChannelAcquireWait,
|
||
AuthCacheTTL: cfg.Proxy.AuthCacheTTL,
|
||
})
|
||
// 渠道健康状态变化后让目录缓存立刻失效,而不是等 TTL
|
||
a.Health.OnChange = gw.InvalidateCatalog
|
||
router := api.NewRouter(a, gw)
|
||
|
||
srv := &http.Server{
|
||
Addr: ":" + strconv.Itoa(cfg.Port),
|
||
Handler: router,
|
||
ReadHeaderTimeout: 10 * time.Second,
|
||
}
|
||
|
||
go func() {
|
||
log.Printf("openteam listening on %s (env=%s)", srv.Addr, cfg.Env)
|
||
if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||
log.Fatalf("server: %v", err)
|
||
}
|
||
}()
|
||
|
||
quit := make(chan os.Signal, 1)
|
||
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
||
<-quit
|
||
|
||
log.Println("shutting down...")
|
||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||
defer cancel()
|
||
if err := srv.Shutdown(ctx); err != nil {
|
||
log.Printf("server shutdown: %v", err)
|
||
}
|
||
a.Usage.Close()
|
||
return nil
|
||
}
|