material_manage_system/internal/repository/warehouse.go
文睿 张 5d508cd2bb fix: 给不存在的仓库建库位时误报创建成功
CreateLocation 的 INSERT...SELECT 匹配不到仓库时影响 0 行,MySQL 不报错:
err 为 nil、LastInsertId() 返回 0。此前只看 err,接口回 201
"Location created successfully" 且 data.id 为 0,而库里一行都没写进去——
刷新后库位消失,重试还是"成功"。

改为以 RowsAffected 为准(INSERT 的 RowsAffected 就是实际插入行数,不受
clientFoundRows 影响),影响 0 行返回 ErrWarehouseNotFound,接口回 400
"仓库不存在,可能已被删除或不属于当前组织"。与既有的 execExpectingRow、
role_permissions 插入保持同一套路。

实测(docker,重建后端镜像后):
  不存在的仓库 999  ->  HTTP 400,warehouse_locations 1 行 -> 1 行
  真实仓库 1        ->  HTTP 201 id=3,1 行 -> 2 行,库中确有此行

新增 warehouse_test.go 覆盖两个分支,变异验证:去掉 affected==0 判断后
"影响 0 行"用例失败(返回 <nil>,正是线上症状)。
2026-09-22 18:21:31 +08:00

96 lines
4.0 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"
"github.com/wmms/wmms/internal/model"
"github.com/wmms/wmms/pkg/database"
)
// ErrWarehouseNotFound 表示仓库 id 不存在,或这条仓库不属于当前组织。
// 建库位时的 INSERT...SELECT 影响 0 行就是这种情况,接口据此回错,
// 而不是把「没写成」报成"创建成功"。
var ErrWarehouseNotFound = errors.New("仓库不存在,可能已被删除或不属于当前组织")
type WarehouseRepository struct{}
func NewWarehouseRepository() *WarehouseRepository { return &WarehouseRepository{} }
func (r *WarehouseRepository) List(organizationID int64) ([]model.Warehouse, error) {
rows, err := database.DB.Query(`SELECT w.id, w.organization_id, w.code, w.name, w.status,
l.id, l.code, l.name, l.status
FROM warehouses w
LEFT JOIN warehouse_locations l ON l.warehouse_id = w.id
WHERE w.organization_id = ? ORDER BY w.code, l.code`, organizationID)
if err != nil {
return nil, err
}
defer rows.Close()
items := make([]model.Warehouse, 0)
index := make(map[int64]int)
for rows.Next() {
var warehouse model.Warehouse
var locationID sql.NullInt64
var locationCode, locationName, locationStatus sql.NullString
if err := rows.Scan(&warehouse.ID, &warehouse.OrganizationID, &warehouse.Code, &warehouse.Name, &warehouse.Status, &locationID, &locationCode, &locationName, &locationStatus); err != nil {
return nil, err
}
position, exists := index[warehouse.ID]
if !exists {
warehouse.Locations = []model.WarehouseLocation{}
items = append(items, warehouse)
position = len(items) - 1
index[warehouse.ID] = position
}
if locationID.Valid {
items[position].Locations = append(items[position].Locations, model.WarehouseLocation{ID: locationID.Int64, WarehouseID: warehouse.ID, Code: locationCode.String, Name: locationName.String, Status: locationStatus.String})
}
}
return items, rows.Err()
}
func (r *WarehouseRepository) Create(item *model.Warehouse) error {
result, err := database.DB.Exec(`INSERT INTO warehouses (organization_id, code, name, status) VALUES (?, ?, ?, ?)`, item.OrganizationID, item.Code, item.Name, item.Status)
if err != nil {
return err
}
item.ID, err = result.LastInsertId()
return err
}
func (r *WarehouseRepository) Update(item *model.Warehouse) error {
return execExpectingRow(`UPDATE warehouses SET name = ?, status = ? WHERE id = ? AND organization_id = ?`, item.Name, item.Status, item.ID, item.OrganizationID)
}
func (r *WarehouseRepository) CreateLocation(item *model.WarehouseLocation, organizationID int64) error {
// 仓库归属条件写在 SELECT 里,避免「先查后插」之间的竞态。
//
// 匹配不到仓库时这条语句影响 0 行,但 MySQL 不报错:err 为 nil,LastInsertId()
// 返回 0。此前只看 err,接口回 201「创建成功」而库里一行都没有(实测请求前后
// warehouse_locations 行数都是 1,刷新后库位消失)。所以这里和按主键更新一样,
// 以 RowsAffected 为准——INSERT 的 RowsAffected 就是实际插入行数,不受
// clientFoundRows 影响。
result, err := database.DB.Exec(`INSERT INTO warehouse_locations (warehouse_id, code, name, status)
SELECT id, ?, ?, ? FROM warehouses WHERE id = ? AND organization_id = ?`, item.Code, item.Name, item.Status, item.WarehouseID, organizationID)
if err != nil {
return err
}
affected, err := result.RowsAffected()
if err != nil {
return err
}
if affected == 0 {
return ErrWarehouseNotFound
}
item.ID, err = result.LastInsertId()
return err
}
func (r *WarehouseRepository) UpdateLocation(item *model.WarehouseLocation, organizationID int64) error {
// JOIN 形式的 UPDATE 同样受 clientFoundRows 影响,影响 0 行也代表库位不存在
// 或不属于该组织(已实测)。
return execExpectingRow(`UPDATE warehouse_locations l JOIN warehouses w ON w.id = l.warehouse_id
SET l.name = ?, l.status = ? WHERE l.id = ? AND w.organization_id = ?`, item.Name, item.Status, item.ID, organizationID)
}