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
+7 -2
View File
@@ -9,13 +9,18 @@ import (
)
// NewOutTradeNo 生成全局唯一的商户订单号:<code>-<时间>-<随机>,控制在 64 字符内。
// 例:yanmei-20260624153012-a1b2c3d4
// 例:yanmei-20260624153012-a1b2c3d4e5f6a7b8
//
// 随机部分 16 hex(64 位)——原 8 hex(32 位)在高并发/多实例下生日碰撞概率偏高
// (jiu 反馈波加固,2026-07)。最长 code(16)+"-"(1)+ts(14)+"-"(1)+suffix(16) = 48 字符,
// 仍在 DB out_trade_no 列 size:64 与支付宝 out_trade_no ≤64 字符上限内。v1/v2 共用本函数,
// 加长向后兼容(旧号仍可查、新号不再变短)。
func NewOutTradeNo(merchantCode string) string {
code := merchantCode
if len(code) > 16 {
code = code[:16]
}
ts := time.Now().Format("20060102150405")
suffix := strings.ReplaceAll(uuid.NewString(), "-", "")[:8]
suffix := strings.ReplaceAll(uuid.NewString(), "-", "")[:16]
return fmt.Sprintf("%s-%s-%s", code, ts, suffix)
}
+42
View File
@@ -0,0 +1,42 @@
package util_test
import (
"strings"
"testing"
"github.com/wangjia/pay/internal/util"
)
// jiu 反馈波:随机部分 8→16 hex(64 位)加固防生日碰撞;断言新长度/格式,
// 并确认仍在 DB out_trade_no 列 size:64 与支付宝 out_trade_no ≤64 字符上限内。
func TestNewOutTradeNoSuffixLength(t *testing.T) {
no := util.NewOutTradeNo("yanmei")
parts := strings.Split(no, "-")
if len(parts) != 3 {
t.Fatalf("out_trade_no = %q, want 3 段(code-ts-suffix)", no)
}
suffix := parts[2]
if len(suffix) != 16 {
t.Fatalf("随机后缀长度 = %d, want 16", len(suffix))
}
if len(no) > 64 {
t.Fatalf("out_trade_no 长度 = %d, want <= 64", len(no))
}
}
// merchantCode 拉满 16 字符时,总长仍须 <= 64(alipay out_trade_no 上限)。
func TestNewOutTradeNoMaxLength(t *testing.T) {
no := util.NewOutTradeNo("0123456789abcdefEXTRA") // >16 字符,函数内部会截断到 16
if len(no) > 64 {
t.Fatalf("out_trade_no 长度 = %d, want <= 64: %q", len(no), no)
}
}
// 两次生成不应重复(随机后缀 + 时间戳兜底)。
func TestNewOutTradeNoUnique(t *testing.T) {
a := util.NewOutTradeNo("pay")
b := util.NewOutTradeNo("pay")
if a == b {
t.Fatalf("两次生成的 out_trade_no 相同: %q", a)
}
}