Files
yml2213andCursor 6dc2375a79 chore(catalog): 清理 LEWAN_ 列表过滤与商品图 UUID 历史回退
商品列表不再过滤镜像 SKU;图片只按英文 SKU 解析,修正 DLC 预设文案。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-14 20:33:42 +08:00

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)
})
}