255 lines
8.1 KiB
Go
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,
|
|
}
|
|
}
|