diff --git a/.gitignore b/.gitignore index 5c2506c..bd2d3fd 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,4 @@ ck 对比.md *.app .DS_Store frontend/dist/ +.workbuddy \ No newline at end of file diff --git a/app.go b/app.go index 0b16204..0410e7c 100644 --- a/app.go +++ b/app.go @@ -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 -} diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 2f9b584..bd7f8e6 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -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) // 获取版本号 diff --git a/frontend/src/components/layout/LeftSidebar.vue b/frontend/src/components/layout/LeftSidebar.vue index 863bf81..a465890 100644 --- a/frontend/src/components/layout/LeftSidebar.vue +++ b/frontend/src/components/layout/LeftSidebar.vue @@ -1,5 +1,6 @@ @@ -22,6 +32,25 @@ const { loadProducts, productInfo, filteredProducts, loading } = useProducts() v{{ version }} + + + 当前活动 + + + 加载中... + + {{ act.displayName }} + + + 切换中… + + + @@ -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); diff --git a/frontend/src/composables/useActivity.ts b/frontend/src/composables/useActivity.ts new file mode 100644 index 0000000..38fcca9 --- /dev/null +++ b/frontend/src/composables/useActivity.ts @@ -0,0 +1,64 @@ +import { ref } from 'vue' +import { useToast } from './useToast' + +// 活动项(与后端 types.ActivityInfo 对齐) +export interface ActivityItem { + id: string + displayName: string +} + +// 模块级单例:活动列表与当前活动在所有组件中共享同一份状态 +const activities = ref([]) +const currentActivity = ref(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 => { + if (id === currentActivity.value?.id) return true + switching.value = true + try { + const d: Record = 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 + } +} diff --git a/frontend/src/env.d.ts b/frontend/src/env.d.ts index b561442..fe4fbcf 100644 --- a/frontend/src/env.d.ts +++ b/frontend/src/env.d.ts @@ -31,6 +31,9 @@ interface Window { ExportAccounts: (accountNames: string[], filename: string) => Promise ExportAccountsWithProduct: (accountNames: string[], commodityID: string, commodityName: string) => Promise OrganizeAccounts: (commodityID: string, commodityName: string, startTime: number, endTime: number) => Promise + GetActivities: () => Promise> + GetCurrentActivity: () => Promise<{ id: string; displayName: string }> + SwitchActivity: (id: string) => Promise<{ success: boolean; activity?: string; displayName?: string; error?: string }> } } } diff --git a/internal/activity/activity.go b/internal/activity/activity.go new file mode 100644 index 0000000..3225aeb --- /dev/null +++ b/internal/activity/activity.go @@ -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 +} diff --git a/internal/client/client.go b/internal/client/client.go index f1b4797..d16238f 100644 --- a/internal/client/client.go +++ b/internal/client/client.go @@ -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") diff --git a/internal/config/config.go b/internal/config/config.go index 8dfdb94..3345ef3 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -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 diff --git a/internal/constants/constants.go b/internal/constants/constants.go index 119e0a8..be570a1 100644 --- a/internal/constants/constants.go +++ b/internal/constants/constants.go @@ -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 diff --git a/internal/types/types.go b/internal/types/types.go index 75c53b1..689faad 100644 --- a/internal/types/types.go +++ b/internal/types/types.go @@ -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"` // 经过秒数上限