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 }