package router import ( "github.com/gin-gonic/gin" "gorm.io/gorm" "github.com/wangjia/pay/config" "github.com/wangjia/pay/internal/channel" "github.com/wangjia/pay/internal/gateway" "github.com/wangjia/pay/internal/handler" "github.com/wangjia/pay/internal/middleware" "github.com/wangjia/pay/internal/service" ) // Setup 装配路由。返回 OrderService 供 main 启动查单兜底任务。 func Setup(r *gin.Engine, db *gorm.DB, reg *channel.Registry) *service.OrderService { orderSvc := service.NewOrderService(db, reg, config.C.Server.BaseURL) orderH := handler.NewOrderHandler(orderSvc) pageH := handler.NewPageHandler(db, "web") r.GET("/health", func(c *gin.Context) { c.JSON(200, gin.H{"status": "ok"}) }) // 页面 r.GET("/result", pageH.ResultPage) r.GET("/qrcode", pageH.QRCode) v1 := r.Group("/api/v1") { v1.GET("/products", pageH.ListProducts) v1.POST("/orders", orderH.Create) v1.POST("/orders/qr", orderH.CreateQR) v1.GET("/orders/:out_trade_no", orderH.GetStatus) v1.POST("/notify/alipay", orderH.AlipayNotify) v1.POST("/notify/wechat", orderH.WechatNotify) } return orderSvc } // SetupV2 装配 pay v2 统一网关路由(/api/v2,与旧版同一 /api 前缀风格)。 // 旧 /api/v1 仅为存量支付宝当面付部署保留(路径 POST /api/v1/orders 与 v2 新契约 // 同名不同形,无法在同一前缀下并存);P3 支付宝 adapter 迁入 v2 后整组删除, // 最终对外只剩 /api/v2 一套。 func SetupV2(r *gin.Engine, g *gateway.Gateway) { h := handler.NewGatewayHandler(g) v2 := r.Group("/api/v2") { // per-IP 限流(jiu 反馈波,P3 限流纵深):只挂改状态端点(下单/重试/取消/订阅创建/订阅取消)—— // - callback 不限:来源是渠道服务器,不是终端用户,限了只会误伤渠道重投。 // - GET 查单/查退款/查订阅不限:只读、幂等,是结果页轮询的正常用法,风险远低于 // 下单类写操作;真要限也该给比写操作更宽松的档,这里选择直接不限。 // - /refunds(创建退款)/admin 组不在本次改动范围内(见 config.RateLimit 注释)。 stateLimit := stateChangeRateLimit() v2.POST("/orders", stateLimit, h.CreateOrder) v2.POST("/subscriptions", stateLimit, h.CreateSubscription) v2.GET("/subscriptions/:sub_id", h.GetSubscription) v2.POST("/subscriptions/:sub_id/cancel", stateLimit, h.CancelSubscription) v2.GET("/orders/:order_no", h.GetStatus) v2.POST("/orders/:order_no/retry", stateLimit, h.Retry) v2.POST("/orders/:order_no/cancel", stateLimit, h.Cancel) v2.POST("/callback/:method", h.Callback) v2.POST("/refunds", h.CreateRefund) v2.GET("/refunds/:refund_id", h.GetRefund) } // 人工退款闭环:独立 admin 组(env PAY_ADMIN_TOKEN 保护),路径与 v2/refunds 分离避免路由冲突。 admin := r.Group("/api/v2/admin") { admin.GET("/manual-refunds", h.ListManualRefunds) admin.POST("/refunds/:refund_id/complete", h.CompleteRefund) } } // stateChangeRateLimit 按 config.C.RateLimit 装配限流中间件;disabled=true 时放行一切 // (no-op 中间件,不是跳过注册——保持路由树形状稳定,便于测试/观测)。每次 SetupV2 调用 // 都构造一个新的 IPRateLimiter 实例(桶状态不跨多次 SetupV2 共享),这也让测试里反复 // 建引擎彼此互不影响。 func stateChangeRateLimit() gin.HandlerFunc { if config.C.RateLimit.Disabled { return func(c *gin.Context) { c.Next() } } return middleware.NewIPRateLimiter(config.C.RateLimit.RequestsPerMin).Gin() }