2025-10-27 23:12:15 +08:00
|
|
|
|
package database
|
|
|
|
|
|
|
|
|
|
|
|
import (
|
2026-03-28 23:30:02 +08:00
|
|
|
|
appconfig "NetworkAuth/config"
|
2026-03-18 21:51:17 +08:00
|
|
|
|
"NetworkAuth/utils"
|
2026-03-19 05:11:44 +08:00
|
|
|
|
"context"
|
2025-10-27 23:12:15 +08:00
|
|
|
|
"fmt"
|
2026-03-18 21:51:17 +08:00
|
|
|
|
"log"
|
|
|
|
|
|
"os"
|
2026-04-09 01:00:31 +08:00
|
|
|
|
"path/filepath"
|
2025-10-27 23:12:15 +08:00
|
|
|
|
"sync"
|
2026-03-18 21:51:17 +08:00
|
|
|
|
"time"
|
2025-10-27 23:12:15 +08:00
|
|
|
|
|
|
|
|
|
|
"github.com/glebarez/sqlite"
|
|
|
|
|
|
"github.com/sirupsen/logrus"
|
|
|
|
|
|
"github.com/spf13/viper"
|
|
|
|
|
|
"gorm.io/driver/mysql"
|
|
|
|
|
|
"gorm.io/gorm"
|
2026-03-18 21:51:17 +08:00
|
|
|
|
gLogger "gorm.io/gorm/logger"
|
2025-10-27 23:12:15 +08:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
// ============================================================================
|
|
|
|
|
|
// 全局变量
|
|
|
|
|
|
// ============================================================================
|
|
|
|
|
|
|
|
|
|
|
|
var (
|
|
|
|
|
|
// dbInstance 全局 *gorm.DB 实例,使用单例确保全局复用
|
|
|
|
|
|
dbInstance *gorm.DB
|
2026-03-19 05:11:44 +08:00
|
|
|
|
// healthCheckCancel 数据库健康检查取消函数
|
|
|
|
|
|
healthCheckCancel context.CancelFunc
|
2025-10-27 23:12:15 +08:00
|
|
|
|
// once 确保初始化只执行一次
|
|
|
|
|
|
once sync.Once
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
// ============================================================================
|
|
|
|
|
|
// 公共函数
|
|
|
|
|
|
// ============================================================================
|
|
|
|
|
|
|
|
|
|
|
|
// Init 初始化数据库连接(根据配置自动选择驱动)
|
|
|
|
|
|
// - 默认使用 SQLite(github.com/glebarez/sqlite)
|
|
|
|
|
|
// - 生产环境支持 MySQL(gorm.io/driver/mysql)
|
|
|
|
|
|
func Init() (*gorm.DB, error) {
|
|
|
|
|
|
var initErr error
|
|
|
|
|
|
once.Do(func() {
|
2026-03-28 23:30:02 +08:00
|
|
|
|
initErr = performInitFromViper()
|
2025-10-27 23:12:15 +08:00
|
|
|
|
})
|
|
|
|
|
|
return dbInstance, initErr
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// GetDB 获取全局 *gorm.DB 实例
|
|
|
|
|
|
// 如果未初始化,会尝试初始化一次
|
|
|
|
|
|
func GetDB() (*gorm.DB, error) {
|
|
|
|
|
|
if dbInstance != nil {
|
|
|
|
|
|
return dbInstance, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
return Init()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-18 21:51:17 +08:00
|
|
|
|
// ReInit 重新初始化数据库连接
|
|
|
|
|
|
// 用于在修改配置后重新连接数据库
|
|
|
|
|
|
func ReInit() (*gorm.DB, error) {
|
2026-03-28 23:30:02 +08:00
|
|
|
|
closeCurrentDB()
|
|
|
|
|
|
|
|
|
|
|
|
// 在 ReInit 时,强制从 viper 重新读取配置并连接,忽略"系统尚未安装"的检查
|
|
|
|
|
|
// 因为这是安装过程触发的
|
|
|
|
|
|
var cfg appconfig.AppConfig
|
|
|
|
|
|
if err := viper.Unmarshal(&cfg); err != nil {
|
|
|
|
|
|
return nil, err
|
2026-03-18 21:51:17 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-28 23:30:02 +08:00
|
|
|
|
if err := performInitWithConfig(&cfg); err != nil {
|
|
|
|
|
|
return nil, err
|
|
|
|
|
|
}
|
2026-03-18 21:51:17 +08:00
|
|
|
|
|
2026-03-28 23:30:02 +08:00
|
|
|
|
if dbInstance == nil {
|
|
|
|
|
|
return nil, fmt.Errorf("数据库实例初始化后为空")
|
|
|
|
|
|
}
|
2026-03-18 21:51:17 +08:00
|
|
|
|
|
2026-03-28 23:30:02 +08:00
|
|
|
|
return dbInstance, nil
|
|
|
|
|
|
}
|
2026-03-18 21:51:17 +08:00
|
|
|
|
|
2026-03-28 23:30:02 +08:00
|
|
|
|
func InitWithAppConfig(cfg *appconfig.AppConfig) (*gorm.DB, error) {
|
|
|
|
|
|
closeCurrentDB()
|
|
|
|
|
|
if err := performInitWithConfig(cfg); err != nil {
|
|
|
|
|
|
return nil, err
|
|
|
|
|
|
}
|
|
|
|
|
|
return dbInstance, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func performInitFromViper() error {
|
|
|
|
|
|
configFile := viper.ConfigFileUsed()
|
2026-03-18 21:51:17 +08:00
|
|
|
|
if configFile == "" {
|
|
|
|
|
|
configFile = "config.json"
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-28 23:30:02 +08:00
|
|
|
|
// 从 viper 中读取配置
|
|
|
|
|
|
var cfg appconfig.AppConfig
|
|
|
|
|
|
if err := viper.Unmarshal(&cfg); err != nil {
|
|
|
|
|
|
return err
|
2026-03-18 21:51:17 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-28 23:30:02 +08:00
|
|
|
|
// 检查数据库类型,如果文件或配置不存在,说明系统尚未安装,跳过数据库连接
|
|
|
|
|
|
switch cfg.Database.Type {
|
|
|
|
|
|
case "sqlite":
|
|
|
|
|
|
dbPath := cfg.Database.SQLite.Path
|
|
|
|
|
|
if dbPath == "" {
|
2026-04-09 01:00:31 +08:00
|
|
|
|
dbPath = "database.db"
|
|
|
|
|
|
}
|
|
|
|
|
|
if !filepath.IsAbs(dbPath) {
|
|
|
|
|
|
dbPath = filepath.Join(utils.GetRootDir(), dbPath)
|
2026-03-28 23:30:02 +08:00
|
|
|
|
}
|
|
|
|
|
|
if _, err := os.Stat(dbPath); os.IsNotExist(err) {
|
|
|
|
|
|
logrus.Info("SQLite 数据库文件不存在,系统尚未安装,跳过数据库连接")
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
2026-03-18 21:51:17 +08:00
|
|
|
|
case "mysql":
|
2026-03-28 23:30:02 +08:00
|
|
|
|
// 只有在明确配置了 host 并且不是安装请求时才去连接 MySQL
|
|
|
|
|
|
// 我们通过检查是否已有有效配置来判断,比如检查 database 是否为空
|
|
|
|
|
|
if cfg.Database.MySQL.Database == "" {
|
|
|
|
|
|
logrus.Info("MySQL 数据库名称未配置,说明系统尚未安装,跳过数据库连接")
|
|
|
|
|
|
return nil
|
2026-03-18 21:51:17 +08:00
|
|
|
|
}
|
2026-03-28 23:30:02 +08:00
|
|
|
|
}
|
2026-03-18 21:51:17 +08:00
|
|
|
|
|
2026-03-28 23:30:02 +08:00
|
|
|
|
return performInitWithConfig(&cfg)
|
|
|
|
|
|
}
|
2026-03-18 21:51:17 +08:00
|
|
|
|
|
2026-03-28 23:30:02 +08:00
|
|
|
|
func performInitWithConfig(cfg *appconfig.AppConfig) error {
|
|
|
|
|
|
if cfg == nil {
|
|
|
|
|
|
return fmt.Errorf("应用配置不能为空")
|
|
|
|
|
|
}
|
|
|
|
|
|
if err := appconfig.ValidateConfigValue(cfg); err != nil {
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
var initErr error
|
|
|
|
|
|
switch cfg.Database.Type {
|
|
|
|
|
|
case "mysql":
|
|
|
|
|
|
initErr = initMySQL(&cfg.Database.MySQL, cfg.Log.Level)
|
|
|
|
|
|
if initErr != nil {
|
|
|
|
|
|
logrus.WithError(initErr).Error("MySQL 数据库连接失败,请检查配置或重新安装")
|
|
|
|
|
|
// 既然 MySQL 连不上,说明系统无法正常工作,直接返回错误,由外层决定是否退出
|
|
|
|
|
|
return initErr
|
2026-03-18 21:51:17 +08:00
|
|
|
|
}
|
2026-03-28 23:30:02 +08:00
|
|
|
|
default:
|
|
|
|
|
|
initErr = initSQLite(&cfg.Database.SQLite, cfg.Log.Level)
|
|
|
|
|
|
}
|
|
|
|
|
|
if initErr != nil || dbInstance == nil {
|
|
|
|
|
|
return initErr
|
2026-03-18 21:51:17 +08:00
|
|
|
|
}
|
2026-03-28 23:30:02 +08:00
|
|
|
|
dbConfig := buildPoolConfig(cfg)
|
|
|
|
|
|
if err := utils.ValidateDatabaseConfig(dbConfig); err != nil {
|
|
|
|
|
|
logrus.WithError(err).Warn("数据库配置验证失败,使用默认配置")
|
|
|
|
|
|
dbConfig = utils.GetDefaultDatabaseConfig()
|
|
|
|
|
|
}
|
|
|
|
|
|
if err := utils.ConfigureConnectionPool(dbInstance, dbConfig); err != nil {
|
|
|
|
|
|
logrus.WithError(err).Error("配置数据库连接池失败")
|
|
|
|
|
|
}
|
|
|
|
|
|
healthCheckCancel = utils.StartHealthCheck(dbInstance, dbConfig)
|
|
|
|
|
|
return nil
|
2026-03-18 21:51:17 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// SetDB 设置全局 *gorm.DB 实例(用于测试)
|
|
|
|
|
|
func SetDB(db *gorm.DB) {
|
|
|
|
|
|
dbInstance = db
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-27 23:12:15 +08:00
|
|
|
|
// ============================================================================
|
|
|
|
|
|
// 私有函数
|
|
|
|
|
|
// ============================================================================
|
|
|
|
|
|
|
2026-03-28 23:30:02 +08:00
|
|
|
|
func closeCurrentDB() {
|
|
|
|
|
|
if healthCheckCancel != nil {
|
|
|
|
|
|
healthCheckCancel()
|
|
|
|
|
|
healthCheckCancel = nil
|
2025-10-27 23:12:15 +08:00
|
|
|
|
}
|
2026-03-28 23:30:02 +08:00
|
|
|
|
if dbInstance != nil {
|
|
|
|
|
|
if sqlDB, err := dbInstance.DB(); err == nil {
|
|
|
|
|
|
sqlDB.Close()
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
dbInstance = nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func buildPoolConfig(cfg *appconfig.AppConfig) *utils.DatabaseConfig {
|
|
|
|
|
|
dbConfig := utils.GetDefaultDatabaseConfig()
|
|
|
|
|
|
if cfg.Database.Type == "mysql" {
|
|
|
|
|
|
if cfg.Database.MySQL.MaxIdleConns > 0 {
|
|
|
|
|
|
dbConfig.MaxIdleConns = cfg.Database.MySQL.MaxIdleConns
|
|
|
|
|
|
}
|
|
|
|
|
|
if cfg.Database.MySQL.MaxOpenConns > 0 {
|
|
|
|
|
|
dbConfig.MaxOpenConns = cfg.Database.MySQL.MaxOpenConns
|
|
|
|
|
|
}
|
|
|
|
|
|
return dbConfig
|
|
|
|
|
|
}
|
|
|
|
|
|
dbConfig.MaxIdleConns = 1
|
|
|
|
|
|
dbConfig.MaxOpenConns = 1
|
|
|
|
|
|
return dbConfig
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func buildGormLogger(level string) gLogger.Interface {
|
2026-03-18 21:51:17 +08:00
|
|
|
|
var logLevel gLogger.LogLevel
|
2026-03-28 23:30:02 +08:00
|
|
|
|
switch level {
|
2026-03-18 21:51:17 +08:00
|
|
|
|
case "debug":
|
|
|
|
|
|
logLevel = gLogger.Info
|
|
|
|
|
|
case "error":
|
|
|
|
|
|
logLevel = gLogger.Error
|
|
|
|
|
|
default:
|
|
|
|
|
|
logLevel = gLogger.Warn
|
|
|
|
|
|
}
|
2026-03-28 23:30:02 +08:00
|
|
|
|
return gLogger.New(log.New(os.Stdout, "\r\n", log.LstdFlags), gLogger.Config{SlowThreshold: 2 * time.Second, LogLevel: logLevel, IgnoreRecordNotFoundError: true, Colorful: false})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func initSQLite(sqliteConfig *appconfig.SQLiteConfig, logLevel string) error {
|
|
|
|
|
|
path := sqliteConfig.Path
|
|
|
|
|
|
if path == "" {
|
2026-04-09 01:00:31 +08:00
|
|
|
|
path = "database.db"
|
|
|
|
|
|
}
|
|
|
|
|
|
if !filepath.IsAbs(path) {
|
|
|
|
|
|
path = filepath.Join(utils.GetRootDir(), path)
|
2026-03-28 23:30:02 +08:00
|
|
|
|
}
|
|
|
|
|
|
dsn := fmt.Sprintf("file:%s?cache=shared&_busy_timeout=5000&_fk=1", path)
|
|
|
|
|
|
db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{Logger: buildGormLogger(logLevel)})
|
2025-10-27 23:12:15 +08:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
logrus.WithError(err).Error("SQLite 初始化失败")
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
if sqlDB, err := db.DB(); err == nil {
|
2026-03-28 23:30:02 +08:00
|
|
|
|
sqlDB.SetMaxOpenConns(1)
|
2025-10-27 23:12:15 +08:00
|
|
|
|
sqlDB.SetMaxIdleConns(1)
|
|
|
|
|
|
}
|
|
|
|
|
|
dbInstance = db
|
2026-04-17 03:12:28 +08:00
|
|
|
|
logrus.WithField("path", utils.DisplayPath(path)).Info("SQLite 连接已建立")
|
2025-10-27 23:12:15 +08:00
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-28 23:30:02 +08:00
|
|
|
|
func initMySQL(mysqlConfig *appconfig.MySQLConfig, logLevel string) error {
|
|
|
|
|
|
host := mysqlConfig.Host
|
|
|
|
|
|
port := mysqlConfig.Port
|
|
|
|
|
|
user := mysqlConfig.Username
|
|
|
|
|
|
pass := mysqlConfig.Password
|
|
|
|
|
|
dbname := mysqlConfig.Database
|
|
|
|
|
|
charset := mysqlConfig.Charset
|
2025-10-27 23:12:15 +08:00
|
|
|
|
if charset == "" {
|
|
|
|
|
|
charset = "utf8mb4"
|
|
|
|
|
|
}
|
|
|
|
|
|
dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=%s&parseTime=True&loc=Local", user, pass, host, port, dbname, charset)
|
2026-03-28 23:30:02 +08:00
|
|
|
|
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{Logger: buildGormLogger(logLevel)})
|
2025-10-27 23:12:15 +08:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
logrus.WithError(err).Error("MySQL 初始化失败")
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
dbInstance = db
|
|
|
|
|
|
logrus.WithField("host", host).WithField("database", dbname).Info("MySQL 连接已建立")
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|