Files
jiu/backend/internal/router/router.go
T
wangjia b3c9d98eae feat: ¥1 首月特惠套餐(新店专享,每店限购一次,官网+App 同步上线)
- 后端:payPlans 新增 promo_first_month(¥1/30 天/标准版权益);下单强校验每店限购一次(仅 paid 计数,弃单可重下),已享用返回 409;新增 GET /license/promo-status 供前端置灰
- 官网:checkout 套餐段第二位插「🔥 首月特惠 ¥1」+ 推广横幅 + 划线原价 ¥299;首页价格区第二位插特惠卡(5 列);已享用自动置灰
- App:购买弹窗套餐段改三档(标准/特惠/高级),特惠选中显示推广横幅与划线价,已享用置灰并禁用提交
- 实付金额以 pay 侧为准,pay 需 seed promo_first_month 产品后生效

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JJ1g8XV1YhhmHRzhwWEW7o
2026-07-03 22:25:07 +08:00

323 lines
12 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
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"
)
func Setup(r *gin.Engine, db *gorm.DB) {
// 服务层
authSvc := service.NewAuthService(db)
licenseSvc := service.NewLicenseService(db)
stockSvc := service.NewStockService(db)
paySvc := service.NewPayService(db, config.C.Pay.BaseURL, config.C.Pay.Secret, config.C.Pay.ReturnURL)
service.StartPayReconcile(paySvc) // 查单兜底(PAY_SECRET 未配置时空转不启动)
// 处理器
authH := handler.NewAuthHandler(authSvc, licenseSvc)
sessionH := handler.NewSessionHandler(authSvc)
licenseH := handler.NewLicenseHandler(licenseSvc)
payH := handler.NewPayHandler(paySvc)
productH := handler.NewProductHandler(db)
warehouseH := handler.NewWarehouseHandler(db)
partnerH := handler.NewPartnerHandler(db)
stockInH := handler.NewStockInHandler(db, stockSvc)
stockOutH := handler.NewStockOutHandler(db, stockSvc)
inventoryH := handler.NewInventoryHandler(db)
importH := handler.NewImportHandler(db)
userH := handler.NewUserHandler(db)
productOptH := handler.NewProductOptionHandler(db)
productAttrH := handler.NewProductAttrHandler(db)
productImageH := handler.NewProductImageHandler(db)
financeH := handler.NewFinanceHandler(db)
numberRuleH := handler.NewNumberRuleHandler(db)
publicH := handler.NewPublicHandler(db)
adminH := handler.NewAdminHandler(db)
shopH := handler.NewShopHandler(db)
errorReportH := handler.NewErrorReportHandler(db)
feedbackH := handler.NewFeedbackHandler(db)
// 健康检查(无需认证,用于前端连通性探测)
r.GET("/health", func(c *gin.Context) {
c.JSON(200, gin.H{"status": "ok"})
})
// 版本信息(无需认证,用于客户端更新检查)
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", publicReadIP, publicH.ProductPage)
v1 := r.Group("/api/v1")
// 公开路由(无需登录):登录/刷新按 IP 限流(与 service 层失败锁定互补)
auth := v1.Group("/auth")
{
auth.POST("/login", loginIP, authH.Login)
auth.POST("/refresh", refreshIP, authH.Refresh)
}
// 公开接口(无需登录):读接口防爬、写接口防刷
public := v1.Group("/public")
{
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)
}
// pay 支付成功 webhook(公开路由,HMAC 验签在 handler 内;按 IP 限流防噪)
v1.POST("/pay/callback", publicReadIP, payH.Callback)
// 需要 JWT 的基础路由组
api := v1.Group("")
api.Use(middleware.JWT(db))
// 每店限流(按 shop_id,挂在 JWT 之后):防单店打爆共享后端,保障多租户公平
api.Use(middleware.RateLimitByShop(middleware.PerSecond(rl.ShopRPS), rl.ShopBurst))
// 会话/在线状态(豁免 LicenseGuard:登出/心跳/在线列表在锁定期也要可用)
{
api.POST("/auth/logout", authH.Logout)
api.POST("/auth/ping", authH.Ping)
// 在线会话列表:所有登录用户只读
api.GET("/sessions", sessionH.List)
// 强制下线:仅 admin/superadmin
api.DELETE("/sessions/:id", middleware.AdminOnly(), sessionH.ForceLogout)
}
// 许可证路由:豁免 LicenseGuard(锁定时仍需查看状态和激活)
license := api.Group("/license")
license.Use(middleware.ReadOnly())
{
license.GET("/info", licenseH.Info)
license.POST("/activate", licenseH.Activate)
license.GET("/verify", licenseH.Verify)
license.POST("/deactivate", licenseH.Deactivate)
license.GET("/devices", licenseH.Devices)
// 在线购买/续费(走 pay 收款中枢;仅管理员,handler 内判权)
license.POST("/purchase", payH.Purchase)
license.GET("/purchase/:out_trade_no", payH.PurchaseStatus)
license.GET("/promo-status", payH.PromoStatus)
}
// 业务路由:ReadOnly + LicenseGuard(过期只读/锁定拦截写操作)
{
api.Use(middleware.ReadOnly(), middleware.LicenseGuard(db))
// 商品
products := api.Group("/products")
{
products.GET("", productH.List)
products.POST("", productH.Create)
products.POST("/find-or-create", productH.FindOrCreate)
products.GET("/:id/detail", productH.Detail)
products.GET("/:id/price-history", productH.PriceHistory)
products.GET("/:id/qrcode", productH.QRCode)
products.PUT("/:id", productH.Update)
products.DELETE("/:id", productH.Delete)
products.POST("/:id/images", productImageH.Upload)
products.DELETE("/:id/images/:image_id", productImageH.Delete)
}
// 仓库
warehouses := api.Group("/warehouses")
{
warehouses.GET("", warehouseH.List)
warehouses.POST("", warehouseH.Create)
warehouses.PUT("/:id", warehouseH.Update)
warehouses.DELETE("/:id", warehouseH.Delete)
}
// 往来单位
partners := api.Group("/partners")
{
partners.GET("", partnerH.List)
partners.POST("", partnerH.Create)
partners.PUT("/:id", partnerH.Update)
partners.DELETE("/:id", partnerH.Delete)
}
// 入库
stockIn := api.Group("/stock-in")
{
stockIn.GET("/orders", stockInH.List)
stockIn.GET("/summary", stockInH.Summary)
stockIn.GET("/orders/:id", stockInH.Get)
stockIn.POST("/orders", stockInH.Create)
stockIn.PUT("/orders/:id", stockInH.Update)
stockIn.DELETE("/orders/:id", stockInH.Delete)
stockIn.PUT("/orders/:id/submit", stockInH.Submit)
stockIn.PUT("/orders/:id/approve", stockInH.Approve)
stockIn.PUT("/orders/:id/reject", stockInH.Reject)
// 撤回(审核中→草稿):管理员/超管任意单,操作员限本人单(handler 内判权)
stockIn.PUT("/orders/:id/withdraw", stockInH.Withdraw)
// 退单(已审核单退货,库存删除+冲应付):仅管理员/超管(service 内判权)
stockIn.POST("/orders/:id/return", stockInH.Return)
// 确认进价(暂估0价→真实价,前向补偿):仅管理员/超管(service 内判权)
stockIn.POST("/orders/:id/confirm-cost", stockInH.ConfirmCost)
}
// 出库
stockOut := api.Group("/stock-out")
{
stockOut.GET("/orders", stockOutH.List)
stockOut.GET("/summary", stockOutH.Summary)
stockOut.GET("/orders/:id", stockOutH.Get)
stockOut.POST("/orders", stockOutH.Create)
stockOut.PUT("/orders/:id", stockOutH.Update)
stockOut.DELETE("/orders/:id", stockOutH.Delete)
stockOut.PUT("/orders/:id/submit", stockOutH.Submit)
stockOut.PUT("/orders/:id/approve", stockOutH.Approve)
stockOut.PUT("/orders/:id/reject", stockOutH.Reject)
// 撤回(审核中→草稿):管理员/超管任意单,操作员限本人单(handler 内判权)
stockOut.PUT("/orders/:id/withdraw", stockOutH.Withdraw)
// 退单(已审核单退货,库存加回+冲应收):管理员/超管 或 本人(service 内判权)
stockOut.POST("/orders/:id/return", stockOutH.Return)
// 确认售价(先出后定价,重算应收):仅管理员/超管(service 内判权)
stockOut.POST("/orders/:id/confirm-sale", stockOutH.ConfirmSale)
}
// 库存
inventory := api.Group("/inventory")
{
inventory.GET("", inventoryH.List)
inventory.GET("/summary", inventoryH.Summary)
inventory.GET("/logs", inventoryH.Logs)
inventory.PUT("/:id/remark", inventoryH.UpdateRemark)
inventory.POST("/checks", inventoryH.CreateCheck)
inventory.GET("/checks/:id", inventoryH.GetCheck)
inventory.PUT("/checks/:id/complete", inventoryH.CompleteCheck)
}
// 用户管理(仅管理员)
users := api.Group("/users")
users.Use(middleware.AdminOnly())
{
users.GET("", userH.List)
users.POST("", userH.Create)
users.PUT("/:id", userH.Update)
users.PUT("/:id/reset-password", userH.ResetPassword)
}
// 财务
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("/trend", financeH.Trend)
}
// 酒行信息
shop := api.Group("/shop")
{
shop.GET("/info", shopH.GetInfo)
shop.PUT("/info", middleware.AdminOnly(), shopH.UpdateInfo)
shop.POST("/logo", middleware.AdminOnly(), shopH.UploadLogo)
}
// 意见反馈(文字 + 附图,直接提交后台)
feedback := api.Group("/feedback")
{
feedback.POST("", feedbackH.Submit)
feedback.POST("/images", feedbackH.UploadImage)
}
// 编号规则
numberRules := api.Group("/number-rules")
{
numberRules.GET("", numberRuleH.List)
numberRules.PUT("/:id", numberRuleH.Update)
}
// 导入
imp := api.Group("/import")
{
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)
}
// 基础数据选项(名称/系列/规格)
opts := api.Group("/product-options")
{
opts.GET("/names", productOptH.ListNames)
opts.POST("/names", productOptH.CreateName)
opts.PUT("/names/:id", productOptH.UpdateName)
opts.DELETE("/names/:id", productOptH.DeleteName)
opts.GET("/series", productOptH.ListSeries)
opts.POST("/series", productOptH.CreateSeries)
opts.PUT("/series/:id", productOptH.UpdateSeries)
opts.DELETE("/series/:id", productOptH.DeleteSeries)
opts.GET("/specs", productOptH.ListSpecs)
opts.POST("/specs", productOptH.CreateSpec)
opts.PUT("/specs/:id", productOptH.UpdateSpec)
opts.DELETE("/specs/:id", productOptH.DeleteSpec)
// 香型 / 分类字典(ProductCategory
opts.GET("/categories", productOptH.ListCategories)
opts.POST("/categories", productOptH.CreateCategory)
opts.PUT("/categories/:id", productOptH.UpdateCategory)
opts.DELETE("/categories/:id", productOptH.DeleteCategory)
// 商品属性字典(产地/保质期/储存方式/描述文档)
opts.GET("/origins", productAttrH.ListOrigins)
opts.POST("/origins", productAttrH.CreateOrigin)
opts.PUT("/origins/:id", productAttrH.UpdateOrigin)
opts.DELETE("/origins/:id", productAttrH.DeleteOrigin)
opts.GET("/shelf-lives", productAttrH.ListShelfLives)
opts.POST("/shelf-lives", productAttrH.CreateShelfLife)
opts.PUT("/shelf-lives/:id", productAttrH.UpdateShelfLife)
opts.DELETE("/shelf-lives/:id", productAttrH.DeleteShelfLife)
opts.GET("/storages", productAttrH.ListStorages)
opts.POST("/storages", productAttrH.CreateStorage)
opts.PUT("/storages/:id", productAttrH.UpdateStorage)
opts.DELETE("/storages/:id", productAttrH.DeleteStorage)
opts.GET("/description-docs", productAttrH.ListDescriptionDocs)
opts.POST("/description-docs", productAttrH.CreateDescriptionDoc)
opts.PUT("/description-docs/:id", productAttrH.UpdateDescriptionDoc)
opts.DELETE("/description-docs/:id", productAttrH.DeleteDescriptionDoc)
}
// 超级管理员专属
superAdmin := api.Group("/admin")
superAdmin.Use(middleware.SuperAdminOnly())
{
superAdmin.POST("/clear-data", adminH.ClearData)
superAdmin.GET("/reconcile", adminH.ReconcileInventory)
superAdmin.GET("/errors", errorReportH.List)
superAdmin.GET("/feedback", feedbackH.List)
superAdmin.PATCH("/feedback/:id", feedbackH.UpdateStatus)
}
}
}