按"锁单优先、支付从容"放宽支付时序,发布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:
yml2213
2026-10-03 19:43:56 +08:00
parent 87f9c7994d
commit 67f0b28ab6
5 changed files with 48 additions and 16 deletions
@@ -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>
+1 -1
View File
@@ -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"
+20 -7
View File
@@ -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 秒才结算,用次数上限会在
+23 -5
View File
@@ -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
View File
@@ -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"
}
}