Files
pay/internal/router/router.go
T
wangjia da8bbefe2e merge: P8 订阅/recurring + 拒付 chargeback 并入(订阅生命周期/续费/取消/past_due/chargeback/事件集收口)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013nMthbVEmQquxBRKb9Fj8u

# Conflicts:
#	internal/model/testdb.go
#	internal/provider/provider.go
#	internal/router/router.go
#	internal/store/order_query_test.go
#	main.go
2026-07-10 19:43:32 +08:00

83 lines
3.5 KiB
Go

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()
}