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/cos" "git.lzcwy.cn/zhengchiliu/zt-closet-manage-backend/pkg/errcode" ) // ContentService 内容审核服务(穿搭/衣物/衣柜分享)。 type ContentService struct { outfitDao *mysqldao.OutfitDao clothesDao *mysqldao.ClothesDao shareDao *mysqldao.ClosetShareDao } // NewContentService 创建内容审核服务。 func NewContentService() *ContentService { return &ContentService{ outfitDao: mysqldao.NewOutfitDao(), clothesDao: mysqldao.NewClothesDao(), shareDao: mysqldao.NewClosetShareDao(), } } // OutfitList 分页查询穿搭(管理端全量视角)。 func (s *ContentService) OutfitList(ctx context.Context, request *dto.OutfitListRequest) (*dto.OutfitListResponse, *errcode.Error) { ctx, span := global.Tracer.Start(ctx, "svc.AdminOutfitList") defer span.End() page, pageSize := normalizePage(request.Page, request.PageSize) list, total, err := s.outfitDao.OutfitList(ctx, mysqldao.OutfitListParams{ UserID: request.UserID, IsPublic: request.IsPublic, AuditStatus: request.AuditStatus, Status: request.Status, Keyword: strings.TrimSpace(request.Keyword), Page: page, Size: pageSize, }) if err != nil { return nil, errcode.DatabaseError } items := make([]dto.OutfitListItem, 0, len(list)) for i := range list { items = append(items, buildOutfitListItem(&list[i])) } return &dto.OutfitListResponse{ PageResponse: dto.PageResponse{Total: total, Page: page, PageSize: pageSize}, List: items, }, nil } // OutfitDetail 穿搭详情(含衣物明细)。 func (s *ContentService) OutfitDetail(ctx context.Context, outfitUUID string) (*dto.OutfitDetailResponse, *errcode.Error) { ctx, span := global.Tracer.Start(ctx, "svc.AdminOutfitDetail") defer span.End() outfit, err := s.outfitDao.OutfitGetByUUID(ctx, outfitUUID) if err != nil { if isNotFound(err) { return nil, errcode.OutfitNotFound } return nil, errcode.DatabaseError } items, err := s.outfitDao.OutfitItemListByOutfitID(ctx, outfit.ID) if err != nil { return nil, errcode.DatabaseError } clothesIDs := make([]int64, 0, len(items)) for _, it := range items { clothesIDs = append(clothesIDs, it.ClothesID) } clothesList, err := s.clothesDao.ClothesListByIDs(ctx, clothesIDs) if err != nil { return nil, errcode.DatabaseError } // 衣物可能被软删,按 ID 索引过滤。 clothesMap := make(map[int64]mysqlmodel.Clothes, len(clothesList)) for _, cl := range clothesList { clothesMap[cl.ID] = cl } detailItems := make([]dto.OutfitClothesItem, 0, len(items)) for _, it := range items { cl, ok := clothesMap[it.ClothesID] if !ok { continue } detailItems = append(detailItems, dto.OutfitClothesItem{ ClothesUUID: cl.ClothesUUID, Name: cl.Name, CategoryMain: cl.CategoryMain, CategorySub: cl.CategorySub, ProcessedImage: cos.GetCosUrl(cl.ProcessedImage), ImageProcessStatus: cl.ImageProcessStatus, }) } return &dto.OutfitDetailResponse{ OutfitListItem: buildOutfitListItem(outfit), Items: detailItems, }, nil } // OutfitAudit 穿搭审核:通过(audit 1→2)/驳回(audit 1→3),写审核日志。 func (s *ContentService) OutfitAudit(ctx context.Context, adminID int64, outfitUUID string, request *dto.OutfitAuditRequest) *errcode.Error { ctx, span := global.Tracer.Start(ctx, "svc.AdminOutfitAudit") defer span.End() outfit, err := s.outfitDao.OutfitGetByUUID(ctx, outfitUUID) if err != nil { if isNotFound(err) { return errcode.OutfitNotFound } return errcode.DatabaseError } if outfit.AuditStatus != OutfitAuditPending { return errcode.OutfitAuditInvalid.WithDetails("仅待审核状态的穿搭可审核") } auditResult := AuditResultPass reason := "" if request.Result == AuditResultReject { auditResult = AuditResultReject reason = strings.TrimSpace(request.Reason) if reason == "" { reason = "未通过审核" } } log := &mysqlmodel.OutfitAuditLog{ AuditTime: nowUnix(), } log.CreatedBy = adminID if err := s.outfitDao.OutfitAuditWithLog(ctx, outfit.ID, auditResult, reason, log); err != nil { return errcode.DatabaseError } return nil } // OutfitBan 封禁穿搭(status → 2 封禁)。 func (s *ContentService) OutfitBan(ctx context.Context, adminID int64, outfitUUID string, request *dto.OutfitBanRequest) *errcode.Error { ctx, span := global.Tracer.Start(ctx, "svc.AdminOutfitBan") defer span.End() outfit, err := s.outfitDao.OutfitGetByUUID(ctx, outfitUUID) if err != nil { if isNotFound(err) { return errcode.OutfitNotFound } return errcode.DatabaseError } if outfit.Status == OutfitStatusBanned { return nil // 幂等。 } if err := s.outfitDao.OutfitUpdateByID(ctx, outfit.ID, map[string]any{ "status": OutfitStatusBanned, "updated_by": adminID, }); err != nil { return errcode.DatabaseError } return nil } // OutfitUnban 解封穿搭(status → 1 正常)。 func (s *ContentService) OutfitUnban(ctx context.Context, adminID int64, outfitUUID string) *errcode.Error { ctx, span := global.Tracer.Start(ctx, "svc.AdminOutfitUnban") defer span.End() outfit, err := s.outfitDao.OutfitGetByUUID(ctx, outfitUUID) if err != nil { if isNotFound(err) { return errcode.OutfitNotFound } return errcode.DatabaseError } if outfit.Status != OutfitStatusBanned { return nil // 幂等。 } if err := s.outfitDao.OutfitUpdateByID(ctx, outfit.ID, map[string]any{ "status": OutfitStatusNormal, "updated_by": adminID, }); err != nil { return errcode.DatabaseError } return nil } // ClothesList 分页查询衣物。 func (s *ContentService) ClothesList(ctx context.Context, request *dto.ClothesListRequest) (*dto.ClothesListResponse, *errcode.Error) { ctx, span := global.Tracer.Start(ctx, "svc.AdminClothesList") defer span.End() page, pageSize := normalizePage(request.Page, request.PageSize) list, total, err := s.clothesDao.ClothesList(ctx, mysqldao.ClothesListParams{ UserID: request.UserID, ImageProcessStatus: request.ImageProcessStatus, CategoryMain: strings.TrimSpace(request.CategoryMain), Keyword: strings.TrimSpace(request.Keyword), Page: page, Size: pageSize, }) if err != nil { return nil, errcode.DatabaseError } items := make([]dto.ClothesItem, 0, len(list)) for i := range list { items = append(items, buildClothesItem(&list[i])) } return &dto.ClothesListResponse{ PageResponse: dto.PageResponse{Total: total, Page: page, PageSize: pageSize}, List: items, }, nil } // ClothesDetail 衣物详情。 func (s *ContentService) ClothesDetail(ctx context.Context, clothesUUID string) (*dto.ClothesItem, *errcode.Error) { ctx, span := global.Tracer.Start(ctx, "svc.AdminClothesDetail") defer span.End() clothes, err := s.clothesDao.ClothesGetByUUID(ctx, clothesUUID) if err != nil { if isNotFound(err) { return nil, errcode.ClothesNotFound } return nil, errcode.DatabaseError } item := buildClothesItem(clothes) return &item, nil } // ShareList 分页查询衣柜分享。 func (s *ContentService) ShareList(ctx context.Context, request *dto.ClosetShareListRequest) (*dto.ClosetShareListResponse, *errcode.Error) { ctx, span := global.Tracer.Start(ctx, "svc.AdminShareList") defer span.End() page, pageSize := normalizePage(request.Page, request.PageSize) list, total, err := s.shareDao.ClosetShareList(ctx, mysqldao.ClosetShareListParams{ UserID: request.UserID, Status: request.Status, Page: page, Size: pageSize, }) if err != nil { return nil, errcode.DatabaseError } items := make([]dto.ClosetShareItem, 0, len(list)) for i := range list { items = append(items, buildShareItem(&list[i])) } return &dto.ClosetShareListResponse{ PageResponse: dto.PageResponse{Total: total, Page: page, PageSize: pageSize}, List: items, }, nil } // ShareBan 封禁分享(status → 4 封禁)。 func (s *ContentService) ShareBan(ctx context.Context, adminID int64, shareUUID string) *errcode.Error { ctx, span := global.Tracer.Start(ctx, "svc.AdminShareBan") defer span.End() share, err := s.shareDao.ClosetShareGetByUUID(ctx, shareUUID) if err != nil { if isNotFound(err) { return errcode.ShareNotFound } return errcode.DatabaseError } if share.Status == ShareStatusBanned { return nil // 幂等。 } if err := s.shareDao.ClosetShareUpdateStatus(ctx, shareUUID, ShareStatusBanned); err != nil { return errcode.DatabaseError } return nil } // ShareUnban 解封分享(status → 1 正常)。 func (s *ContentService) ShareUnban(ctx context.Context, adminID int64, shareUUID string) *errcode.Error { ctx, span := global.Tracer.Start(ctx, "svc.AdminShareUnban") defer span.End() share, err := s.shareDao.ClosetShareGetByUUID(ctx, shareUUID) if err != nil { if isNotFound(err) { return errcode.ShareNotFound } return errcode.DatabaseError } if share.Status != ShareStatusBanned { return nil // 幂等。 } if err := s.shareDao.ClosetShareUpdateStatus(ctx, shareUUID, ShareStatusNormal); err != nil { return errcode.DatabaseError } return nil } // buildOutfitListItem 穿搭模型 → 列表项。 func buildOutfitListItem(o *mysqlmodel.Outfit) dto.OutfitListItem { return dto.OutfitListItem{ ID: o.ID, OutfitUUID: o.OutfitUUID, UserID: o.UserID, OutfitName: o.OutfitName, OutfitDesc: o.OutfitDesc, Scene: o.Scene, Source: o.Source, IsPublic: o.IsPublic, AuditStatus: o.AuditStatus, Status: o.Status, CreatedAt: o.CreatedAt, } } // buildClothesItem 衣物模型 → 响应。 func buildClothesItem(c *mysqlmodel.Clothes) dto.ClothesItem { return dto.ClothesItem{ ID: c.ID, ClothesUUID: c.ClothesUUID, UserID: c.UserID, Name: c.Name, CategoryMain: c.CategoryMain, CategorySub: c.CategorySub, Style: c.Style, ColorMain: c.ColorMain, ColorSub: c.ColorSub, Thickness: c.Thickness, Version: c.Version, Material: c.Material, Pattern: c.Pattern, SleeveType: c.SleeveType, ClothesType: c.ClothesType, NecklineType: c.NecklineType, Formality: c.Formality, Occasions: c.Occasions, VisualImpact: c.VisualImpact, Remark: c.Remark, ProcessedImage: cos.GetCosUrl(c.ProcessedImage), ImageSize: c.ImageSize, ImageProcessStatus: c.ImageProcessStatus, FailReason: c.FailReason, CreatedAt: c.CreatedAt, } } // buildShareItem 分享模型 → 响应。 func buildShareItem(s *mysqlmodel.ClosetShare) dto.ClosetShareItem { return dto.ClosetShareItem{ ID: s.ID, ShareUUID: s.ShareUUID, UserID: s.UserID, ShareType: s.ShareType, ExpireTime: s.ExpireTime, Status: s.Status, CreatedAt: s.CreatedAt, } }