material_manage_system/internal/repository/partner.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

141 lines
4.9 KiB
Go
Raw 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 repository
import (
"database/sql"
"errors"
"fmt"
"github.com/wmms/wmms/internal/model"
"github.com/wmms/wmms/pkg/database"
)
type PartnerRepository struct{}
func NewPartnerRepository() *PartnerRepository { return &PartnerRepository{} }
func (r *PartnerRepository) List(organizationID int64, role string, limit, offset int) ([]model.BusinessPartner, int64, error) {
// 角色过滤写成一份,由计数和取数共用:两处各写一遍,改一处漏一处就会让
// 总数和列表对不上(总数算的是全部,列表却只有供应商)。
filter := `FROM organization_partners op
JOIN business_partners bp ON bp.id = op.business_partner_id
WHERE op.organization_id = ?`
args := []interface{}{organizationID}
if role == "supplier" {
filter += ` AND op.is_supplier = TRUE`
}
if role == "customer" {
filter += ` AND op.is_customer = TRUE`
}
var total int64
if err := database.DB.QueryRow(`SELECT COUNT(*) `+filter, args...).Scan(&total); err != nil {
return nil, 0, err
}
query := `SELECT op.id, op.organization_id, bp.id, op.partner_code, bp.legal_name,
COALESCE(bp.unified_credit_code, ''), COALESCE(bp.contact_name, ''),
COALESCE(bp.contact_phone, ''), COALESCE(bp.address, ''),
op.is_supplier, op.is_customer, COALESCE(op.supplier_status, ''),
COALESCE(op.customer_status, ''), COALESCE(op.settlement_currency, ''),
COALESCE(op.payment_terms, '')
` + filter + `
ORDER BY op.partner_code
LIMIT ? OFFSET ?`
// 计数查询已经用完 args,这里追加分页参数不会再影响到它
rows, err := database.DB.Query(query, append(args, limit, offset)...)
if err != nil {
return nil, 0, err
}
defer rows.Close()
items := make([]model.BusinessPartner, 0)
for rows.Next() {
var item model.BusinessPartner
if err := rows.Scan(&item.ID, &item.OrganizationID, &item.BusinessPartnerID, &item.PartnerCode,
&item.LegalName, &item.UnifiedCreditCode, &item.ContactName, &item.ContactPhone,
&item.Address, &item.IsSupplier, &item.IsCustomer, &item.SupplierStatus,
&item.CustomerStatus, &item.SettlementCurrency, &item.PaymentTerms); err != nil {
return nil, 0, err
}
items = append(items, item)
}
return items, total, rows.Err()
}
func (r *PartnerRepository) Create(item *model.BusinessPartner) (err error) {
tx, err := database.DB.Begin()
if err != nil {
return err
}
defer func() {
if err != nil {
_ = tx.Rollback()
}
}()
result, err := tx.Exec(`INSERT INTO business_partners
(legal_name, unified_credit_code, contact_name, contact_phone, address)
VALUES (?, NULLIF(?, ''), NULLIF(?, ''), NULLIF(?, ''), NULLIF(?, ''))`,
item.LegalName, item.UnifiedCreditCode, item.ContactName, item.ContactPhone, item.Address)
if err != nil {
return err
}
item.BusinessPartnerID, err = result.LastInsertId()
if err != nil {
return err
}
result, err = tx.Exec(`INSERT INTO organization_partners
(organization_id, business_partner_id, partner_code, is_supplier, is_customer,
supplier_status, customer_status, settlement_currency, payment_terms)
VALUES (?, ?, ?, ?, ?, NULLIF(?, ''), NULLIF(?, ''), NULLIF(?, ''), NULLIF(?, ''))`,
item.OrganizationID, item.BusinessPartnerID, item.PartnerCode, item.IsSupplier, item.IsCustomer,
item.SupplierStatus, item.CustomerStatus, item.SettlementCurrency, item.PaymentTerms)
if err != nil {
return err
}
item.ID, err = result.LastInsertId()
if err != nil {
return err
}
return tx.Commit()
}
func (r *PartnerRepository) Update(item *model.BusinessPartner) (err error) {
tx, err := database.DB.Begin()
if err != nil {
return err
}
defer func() {
if err != nil {
_ = tx.Rollback()
}
}()
// 这一句兼任存在性检查:查不到就说明伙伴不存在或不属于当前组织,
// 不能让它以 sql.ErrNoRows 一路冒到接口变成 500。
var businessPartnerID int64
if err = tx.QueryRow(`SELECT business_partner_id FROM organization_partners WHERE id = ? AND organization_id = ? FOR UPDATE`, item.ID, item.OrganizationID).Scan(&businessPartnerID); err != nil {
if errors.Is(err, sql.ErrNoRows) {
return ErrRecordNotFound
}
return err
}
if _, err = tx.Exec(`UPDATE business_partners SET legal_name = ?, unified_credit_code = NULLIF(?, ''),
contact_name = NULLIF(?, ''), contact_phone = NULLIF(?, ''), address = NULLIF(?, '') WHERE id = ?`,
item.LegalName, item.UnifiedCreditCode, item.ContactName, item.ContactPhone, item.Address, businessPartnerID); err != nil {
return err
}
if _, err = tx.Exec(`UPDATE organization_partners SET is_supplier = ?, is_customer = ?,
supplier_status = NULLIF(?, ''), customer_status = NULLIF(?, ''),
settlement_currency = NULLIF(?, ''), payment_terms = NULLIF(?, '') WHERE id = ? AND organization_id = ?`,
item.IsSupplier, item.IsCustomer, item.SupplierStatus, item.CustomerStatus,
item.SettlementCurrency, item.PaymentTerms, item.ID, item.OrganizationID); err != nil {
return err
}
if err = tx.Commit(); err != nil {
return fmt.Errorf("commit partner update: %w", err)
}
return nil
}