UPDATE 漏列: - code_rules 的 SET 补上 category_id。此前改分类提示"保存成功"但不落库, 是唯一用户可达的活缺陷(同页的分类下拉没有 disabled,且规则只能改不能删, 没有"删了重建"这条退路) - code_rules 有 uk_code_rules_org_category,补列后改到已占用的分类会撞唯一键, 新建与更新统一翻成中文的 ErrDuplicateCodeRule - 分类/单位/仓库/库位/伙伴的编码定为不可变:分类编码是编码规则前缀的来源 (init.sql 的种子数据直接取 material_categories.code),伙伴编码是对外业务 编号,改掉会让历史数据脱节且查不回来。service 的 Update* 不再要求编码非空 ——原先是"必须传、传了却被丢弃";新建侧的必填校验保留 - 顺带补齐两处同款漏网:organization.go 的 code 与 partner.go 的 partner_code (前者 service 本就不校验编码,自洽,无需改) 记录不存在时的静默成功: - DSN 开启 clientFoundRows,让 RowsAffected 返回"匹配"行数而非"实际改变"行数。 否则"编辑时一个字段都没改"和"记录不存在"都返回 0,无法区分,直接加检查会把 正常保存误判成记录不存在(真 MySQL 8.0 实测) - 新增 repository.execExpectingRow,7 个 Update* 影响 0 行时返回 ErrRecordNotFound; 伙伴走事务内的 SELECT ... FOR UPDATE,只需翻译 sql.ErrNoRows - 既有 4 处 RowsAffected 用法逐处核对过,均不受该 flag 影响 测试:补 6 个 Update* 的存在性检查、编码必填约束的两端、唯一键翻译、DSN 拼接; 全部做过变异验证(抽掉实现后对应测试确实变红)。
63 lines
1.7 KiB
Go
63 lines
1.7 KiB
Go
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
|
||
}
|