a6b068b070
照 alipay adapter 的 RSA + redirect/qr + 表单请求模式实现哪吒(nzzf.org)聚合支付: SHA256WithRSA + Base64 签名(商户私钥出站签名/平台公钥验响应及回调),Create 走 POST /api/pay/create,VerifyCallback 处理 GET 异步通知(handler 层新增 Query 透传 + nezha 专属纯文本 success/fail 回复),Query 走 POST /api/pay/query。BuildRegistry 按凭证齐备与否决策注册,缺凭证只 log 跳过不 fatal。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013nMthbVEmQquxBRKb9Fj8u
85 lines
3.6 KiB
Go
85 lines
3.6 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)
|
|
// GET 同一 handler:部分渠道(如 nezha)异步通知走 GET query string,不是 POST body。
|
|
v2.GET("/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()
|
|
}
|