From 7cff2dffed2811ed6c6c6b6300078c34c3c0e9b5 Mon Sep 17 00:00:00 2001 From: yml2213 Date: Thu, 30 Jul 2026 15:12:40 +0800 Subject: [PATCH] =?UTF-8?q?feat(merchant):=20=E6=96=B0=E5=BB=BA=E5=95=86?= =?UTF-8?q?=E6=88=B7=E8=87=AA=E5=8A=A8=E5=A4=8D=E5=88=B6=E8=87=AA=E8=90=A5?= =?UTF-8?q?=E5=95=86=E6=88=B7=E7=9A=84=2027=20=E4=B8=AA=E9=BB=98=E8=AE=A4?= =?UTF-8?q?=E5=95=86=E5=93=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - CreateMerchant 事务内调用 copyDefaultProducts,将自营商户全部可售商品复制给新商户 - 新增 model.MerchantCodeSelfOperated 常量,消除散落的 self-operated 魔法字符串 - tenant.go 替换魔法字符串为常量 - 实测:新建商户后自动获得 27 个商品,价格/货币/库存/关联商品均正确 --- backend/internal/model/merchant.go | 3 ++ backend/internal/service/merchant.go | 42 ++++++++++++++++++++++++++++ backend/internal/service/tenant.go | 2 +- 3 files changed, 46 insertions(+), 1 deletion(-) diff --git a/backend/internal/model/merchant.go b/backend/internal/model/merchant.go index 4278433..908c06c 100644 --- a/backend/internal/model/merchant.go +++ b/backend/internal/model/merchant.go @@ -10,6 +10,9 @@ const ( MerchantStatusActive = "active" MerchantStatusDisabled = "disabled" + // MerchantCodeSelfOperated 是平台自营商户编码,作为默认商品目录的模板来源。 + MerchantCodeSelfOperated = "self-operated" + MerchantFeatureProducts = "products" MerchantFeatureOrders = "orders" MerchantFeatureWallet = "wallet" diff --git a/backend/internal/service/merchant.go b/backend/internal/service/merchant.go index 1b7cc07..b0f3885 100644 --- a/backend/internal/service/merchant.go +++ b/backend/internal/service/merchant.go @@ -103,6 +103,9 @@ func (s *MerchantService) CreateMerchant(in CreateMerchantInput, actorUserID uin }).Error; err != nil { return err } + if err := copyDefaultProducts(tx, merchant.ID); err != nil { + return err + } return writeAudit(tx, &merchant.ID, &actorUserID, nil, "merchant.create", "merchant", fmt.Sprint(merchant.ID), map[string]string{"code": merchant.Code}) }) if err != nil { @@ -532,6 +535,45 @@ func (s *MerchantService) UpdateAPIClientStatus(merchantID, id uint, status stri }) } +// copyDefaultProducts 将自营商户的全部可售商品复制给新建商户,作为默认商品目录。 +// 自营商户(self-operated)充当平台默认商品模板,新商户开箱即用。 +// 使用 OnConflict DoNothing 保证幂等:即使重复调用也不会报唯一索引冲突。 +func copyDefaultProducts(tx *gorm.DB, merchantID uint) error { + var selfMerchant model.Merchant + if err := tx.Where("code = ?", model.MerchantCodeSelfOperated).First(&selfMerchant).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil // 自营商户不存在时跳过,不阻断建商户 + } + return err + } + if selfMerchant.ID == merchantID { + return nil // 自营商户自身无需复制 + } + var templates []model.MerchantProduct + if err := tx.Where("merchant_id = ?", selfMerchant.ID).Find(&templates).Error; err != nil { + return err + } + if len(templates) == 0 { + return nil + } + products := make([]model.MerchantProduct, 0, len(templates)) + for _, t := range templates { + products = append(products, model.MerchantProduct{ + MerchantID: merchantID, + ProductID: t.ProductID, + SKU: t.SKU, + DisplayName: t.DisplayName, + PriceAmount: t.PriceAmount, + CostAmount: t.CostAmount, + Currency: t.Currency, + Stock: t.Stock, + Status: t.Status, + FulfillmentConfig: t.FulfillmentConfig, + }) + } + return tx.Clauses(clause.OnConflict{DoNothing: true}).Create(&products).Error +} + func ensureProduct(tx *gorm.DB, in CreateMerchantProductInput) (*model.Product, error) { if in.ProductCode != "" { var product model.Product diff --git a/backend/internal/service/tenant.go b/backend/internal/service/tenant.go index 90021a6..6a2a275 100644 --- a/backend/internal/service/tenant.go +++ b/backend/internal/service/tenant.go @@ -47,7 +47,7 @@ func (s *TenantService) ResolveMember(userID uint, merchantRef string) (*model.M func (s *TenantService) EnsureSelfMember(userID uint, role string, status int) error { var merchant model.Merchant - if err := s.db.Where("code = ?", "self-operated").First(&merchant).Error; err != nil { + if err := s.db.Where("code = ?", model.MerchantCodeSelfOperated).First(&merchant).Error; err != nil { return err } return s.EnsureMember(merchant.ID, userID, role, status, role == model.MemberRoleOwner)