增加电竞手册

This commit is contained in:
yml2213
2026-07-29 10:36:44 +08:00
parent 00fa48722f
commit f4d33d19c0
11 changed files with 382 additions and 98 deletions
+1
View File
@@ -24,3 +24,4 @@ ck 对比.md
*.app
.DS_Store
frontend/dist/
.workbuddy
+51 -55
View File
@@ -13,6 +13,7 @@ import (
"github.com/golang/freetype/truetype"
"douyu_go/internal/activity"
"douyu_go/internal/client"
"douyu_go/internal/config"
"douyu_go/internal/constants"
@@ -77,7 +78,13 @@ func (a *App) startup(ctx context.Context) {
a.trace = tr
a.closeTrace = closeTrace
a.client = client.New(a.cfg.TimeAPIURL, a.trace)
// 解析当前活动:配置中的 activity ID 决定使用哪套接口参数
act := activity.Get(a.cfg.Activity)
if act == nil {
act = activity.Default()
logger.LogEvent("活动", "warn", "配置中的活动 %q 未识别,已回退到默认活动 %s", a.cfg.Activity, act.DisplayName)
}
a.client = client.New(act, a.cfg.TimeAPIURL, a.trace)
a.font = imgpkg.LoadFont(a.paths.FontFiles)
a.scheduler = seckill.New(a.client, a.paths, a.font, a.cfg.Concurrency, a.cfg.Attempts, a.cfg.RetryStages, a.cfg.SeckillMode)
a.scheduler.LoadAccounts()
@@ -595,6 +602,42 @@ func (a *App) GetVersion() string {
return constants.Version
}
// GetActivities 返回所有已注册的手册活动,供前端下拉选择。
func (a *App) GetActivities() []types.ActivityInfo {
list := activity.List()
out := make([]types.ActivityInfo, 0, len(list))
for _, act := range list {
out = append(out, types.ActivityInfo{ID: act.ID, DisplayName: act.DisplayName})
}
return out
}
// GetCurrentActivity 返回当前生效的活动。
func (a *App) GetCurrentActivity() types.ActivityInfo {
act := a.client.Activity
return types.ActivityInfo{ID: act.ID, DisplayName: act.DisplayName}
}
// SwitchActivity 切换当前活动并持久化到配置;切换后重建客户端与时间基准。
func (a *App) SwitchActivity(id string) map[string]any {
act := activity.Get(id)
if act == nil {
return map[string]any{"success": false, "error": "未知活动: " + id}
}
a.cfg.Activity = act.ID
a.cfg.TimeAPIURL = act.TimeAPIURL
if err := config.SaveConfig(a.paths.ConfigFile, a.cfg); err != nil {
logger.LogEvent("活动", "error", "切换活动配置保存失败: %v", err)
return map[string]any{"success": false, "error": "配置保存失败: " + err.Error()}
}
a.client = client.New(act, a.cfg.TimeAPIURL, a.trace)
if sample, err := a.client.ServerTimeSample(); err == nil {
logger.SetDouyuOffset(sample.Offset)
}
logger.LogEvent("活动", "success", "已切换到活动: %s", act.DisplayName)
return map[string]any{"success": true, "activity": act.ID, "displayName": act.DisplayName}
}
// GetSettings 返回当前配置。
func (a *App) GetSettings() types.Config {
return a.cfg
@@ -694,13 +737,13 @@ func maskCookie(cookie string) string {
if total > len(masked) {
masked = append(masked, fmt.Sprintf("...共%d项", total))
}
return joinStrings(masked, "; ")
return strings.Join(masked, "; ")
}
func splitCookie(cookie string) []string {
var result []string
for _, part := range splitString(cookie, ";") {
trimmed := trimSpace(part)
for _, part := range strings.Split(cookie, ";") {
trimmed := strings.TrimSpace(part)
if trimmed != "" {
result = append(result, trimmed)
}
@@ -709,15 +752,15 @@ func splitCookie(cookie string) []string {
}
func splitCookiePair(pair string) (string, string) {
idx := indexOf(pair, "=")
idx := strings.Index(pair, "=")
if idx < 0 {
return trimSpace(pair), ""
return strings.TrimSpace(pair), ""
}
return trimSpace(pair[:idx]), pair[idx+1:]
return strings.TrimSpace(pair[:idx]), pair[idx+1:]
}
func maskValue(value string) string {
value = trimSpace(value)
value = strings.TrimSpace(value)
if value == "" {
return "****"
}
@@ -727,50 +770,3 @@ func maskValue(value string) string {
}
return string(runes[:4]) + "****" + string(runes[len(runes)-4:])
}
// 简单的字符串工具函数,避免引入额外包
func splitString(s, sep string) []string {
var result []string
start := 0
for i := 0; i+len(sep) <= len(s); {
if s[i:i+len(sep)] == sep {
result = append(result, s[start:i])
start = i + len(sep)
i = start
} else {
i++
}
}
result = append(result, s[start:])
return result
}
func trimSpace(s string) string {
for len(s) > 0 && (s[0] == ' ' || s[0] == '\t' || s[0] == '\n' || s[0] == '\r') {
s = s[1:]
}
for len(s) > 0 && (s[len(s)-1] == ' ' || s[len(s)-1] == '\t' || s[len(s)-1] == '\n' || s[len(s)-1] == '\r') {
s = s[:len(s)-1]
}
return s
}
func indexOf(s, sub string) int {
for i := 0; i+len(sub) <= len(s); i++ {
if s[i:i+len(sub)] == sub {
return i
}
}
return -1
}
func joinStrings(parts []string, sep string) string {
if len(parts) == 0 {
return ""
}
result := parts[0]
for _, p := range parts[1:] {
result += sep + p
}
return result
}
+3
View File
@@ -5,6 +5,7 @@ import { useProducts } from '@/composables/useProducts'
import { useAccounts } from '@/composables/useAccounts'
import { useScheduler } from '@/composables/useScheduler'
import { useLogs } from '@/composables/useLogs'
import { useActivity } from '@/composables/useActivity'
import AppShell from '@/components/layout/AppShell.vue'
import ToastContainer from '@/components/ToastContainer.vue'
@@ -14,6 +15,7 @@ const { loadAccounts, restoreSelection: restoreAccountSelection } = useAccounts(
const { clearLogs, fetchServerLogs, checkResults } = useLogs()
const { restoreSchedule, setRunState } = useScheduler()
const { clearResults } = useLogs()
const { loadActivities } = useActivity()
const version = ref('')
const serverTime = ref('--')
@@ -61,6 +63,7 @@ onMounted(async () => {
// 加载数据
await loadAccounts()
await loadActivities()
await loadProducts(false)
// 获取版本号
@@ -1,5 +1,6 @@
<script setup lang="ts">
import { useProducts } from '@/composables/useProducts'
import { useActivity } from '@/composables/useActivity'
import ProductSearch from '@/components/product/ProductSearch.vue'
import ProductList from '@/components/product/ProductList.vue'
import ProductCard from '@/components/product/ProductCard.vue'
@@ -9,6 +10,15 @@ defineProps<{
}>()
const { loadProducts, productInfo, filteredProducts, loading } = useProducts()
const { activities, currentActivity, switching, switchActivity } = useActivity()
const onSwitchActivity = async (e: Event) => {
const id = (e.target as HTMLSelectElement).value
const ok = await switchActivity(id)
if (ok) {
await loadProducts(true)
}
}
</script>
<template>
@@ -22,6 +32,25 @@ const { loadProducts, productInfo, filteredProducts, loading } = useProducts()
<span class="stat-pill">v{{ version }}</span>
</div>
<!-- 活动选择 -->
<div class="activity-box">
<label class="activity-label">当前活动</label>
<div class="activity-row">
<select
class="activity-select"
:value="currentActivity?.id"
:disabled="switching || activities.length === 0"
@change="onSwitchActivity"
>
<option v-if="activities.length === 0" value="">加载中...</option>
<option v-for="act in activities" :key="act.id" :value="act.id">
{{ act.displayName }}
</option>
</select>
<span v-if="switching" class="activity-spin">切换中…</span>
</div>
</div>
<!-- 操作区 -->
<div class="left-header">
<div class="product-actions">
@@ -85,6 +114,64 @@ const { loadProducts, productInfo, filteredProducts, loading } = useProducts()
letter-spacing: .02em;
}
.brand + .activity-box {
border-top: 0;
}
.activity-box {
padding: 12px 18px;
border-bottom: 1px solid var(--line);
display: flex;
flex-direction: column;
gap: 6px;
}
.activity-label {
font-size: 11px;
color: var(--muted);
letter-spacing: .04em;
}
.activity-row {
display: flex;
align-items: center;
gap: 8px;
}
.activity-select {
flex: 1;
min-width: 0;
appearance: none;
background: var(--surface-2, var(--surface));
color: var(--text-strong);
border: 1px solid var(--line);
border-radius: 8px;
padding: 7px 10px;
font-size: 13px;
cursor: pointer;
transition: border-color .15s ease;
}
.activity-select:hover:not(:disabled) {
border-color: var(--primary);
}
.activity-select:focus {
outline: none;
border-color: var(--primary);
}
.activity-select:disabled {
opacity: .6;
cursor: default;
}
.activity-spin {
font-size: 11px;
color: var(--primary);
white-space: nowrap;
}
.left-header {
padding: 14px 16px;
border-bottom: 1px solid var(--line);
+64
View File
@@ -0,0 +1,64 @@
import { ref } from 'vue'
import { useToast } from './useToast'
// 活动项(与后端 types.ActivityInfo 对齐)
export interface ActivityItem {
id: string
displayName: string
}
// 模块级单例:活动列表与当前活动在所有组件中共享同一份状态
const activities = ref<ActivityItem[]>([])
const currentActivity = ref<ActivityItem | null>(null)
const switching = ref(false)
// 与项目其他 composable 一致,直接走 Wails 运行时注入的 window.go.main.App
const App = window.go.main.App
export function useActivity() {
const { showToast } = useToast()
// 拉取全部活动与当前生效活动
const loadActivities = async () => {
try {
const list = await App.GetActivities()
activities.value = list || []
try {
currentActivity.value = await App.GetCurrentActivity()
} catch {
// 当前活动读取失败不阻塞列表展示
}
} catch (e: any) {
showToast('读取活动列表失败', e?.message || '', 'error')
}
}
// 切换活动;成功返回 true,调用方可据此刷新商品等数据
const switchActivity = async (id: string): Promise<boolean> => {
if (id === currentActivity.value?.id) return true
switching.value = true
try {
const d: Record<string, any> = await App.SwitchActivity(id)
if (d && d.success) {
currentActivity.value = { id: d.activity, displayName: d.displayName }
showToast('活动已切换', (d.displayName || id) + ',正在刷新商品...', 'success')
return true
}
showToast('切换失败', d?.error || '', 'error')
return false
} catch (e: any) {
showToast('切换失败', e?.message || '', 'error')
return false
} finally {
switching.value = false
}
}
return {
activities,
currentActivity,
switching,
loadActivities,
switchActivity
}
}
+3
View File
@@ -31,6 +31,9 @@ interface Window {
ExportAccounts: (accountNames: string[], filename: string) => Promise<any>
ExportAccountsWithProduct: (accountNames: string[], commodityID: string, commodityName: string) => Promise<any>
OrganizeAccounts: (commodityID: string, commodityName: string, startTime: number, endTime: number) => Promise<any>
GetActivities: () => Promise<Array<{ id: string; displayName: string }>>
GetCurrentActivity: () => Promise<{ id: string; displayName: string }>
SwitchActivity: (id: string) => Promise<{ success: boolean; activity?: string; displayName?: string; error?: string }>
}
}
}
+113
View File
@@ -0,0 +1,113 @@
package activity
import "douyu_go/internal/constants"
// Activity 描述一个斗鱼手册活动的全部接口参数。
//
// 不同活动(和平手册 / 电竞手册)的 manualID、roomID、actAlias 以及接口路径段都不同,
// 集中在这里可以让调度核心逻辑保持活动无关,新增活动只需在 registry 中加一项。
//
// 注意:peacematch 的 exchange / preExchange / batchExchangeLimit / storedetail 端点
// 没有出现在抓包里,这里按 peace 的 1:1 路径映射推断(storehome、exchangeList 已验证
// 确实落在 peacematch 路径下)。若实际抢购报错,优先核对这几个端点。
type Activity struct {
ID string // 注册表 key,例如 "peace" / "peacematch"
DisplayName string // 前端展示名
ManualID string
RoomID string
ActAlias string
BaseURL string
ShopReferer string
TimeAPIURL string // 服务端时间校准接口(storedetail)
StoreHomeURL string
CSRFURL string
BatchLimitURL string
PreExchangeURL string
ExchangeURL string
ExchangeListURL string
BindInfoURL string
ScoreURL string
ScoreMethod string // "POST" 或 "GET"
}
const douyuBase = constants.BaseURL
// peace 和平手册(旧活动,ActAlias=20260120QYOOB)。
var peace = &Activity{
ID: "peace",
DisplayName: "和平手册",
ManualID: "G4KA4Qnz4LDp7",
RoomID: "9263298",
ActAlias: "20260120QYOOB",
BaseURL: douyuBase,
ShopReferer: douyuBase + "/pages/live-peace-handbook/web/shop?ditchname=pass0&roomId=9263298",
TimeAPIURL: douyuBase + "/wgapi/ordnc/activity/peace/storedetail?manualID=G4KA4Qnz4LDp7&commodityID=20260120QYOOB_bag274&rid=9263298",
StoreHomeURL: douyuBase + "/wgapi/ordnc/activity/peace/storehome?manualID=G4KA4Qnz4LDp7&rid=9263298",
CSRFURL: douyuBase + "/japi/carnival/nc/common/generateCsrf",
BatchLimitURL: douyuBase + "/wgapi/ordnc/activity/peace/batchExchangeLimit",
PreExchangeURL: douyuBase + "/wgapi/ordnc/activity/peace/preExchange",
ExchangeURL: douyuBase + "/wgapi/ordnc/activity/peace/exchange",
ExchangeListURL: douyuBase + "/wgapi/ordnc/activity/peace/exchangeList?manualID=G4KA4Qnz4LDp7&limit=100&offset=0",
BindInfoURL: douyuBase + "/japi/carnivalApi/v2/tencent/bindInfo?actAlias=20260120QYOOB",
ScoreURL: douyuBase + "/japi/oms/web/peace/credit/balance",
ScoreMethod: "POST",
}
// peacematch 电竞手册(新活动,ActAlias=20260226ZMGDG)。
// 数据来源:/Users/yml/Desktop/抓包/douyu/7.29/电竞手册-基础信息.har 及 7.28 系列抓包。
var peacematch = &Activity{
ID: "peacematch",
DisplayName: "电竞手册",
ManualID: "1W0yGkOdDbAXb",
RoomID: "9263298",
ActAlias: "20260226ZMGDG",
BaseURL: douyuBase,
ShopReferer: douyuBase + "/pages/live-peace-pel/web/shop?roomId=9263298&isAnchorSide=0",
TimeAPIURL: douyuBase + "/wgapi/ordnc/activity/peacematch/storedetail?manualID=1W0yGkOdDbAXb&commodityID=20260226ZMGDG_bag139&rid=9263298",
StoreHomeURL: douyuBase + "/wgapi/ordnc/activity/peacematch/storehome?manualID=1W0yGkOdDbAXb&rid=9263298",
// generateCsrf 为嘉年华平台级接口,抓包未单独出现,沿用 peace 同款端点。
CSRFURL: douyuBase + "/japi/carnival/nc/common/generateCsrf",
// 以下 4 个兑换类端点未在抓包中捕获,按 peace 的 1:1 路径映射推断,待实跑验证。
BatchLimitURL: douyuBase + "/wgapi/ordnc/activity/peacematch/batchExchangeLimit",
PreExchangeURL: douyuBase + "/wgapi/ordnc/activity/peacematch/preExchange",
ExchangeURL: douyuBase + "/wgapi/ordnc/activity/peacematch/exchange",
ExchangeListURL: douyuBase + "/wgapi/ordnc/activity/peacematch/exchangeList?manualID=1W0yGkOdDbAXb&limit=100&offset=0",
BindInfoURL: douyuBase + "/wgapi/ordnc/live/memberthird/tencent/bindInfo?actAlias=20260226ZMGDG",
ScoreURL: douyuBase + "/wgapi/ordnc/activity/peacematch/score/balance",
ScoreMethod: "GET",
}
var registry = map[string]*Activity{
peace.ID: peace,
peacematch.ID: peacematch,
}
// DefaultActivityID 默认活动,新分支默认启用「电竞手册」。
const DefaultActivityID = "peacematch"
// List 返回所有注册活动,按注册顺序(和平手册在前)。
func List() []*Activity {
return []*Activity{peace, peacematch}
}
// Get 按 ID 获取活动;不存在时返回 nil。
func Get(id string) *Activity {
if id == "" {
return nil
}
return registry[id]
}
// Default 返回默认活动。
func Default() *Activity {
if a := registry[DefaultActivityID]; a != nil {
return a
}
return peace
}
+44 -27
View File
@@ -12,6 +12,7 @@ import (
"strings"
"time"
"douyu_go/internal/activity"
"douyu_go/internal/constants"
"douyu_go/internal/logger"
"douyu_go/internal/trace"
@@ -22,13 +23,22 @@ import (
// DouyuClient 斗鱼接口客户端。
type DouyuClient struct {
HTTPClient *http.Client
Activity *activity.Activity
TimeAPIURL string
Trace *trace.Recorder
}
// New 创建斗鱼客户端。
func New(timeAPIURL string, tr *trace.Recorder) *DouyuClient {
// New 创建斗鱼客户端。act 决定使用哪个活动的接口参数;
// timeAPIURL 为空时回退到 act.TimeAPIURL。
func New(act *activity.Activity, timeAPIURL string, tr *trace.Recorder) *DouyuClient {
if act == nil {
act = activity.Default()
}
if timeAPIURL == "" {
timeAPIURL = act.TimeAPIURL
}
return &DouyuClient{
Activity: act,
TimeAPIURL: timeAPIURL,
Trace: tr,
HTTPClient: &http.Client{
@@ -206,7 +216,7 @@ func (c *DouyuClient) Products() ([]types.Product, error) {
List []types.Product `json:"list"`
} `json:"data"`
}
if err := c.getJSON(constants.StoreHomeURL, "", "商品列表", &result); err != nil {
if err := c.getJSON(c.Activity.StoreHomeURL, "", "商品列表", &result); err != nil {
return nil, err
}
if result.Error != 0 {
@@ -215,23 +225,30 @@ func (c *DouyuClient) Products() ([]types.Product, error) {
return result.Data.List, nil
}
// Score 查询账号积分余额。
// Score 查询账号积分余额。不同活动的积分接口方法/路径不同:
// 和平手册用 POST /japi/oms/web/peace/credit/balance;
// 电竞手册用 GET /wgapi/ordnc/activity/peacematch/score/balance。
func (c *DouyuClient) Score(cookie string) (int, error) {
req, err := http.NewRequest("POST", constants.ScoreURL, strings.NewReader(`{}`))
if err != nil {
return 0, err
}
c.setJSONHeaders(req, cookie)
req.Header.Set("Referer", constants.BaseURL+"/")
var result struct {
types.APIResponse
Data struct {
Balance int `json:"balance"`
} `json:"data"`
}
if _, err := c.doJSON(req, &result, "积分余额"); err != nil {
return 0, err
if c.Activity.ScoreMethod == "GET" {
if err := c.getJSON(c.Activity.ScoreURL, cookie, "积分余额", &result); err != nil {
return 0, err
}
} else {
req, err := http.NewRequest("POST", c.Activity.ScoreURL, strings.NewReader(`{}`))
if err != nil {
return 0, err
}
c.setJSONHeaders(req, cookie)
req.Header.Set("Referer", c.Activity.BaseURL+"/")
if _, err := c.doJSON(req, &result, "积分余额"); err != nil {
return 0, err
}
}
if result.Error != 0 {
return 0, fmt.Errorf("积分接口失败: %s", result.Msg)
@@ -241,7 +258,7 @@ func (c *DouyuClient) Score(cookie string) (int, error) {
// GenerateCSRF 调用 generateCsrf,并从 Set-Cookie 中取 cvl_csrf_token。
func (c *DouyuClient) GenerateCSRF(cookie string) (string, error) {
req, err := http.NewRequest("POST", constants.CSRFURL, nil)
req, err := http.NewRequest("POST", c.Activity.CSRFURL, nil)
if err != nil {
return "", err
}
@@ -319,10 +336,10 @@ func (c *DouyuClient) BatchExchangeLimit(cookie, commodityID string, csrf *strin
Limit string `json:"limit"`
} `json:"data"`
}
if err := c.postMultipart(constants.BatchLimitURL, cookie, csrf, "批量限制", map[string]string{
"manualID": constants.ManualID,
if err := c.postMultipart(c.Activity.BatchLimitURL, cookie, csrf, "批量限制", map[string]string{
"manualID": c.Activity.ManualID,
"commodityID": commodityID,
"rid": constants.RoomID,
"rid": c.Activity.RoomID,
"csrfToken": currentCSRF,
}, &result); err != nil {
return 0, err
@@ -344,9 +361,9 @@ func (c *DouyuClient) PreExchange(cookie, commodityID string, csrf *string) erro
currentCSRF = *csrf
}
var result types.APIResponse
if err := c.postMultipart(constants.PreExchangeURL, cookie, csrf, "预兑换", map[string]string{
"manualID": constants.ManualID,
"rid": constants.RoomID,
if err := c.postMultipart(c.Activity.PreExchangeURL, cookie, csrf, "预兑换", map[string]string{
"manualID": c.Activity.ManualID,
"rid": c.Activity.RoomID,
"commodityID": commodityID,
"num": "1",
"csrfToken": currentCSRF,
@@ -373,11 +390,11 @@ func (c *DouyuClient) Exchange(cookie, commodityID string, csrf *string) (string
CommodityTyp int `json:"commodityType"`
} `json:"data"`
}
if err := c.postMultipart(constants.ExchangeURL, cookie, csrf, "正式兑换", map[string]string{
"manualID": constants.ManualID,
if err := c.postMultipart(c.Activity.ExchangeURL, cookie, csrf, "正式兑换", map[string]string{
"manualID": c.Activity.ManualID,
"commodityID": commodityID,
"num": "1",
"rid": constants.RoomID,
"rid": c.Activity.RoomID,
"csrfToken": currentCSRF,
}, &result); err != nil {
return "", err
@@ -401,7 +418,7 @@ func (c *DouyuClient) RoleName(cookie string) (string, error) {
} `json:"gameRole"`
} `json:"data"`
}
if err := c.getJSON(constants.BindInfoURL, cookie, "角色绑定", &result); err != nil {
if err := c.getJSON(c.Activity.BindInfoURL, cookie, "角色绑定", &result); err != nil {
return "", err
}
if result.Error != 0 {
@@ -434,7 +451,7 @@ type ExchangeListResponse struct {
// ExchangeList 查询兑换记录列表。
func (c *DouyuClient) ExchangeList(cookie string) ([]ExchangeRecord, error) {
var result ExchangeListResponse
if err := c.getJSON(constants.ExchangeListURL, cookie, "兑换记录", &result); err != nil {
if err := c.getJSON(c.Activity.ExchangeListURL, cookie, "兑换记录", &result); err != nil {
return nil, err
}
if result.Error != 0 {
@@ -606,8 +623,8 @@ func (c *DouyuClient) setBrowserHeaders(req *http.Request, cookie string) {
req.Header.Set("Accept", "application/json, text/plain, */*")
req.Header.Set("Accept-Language", "zh-CN,zh;q=0.9,en-US;q=0.6,en;q=0.5")
req.Header.Set("User-Agent", constants.UserAgent)
req.Header.Set("Origin", constants.BaseURL)
req.Header.Set("Referer", constants.ShopReferer)
req.Header.Set("Origin", c.Activity.BaseURL)
req.Header.Set("Referer", c.Activity.ShopReferer)
req.Header.Set("Sec-Fetch-Dest", "empty")
req.Header.Set("Sec-Fetch-Mode", "cors")
req.Header.Set("Sec-Fetch-Site", "same-origin")
+9 -2
View File
@@ -8,6 +8,7 @@ import (
"runtime"
"strconv"
"douyu_go/internal/activity"
"douyu_go/internal/constants"
"douyu_go/internal/logger"
"douyu_go/internal/types"
@@ -75,7 +76,8 @@ func envInt(key string, fallback int) int {
// defaultConfig 返回内置默认配置。
func defaultConfig() types.Config {
return types.Config{
TimeAPIURL: constants.DefaultTimeAPI,
Activity: activity.DefaultActivityID,
TimeAPIURL: activity.Default().TimeAPIURL,
TraceEnabled: false,
TraceMaxBody: 0,
LogLevel: "info",
@@ -118,8 +120,13 @@ func LoadConfig(path string) types.Config {
logger.LogEvent("配置", "warn", "解析 %s 失败,使用内置默认配置: %v", path, err)
return defaultConfig()
}
if cfg.Activity == "" {
cfg.Activity = activity.DefaultActivityID
}
if cfg.TimeAPIURL == "" {
cfg.TimeAPIURL = constants.DefaultTimeAPI
if act := activity.Get(cfg.Activity); act != nil {
cfg.TimeAPIURL = act.TimeAPIURL
}
}
if cfg.Concurrency <= 0 {
cfg.Concurrency = constants.DefaultConcurrency
-14
View File
@@ -9,21 +9,7 @@ const Version = "0.1.2"
var BuildTime = "unknown"
const (
ManualID = "G4KA4Qnz4LDp7"
RoomID = "9263298"
ActAlias = "20260120QYOOB"
BaseURL = "https://www.douyu.com"
ShopReferer = BaseURL + "/pages/live-peace-handbook/web/shop?ditchname=pass0&roomId=" + RoomID
DefaultTimeAPI = BaseURL + "/wgapi/ordnc/activity/peace/storedetail?manualID=" + ManualID + "&commodityID=20260120QYOOB_bag274&rid=" + RoomID
StoreHomeURL = BaseURL + "/wgapi/ordnc/activity/peace/storehome?manualID=" + ManualID + "&rid=" + RoomID
CSRFURL = BaseURL + "/japi/carnival/nc/common/generateCsrf"
BatchLimitURL = BaseURL + "/wgapi/ordnc/activity/peace/batchExchangeLimit"
PreExchangeURL = BaseURL + "/wgapi/ordnc/activity/peace/preExchange"
ExchangeURL = BaseURL + "/wgapi/ordnc/activity/peace/exchange"
ExchangeListURL = BaseURL + "/wgapi/ordnc/activity/peace/exchangeList?manualID=" + ManualID + "&limit=100&offset=0"
BindInfoURL = BaseURL + "/japi/carnivalApi/v2/tencent/bindInfo?actAlias=" + ActAlias
ScoreURL = BaseURL + "/japi/oms/web/peace/credit/balance"
UserAgent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/149.0.0.0 Safari/537.36"
DefaultAttempts = 2
DefaultConcurrency = 150
+7
View File
@@ -8,6 +8,7 @@ import (
// Config 应用配置。
type Config struct {
TimeAPIURL string `json:"timeApiUrl"`
Activity string `json:"activity"` // 当前活动 ID,见 internal/activity
TraceEnabled bool `json:"traceEnabled"`
TraceMaxBody int `json:"traceMaxBody"`
LogLevel string `json:"logLevel"` // debug / info / warn / error
@@ -17,6 +18,12 @@ type Config struct {
SeckillMode SeckillMode `json:"seckillMode"` // 抢购模式:0=标准, 1=激进
}
// ActivityInfo 活动概要,供前端下拉选择使用。
type ActivityInfo struct {
ID string `json:"id"`
DisplayName string `json:"displayName"`
}
// RetryStage 重试间隔阶段配置。
type RetryStage struct {
MaxElapsed int `json:"maxElapsed"` // 经过秒数上限