增加和平小店关键词固定抢购
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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,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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"`
|
||||
|
||||
Reference in New Issue
Block a user