+
+
+
+
+
+
+
+
+
+
+
+
+ ms
+
+
{
text-align: center;
}
+.fixed-keyword input { width: 132px; }
+.btn-fixed { border-color: var(--warning); color: var(--warning); }
+.btn-watch { border-color: var(--accent-border); color: var(--accent); }
+.btn-watch.watching { background: var(--accent-soft); }
+
@media (max-width: 1180px) {
.control-strip {
flex-direction: column;
diff --git a/frontend/src/composables/useScheduler.ts b/frontend/src/composables/useScheduler.ts
index c6ea929..f2c08c9 100644
--- a/frontend/src/composables/useScheduler.ts
+++ b/frontend/src/composables/useScheduler.ts
@@ -3,7 +3,7 @@ import { useToast } from './useToast'
import { useAccounts } from './useAccounts'
import { useProducts } from './useProducts'
import { useLogs } from './useLogs'
-import { CancelSchedule, SetSchedule, StartSeckill } from '@wails/go/app/App'
+import { CancelSchedule, SetSchedule, StartFixedSeckill, StartSeckill } from '@wails/go/app/App'
const scheduleActive = ref(false)
const countdownSeconds = ref(0)
@@ -103,6 +103,29 @@ export function useScheduler() {
}
}
+ const startFixed = async (attempts: number, delayMs: number) => {
+ const ap = buildAccountProducts(selectedProductId.value || '')
+ if (!ap) return false
+ resetStatus()
+ clearLocalResults()
+ markSelectedPending('固定抢购中,等待后端结果')
+ setRunState('固定抢购已提交')
+ try {
+ const d = await StartFixedSeckill({ accountProducts: ap, attempts, delayMs })
+ if (d?.success === false) throw new Error(d.error || '提交失败')
+ setRunState(d.msg || '固定抢购已启动')
+ addLocalLog(`固定抢购: ${Object.keys(ap).length} 个账号,${attempts} 次,间隔 ${delayMs}ms`, 'i')
+ showToast('固定抢购已启动', `${Object.keys(ap).length} 个账号`, 'info', 3000)
+ return true
+ } catch (e: any) {
+ setRunState('固定抢购提交失败')
+ markSelectedStatus({ ok: false, message: `提交失败: ${e.message}` })
+ addLocalLog('固定抢购提交失败: ' + e.message, 'e')
+ showToast('提交失败', e.message, 'error')
+ return false
+ }
+ }
+
const cancelSchedule = async () => {
try {
await CancelSchedule()
@@ -158,6 +181,7 @@ export function useScheduler() {
runState,
setRunState,
startNow,
+ startFixed,
startSchedule,
cancelSchedule,
formatRemain,
diff --git a/internal/app/app_seckill.go b/internal/app/app_seckill.go
index 2712db8..d2c4d33 100644
--- a/internal/app/app_seckill.go
+++ b/internal/app/app_seckill.go
@@ -21,6 +21,27 @@ func (a *App) StartSeckill(accountProducts map[string]string) types.MessageResul
return types.MessageResult{Success: true, Msg: "抢购已启动"}
}
+// StartFixedSeckill 启动和平小店固定次数/间隔抢购。
+func (a *App) StartFixedSeckill(req types.FixedSeckillRequest) types.MessageResult {
+ if !a.getClient().Activity.IsXiaodian {
+ return types.MessageResult{Error: "固定次数抢购仅支持和平小店"}
+ }
+ if len(req.AccountProducts) == 0 {
+ return types.MessageResult{Error: "请选择账号和商品"}
+ }
+ if req.Attempts < 1 || req.Attempts > 100 {
+ return types.MessageResult{Error: "抢购次数需在 1-100 之间"}
+ }
+ if req.DelayMs < 0 || req.DelayMs > 60000 {
+ return types.MessageResult{Error: "抢购间隔需在 0-60000 毫秒之间"}
+ }
+ if !a.scheduler.TryStartRun() {
+ return types.MessageResult{Error: "抢购任务正在运行,请勿重复提交"}
+ }
+ go a.scheduler.RunFixed(utils.CloneStringMap(req.AccountProducts), req.Attempts, req.DelayMs)
+ return types.MessageResult{Success: true, Msg: "固定次数抢购已启动"}
+}
+
// SetSchedule 设置定时抢购。
func (a *App) SetSchedule(req types.SeckillRequest) (types.ScheduleResult, error) {
return a.scheduler.ScheduleRun(req)
diff --git a/internal/seckill/scheduler.go b/internal/seckill/scheduler.go
index 68d8456..c6dd62e 100644
--- a/internal/seckill/scheduler.go
+++ b/internal/seckill/scheduler.go
@@ -432,6 +432,75 @@ func (s *Scheduler) RunNow(accountProducts map[string]string) {
len(successAccounts), totalAccounts, len(stoppedAccounts)-len(successAccounts), totalAttempts, utils.RoundDuration(time.Since(startTime)))
}
+// RunFixed 按固定轮数和轮间隔抢购,账号成功后不再参与后续轮次。
+func (s *Scheduler) RunFixed(accountProducts map[string]string, attempts, delayMs int) {
+ defer s.finishRun()
+ s.ClearResults()
+ settings := s.settingsSnapshot()
+ if attempts < 1 {
+ attempts = 1
+ }
+ if attempts > 100 {
+ attempts = 100
+ }
+ if delayMs < 0 {
+ delayMs = 0
+ }
+ if delayMs > 60000 {
+ delayMs = 60000
+ }
+
+ accounts := make([]types.Account, 0, len(accountProducts))
+ for name, commodityID := range accountProducts {
+ cookie, ok := s.AccountCookieByName(name)
+ if !ok {
+ continue
+ }
+ product := s.productForCommodityID(commodityID)
+ if product.CommodityID == "" {
+ product.CommodityID = commodityID
+ }
+ accounts = append(accounts, types.Account{Name: name, Cookie: cookie})
+ }
+ if len(accounts) == 0 {
+ logger.LogEvent("抢购", "warn", "固定次数抢购没有可执行账号")
+ return
+ }
+
+ stopped := make(map[string]bool)
+ start := time.Now()
+ logger.LogEvent("抢购", "info", "固定次数抢购开始: %d 个账号,次数=%d,间隔=%dms", len(accounts), attempts, delayMs)
+ for round := 1; round <= attempts; round++ {
+ roundAttempts := make([]types.ExchangeAttempt, 0, len(accounts))
+ for _, account := range accounts {
+ if stopped[account.Name] {
+ continue
+ }
+ commodityID := accountProducts[account.Name]
+ product := s.productForCommodityID(commodityID)
+ if product.CommodityID == "" {
+ product.CommodityID = commodityID
+ }
+ roundAttempts = append(roundAttempts, types.ExchangeAttempt{Account: account, Product: product, Index: round})
+ }
+ if len(roundAttempts) == 0 {
+ break
+ }
+ if round > 1 && delayMs > 0 {
+ time.Sleep(time.Duration(delayMs) * time.Millisecond)
+ }
+ outcome := s.executeRound(roundAttempts, settings)
+ for name := range outcome.successAccounts {
+ stopped[name] = true
+ }
+ for name := range outcome.stoppedAccounts {
+ stopped[name] = true
+ }
+ logger.LogEvent("抢购", "info", "固定抢购第%d/%d轮完成: 成功=%d,剩余账号=%d", round, attempts, len(outcome.successAccounts), len(accounts)-len(stopped))
+ }
+ logger.LogEvent("抢购", "info", "固定次数抢购结束: 成功=%d/%d,耗时=%v", len(stopped), len(accounts), utils.RoundDuration(time.Since(start)))
+}
+
// filterPendingAttempts 过滤掉已终止账号和已确认无库存的商品。
func (s *Scheduler) filterPendingAttempts(allAttempts []types.ExchangeAttempt, stoppedAccounts, stoppedProducts map[string]string) []types.ExchangeAttempt {
var pending []types.ExchangeAttempt
diff --git a/internal/types/types.go b/internal/types/types.go
index b559ed3..02a8c5b 100644
--- a/internal/types/types.go
+++ b/internal/types/types.go
@@ -106,6 +106,13 @@ type SeckillRequest struct {
TargetTime string `json:"targetTime"`
}
+// FixedSeckillRequest 是和平小店固定次数/间隔抢购请求。
+type FixedSeckillRequest struct {
+ AccountProducts map[string]string `json:"accountProducts"`
+ Attempts int `json:"attempts"`
+ DelayMs int `json:"delayMs"`
+}
+
// APIResponse 斗鱼 API 通用响应。
type APIResponse struct {
Error int `json:"error"`