package database import ( "database/sql" "fmt" "strings" "time" _ "github.com/go-sql-driver/mysql" ) var DB *sql.DB // appendDSNParam 往 DSN 里补一个连接参数。key 已经出现过就原样返回——调用方 // 显式配的值优先,哪怕配的是反值。 func appendDSNParam(dsn, key, value string) string { if strings.Contains(dsn, key+"=") { return dsn } if strings.Contains(dsn, "?") { return dsn + "&" + key + "=" + value } return dsn + "?" + key + "=" + value } // normalizeDSN 补全连接参数。这两个都在代码里兜底,不要求每个部署环境都记得写: // - parseTime=true 把 DATETIME 列扫成 time.Time; // - clientFoundRows=true 让 UPDATE 的 RowsAffected 返回「匹配」的行数,而不是 // MySQL 默认的「实际改变」的行数。默认语义下「编辑时一个字段都没改」和 // 「记录不存在」都返回 0,主数据的 Update* 就没法据此判断记录是否存在; // 开启后两者分别是 1 和 0,可以区分。对仓库/库位的 JOIN 形式 UPDATE 同样有效。 func normalizeDSN(dsn string) string { dsn = appendDSNParam(dsn, "parseTime", "true") return appendDSNParam(dsn, "clientFoundRows", "true") } func Init(dsn string) error { dsn = normalizeDSN(dsn) var err error DB, err = sql.Open("mysql", dsn) if err != nil { return fmt.Errorf("failed to open database: %w", err) } DB.SetMaxOpenConns(25) DB.SetMaxIdleConns(5) DB.SetConnMaxLifetime(5 * time.Minute) if err = DB.Ping(); err != nil { return fmt.Errorf("failed to ping database: %w", err) } return nil } func Close() error { if DB != nil { return DB.Close() } return nil }