fix(v2): jiu 反馈波——端型透传(wap/page)+ alipay qr(当面付移植)+ 单号增熵/挡板限流

1. CreateOrderInput/RetryOrder 加 Metadata 通道,handler 白名单过滤(is_mobile/render)
   后原样传给 provider.CreateRequest;alipay adapter 据 is_mobile 选 wap/page。
2. alipay adapter 移植 v1 当面付(TradePreCreate):Metadata["render"]=="qr" → 二维码
   render_type,payload={qr_content,display_amount,currency},默认 2 小时窗口。
3. NewOutTradeNo 随机部分 8→16 hex 防生日碰撞(仍 <=64 字符,合规 DB 列/支付宝上限);
   v2 改状态端点(下单/重试/取消)加 per-IP 内存令牌桶限流,默认开 30/min,
   config.rate_limit.disabled 可关;callback/GET 查询不限。

顺带修:internal/reconcile/sync_test.go 的 gateway.New 调用漏传 refunds store,
预先存在的编译期回归(与本次改动无关,但挡住 go test ./... 全绿,一并修掉)。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013nMthbVEmQquxBRKb9Fj8u
This commit is contained in:
wangjia
2026-07-10 18:40:15 +08:00
parent 7b90c24a46
commit 8051b0fb16
15 changed files with 698 additions and 17 deletions
+21 -3
View File
@@ -8,6 +8,7 @@ import (
"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"
)
@@ -44,10 +45,16 @@ func SetupV2(r *gin.Engine, g *gateway.Gateway) {
h := handler.NewGatewayHandler(g)
v2 := r.Group("/api/v2")
{
v2.POST("/orders", h.CreateOrder)
// per-IP 限流(jiu 反馈波,P3 限流纵深):只挂改状态端点(下单/重试/取消)——
// - callback 不限:来源是渠道服务器,不是终端用户,限了只会误伤渠道重投。
// - GET 查单/查退款不限:只读、幂等,是结果页轮询的正常用法,风险远低于
// 下单类写操作;真要限也该给比写操作更宽松的档,这里选择直接不限。
// - /refunds(创建退款)/admin 组不在本次改动范围内(见 config.RateLimit 注释)。
stateLimit := stateChangeRateLimit()
v2.POST("/orders", stateLimit, h.CreateOrder)
v2.GET("/orders/:order_no", h.GetStatus)
v2.POST("/orders/:order_no/retry", h.Retry)
v2.POST("/orders/:order_no/cancel", h.Cancel)
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)
@@ -59,3 +66,14 @@ func SetupV2(r *gin.Engine, g *gateway.Gateway) {
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()
}