chore: release server-v1.0.62
Deploy Server / release-deploy-server (push) Successful in 51s

服务端安全加固:多维限流(按 IP/按门店)+ 敏感接口独立速率上限抵御 DDoS/刷接口;
登录暴力破解新增按来源 IP 锁定;反代后正确识别真实客户端 IP;
门店 custom_fields 轻量配置(录入默认值)透传保存。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y2Wdwo7SmgBJU37cBrkhPK
This commit is contained in:
wangjia
2026-06-19 20:20:44 +08:00
parent 0d967e899a
commit 7bbc944ae2
14 changed files with 628 additions and 57 deletions
+36 -23
View File
@@ -4,6 +4,7 @@ import (
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"github.com/wangjia/jiu/backend/config"
"github.com/wangjia/jiu/backend/internal/handler"
"github.com/wangjia/jiu/backend/internal/middleware"
"github.com/wangjia/jiu/backend/internal/service"
@@ -46,31 +47,43 @@ func Setup(r *gin.Engine, db *gorm.DB) {
// 版本信息(无需认证,用于客户端更新检查)
r.GET("/version", handler.GetVersion)
// 限流维度(按真实客户端 IP,依赖 main.go 的 SetTrustedProxies)。
// 防爆破/防刷写/防爬:未鉴权接口逐条挂 per-IP 限流,公开商品列表最紧。
rl := config.C.RateLimit
publicReadIP := middleware.RateLimitByIP(middleware.PerMinute(rl.PublicReadPerMin), rl.PublicReadPerMin)
shopListIP := middleware.RateLimitByIP(middleware.PerMinute(rl.ShopListPerMin), rl.ShopListPerMin)
loginIP := middleware.RateLimitByIP(middleware.PerMinute(rl.LoginPerMin), rl.LoginPerMin)
refreshIP := middleware.RateLimitByIP(middleware.PerMinute(rl.RefreshPerMin), rl.RefreshPerMin)
registerIP := middleware.RateLimitByIP(middleware.PerMinute(rl.RegisterPerMin), rl.RegisterPerMin)
errorsIP := middleware.RateLimitByIP(middleware.PerMinute(rl.ErrorsPerMin), rl.ErrorsPerMin)
// 公开商品详情页(注入 OG 标签的 Flutter index.html,供社交分享爬虫读取)
r.GET("/product/:public_id", publicH.ProductPage)
r.GET("/product/:public_id", publicReadIP, publicH.ProductPage)
v1 := r.Group("/api/v1")
// 公开路由(无需登录)
// 公开路由(无需登录):登录/刷新按 IP 限流(与 service 层失败锁定互补)
auth := v1.Group("/auth")
{
auth.POST("/login", authH.Login)
auth.POST("/refresh", authH.Refresh)
auth.POST("/login", loginIP, authH.Login)
auth.POST("/refresh", refreshIP, authH.Refresh)
}
// 公开接口(无需登录)
// 公开接口(无需登录):读接口防爬、写接口防刷
public := v1.Group("/public")
{
public.GET("/products/:public_id", publicH.GetProduct)
public.GET("/shops/:shop_code/products", publicH.ListShopProducts)
public.GET("/release", publicH.GetRelease)
public.POST("/errors", errorReportH.Submit)
public.POST("/register", authH.Register)
public.GET("/products/:public_id", publicReadIP, publicH.GetProduct)
public.GET("/shops/:shop_code/products", shopListIP, publicH.ListShopProducts)
public.GET("/release", publicReadIP, publicH.GetRelease)
public.POST("/errors", errorsIP, errorReportH.Submit)
public.POST("/register", registerIP, authH.Register)
}
// 需要 JWT 的基础路由组
api := v1.Group("")
api.Use(middleware.JWT(db))
// 每店限流(按 shop_id,挂在 JWT 之后):防单店打爆共享后端,保障多租户公平
api.Use(middleware.RateLimitByShop(middleware.PerSecond(rl.ShopRPS), rl.ShopBurst))
// 会话/在线状态(豁免 LicenseGuard:登出/心跳/在线列表在锁定期也要可用)
{
@@ -179,11 +192,11 @@ func Setup(r *gin.Engine, db *gorm.DB) {
// 财务
finance := api.Group("/finance")
{
finance.GET("/records", financeH.ListRecords)
finance.POST("/records", financeH.Create)
finance.PUT("/records/:id/close", financeH.Close)
finance.PUT("/records/close-by-ref", financeH.CloseByRef)
finance.GET("/summary", financeH.Summary)
finance.GET("/records", financeH.ListRecords)
finance.POST("/records", financeH.Create)
finance.PUT("/records/:id/close", financeH.Close)
finance.PUT("/records/close-by-ref", financeH.CloseByRef)
finance.GET("/summary", financeH.Summary)
}
// 酒行信息
@@ -211,15 +224,15 @@ func Setup(r *gin.Engine, db *gorm.DB) {
// 导入
imp := api.Group("/import")
{
imp.POST("/products", importH.ImportProducts)
imp.POST("/partners", importH.ImportPartners)
imp.POST("/product-names", importH.ImportProductNames)
imp.POST("/products", importH.ImportProducts)
imp.POST("/partners", importH.ImportPartners)
imp.POST("/product-names", importH.ImportProductNames)
imp.POST("/product-series", importH.ImportProductSeries)
imp.POST("/product-specs", importH.ImportProductSpecs)
imp.POST("/product-codes", importH.ImportProductCodes)
imp.POST("/stock-in", importH.ImportStockIn)
imp.POST("/stock-out", importH.ImportStockOut)
imp.POST("/inventory", importH.ImportInventory)
imp.POST("/product-specs", importH.ImportProductSpecs)
imp.POST("/product-codes", importH.ImportProductCodes)
imp.POST("/stock-in", importH.ImportStockIn)
imp.POST("/stock-out", importH.ImportStockOut)
imp.POST("/inventory", importH.ImportInventory)
}
// 基础数据选项(名称/系列/规格)