增加电竞手册
This commit is contained in:
@@ -24,3 +24,4 @@ ck 对比.md
|
||||
*.app
|
||||
.DS_Store
|
||||
frontend/dist/
|
||||
.workbuddy
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
Vendored
+3
@@ -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 }>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"` // 经过秒数上限
|
||||
|
||||
Reference in New Issue
Block a user