From 16ca9c02ba7a1ae1e8e982c2832a3ac34226b933 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=96=87=E7=9D=BF=20=E5=BC=A0?= Date: Mon, 21 Sep 2026 15:03:57 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E6=8B=86=E5=88=86=20MasterDataServ?= =?UTF-8?q?ice=20=E7=9A=84=20Create/Update=EF=BC=8C=E5=88=86=E7=B1=BB?= =?UTF-8?q?=E4=B8=8E=E5=8D=95=E4=BD=8D=E5=88=9B=E5=BB=BA=E4=B8=8D=E5=86=8D?= =?UTF-8?q?=E5=8F=97=E8=84=8F=20id=20=E5=BD=B1=E5=93=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/handler/admin_master.go | 12 ++-- internal/service/admin_master.go | 82 ++++++++++++++++++++++----- internal/service/admin_master_test.go | 43 ++++++++++++++ 3 files changed, 116 insertions(+), 21 deletions(-) diff --git a/internal/handler/admin_master.go b/internal/handler/admin_master.go index e8a688a4..9c2b6ee2 100644 --- a/internal/handler/admin_master.go +++ b/internal/handler/admin_master.go @@ -133,7 +133,7 @@ func (h *Handler) CreateMaterialCategory(c *gin.Context) { return } item.OrganizationID = orgID - if err := h.masterDataSvc.SaveCategory(&item); err != nil { + if err := h.masterDataSvc.CreateCategory(&item); err != nil { response.BadRequest(c, err.Error()) return } @@ -153,7 +153,7 @@ func (h *Handler) UpdateMaterialCategory(c *gin.Context) { return } item.ID, item.OrganizationID = id, orgID - if err := h.masterDataSvc.SaveCategory(&item); err != nil { + if err := h.masterDataSvc.UpdateCategory(&item); err != nil { response.BadRequest(c, err.Error()) return } @@ -182,7 +182,7 @@ func (h *Handler) CreateUnit(c *gin.Context) { return } item.OrganizationID = orgID - if err := h.masterDataSvc.SaveUnit(&item); err != nil { + if err := h.masterDataSvc.CreateUnit(&item); err != nil { response.BadRequest(c, err.Error()) return } @@ -202,7 +202,7 @@ func (h *Handler) UpdateUnit(c *gin.Context) { return } item.ID, item.OrganizationID = id, orgID - if err := h.masterDataSvc.SaveUnit(&item); err != nil { + if err := h.masterDataSvc.UpdateUnit(&item); err != nil { response.BadRequest(c, err.Error()) return } @@ -251,7 +251,7 @@ func (h *Handler) CreateCodeRule(c *gin.Context) { return } item.OrganizationID = orgID - if err := h.masterDataSvc.SaveCodeRule(&item); err != nil { + if err := h.masterDataSvc.CreateCodeRule(&item); err != nil { response.BadRequest(c, err.Error()) return } @@ -271,7 +271,7 @@ func (h *Handler) UpdateCodeRule(c *gin.Context) { return } item.ID, item.OrganizationID = id, orgID - if err := h.masterDataSvc.SaveCodeRule(&item); err != nil { + if err := h.masterDataSvc.UpdateCodeRule(&item); err != nil { response.BadRequest(c, err.Error()) return } diff --git a/internal/service/admin_master.go b/internal/service/admin_master.go index 53258c24..14c0332e 100644 --- a/internal/service/admin_master.go +++ b/internal/service/admin_master.go @@ -68,7 +68,28 @@ func NewMasterDataService() *MasterDataService { func (s *MasterDataService) ListCategories(orgID int64) ([]model.MaterialCategory, error) { return s.repo.ListCategories(orgID) } -func (s *MasterDataService) SaveCategory(item *model.MaterialCategory) error { +// CreateCategory 新建分类。请求体可能带着前端表单残留的 id,这里不读取 item.ID, +// repository 的 INSERT 也不写 id 列,所以脏 id 不会把新建变成覆盖。 +func (s *MasterDataService) CreateCategory(item *model.MaterialCategory) error { + if err := prepareCategory(item); err != nil { + return err + } + return s.repo.CreateCategory(item) +} + +// UpdateCategory 更新分类。id 来自路由参数,必须显式存在。 +func (s *MasterDataService) UpdateCategory(item *model.MaterialCategory) error { + if item.ID == 0 { + return errUpdateRequiresID + } + if err := prepareCategory(item); err != nil { + return err + } + return s.repo.UpdateCategory(item) +} + +// prepareCategory 是新建与更新共用的字段清理、校验和默认值补全。 +func prepareCategory(item *model.MaterialCategory) error { item.Code = strings.TrimSpace(item.Code) item.Name = strings.TrimSpace(item.Name) item.Description = strings.TrimSpace(item.Description) @@ -78,15 +99,32 @@ func (s *MasterDataService) SaveCategory(item *model.MaterialCategory) error { if item.Status == "" { item.Status = "active" } - if item.ID == 0 { - return s.repo.CreateCategory(item) - } - return s.repo.UpdateCategory(item) + return nil } func (s *MasterDataService) ListUnits(orgID int64) ([]model.Unit, error) { return s.repo.ListUnits(orgID) } -func (s *MasterDataService) SaveUnit(item *model.Unit) error { +// CreateUnit 新建单位。同样不读取 item.ID,避免表单残留的脏 id 把新建变成覆盖。 +func (s *MasterDataService) CreateUnit(item *model.Unit) error { + if err := prepareUnit(item); err != nil { + return err + } + return s.repo.CreateUnit(item) +} + +// UpdateUnit 更新单位。id 来自路由参数,必须显式存在。 +func (s *MasterDataService) UpdateUnit(item *model.Unit) error { + if item.ID == 0 { + return errUpdateRequiresID + } + if err := prepareUnit(item); err != nil { + return err + } + return s.repo.UpdateUnit(item) +} + +// prepareUnit 是新建与更新共用的字段清理、校验和默认值补全。 +func prepareUnit(item *model.Unit) error { item.Code = strings.TrimSpace(item.Code) item.Name = strings.TrimSpace(item.Name) if item.Code == "" || item.Name == "" { @@ -98,10 +136,7 @@ func (s *MasterDataService) SaveUnit(item *model.Unit) error { if item.PrecisionScale < 0 || item.PrecisionScale > 6 { return errors.New("precision_scale must be between 0 and 6") } - if item.ID == 0 { - return s.repo.CreateUnit(item) - } - return s.repo.UpdateUnit(item) + return nil } func (s *MasterDataService) ListCodeRules(orgID int64) ([]model.CodeRule, error) { return s.repo.ListCodeRules(orgID) @@ -119,7 +154,27 @@ func (s *MasterDataService) NextMaterialCode(orgID, categoryID int64) (*model.Ne } return &model.NextMaterialCode{Available: true, Code: rule.FormatCode(rule.NextSequence)}, nil } -func (s *MasterDataService) SaveCodeRule(item *model.CodeRule) error { +// CreateCodeRule 新建编码规则。同样不读取 item.ID,避免表单残留的脏 id 把新建变成覆盖。 +func (s *MasterDataService) CreateCodeRule(item *model.CodeRule) error { + if err := prepareCodeRule(item); err != nil { + return err + } + return s.repo.CreateCodeRule(item) +} + +// UpdateCodeRule 更新编码规则。id 来自路由参数,必须显式存在。 +func (s *MasterDataService) UpdateCodeRule(item *model.CodeRule) error { + if item.ID == 0 { + return errUpdateRequiresID + } + if err := prepareCodeRule(item); err != nil { + return err + } + return s.repo.UpdateCodeRule(item) +} + +// prepareCodeRule 是新建与更新共用的字段清理、校验和默认值补全。 +func prepareCodeRule(item *model.CodeRule) error { item.Prefix = strings.TrimSpace(item.Prefix) if item.CategoryID <= 0 || item.Prefix == "" { return errors.New("category_id and prefix are required") @@ -133,10 +188,7 @@ func (s *MasterDataService) SaveCodeRule(item *model.CodeRule) error { if item.Status == "" { item.Status = "active" } - if item.ID == 0 { - return s.repo.CreateCodeRule(item) - } - return s.repo.UpdateCodeRule(item) + return nil } type PartnerService struct{ repo *repository.PartnerRepository } diff --git a/internal/service/admin_master_test.go b/internal/service/admin_master_test.go index 4e26d1db..20b74d25 100644 --- a/internal/service/admin_master_test.go +++ b/internal/service/admin_master_test.go @@ -55,3 +55,46 @@ func TestPartnerCreateStillRequiresARole(t *testing.T) { t.Fatal("既非供应商也非客户时应当被拒绝") } } + +// MasterDataSettings.vue 的 openCreate 同样是 Object.assign 复用表单, +// 分类新增也会带上前一次编辑残留的 id,必须走 INSERT。 +func TestCategoryCreateInsertsEvenWhenItemCarriesStaleID(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 material_categories").WillReturnResult(sqlmock.NewResult(9, 1)) + + item := &model.MaterialCategory{ID: 1, Code: "A99", Name: "新分类"} + if err := NewMasterDataService().CreateCategory(item); err != nil { + t.Fatalf("CreateCategory: %v", err) + } + if err := mock.ExpectationsWereMet(); err != nil { + t.Fatalf("应当执行 INSERT,实际 SQL 不是插入: %v", err) + } +} + +func TestUnitUpdateRequiresID(t *testing.T) { + err := NewMasterDataService().UpdateUnit(&model.Unit{Code: "pcs", Name: "件"}) + if err == nil { + t.Fatal("缺少 id 的更新应当被拒绝") + } +} + +func TestCodeRuleUpdateRequiresID(t *testing.T) { + err := NewMasterDataService().UpdateCodeRule(&model.CodeRule{CategoryID: 1, Prefix: "A", SequenceLength: 4}) + if err == nil { + t.Fatal("缺少 id 的更新应当被拒绝") + } +} + +// 单位的小数位校验不能因为拆方法而丢失。 +func TestUnitCreateStillValidatesPrecisionScale(t *testing.T) { + err := NewMasterDataService().CreateUnit(&model.Unit{Code: "kg", Name: "千克", PrecisionScale: 9}) + if err == nil { + t.Fatal("precision_scale 超过 6 应当被拒绝") + } +}