修改项目为前后端分离方案

This commit is contained in:
2026-03-28 23:30:02 +08:00
parent d8536354d4
commit 7a7d3aeaaa
77 changed files with 1447 additions and 23765 deletions

View File

@@ -1,6 +1,7 @@
package database
import (
appconfig "NetworkAuth/config"
"NetworkAuth/utils"
"context"
"fmt"
@@ -40,7 +41,7 @@ var (
func Init() (*gorm.DB, error) {
var initErr error
once.Do(func() {
initErr = performInit()
initErr = performInitFromViper()
})
return dbInstance, initErr
}
@@ -57,83 +58,101 @@ func GetDB() (*gorm.DB, error) {
// ReInit 重新初始化数据库连接
// 用于在修改配置后重新连接数据库
func ReInit() (*gorm.DB, error) {
// 如果已有连接,尝试关闭它
if dbInstance != nil {
if healthCheckCancel != nil {
healthCheckCancel()
healthCheckCancel = nil
}
if sqlDB, err := dbInstance.DB(); err == nil {
sqlDB.Close()
}
}
dbInstance = nil
closeCurrentDB()
// 重新执行初始化逻辑(不经过 once.Do
return dbInstance, performInit()
// 在 ReInit 时,强制从 viper 重新读取配置并连接,忽略"系统尚未安装"的检查
// 因为这是安装过程触发的
var cfg appconfig.AppConfig
if err := viper.Unmarshal(&cfg); err != nil {
return nil, err
}
if err := performInitWithConfig(&cfg); err != nil {
return nil, err
}
if dbInstance == nil {
return nil, fmt.Errorf("数据库实例初始化后为空")
}
return dbInstance, nil
}
func performInit() error {
// 检查是否已经有配置文件(通过检查文件是否存在)
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()
// 如果 viper 没有使用配置文件(可能是因为没找到文件而使用了默认配置),
// 或者配置文件路径为空,我们应该假设处于未安装状态。
// 但 viper.ConfigFileUsed() 在 ReadInConfig 成功后会返回文件名。
// 如果 ReadInConfig 失败因为文件不存在viper 可能会返回空或者我们在 config.go 中设置的路径。
// 在 config.go 中,如果文件不存在,我们加载了默认配置但没有写文件。
// 此时 viper.ConfigFileUsed() 可能是空的或者我们设置的路径。
// 让我们检查该路径对应的文件是否存在。
if configFile == "" {
configFile = "config.json"
}
_, err := os.Stat(configFile)
isConfigExists := !os.IsNotExist(err)
// 如果配置文件不存在,说明还没有经过安装初始化,暂时不连接数据库
if !isConfigExists {
logrus.Info("尚未初始化配置,跳过数据库连接")
return nil
// 从 viper 中读取配置
var cfg appconfig.AppConfig
if err := viper.Unmarshal(&cfg); err != nil {
return err
}
var initErr error
dbType := viper.GetString("database.type")
switch dbType {
// 检查数据库类型,如果文件或配置不存在,说明系统尚未安装,跳过数据库连接
switch cfg.Database.Type {
case "sqlite":
dbPath := cfg.Database.SQLite.Path
if dbPath == "" {
dbPath = "./database.db"
}
if _, err := os.Stat(dbPath); os.IsNotExist(err) {
logrus.Info("SQLite 数据库文件不存在,系统尚未安装,跳过数据库连接")
return nil
}
case "mysql":
initErr = initMySQL()
// 只有在明确配置了 host 并且不是安装请求时才去连接 MySQL
// 我们通过检查是否已有有效配置来判断,比如检查 database 是否为空
if cfg.Database.MySQL.Database == "" {
logrus.Info("MySQL 数据库名称未配置,说明系统尚未安装,跳过数据库连接")
return nil
}
}
return performInitWithConfig(&cfg)
}
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
}
default:
initErr = initSQLite()
initErr = initSQLite(&cfg.Database.SQLite, cfg.Log.Level)
}
// 如果数据库初始化成功,配置连接池和启动健康检查
if initErr == nil && dbInstance != nil {
// 加载数据库配置
var configPrefix string
if dbType == "mysql" {
configPrefix = "database.mysql"
} else {
configPrefix = "database.sqlite"
}
dbConfig := utils.LoadDatabaseConfig(configPrefix)
// 验证配置
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)
if initErr != nil || dbInstance == nil {
return initErr
}
return initErr
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
}
// SetDB 设置全局 *gorm.DB 实例(用于测试)
@@ -145,16 +164,38 @@ func SetDB(db *gorm.DB) {
// 私有函数
// ============================================================================
// initSQLite 初始化 SQLite 数据库
// 使用 viper 中的 database.sqlite.path 作为数据库文件路径
func initSQLite() error {
path := viper.GetString("database.sqlite.path")
if path == "" {
path = "./recharge.db"
func closeCurrentDB() {
if healthCheckCancel != nil {
healthCheckCancel()
healthCheckCancel = nil
}
dsn := fmt.Sprintf("file:%s?cache=shared&_busy_timeout=5000&_fk=1", path)
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 {
var logLevel gLogger.LogLevel
switch viper.GetString("logger.level") {
switch level {
case "debug":
logLevel = gLogger.Info
case "error":
@@ -162,55 +203,45 @@ func initSQLite() error {
default:
logLevel = gLogger.Warn
}
gl := gLogger.New(log.New(os.Stdout, "\r\n", log.LstdFlags), gLogger.Config{SlowThreshold: 2 * time.Second, LogLevel: logLevel, IgnoreRecordNotFoundError: true, Colorful: false})
db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{Logger: gl})
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 == "" {
path = "./database.db"
}
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)})
if err != nil {
logrus.WithError(err).Error("SQLite 初始化失败")
return err
}
// SQLite 连接池配置SQLite 对连接池支持有限,但仍可设置基本参数)
if sqlDB, err := db.DB(); err == nil {
// SQLite 通常使用单连接,但可以设置一些基本参数
sqlDB.SetMaxOpenConns(1) // SQLite 建议使用单连接
sqlDB.SetMaxOpenConns(1)
sqlDB.SetMaxIdleConns(1)
}
dbInstance = db
logrus.WithField("path", path).Info("SQLite 连接已建立")
return nil
}
// initMySQL 初始化 MySQL 数据库
// 从 viper 读取 database.mysql.* 配置构建 DSN
func initMySQL() error {
host := viper.GetString("database.mysql.host")
port := viper.GetInt("database.mysql.port")
user := viper.GetString("database.mysql.username")
pass := viper.GetString("database.mysql.password")
dbname := viper.GetString("database.mysql.database")
charset := viper.GetString("database.mysql.charset")
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
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)
var logLevel gLogger.LogLevel
switch viper.GetString("logger.level") {
case "debug":
logLevel = gLogger.Info
case "error":
logLevel = gLogger.Error
default:
logLevel = gLogger.Warn
}
gl := gLogger.New(log.New(os.Stdout, "\r\n", log.LstdFlags), gLogger.Config{SlowThreshold: 2 * time.Second, LogLevel: logLevel, IgnoreRecordNotFoundError: true, Colorful: false})
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{Logger: gl})
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{Logger: buildGormLogger(logLevel)})
if err != nil {
logrus.WithError(err).Error("MySQL 初始化失败")
return err
}
dbInstance = db
logrus.WithField("host", host).WithField("database", dbname).Info("MySQL 连接已建立")
return nil