diff --git a/frontend/src/components/modal/SettingsModal.vue b/frontend/src/components/modal/SettingsModal.vue
index 17d5b37..2c2f110 100644
--- a/frontend/src/components/modal/SettingsModal.vue
+++ b/frontend/src/components/modal/SettingsModal.vue
@@ -246,7 +246,8 @@ defineExpose({ open, close })
- 抢购窗口:75 秒;窗口内每个账号各自独立重试,互不阻塞
- 默认:阶段 2 为 200ms,阶段 3 为 500ms,其余阶段 1000ms
- - 已锁单的订单即使窗口结束也会把支付重试完(最长 120 秒)
+ - 锁单优先:锁到即占住库存;支付晚几秒无损失,所以支付重试刻意放缓
+ - 已锁单的订单即使窗口结束也会把支付重试完(最长 180 秒)
- 首发请求:第一个账号立即发出,其余按几毫秒步长错开,整批不超过 0.3 秒
- "活动火爆/网络繁忙"是请求级限流(同一秒别的账号仍能锁单),会继续重试
- "库存已耗完"也不提前停止:别人锁单未支付时库存会回流,会降频探测到窗口结束
diff --git a/internal/constants/constants.go b/internal/constants/constants.go
index 90a3b11..22049ef 100644
--- a/internal/constants/constants.go
+++ b/internal/constants/constants.go
@@ -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"
diff --git a/internal/seckill/attempt.go b/internal/seckill/attempt.go
index 30cb3b4..662a4a5 100644
--- a/internal/seckill/attempt.go
+++ b/internal/seckill/attempt.go
@@ -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 秒才结算,用次数上限会在
diff --git a/internal/seckill/scheduler_test.go b/internal/seckill/scheduler_test.go
index a69d696..6dbc27f 100644
--- a/internal/seckill/scheduler_test.go
+++ b/internal/seckill/scheduler_test.go
@@ -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)
}
}
diff --git a/wails.json b/wails.json
index ae7d2af..c5771b3 100644
--- a/wails.json
+++ b/wails.json
@@ -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"
}
}