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