Files
backend_v2/internal/router/router.go
toom1996 1fd9c48f58 update
2026-09-02 21:51:35 +08:00

104 lines
4.9 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Package router 负责路由注册。
//
// 所有 URL 的唯一定义处 —— 想知道服务暴露了哪些接口,只看这个文件即可。
package router
import (
"fashionapi/internal/config"
"fashionapi/internal/handler"
"fashionapi/internal/middleware"
"fashionapi/internal/pkg/jwt"
"github.com/gin-gonic/gin"
)
// Options 路由注册所需的全部依赖,由 main 装配后传入。
type Options struct {
Config *config.Config
JWT *jwt.Manager
Article *handler.ArticleHandler
Brand *handler.BrandHandler
StreetSnap *handler.StreetSnapHandler
Auth *handler.AuthHandler
Favorite *handler.FavoriteHandler
History *handler.HistoryHandler
Health *handler.HealthHandler
SSG *handler.SSGHandler
}
// New 构建 Gin 引擎并注册全部对外路由。
//
// 注意:SSG 内部接口(/api/ssg/*)不在这里注册,而是由 NewSSG 挂在独立的内部端口上,
// 二者物理隔离,确保构建期全量数据不会从对外公开端口泄露。
func New(opt Options) *gin.Engine {
r := gin.Default()
r.Use(middleware.CORS(opt.Config.CORS))
// 上传图片的静态服务。
// 接口返回的 cover / image 是相对路径(如 /uploads/2026/08/xx.jpg),
// 前端用 toAbs() 拼上 base 后直接访问,因此这里必须对外提供静态文件。
r.Static(opt.Config.Upload.URLPrefix, opt.Config.Upload.Dir)
api := r.Group("/api/v1")
{
// 健康检查,供容器 / 反向代理探活
api.GET("/health", opt.Health.Check)
// 对外公开接口:只读查询,不暴露任何后台管理字段
public := api.Group("/public")
// ClientSign 仅挂在「列表 / 品牌」两个批量查询接口上:
// 这俩是爬虫批量 dump 的主要目标,要求前端 JS 签名可抬高抓取成本。
public.Use(middleware.ClientSign(opt.Config.ClientSign))
// 列表接口「首页公开、翻页需登录」:page=1(或缺省)无需 token 直接放行,
// page>1 才要求 Authorization: Bearer <token>,否则 401。
// 这样既让游客/SEO 看到首屏,又保留翻页 / 批量枚举的登录门槛(防爬虫 dump 全量)。
// 注意:详情接口 /api/v1/public/*/:id 不在此组内,保持公开——详情页走 SSR
// 服务端渲染、无用户 token,且分享链接/SEO 需可访问;列表隐藏即可防批量枚举。
public.Use(middleware.PublicFirstPageAuth(opt.JWT))
{
// 走秀档案列表(按品牌过滤走 ?brand_id 查询参数)
public.GET("/runway-looks", opt.Article.List)
// 品牌列表(A-Z 索引 / 搜索)
public.GET("/brands", opt.Brand.List)
// 街拍列表(批量接口,受 ClientSign 保护,与 runway-looks/brands 同批)
public.GET("/street-snaps", opt.StreetSnap.List)
}
// 走秀详情:被 SSR 服务端按需渲染(getSsrArticle)直接 fetch 调用,属可信内部取数,
// 不走前端 JS 签名,单独挂载以免破坏服务端渲染。单篇枚举防护由 HashID 承担。
api.GET("/public/runway-looks/:id", opt.Article.Detail)
// 街拍详情:同走秀详情,单条枚举防护由 HashID 承担,不走前端签名。
api.GET("/public/street-snaps/:id", opt.StreetSnap.Detail)
// 账号体系:双令牌(access 短命无状态 + refresh 落库可吊销)。
//
// 登录/刷新/登出均为公开端点(无需 Bearer,否则拿不到 token 或刷新不了)。
// /me 需 Bearer(middleware.Auth);踢下线(logout-all)由 refresh token 反查用户,亦公开。
auth := api.Group("/auth")
{
// 登录:公开端点,无需鉴权中间件(否则永远进不来)
auth.POST("/login", opt.Auth.Login)
// 续期:用 refresh token 换新的 access token
auth.POST("/refresh", opt.Auth.Refresh)
// 单设备登出:吊销当前 refresh token
auth.POST("/logout", opt.Auth.Logout)
// 全设备登出 / 踢下线:吊销该用户全部 refresh token
auth.POST("/logout-all", opt.Auth.LogoutAll)
// 取当前用户(需 Bearer)
auth.GET("/me", middleware.Auth(opt.JWT), opt.Auth.Me)
// 收藏(需 Bearer):列表 / 新增 / 删除
auth.GET("/favorites", middleware.Auth(opt.JWT), opt.Favorite.List)
auth.POST("/favorites", middleware.Auth(opt.JWT), opt.Favorite.Add)
auth.DELETE("/favorites/:target_uid", middleware.Auth(opt.JWT), opt.Favorite.Remove)
// 批量校验:列表页把当前可见 id 发来,问哪些已收藏(与收藏总量解耦,10万也常数级)
auth.POST("/favorites/check", middleware.Auth(opt.JWT), opt.Favorite.Check)
// 浏览历史(需 Bearer):分页列表(可按 kind 过滤)/ 记录 / 删除 / 清空
auth.GET("/history", middleware.Auth(opt.JWT), opt.History.List)
auth.POST("/history", middleware.Auth(opt.JWT), opt.History.Record)
auth.DELETE("/history/:target_uid", middleware.Auth(opt.JWT), opt.History.Remove)
auth.DELETE("/history", middleware.Auth(opt.JWT), opt.History.Clear)
}
}
return r
}