This commit is contained in:
toom1996
2026-08-26 10:45:21 +08:00
parent 3cf1368e68
commit 30c9f21da4
48 changed files with 3442 additions and 0 deletions

98
internal/dto/article.go Normal file
View File

@ -0,0 +1,98 @@
package dto
// ---------- 请求 ----------
// ArticleQuery 文章列表查询条件。
//
// 全部筛选项均支持多值(HBX 风格复选框),由 handler 从 query string 解析后填充。
type ArticleQuery struct {
Page int
Size int
Keyword string // 标题模糊搜索
BrandID string // 单个品牌 id
BrandIDs []uint32 // 多个品牌 id(brand_ids 逗号分隔)
CollectionTypes []string // rtw / menswear / couture / resort / pre_fall
Seasons []string // spring / fall
SeasonCode string // SS26 / FW25,精确匹配
Years []int // 年份
Sort string // newest / year_desc / year_asc / image_count
WithImages int // 列表附带每篇前 N 张图,0 表示不带
// Locale 语言:cn | en。空时由 service 回落默认 en。
// 决定 title / description / brand_name 出参选取 *_en 还是 *_cn。
Locale string
}
// 列表分页与附图数量的边界,与原项目保持一致。
const (
articleDefaultSize = 12
articleMaxSize = 100
withImagesMax = 12
)
// Normalize 校正分页与附图参数,防止非法值打穿数据库。
func (q *ArticleQuery) Normalize() {
if q.Page < 1 {
q.Page = 1
}
if q.Size < 1 || q.Size > articleMaxSize {
q.Size = articleDefaultSize
}
if q.WithImages < 0 {
q.WithImages = 0
}
if q.WithImages > withImagesMax {
q.WithImages = withImagesMax
}
if q.Sort == "" {
q.Sort = "newest"
}
}
// Offset 返回 SQL 偏移量。
func (q ArticleQuery) Offset() int { return (q.Page - 1) * q.Size }
// ---------- 响应 ----------
// PublicArticleImage 对外展示用的图片结构(仅 url + 名称)。
type PublicArticleImage struct {
Image string `json:"image"`
Name string `json:"name"`
}
// PublicArticle 对外展示用的精简文章结构。
//
// 刻意剔除后台管理字段(is_deleted / updated_at / description 全文)。
// 对外只暴露编码后的 UID(非自增主键),防止爬虫顺序枚举;
// 同时把 brand_id 也编码为 brand_uid,品牌跳转同样不可枚举。
type PublicArticle struct {
UID string `json:"id"` // 编码后的文章 id(无序串)
BrandUID string `json:"brand_id"` // 编码后的品牌 id(无序串)
Title string `json:"title"`
Summary string `json:"summary"` // description 截断后的对外摘要
Cover string `json:"cover"` // 封面相对路径,如 /uploads/xxx.jpg
BrandName string `json:"brand_name"` // JOIN brand 得到
Year uint16 `json:"year"`
ImageCount uint16 `json:"image_count"`
CollectionType string `json:"collection_type"` // rtw/menswear/couture/resort/pre_fall
Season string `json:"season"` // spring/fall/空
SeasonCode string `json:"season_code"` // SS26/FW25/RES26/PF25
PublishedAt uint32 `json:"published_at"` // = created_at,Unix 秒
Images []PublicArticleImage `json:"images"` // with_images 参数控制,未请求时为 null
}
// PublicArticleDetail 对外只读文章详情:含正文与完整图片集,不暴露管理字段。
type PublicArticleDetail struct {
UID string `json:"id"` // 编码后的文章 id(无序串)
BrandUID string `json:"brand_id"` // 编码后的品牌 id(无序串)
Title string `json:"title"`
Summary string `json:"summary"`
Description string `json:"description"`
Cover string `json:"cover"`
BrandName string `json:"brand_name"`
Year uint16 `json:"year"`
ImageCount uint16 `json:"image_count"`
SourceURL string `json:"source_url"`
PublishedAt uint32 `json:"published_at"`
Images []PublicArticleImage `json:"images"`
}

30
internal/dto/auth.go Normal file
View File

@ -0,0 +1,30 @@
package dto
// ---------- 请求 ----------
// AuthRequest 注册 / 登录的统一请求体。
//
// 沿用原项目的单结构设计:注册用 username + email + password,
// 登录用 account(可填邮箱或用户名)+ password。
type AuthRequest struct {
Username string `json:"username"`
Email string `json:"email"`
Password string `json:"password"`
Account string `json:"account"`
}
// ---------- 响应 ----------
// UserPayload 对外返回的用户信息,绝不包含密码哈希。
type UserPayload struct {
ID uint32 `json:"id"`
Username string `json:"username"`
Email string `json:"email"`
}
// AuthResult 注册 / 登录成功后的返回:令牌 + 用户信息。
// 前端 src/lib/auth.ts 依赖 token 与 user 两个顶层字段。
type AuthResult struct {
Token string `json:"token"`
User UserPayload `json:"user"`
}

74
internal/dto/brand.go Normal file
View File

@ -0,0 +1,74 @@
package dto
// ---------- 请求 ----------
// BrandQuery 品牌列表查询条件。
type BrandQuery struct {
Letter string // A-Z 字母索引;OTHER 表示中文分桶;空表示全部拉丁字母
Keyword string
Page int
Size int
// OnlyWithArticles 只返回"拥有走秀档案"的品牌,默认 true。
// 库中有 2 万+ 无档案的品牌,若不过滤会污染 A-Z 索引。
OnlyWithArticles bool
// Featured 仅保留"代表品牌"精选集合(按指标排名的前 N 个)。
Featured bool
Metric string // images(默认)| shows
Limit int // 精选集合大小,默认 200
// Locale 语言:cn | en。空时由 service 回落默认 en。
// 决定 name / show_name 出参选取 name_en 还是 name_cn。
Locale string
}
// 品牌列表分页与精选集合的边界,与原项目保持一致。
const (
brandDefaultSize = 200
brandMaxSize = 500
featuredDefaultLimit = 200
featuredMaxLimit = 500
)
// Normalize 校正分页与精选集合参数。
func (q *BrandQuery) Normalize() {
if q.Page < 1 {
q.Page = 1
}
if q.Size < 1 || q.Size > brandMaxSize {
q.Size = brandDefaultSize
}
if q.Metric != "shows" {
q.Metric = "images"
}
if q.Limit < 1 || q.Limit > featuredMaxLimit {
q.Limit = featuredDefaultLimit
}
}
// Offset 返回 SQL 偏移量。
func (q BrandQuery) Offset() int { return (q.Page - 1) * q.Size }
// ---------- 响应 ----------
// PublicBrand 对外展示用的精简品牌结构,剔除 created_at / updated_at / is_deleted。
// 注意:对外只暴露编码后的 UID(非自增主键),防止顺序枚举。
type PublicBrand struct {
UID string `json:"id"` // 编码后的品牌 id(无序串),对外唯一标识
Name string `json:"name"`
ShowName string `json:"show_name"`
ArticleCount int `json:"article_count"` // 该品牌下的走秀档案数,用于索引计数
}
// PopularBrand 首页"热门品牌"区块所需结构。
//
// 与 PublicBrand 不同:这里要带上"代表走秀"的封面与标题,供 SSG 在构建期直接渲染卡片,
// 前端无需再逐篇请求详情。字段对齐需求:id(品牌 id 编码串)/ brand(品牌名)/ cover(封面图)/ title(代表走秀标题)/ article_id(代表走秀 id 编码串,用于跳转到详情页)。
type PopularBrand struct {
UID string `json:"id"` // 编码后的品牌 id(无序串)
ArticleUID string `json:"article_id"` // 编码后的代表走秀 id(无序串)
Brand string `json:"brand"` // 品牌名称
Cover string `json:"cover"` // 代表走秀封面图(相对路径,前端 toAbs 拼 base 后访问)
Title string `json:"title"` // 代表走秀标题
}