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

177 lines
5.1 KiB
Go

package repository
import (
"errors"
"fmt"
"strings"
"github.com/wmms/wmms/internal/model"
"github.com/wmms/wmms/pkg/database"
)
type OrganizationRepository struct{}
var ErrProtectedRole = errors.New("super administrator role cannot be changed")
func NewOrganizationRepository() *OrganizationRepository { return &OrganizationRepository{} }
func (r *OrganizationRepository) List() ([]model.Organization, error) {
rows, err := database.DB.Query(`SELECT id, code, name, status, default_timezone, created_at, updated_at
FROM organizations ORDER BY id`)
if err != nil {
return nil, err
}
defer rows.Close()
organizations := make([]model.Organization, 0)
for rows.Next() {
var item model.Organization
if err := rows.Scan(&item.ID, &item.Code, &item.Name, &item.Status, &item.DefaultTimezone, &item.CreatedAt, &item.UpdatedAt); err != nil {
return nil, err
}
organizations = append(organizations, item)
}
return organizations, rows.Err()
}
func (r *OrganizationRepository) Create(item *model.Organization) error {
result, err := database.DB.Exec(`INSERT INTO organizations (code, name, status, default_timezone) VALUES (?, ?, ?, ?)`, item.Code, item.Name, item.Status, item.DefaultTimezone)
if err != nil {
return err
}
item.ID, err = result.LastInsertId()
return err
}
func (r *OrganizationRepository) Update(item *model.Organization) error {
return execExpectingRow(`UPDATE organizations SET name = ?, status = ?, default_timezone = ? WHERE id = ?`, item.Name, item.Status, item.DefaultTimezone, item.ID)
}
func (r *OrganizationRepository) ListRoles() ([]model.Role, error) {
rows, err := database.DB.Query(`SELECT id, code, name, enabled, created_at, updated_at FROM roles ORDER BY id`)
if err != nil {
return nil, err
}
defer rows.Close()
roles := make([]model.Role, 0)
roleIndexes := make(map[int64]int)
for rows.Next() {
var item model.Role
if err := rows.Scan(&item.ID, &item.Code, &item.Name, &item.Enabled, &item.CreatedAt, &item.UpdatedAt); err != nil {
return nil, err
}
item.Permissions = []model.Permission{}
roleIndexes[item.ID] = len(roles)
roles = append(roles, item)
}
if err := rows.Err(); err != nil {
return nil, err
}
permissionRows, err := database.DB.Query(`SELECT rp.role_id, p.id, p.code, p.name
FROM role_permissions rp JOIN permissions p ON p.id = rp.permission_id
ORDER BY rp.role_id, p.code`)
if err != nil {
return nil, err
}
defer permissionRows.Close()
for permissionRows.Next() {
var roleID int64
var permission model.Permission
if err := permissionRows.Scan(&roleID, &permission.ID, &permission.Code, &permission.Name); err != nil {
return nil, err
}
if index, ok := roleIndexes[roleID]; ok {
roles[index].Permissions = append(roles[index].Permissions, permission)
}
}
return roles, permissionRows.Err()
}
func (r *OrganizationRepository) ListPermissions() ([]model.Permission, error) {
rows, err := database.DB.Query(`SELECT id, code, name FROM permissions ORDER BY code`)
if err != nil {
return nil, err
}
defer rows.Close()
permissions := make([]model.Permission, 0)
for rows.Next() {
var permission model.Permission
if err := rows.Scan(&permission.ID, &permission.Code, &permission.Name); err != nil {
return nil, err
}
permissions = append(permissions, permission)
}
return permissions, rows.Err()
}
func (r *OrganizationRepository) UpdateRole(roleID int64, name string, enabled bool, permissionCodes []string) (err error) {
tx, err := database.DB.Begin()
if err != nil {
return err
}
defer func() {
if err != nil {
_ = tx.Rollback()
}
}()
var roleCode string
if err = tx.QueryRow(`SELECT code FROM roles WHERE id = ? FOR UPDATE`, roleID).Scan(&roleCode); err != nil {
return err
}
if roleCode == "super_admin" {
return ErrProtectedRole
}
if _, err = tx.Exec(`UPDATE roles SET name = ?, enabled = ? WHERE id = ?`, name, enabled, roleID); err != nil {
return err
}
if _, err = tx.Exec(`DELETE FROM role_permissions WHERE role_id = ?`, roleID); err != nil {
return err
}
permissionCodes = uniqueStrings(permissionCodes)
if len(permissionCodes) > 0 {
placeholders := strings.TrimSuffix(strings.Repeat("?, ", len(permissionCodes)), ", ")
query := fmt.Sprintf(`INSERT INTO role_permissions (role_id, permission_id)
SELECT ?, id FROM permissions WHERE code IN (%s)`, placeholders)
args := make([]interface{}, 0, len(permissionCodes)+1)
args = append(args, roleID)
for _, code := range permissionCodes {
args = append(args, code)
}
result, execErr := tx.Exec(query, args...)
if execErr != nil {
err = execErr
return err
}
var affected int64
affected, err = result.RowsAffected()
if err != nil {
return err
}
if affected != int64(len(permissionCodes)) {
return fmt.Errorf("one or more permission codes do not exist")
}
}
return tx.Commit()
}
func uniqueStrings(values []string) []string {
seen := make(map[string]struct{}, len(values))
result := make([]string, 0, len(values))
for _, value := range values {
value = strings.TrimSpace(value)
if value == "" {
continue
}
if _, exists := seen[value]; exists {
continue
}
seen[value] = struct{}{}
result = append(result, value)
}
return result
}