Files
openteam/server/internal/store/models.go
T
Sakurasan 360c6b33a6 M0+M1: 基建 + 用户/密钥/核心代理
后端 (Go/Gin/GORM):
- 配置(viper+env)、SQLite/Postgres 迁移、argon2id、AES-GCM 渠道密钥、JWT+refresh cookie
- 用户注册/登录/刷新/登出、API Key CRUD(仅存哈希、明文一次展示)
- 代理网关: /v1/chat/completions、/v1/responses、/v1/models 直通 OpenAI 渠道
  非流式+流式(SSE 零缓冲转发), 用量捕获(chat 末块/responses completed 嵌套),
  OpenAI 错误格式(401/402/404/502), 余额检查
- 异步批量记账 + 余额流水 + 日聚合, admin 用户/余额/配置 API
- 单测: crypto/jwt/apikey/流式 usage 提取

前端 (Vue3+TS+Vite+Tailwind v4):
- taste-skill 设计 tokens: 深色仪表盘, 石墨+信号铜色, Outfit+JetBrains Mono
- Landing/登录/注册, 控制台(仪表盘图表/密钥管理/用量明细)
- 基础组件 Button/Input/Badge/Modal, ECharts 用量图

部署: docker-compose(nginx+api+postgres), 双 Dockerfile, nginx SSE 反代
联调: scripts/mockupstream 本地 mock 上游, 端到端验证通过
2026-08-15 13:10:47 +08:00

203 lines
9.3 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Package store 数据模型与仓储层(GORM)。
// 字段设计对应 PLANNING.md §5:金额/价格 numeric(20,8),token bigint,时间 UTC。
package store
import (
"time"
)
// 角色 / 状态枚举(字符串存库,便于阅读与迁移)
const (
RoleUser = "user"
RoleAdmin = "admin"
UserStatusActive = "active"
UserStatusDisabled = "disabled"
KeyStatusActive = "active"
KeyStatusRevoked = "revoked"
ChannelProviderOpenAI = "openai"
ChannelProviderAnthropic = "anthropic"
ChannelProviderCompatible = "compatible"
ChannelHealthHealthy = "healthy"
ChannelHealthDegraded = "degraded"
ChannelHealthCooldown = "cooldown"
UsageStatusSuccess = "success"
UsageStatusError = "error"
UsageStatusCanceled = "canceled"
BalanceTypeRecharge = "recharge"
BalanceTypeUsage = "usage"
BalanceTypeRefund = "refund"
BalanceTypeAdminAdjust = "admin_adjust"
RechargeStatusPending = "pending"
RechargeStatusCredited = "credited"
RechargeStatusRejected = "rejected"
RechargeMethodManual = "manual"
RechargeMethodOnline = "online"
)
// User 用户(PLANNING §5.1)
type User struct {
ID uint64 `gorm:"primaryKey;autoIncrement" json:"id"`
Username string `gorm:"uniqueIndex;size:64;not null" json:"username"`
Email string `gorm:"uniqueIndex;size:255;not null" json:"email"`
PasswordHash string `gorm:"size:255;not null" json:"-"`
Role string `gorm:"size:16;not null;default:user" json:"role"`
Balance float64 `gorm:"type:numeric(20,8);not null;default:0" json:"balance"`
Status string `gorm:"size:16;not null;default:active" json:"status"`
InviteCode *string `json:"invite_code,omitempty"`
LastLoginAt *time.Time `json:"last_login_at,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// APIKey 密钥(PLANNING §5.2):库中只存 SHA-256 哈希 + 展示前缀
type APIKey struct {
ID uint64 `gorm:"primaryKey;autoIncrement" json:"id"`
UserID uint64 `gorm:"index;not null" json:"user_id"`
Name string `gorm:"size:64;not null" json:"name"`
KeyHash string `gorm:"uniqueIndex;size:64;not null" json:"-"`
KeyPrefix string `gorm:"size:32;not null" json:"key_prefix"`
QuotaTokensPerDay *int64 `json:"quota_tokens_per_day,omitempty"`
QuotaRequestsPerDay *int `json:"quota_requests_per_day,omitempty"`
AllowedModels []string `gorm:"type:jsonb;serializer:json" json:"allowed_models,omitempty"`
ExpiresAt *time.Time `json:"expires_at,omitempty"`
Status string `gorm:"size:16;not null;default:active" json:"status"`
LastUsedAt *time.Time `json:"last_used_at,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// Channel 上游渠道(PLANNING §5.3)
type Channel struct {
ID uint64 `gorm:"primaryKey;autoIncrement" json:"id"`
Name string `gorm:"uniqueIndex;size:64;not null" json:"name"`
Provider string `gorm:"size:16;not null" json:"provider"` // openai|anthropic|compatible
BaseURL string `gorm:"size:255;not null" json:"base_url"`
APIKeyEnc string `gorm:"size:1024;not null" json:"-"` // AES-GCM 密文
Weight int `gorm:"not null;default:1" json:"weight"`
Priority int `gorm:"not null;default:0" json:"priority"` // 数值小优先
TimeoutMS int `gorm:"not null;default:120000" json:"timeout_ms"`
MaxConcurrency int `gorm:"not null;default:16" json:"max_concurrency"`
HealthStatus string `gorm:"size:16;not null;default:healthy" json:"health_status"`
Enabled bool `gorm:"not null;default:true" json:"enabled"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// Model 全局模型 + 定价(PLANNING §5.4,价格按每百万 token,USD)
type Model struct {
ID uint64 `gorm:"primaryKey;autoIncrement" json:"id"`
Name string `gorm:"uniqueIndex;size:128;not null" json:"name"`
DisplayName string `gorm:"size:128" json:"display_name"`
InputPrice float64 `gorm:"type:numeric(20,8);not null;default:0" json:"input_price"`
OutputPrice float64 `gorm:"type:numeric(20,8);not null;default:0" json:"output_price"`
CacheReadPrice float64 `gorm:"type:numeric(20,8);not null;default:0" json:"cache_read_price"`
Enabled bool `gorm:"not null;default:true" json:"enabled"`
Sort int `gorm:"not null;default:0" json:"sort"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// ChannelModelBinding 渠道↔模型绑定(多对多,PLANNING §5.4)
type ChannelModelBinding struct {
ID uint64 `gorm:"primaryKey;autoIncrement" json:"id"`
ChannelID uint64 `gorm:"index:idx_ch_model,unique;not null" json:"channel_id"`
ModelID uint64 `gorm:"index:idx_ch_model,unique;not null" json:"model_id"`
UpstreamModel string `gorm:"size:255;not null" json:"upstream_model"`
Weight int `gorm:"not null;default:1" json:"weight"`
Channel Channel `gorm:"foreignKey:ChannelID" json:"-"`
Model Model `gorm:"foreignKey:ModelID" json:"-"`
}
// UsageLog 请求级用量明细(PLANNING §5.5)
type UsageLog struct {
ID uint64 `gorm:"primaryKey;autoIncrement" json:"id"`
RequestID string `gorm:"size:128" json:"request_id"` // 上游 request id
TraceID string `gorm:"size:64;index" json:"trace_id"`
UserID uint64 `gorm:"index:idx_user_created;not null" json:"user_id"`
KeyID uint64 `json:"key_id"`
ChannelID uint64 `json:"channel_id"`
ModelID uint64 `json:"model_id"`
ModelName string `gorm:"size:128" json:"model_name"`
Protocol string `gorm:"size:32" json:"protocol"` // responses|chat|messages
InputTokens int64 `json:"input_tokens"`
OutputTokens int64 `json:"output_tokens"`
CacheReadTokens int64 `json:"cache_read_tokens"`
CacheCreationTokens int64 `json:"cache_creation_tokens"`
InputPrice float64 `gorm:"type:numeric(20,8)" json:"input_price"` // 快照
OutputPrice float64 `gorm:"type:numeric(20,8)" json:"output_price"` // 快照
CacheReadPrice float64 `gorm:"type:numeric(20,8)" json:"cache_read_price"` // 快照
Cost float64 `gorm:"type:numeric(20,8)" json:"cost"`
LatencyMS int `json:"latency_ms"`
Status string `gorm:"size:16;not null" json:"status"`
ErrorCode *string `json:"error_code,omitempty"`
CreatedAt time.Time `gorm:"index" json:"created_at"`
}
// UsageDaily 日粒度预聚合(PLANNING §5.6)
type UsageDaily struct {
ID uint64 `gorm:"primaryKey;autoIncrement" json:"id"`
UserID uint64 `gorm:"index:idx_daily_user_model,unique" json:"user_id"`
ModelID uint64 `gorm:"index:idx_daily_user_model,unique" json:"model_id"`
Date string `gorm:"size:10;index:idx_daily_user_model,unique" json:"date"` // YYYY-MM-DD (UTC)
Requests int64 `json:"requests"`
InputTokens int64 `json:"input_tokens"`
OutputTokens int64 `json:"output_tokens"`
CacheReadTokens int64 `json:"cache_read_tokens"`
Cost float64 `gorm:"type:numeric(20,8)" json:"cost"`
}
// RechargeOrder 充值订单(PLANNING §5.7,预留:首版不做充值)
type RechargeOrder struct {
ID uint64 `gorm:"primaryKey;autoIncrement" json:"id"`
UserID uint64 `gorm:"index;not null" json:"user_id"`
Amount float64 `gorm:"type:numeric(20,8);not null" json:"amount"`
Status string `gorm:"size:16;not null;default:pending" json:"status"`
Method string `gorm:"size:16;not null;default:manual" json:"method"`
TransactionID string `gorm:"size:128" json:"transaction_id,omitempty"`
ReviewedBy *uint64 `json:"reviewed_by,omitempty"`
ReviewedAt *time.Time `json:"reviewed_at,omitempty"`
Remark string `gorm:"size:512" json:"remark,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// BalanceLog 余额流水(PLANNING §5.8,幂等:ref_id + type 唯一)
type BalanceLog struct {
ID uint64 `gorm:"primaryKey;autoIncrement" json:"id"`
UserID uint64 `gorm:"index:idx_balance_user;not null" json:"user_id"`
Change float64 `gorm:"type:numeric(20,8);not null" json:"change"`
BalanceAfter float64 `gorm:"type:numeric(20,8);not null" json:"balance_after"`
Type string `gorm:"size:16;not null" json:"type"`
RefID string `gorm:"size:128;index:idx_balance_ref,unique" json:"ref_id"`
Remark string `gorm:"size:512" json:"remark,omitempty"`
CreatedAt time.Time `json:"created_at"`
}
// SystemConfig 系统配置(PLANNING §5.9)
type SystemConfig struct {
Key string `gorm:"primaryKey;size:64" json:"key"`
Value string `gorm:"type:jsonb;not null" json:"value"`
}
// AllModels 迁移顺序(外键依赖在后)
func AllModels() []any {
return []any{
&User{},
&APIKey{},
&Channel{},
&Model{},
&ChannelModelBinding{},
&UsageLog{},
&UsageDaily{},
&RechargeOrder{},
&BalanceLog{},
&SystemConfig{},
}
}