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 上游, 端到端验证通过
This commit is contained in:
@@ -0,0 +1,34 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"github.com/glebarez/sqlite"
|
||||
"gorm.io/gorm"
|
||||
gormlogger "gorm.io/gorm/logger"
|
||||
)
|
||||
|
||||
// Open 打开数据库连接并自动迁移。
|
||||
// 开发默认 SQLite(dsn 支持 file:...?_journal_mode=WAL),生产可切 postgres。
|
||||
func Open(driver, dsn string) (*gorm.DB, error) {
|
||||
var dialector gorm.Dialector
|
||||
switch driver {
|
||||
case "postgres":
|
||||
dialector = postgresDialector(dsn)
|
||||
default:
|
||||
dialector = sqlite.Open(dsn)
|
||||
}
|
||||
|
||||
db, err := gorm.Open(dialector, &gorm.Config{
|
||||
Logger: gormlogger.Default.LogMode(gormlogger.Warn),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := db.AutoMigrate(AllModels()...); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
log.Printf("store: connected driver=%s (migrated)", driver)
|
||||
return db, nil
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"gorm.io/driver/postgres"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// postgresDialector 延迟引用 postgres 驱动,避免开发环境额外依赖。
|
||||
func postgresDialector(dsn string) gorm.Dialector {
|
||||
return postgres.Open(dsn)
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
// 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{},
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user