// Package app 应用容器:装配配置、数据库、密码/加密/JWT 与记账器。 package app import ( "context" "log" "time" "github.com/openteam/server/internal/config" "github.com/openteam/server/internal/pkg/crypto" "github.com/openteam/server/internal/pkg/jwt" "github.com/openteam/server/internal/store" "github.com/openteam/server/internal/usage" "gorm.io/gorm" ) type App struct { Cfg *config.Config DB *gorm.DB Hasher *crypto.PasswordHasher Enc *crypto.Encryptor JWT *jwt.Manager Usage *usage.Recorder startedAt time.Time } func New(cfg *config.Config) (*App, error) { db, err := store.Open(cfg.DB.Driver, cfg.DB.DSN) if err != nil { return nil, err } a := &App{ Cfg: cfg, DB: db, Hasher: crypto.NewPasswordHasher(cfg.Auth.Argon2Time, cfg.Auth.Argon2Memory, cfg.Auth.Argon2Threads, cfg.Auth.Argon2KeyLen, cfg.Auth.SaltLen), Enc: crypto.NewEncryptor(cfg.Master), JWT: jwt.NewManager(cfg.JWT.Secret, cfg.JWT.Issuer, cfg.JWT.AccessTTL, cfg.JWT.RefreshTTL), startedAt: time.Now(), } a.Usage = usage.NewRecorder(db) if err := a.Seed(); err != nil { return nil, err } return a, nil } func (a *App) Close() { a.Usage.Close() } // Seed 首次启动初始化:管理员账号 + 默认渠道 + 默认模型。 func (a *App) Seed() error { // 1. 管理员(从环境变量读取,默认 admin/admin123,生产必须改) var count int64 a.DB.Model(&store.User{}).Where("role = ?", store.RoleAdmin).Count(&count) if count == 0 { hash, err := a.Hasher.HashPassword(envOr("OT_ADMIN_PASSWORD", "admin123")) if err != nil { return err } admin := store.User{ Username: envOr("OT_ADMIN_USERNAME", "admin"), Email: envOr("OT_ADMIN_EMAIL", "admin@localhost"), PasswordHash: hash, Role: store.RoleAdmin, Balance: 1000, // 初始余额,便于联调;生产由充值/调整决定 Status: store.UserStatusActive, } if err := a.DB.Create(&admin).Error; err != nil { return err } log.Printf("seed: created admin user %q (change the default password!)", admin.Username) } // 2. 默认渠道(配置了上游 key 时创建) if a.Cfg.Proxy.UpstreamKey != "" { var chCount int64 a.DB.Model(&store.Channel{}).Count(&chCount) if chCount == 0 { enc, err := a.Enc.Encrypt(a.Cfg.Proxy.UpstreamKey) if err != nil { return err } ch := store.Channel{ Name: a.Cfg.Proxy.DefaultChannelName, Provider: store.ChannelProviderOpenAI, BaseURL: a.Cfg.Proxy.UpstreamBaseURL, APIKeyEnc: enc, Weight: 1, Priority: 0, TimeoutMS: int(a.Cfg.Proxy.Timeout / time.Millisecond), MaxConcurrency: 16, HealthStatus: store.ChannelHealthHealthy, Enabled: true, } if err := a.DB.Create(&ch).Error; err != nil { return err } // 默认模型 + 绑定 m := store.Model{ Name: a.Cfg.Proxy.DefaultModel, DisplayName: a.Cfg.Proxy.DefaultModel, InputPrice: 0.15, // 每百万 token,示例价 OutputPrice: 0.60, Enabled: true, } if err := a.DB.Create(&m).Error; err == nil { a.DB.Create(&store.ChannelModelBinding{ChannelID: ch.ID, ModelID: m.ID, UpstreamModel: m.Name}) } log.Printf("seed: created default channel %q (%s)", ch.Name, ch.BaseURL) } } return nil } func (a *App) Shutdown(ctx context.Context) { a.Usage.Close() if sqlDB, err := a.DB.DB(); err == nil { _ = sqlDB.Close() } _ = ctx } func envOr(key, fallback string) string { v := envLookup(key) if v == "" { return fallback } return v }