拆分 service 与前端大文件,修复 CORS 配置与格式问题
- 后端 internal/service 按职责拆分: fulfillment.go(1397→527)拆出 wallet/timeout/data/order/query/dashboard/shipnotify delivery.go(1124→801)拆出 upstream/link/state/helpers merchant.go(855→251)拆出 member/product/api_client/catalog/helpers - 前端 MerchantCenter.tsx(1327→606)拆出 merchantCenterTabs/merchantCenterUtils - docker-compose backend 透传 CORS_ALLOWED_ORIGINS - CORS 白名单实现(config/router/README/.env.example 配套) - 修复 gofmt 与文件尾部多余空行
This commit is contained in:
@@ -1,19 +1,15 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"affiliate_dash/internal/model"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
var merchantCodePattern = regexp.MustCompile(`^[a-z0-9][a-z0-9-]{2,63}$`)
|
||||
@@ -251,605 +247,3 @@ func (s *MerchantService) GetMerchant(merchantID uint) (*model.Merchant, error)
|
||||
}
|
||||
return &merchant, nil
|
||||
}
|
||||
|
||||
type AddMemberInput struct {
|
||||
UserID uint
|
||||
Role string
|
||||
IsDefault bool
|
||||
}
|
||||
|
||||
func (s *MerchantService) AddMember(merchantID uint, in AddMemberInput, actorUserID uint) (*model.MerchantMember, error) {
|
||||
if !isValidMemberRole(in.Role) {
|
||||
return nil, errors.New("无效的商户成员角色")
|
||||
}
|
||||
member := &model.MerchantMember{
|
||||
MerchantID: merchantID,
|
||||
UserID: in.UserID,
|
||||
Role: in.Role,
|
||||
Status: 1,
|
||||
IsDefault: in.IsDefault,
|
||||
}
|
||||
err := s.db.Transaction(func(tx *gorm.DB) error {
|
||||
var merchant model.Merchant
|
||||
if err := tx.Where("id = ? AND status = ?", merchantID, model.MerchantStatusActive).First(&merchant).Error; err != nil {
|
||||
return errors.New("商户不存在或已禁用")
|
||||
}
|
||||
var user model.User
|
||||
if err := tx.Where("id = ? AND status = ?", in.UserID, 1).First(&user).Error; err != nil {
|
||||
return errors.New("用户不存在或已禁用")
|
||||
}
|
||||
if err := tx.Clauses(clause.OnConflict{
|
||||
Columns: []clause.Column{{Name: "merchant_id"}, {Name: "user_id"}},
|
||||
DoUpdates: clause.Assignments(map[string]interface{}{
|
||||
"role": in.Role,
|
||||
"status": 1,
|
||||
"is_default": in.IsDefault,
|
||||
}),
|
||||
}).Create(member).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return writeAudit(tx, &merchantID, &actorUserID, nil, "merchant.member.upsert", "merchant_member", fmt.Sprintf("%d:%d", merchantID, in.UserID), map[string]string{"role": in.Role})
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := s.db.Where("merchant_id = ? AND user_id = ?", merchantID, in.UserID).First(member).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return member, nil
|
||||
}
|
||||
|
||||
func (s *MerchantService) ListMembers(merchantID uint) ([]model.MerchantMember, error) {
|
||||
var members []model.MerchantMember
|
||||
err := s.db.Preload("User").Where("merchant_id = ?", merchantID).Order("id ASC").Find(&members).Error
|
||||
return members, err
|
||||
}
|
||||
|
||||
type CreateMerchantProductInput struct {
|
||||
ProductCode string
|
||||
ProductName string
|
||||
Category string
|
||||
Description string
|
||||
Attributes string
|
||||
SKU string
|
||||
DisplayName string
|
||||
PriceAmount int64
|
||||
CostAmount int64
|
||||
Currency string
|
||||
Stock int64
|
||||
Status string
|
||||
FulfillmentConfig string
|
||||
}
|
||||
|
||||
func (s *MerchantService) CreateMerchantProduct(merchantID uint, in CreateMerchantProductInput, actorUserID uint) (*model.MerchantProduct, error) {
|
||||
in.SKU = strings.TrimSpace(in.SKU)
|
||||
in.ProductCode = strings.TrimSpace(in.ProductCode)
|
||||
in.ProductName = strings.TrimSpace(in.ProductName)
|
||||
if in.SKU == "" {
|
||||
return nil, errors.New("商户商品 SKU 不能为空")
|
||||
}
|
||||
if in.PriceAmount < 0 || in.CostAmount < 0 {
|
||||
return nil, errors.New("商品金额不能小于零")
|
||||
}
|
||||
if in.Stock < -1 {
|
||||
return nil, errors.New("库存只能为 -1 或非负整数")
|
||||
}
|
||||
if in.Currency == "" {
|
||||
in.Currency = "POINT"
|
||||
}
|
||||
in.Currency = strings.ToUpper(in.Currency)
|
||||
if in.Status == "" {
|
||||
in.Status = model.ProductStatusActive
|
||||
}
|
||||
if in.Status != model.ProductStatusActive && in.Status != model.ProductStatusInactive {
|
||||
return nil, errors.New("无效的商品状态")
|
||||
}
|
||||
|
||||
merchantProduct := &model.MerchantProduct{}
|
||||
err := s.db.Transaction(func(tx *gorm.DB) error {
|
||||
var merchant model.Merchant
|
||||
if err := tx.Where("id = ? AND status = ?", merchantID, model.MerchantStatusActive).First(&merchant).Error; err != nil {
|
||||
return errors.New("商户不存在或已禁用")
|
||||
}
|
||||
product, err := ensureProduct(tx, in)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
merchantProduct = &model.MerchantProduct{
|
||||
MerchantID: merchantID,
|
||||
ProductID: product.ID,
|
||||
SKU: in.SKU,
|
||||
DisplayName: fallbackName(in.DisplayName, product.Name),
|
||||
PriceAmount: in.PriceAmount,
|
||||
CostAmount: in.CostAmount,
|
||||
Currency: in.Currency,
|
||||
Stock: in.Stock,
|
||||
Status: in.Status,
|
||||
FulfillmentConfig: in.FulfillmentConfig,
|
||||
}
|
||||
if err := tx.Create(merchantProduct).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return writeAudit(tx, &merchantID, &actorUserID, nil, "merchant_product.create", "merchant_product", fmt.Sprint(merchantProduct.ID), map[string]string{"sku": in.SKU})
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return merchantProduct, nil
|
||||
}
|
||||
|
||||
func (s *MerchantService) ListMerchantProducts(merchantID uint, page, size int, activeOnly bool) ([]model.MerchantProduct, int64, error) {
|
||||
page, size = normalizePage(page, size)
|
||||
tx := s.db.Model(&model.MerchantProduct{}).Where("merchant_id = ?", merchantID)
|
||||
if activeOnly {
|
||||
tx = tx.Where("status = ?", model.ProductStatusActive)
|
||||
}
|
||||
var total int64
|
||||
if err := tx.Count(&total).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
var products []model.MerchantProduct
|
||||
err := tx.Preload("Product").Order("id DESC").Offset((page - 1) * size).Limit(size).Find(&products).Error
|
||||
return products, total, err
|
||||
}
|
||||
|
||||
type UpdateMerchantProductInput struct {
|
||||
DisplayName *string
|
||||
PriceAmount *int64
|
||||
CostAmount *int64
|
||||
Stock *int64
|
||||
Status *string
|
||||
FulfillmentConfig *string
|
||||
}
|
||||
|
||||
func (s *MerchantService) UpdateMerchantProduct(merchantID, id uint, in UpdateMerchantProductInput, actorUserID uint) error {
|
||||
updates := make(map[string]interface{})
|
||||
if in.DisplayName != nil {
|
||||
updates["display_name"] = *in.DisplayName
|
||||
}
|
||||
if in.PriceAmount != nil {
|
||||
if *in.PriceAmount < 0 {
|
||||
return errors.New("商品售价不能小于零")
|
||||
}
|
||||
updates["price_amount"] = *in.PriceAmount
|
||||
}
|
||||
if in.CostAmount != nil {
|
||||
if *in.CostAmount < 0 {
|
||||
return errors.New("商品成本不能小于零")
|
||||
}
|
||||
updates["cost_amount"] = *in.CostAmount
|
||||
}
|
||||
if in.Stock != nil {
|
||||
if *in.Stock < -1 {
|
||||
return errors.New("库存只能为 -1 或非负整数")
|
||||
}
|
||||
updates["stock"] = *in.Stock
|
||||
}
|
||||
if in.Status != nil {
|
||||
if *in.Status != model.ProductStatusActive && *in.Status != model.ProductStatusInactive {
|
||||
return errors.New("无效的商品状态")
|
||||
}
|
||||
updates["status"] = *in.Status
|
||||
}
|
||||
if in.FulfillmentConfig != nil {
|
||||
updates["fulfillment_config"] = *in.FulfillmentConfig
|
||||
}
|
||||
if len(updates) == 0 {
|
||||
return errors.New("没有可更新字段")
|
||||
}
|
||||
return s.db.Transaction(func(tx *gorm.DB) error {
|
||||
result := tx.Model(&model.MerchantProduct{}).Where("id = ? AND merchant_id = ?", id, merchantID).Updates(updates)
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
return errors.New("商户商品不存在")
|
||||
}
|
||||
return writeAudit(tx, &merchantID, &actorUserID, nil, "merchant_product.update", "merchant_product", fmt.Sprint(id), nil)
|
||||
})
|
||||
}
|
||||
|
||||
type APICredential struct {
|
||||
Client *model.APIClient `json:"client"`
|
||||
Secret string `json:"secret"`
|
||||
}
|
||||
|
||||
type CreateAPIClientInput struct {
|
||||
Name string
|
||||
Scopes string
|
||||
SignatureVersion string
|
||||
ExpiresAt *time.Time
|
||||
}
|
||||
|
||||
// MaxAPIClientsPerMerchant 每个商户最多可创建的 API 密钥数量,防止密钥滥用。
|
||||
const MaxAPIClientsPerMerchant = 5
|
||||
|
||||
func (s *MerchantService) CreateAPIClient(merchantID uint, in CreateAPIClientInput, actorUserID uint) (*APICredential, error) {
|
||||
in.Name = strings.TrimSpace(in.Name)
|
||||
if in.Name == "" {
|
||||
return nil, errors.New("API 客户端名称不能为空")
|
||||
}
|
||||
if len(ParseScopes(in.Scopes)) == 0 {
|
||||
return nil, errors.New("至少配置一个 API 权限")
|
||||
}
|
||||
if in.SignatureVersion == "" {
|
||||
in.SignatureVersion = "v1"
|
||||
}
|
||||
if in.SignatureVersion != "v1" {
|
||||
return nil, errors.New("无效的签名版本")
|
||||
}
|
||||
appKey, err := randomToken("ak_", 24)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
secret, err := randomToken("sk_", 32)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ciphertext, err := s.codec.Encrypt(secret)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
client := &model.APIClient{
|
||||
MerchantID: merchantID,
|
||||
Name: in.Name,
|
||||
AppKey: appKey,
|
||||
SecretCiphertext: ciphertext,
|
||||
SignatureVersion: in.SignatureVersion,
|
||||
Scopes: strings.Join(scopeList(in.Scopes), ","),
|
||||
Status: model.APIClientStatusActive,
|
||||
ExpiresAt: in.ExpiresAt,
|
||||
}
|
||||
err = s.db.Transaction(func(tx *gorm.DB) error {
|
||||
var merchant model.Merchant
|
||||
if err := tx.Where("id = ? AND status = ?", merchantID, model.MerchantStatusActive).First(&merchant).Error; err != nil {
|
||||
return errors.New("商户不存在或已禁用")
|
||||
}
|
||||
var clientCount int64
|
||||
if err := tx.Model(&model.APIClient{}).
|
||||
Where("merchant_id = ? AND status = ?", merchantID, model.APIClientStatusActive).
|
||||
Count(&clientCount).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if clientCount >= MaxAPIClientsPerMerchant {
|
||||
return fmt.Errorf("每个商户最多可创建 %d 个 API 密钥", MaxAPIClientsPerMerchant)
|
||||
}
|
||||
if err := tx.Create(client).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return writeAudit(tx, &merchantID, &actorUserID, nil, "api_client.create", "api_client", fmt.Sprint(client.ID), map[string]string{"name": in.Name})
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &APICredential{Client: client, Secret: secret}, nil
|
||||
}
|
||||
|
||||
func (s *MerchantService) ListAPIClients(merchantID uint) ([]model.APIClient, error) {
|
||||
var clients []model.APIClient
|
||||
err := s.db.Where("merchant_id = ?", merchantID).Order("id DESC").Find(&clients).Error
|
||||
return clients, err
|
||||
}
|
||||
|
||||
func (s *MerchantService) UpdateAPIClientStatus(merchantID, id uint, status string, actorUserID uint) error {
|
||||
if status != model.APIClientStatusActive && status != model.APIClientStatusDisabled {
|
||||
return errors.New("无效的 API 客户端状态")
|
||||
}
|
||||
return s.db.Transaction(func(tx *gorm.DB) error {
|
||||
result := tx.Model(&model.APIClient{}).Where("id = ? AND merchant_id = ?", id, merchantID).Update("status", status)
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
return errors.New("API 客户端不存在")
|
||||
}
|
||||
return writeAudit(tx, &merchantID, &actorUserID, nil, "api_client.status.update", "api_client", fmt.Sprint(id), map[string]string{"status": status})
|
||||
})
|
||||
}
|
||||
|
||||
// DeleteAPIClient 删除商户的 API 密钥(物理删除,不可恢复)。
|
||||
// 删除前要求先停用,避免在用的密钥被误删。
|
||||
func (s *MerchantService) DeleteAPIClient(merchantID, id uint, actorUserID uint) error {
|
||||
return s.db.Transaction(func(tx *gorm.DB) error {
|
||||
var client model.APIClient
|
||||
if err := tx.Where("id = ? AND merchant_id = ?", id, merchantID).First(&client).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return errors.New("API 客户端不存在")
|
||||
}
|
||||
return err
|
||||
}
|
||||
if client.Status == model.APIClientStatusActive {
|
||||
return errors.New("请先停用该密钥再删除")
|
||||
}
|
||||
if err := tx.Delete(&client).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return writeAudit(tx, &merchantID, &actorUserID, nil, "api_client.delete", "api_client", fmt.Sprint(id), map[string]string{"name": client.Name, "app_key": client.AppKey})
|
||||
})
|
||||
}
|
||||
|
||||
// ProductCatalogItem 是商品目录(自营商户可售商品)的展示项,供平台管理员分配商品时勾选。
|
||||
type ProductCatalogItem struct {
|
||||
ID uint `json:"id"`
|
||||
ProductID uint `json:"product_id"`
|
||||
SKU string `json:"sku"`
|
||||
DisplayName string `json:"display_name"`
|
||||
Category string `json:"category"`
|
||||
PriceAmount int64 `json:"price_amount"`
|
||||
CostAmount int64 `json:"cost_amount"`
|
||||
Currency string `json:"currency"`
|
||||
Stock int64 `json:"stock"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
// ListProductCatalog 返回自营商户的全部可售商品,作为平台默认商品目录供分配。
|
||||
func (s *MerchantService) ListProductCatalog() ([]ProductCatalogItem, error) {
|
||||
var selfMerchant model.Merchant
|
||||
if err := s.db.Where("code = ?", model.MerchantCodeSelfOperated).First(&selfMerchant).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, errors.New("平台商品目录尚未初始化")
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
var products []model.MerchantProduct
|
||||
if err := s.db.Preload("Product").Where("merchant_id = ?", selfMerchant.ID).Order("id ASC").Find(&products).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items := make([]ProductCatalogItem, 0, len(products))
|
||||
for _, p := range products {
|
||||
category := ""
|
||||
if p.Product != nil {
|
||||
category = p.Product.Category
|
||||
}
|
||||
items = append(items, ProductCatalogItem{
|
||||
ID: p.ID,
|
||||
ProductID: p.ProductID,
|
||||
SKU: p.SKU,
|
||||
DisplayName: p.DisplayName,
|
||||
Category: category,
|
||||
PriceAmount: p.PriceAmount,
|
||||
CostAmount: p.CostAmount,
|
||||
Currency: p.Currency,
|
||||
Stock: p.Stock,
|
||||
Status: p.Status,
|
||||
})
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
// ListMerchantProductsByAdmin 供平台管理员查看指定商户的可售商品(不限功能开关)。
|
||||
func (s *MerchantService) ListMerchantProductsByAdmin(merchantID uint) ([]ProductCatalogItem, error) {
|
||||
var products []model.MerchantProduct
|
||||
if err := s.db.Preload("Product").Where("merchant_id = ?", merchantID).Order("id ASC").Find(&products).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items := make([]ProductCatalogItem, 0, len(products))
|
||||
for _, p := range products {
|
||||
category := ""
|
||||
if p.Product != nil {
|
||||
category = p.Product.Category
|
||||
}
|
||||
items = append(items, ProductCatalogItem{
|
||||
ID: p.ID,
|
||||
ProductID: p.ProductID,
|
||||
SKU: p.SKU,
|
||||
DisplayName: p.DisplayName,
|
||||
Category: category,
|
||||
PriceAmount: p.PriceAmount,
|
||||
CostAmount: p.CostAmount,
|
||||
Currency: p.Currency,
|
||||
Stock: p.Stock,
|
||||
Status: p.Status,
|
||||
})
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
// AssignProductsInput 批量分配商品给商户的入参。
|
||||
type AssignProductsInput struct {
|
||||
// CatalogIDs 为自营商户商品目录 ID 列表;为空表示清空该商户全部商品。
|
||||
CatalogIDs []uint
|
||||
}
|
||||
|
||||
// AssignProducts 按自营商户商品目录 ID 批量同步商户的可售商品:
|
||||
// 目录中勾选的商品会被复制(已存在则跳过),未勾选的已有商品会被移除。
|
||||
func (s *MerchantService) AssignProducts(merchantID uint, in AssignProductsInput, actorUserID uint) (int, error) {
|
||||
assigned := 0
|
||||
err := s.db.Transaction(func(tx *gorm.DB) error {
|
||||
var merchant model.Merchant
|
||||
if err := tx.Where("id = ?", merchantID).First(&merchant).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return errors.New("商户不存在")
|
||||
}
|
||||
return err
|
||||
}
|
||||
if merchant.Code == model.MerchantCodeSelfOperated {
|
||||
return errors.New("自营商户的商品目录由平台维护,不可分配")
|
||||
}
|
||||
var selfMerchant model.Merchant
|
||||
if err := tx.Where("code = ?", model.MerchantCodeSelfOperated).First(&selfMerchant).Error; err != nil {
|
||||
return errors.New("平台商品目录尚未初始化")
|
||||
}
|
||||
|
||||
// 读取目录全量,构造 id -> 模板 的映射
|
||||
var templates []model.MerchantProduct
|
||||
if err := tx.Where("merchant_id = ?", selfMerchant.ID).Find(&templates).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
tmplByID := make(map[uint]model.MerchantProduct, len(templates))
|
||||
for _, t := range templates {
|
||||
tmplByID[t.ID] = t
|
||||
}
|
||||
|
||||
// 读取商户已有商品,构造 sku -> 已有 的映射
|
||||
var existing []model.MerchantProduct
|
||||
if err := tx.Where("merchant_id = ?", merchantID).Find(&existing).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
existBySKU := make(map[string]model.MerchantProduct, len(existing))
|
||||
for _, e := range existing {
|
||||
existBySKU[e.SKU] = e
|
||||
}
|
||||
|
||||
// 计算需要新增的 SKU 集合
|
||||
wantSKUs := make(map[string]bool, len(in.CatalogIDs))
|
||||
toCreate := make([]model.MerchantProduct, 0, len(in.CatalogIDs))
|
||||
for _, id := range in.CatalogIDs {
|
||||
t, ok := tmplByID[id]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
wantSKUs[t.SKU] = true
|
||||
if _, has := existBySKU[t.SKU]; !has {
|
||||
toCreate = append(toCreate, 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,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 移除未勾选的已有商品
|
||||
var removeIDs []uint
|
||||
for _, e := range existing {
|
||||
if !wantSKUs[e.SKU] {
|
||||
removeIDs = append(removeIDs, e.ID)
|
||||
}
|
||||
}
|
||||
if len(removeIDs) > 0 {
|
||||
if err := tx.Where("merchant_id = ? AND id IN ?", merchantID, removeIDs).Delete(&model.MerchantProduct{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// 新增勾选但尚未拥有的商品
|
||||
if len(toCreate) > 0 {
|
||||
if err := tx.Clauses(clause.OnConflict{DoNothing: true}).Create(&toCreate).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
assigned = len(wantSKUs)
|
||||
return writeAudit(tx, &merchantID, &actorUserID, nil, "merchant.products.assign", "merchant", fmt.Sprint(merchantID), map[string]string{"assigned": fmt.Sprint(assigned)})
|
||||
})
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return assigned, nil
|
||||
}
|
||||
|
||||
// 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
|
||||
err := tx.Where("code = ?", in.ProductCode).First(&product).Error
|
||||
if err == nil {
|
||||
return &product, nil
|
||||
}
|
||||
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if in.ProductName == "" {
|
||||
return nil, errors.New("新建平台商品时商品名称不能为空")
|
||||
}
|
||||
code := in.ProductCode
|
||||
if code == "" {
|
||||
token, err := randomToken("prd_", 12)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
code = token
|
||||
}
|
||||
product := &model.Product{
|
||||
Code: code,
|
||||
Name: in.ProductName,
|
||||
Category: in.Category,
|
||||
Description: in.Description,
|
||||
Attributes: in.Attributes,
|
||||
Status: model.ProductStatusActive,
|
||||
}
|
||||
if err := tx.Create(product).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return product, nil
|
||||
}
|
||||
|
||||
func normalizePage(page, size int) (int, int) {
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
if size < 1 || size > 100 {
|
||||
size = 20
|
||||
}
|
||||
return page, size
|
||||
}
|
||||
|
||||
func randomToken(prefix string, byteCount int) (string, error) {
|
||||
raw := make([]byte, byteCount)
|
||||
if _, err := rand.Read(raw); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return prefix + base64.RawURLEncoding.EncodeToString(raw), nil
|
||||
}
|
||||
|
||||
func scopeList(scopes string) []string {
|
||||
set := ParseScopes(scopes)
|
||||
items := make([]string, 0, len(set))
|
||||
for scope := range set {
|
||||
items = append(items, scope)
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
func fallbackName(value, fallback string) string {
|
||||
if strings.TrimSpace(value) != "" {
|
||||
return strings.TrimSpace(value)
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user