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