Files
goodBaby/handler/db/db.go
T
meiandClaude Fable 5 ae7082f13c feat: 管理员后台配置 + PostgreSQL 支持
管理员后台:
- User 新增 IsAdmin,第一个注册的用户自动成为管理员;
  老库升级时自动把最早注册的用户提升为管理员
- 新增 AdminMiddleware 与 /api/v1/admin/config 读写接口
- 可改开放注册、检查间隔、规则重试时长、日志保留条数,保存即生效
- 检查间隔改动后自动重排定时任务,为此把 cron 抽成 handler/scheduler
- 监听地址、数据库、会话密钥等启动项不开放,接口只返回只读展示
- 设置页新增"系统配置"区块,仅管理员可见

PostgreSQL:
- 新增 database_driver / database_dsn 配置,默认仍为 sqlite
- postgres 用独立连接池(20/5/1h),sqlite 保持单连接避免写锁
- 启动时先 Ping,配置写错在启动阶段就报错而不是等到首次查询
- 支持 GOODBABY_DB_DRIVER / GOODBABY_DB_DSN 环境变量,
  只给 DSN 时自动按 postgres 处理

文档: 新增 docs/database.md(含 compose 示例与切换说明),README 补充管理员与数据库章节

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 07:17:16 +08:00

143 lines
3.6 KiB
Go
Raw Permalink 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 db
import (
"fmt"
"log"
"sync"
"time"
"github.com/glebarez/sqlite"
"github.com/ssdomei232/goodBaby/configs"
"github.com/ssdomei232/goodBaby/model"
"gorm.io/driver/postgres"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)
var (
once sync.Once
instance *gorm.DB
initErr error
)
// GetGormDB 返回全局唯一的 gorm 连接。
//
// 之前每次调用都会新开一个 sqlite 连接,在并发执行规则时很容易触发
// "database is locked",这里改为单例并开启 WAL。
func GetGormDB() (*gorm.DB, error) {
once.Do(func() {
instance, initErr = open()
})
return instance, initErr
}
// MustInit 在启动阶段初始化数据库并执行迁移,失败直接退出
func MustInit() *gorm.DB {
gormDB, err := GetGormDB()
if err != nil {
log.Fatalf("初始化数据库失败: %v", err)
}
if err := AutoMigrate(gormDB); err != nil {
log.Fatalf("数据库迁移失败: %v", err)
}
if err := ensureAdmin(gormDB); err != nil {
log.Fatalf("初始化管理员失败: %v", err)
}
return gormDB
}
// ensureAdmin 保证系统里至少有一个管理员。
//
// IsAdmin 是后加的字段,老数据库升级上来时所有人都不是管理员,
// 这里把最早注册的那个用户提升为管理员。
func ensureAdmin(gormDB *gorm.DB) error {
var adminCount int64
if err := gormDB.Model(&model.User{}).Where("is_admin = ?", true).Count(&adminCount).Error; err != nil {
return err
}
if adminCount > 0 {
return nil
}
var first model.User
if err := gormDB.Order("id ASC").First(&first).Error; err != nil {
// 一个用户都没有,等第一个注册的人来当管理员
return nil
}
if err := gormDB.Model(&model.User{}).Where("id = ?", first.ID).
Update("is_admin", true).Error; err != nil {
return err
}
log.Printf("已将用户 %s (ID: %d) 提升为管理员", first.Username, first.ID)
return nil
}
func open() (*gorm.DB, error) {
config, err := configs.GetConfig()
if err != nil {
return nil, fmt.Errorf("读取配置失败: %w", err)
}
gormConfig := &gorm.Config{Logger: logger.Default.LogMode(logger.Warn)}
var (
gormDB *gorm.DB
driver = config.DatabaseDriver
)
switch driver {
case configs.DriverPostgres:
if config.DatabaseDSN == "" {
return nil, fmt.Errorf("使用 postgres 时必须配置 database_dsn")
}
gormDB, err = gorm.Open(postgres.Open(config.DatabaseDSN), gormConfig)
case configs.DriverSQLite, "":
driver = configs.DriverSQLite
dsn := fmt.Sprintf(
"file:%s?_pragma=busy_timeout(10000)&_pragma=journal_mode(WAL)&_pragma=foreign_keys(1)",
config.DatabasePath,
)
gormDB, err = gorm.Open(sqlite.Open(dsn), gormConfig)
default:
return nil, fmt.Errorf("不支持的数据库驱动: %s(可选 sqlite / postgres)", config.DatabaseDriver)
}
if err != nil {
return nil, fmt.Errorf("打开数据库失败: %w", err)
}
sqlDB, err := gormDB.DB()
if err != nil {
return nil, err
}
if driver == configs.DriverSQLite {
// sqlite 单写多读,限制连接数避免写冲突
sqlDB.SetMaxOpenConns(1)
sqlDB.SetMaxIdleConns(1)
} else {
sqlDB.SetMaxOpenConns(20)
sqlDB.SetMaxIdleConns(5)
sqlDB.SetConnMaxLifetime(time.Hour)
}
// 提前握手,配置写错时在启动阶段就报错,而不是等到第一次查询
if err := sqlDB.Ping(); err != nil {
return nil, fmt.Errorf("连接数据库失败: %w", err)
}
log.Printf("数据库已连接 (driver=%s)", driver)
return gormDB, nil
}
// AutoMigrate 建表 / 补齐新增字段
func AutoMigrate(gormDB *gorm.DB) error {
return gormDB.AutoMigrate(
&model.User{},
&model.Timer{},
&model.Rule{},
&model.Account{},
&model.ExecutionLog{},
)
}