first commit
This commit is contained in:
@@ -0,0 +1,372 @@
|
||||
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,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user