Files
sundynix-agentix/sundynix-gateway/internal/handler/payment_reconcile_test.go
T
Blizzard 70b0419387 fix(payment): 支付轮询链路收口——查单节流 + 前端轮询重写
后端:
- 主动查单加最小间隔(5s)。此前 BillingOrderStatus 见 pending 就直连微信查单,
  而前端 2.5s 轮一次 —— 单笔订单在 30min TTL 内可打出约 720 次渠道调用,
  微信侧有频控,并发用户一多先被限流的是我们自己。回调才是入账主路径,
  查单只是兜底,节流最坏只让在场用户多等 5s。
- 标记随订单落终态清除,并在补偿定时器每轮 prune 掉超 TTL 的残留
  (用户扫码前就关弹窗的订单不会再被轮询,其标记无人回收)。
- 下单响应补 expires_at:二维码有效期由服务端 orderTTL 说了算,
  前端硬编码一份迟早漂移。

前端(sundynix-web):
- orderStatus 不再丢掉 warn。服务端在「已付但金额与订单不符」时不入账、
  挂起人工核对,订单一直停在 pending —— 丢掉 warn 用户就会一直等一个
  永远不会来的结果。现在弹窗显式告警并给出订单号。
- setInterval → 递归 setTimeout:请求慢于间隔时 setInterval 会把请求摞起来,
  对「每次可能触发渠道查单」的端点尤其糟。
- 标签页切走时暂停轮询(用户扫完码要切去微信 App),切回前台立刻补查一次。
- 连续失败指数退避至 15s 封顶,断网时不再定频猛打。
- 二维码倒计时 + 失效遮罩,过期不再让用户扫一个必然失败的码。
- 明说「关掉也不会丢钱,到账会自动补上」——掉单补偿定时器本就兜底,
  但此前 UI 没讲,用户只能守着弹窗。
- 关闭弹窗一律刷新余额:用户可能在关闭前一刻付款、状态刚落地。

测试:新增 handler 包首个测试,钉住节流与标记回收行为(-race 通过)。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 12:46:40 +08:00

53 lines
1.5 KiB
Go

package handler
import (
"testing"
"time"
)
// 查单节流是限制「我们打微信查单接口」频率的唯一闸门:前端支付弹窗 2.5s 轮一次,
// 没有它的话单笔订单在 30min TTL 内能打出数百次渠道调用。这里把行为钉死。
func TestAllowQuery_Throttle(t *testing.T) {
const id = "test-order-throttle"
defer forgetQuery(id)
if !allowQuery(id) {
t.Fatal("首次查单必须放行——否则用户扫码后要白等一个间隔才确认")
}
if allowQuery(id) {
t.Fatal("间隔内的第二次应被拦下")
}
// 间隔过后重新放行
lastQuery.Store(id, time.Now().Add(-minQueryInterval-time.Second))
if !allowQuery(id) {
t.Fatal("超过 minQueryInterval 后应重新放行")
}
}
// 订单落终态后标记要清掉,否则同一订单号(或长期挂着的 pending)会把 map 撑大。
func TestForgetQuery_And_Prune(t *testing.T) {
const id = "test-order-forget"
allowQuery(id)
forgetQuery(id)
if _, ok := lastQuery.Load(id); ok {
t.Fatal("forgetQuery 应删除标记")
}
// 用户扫码前就关掉弹窗的订单不会再被轮询,标记无人清理 —— 靠定时 prune 回收
const stale = "test-order-stale"
lastQuery.Store(stale, time.Now().Add(-orderTTL-time.Minute))
const fresh = "test-order-fresh"
lastQuery.Store(fresh, time.Now())
defer forgetQuery(fresh)
pruneQueryMarks()
if _, ok := lastQuery.Load(stale); ok {
t.Fatal("超过 orderTTL 的陈旧标记应被回收")
}
if _, ok := lastQuery.Load(fresh); !ok {
t.Fatal("仍在有效期内的标记不该被误删")
}
}