- 回退渠道 homepage/favicon 字段与代理接口(未采用) - Base URL 改为可选: 留空按供应商默认(openai/anthropic), 兼容兼容型渠道必须填; 填完整地址(含 /v1)时归一化去尾 - 网关 upstreamURL 兜底去 /v1, 避免路径重复 Co-Authored-By: Claude <noreply@anthropic.com>
222 lines
10 KiB
Go
222 lines
10 KiB
Go
// Package store 数据模型与仓储层(GORM)。
|
||
// 字段设计对应 PLANNING.md §6:金额/价格 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"
|
||
|
||
// 渠道原生支持的协议格式
|
||
FormatChat = "chat" // OpenAI Chat Completions
|
||
FormatResponses = "responses" // OpenAI Responses API
|
||
FormatMessages = "messages" // Anthropic Messages
|
||
|
||
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 §6.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 §6.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 §6.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(供应商/默认格式)
|
||
Formats []string `gorm:"type:jsonb;serializer:json" json:"formats,omitempty"` // 原生支持的协议格式 chat|responses|messages
|
||
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"`
|
||
}
|
||
|
||
// FormatsEffective 返回渠道实际支持的原生协议;未显式配置时按 provider 推断。
|
||
func (c *Channel) FormatsEffective() []string {
|
||
if len(c.Formats) > 0 {
|
||
return c.Formats
|
||
}
|
||
switch c.Provider {
|
||
case ChannelProviderAnthropic:
|
||
return []string{FormatMessages}
|
||
case ChannelProviderOpenAI:
|
||
return []string{FormatChat, FormatResponses}
|
||
default: // compatible
|
||
return []string{FormatChat}
|
||
}
|
||
}
|
||
|
||
// Model 全局模型 + 定价(PLANNING §6.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 §6.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 §6.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 §6.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 §6.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 §6.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 §6.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{},
|
||
}
|
||
}
|