update
This commit is contained in:
63
internal/router/router.go
Normal file
63
internal/router/router.go
Normal file
@ -0,0 +1,63 @@
|
||||
// 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
|
||||
Auth *handler.AuthHandler
|
||||
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")
|
||||
{
|
||||
// 健康检查,供容器 / 反向代理探活
|
||||
api.GET("/health", opt.Health.Check)
|
||||
|
||||
// 对外公开接口:只读查询,不暴露任何后台管理字段
|
||||
public := api.Group("/public")
|
||||
{
|
||||
public.GET("/articles", opt.Article.List)
|
||||
public.GET("/articles/:id", opt.Article.Detail)
|
||||
public.GET("/brands", opt.Brand.List)
|
||||
}
|
||||
|
||||
// 账号体系:注册 / 登录 / 登出 / 当前用户(JWT 无状态)
|
||||
auth := api.Group("/auth")
|
||||
{
|
||||
auth.POST("/register", opt.Auth.Register)
|
||||
auth.POST("/login", opt.Auth.Login)
|
||||
auth.POST("/logout", opt.Auth.Logout)
|
||||
auth.GET("/me", middleware.Auth(opt.JWT), opt.Auth.Me)
|
||||
}
|
||||
}
|
||||
|
||||
return r
|
||||
}
|
||||
72
internal/router/ssg.go
Normal file
72
internal/router/ssg.go
Normal file
@ -0,0 +1,72 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"fashionapi/internal/middleware"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// NewSSG 构建「仅供构建期使用的内部引擎」,并返回它。
|
||||
//
|
||||
// 与 New()(对外公开引擎,挂在 8090)的关键区别:
|
||||
// 1. 不装载 CORS 中间件 —— SSG 是 server-to-server 的 node fetch 调用,CORS 是浏览器机制,用不上,反而减小攻击面;
|
||||
// 2. 装载 SSGToken 中间件 —— 若配置了 SSG_TOKEN 则强制校验(防御纵深);
|
||||
// 3. 只注册 /api/ssg/* 路由 —— 全量/全 ID 这类「构建专用」数据只在这里出现;
|
||||
// 4. 该引擎由 main 监听在独立的内部端口(默认 8091),docker-compose 把它绑定到 127.0.0.1,
|
||||
// nginx 也绝不反代,因此外部网络根本连不到,SSG 数据不会外泄。
|
||||
//
|
||||
// 这样即使有人从公网域名探测 /api/ssg/*,请求只会打到 nginx(80)或公开引擎(8090),
|
||||
// 二者都没有该路由,自然 404 / 由 nginx 拦截,数据永不出口。
|
||||
func NewSSG(opt Options) *gin.Engine {
|
||||
r := gin.New()
|
||||
r.Use(gin.Recovery())
|
||||
r.Use(ssgLogger())
|
||||
r.Use(middleware.SSGToken(opt.Config.Server.SSGToken))
|
||||
|
||||
ssg := r.Group("/api/ssg")
|
||||
{
|
||||
ssg.GET("/brands", opt.SSG.Brands) // 品牌索引页全量数据(A-Z)
|
||||
ssg.GET("/brands/popular", opt.SSG.Popular) // 首页热门品牌区块
|
||||
ssg.GET("/articles/ids", opt.SSG.ArticleIDs) // 文章详情页路径枚举(getStaticPaths)
|
||||
}
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
// ssgLogger 输出与公开引擎一致的请求日志,但会把 query string 里的 token 脱敏为 ***,
|
||||
// 避免 SSG_TOKEN 在日志里明文泄露。
|
||||
func ssgLogger() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
start := time.Now()
|
||||
path := c.Request.URL.Path
|
||||
rawQuery := c.Request.URL.RawQuery
|
||||
if rawQuery != "" {
|
||||
if q, err := url.ParseQuery(rawQuery); err == nil && q.Get("token") != "" {
|
||||
q.Set("token", "***")
|
||||
rawQuery = q.Encode()
|
||||
}
|
||||
}
|
||||
|
||||
c.Next()
|
||||
|
||||
latency := time.Since(start)
|
||||
queryPart := ""
|
||||
if rawQuery != "" {
|
||||
queryPart = "?" + rawQuery
|
||||
}
|
||||
fmt.Fprintf(gin.DefaultWriter,
|
||||
"[GIN] %s | %3d | %13v | %15s | %-7s %s%s\n",
|
||||
time.Now().Format("2006/01/02 - 15:04:05"),
|
||||
c.Writer.Status(),
|
||||
latency,
|
||||
c.ClientIP(),
|
||||
c.Request.Method,
|
||||
path,
|
||||
queryPart,
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user