package service import ( "context" "strings" "time" "fashionapi/internal/dto" "fashionapi/internal/model" "fashionapi/internal/repository" ) // FavoriteService 收藏业务接口。所有方法均要求调用方已通过中间件解析出 userID。 type FavoriteService interface { // ListPaged 分页取当前用户收藏(服务端分页,与收藏总量解耦)。 // kind 传空表示不限粒度;传 gallery / image 只取对应粒度。 ListPaged(ctx context.Context, userID uint32, page, perPage int, kind string) ([]dto.FavoriteItem, int64, error) // Add 新增收藏(幂等)。 Add(ctx context.Context, userID uint32, req dto.FavoriteAdd) (*dto.FavoriteItem, error) // Remove 删除收藏。 Remove(ctx context.Context, userID uint32, targetUID string) error // Check 返回给定 ids 中已收藏的子集(列表页打勾用)。 Check(ctx context.Context, userID uint32, ids []string) ([]string, error) } type favoriteService struct { favs repository.FavoriteRepository } // NewFavoriteService 创建收藏服务。 func NewFavoriteService(favs repository.FavoriteRepository) FavoriteService { return &favoriteService{favs: favs} } func (s *favoriteService) ListPaged(ctx context.Context, userID uint32, page, perPage int, kind string) ([]dto.FavoriteItem, int64, error) { rows, err := s.favs.ListPaged(ctx, userID, page, perPage, kind) if err != nil { return nil, 0, internalErr(err.Error()) } total, err := s.favs.CountByUser(ctx, userID, kind) if err != nil { return nil, 0, internalErr(err.Error()) } out := make([]dto.FavoriteItem, 0, len(rows)) for _, r := range rows { out = append(out, toFavoriteItem(r)) } return out, total, nil } func (s *favoriteService) Add(ctx context.Context, userID uint32, req dto.FavoriteAdd) (*dto.FavoriteItem, error) { if !validTargetType(req.TargetType) { return nil, badRequest("invalid target_type") } if req.TargetUID == "" { return nil, badRequest("target_uid required") } // 粒度:请求未显式指定时按 target_type 推断(*_image 即图片收藏)。 kind := req.Kind if kind == "" { kind = inferKind(req.TargetType) } if kind != dto.FavoriteKindGallery && kind != dto.FavoriteKindImage { return nil, badRequest("invalid kind") } // 图片收藏必须带上图片地址,否则个人中心无从渲染。 if kind == dto.FavoriteKindImage && req.ImageURL == "" { return nil, badRequest("image_url required for image favorite") } row := &model.Favorite{ UserID: userID, TargetType: req.TargetType, TargetUID: req.TargetUID, Title: req.Title, Cover: req.Cover, Brand: req.Brand, Kind: kind, ImageURL: req.ImageURL, ParentUID: req.ParentUID, CreatedAt: uint32(time.Now().Unix()), } if err := s.favs.Add(ctx, row); err != nil { return nil, internalErr(err.Error()) } item := toFavoriteItem(*row) return &item, nil } func (s *favoriteService) Remove(ctx context.Context, userID uint32, targetUID string) error { if targetUID == "" { return badRequest("target_uid required") } if err := s.favs.Remove(ctx, userID, targetUID); err != nil { return internalErr(err.Error()) } return nil } func (s *favoriteService) Check(ctx context.Context, userID uint32, ids []string) ([]string, error) { if len(ids) == 0 { return []string{}, nil } got, err := s.favs.FilterExisting(ctx, userID, ids) if err != nil { return nil, internalErr(err.Error()) } return got, nil } // validTargetType 允许的收藏目标类型:图集两种 + 单图两种。 func validTargetType(t string) bool { switch t { case "runway", "street", "runway_image", "snap_image": return true } return false } // inferKind 由 target_type 推断收藏粒度。 func inferKind(targetType string) string { if strings.HasSuffix(targetType, "_image") { return dto.FavoriteKindImage } return dto.FavoriteKindGallery } func toFavoriteItem(r model.Favorite) dto.FavoriteItem { kind := r.Kind if kind == "" { kind = dto.FavoriteKindGallery // 历史数据无 kind 列值,一律视为图集 } return dto.FavoriteItem{ ID: r.TargetUID, Type: r.TargetType, Title: r.Title, Cover: r.Cover, Brand: r.Brand, AddedAt: r.CreatedAt, Kind: kind, ImageURL: r.ImageURL, ParentUID: r.ParentUID, } }