Files
zt-closet-manage-backend/internal/service/content.go
T
2026-09-11 01:47:20 +08:00

373 lines
11 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/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,
}
}