Files
2026-09-14 01:16:47 +08:00

255 lines
8.1 KiB
Go

package service
import (
"context"
"strings"
"git.lzcwy.cn/zhengchiliu/zt-closet-manage-backend/global"
"git.lzcwy.cn/zhengchiliu/zt-closet-manage-backend/internal/dto"
"git.lzcwy.cn/zhengchiliu/zt-closet-manage-backend/internal/mysqldao"
"git.lzcwy.cn/zhengchiliu/zt-closet-manage-backend/internal/mysqlmodel"
"git.lzcwy.cn/zhengchiliu/zt-closet-manage-backend/pkg/errcode"
)
// GoodsService 商品管理服务。
type GoodsService struct {
goodsDao *mysqldao.GoodsDao
}
// NewGoodsService 创建商品服务。
func NewGoodsService() *GoodsService {
return &GoodsService{goodsDao: mysqldao.NewGoodsDao()}
}
// List 分页查询商品(管理端含下架商品)。
func (s *GoodsService) List(ctx context.Context, request *dto.GoodsListRequest) (*dto.GoodsListResponse, *errcode.Error) {
ctx, span := global.Tracer.Start(ctx, "svc.AdminGoodsList")
defer span.End()
page, pageSize := normalizePage(request.Page, request.PageSize)
list, total, err := s.goodsDao.GoodsList(ctx, mysqldao.GoodsListParams{
GoodsType: request.GoodsType,
Status: request.Status,
Keyword: strings.TrimSpace(request.Keyword),
Page: page,
Size: pageSize,
})
if err != nil {
return nil, errcode.DatabaseError
}
items := make([]dto.GoodsItem, 0, len(list))
for i := range list {
items = append(items, buildGoodsItem(&list[i]))
}
return &dto.GoodsListResponse{
PageResponse: dto.PageResponse{Total: total, Page: page, PageSize: pageSize},
List: items,
}, nil
}
// Create 创建商品。
func (s *GoodsService) Create(ctx context.Context, adminID int64, request *dto.GoodsCreateRequest) (*dto.GoodsItem, *errcode.Error) {
ctx, span := global.Tracer.Start(ctx, "svc.AdminGoodsCreate")
defer span.End()
code := strings.TrimSpace(request.GoodsCode)
if code == "" {
return nil, errcode.InvalidParams.WithDetails("商品编码不能为空")
}
if request.DiscountPrice > request.Price {
return nil, errcode.GoodsPriceInvalid.WithDetails("售价不能高于原价")
}
// 编码唯一性校验(含软删,防历史编码占用)。
if _, err := s.goodsDao.GoodsGetByCode(ctx, code); err == nil {
return nil, errcode.GoodsCodeExists
} else if !isNotFound(err) {
return nil, errcode.DatabaseError
}
// 虚拟支付道具登记校验:已发布必须有道具 ID;道具 ID 全局唯一(防两个商品指向同一微信道具导致发货金额校验失败)。
productID := strings.TrimSpace(request.ProductID)
if request.ProductStatus == GoodsProductPublished && productID == "" {
return nil, errcode.GoodsProductInvalid.WithDetails("发布状态为「已发布」时必须填写道具 ID")
}
if productID != "" {
if _, err := s.goodsDao.GoodsGetByProductID(ctx, productID); err == nil {
return nil, errcode.GoodsProductIDExists
} else if !isNotFound(err) {
return nil, errcode.DatabaseError
}
}
status := request.Status
if status == 0 {
status = GoodsStatusOffShelf // 新建默认下架,运营确认后再上架。
}
goods := &mysqlmodel.Goods{
GoodsCode: code,
GoodsName: strings.TrimSpace(request.GoodsName),
GoodsType: request.GoodsType,
Price: request.Price,
DiscountPrice: request.DiscountPrice,
MemberDays: request.MemberDays,
CosQuota: request.UploadQuota,
AIRecommendQuota: request.AIRecommendQuota,
ClosetShareQuota: request.ClosetShareQuota,
OutfitShareQuota: request.OutfitShareQuota,
ValidDays: request.ValidDays,
Description: request.Description,
Status: status,
ProductID: productID,
ProductStatus: request.ProductStatus,
}
goods.CreatedBy = adminID
goods.UpdatedBy = adminID
if err := s.goodsDao.GoodsCreate(ctx, goods); err != nil {
return nil, errcode.DatabaseError
}
item := buildGoodsItem(goods)
return &item, nil
}
// Update 更新商品(传什么改什么;不修改编码与类型)。
func (s *GoodsService) Update(ctx context.Context, adminID int64, id int64, request *dto.GoodsUpdateRequest) *errcode.Error {
ctx, span := global.Tracer.Start(ctx, "svc.AdminGoodsUpdate")
defer span.End()
existing, err := s.goodsDao.GoodsGetByID(ctx, id)
if err != nil {
if isNotFound(err) {
return errcode.GoodsNotFound
}
return errcode.DatabaseError
}
// 虚拟支付道具登记:以「现有值 + 本次改动」的最终态校验,防止只改其中一个字段绕过校验。
productID := existing.ProductID
if request.ProductID != nil {
productID = strings.TrimSpace(*request.ProductID)
}
productStatus := existing.ProductStatus
if request.ProductStatus != nil {
productStatus = *request.ProductStatus
}
if productStatus == GoodsProductPublished && productID == "" {
return errcode.GoodsProductInvalid.WithDetails("发布状态为「已发布」时必须填写道具 ID")
}
if request.ProductID != nil && productID != "" {
if other, derr := s.goodsDao.GoodsGetByProductID(ctx, productID); derr == nil {
if other.ID != id {
return errcode.GoodsProductIDExists
}
} else if !isNotFound(derr) {
return errcode.DatabaseError
}
}
updates := map[string]any{"updated_by": adminID}
if request.GoodsName != nil {
updates["goods_name"] = strings.TrimSpace(*request.GoodsName)
}
if request.Price != nil {
updates["price"] = *request.Price
}
if request.DiscountPrice != nil {
updates["discount_price"] = *request.DiscountPrice
}
if request.MemberDays != nil {
updates["member_days"] = *request.MemberDays
}
if request.UploadQuota != nil {
updates["cos_quota"] = *request.UploadQuota
}
if request.AIRecommendQuota != nil {
updates["ai_recommend_quota"] = *request.AIRecommendQuota
}
if request.ClosetShareQuota != nil {
updates["closet_share_quota"] = *request.ClosetShareQuota
}
if request.OutfitShareQuota != nil {
updates["outfit_share_quota"] = *request.OutfitShareQuota
}
if request.ValidDays != nil {
updates["valid_days"] = *request.ValidDays
}
if request.Description != nil {
updates["description"] = *request.Description
}
if request.ProductID != nil {
updates["product_id"] = productID
}
if request.ProductStatus != nil {
updates["product_status"] = *request.ProductStatus
}
if len(updates) == 1 {
return nil // 无可更新字段。
}
if err := s.goodsDao.GoodsUpdate(ctx, id, updates); err != nil {
return errcode.DatabaseError
}
return nil
}
// UpdateStatus 商品上下架。
func (s *GoodsService) UpdateStatus(ctx context.Context, adminID int64, id int64, request *dto.GoodsStatusRequest) *errcode.Error {
ctx, span := global.Tracer.Start(ctx, "svc.AdminGoodsUpdateStatus")
defer span.End()
if _, err := s.goodsDao.GoodsGetByID(ctx, id); err != nil {
if isNotFound(err) {
return errcode.GoodsNotFound
}
return errcode.DatabaseError
}
if err := s.goodsDao.GoodsUpdate(ctx, id, map[string]any{"status": request.Status, "updated_by": adminID}); err != nil {
return errcode.DatabaseError
}
return nil
}
// Delete 软删除商品(历史订单已快照商品信息,删除不影响历史订单)。
func (s *GoodsService) Delete(ctx context.Context, adminID int64, id int64) *errcode.Error {
ctx, span := global.Tracer.Start(ctx, "svc.AdminGoodsDelete")
defer span.End()
if _, err := s.goodsDao.GoodsGetByID(ctx, id); err != nil {
if isNotFound(err) {
return errcode.GoodsNotFound
}
return errcode.DatabaseError
}
if err := s.goodsDao.GoodsSoftDelete(ctx, id); err != nil {
return errcode.DatabaseError
}
return nil
}
// buildGoodsItem 商品模型 → 响应(注意 dto.UploadQuota 对应 model.CosQuota)。
func buildGoodsItem(g *mysqlmodel.Goods) dto.GoodsItem {
return dto.GoodsItem{
ID: g.ID,
GoodsCode: g.GoodsCode,
GoodsName: g.GoodsName,
GoodsType: g.GoodsType,
Price: g.Price,
DiscountPrice: g.DiscountPrice,
MemberDays: g.MemberDays,
UploadQuota: g.CosQuota,
AIRecommendQuota: g.AIRecommendQuota,
ClosetShareQuota: g.ClosetShareQuota,
OutfitShareQuota: g.OutfitShareQuota,
ValidDays: g.ValidDays,
Description: g.Description,
Status: g.Status,
ProductID: g.ProductID,
ProductStatus: g.ProductStatus,
CreatedAt: g.CreatedAt,
UpdatedAt: g.UpdatedAt,
}
}