// Package db 提供数据库连接管理(MySQL 二连接实例,用于外部 LLM 配置中心) package db import ( "context" "fmt" "time" "github.com/echochat/backend/config" "github.com/echochat/backend/pkg/logs" "go.uber.org/zap" "gorm.io/driver/mysql" "gorm.io/gorm" ) // LLMSourceDB 包装外部 LLM 配置库连接 // // 之所以用 wrapper 而不是直接返回 *gorm.DB: // - 主库 (*gorm.DB) 在 Wire 中已被全局注入到所有 DAO,不能让 LLM 库被误用为业务库 // - Enabled=false 时仍要构造一个空 wrapper,避免下游 nil 引用 // - 下游 DAO 通过 IsEnabled() 决定是否走数据库路径或返回未配置错误 type LLMSourceDB struct { db *gorm.DB enabled bool } // IsEnabled 是否真正连接了 MySQL;false 时禁止访问 DB() func (l *LLMSourceDB) IsEnabled() bool { return l != nil && l.enabled && l.db != nil } // DB 获取底层 GORM 实例(仅 IsEnabled() == true 时返回非 nil) // 调用方应先判 IsEnabled,再使用返回值。 func (l *LLMSourceDB) DB() *gorm.DB { if !l.IsEnabled() { return nil } return l.db } // NewLLMSourceDB 初始化外部 LLM 配置 MySQL 连接 // // 行为约定: // - cfg.Enabled == false:跳过连接,返回 enabled=false 的空 wrapper(不报错),保证 Wire 流程不阻塞 // - cfg.Enabled == true 但连接失败:返回 error,由 main 决定是否硬失败 // // 之所以连接失败时硬报错而不是 graceful degrade: // - 用户已显式 enabled=true,预期就要用,沉默降级反而难排查 // - 转写功能未启用时只需保持 enabled=false 即可 func NewLLMSourceDB(cfg *config.LLMSourceConfig) (*LLMSourceDB, error) { funcName := "db.NewLLMSourceDB" ctx := context.Background() if cfg == nil || !cfg.Enabled { logs.Info(ctx, funcName, "外部 LLM 配置库未启用,跳过 MySQL 连接(语音转写功能将不可用)") return &LLMSourceDB{enabled: false}, nil } logs.Info(ctx, funcName, "正在连接外部 LLM 配置 MySQL", zap.String("host", cfg.Host), zap.Int("port", cfg.Port), zap.String("dbname", cfg.DBName), ) gdb, err := gorm.Open(mysql.Open(cfg.DSN()), &gorm.Config{ Logger: &zapGormLogger{}, }) if err != nil { logs.Error(ctx, funcName, "MySQL 连接失败", zap.Error(err)) return nil, fmt.Errorf("连接 LLM 配置 MySQL 失败: %w", err) } sqlDB, err := gdb.DB() if err != nil { return nil, fmt.Errorf("获取 MySQL 底层 sql.DB 失败: %w", err) } maxIdle := cfg.MaxIdleConns if maxIdle <= 0 { maxIdle = 5 } maxOpen := cfg.MaxOpenConns if maxOpen <= 0 { maxOpen = 20 } sqlDB.SetMaxIdleConns(maxIdle) sqlDB.SetMaxOpenConns(maxOpen) sqlDB.SetConnMaxLifetime(time.Hour) if err := sqlDB.Ping(); err != nil { logs.Error(ctx, funcName, "MySQL Ping 失败", zap.Error(err)) return nil, fmt.Errorf("ping LLM 配置 MySQL 失败: %w", err) } logs.Info(ctx, funcName, "外部 LLM 配置 MySQL 连接成功") return &LLMSourceDB{db: gdb, enabled: true}, nil }