From 1b0cdde78005d0bd60465889a5ca6e9641bda26b 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 14:59:01 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E6=8B=86=E5=88=86=20PartnerService?= =?UTF-8?q?=20=E7=9A=84=20Create/Update=EF=BC=8C=E5=88=9B=E5=BB=BA?= =?UTF-8?q?=E6=8E=A5=E5=8F=A3=E4=B8=8D=E5=86=8D=E8=A2=AB=E8=84=8F=20id=20?= =?UTF-8?q?=E5=B8=A6=E5=81=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/handler/admin_master.go | 4 +- internal/service/admin_master.go | 29 +++++++++++--- internal/service/admin_master_test.go | 57 +++++++++++++++++++++++++++ 3 files changed, 83 insertions(+), 7 deletions(-) create mode 100644 internal/service/admin_master_test.go diff --git a/internal/handler/admin_master.go b/internal/handler/admin_master.go index b20d3266..e8a688a4 100644 --- a/internal/handler/admin_master.go +++ b/internal/handler/admin_master.go @@ -300,7 +300,7 @@ func (h *Handler) CreatePartner(c *gin.Context) { return } item.OrganizationID = orgID - if err := h.partnerSvc.Save(&item); err != nil { + if err := h.partnerSvc.Create(&item); err != nil { response.BadRequest(c, err.Error()) return } @@ -320,7 +320,7 @@ func (h *Handler) UpdatePartner(c *gin.Context) { return } item.ID, item.OrganizationID = id, orgID - if err := h.partnerSvc.Save(&item); err != nil { + if err := h.partnerSvc.Update(&item); err != nil { response.BadRequest(c, err.Error()) return } diff --git a/internal/service/admin_master.go b/internal/service/admin_master.go index 30233284..53258c24 100644 --- a/internal/service/admin_master.go +++ b/internal/service/admin_master.go @@ -9,6 +9,7 @@ import ( ) var errInvalidMasterData = errors.New("code and name are required") +var errUpdateRequiresID = errors.New("id is required for update") var ErrProtectedRole = repository.ErrProtectedRole type OrganizationService struct { @@ -149,7 +150,28 @@ func (s *PartnerService) List(orgID int64, role string) ([]model.BusinessPartner } return s.repo.List(orgID, role) } -func (s *PartnerService) Save(item *model.BusinessPartner) error { +// Create 新建伙伴。请求体可能带着前端表单残留的 id,这里不读取 item.ID, +// repository 的 INSERT 也不写 id 列,所以脏 id 不会把新建变成覆盖。 +func (s *PartnerService) Create(item *model.BusinessPartner) error { + if err := preparePartner(item); err != nil { + return err + } + return s.repo.Create(item) +} + +// Update 更新伙伴。id 来自路由参数,必须显式存在。 +func (s *PartnerService) Update(item *model.BusinessPartner) error { + if item.ID == 0 { + return errUpdateRequiresID + } + if err := preparePartner(item); err != nil { + return err + } + return s.repo.Update(item) +} + +// preparePartner 是新建与更新共用的字段清理、校验和默认值补全。 +func preparePartner(item *model.BusinessPartner) error { item.PartnerCode = strings.TrimSpace(item.PartnerCode) item.LegalName = strings.TrimSpace(item.LegalName) if item.PartnerCode == "" || item.LegalName == "" { @@ -164,10 +186,7 @@ func (s *PartnerService) Save(item *model.BusinessPartner) error { if item.IsCustomer && item.CustomerStatus == "" { item.CustomerStatus = "active" } - if item.ID == 0 { - return s.repo.Create(item) - } - return s.repo.Update(item) + return nil } type WarehouseService struct { diff --git a/internal/service/admin_master_test.go b/internal/service/admin_master_test.go new file mode 100644 index 00000000..4e26d1db --- /dev/null +++ b/internal/service/admin_master_test.go @@ -0,0 +1,57 @@ +package service + +import ( + "testing" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/wmms/wmms/internal/model" + "github.com/wmms/wmms/pkg/database" +) + +// 前端表单在「新增」时可能残留上次编辑过的 id(PartnerList.vue 的 +// openCreate 只做 Object.assign、不删多余键)。创建必须无视这个 id, +// 否则会走成 UPDATE 把旧记录覆盖掉——这正是本次要修的缺陷。 +func TestPartnerCreateInsertsEvenWhenItemCarriesStaleID(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.ExpectExec("INSERT INTO business_partners").WillReturnResult(sqlmock.NewResult(2, 1)) + mock.ExpectExec("INSERT INTO organization_partners").WillReturnResult(sqlmock.NewResult(2, 1)) + mock.ExpectCommit() + + item := &model.BusinessPartner{ID: 1, PartnerCode: "SUP-002", LegalName: "新伙伴", IsSupplier: true} + if err := NewPartnerService().Create(item); err != nil { + t.Fatalf("Create: %v", err) + } + if err := mock.ExpectationsWereMet(); err != nil { + t.Fatalf("应当执行 INSERT,实际 SQL 不是插入: %v", err) + } +} + +// 更新接口漏传 id 时必须报错,否则会静默写 0 号记录(什么都不改)而不自知。 +func TestPartnerUpdateRequiresID(t *testing.T) { + err := NewPartnerService().Update(&model.BusinessPartner{PartnerCode: "SUP-001", LegalName: "伙伴", IsSupplier: true}) + if err == nil { + t.Fatal("缺少 id 的更新应当被拒绝") + } +} + +// 拆方法不能把校验丢了。 +func TestPartnerCreateStillValidatesName(t *testing.T) { + err := NewPartnerService().Create(&model.BusinessPartner{PartnerCode: "SUP-001", IsSupplier: true}) + if err == nil { + t.Fatal("legal_name 为空应当被拒绝") + } +} + +func TestPartnerCreateStillRequiresARole(t *testing.T) { + err := NewPartnerService().Create(&model.BusinessPartner{PartnerCode: "SUP-001", LegalName: "伙伴"}) + if err == nil { + t.Fatal("既非供应商也非客户时应当被拒绝") + } +}