- 后端 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>
57 lines
1.2 KiB
Go
57 lines
1.2 KiB
Go
package store
|
||
|
||
import (
|
||
"log"
|
||
"os"
|
||
"path/filepath"
|
||
"strings"
|
||
|
||
"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:
|
||
// 确保 SQLite 文件所在目录存在
|
||
if dir := sqliteDir(dsn); dir != "" {
|
||
_ = os.MkdirAll(dir, 0o755)
|
||
}
|
||
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
|
||
}
|
||
|
||
// sqliteDir 提取 SQLite DSN 中的目录部分(忽略 file: 前缀与查询参数)。
|
||
func sqliteDir(dsn string) string {
|
||
d := dsn
|
||
if i := strings.IndexByte(d, '?'); i >= 0 {
|
||
d = d[:i]
|
||
}
|
||
if strings.HasPrefix(d, "file:") {
|
||
d = d[len("file:"):]
|
||
}
|
||
if d == "" || d == ":memory:" || strings.Contains(d, "::") {
|
||
return ""
|
||
}
|
||
return filepath.Dir(d)
|
||
}
|