This commit is contained in:
toom1996
2026-08-30 10:45:34 +08:00
parent a6724265ad
commit 9c3403a903
23 changed files with 284 additions and 508 deletions

View File

@ -20,9 +20,6 @@ type ArticleRepository interface {
ListImages(ctx context.Context, runwayID string) ([]model.BrandRunwayImage, error)
// ImagesByRunwayIDs 批量查询多篇文章的图片,单次 IN 查询避免 N+1。
ImagesByRunwayIDs(ctx context.Context, ids []uint32) (map[uint32][]model.BrandRunwayImage, error)
// IDs 返回全部未删除文章的 id(升序),供 SSG 的 getStaticPaths 枚举路径使用。
// 只回主键,不带回封面/描述等大字段,比拉 size=500 再 .map(id) 省得多。
IDs(ctx context.Context) ([]uint32, error)
}
type articleRepository struct {
@ -35,17 +32,16 @@ func NewArticleRepository(db *gorm.DB) ArticleRepository {
}
// selectColumns 列表查询的投影列:只取对外需要的字段,不做 SELECT *。
const articleListColumns = `brand_runway.id, brand_runway.brand_id, brand_runway.title,
// 注意:year / season / season_code / collection_type / created_at 等仅用于排序,不 SELECT 到响应里
// (前端按年份/季节筛选是走查询参数,响应无需回带这些元数据)。
const articleListColumns = `brand_runway.id, brand_runway.brand_id,
brand_runway.title_en, brand_runway.title_cn,
brand_runway.description, brand_runway.description_en, brand_runway.description_cn,
brand_runway.cover, brand_runway.year, brand_runway.image_count,
brand_runway.created_at, brand_runway.collection_type, brand_runway.season,
brand_runway.season_code, b.name AS brand_name, b.name_en AS brand_name_en, b.name_cn AS brand_name_cn`
brand_runway.cover, brand_runway.image_count,
b.name_en AS brand_name_en, b.name_cn AS brand_name_cn`
const articleDetailColumns = `brand_runway.id, brand_runway.title, brand_runway.title_en, brand_runway.title_cn,
brand_runway.description, brand_runway.description_en, brand_runway.description_cn,
brand_runway.cover, brand_runway.year, brand_runway.image_count, brand_runway.created_at,
brand_runway.source_url, b.name AS brand_name, b.name_en AS brand_name_en, b.name_cn AS brand_name_cn`
const articleDetailColumns = `brand_runway.id, brand_runway.title_en, brand_runway.title_cn,
brand_runway.description_en, brand_runway.description_cn,
brand_runway.cover, b.name_en AS brand_name_en, b.name_cn AS brand_name_cn`
// brandJoin 关联品牌表取品牌名;LEFT JOIN 保证品牌被软删时文章依然可见。
const brandJoin = "LEFT JOIN brand b ON b.id = brand_runway.brand_id AND b.is_deleted = 0"
@ -58,21 +54,16 @@ func filterScope(q dto.ArticleQuery) func(*gorm.DB) *gorm.DB {
return func(db *gorm.DB) *gorm.DB {
db = db.Where("brand_runway.is_deleted = 0")
if q.Keyword != "" {
kw := "%" + q.Keyword + "%"
db = db.Where("brand_runway.title_en LIKE ? OR brand_runway.title_cn LIKE ?", kw, kw)
}
if q.BrandID != "" && q.BrandID != "0" {
db = db.Where("brand_runway.brand_id = ?", q.BrandID)
}
// 多值筛选统一策略:单值用 = (命中索引更精准),多值用 IN,空集合不加条件。
// 按品牌过滤来自列表接口的 ?brand_id 查询参数(q.BrandIDs 由 handler 解码注入)。
db = whereMulti(db, "brand_runway.brand_id", toAnySlice(q.BrandIDs))
db = whereMulti(db, "brand_runway.collection_type", toAnySlice(q.CollectionTypes))
db = whereMulti(db, "brand_runway.season", toAnySlice(q.Seasons))
db = whereMulti(db, "brand_runway.year", toAnySlice(q.Years))
if q.SeasonCode != "" {
db = db.Where("brand_runway.season_code = ?", q.SeasonCode)
if q.Collection != "" {
db = db.Where("brand_runway.collection_type = ?", q.Collection)
}
if q.Season != "" {
db = db.Where("brand_runway.season = ?", q.Season)
}
if q.Year != 0 {
db = db.Where("brand_runway.year = ?", q.Year)
}
return db
}
@ -104,17 +95,19 @@ func toAnySlice[T any](in []T) []any {
// orderBy 把 sort 参数映射为 ORDER BY 子句。
//
// 白名单映射而非直接拼接用户输入,从根上排除 SQL 注入;
// 每个分支都以 created_at + id 兜底,保证分页结果稳定不跳行。
// 每个分支都以 id 兜底,保证分页结果稳定不跳行。
//
// newest(默认):按走秀真实新旧排序 —— created_at 仅是导入时间,不能代表走秀新旧,
// 故用 year DESC + season_code 优先级(FW>PF>SS>RES)+ id 兜底,与首页「最新走秀」口径一致。
func orderBy(sort string) string {
seasonPrio := `CASE brand_runway.season_code WHEN 'FW' THEN 4 WHEN 'PF' THEN 3 WHEN 'SS' THEN 2 WHEN 'RES' THEN 1 ELSE 0 END`
switch sort {
case "year_desc":
return "brand_runway.year DESC, brand_runway.created_at DESC, brand_runway.id DESC"
case "year_asc":
return "brand_runway.year ASC, brand_runway.created_at DESC, brand_runway.id DESC"
case "image_count":
return "brand_runway.image_count DESC, brand_runway.created_at DESC, brand_runway.id DESC"
return "brand_runway.year ASC, brand_runway.id ASC"
case "year_desc":
return "brand_runway.year DESC, brand_runway.id DESC"
default: // newest
return "brand_runway.created_at DESC, brand_runway.id DESC"
return "brand_runway.year DESC, " + seasonPrio + " DESC, brand_runway.id DESC"
}
}
@ -195,14 +188,3 @@ func (r *articleRepository) ImagesByRunwayIDs(ctx context.Context, ids []uint32)
}
// IDs 返回全部未删除文章的 id(升序),供 SSG 构建期枚举详情页路径。
func (r *articleRepository) IDs(ctx context.Context) ([]uint32, error) {
var ids []uint32
if err := r.db.WithContext(ctx).
Model(&model.BrandRunway{}).
Where("is_deleted = 0").
Order("id ASC").
Pluck("id", &ids).Error; err != nil {
return nil, err
}
return ids, nil
}