70b0419387
后端: - 主动查单加最小间隔(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>
53 lines
1.5 KiB
Go
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("仍在有效期内的标记不该被误删")
|
|
}
|
|
}
|