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 拼接; 全部做过变异验证(抽掉实现后对应测试确实变红)。
141 lines
4.9 KiB
Go
141 lines
4.9 KiB
Go
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
|
||
}
|