diff --git a/internal/service/admin_master.go b/internal/service/admin_master.go index 21cf1f6f..46232ecb 100644 --- a/internal/service/admin_master.go +++ b/internal/service/admin_master.go @@ -68,6 +68,7 @@ func NewMasterDataService() *MasterDataService { func (s *MasterDataService) ListCategories(orgID int64) ([]model.MaterialCategory, error) { return s.repo.ListCategories(orgID) } + // CreateCategory 新建分类。请求体可能带着前端表单残留的 id,这里不读取 item.ID, // repository 的 INSERT 也不写 id 列,所以脏 id 不会把新建变成覆盖。 func (s *MasterDataService) CreateCategory(item *model.MaterialCategory) error { @@ -104,6 +105,7 @@ func prepareCategory(item *model.MaterialCategory) error { func (s *MasterDataService) ListUnits(orgID int64) ([]model.Unit, error) { return s.repo.ListUnits(orgID) } + // CreateUnit 新建单位。同样不读取 item.ID,避免表单残留的脏 id 把新建变成覆盖。 func (s *MasterDataService) CreateUnit(item *model.Unit) error { if err := prepareUnit(item); err != nil { @@ -154,6 +156,7 @@ func (s *MasterDataService) NextMaterialCode(orgID, categoryID int64) (*model.Ne } return &model.NextMaterialCode{Available: true, Code: rule.FormatCode(rule.NextSequence)}, nil } + // CreateCodeRule 新建编码规则。同样不读取 item.ID,避免表单残留的脏 id 把新建变成覆盖。 func (s *MasterDataService) CreateCodeRule(item *model.CodeRule) error { if err := prepareCodeRule(item); err != nil { @@ -202,6 +205,7 @@ func (s *PartnerService) List(orgID int64, role string) ([]model.BusinessPartner } return s.repo.List(orgID, role) } + // Create 新建伙伴。请求体可能带着前端表单残留的 id,这里不读取 item.ID, // repository 的 INSERT 也不写 id 列,所以脏 id 不会把新建变成覆盖。 func (s *PartnerService) Create(item *model.BusinessPartner) error { @@ -251,6 +255,7 @@ func NewWarehouseService() *WarehouseService { func (s *WarehouseService) List(orgID int64) ([]model.Warehouse, error) { return s.repo.List(orgID) } + // Create 新建仓库。不读取 item.ID,脏 id 不会把新建变成覆盖。 func (s *WarehouseService) Create(item *model.Warehouse) error { if err := prepareWarehouse(item); err != nil { diff --git a/internal/service/admin_master_test.go b/internal/service/admin_master_test.go index 23c91175..517c0a3b 100644 --- a/internal/service/admin_master_test.go +++ b/internal/service/admin_master_test.go @@ -131,3 +131,69 @@ func TestLocationCreateRequiresWarehouseID(t *testing.T) { t.Fatal("缺少 warehouse_id 的库位创建应当被拒绝") } } + +// 上面的 UpdateRequiresID 只覆盖了拒绝分支,方法体里真正的 s.repo.Update 从未被执行。 +// 六个 Update* 互为复制粘贴的模板,若某一处尾行被误写成 s.repo.Create, +// 生产行为会退化成「编辑即新增」——与本次修复的缺陷同级。这里补上走通的路径。 +func TestPartnerUpdateUpdatesInsteadOfInserting(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.ExpectBegin() + mock.ExpectQuery("SELECT business_partner_id FROM organization_partners"). + WithArgs(int64(12), int64(1)). + WillReturnRows(sqlmock.NewRows([]string{"business_partner_id"}).AddRow(int64(7))) + mock.ExpectExec("UPDATE business_partners").WillReturnResult(sqlmock.NewResult(0, 1)) + mock.ExpectExec("UPDATE organization_partners").WillReturnResult(sqlmock.NewResult(0, 1)) + mock.ExpectCommit() + + item := &model.BusinessPartner{ + ID: 12, + OrganizationID: 1, + PartnerCode: "SUP-007", + LegalName: "伙伴改名", + IsSupplier: true, + } + if err := NewPartnerService().Update(item); err != nil { + t.Fatalf("Update: %v", err) + } + // 只登记了 SELECT ... FOR UPDATE 与两条 UPDATE。若尾行被误写成 Create, + // 事务里发出的 INSERT 与这里的期望不匹配:要么 Create 直接报错、要么 + // 这两条 UPDATE 永不被消费,ExpectationsWereMet 必然失败。 + if err := mock.ExpectationsWereMet(); err != nil { + t.Fatalf("应当执行 UPDATE,实际 SQL 不是更新: %v", err) + } +} + +func TestCategoryUpdateUpdatesInsteadOfInserting(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("UPDATE material_categories"). + WithArgs("改名后的分类", "说明", nil, "active", int64(9), int64(1)). + WillReturnResult(sqlmock.NewResult(0, 1)) + + item := &model.MaterialCategory{ + ID: 9, + OrganizationID: 1, + Code: "A01", + Name: "改名后的分类", + Description: "说明", + } + if err := NewMasterDataService().UpdateCategory(item); err != nil { + t.Fatalf("UpdateCategory: %v", err) + } + // 这里只登记了 UPDATE;误写成 Create 时发出的 INSERT 不会被消费, + // Expectation 落空,ExpectationsWereMet 报错。 + if err := mock.ExpectationsWereMet(); err != nil { + t.Fatalf("应当执行 UPDATE,实际 SQL 不是插入: %v", err) + } +}