增加和平小店关键词固定抢购

This commit is contained in:
yml2213
2026-08-08 18:40:53 +08:00
parent 51e91b0483
commit a4fed8befb
5 changed files with 214 additions and 3 deletions
@@ -1,13 +1,22 @@
<script setup lang="ts">
import { computed, ref } from 'vue'
import { computed, onUnmounted, ref, watch } from 'vue'
import { useScheduler } from '@/composables/useScheduler'
import { useAccounts } from '@/composables/useAccounts'
import { useActivity } from '@/composables/useActivity'
import { useProducts } from '@/composables/useProducts'
import { useToast } from '@/composables/useToast'
const { scheduleActive, startNow, startSchedule, cancelSchedule, restoreSchedule } = useScheduler()
const { scheduleActive, startNow, startFixed, startSchedule, cancelSchedule, restoreSchedule } = useScheduler()
const { reloadAccounts, queryAllScore, queryAllXiaodianAssets } = useAccounts()
const { currentActivity } = useActivity()
const isXiaodian = computed(() => currentActivity.value?.id === 'xiaodian')
const { products, selectedProductId, loadProducts, selectProduct } = useProducts()
const { showToast } = useToast()
const fixedKeyword = ref(localStorage.getItem('xiaodianFixedKeyword') || '')
const fixedAttempts = ref(Number(localStorage.getItem('xiaodianFixedAttempts') || 3))
const fixedDelayMs = ref(Number(localStorage.getItem('xiaodianFixedDelayMs') || 500))
const keywordWatching = ref(false)
let keywordTimer: number | null = null
// 从本地缓存恢复定时设置
const { savedTime } = restoreSchedule()
@@ -24,6 +33,61 @@ const queryAssets = () => {
}
queryAllScore()
}
const saveFixedConfig = () => {
localStorage.setItem('xiaodianFixedKeyword', fixedKeyword.value.trim())
localStorage.setItem('xiaodianFixedAttempts', String(fixedAttempts.value))
localStorage.setItem('xiaodianFixedDelayMs', String(fixedDelayMs.value))
}
const startFixedBuy = () => {
saveFixedConfig()
if (!selectedProductId.value) return
startFixed(fixedAttempts.value, fixedDelayMs.value)
}
const toggleKeywordWatch = () => {
keywordWatching.value = !keywordWatching.value
if (keywordWatching.value) {
saveFixedConfig()
if (!fixedKeyword.value.trim()) {
keywordWatching.value = false
showToast('请输入关键词', '设置商品关键词后再开始监控', 'warn')
return
}
void loadProducts(true)
keywordTimer = window.setInterval(async () => {
if (!keywordWatching.value || scheduleActive.value) return
await loadProducts(false)
const keyword = fixedKeyword.value.trim().toLowerCase()
const hit = products.value.find(p => p.commodityName.toLowerCase().includes(keyword))
if (hit) {
keywordWatching.value = false
if (keywordTimer) clearInterval(keywordTimer)
selectProduct(hit.commodityId, hit.commodityName)
await startFixed(fixedAttempts.value, fixedDelayMs.value)
}
}, 3000)
} else if (keywordTimer) {
clearInterval(keywordTimer)
keywordTimer = null
}
}
watch(isXiaodian, (enabled) => {
if (!enabled && keywordWatching.value) {
keywordWatching.value = false
if (keywordTimer) {
clearInterval(keywordTimer)
keywordTimer = null
}
}
})
onUnmounted(() => {
if (keywordTimer) clearInterval(keywordTimer)
})
</script>
<template>
@@ -46,9 +110,30 @@ const queryAssets = () => {
</button>
<button class="btn" @click="reloadAccounts">加载账号</button>
<button class="btn" @click="queryAssets">{{ isXiaodian ? '查询点券/碎片' : '查询积分' }}</button>
<template v-if="isXiaodian">
<button class="btn btn-fixed" @click="startFixedBuy">固定抢购</button>
<button class="btn btn-watch" :class="{ watching: keywordWatching }" @click="toggleKeywordWatch">
{{ keywordWatching ? '停止监控' : '关键词监控' }}
</button>
</template>
</div>
<div class="schedule-bar">
<template v-if="isXiaodian">
<div class="schedule-item fixed-keyword">
<label>上架关键词</label>
<input v-model="fixedKeyword" type="text" placeholder="例如:礼包" @change="saveFixedConfig">
</div>
<div class="schedule-item">
<label>次数</label>
<input v-model.number="fixedAttempts" type="number" min="1" max="100" step="1" @change="saveFixedConfig">
</div>
<div class="schedule-item">
<label>间隔</label>
<input v-model.number="fixedDelayMs" type="number" min="0" max="60000" step="50" @change="saveFixedConfig">
<span class="input-unit">ms</span>
</div>
</template>
<div class="schedule-item">
<label>目标</label>
<input
@@ -127,6 +212,11 @@ const queryAssets = () => {
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;
+25 -1
View File
@@ -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,
+21
View File
@@ -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)
+69
View File
@@ -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
+7
View File
@@ -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"`