修复库存监控提前停止并发布0.2.0
This commit is contained in:
@@ -5,6 +5,7 @@ import { useAccounts } from '@/composables/useAccounts'
|
||||
import { useActivity } from '@/composables/useActivity'
|
||||
import { useProducts } from '@/composables/useProducts'
|
||||
import { useToast } from '@/composables/useToast'
|
||||
import { GetResults } from '@wails/go/app/App'
|
||||
|
||||
const { scheduleActive, seckillRunning, startNow, stopSeckill, startFixed, startSchedule, cancelSchedule, restoreSchedule, subscribeSeckillState, unsubscribeSchedulerEvents } = useScheduler()
|
||||
const {
|
||||
@@ -36,6 +37,8 @@ const stockWatchAttempts = ref(Number(localStorage.getItem('xiaodianStockWatchAt
|
||||
const stockWatchDelayMs = ref(Number(localStorage.getItem('xiaodianStockWatchDelayMs') || 500))
|
||||
const stockWatching = ref(false)
|
||||
const stockWatchAccountProducts = ref<Record<string, string>>({})
|
||||
const stockWatchRunAccountProducts = ref<Record<string, string>>({})
|
||||
const stockWatchReportSessionID = ref('')
|
||||
const stockWatchTargetName = computed(() => {
|
||||
const assignments = stockWatchAccountProducts.value
|
||||
const accountCount = Object.keys(assignments).length
|
||||
@@ -46,6 +49,7 @@ const stockWatchTargetName = computed(() => {
|
||||
let stockWatchTimer: number | null = null
|
||||
let stockWatchStartTimer: number | null = null
|
||||
let stockRefreshing = false
|
||||
let stockWatchSettling = false
|
||||
|
||||
// 从本地缓存恢复定时设置
|
||||
const { savedTime } = restoreSchedule()
|
||||
@@ -137,6 +141,65 @@ const stopStockWatch = () => {
|
||||
}
|
||||
stockRefreshing = false
|
||||
stockWatchAccountProducts.value = {}
|
||||
stockWatchRunAccountProducts.value = {}
|
||||
stockWatchReportSessionID.value = ''
|
||||
}
|
||||
|
||||
const createStockWatchReportSessionID = () => {
|
||||
if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
|
||||
return `stock_${crypto.randomUUID()}`
|
||||
}
|
||||
return `stock_${Date.now()}_${Math.random().toString(36).slice(2)}`
|
||||
}
|
||||
|
||||
const isStockWatchAccountTerminal = (message: string) => {
|
||||
return /Livelink.*登录态校验失败|余额不足|点券不足|已拥有该道具|已经拥有该道具|已拥有该商品|未绑定账号/.test(message)
|
||||
}
|
||||
|
||||
const isStockWatchProductTerminal = (message: string) => {
|
||||
return /商品不存在|商品已下架|不支持该商品/.test(message)
|
||||
}
|
||||
|
||||
const stockWatchResultAccountName = (key: string) => String(key || '').replace(/_第\d+包$/, '')
|
||||
|
||||
const settleStockWatchRun = async () => {
|
||||
if (stockWatchSettling || Object.keys(stockWatchRunAccountProducts.value).length === 0) return
|
||||
stockWatchSettling = true
|
||||
const runAssignments = stockWatchRunAccountProducts.value
|
||||
try {
|
||||
const data = await GetResults() as Record<string, string>
|
||||
const messagesByAccount: Record<string, string[]> = {}
|
||||
Object.entries(data || {}).forEach(([key, value]) => {
|
||||
const accountName = stockWatchResultAccountName(key)
|
||||
if (!runAssignments[accountName]) return
|
||||
if (!messagesByAccount[accountName]) messagesByAccount[accountName] = []
|
||||
messagesByAccount[accountName].push(String(value || ''))
|
||||
})
|
||||
|
||||
Object.entries(runAssignments).forEach(([accountName, productID]) => {
|
||||
const messages = messagesByAccount[accountName] || []
|
||||
const succeeded = messages.some(message => message.startsWith('成功|'))
|
||||
const terminalAccount = messages.some(isStockWatchAccountTerminal)
|
||||
const terminalProduct = messages.some(isStockWatchProductTerminal)
|
||||
if (succeeded || terminalAccount || terminalProduct) {
|
||||
delete stockWatchAccountProducts.value[accountName]
|
||||
}
|
||||
if (terminalProduct) {
|
||||
Object.entries(stockWatchAccountProducts.value).forEach(([name, assignedProductID]) => {
|
||||
if (assignedProductID === productID) delete stockWatchAccountProducts.value[name]
|
||||
})
|
||||
}
|
||||
})
|
||||
} catch {
|
||||
// 结果读取失败时保留全部账号,下一轮库存刷新后仍可继续抢购。
|
||||
} finally {
|
||||
stockWatchRunAccountProducts.value = {}
|
||||
stockWatchSettling = false
|
||||
}
|
||||
|
||||
if (stockWatching.value && Object.keys(stockWatchAccountProducts.value).length === 0) {
|
||||
stopStockWatch()
|
||||
}
|
||||
}
|
||||
|
||||
const beginKeywordWatch = async () => {
|
||||
@@ -199,7 +262,7 @@ const toggleKeywordWatch = () => {
|
||||
const tryStartStockBuy = async () => {
|
||||
const assignments = stockWatchAccountProducts.value
|
||||
if (Object.keys(assignments).length === 0) return false
|
||||
if (scheduleActive.value || seckillRunning.value) return false
|
||||
if (scheduleActive.value || seckillRunning.value || stockWatchSettling || Object.keys(stockWatchRunAccountProducts.value).length > 0) return false
|
||||
const productsByID = new Map(products.value.map(product => [product.commodityId, product]))
|
||||
const readyAccountProducts: Record<string, string> = {}
|
||||
const readyProducts = new Map<string, typeof products.value[number]>()
|
||||
@@ -216,15 +279,14 @@ const tryStartStockBuy = async () => {
|
||||
const productText = [...readyProducts.values()]
|
||||
.map(product => `${product.commodityName} ${product.stockLabel || `库存 ${product.count}`}`)
|
||||
.join(';')
|
||||
const started = await startFixed(stockWatchAttempts.value, stockWatchDelayMs.value, readyAccountProducts)
|
||||
if (!started) return false
|
||||
|
||||
Object.keys(readyAccountProducts).forEach(accountName => {
|
||||
delete stockWatchAccountProducts.value[accountName]
|
||||
})
|
||||
if (Object.keys(stockWatchAccountProducts.value).length === 0) {
|
||||
stopStockWatch()
|
||||
// startFixed 返回只代表任务已提交;账号是否应停止监控要等本轮真实结果。
|
||||
stockWatchRunAccountProducts.value = { ...readyAccountProducts }
|
||||
const started = await startFixed(stockWatchAttempts.value, stockWatchDelayMs.value, readyAccountProducts, stockWatchReportSessionID.value)
|
||||
if (!started) {
|
||||
stockWatchRunAccountProducts.value = {}
|
||||
return false
|
||||
}
|
||||
|
||||
showToast('检测到库存', `${productText},已启动 ${Object.keys(readyAccountProducts).length} 个账号抢购`, 'info', 3000)
|
||||
return true
|
||||
}
|
||||
@@ -238,7 +300,7 @@ const beginStockWatch = async () => {
|
||||
|
||||
const interval = Math.max(1, Math.min(600, Number(stockWatchIntervalSec.value) || 3)) * 1000
|
||||
stockWatchTimer = window.setInterval(async () => {
|
||||
if (!stockWatching.value || scheduleActive.value || seckillRunning.value || stockRefreshing) return
|
||||
if (!stockWatching.value || scheduleActive.value || seckillRunning.value || stockRefreshing || stockWatchSettling) return
|
||||
stockRefreshing = true
|
||||
try {
|
||||
await loadProductsForAccounts(accountNames(), false)
|
||||
@@ -269,6 +331,7 @@ const toggleStockWatch = () => {
|
||||
saveStockWatchConfig()
|
||||
stopKeywordWatch()
|
||||
stockWatchAccountProducts.value = { ...accountProducts }
|
||||
stockWatchReportSessionID.value = createStockWatchReportSessionID()
|
||||
stockWatching.value = true
|
||||
const [hours, minutes, seconds] = stockWatchStartTime.value.split(':').map(Number)
|
||||
const target = new Date()
|
||||
@@ -292,6 +355,12 @@ watch(isXiaodian, (enabled) => {
|
||||
}
|
||||
})
|
||||
|
||||
watch(seckillRunning, (running, wasRunning) => {
|
||||
if (!running && wasRunning && stockWatching.value) {
|
||||
void settleStockWatchRun()
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
stopKeywordWatch()
|
||||
stopStockWatch()
|
||||
|
||||
@@ -161,7 +161,7 @@ export function useScheduler() {
|
||||
}
|
||||
}
|
||||
|
||||
const startFixed = async (attempts: number, delayMs: number, accountProducts?: Record<string, string>) => {
|
||||
const startFixed = async (attempts: number, delayMs: number, accountProducts?: Record<string, string>, reportSessionID = '') => {
|
||||
const ap = accountProducts || buildAccountProducts(selectedProductId.value || '', false)
|
||||
if (!ap) return false
|
||||
resetStatus()
|
||||
@@ -169,7 +169,7 @@ export function useScheduler() {
|
||||
markAccountNamesPending(Object.keys(ap), '固定抢购中,等待后端结果')
|
||||
setRunState('固定抢购已提交')
|
||||
try {
|
||||
const d = await StartFixedSeckill({ accountProducts: ap, attempts, delayMs })
|
||||
const d = await StartFixedSeckill({ accountProducts: ap, attempts, delayMs, reportSessionId: reportSessionID })
|
||||
if (d?.success === false) throw new Error(d.error || '提交失败')
|
||||
seckillRunning.value = true
|
||||
pendingXiaodianAssetAccounts.value = Object.keys(ap)
|
||||
|
||||
@@ -68,7 +68,7 @@ func (a *App) startXiaodianSeckill(req types.FixedSeckillRequest, message string
|
||||
if !a.scheduler.TryStartRun() {
|
||||
return types.MessageResult{Error: "抢购任务正在运行,请勿重复提交"}
|
||||
}
|
||||
go a.scheduler.RunXiaodian(utils.CloneStringMap(req.AccountProducts), req.Attempts, req.DelayMs)
|
||||
go a.scheduler.RunXiaodian(utils.CloneStringMap(req.AccountProducts), req.Attempts, req.DelayMs, req.ReportSessionID)
|
||||
return types.MessageResult{Success: true, Msg: message}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"github.com/wailsapp/wails/v2/pkg/runtime"
|
||||
"github.com/xuri/excelize/v2"
|
||||
|
||||
"douyu_go/internal/constants"
|
||||
"douyu_go/internal/logger"
|
||||
"douyu_go/internal/types"
|
||||
)
|
||||
@@ -274,7 +275,7 @@ func xiaodianDetailsHeaders() []string {
|
||||
|
||||
func xiaodianSummaryRow(sequence int, report types.XiaodianRunReport, item xiaodianReportExportData) []interface{} {
|
||||
account := item.ReportAccount
|
||||
matched := selectedXiaodianRecord(item)
|
||||
matched := selectedXiaodianRecord(item, report)
|
||||
remarks := reportExportRemarks(item)
|
||||
return []interface{}{
|
||||
sequence,
|
||||
@@ -288,7 +289,7 @@ func xiaodianSummaryRow(sequence int, report types.XiaodianRunReport, item xiaod
|
||||
reportAssetValue(item, false),
|
||||
exportText(account.Status),
|
||||
exportText(account.FailureReason),
|
||||
exportText(selectedRecordVerification(item)),
|
||||
exportText(selectedRecordVerification(item, report)),
|
||||
exportText(selectedRecordStatus(item, matched)),
|
||||
account.Attempts,
|
||||
exportText(remarks),
|
||||
@@ -384,25 +385,58 @@ func reportAssetValue(item xiaodianReportExportData, coupons bool) interface{} {
|
||||
return item.Assets.Fragments
|
||||
}
|
||||
|
||||
func selectedXiaodianRecord(item xiaodianReportExportData) *types.XiaodianExchangeRecord {
|
||||
func selectedXiaodianRecord(item xiaodianReportExportData, report types.XiaodianRunReport) *types.XiaodianExchangeRecord {
|
||||
for index := range item.Records {
|
||||
if item.Records[index].CommodityID == item.ReportAccount.ProductID {
|
||||
return &item.Records[index]
|
||||
record := &item.Records[index]
|
||||
if record.CommodityID == item.ReportAccount.ProductID && recordBelongsToReport(*record, report) {
|
||||
return record
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func selectedRecordVerification(item xiaodianReportExportData) string {
|
||||
func selectedRecordVerification(item xiaodianReportExportData, report types.XiaodianRunReport) string {
|
||||
if item.RecordError != "" {
|
||||
return "查询失败"
|
||||
}
|
||||
if selectedXiaodianRecord(item) != nil {
|
||||
if selectedXiaodianRecord(item, report) != nil {
|
||||
return "已兑换"
|
||||
}
|
||||
return "未发现订单"
|
||||
}
|
||||
|
||||
func recordBelongsToReport(record types.XiaodianExchangeRecord, report types.XiaodianRunReport) bool {
|
||||
if report.StartedAt.IsZero() {
|
||||
return false
|
||||
}
|
||||
value := strings.TrimSpace(record.BuyTime)
|
||||
if value == "" {
|
||||
value = strings.TrimSpace(record.PayTime)
|
||||
}
|
||||
recordedAt, ok := parseXiaodianRecordTime(value)
|
||||
return ok && !recordedAt.Before(report.StartedAt)
|
||||
}
|
||||
|
||||
func parseXiaodianRecordTime(value string) (time.Time, bool) {
|
||||
for _, layout := range []string{
|
||||
"2006-01-02 15:04:05.999999999",
|
||||
"2006-01-02 15:04:05",
|
||||
time.RFC3339Nano,
|
||||
time.RFC3339,
|
||||
} {
|
||||
if strings.Contains(layout, "Z07") {
|
||||
if parsed, err := time.Parse(layout, value); err == nil {
|
||||
return parsed, true
|
||||
}
|
||||
continue
|
||||
}
|
||||
if parsed, err := time.ParseInLocation(layout, value, constants.CST); err == nil {
|
||||
return parsed, true
|
||||
}
|
||||
}
|
||||
return time.Time{}, false
|
||||
}
|
||||
|
||||
func selectedRecordStatus(item xiaodianReportExportData, record *types.XiaodianExchangeRecord) string {
|
||||
if item.RecordError != "" {
|
||||
return "查询失败"
|
||||
|
||||
@@ -95,3 +95,20 @@ func TestSelectedRecordStatus(t *testing.T) {
|
||||
t.Fatalf("orderStatusText(empty) = %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSelectedXiaodianRecordIgnoresHistoryBeforeReport(t *testing.T) {
|
||||
report := types.XiaodianRunReport{
|
||||
StartedAt: time.Date(2026, 8, 9, 15, 0, 0, 0, time.Local),
|
||||
}
|
||||
item := xiaodianReportExportData{
|
||||
ReportAccount: types.XiaodianRunReportAccount{ProductID: "goods-1"},
|
||||
Records: []types.XiaodianExchangeRecord{
|
||||
{CommodityID: "goods-1", BuyTime: "2026-08-09 14:59:59", Status: "3"},
|
||||
{CommodityID: "goods-1", BuyTime: "2026-08-09 15:00:01", Status: "3"},
|
||||
},
|
||||
}
|
||||
matched := selectedXiaodianRecord(item, report)
|
||||
if matched == nil || matched.BuyTime != "2026-08-09 15:00:01" {
|
||||
t.Fatalf("历史订单不应被选中: %+v", matched)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ package constants
|
||||
import "time"
|
||||
|
||||
// Version 应用版本号,与 wails.json version 同步。
|
||||
const Version = "0.1.9"
|
||||
const Version = "0.2.0"
|
||||
|
||||
// BuildTime 构建时间,由编译时 ldflags 注入。
|
||||
var BuildTime = "unknown"
|
||||
|
||||
@@ -132,7 +132,7 @@ func (s *Scheduler) scheduleTimers(accountProducts map[string]string, targetTime
|
||||
return
|
||||
}
|
||||
if s.clientSnapshot().Activity.IsXiaodian {
|
||||
s.RunXiaodian(accountProducts, attempts, delayMs)
|
||||
s.RunXiaodian(accountProducts, attempts, delayMs, "")
|
||||
return
|
||||
}
|
||||
s.RunNow(accountProducts)
|
||||
|
||||
@@ -616,7 +616,7 @@ func (s *Scheduler) RunFixed(accountProducts map[string]string, attempts, delayM
|
||||
|
||||
// RunXiaodian 使用和平小店专用策略:固定次数、固定间隔、每轮直接调用 livebuy。
|
||||
// 小店补货不依赖旧活动的 75 秒阶段窗口,也不执行手册的预检链路。
|
||||
func (s *Scheduler) RunXiaodian(accountProducts map[string]string, attempts, delayMs int) {
|
||||
func (s *Scheduler) RunXiaodian(accountProducts map[string]string, attempts, delayMs int, reportSessionID string) {
|
||||
defer s.finishRun()
|
||||
s.ClearResults()
|
||||
|
||||
@@ -641,7 +641,7 @@ func (s *Scheduler) RunXiaodian(accountProducts map[string]string, attempts, del
|
||||
logger.LogEvent("抢购", "warn", "和平小店抢购没有可执行账号")
|
||||
return
|
||||
}
|
||||
s.startXiaodianReport(accounts, products)
|
||||
s.startXiaodianReport(accounts, products, reportSessionID)
|
||||
|
||||
stoppedAccounts := make(map[string]string)
|
||||
cooldownAccounts := make(map[string]time.Time)
|
||||
|
||||
@@ -15,9 +15,51 @@ import (
|
||||
|
||||
const xiaodianReportFileName = "last_xiaodian_report.json"
|
||||
|
||||
func (s *Scheduler) startXiaodianReport(accounts []types.Account, products map[string]types.Product) {
|
||||
func (s *Scheduler) startXiaodianReport(accounts []types.Account, products map[string]types.Product, sessionID string) {
|
||||
s.reportMu.Lock()
|
||||
defer s.reportMu.Unlock()
|
||||
|
||||
// 库存监控会把不同商品/账号拆成多次任务;同一会话追加账号,普通抢购则创建新报告。
|
||||
if sessionID != "" && s.latestXiaodianReport != nil && s.latestXiaodianReport.SessionID == sessionID {
|
||||
report := s.latestXiaodianReport
|
||||
report.Stopped = false
|
||||
report.FinishedAt = time.Time{}
|
||||
existing := make(map[string]int, len(report.Accounts))
|
||||
for index := range report.Accounts {
|
||||
existing[report.Accounts[index].AccountName] = index
|
||||
}
|
||||
for _, account := range accounts {
|
||||
product := products[account.Name]
|
||||
if index, ok := existing[account.Name]; ok {
|
||||
// 商品映射在会话内保持账号独立;仅补齐可能缺失的商品元数据。
|
||||
item := &report.Accounts[index]
|
||||
if item.ProductID == "" {
|
||||
item.ProductID = product.CommodityID
|
||||
}
|
||||
if item.ProductName == "" {
|
||||
item.ProductName = product.CommodityName
|
||||
}
|
||||
continue
|
||||
}
|
||||
report.Accounts = append(report.Accounts, types.XiaodianRunReportAccount{
|
||||
AccountName: account.Name,
|
||||
ProductID: product.CommodityID,
|
||||
ProductName: product.CommodityName,
|
||||
ProductPrice: product.Score,
|
||||
PayType: product.PayType,
|
||||
PayLabel: product.PayLabel,
|
||||
Status: "待执行",
|
||||
})
|
||||
}
|
||||
sort.Slice(report.Accounts, func(i, j int) bool {
|
||||
return report.Accounts[i].AccountName < report.Accounts[j].AccountName
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
report := types.XiaodianRunReport{
|
||||
ReportID: fmt.Sprintf("xiaodian_%s", time.Now().Format("20060102_150405.000")),
|
||||
SessionID: sessionID,
|
||||
StartedAt: time.Now(),
|
||||
Accounts: make([]types.XiaodianRunReportAccount, 0, len(accounts)),
|
||||
}
|
||||
@@ -36,10 +78,7 @@ func (s *Scheduler) startXiaodianReport(accounts []types.Account, products map[s
|
||||
sort.Slice(report.Accounts, func(i, j int) bool {
|
||||
return report.Accounts[i].AccountName < report.Accounts[j].AccountName
|
||||
})
|
||||
|
||||
s.reportMu.Lock()
|
||||
s.latestXiaodianReport = &report
|
||||
s.reportMu.Unlock()
|
||||
}
|
||||
|
||||
func (s *Scheduler) updateXiaodianReport(results map[string]types.ExchangeResult) {
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
package seckill
|
||||
|
||||
import "testing"
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"douyu_go/internal/types"
|
||||
)
|
||||
|
||||
func TestReportFailureReasonUsesXiaodianStopReason(t *testing.T) {
|
||||
message := "失败|和平小店兑换失败: 对不起,Livelink登录态校验失败!"
|
||||
@@ -8,3 +12,45 @@ func TestReportFailureReasonUsesXiaodianStopReason(t *testing.T) {
|
||||
t.Fatalf("reportFailureReason() = %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStartXiaodianReportAppendsWithinSession(t *testing.T) {
|
||||
scheduler := &Scheduler{}
|
||||
firstAccounts := []types.Account{{Name: "账号A"}}
|
||||
firstProducts := map[string]types.Product{
|
||||
"账号A": {CommodityID: "goods-a", CommodityName: "商品A"},
|
||||
}
|
||||
scheduler.startXiaodianReport(firstAccounts, firstProducts, "stock-session")
|
||||
scheduler.updateXiaodianReport(map[string]types.ExchangeResult{
|
||||
"账号A": {OK: true, Account: "账号A", Message: "成功|角色A"},
|
||||
})
|
||||
|
||||
secondAccounts := []types.Account{{Name: "账号B"}}
|
||||
secondProducts := map[string]types.Product{
|
||||
"账号B": {CommodityID: "goods-b", CommodityName: "商品B"},
|
||||
}
|
||||
scheduler.startXiaodianReport(secondAccounts, secondProducts, "stock-session")
|
||||
report, ok := scheduler.LatestXiaodianReport()
|
||||
if !ok || len(report.Accounts) != 2 {
|
||||
t.Fatalf("同一监控会话应保留两批账号,报告=%+v", report)
|
||||
}
|
||||
if report.Accounts[0].ProductID != "goods-a" || report.Accounts[1].ProductID != "goods-b" {
|
||||
t.Fatalf("账号商品映射丢失: %+v", report.Accounts)
|
||||
}
|
||||
if report.Accounts[0].Status != "成功" || report.Accounts[0].Attempts != 1 {
|
||||
t.Fatalf("前一批结果被覆盖: %+v", report.Accounts[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestStartXiaodianReportDifferentSessionReplacesReport(t *testing.T) {
|
||||
scheduler := &Scheduler{}
|
||||
scheduler.startXiaodianReport([]types.Account{{Name: "账号A"}}, map[string]types.Product{
|
||||
"账号A": {CommodityID: "goods-a"},
|
||||
}, "session-a")
|
||||
scheduler.startXiaodianReport([]types.Account{{Name: "账号B"}}, map[string]types.Product{
|
||||
"账号B": {CommodityID: "goods-b"},
|
||||
}, "session-b")
|
||||
report, ok := scheduler.LatestXiaodianReport()
|
||||
if !ok || report.SessionID != "session-b" || len(report.Accounts) != 1 || report.Accounts[0].AccountName != "账号B" {
|
||||
t.Fatalf("不同会话不应串报告: %+v", report)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -149,6 +149,8 @@ type FixedSeckillRequest struct {
|
||||
AccountProducts map[string]string `json:"accountProducts"`
|
||||
Attempts int `json:"attempts"`
|
||||
DelayMs int `json:"delayMs"`
|
||||
// ReportSessionID 用于把库存监控拆分的多次抢购合并到同一份报告。
|
||||
ReportSessionID string `json:"reportSessionId,omitempty"`
|
||||
}
|
||||
|
||||
// APIResponse 斗鱼 API 通用响应。
|
||||
|
||||
@@ -5,6 +5,7 @@ import "time"
|
||||
// XiaodianRunReport 保存一次和平小店抢购的可导出快照,不包含 Cookie 等敏感凭据。
|
||||
type XiaodianRunReport struct {
|
||||
ReportID string `json:"reportId"`
|
||||
SessionID string `json:"sessionId,omitempty"`
|
||||
StartedAt time.Time `json:"startedAt"`
|
||||
FinishedAt time.Time `json:"finishedAt"`
|
||||
Stopped bool `json:"stopped"`
|
||||
|
||||
+2
-2
@@ -2,7 +2,7 @@
|
||||
"$schema": "https://wails.io/schemas/config.v2.json",
|
||||
"name": "douyu_go",
|
||||
"outputfilename": "douyu_go",
|
||||
"version": "0.1.9",
|
||||
"version": "0.2.0",
|
||||
"frontend:dir": "frontend",
|
||||
"frontend:install": "npm install",
|
||||
"frontend:build": "npm run build",
|
||||
@@ -15,6 +15,6 @@
|
||||
"email": "yml@example.com"
|
||||
},
|
||||
"info": {
|
||||
"productVersion": "0.1.9"
|
||||
"productVersion": "0.2.0"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user