商品列表不再过滤镜像 SKU;图片只按英文 SKU 解析,修正 DLC 预设文案。 Co-authored-by: Cursor <cursoragent@cursor.com>
207 lines
6.7 KiB
Go
207 lines
6.7 KiB
Go
package service
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"affiliate_dash/internal/model"
|
|
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
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
|
|
}
|
|
|
|
type ListMerchantProductsOptions struct {
|
|
ActiveOnly bool
|
|
Status string // active / inactive;空表示不限(ActiveOnly 优先)
|
|
Keyword string
|
|
}
|
|
|
|
func (s *MerchantService) ListMerchantProducts(merchantID uint, page, size int, activeOnly bool) ([]model.MerchantProduct, int64, error) {
|
|
return s.ListMerchantProductsWithOptions(merchantID, page, size, ListMerchantProductsOptions{
|
|
ActiveOnly: activeOnly,
|
|
})
|
|
}
|
|
|
|
// ListMerchantProductsForClient 商户开放 API / 手工下单选品:只返回在售英文主 sku。
|
|
func (s *MerchantService) ListMerchantProductsForClient(merchantID uint, page, size int) ([]model.MerchantProduct, int64, error) {
|
|
return s.ListMerchantProductsWithOptions(merchantID, page, size, ListMerchantProductsOptions{
|
|
ActiveOnly: true,
|
|
})
|
|
}
|
|
|
|
func (s *MerchantService) ListMerchantProductsWithOptions(merchantID uint, page, size int, opts ListMerchantProductsOptions) ([]model.MerchantProduct, int64, error) {
|
|
page, size = normalizePage(page, size)
|
|
tx := s.db.Model(&model.MerchantProduct{}).Where("merchant_id = ?", merchantID)
|
|
status := strings.TrimSpace(opts.Status)
|
|
if opts.ActiveOnly {
|
|
tx = tx.Where("status = ?", model.ProductStatusActive)
|
|
} else if status != "" {
|
|
if status != model.ProductStatusActive && status != model.ProductStatusInactive {
|
|
return nil, 0, errors.New("无效的商品状态")
|
|
}
|
|
tx = tx.Where("status = ?", status)
|
|
}
|
|
if keyword := strings.TrimSpace(opts.Keyword); keyword != "" {
|
|
like := "%" + keyword + "%"
|
|
tx = tx.Where("(sku ILIKE ? OR display_name ILIKE ?)", like, like)
|
|
}
|
|
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
|
|
}
|
|
|
|
// DeleteMerchantProduct 软删除商户商品;仅允许删除已下架商品,避免误删在售品。
|
|
func (s *MerchantService) DeleteMerchantProduct(merchantID, id uint, actorUserID uint) error {
|
|
return s.db.Transaction(func(tx *gorm.DB) error {
|
|
var item model.MerchantProduct
|
|
if err := tx.Where("id = ? AND merchant_id = ?", id, merchantID).First(&item).Error; err != nil {
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return errors.New("商品不存在")
|
|
}
|
|
return err
|
|
}
|
|
if item.Status != model.ProductStatusInactive {
|
|
return errors.New("请先下架后再删除")
|
|
}
|
|
if err := tx.Delete(&item).Error; err != nil {
|
|
return err
|
|
}
|
|
return writeAudit(tx, &merchantID, &actorUserID, nil, "merchant_product.delete", "merchant_product", fmt.Sprint(id), map[string]string{
|
|
"sku": item.SKU,
|
|
})
|
|
})
|
|
}
|
|
|
|
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)
|
|
})
|
|
}
|