material_manage_system/pkg/database/database.go
文睿 张 696e90893b fix: 主数据编辑的两类静默失败
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 拼接;
全部做过变异验证(抽掉实现后对应测试确实变红)。
2026-09-21 17:26:53 +08:00

63 lines
1.7 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
}