update
This commit is contained in:
@ -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
|
||||
}
|
||||
|
||||
@ -25,9 +25,6 @@ type BrandRepository interface {
|
||||
// 供 Hot 等已知 id 列表的场景使用,避免借道 List 的 featured 旗标复用查询。
|
||||
ListByIDs(ctx context.Context, ids []uint32) ([]model.Brand, error)
|
||||
|
||||
// ArticleCountByBrandIDs 批量统计各品牌的走秀档案数,单次分组查询避免 N+1。
|
||||
ArticleCountByBrandIDs(ctx context.Context, ids []uint32) (map[uint32]int, error)
|
||||
|
||||
// PopularWithCover 返回"热门品牌"及其代表走秀的封面/标题/文章 ID。
|
||||
// 规则:先按品牌总图片数取前 limit 个热门品牌,再为每个品牌取最新一篇走秀。
|
||||
// 返回的是仓储层裸数据(数字主键),对外编码由 service 层负责。
|
||||
@ -84,10 +81,12 @@ func (r *brandRepository) brandFilterScope(q dto.BrandQuery, restrictIDs []uint3
|
||||
db = db.Where("name_en REGEXP ?", latinPrefixRegexp)
|
||||
}
|
||||
|
||||
// 中文分桶不限档案,直接列出全部中文名品牌,便于检索
|
||||
// 关键词搜索时不限制"必须有走秀档案":用户按名字找品牌,
|
||||
// 即使该品牌暂无走秀档案也应被命中(如 A Bathing Ape / 猿人头)。
|
||||
if q.OnlyWithArticles && q.Keyword == "" && q.Letter != "OTHER" {
|
||||
// 字母索引始终只返回"拥有走秀档案"的品牌(库中有 2 万+ 无档案品牌,
|
||||
// 不过滤会污染 A-Z 索引)。原 only_with_articles 参数已删除,此过滤无条件生效。
|
||||
// 例外:关键词搜索时不限制"必须有走秀档案"(用户按名字找品牌,
|
||||
// 即使该品牌暂无走秀档案也应被命中,如 A Bathing Ape / 猿人头);
|
||||
// 中文分桶(OTHER)同样列出全部中文名品牌便于检索。
|
||||
if q.Keyword == "" && q.Letter != "OTHER" {
|
||||
db = db.Where("id IN (?)", r.hasArticlesSubQuery())
|
||||
}
|
||||
|
||||
@ -177,31 +176,6 @@ func (r *brandRepository) ListByIDs(ctx context.Context, ids []uint32) ([]model.
|
||||
return items, 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 返回"热门品牌"及其最新走秀的封面与标题。
|
||||
//
|
||||
// 规则:
|
||||
|
||||
@ -10,13 +10,10 @@ import (
|
||||
)
|
||||
|
||||
// UserRepository 用户数据访问接口。
|
||||
//
|
||||
// 当前只保留 FindByID(供 /auth/me 取当前用户)。
|
||||
// ExistsByUsernameOrEmail / Create / FindByAccount 是注册登录专用,已随路由下线一并删除。
|
||||
type UserRepository interface {
|
||||
// ExistsByUsernameOrEmail 判断用户名或邮箱是否已被占用。
|
||||
ExistsByUsernameOrEmail(ctx context.Context, username, email string) (bool, error)
|
||||
// Create 创建用户。
|
||||
Create(ctx context.Context, u *model.User) error
|
||||
// FindByAccount 按邮箱或用户名查找未删除的用户。不存在时返回 ErrNotFound。
|
||||
FindByAccount(ctx context.Context, account string) (*model.User, error)
|
||||
// FindByID 按主键查找用户。不存在时返回 ErrNotFound。
|
||||
FindByID(ctx context.Context, id uint32) (*model.User, error)
|
||||
}
|
||||
@ -30,30 +27,6 @@ func NewUserRepository(db *gorm.DB) UserRepository {
|
||||
return &userRepository{db: db}
|
||||
}
|
||||
|
||||
func (r *userRepository) ExistsByUsernameOrEmail(ctx context.Context, username, email string) (bool, error) {
|
||||
var cnt int64
|
||||
err := r.db.WithContext(ctx).
|
||||
Model(&model.User{}).
|
||||
Where("username = ? OR email = ?", username, email).
|
||||
Count(&cnt).Error
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return cnt > 0, nil
|
||||
}
|
||||
|
||||
func (r *userRepository) Create(ctx context.Context, u *model.User) error {
|
||||
return r.db.WithContext(ctx).Create(u).Error
|
||||
}
|
||||
|
||||
func (r *userRepository) FindByAccount(ctx context.Context, account string) (*model.User, error) {
|
||||
var u model.User
|
||||
err := r.db.WithContext(ctx).
|
||||
Where("is_deleted = 0 AND (email = ? OR username = ?)", account, account).
|
||||
First(&u).Error
|
||||
return wrapUser(&u, err)
|
||||
}
|
||||
|
||||
func (r *userRepository) FindByID(ctx context.Context, id uint32) (*model.User, error) {
|
||||
var u model.User
|
||||
err := r.db.WithContext(ctx).First(&u, id).Error
|
||||
|
||||
Reference in New Issue
Block a user