diff --git a/internal/handler/admin_master.go b/internal/handler/admin_master.go index 9c2b6ee2..dcc4ca8d 100644 --- a/internal/handler/admin_master.go +++ b/internal/handler/admin_master.go @@ -349,7 +349,7 @@ func (h *Handler) CreateWarehouse(c *gin.Context) { return } item.OrganizationID = orgID - if err := h.warehouseSvc.Save(&item); err != nil { + if err := h.warehouseSvc.Create(&item); err != nil { response.BadRequest(c, err.Error()) return } @@ -369,7 +369,7 @@ func (h *Handler) UpdateWarehouse(c *gin.Context) { return } item.ID, item.OrganizationID = id, orgID - if err := h.warehouseSvc.Save(&item); err != nil { + if err := h.warehouseSvc.Update(&item); err != nil { response.BadRequest(c, err.Error()) return } @@ -389,7 +389,7 @@ func (h *Handler) CreateWarehouseLocation(c *gin.Context) { return } item.WarehouseID = warehouseID - if err := h.warehouseSvc.SaveLocation(&item, orgID); err != nil { + if err := h.warehouseSvc.CreateLocation(&item, orgID); err != nil { response.BadRequest(c, err.Error()) return } @@ -414,7 +414,7 @@ func (h *Handler) UpdateWarehouseLocation(c *gin.Context) { return } item.ID, item.WarehouseID = locationID, warehouseID - if err := h.warehouseSvc.SaveLocation(&item, orgID); err != nil { + if err := h.warehouseSvc.UpdateLocation(&item, orgID); err != nil { response.BadRequest(c, err.Error()) return } diff --git a/internal/service/admin_master.go b/internal/service/admin_master.go index 14c0332e..21cf1f6f 100644 --- a/internal/service/admin_master.go +++ b/internal/service/admin_master.go @@ -251,7 +251,27 @@ func NewWarehouseService() *WarehouseService { func (s *WarehouseService) List(orgID int64) ([]model.Warehouse, error) { return s.repo.List(orgID) } -func (s *WarehouseService) Save(item *model.Warehouse) error { +// Create 新建仓库。不读取 item.ID,脏 id 不会把新建变成覆盖。 +func (s *WarehouseService) Create(item *model.Warehouse) error { + if err := prepareWarehouse(item); err != nil { + return err + } + return s.repo.Create(item) +} + +// Update 更新仓库。id 来自路由参数,必须显式存在。 +func (s *WarehouseService) Update(item *model.Warehouse) error { + if item.ID == 0 { + return errUpdateRequiresID + } + if err := prepareWarehouse(item); err != nil { + return err + } + return s.repo.Update(item) +} + +// prepareWarehouse 是新建与更新共用的字段清理、校验和默认值补全。 +func prepareWarehouse(item *model.Warehouse) error { item.Code = strings.TrimSpace(item.Code) item.Name = strings.TrimSpace(item.Name) if item.Code == "" || item.Name == "" { @@ -260,12 +280,30 @@ func (s *WarehouseService) Save(item *model.Warehouse) error { if item.Status == "" { item.Status = "active" } - if item.ID == 0 { - return s.repo.Create(item) - } - return s.repo.Update(item) + return nil } -func (s *WarehouseService) SaveLocation(item *model.WarehouseLocation, orgID int64) error { + +// CreateLocation 新建库位。orgID 用于校验仓库归属,item.ID 不参与。 +func (s *WarehouseService) CreateLocation(item *model.WarehouseLocation, orgID int64) error { + if err := prepareLocation(item); err != nil { + return err + } + return s.repo.CreateLocation(item, orgID) +} + +// UpdateLocation 更新库位。id 来自路由参数,必须显式存在。 +func (s *WarehouseService) UpdateLocation(item *model.WarehouseLocation, orgID int64) error { + if item.ID == 0 { + return errUpdateRequiresID + } + if err := prepareLocation(item); err != nil { + return err + } + return s.repo.UpdateLocation(item, orgID) +} + +// prepareLocation 是新建与更新共用的字段清理、校验和默认值补全。 +func prepareLocation(item *model.WarehouseLocation) error { item.Code = strings.TrimSpace(item.Code) item.Name = strings.TrimSpace(item.Name) if item.Code == "" || item.Name == "" || item.WarehouseID <= 0 { @@ -274,8 +312,5 @@ func (s *WarehouseService) SaveLocation(item *model.WarehouseLocation, orgID int if item.Status == "" { item.Status = "active" } - if item.ID == 0 { - return s.repo.CreateLocation(item, orgID) - } - return s.repo.UpdateLocation(item, orgID) + return nil } diff --git a/internal/service/admin_master_test.go b/internal/service/admin_master_test.go index 20b74d25..23c91175 100644 --- a/internal/service/admin_master_test.go +++ b/internal/service/admin_master_test.go @@ -98,3 +98,36 @@ func TestUnitCreateStillValidatesPrecisionScale(t *testing.T) { t.Fatal("precision_scale 超过 6 应当被拒绝") } } + +func TestWarehouseCreateInsertsEvenWhenItemCarriesStaleID(t *testing.T) { + db, mock, err := sqlmock.New() + if err != nil { + t.Fatalf("sqlmock: %v", err) + } + database.DB = db + t.Cleanup(func() { database.DB = nil; db.Close() }) + + mock.ExpectExec("INSERT INTO warehouses").WillReturnResult(sqlmock.NewResult(5, 1)) + + item := &model.Warehouse{ID: 3, Code: "WH2", Name: "二号仓"} + if err := NewWarehouseService().Create(item); err != nil { + t.Fatalf("Create: %v", err) + } + if err := mock.ExpectationsWereMet(); err != nil { + t.Fatalf("应当执行 INSERT,实际 SQL 不是插入: %v", err) + } +} + +func TestWarehouseUpdateRequiresID(t *testing.T) { + err := NewWarehouseService().Update(&model.Warehouse{Code: "WH1", Name: "一号仓"}) + if err == nil { + t.Fatal("缺少 id 的更新应当被拒绝") + } +} + +func TestLocationCreateRequiresWarehouseID(t *testing.T) { + err := NewWarehouseService().CreateLocation(&model.WarehouseLocation{Code: "L1", Name: "库位一"}, 1) + if err == nil { + t.Fatal("缺少 warehouse_id 的库位创建应当被拒绝") + } +}