后端 (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 上游, 端到端验证通过
203 lines
9.3 KiB
Go
203 lines
9.3 KiB
Go
// 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{},
|
||
}
|
||
}
|