| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140 |
- // Package db 数据库连接池装配 + AutoMigrate 收口(架构规格 §3.5/§3.6,GORM + Repository 分层)。
- //
- // 职责边界(依赖方向铁律:infra 不依赖任何业务域):
- // - 本包**只提供**"建连接池 + 执行 AutoMigrate"两个通用能力,**不**内置任何业务 model;
- // 具体要迁哪些表由**调用方**(cmd/api 装配层 / 集成测试)传入 model 列表——
- // 这样 infra 保持零业务域依赖(若在本包 import identity/model、org/model 会违反铁律)。
- // - 业务域 model 的"全表清单"由各域 model 包提供(identity/model.AllTables / org/model.AllTables /
- // academic/model.AllTables),装配层合并后传入 Migrate。
- // 注:academic 域 27 表由 AC-14 在 cmd/api/main.go 装配处接入(本包不感知业务域,保持零依赖);
- // 接入时的守卫语义不变:仅 `UNIFIED_AUTO_MIGRATE=1` 才执行 AutoMigrate。
- //
- // 与 DDL 的关系(unified/02-data-model/core-model-ddl.sql):
- // - 生产环境割接以 **DDL 文件为准**(PolarDB 建表,information_schema 核对);
- // - AutoMigrate 用于 **dev 环境快速建表 + e2e 测试隔离建表**(幂等:表存在则对齐,不存在则建);
- // - 两者必须逐列一致(T2 验收:GORM model tag 与 DDL 列名/类型/可空性对齐,单测对账)。
- //
- // 典型使用(cmd/api/main.go,T4 接线后):
- //
- // db, err := db.NewDB(cfg.DB) // 建连接池 + ping
- // all := append(append(identitymodel.AllTables(), orgmodel.AllTables()...), task.Task{})
- // if err := db.Migrate(db, all...); err != nil { log.Fatalf("migrate: %v", err) }
- // defer db.Close(db)
- //
- // 双实例(ADR-015):两个 Go 进程各调一次 NewDB(同一 DSN 连同一库),instanceId 只影响
- // 日志/临时目录,不影响连接池——DB 连接池天然共享同一个 PolarDB,无需"双 DSN"。
- package db
- import (
- "fmt"
- "time"
- "gorm.io/driver/mysql"
- "gorm.io/gorm"
- "gorm.io/gorm/logger"
- "yuansiwei/unified-backend/internal/infra/config"
- "yuansiwei/unified-backend/internal/infra/tz"
- )
- // NewDB 建立 MySQL 连接池并验证连通性(fail-fast:连不上库不允许启动,
- // 避免运行到首个查询才暴露"DSN 配错/库没起")。
- //
- // 入参:
- // - cfg config.DBCfg:PolarDB/MySQL 配置(DSN + 连接池参数,见 config.DBCfg)。
- //
- // 出参:
- // - *gorm.DB:就绪的 GORM 句柄(连接池已按 cfg 调参、已 ping 成功)。
- // - error:以下场景返回错误:
- // - DSN 为空(fail-fast,配置缺失);
- // - gorm.Open 失败(驱动初始化/连接失败);
- // - Ping 失败(网络不通/账密错/库不存在)。
- //
- // 连接池参数(§3.6 数据访问纪律):
- // - MaxOpenConns:限制并发连接,防双实例合计打满 PolarDB max_connections;
- // - MaxIdleConns:空闲连接复用,降低握手开销;
- // - ConnMaxLifetimeSec:连接最长存活,防连接池漂移(DB 侧 wait_timeout 与客户端对齐)。
- //
- // 外键约束:gorm.Config 显式 DisableForeignKeyConstraintWhenMigrating=true——
- // 本项目"外键不建物理 FK,应用层 Repository 强制作用域保证"(P0-11 收敛规则 7 / DDL 注释),
- // AutoMigrate 不得自动补 FK 约束(与 DDL 的 FOREIGN_KEY_CHECKS=0 语义一致)。
- func NewDB(cfg config.DBCfg) (*gorm.DB, error) {
- if cfg.DSN == "" {
- return nil, fmt.Errorf("db: DSN 为空(须经 config/dev.json 或环境变量 CONFIG_DB__DSN 提供)")
- }
- // 时区/解析收口(**唯一允许写 loc 的地方**):无论配置或环境变量给的是什么,
- // 一律归一为 parseTime=true + loc=Asia%2FShanghai,与进程时区(tz.Apply)保持一致。
- // 反面:`loc=Local` 会随机器时区漂移,开发机自测绿、UTC 容器整体错 8 小时。
- dsn := tz.NormalizeDSN(cfg.DSN)
- gdb, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
- // 与 DDL"不建物理外键"对齐:迁移时不自动生成 FK 约束
- DisableForeignKeyConstraintWhenMigrating: true,
- // 生产/双实例默认静默 SQL 日志(避免高频只读接口刷日志);联调期可换 logger.Warn
- Logger: logger.Default.LogMode(logger.Silent),
- })
- if err != nil {
- return nil, fmt.Errorf("db: gorm.Open 失败: %w", err)
- }
- sqlDB, err := gdb.DB()
- if err != nil {
- return nil, fmt.Errorf("db: 取底层 *sql.DB 失败: %w", err)
- }
- // 连接池调参(0 值表示用 driver 默认;显式设置保证行为可预期)
- if cfg.MaxOpenConns > 0 {
- sqlDB.SetMaxOpenConns(cfg.MaxOpenConns)
- }
- if cfg.MaxIdleConns > 0 {
- sqlDB.SetMaxIdleConns(cfg.MaxIdleConns)
- }
- if cfg.ConnMaxLifetimeSec > 0 {
- sqlDB.SetConnMaxLifetime(time.Duration(cfg.ConnMaxLifetimeSec) * time.Second)
- }
- // 连通性验证(fail-fast)
- if err := sqlDB.Ping(); err != nil {
- return nil, fmt.Errorf("db: Ping 失败(网络/账密/库名): %w", err)
- }
- return gdb, nil
- }
- // Migrate 对给定模型执行 AutoMigrate(幂等建表/对齐,dev + e2e 用)。
- //
- // 入参:
- // - gdb *gorm.DB:NewDB 返回的句柄。
- // - models ...any:要迁移的模型(*struct 指针,如 &model.User{};由调用方合并各域 AllTables 传入)。
- //
- // 出参:
- // - error:AutoMigrate 失败时返回(DDL 语法错/权限不足/表结构冲突)。
- //
- // 幂等语义:表不存在则按 model 建;已存在则逐列比对,缺列补列、类型不符告警
- // (GORM AutoMigrate 不删多余列、不强改已有列类型——避免误删生产数据,对齐"迁移最易失真处"纪律)。
- //
- // 说明:本函数**不硬编码**任何 model(保持 infra 零业务依赖);
- // 传入空 models 时为无操作(不报错,便于条件迁移)。
- func Migrate(gdb *gorm.DB, models ...any) error {
- if len(models) == 0 {
- return nil
- }
- if err := gdb.AutoMigrate(models...); err != nil {
- return fmt.Errorf("db: AutoMigrate 失败(%d 个模型): %w", len(models), err)
- }
- return nil
- }
- // Close 关闭连接池(优雅退出路径:cmd/api 收到 SIGTERM 后先排空在途任务再 Close,
- // 防连接泄漏——GORM 底层 *sql.DB 不显式 Close 会残留连接占用 PolarDB max_connections)。
- //
- // 入参:
- // - gdb *gorm.DB:NewDB 返回的句柄(nil 安全)。
- //
- // 出参:
- // - error:底层 sqlDB.Close 失败时返回(通常仅进程即将退出时调用,错误可忽略)。
- func Close(gdb *gorm.DB) error {
- if gdb == nil {
- return nil
- }
- sqlDB, err := gdb.DB()
- if err != nil {
- return fmt.Errorf("db: 取底层 *sql.DB 失败: %w", err)
- }
- return sqlDB.Close()
- }
|