Files
openteam/server/internal/app/app.go
T
SakurasanandClaude ec4de8d913 M0-M4: 推倒重来基线(基建+用户/密钥/核心代理+前端+管理后台+三协议互转)
- 后端 Go+Gin+GORM: 配置(OT_ env)/SQLite/Postgres 双驱动、用户体系(argon2id+JWT access/refresh)、
  API Key(sk- 48位, 仅存 SHA-256 哈希)
- 代理网关: /v1/chat/completions、/v1/responses、/v1/messages、/v1/models;错误按客户端协议返回
- 三协议互转(convert 包): Chat↔Messages↔Responses 请求/响应 + 流式 SSE 逐事件转换(直通优先)
- 用量计费: 异步批量记账、余额扣减、balance_logs、usage_daily 日聚合
- 管理 API: 用户/渠道 CRUD+测试+模型导入/模型定价+绑定/统计/系统配置
- 前端 Vue3+TS+Tailwind(taste-skill 设计 tokens): Landing/登录注册/控制台/管理后台,
  自建组件+Phosphor 图标+自建 SVG 趋势图, 已过 web-design-guidelines 复查
- mock 上游: OpenAI+Anthropic 双协议模拟(含流式)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-15 15:34:06 +08:00

133 lines
3.6 KiB
Go

// 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
}