按"锁单优先、支付从容"放宽支付时序,发布0.3.4
设计前提(此前被我自己违反过一次):锁单一旦成功就占住了库存(斗鱼锁单有效期 300 秒), 支付晚几秒没有任何损失,所以支付环节不该抢时间——斗鱼对"锁单→支付"有最短间隔, 打得太密只会收到"您点击太快"、把账号往风控上推。 - 首次支付等待 2.3-3.0s → 2.5-3.5s,给实测阈值 (1.68s, 2.1s] 留更足余量 - 支付退避阶梯全面放缓(不再追求快重试): 在途 300ms→1s 改为 500ms→2s;频控 800ms→2s 改为 1.5s→4s;其它 400ms→1.2s 改为 800ms→2.5s - 单笔支付总预算 120s → 180s:仍远小于锁单 300 秒有效期,但足够扛住两次 10-03 级别的 服务端堵塞,同时把请求数减半 - 把这条设计前提写进 payLockedOrder 与支付阶梯的注释,避免以后又被当成"浪费"优化掉 - 新增 TestFirstPayDelayStaysAboveServerMinimum:断言首次支付等待不低于实测阈值 2 秒、 等待区间合法、总预算大于首次等待上限 - 设置页补充说明:锁单优先、支付晚几秒无损失,以及支付重试上限改为 180 秒 确认既有架构已经满足该前提(无需改动):每账号独立循环,一个账号在支付时其它账号继续锁单; 并发额度只按单次请求发放,支付等待期间不占额度;75 秒窗口只限制发起新锁单,不限制支付; 已锁单的账号不再发起新锁单以免重复下单。
This commit is contained in:
@@ -246,7 +246,8 @@ defineExpose({ open, close })
|
||||
<ul class="info-list">
|
||||
<li>抢购窗口:<strong>75 秒</strong>;窗口内每个账号各自独立重试,互不阻塞</li>
|
||||
<li>默认:阶段 2 为 200ms,阶段 3 为 500ms,其余阶段 1000ms</li>
|
||||
<li>已锁单的订单即使窗口结束也会把<strong>支付</strong>重试完(最长 120 秒)</li>
|
||||
<li><strong>锁单优先</strong>:锁到即占住库存;支付晚几秒无损失,所以支付重试刻意放缓</li>
|
||||
<li>已锁单的订单即使窗口结束也会把<strong>支付</strong>重试完(最长 180 秒)</li>
|
||||
<li>首发请求:第一个账号立即发出,其余按几毫秒步长错开,整批不超过 0.3 秒</li>
|
||||
<li>"活动火爆/网络繁忙"是请求级限流(同一秒别的账号仍能锁单),会继续重试</li>
|
||||
<li>"库存已耗完"也不提前停止:别人锁单未支付时库存会回流,会降频探测到窗口结束</li>
|
||||
|
||||
@@ -3,7 +3,7 @@ package constants
|
||||
import "time"
|
||||
|
||||
// Version 应用版本号,与 wails.json version 同步。
|
||||
const Version = "0.3.3"
|
||||
const Version = "0.3.4"
|
||||
|
||||
// BuildTime 构建时间,由编译时 ldflags 注入。
|
||||
var BuildTime = "unknown"
|
||||
|
||||
@@ -31,8 +31,11 @@ const (
|
||||
// 整体卡到 18:01:07 才恢复,137 单全在那时结算(锁单在 18:00:00,即服务端
|
||||
// 堵塞约 65 秒),所以预算必须明显大于这个量级——用次数上限会在 ~37 秒就放弃,
|
||||
// 反而丢掉旧版能确认的成功单。真正放弃前一律如实报"结果未知"。
|
||||
//
|
||||
// 既然"锁单优先、支付从容",预算也不必卡太紧:180 秒仍远小于锁单 300 秒有效期,
|
||||
// 但足够扛住两次 10-03 级别的服务端堵塞。
|
||||
// 声明为变量是为了让测试能缩短。
|
||||
var maxPayDuration = 120 * time.Second
|
||||
var maxPayDuration = 180 * time.Second
|
||||
|
||||
// retryLadder 是一类错误的重试退避阶梯:base 起跳、逐次翻倍、limit 封顶。
|
||||
type retryLadder struct {
|
||||
@@ -42,10 +45,15 @@ type retryLadder struct {
|
||||
|
||||
// 支付退避阶梯。订单已受理但结果未定时必须快重试,只有明确频控才退避稍长。
|
||||
// 声明为变量是为了让测试能缩短。
|
||||
// 支付退避阶梯。
|
||||
//
|
||||
// 设计前提:**锁单优先,支付从容**。锁单一旦成功就占住了库存(斗鱼锁单有效期 300 秒),
|
||||
// 支付晚几秒没有任何损失,所以这里刻意不做"快重试"——斗鱼对锁单→支付有最短间隔,
|
||||
// 打得太密只会收到"您点击太快"、把账号往风控上推。宁可每次多等一两秒。
|
||||
var (
|
||||
payRetryInFlight = retryLadder{base: 300 * time.Millisecond, limit: time.Second}
|
||||
payRetryRate = retryLadder{base: 800 * time.Millisecond, limit: 2 * time.Second}
|
||||
payRetryOther = retryLadder{base: 400 * time.Millisecond, limit: 1200 * time.Millisecond}
|
||||
payRetryInFlight = retryLadder{base: 500 * time.Millisecond, limit: 2 * time.Second}
|
||||
payRetryRate = retryLadder{base: 1500 * time.Millisecond, limit: 4 * time.Second}
|
||||
payRetryOther = retryLadder{base: 800 * time.Millisecond, limit: 2500 * time.Millisecond}
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -54,8 +62,8 @@ var (
|
||||
// "您点击太快,请稍等再试"拒绝,而 +2.56s/+3.30s 发起的都成功;19:20 那次
|
||||
// 在 +2.1s~+3.9s 发起的 6 笔全部一次通过。阈值落在 (1.68s, 2.1s] 之间,
|
||||
// 这里取 2.3-3.0 秒留出余量:抢在 2 秒前支付只会白挨拒绝、多花时间。
|
||||
peaceLockPayMin = 2300 * time.Millisecond
|
||||
peaceLockPayMax = 3000 * time.Millisecond
|
||||
peaceLockPayMin = 2500 * time.Millisecond
|
||||
peaceLockPayMax = 3500 * time.Millisecond
|
||||
)
|
||||
|
||||
// 斗鱼订单接口错误码。
|
||||
@@ -224,8 +232,13 @@ func (s *Scheduler) lockExchangeOrder(cli *client.DouyuClient, attempt types.Exc
|
||||
|
||||
// payLockedOrder 把一笔已锁定的订单支付到成功。
|
||||
//
|
||||
// 设计前提:**锁单优先,支付从容**。锁单成功即占住库存(有效期 300 秒),
|
||||
// 支付晚几秒无损失,所以这里不追求快——斗鱼对"锁单→支付"有最短间隔
|
||||
// (实测阈值落在 (1.68s, 2.1s]),打太密只会拿到"您点击太快"并把账号往风控推。
|
||||
// 只有"结果不确定"时才需要频繁确认。
|
||||
//
|
||||
// 支付重试针对同一个 orderID,是幂等的(斗鱼用「订单已支付,请勿重复支付」
|
||||
// 兜底),因此可以快重试,成本只有一次几十毫秒的请求。
|
||||
// 兜底)。
|
||||
//
|
||||
// 只受三道闸约束:停止指令、maxPayDuration 时间预算、锁单临近过期(285 秒)。
|
||||
// 刻意不设次数上限——抢购当天服务端堵塞了约 65 秒才结算,用次数上限会在
|
||||
|
||||
@@ -139,17 +139,35 @@ func TestOrderErrorClassification(t *testing.T) {
|
||||
func TestPayRetryDelayStaysShort(t *testing.T) {
|
||||
for try := 1; try <= 500; try++ {
|
||||
inFlight := payRetryDelay(orderErrInFlight, try)
|
||||
if inFlight > 1300*time.Millisecond {
|
||||
if inFlight > 2600*time.Millisecond {
|
||||
t.Fatalf("在途重试第%d次退避 %v,太长了", try, inFlight)
|
||||
}
|
||||
rateLimited := payRetryDelay(orderErrRateLimited, try)
|
||||
if rateLimited > 2600*time.Millisecond {
|
||||
if rateLimited > 5100*time.Millisecond {
|
||||
t.Fatalf("频控重试第%d次退避 %v,太长了", try, rateLimited)
|
||||
}
|
||||
}
|
||||
// 首次在途重试必须在 200-500ms 区间(用户选定的快重试档位)。
|
||||
if got := payRetryDelay(orderErrInFlight, 1); got < 300*time.Millisecond || got > 400*time.Millisecond {
|
||||
t.Fatalf("首次在途重试 = %v, want 300ms±抖动", got)
|
||||
// 锁单优先、支付从容:首次在途重试按 500ms 起跳(不再是抢时间的 300ms)。
|
||||
if got := payRetryDelay(orderErrInFlight, 1); got < 500*time.Millisecond || got > 650*time.Millisecond {
|
||||
t.Fatalf("首次在途重试 = %v, want 500ms±抖动", got)
|
||||
}
|
||||
}
|
||||
|
||||
// TestFirstPayDelayStaysAboveServerMinimum 守住"锁单优先、支付从容"这条设计前提。
|
||||
//
|
||||
// 实测(2026-10-03 两次模拟):锁单后 +0.50s/+0.60s/+1.37s/+1.68s 发起的支付
|
||||
// 一律被"您点击太快,请稍等再试"拒绝,+2.1s 以上发起的全部一次通过。
|
||||
// 0.3.1 曾把等待压到 0.3-0.7 秒,结果 6 个号各白挨两次拒绝、反而多花 2.7 秒。
|
||||
func TestFirstPayDelayStaysAboveServerMinimum(t *testing.T) {
|
||||
const measuredMinimum = 2 * time.Second
|
||||
if peaceLockPayMin < measuredMinimum {
|
||||
t.Fatalf("首次支付等待下限 = %v,必须不低于实测阈值 %v", peaceLockPayMin, measuredMinimum)
|
||||
}
|
||||
if peaceLockPayMax < peaceLockPayMin {
|
||||
t.Fatalf("支付等待区间非法: [%v, %v]", peaceLockPayMin, peaceLockPayMax)
|
||||
}
|
||||
if maxPayDuration <= peaceLockPayMax {
|
||||
t.Fatalf("支付总预算 %v 必须大于首次等待上限 %v", maxPayDuration, peaceLockPayMax)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -2,7 +2,7 @@
|
||||
"$schema": "https://wails.io/schemas/config.v2.json",
|
||||
"name": "douyu_go",
|
||||
"outputfilename": "douyu_go",
|
||||
"version": "0.3.3",
|
||||
"version": "0.3.4",
|
||||
"frontend:dir": "frontend",
|
||||
"frontend:install": "npm install",
|
||||
"frontend:build": "npm run build",
|
||||
@@ -15,6 +15,6 @@
|
||||
"email": "yml@example.com"
|
||||
},
|
||||
"info": {
|
||||
"productVersion": "0.3.3"
|
||||
"productVersion": "0.3.4"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user