update
This commit is contained in:
292
internal/repository/brand_repository.go
Normal file
292
internal/repository/brand_repository.go
Normal file
@ -0,0 +1,292 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"fashionapi/internal/dto"
|
||||
"fashionapi/internal/model"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// BrandRepository 品牌数据访问接口。
|
||||
type BrandRepository interface {
|
||||
// List 按字母索引 / 关键词分页查询品牌。
|
||||
//
|
||||
// restrictIDs 用于 featured 模式:非 nil 时结果被限制在这些品牌 id 内;
|
||||
// 若 q.Featured 为 true 而 restrictIDs 为空,则返回空结果——绝不能退化成"全部品牌"。
|
||||
List(ctx context.Context, q dto.BrandQuery, restrictIDs []uint32) ([]model.Brand, int64, error)
|
||||
|
||||
// FeaturedIDs 返回"代表品牌"的 id 集合:在拥有走秀档案的品牌中,
|
||||
// 按 metric(images = 图片总数 / shows = 发布会数)排名取前 limit 个。
|
||||
FeaturedIDs(ctx context.Context, metric string, limit int) ([]uint32, error)
|
||||
|
||||
// ArticleCountByBrandIDs 批量统计各品牌的走秀档案数,单次分组查询避免 N+1。
|
||||
ArticleCountByBrandIDs(ctx context.Context, ids []uint32) (map[uint32]int, error)
|
||||
|
||||
// PopularWithCover 返回"热门品牌"及其代表走秀的封面/标题/文章 ID。
|
||||
// 规则:先按品牌总图片数取前 limit 个热门品牌,再为每个品牌取最新一篇走秀。
|
||||
// 返回的是仓储层裸数据(数字主键),对外编码由 service 层负责。
|
||||
PopularWithCover(ctx context.Context, limit int) ([]PopularBrandRow, error)
|
||||
}
|
||||
|
||||
// PopularBrandRow 热门品牌裸数据:品牌数字主键 + 中英文名称 + 代表走秀封面/中英文标题/文章 ID。
|
||||
// 注意:数字主键不出仓储层,service 层经 hashid 编码后才对外暴露;i18n 选列由 service 按 locale 决定。
|
||||
type PopularBrandRow struct {
|
||||
ID uint32
|
||||
ArticleID uint32
|
||||
BrandEn string
|
||||
BrandCn string
|
||||
TitleEn string
|
||||
TitleCn string
|
||||
Cover string
|
||||
}
|
||||
|
||||
type brandRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
// NewBrandRepository 创建品牌仓储。
|
||||
func NewBrandRepository(db *gorm.DB) BrandRepository {
|
||||
return &brandRepository{db: db}
|
||||
}
|
||||
|
||||
// latinPrefixRegexp 匹配以拉丁字母开头的品牌名。
|
||||
// A-Z 索引语义要求排除中文 / 符号 / 引号开头的脏数据。
|
||||
// 排序与索引统一基于 name_en(拉丁规范名);name_en 为空的纯中文品牌归入 OTHER 桶。
|
||||
const latinPrefixRegexp = "^[A-Za-z]"
|
||||
|
||||
// hasArticlesSubQuery 子查询:所有"至少有一篇走秀档案"的品牌 id。
|
||||
// 库中存在 2 万+ 无档案品牌,索引必须靠它收敛。
|
||||
func (r *brandRepository) hasArticlesSubQuery() *gorm.DB {
|
||||
return r.db.Model(&model.BrandRunway{}).
|
||||
Select("DISTINCT brand_id").
|
||||
Where("is_deleted = 0")
|
||||
}
|
||||
|
||||
// brandFilterScope 把品牌查询条件编译为 GORM Scope。
|
||||
func (r *brandRepository) brandFilterScope(q dto.BrandQuery, restrictIDs []uint32) func(*gorm.DB) *gorm.DB {
|
||||
return func(db *gorm.DB) *gorm.DB {
|
||||
db = db.Where("is_deleted = 0")
|
||||
|
||||
// 字母索引分桶(基于 name_en)
|
||||
switch {
|
||||
case q.Letter == "OTHER":
|
||||
// 纯中文品牌:name_en 为空
|
||||
db = db.Where("name_en = ''")
|
||||
case q.Letter >= "A" && q.Letter <= "Z" && len(q.Letter) == 1:
|
||||
db = db.Where("UPPER(LEFT(name_en,1)) = ?", q.Letter)
|
||||
default:
|
||||
db = db.Where("name_en REGEXP ?", latinPrefixRegexp)
|
||||
}
|
||||
|
||||
// 中文分桶不限档案,直接列出全部中文名品牌,便于检索
|
||||
if q.OnlyWithArticles && q.Letter != "OTHER" {
|
||||
db = db.Where("id IN (?)", r.hasArticlesSubQuery())
|
||||
}
|
||||
|
||||
if q.Keyword != "" {
|
||||
kw := "%" + q.Keyword + "%"
|
||||
db = db.Where("name_en LIKE ? OR name_cn LIKE ?", kw, kw)
|
||||
}
|
||||
|
||||
// featured 模式:收敛到精选集合;取不到 id 就收窄为空集
|
||||
if q.Featured {
|
||||
if len(restrictIDs) == 0 {
|
||||
db = db.Where("1 = 0")
|
||||
} else {
|
||||
db = db.Where("id IN (?)", restrictIDs)
|
||||
}
|
||||
}
|
||||
return db
|
||||
}
|
||||
}
|
||||
|
||||
func (r *brandRepository) List(ctx context.Context, q dto.BrandQuery, restrictIDs []uint32) ([]model.Brand, int64, error) {
|
||||
scope := r.brandFilterScope(q, restrictIDs)
|
||||
|
||||
var total int64
|
||||
if err := r.db.WithContext(ctx).
|
||||
Model(&model.Brand{}).
|
||||
Scopes(scope).
|
||||
Count(&total).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
if total == 0 {
|
||||
return []model.Brand{}, 0, nil
|
||||
}
|
||||
|
||||
var items []model.Brand
|
||||
if err := r.db.WithContext(ctx).
|
||||
Model(&model.Brand{}).
|
||||
Scopes(scope).
|
||||
Order("name_en ASC").
|
||||
Offset(q.Offset()).
|
||||
Limit(q.Size).
|
||||
Find(&items).Error; err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
return items, total, nil
|
||||
}
|
||||
|
||||
func (r *brandRepository) FeaturedIDs(ctx context.Context, metric string, limit int) ([]uint32, error) {
|
||||
orderCol := "images"
|
||||
if metric == "shows" {
|
||||
orderCol = "shows"
|
||||
}
|
||||
|
||||
var agg []struct {
|
||||
BrandID uint32 `gorm:"column:brand_id"`
|
||||
}
|
||||
err := r.db.WithContext(ctx).
|
||||
Model(&model.BrandRunway{}).
|
||||
Select("brand_id, COALESCE(SUM(image_count),0) AS images, COUNT(*) AS shows").
|
||||
Where("is_deleted = 0").
|
||||
Group("brand_id").
|
||||
Order(orderCol + " DESC").
|
||||
Limit(limit).
|
||||
Scan(&agg).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ids := make([]uint32, 0, len(agg))
|
||||
for _, a := range agg {
|
||||
ids = append(ids, a.BrandID)
|
||||
}
|
||||
return ids, nil
|
||||
}
|
||||
|
||||
func (r *brandRepository) ArticleCountByBrandIDs(ctx context.Context, ids []uint32) (map[uint32]int, error) {
|
||||
counts := make(map[uint32]int, len(ids))
|
||||
if len(ids) == 0 {
|
||||
return counts, nil
|
||||
}
|
||||
|
||||
var grp []struct {
|
||||
BrandID uint32 `gorm:"column:brand_id"`
|
||||
Cnt int `gorm:"column:cnt"`
|
||||
}
|
||||
err := r.db.WithContext(ctx).
|
||||
Model(&model.BrandRunway{}).
|
||||
Select("brand_id, COUNT(*) AS cnt").
|
||||
Where("brand_id IN ? AND is_deleted = 0", ids).
|
||||
Group("brand_id").
|
||||
Scan(&grp).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, g := range grp {
|
||||
counts[g.BrandID] = g.Cnt
|
||||
}
|
||||
return counts, nil
|
||||
}
|
||||
|
||||
// PopularWithCover 返回"热门品牌"及其最新走秀的封面与标题。
|
||||
//
|
||||
// 规则:
|
||||
// 1. 先按品牌总图片数排名取前 limit 个"热门品牌"(复用 FeaturedIDs("images"))
|
||||
// 2. 为每个热门品牌取最新一篇走秀(按真实季节年份:year DESC,season 优先级 FW>PF>SS>RES,并列取 id 最大)
|
||||
// 3. 结果按热门品牌排名顺序返回
|
||||
//
|
||||
// 注意:created_at/updated_at 是数据导入时间,不能代表走秀新旧,因此必须用 year + season_code。
|
||||
func (r *brandRepository) PopularWithCover(ctx context.Context, limit int) ([]PopularBrandRow, error) {
|
||||
if limit < 1 {
|
||||
limit = 20
|
||||
}
|
||||
if limit > 100 {
|
||||
limit = 100
|
||||
}
|
||||
|
||||
// 1) 热门品牌 id(按总图片数排名)
|
||||
ids, err := r.FeaturedIDs(ctx, "images", limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(ids) == 0 {
|
||||
return []PopularBrandRow{}, nil
|
||||
}
|
||||
|
||||
// 2) 为这些品牌各自取最新一篇走秀
|
||||
sql := `
|
||||
WITH latest_per_brand AS (
|
||||
SELECT id, brand_id, cover, title_en, title_cn,
|
||||
ROW_NUMBER() OVER (
|
||||
PARTITION BY brand_id
|
||||
ORDER BY
|
||||
year DESC,
|
||||
CASE
|
||||
WHEN season_code LIKE 'FW%' THEN 4
|
||||
WHEN season_code LIKE 'PF%' THEN 3
|
||||
WHEN season_code LIKE 'SS%' THEN 2
|
||||
WHEN season_code LIKE 'RES%' THEN 1
|
||||
ELSE 0
|
||||
END DESC,
|
||||
id DESC
|
||||
) AS rn
|
||||
FROM brand_runway
|
||||
WHERE is_deleted = 0 AND brand_id IN (?)
|
||||
)
|
||||
SELECT id, brand_id, cover, title_en, title_cn
|
||||
FROM latest_per_brand
|
||||
WHERE rn = 1`
|
||||
|
||||
var rows []struct {
|
||||
ID uint32 `gorm:"column:id"`
|
||||
BrandID uint32 `gorm:"column:brand_id"`
|
||||
Cover string `gorm:"column:cover"`
|
||||
TitleEn string `gorm:"column:title_en"`
|
||||
TitleCn string `gorm:"column:title_cn"`
|
||||
}
|
||||
if err := r.db.WithContext(ctx).Raw(sql, ids).Scan(&rows).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 3) 取品牌中英文名称
|
||||
nameMap := make(map[uint32]struct{ En, Cn string }, len(ids))
|
||||
{
|
||||
var brands []model.Brand
|
||||
if err := r.db.WithContext(ctx).
|
||||
Where("id IN ? AND is_deleted = 0", ids).Find(&brands).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, b := range brands {
|
||||
nameMap[b.ID] = struct{ En, Cn string }{b.NameEn, b.NameCn}
|
||||
}
|
||||
}
|
||||
|
||||
// 4) 按热度排名顺序(ids)组装,保证前端展示顺序与热门排名一致
|
||||
latestByBrand := make(map[uint32]struct {
|
||||
ArticleID uint32
|
||||
Cover string
|
||||
TitleEn string
|
||||
TitleCn string
|
||||
}, len(rows))
|
||||
for _, rw := range rows {
|
||||
latestByBrand[rw.BrandID] = struct {
|
||||
ArticleID uint32
|
||||
Cover string
|
||||
TitleEn string
|
||||
TitleCn string
|
||||
}{ArticleID: rw.ID, Cover: rw.Cover, TitleEn: rw.TitleEn, TitleCn: rw.TitleCn}
|
||||
}
|
||||
|
||||
list := make([]PopularBrandRow, 0, len(ids))
|
||||
for _, id := range ids {
|
||||
l, ok := latestByBrand[id]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
nm := nameMap[id]
|
||||
list = append(list, PopularBrandRow{
|
||||
ID: id,
|
||||
ArticleID: l.ArticleID,
|
||||
BrandEn: nm.En,
|
||||
BrandCn: nm.Cn,
|
||||
TitleEn: l.TitleEn,
|
||||
TitleCn: l.TitleCn,
|
||||
Cover: l.Cover,
|
||||
})
|
||||
}
|
||||
return list, nil
|
||||
}
|
||||
Reference in New Issue
Block a user