收紧重试退避并把首发请求改成立即发出,发布0.3.1
- 退避上限从 5 秒压到 1.5 秒(含抖动最长约 1.9 秒),连续失败放大倍数 8→4,抖动 50%→25%: 2026-10-03 实测库存只活了约 1.1 秒(PC2 四个实例 18:00:00.171~18:00:01.101 一直能锁到 同一个礼包),而 PC1 默认实例的 31 个号在 18:00:00.225 失败后一直睡到 18:01:09.379, 唯一有货的那段窗口被退避吃掉;超过 2 秒的等待都可能正好错过库存回流 - 首发请求不再随机等 150-700 毫秒,改为"第 0 个账号立即发出 + 其余按 8ms 步长错开", 整批铺开不超过 300 毫秒(账号再多也不会拖长)。定时器本来就提前约 100ms 触发以补偿 RTT, 再抖几百毫秒等于把这个提前量整个抵消,请求实际落到 t+150ms~t+650ms - 锁单成功到首次支付的等待从 2-4 秒压到 300-700 毫秒:这段纯属把关键路径上的时间白送出去, "订单操作进行中"现在按"已受理、结果未定"快重试,不需要靠等待规避 - 支付退避上限同步收紧:频控 3s→2s,其它 2s→1.2s(在途仍为 300ms→1s) - 新增 TestRetryDelayNeverBacksOffLong:遍历 75 秒 × 40 次连续失败,断言任何一次退避都 小于 2 秒,并覆盖"重试间隔策略被配到 5000ms"的极端配置 - 新增 TestFirstWaveStartsImmediately:单账号步长必须为 0、位次从 0 递增、 2/34/100/500/2000 个账号下步长 ≤8ms 且整批铺开 ≤300ms - 设置页文案同步:退避"任何一次等待都不超过约 2 秒"、首发错开说明
This commit is contained in:
@@ -233,7 +233,7 @@ defineExpose({ open, close })
|
||||
</div>
|
||||
<div class="form-hint">
|
||||
按经过秒数从小到大排列,控制精英/电竞手册的<strong>锁单</strong>重试节奏;
|
||||
同一账号连续失败会在此基础上成倍放大(最长 5 秒)
|
||||
同一账号连续失败会在此基础上成倍放大,但<strong>任何一次等待都不超过约 2 秒</strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -243,6 +243,7 @@ defineExpose({ open, close })
|
||||
<li>抢购窗口:<strong>75 秒</strong>;窗口内每个账号各自独立重试,互不阻塞</li>
|
||||
<li>默认:阶段 2 为 200ms,阶段 3 为 500ms,其余阶段 1000ms</li>
|
||||
<li>已锁单的订单即使窗口结束也会把<strong>支付</strong>重试完(最长 120 秒)</li>
|
||||
<li>首发请求:第一个账号立即发出,其余按几毫秒步长错开,整批不超过 0.3 秒</li>
|
||||
<li>"活动火爆/网络繁忙"是请求级限流(同一秒别的账号仍能锁单),会继续重试</li>
|
||||
<li>"库存已耗完"也不提前停止:别人锁单未支付时库存会回流,会降频探测到窗口结束</li>
|
||||
</ul>
|
||||
|
||||
@@ -3,7 +3,7 @@ package constants
|
||||
import "time"
|
||||
|
||||
// Version 应用版本号,与 wails.json version 同步。
|
||||
const Version = "0.3.0"
|
||||
const Version = "0.3.1"
|
||||
|
||||
// BuildTime 构建时间,由编译时 ldflags 注入。
|
||||
var BuildTime = "unknown"
|
||||
|
||||
@@ -44,15 +44,16 @@ type retryLadder struct {
|
||||
// 声明为变量是为了让测试能缩短。
|
||||
var (
|
||||
payRetryInFlight = retryLadder{base: 300 * time.Millisecond, limit: time.Second}
|
||||
payRetryRate = retryLadder{base: time.Second, limit: 3 * time.Second}
|
||||
payRetryOther = retryLadder{base: 500 * time.Millisecond, limit: 2 * time.Second}
|
||||
payRetryRate = retryLadder{base: 800 * time.Millisecond, limit: 2 * time.Second}
|
||||
payRetryOther = retryLadder{base: 400 * time.Millisecond, limit: 1200 * time.Millisecond}
|
||||
)
|
||||
|
||||
var (
|
||||
peacePreLockMin = 300 * time.Millisecond
|
||||
peacePreLockMax = 1200 * time.Millisecond
|
||||
peaceLockPayMin = 2 * time.Second
|
||||
peaceLockPayMax = 4 * time.Second
|
||||
// 锁单成功后到第一次支付的等待。原来是 2-4 秒,纯属把关键路径上的时间
|
||||
// 白送出去——"订单操作进行中"现在按"已受理、结果未定"快重试处理,
|
||||
// 不需要靠等待来规避。
|
||||
peaceLockPayMin = 300 * time.Millisecond
|
||||
peaceLockPayMax = 700 * time.Millisecond
|
||||
)
|
||||
|
||||
// 斗鱼订单接口错误码。
|
||||
@@ -135,9 +136,15 @@ func (s *Scheduler) runPeaceLockPay(attempt types.ExchangeAttempt, ctl *runContr
|
||||
"精英手册锁单支付: 账号=%s 商品=%s(%s) 第%d次",
|
||||
attempt.Account.Name, attempt.Product.CommodityName, attempt.Product.CommodityID, attempt.Index)
|
||||
|
||||
// 首发请求按 300-1200ms 抖动打散;后续重试的间隔由外层节奏计算。
|
||||
// 首发请求立刻发出:第 0 个账号不等待,其余按几毫秒步长错开(整批不超过 0.3 秒)。
|
||||
// 定时器已提前约 100ms 触发以补偿 RTT,这里再抖几百毫秒会把这套提前量抵消掉。
|
||||
if attempt.Index == 1 && ctl != nil {
|
||||
if !ctl.sleep(randomSpan(peacePreLockMin, peacePreLockMax)) {
|
||||
slot, step := ctl.firstWaveSlot()
|
||||
delay := time.Duration(slot) * step
|
||||
if step > 0 {
|
||||
delay += time.Duration(rand.Int63n(int64(step)))
|
||||
}
|
||||
if !ctl.sleep(delay) {
|
||||
out.stopped = ctl.stopped()
|
||||
if out.stopped {
|
||||
out.result.Message = "失败|已收到停止指令"
|
||||
|
||||
@@ -17,26 +17,59 @@ type runControl struct {
|
||||
stop <-chan struct{}
|
||||
limit chan struct{}
|
||||
|
||||
mu sync.Mutex
|
||||
lockReqs int
|
||||
payReqs int
|
||||
exchangeReqs int
|
||||
firstWaveStep time.Duration
|
||||
|
||||
mu sync.Mutex
|
||||
lockReqs int
|
||||
payReqs int
|
||||
exchangeReqs int
|
||||
firstWaveUsed int
|
||||
}
|
||||
|
||||
// newRunControl 创建运行控制块。concurrency 限制同时在途的请求数。
|
||||
func newRunControl(s *Scheduler, start time.Time, deadline time.Time, concurrency int) *runControl {
|
||||
// newRunControl 创建运行控制块。concurrency 限制同时在途的请求数,
|
||||
// accountCount 用来决定首发请求的错开步长。
|
||||
func newRunControl(s *Scheduler, start time.Time, deadline time.Time, concurrency, accountCount int) *runControl {
|
||||
if concurrency <= 0 {
|
||||
concurrency = 1
|
||||
}
|
||||
return &runControl{
|
||||
s: s,
|
||||
start: start,
|
||||
deadline: deadline,
|
||||
stop: s.stopRunChan(),
|
||||
limit: make(chan struct{}, concurrency),
|
||||
s: s,
|
||||
start: start,
|
||||
deadline: deadline,
|
||||
stop: s.stopRunChan(),
|
||||
limit: make(chan struct{}, concurrency),
|
||||
firstWaveStep: firstWaveStepFor(accountCount),
|
||||
}
|
||||
}
|
||||
|
||||
// firstWaveStepFor 计算首发请求之间的错开步长。
|
||||
//
|
||||
// 首发是整场抢购最关键的一波,不能像早期版本那样随机等 150-700ms——定时器本来就
|
||||
// 提前约 100ms 触发以补偿 RTT,再抖几百毫秒等于把提前量整个抵消。
|
||||
// 这里让第 0 个账号立即发出,其余按步长依次错开,整批最多铺开 firstWaveSpread。
|
||||
func firstWaveStepFor(accountCount int) time.Duration {
|
||||
if accountCount <= 1 {
|
||||
return 0
|
||||
}
|
||||
step := firstWaveSpread / time.Duration(accountCount-1)
|
||||
if step > firstWaveMaxStep {
|
||||
step = firstWaveMaxStep
|
||||
}
|
||||
if step < 0 {
|
||||
step = 0
|
||||
}
|
||||
return step
|
||||
}
|
||||
|
||||
// firstWaveSlot 领取本账号的首发错开位次与步长。
|
||||
func (c *runControl) firstWaveSlot() (int, time.Duration) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
slot := c.firstWaveUsed
|
||||
c.firstWaveUsed++
|
||||
return slot, c.firstWaveStep
|
||||
}
|
||||
|
||||
// enter 占用一个并发额度;返回 false 表示任务已被停止。
|
||||
func (c *runControl) enter() bool {
|
||||
select {
|
||||
|
||||
@@ -424,7 +424,7 @@ func (s *Scheduler) RunNow(accountProducts map[string]string) {
|
||||
|
||||
start := time.Now()
|
||||
deadline := start.Add(seckillMaxDuration)
|
||||
ctl := newRunControl(s, start, deadline, settings.concurrency)
|
||||
ctl := newRunControl(s, start, deadline, settings.concurrency, len(tasks))
|
||||
|
||||
s.clearAttemptCache()
|
||||
if settings.seckillMode == types.SeckillModeAggressive {
|
||||
@@ -463,10 +463,22 @@ const (
|
||||
)
|
||||
|
||||
// 每账号重试节奏的上下限与递增倍数。
|
||||
//
|
||||
// 抢购是争分夺秒的,退避不能长:2026-10-03 的日志里,PC2 四个实例在
|
||||
// 18:00:00.171~18:00:01.101 一直能锁到同一个礼包(库存至少活了 1.1 秒),
|
||||
// 而 PC1 默认实例的 31 个号在 18:00:00.225 失败后一直睡到 18:01:09.379——
|
||||
// 唯一有货的那段窗口被退避白白吃掉了。所以这里把任何一次等待都压在 2 秒以内。
|
||||
const (
|
||||
minRetryDelay = 100 * time.Millisecond
|
||||
maxRetryDelay = 5 * time.Second
|
||||
maxRetryEscalation = 8
|
||||
maxRetryDelay = 1500 * time.Millisecond
|
||||
maxRetryEscalation = 4
|
||||
)
|
||||
|
||||
// 首发请求的错开参数:第 0 个账号立即发出,其余按小步长依次错开,
|
||||
// 整批最多铺开 firstWaveSpread(账号再多也不会拖长),单个步长不超过 firstWaveMaxStep。
|
||||
const (
|
||||
firstWaveSpread = 300 * time.Millisecond
|
||||
firstWaveMaxStep = 8 * time.Millisecond
|
||||
)
|
||||
|
||||
// stockOutWarnAfter 是同一账号连续收到"商品已售罄"多少次后,打一条"转为低频探测"
|
||||
@@ -600,7 +612,11 @@ func expiryOutcome(lastStockOut, lastThrottled bool, lastFailure string) (string
|
||||
//
|
||||
// 基准节奏来自配置的"重试间隔策略"(按经过秒数分档)——早期版本把它只用在
|
||||
// 轮与轮之间,单次锁单失败却硬编码睡 30 秒,配置形同虚设。同一账号连续失败时
|
||||
// 再成倍放大,避免单个账号猛打;并叠加抖动,防止所有账号同步重试形成尖峰。
|
||||
// 再成倍放大(最多 ×4),避免单个账号猛打;并叠加 0-25% 抖动,
|
||||
// 防止所有账号同步重试形成尖峰。
|
||||
//
|
||||
// 上限刻意压到 1.5 秒:抢购窗口只有 75 秒,库存随时可能因为别人锁单未支付而回流,
|
||||
// 任何超过 2 秒的等待都可能正好错过它。
|
||||
func retryDelay(stages []types.RetryStage, elapsed time.Duration, failStreak int) time.Duration {
|
||||
base := retryIntervalFor(stages, elapsed)
|
||||
if base <= 0 {
|
||||
@@ -617,7 +633,7 @@ func retryDelay(stages []types.RetryStage, elapsed time.Duration, failStreak int
|
||||
if delay < minRetryDelay {
|
||||
delay = minRetryDelay
|
||||
}
|
||||
return jitterFraction(delay, 2)
|
||||
return jitterFraction(delay, 4)
|
||||
}
|
||||
|
||||
// resultKey 生成结果键。
|
||||
|
||||
@@ -34,14 +34,11 @@ func makeAttempts(names ...string) []types.ExchangeAttempt {
|
||||
// 让测试只关注重试逻辑本身。
|
||||
func speedUpPeace(t *testing.T) {
|
||||
t.Helper()
|
||||
oldPreMin, oldPreMax := peacePreLockMin, peacePreLockMax
|
||||
oldPayMin, oldPayMax := peaceLockPayMin, peaceLockPayMax
|
||||
oldDuration := seckillMaxDuration
|
||||
peacePreLockMin, peacePreLockMax = 0, 0
|
||||
peaceLockPayMin, peaceLockPayMax = 0, 0
|
||||
seckillMaxDuration = 2 * time.Second
|
||||
t.Cleanup(func() {
|
||||
peacePreLockMin, peacePreLockMax = oldPreMin, oldPreMax
|
||||
peaceLockPayMin, peaceLockPayMax = oldPayMin, oldPayMax
|
||||
seckillMaxDuration = oldDuration
|
||||
})
|
||||
@@ -145,7 +142,7 @@ func TestPayRetryDelayStaysShort(t *testing.T) {
|
||||
t.Fatalf("在途重试第%d次退避 %v,太长了", try, inFlight)
|
||||
}
|
||||
rateLimited := payRetryDelay(orderErrRateLimited, try)
|
||||
if rateLimited > 4*time.Second {
|
||||
if rateLimited > 2600*time.Millisecond {
|
||||
t.Fatalf("频控重试第%d次退避 %v,太长了", try, rateLimited)
|
||||
}
|
||||
}
|
||||
@@ -195,7 +192,7 @@ func TestPayKeepsRetryingBeyondOldAttemptCap(t *testing.T) {
|
||||
}
|
||||
})
|
||||
|
||||
ctl := newRunControl(scheduler, time.Now(), time.Now().Add(30*time.Second), 4)
|
||||
ctl := newRunControl(scheduler, time.Now(), time.Now().Add(30*time.Second), 4, 1)
|
||||
out := scheduler.runPeaceLockPay(peaceAttempt("a"), ctl)
|
||||
if !out.result.OK {
|
||||
t.Fatalf("服务端在 %d 次「处理中」之后结算,应当继续重试到成功, result=%#v", inFlightTimes, out.result)
|
||||
@@ -224,7 +221,7 @@ func TestPayGivesUpHonestlyAtBudget(t *testing.T) {
|
||||
}
|
||||
})
|
||||
|
||||
ctl := newRunControl(scheduler, time.Now(), time.Now().Add(30*time.Second), 4)
|
||||
ctl := newRunControl(scheduler, time.Now(), time.Now().Add(30*time.Second), 4, 1)
|
||||
out := scheduler.runPeaceLockPay(peaceAttempt("a"), ctl)
|
||||
if out.result.OK {
|
||||
t.Fatal("预算内不可能成功,不应报成功")
|
||||
@@ -256,8 +253,8 @@ func TestRetryDelayFollowsConfiguredStages(t *testing.T) {
|
||||
{5 * time.Second, 0, 200 * time.Millisecond, 300 * time.Millisecond},
|
||||
{31 * time.Second, 0, time.Second, 1500 * time.Millisecond},
|
||||
// 连续失败成倍放大,但不超过 maxRetryDelay
|
||||
{1 * time.Second, 3, 480 * time.Millisecond, 720 * time.Millisecond},
|
||||
{31 * time.Second, 10, 5 * time.Second, 5*time.Second + 2500*time.Millisecond},
|
||||
{1 * time.Second, 3, 480 * time.Millisecond, 620 * time.Millisecond},
|
||||
{31 * time.Second, 10, 1500 * time.Millisecond, 1900 * time.Millisecond},
|
||||
}
|
||||
for _, c := range cases {
|
||||
got := retryDelay(stages, c.elapsed, c.streak)
|
||||
@@ -271,6 +268,34 @@ func TestRetryDelayFollowsConfiguredStages(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestRetryDelayNeverBacksOffLong 抢购是争分夺秒的:任何一次退避都必须压在
|
||||
// 2 秒以内。2026-10-03 的日志里,库存只活了约 1.1 秒(PC2 四个实例
|
||||
// 18:00:00.171~18:00:01.101 一直在成功锁单),超过 2 秒的等待就可能正好错过库存回流。
|
||||
func TestRetryDelayNeverBacksOffLong(t *testing.T) {
|
||||
stages := []types.RetryStage{
|
||||
{MaxElapsed: 4, Interval: 120},
|
||||
{MaxElapsed: 12, Interval: 200},
|
||||
{MaxElapsed: 30, Interval: 500},
|
||||
{MaxElapsed: 75, Interval: 1000},
|
||||
}
|
||||
worst := time.Duration(0)
|
||||
for elapsed := time.Duration(0); elapsed <= 75*time.Second; elapsed += 250 * time.Millisecond {
|
||||
for streak := 0; streak <= 40; streak++ {
|
||||
if got := retryDelay(stages, elapsed, streak); got > worst {
|
||||
worst = got
|
||||
}
|
||||
}
|
||||
}
|
||||
if worst >= 2*time.Second {
|
||||
t.Fatalf("最大退避 = %v,必须小于 2 秒", worst)
|
||||
}
|
||||
// 也要覆盖用户把阶段间隔调到很大值的极端配置。
|
||||
extreme := []types.RetryStage{{MaxElapsed: 75, Interval: 5000}}
|
||||
if got := retryDelay(extreme, 40*time.Second, 10); got >= 2*time.Second {
|
||||
t.Fatalf("极端配置下退避 = %v,仍必须小于 2 秒", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRetryIntervalFor(t *testing.T) {
|
||||
stages := []types.RetryStage{
|
||||
{MaxElapsed: 4, Interval: 120},
|
||||
@@ -343,7 +368,7 @@ func TestPeacePurchaseLocksThenPaysWithoutChecks(t *testing.T) {
|
||||
}
|
||||
})
|
||||
|
||||
ctl := newRunControl(scheduler, time.Now(), time.Now().Add(time.Minute), 4)
|
||||
ctl := newRunControl(scheduler, time.Now(), time.Now().Add(time.Minute), 4, 1)
|
||||
out := scheduler.runPeaceLockPay(peaceAttempt("a"), ctl)
|
||||
if !out.result.OK || out.result.RoleName != "测试角色" {
|
||||
t.Fatalf("result = %#v", out)
|
||||
@@ -388,7 +413,7 @@ func TestPayInFlightRetriesQuickly(t *testing.T) {
|
||||
}
|
||||
})
|
||||
|
||||
ctl := newRunControl(scheduler, time.Now(), time.Now().Add(30*time.Second), 4)
|
||||
ctl := newRunControl(scheduler, time.Now(), time.Now().Add(30*time.Second), 4, 1)
|
||||
start := time.Now()
|
||||
out := scheduler.runPeaceLockPay(peaceAttempt("a"), ctl)
|
||||
elapsed := time.Since(start)
|
||||
@@ -427,7 +452,7 @@ func TestPayFinishesAfterWindowClosed(t *testing.T) {
|
||||
}
|
||||
})
|
||||
|
||||
ctl := newRunControl(scheduler, time.Now(), time.Now().Add(50*time.Millisecond), 4)
|
||||
ctl := newRunControl(scheduler, time.Now(), time.Now().Add(50*time.Millisecond), 4, 1)
|
||||
out := scheduler.runPeaceLockPay(peaceAttempt("a"), ctl)
|
||||
if !out.result.OK {
|
||||
t.Fatalf("窗口到期后仍应把已锁订单支付完, result=%#v", out.result)
|
||||
@@ -468,7 +493,7 @@ func TestSlowPayDoesNotBlockOtherAccounts(t *testing.T) {
|
||||
})
|
||||
|
||||
start := time.Now()
|
||||
ctl := newRunControl(scheduler, start, start.Add(2500*time.Millisecond), 8)
|
||||
ctl := newRunControl(scheduler, start, start.Add(2500*time.Millisecond), 8, 1)
|
||||
settings := runSettings{
|
||||
concurrency: 8,
|
||||
seckillMode: types.SeckillModeAggressive,
|
||||
@@ -508,7 +533,7 @@ func TestStockOutKeepsProbingToWindowEnd(t *testing.T) {
|
||||
})
|
||||
|
||||
start := time.Now()
|
||||
ctl := newRunControl(scheduler, start, start.Add(1200*time.Millisecond), 4)
|
||||
ctl := newRunControl(scheduler, start, start.Add(1200*time.Millisecond), 4, 1)
|
||||
settings := runSettings{
|
||||
concurrency: 4,
|
||||
seckillMode: types.SeckillModeAggressive,
|
||||
@@ -550,7 +575,7 @@ func TestStockOutAfterThrottleStillReported(t *testing.T) {
|
||||
})
|
||||
|
||||
start := time.Now()
|
||||
ctl := newRunControl(scheduler, start, start.Add(1200*time.Millisecond), 4)
|
||||
ctl := newRunControl(scheduler, start, start.Add(1200*time.Millisecond), 4, 1)
|
||||
settings := runSettings{
|
||||
concurrency: 4,
|
||||
seckillMode: types.SeckillModeAggressive,
|
||||
@@ -585,7 +610,7 @@ func TestThrottledKeepsRetryingToWindowEnd(t *testing.T) {
|
||||
})
|
||||
|
||||
start := time.Now()
|
||||
ctl := newRunControl(scheduler, start, start.Add(1200*time.Millisecond), 4)
|
||||
ctl := newRunControl(scheduler, start, start.Add(1200*time.Millisecond), 4, 1)
|
||||
settings := runSettings{
|
||||
concurrency: 4,
|
||||
seckillMode: types.SeckillModeAggressive,
|
||||
@@ -607,6 +632,40 @@ func TestThrottledKeepsRetryingToWindowEnd(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestFirstWaveStartsImmediately 首发是整场最关键的一波:第 0 个账号必须立即发出
|
||||
// (定时器已提前约 100ms 补偿 RTT,不能再自己抖几百毫秒),其余按小步长错开,
|
||||
// 整批铺开时间有上限。
|
||||
func TestFirstWaveStartsImmediately(t *testing.T) {
|
||||
if got := firstWaveStepFor(1); got != 0 {
|
||||
t.Fatalf("单账号步长 = %v, want 0(必须立即发出)", got)
|
||||
}
|
||||
for _, count := range []int{2, 34, 100, 500, 2000} {
|
||||
step := firstWaveStepFor(count)
|
||||
if step > firstWaveMaxStep {
|
||||
t.Fatalf("账号数 %d 的步长 = %v, 超过上限 %v", count, step, firstWaveMaxStep)
|
||||
}
|
||||
spread := time.Duration(count-1) * step
|
||||
if spread > firstWaveSpread+step {
|
||||
t.Fatalf("账号数 %d 整批铺开 %v, 超过上限 %v", count, spread, firstWaveSpread)
|
||||
}
|
||||
}
|
||||
// 位次必须从 0 开始且逐个递增,保证总有账号在 0 延时首发。
|
||||
s := &Scheduler{}
|
||||
ctl := newRunControl(s, time.Now(), time.Now().Add(time.Minute), 4, 4)
|
||||
for want := 0; want < 4; want++ {
|
||||
got, step := ctl.firstWaveSlot()
|
||||
if got != want {
|
||||
t.Fatalf("第%d个位次 = %d", want, got)
|
||||
}
|
||||
if step <= 0 {
|
||||
t.Fatalf("4 个账号时步长应为正数, got %v", step)
|
||||
}
|
||||
}
|
||||
if got, _ := ctl.firstWaveSlot(); got != 4 {
|
||||
t.Fatalf("位次应继续递增, got %d", got)
|
||||
}
|
||||
}
|
||||
|
||||
// ========== RunNow 端到端 ==========
|
||||
|
||||
func TestRunNowEndToEnd(t *testing.T) {
|
||||
|
||||
+2
-2
@@ -2,7 +2,7 @@
|
||||
"$schema": "https://wails.io/schemas/config.v2.json",
|
||||
"name": "douyu_go",
|
||||
"outputfilename": "douyu_go",
|
||||
"version": "0.3.0",
|
||||
"version": "0.3.1",
|
||||
"frontend:dir": "frontend",
|
||||
"frontend:install": "npm install",
|
||||
"frontend:build": "npm run build",
|
||||
@@ -15,6 +15,6 @@
|
||||
"email": "yml@example.com"
|
||||
},
|
||||
"info": {
|
||||
"productVersion": "0.3.0"
|
||||
"productVersion": "0.3.1"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user