Files
backend_v2/internal/service/brand_service.go
toom1996 74ba700598 update
2026-09-07 00:01:48 +08:00

174 lines
5.6 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package service
import (
"context"
"errors"
"net/http"
"sort"
"time"
"fashionapi/internal/dto"
"fashionapi/internal/model"
"fashionapi/internal/pkg/hashid"
"fashionapi/internal/repository"
)
// BrandService 品牌业务接口。
type BrandService interface {
List(ctx context.Context, q dto.BrandQuery) ([]dto.PublicBrand, int64, error)
// Hot 返回热门品牌(按走秀图片数排名前 limit 个)并附带各品牌档案数。
// 供 SSG 构建期(RunwayLooks 侧栏 / 品牌弹窗 HOT 标签)一次性取前 N 个热门品牌;
// 复用 List 的 featured 收窄逻辑(FeaturedIDs("images", limit)),与首页精选口径一致。
Hot(ctx context.Context, limit int, locale string) ([]dto.PublicBrand, error)
// Get 按编码 id 取单个品牌(后台编辑用)。不存在返回 ErrBrandNotFound。
Get(ctx context.Context, uid string) (*model.Brand, error)
// Create 新建品牌,返回编码后的 id。
Create(ctx context.Context, nameEn, nameCn string) (string, error)
// Update 按编码 id 更新品牌名称。
Update(ctx context.Context, uid, nameEn, nameCn string) error
// CrawlTasks 返回爬虫抓取所需的品牌列表(编码 brand_uid + 英文品牌名)。
// brandID>0 时仅返回该品牌(对应 --brand 单品牌调试)。
CrawlTasks(ctx context.Context, brandID uint32) ([]dto.CrawlBrand, error)
}
// ErrBrandNotFound 品牌不存在。
var ErrBrandNotFound = NewError(http.StatusNotFound, "brand not found")
type brandService struct {
brands repository.BrandRepository
}
// NewBrandService 创建品牌服务。
func NewBrandService(brands repository.BrandRepository) BrandService {
return &brandService{brands: brands}
}
// List 查询品牌列表。
//
// featured=1 时先按指标算出"代表品牌"集合,再把列表收敛到该集合内 ——
// 全库 1600+ 有档案品牌对 A-Z 索引来说过多,精选前 200 更符合导航场景。
func (s *brandService) List(ctx context.Context, q dto.BrandQuery) ([]dto.PublicBrand, int64, error) {
q.Normalize()
var restrictIDs []uint32
if q.Featured {
ids, err := s.brands.FeaturedIDs(ctx, q.Metric, q.Limit)
if err != nil {
// 取不到精选集合时交给 repository 收窄为空集,
// 绝不静默退化成"返回全部品牌"——那会让前端索引突然膨胀。
restrictIDs = nil
} else {
restrictIDs = ids
}
}
items, total, err := s.brands.List(ctx, q, restrictIDs)
if err != nil {
return nil, 0, internalErr(err.Error())
}
return s.toPublicBrands(items, q.Locale), total, nil
}
// Hot 返回热门品牌:按走秀图片总数排名取前 limit 个,结果按热度顺序(而非字母序)排列。
//
// 供 SSG 构建期(走秀页侧栏品牌 filter / 品牌弹窗 HOT 标签)一次性取用——
// 前端不再从全量列表里自行切片,热度排名由后端唯一口径给出。
func (s *brandService) Hot(ctx context.Context, limit int, locale string) ([]dto.PublicBrand, error) {
ids, err := s.brands.FeaturedIDs(ctx, "images", limit)
if err != nil {
return nil, internalErr(err.Error())
}
if len(ids) == 0 {
return []dto.PublicBrand{}, nil
}
items, err := s.brands.ListByIDs(ctx, ids)
if err != nil {
return nil, internalErr(err.Error())
}
// IN 查询不保证顺序,这里重排回 FeaturedIDs 的热度排名顺序。
pos := make(map[uint32]int, len(ids))
for i, id := range ids {
pos[id] = i
}
sort.Slice(items, func(i, j int) bool { return pos[items[i].ID] < pos[items[j].ID] })
return s.toPublicBrands(items, locale), nil
}
// toPublicBrands 把仓储层品牌行映射为对外精简结构(编码 id + 双语名)。
func (s *brandService) toPublicBrands(items []model.Brand, locale string) []dto.PublicBrand {
list := make([]dto.PublicBrand, 0, len(items))
for _, b := range items {
list = append(list, dto.PublicBrand{
UID: hashid.Encode(b.ID),
Name: pickLocale(b.NameEn, b.NameCn, locale),
NameEn: b.NameEn,
NameCn: b.NameCn,
})
}
return list
}
// Get 按编码 id 取单个品牌(后台编辑用)。
func (s *brandService) Get(ctx context.Context, uid string) (*model.Brand, error) {
id, err := hashid.Decode(uid)
if err != nil {
return nil, ErrBrandNotFound
}
b, err := s.brands.FindByID(ctx, id)
if err != nil {
if errors.Is(err, repository.ErrNotFound) {
return nil, ErrBrandNotFound
}
return nil, internalErr(err.Error())
}
return b, nil
}
// Create 新建品牌,返回编码后的 id。
func (s *brandService) Create(ctx context.Context, nameEn, nameCn string) (string, error) {
now := uint32(time.Now().Unix())
id, err := s.brands.Create(ctx, &model.Brand{
NameEn: nameEn,
NameCn: nameCn,
CreatedAt: now,
UpdatedAt: now,
})
if err != nil {
return "", internalErr(err.Error())
}
return hashid.Encode(id), nil
}
// Update 按编码 id 更新品牌名称。
func (s *brandService) Update(ctx context.Context, uid, nameEn, nameCn string) error {
id, err := hashid.Decode(uid)
if err != nil {
return ErrBrandNotFound
}
if err := s.brands.Update(ctx, id, nameEn, nameCn); err != nil {
return internalErr(err.Error())
}
return nil
}
// CrawlTasks 返回爬虫取任务所需的品牌列表:编码 brand_uid + 英文品牌名。
// 爬虫直接拿 brand_uid 上送 ingest,无需自己编码;name 用于拼 vogue 抓取 URL。
func (s *brandService) CrawlTasks(ctx context.Context, brandID uint32) ([]dto.CrawlBrand, error) {
brands, err := s.brands.CrawlBrands(ctx, brandID)
if err != nil {
return nil, internalErr(err.Error())
}
out := make([]dto.CrawlBrand, 0, len(brands))
for _, b := range brands {
out = append(out, dto.CrawlBrand{
BrandUID: hashid.Encode(b.ID),
Name: b.NameEn,
})
}
return out, nil
}