新增: 兑换复验功能
- 新增 exchangeList 接口查询斗鱼真实兑换记录
- 前端添加「兑换复验」标签页
- 点击「🔍 复验」按钮可查询所有账号的兑换记录
- 复验结果与软件内部状态对比,用于验证抢购结果
- 移除 frontend/dist 的 git 跟踪
This commit is contained in:
+2
-1
@@ -5,7 +5,7 @@ logs/
|
|||||||
success_images/
|
success_images/
|
||||||
build/
|
build/
|
||||||
config.json
|
config.json
|
||||||
|
frontend/dist
|
||||||
frontend/node_modules
|
frontend/node_modules
|
||||||
|
|
||||||
# Wails 自动生成的前端绑定
|
# Wails 自动生成的前端绑定
|
||||||
@@ -23,3 +23,4 @@ ck 对比.md
|
|||||||
*.exe
|
*.exe
|
||||||
*.app
|
*.app
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
frontend/dist/
|
||||||
|
|||||||
@@ -210,6 +210,36 @@ func (a *App) QueryScore(accountName string) map[string]any {
|
|||||||
return map[string]any{"score": score}
|
return map[string]any{"score": score}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// VerifyExchange 查询兑换记录,用于复验抢购结果。
|
||||||
|
func (a *App) VerifyExchange(accountName string) map[string]any {
|
||||||
|
if accountName == "" {
|
||||||
|
return map[string]any{"success": false, "error": "缺少账号名"}
|
||||||
|
}
|
||||||
|
cookie, ok := a.scheduler.AccountCookieByName(accountName)
|
||||||
|
if !ok || cookie == "" {
|
||||||
|
return map[string]any{"success": false, "error": fmt.Sprintf("账号不存在或 Cookie 为空: %s", accountName)}
|
||||||
|
}
|
||||||
|
|
||||||
|
records, err := a.client.ExchangeList(cookie)
|
||||||
|
if err != nil {
|
||||||
|
return map[string]any{"success": false, "error": "查询失败: " + err.Error()}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 过滤当前活动的记录
|
||||||
|
var matched []client.ExchangeRecord
|
||||||
|
for _, r := range records {
|
||||||
|
if r.CommodityID != "" {
|
||||||
|
matched = append(matched, r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return map[string]any{
|
||||||
|
"success": true,
|
||||||
|
"records": matched,
|
||||||
|
"count": len(matched),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// GetVersion 返回应用版本号。
|
// GetVersion 返回应用版本号。
|
||||||
func (a *App) GetVersion() string {
|
func (a *App) GetVersion() string {
|
||||||
return constants.Version
|
return constants.Version
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, watch, nextTick } from 'vue'
|
import { ref, computed, watch, nextTick } from 'vue'
|
||||||
import { useLogs } from '@/composables/useLogs'
|
import { useLogs } from '@/composables/useLogs'
|
||||||
import { useAccounts } from '@/composables/useAccounts'
|
import { useAccounts, useVerifyExchange } from '@/composables/useAccounts'
|
||||||
|
import { useToast } from '@/composables/useToast'
|
||||||
|
|
||||||
const { logs, results, clearLogs } = useLogs()
|
const { logs, results, clearLogs } = useLogs()
|
||||||
const { accounts, selectedIndices, statusMap } = useAccounts()
|
const { accounts, selectedIndices, statusMap } = useAccounts()
|
||||||
|
const { verifyResults, verifying, verifyAll, clearResults } = useVerifyExchange()
|
||||||
|
const { showToast } = useToast()
|
||||||
|
|
||||||
const activeTab = ref<'log' | 'result' | 'stats'>('log')
|
const activeTab = ref<'log' | 'result' | 'stats' | 'verify'>('log')
|
||||||
const logBodyRef = ref<HTMLElement | null>(null)
|
const logBodyRef = ref<HTMLElement | null>(null)
|
||||||
|
|
||||||
// 自动滚动到底部
|
// 自动滚动到底部
|
||||||
@@ -46,6 +49,31 @@ const stats = computed(() => {
|
|||||||
|
|
||||||
// 结果列表
|
// 结果列表
|
||||||
const resultEntries = computed(() => Object.entries(results.value))
|
const resultEntries = computed(() => Object.entries(results.value))
|
||||||
|
|
||||||
|
// 复验结果列表
|
||||||
|
const verifyEntries = computed(() => {
|
||||||
|
return Object.entries(verifyResults.value).map(([name, result]) => ({
|
||||||
|
name,
|
||||||
|
...result
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
|
||||||
|
// 复验统计
|
||||||
|
const verifyStats = computed(() => {
|
||||||
|
const entries = verifyEntries.value
|
||||||
|
const verified = entries.filter(e => e.success && e.count > 0).length
|
||||||
|
const noRecord = entries.filter(e => e.success && e.count === 0).length
|
||||||
|
const failed = entries.filter(e => !e.success).length
|
||||||
|
return { verified, noRecord, failed, total: entries.length }
|
||||||
|
})
|
||||||
|
|
||||||
|
// 执行复验
|
||||||
|
const handleVerify = async () => {
|
||||||
|
showToast('复验开始', '正在查询斗鱼兑换记录...', 'info')
|
||||||
|
await verifyAll()
|
||||||
|
activeTab.value = 'verify'
|
||||||
|
showToast('复验完成', `已查询 ${verifyStats.value.total} 个账号`, 'success')
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -73,8 +101,24 @@ const resultEntries = computed(() => Object.entries(results.value))
|
|||||||
>
|
>
|
||||||
统计
|
统计
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
class="log-tab"
|
||||||
|
:class="{ active: activeTab === 'verify' }"
|
||||||
|
@click="activeTab = 'verify'"
|
||||||
|
>
|
||||||
|
兑换复验
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="log-actions">
|
||||||
|
<button
|
||||||
|
class="btn btn-compact btn-verify"
|
||||||
|
:disabled="verifying"
|
||||||
|
@click="handleVerify"
|
||||||
|
>
|
||||||
|
{{ verifying ? '查询中...' : '🔍 复验' }}
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-compact" @click="clearLogs">清空</button>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn btn-compact" @click="clearLogs">清空</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 实时日志 -->
|
<!-- 实时日志 -->
|
||||||
@@ -129,6 +173,47 @@ const resultEntries = computed(() => Object.entries(results.value))
|
|||||||
<div class="log-result log-i">待处理账号: {{ stats.pending }}</div>
|
<div class="log-result log-i">待处理账号: {{ stats.pending }}</div>
|
||||||
<div class="log-result">已选账号: {{ stats.selected }} / 总账号: {{ stats.total }}</div>
|
<div class="log-result">已选账号: {{ stats.selected }} / 总账号: {{ stats.total }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 兑换复验 -->
|
||||||
|
<div
|
||||||
|
v-show="activeTab === 'verify'"
|
||||||
|
class="log-panel"
|
||||||
|
>
|
||||||
|
<div v-if="verifyEntries.length === 0" class="result-empty">
|
||||||
|
点击「🔍 复验」按钮查询斗鱼真实兑换记录
|
||||||
|
</div>
|
||||||
|
<template v-else>
|
||||||
|
<div class="verify-summary">
|
||||||
|
<span class="log-s">✅ 已兑换: {{ verifyStats.verified }}</span>
|
||||||
|
<span class="log-w">⚠️ 无记录: {{ verifyStats.noRecord }}</span>
|
||||||
|
<span class="log-e">❌ 查询失败: {{ verifyStats.failed }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="verify-list">
|
||||||
|
<div
|
||||||
|
v-for="entry in verifyEntries"
|
||||||
|
:key="entry.name"
|
||||||
|
class="verify-item"
|
||||||
|
:class="{
|
||||||
|
'verify-success': entry.success && entry.count > 0,
|
||||||
|
'verify-empty': entry.success && entry.count === 0,
|
||||||
|
'verify-error': !entry.success
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<div class="verify-account">{{ entry.name }}</div>
|
||||||
|
<template v-if="entry.success">
|
||||||
|
<div v-if="entry.count > 0" class="verify-records">
|
||||||
|
<div v-for="(record, idx) in entry.records" :key="idx" class="verify-record">
|
||||||
|
<span class="record-name">{{ record.commodityName }}</span>
|
||||||
|
<span class="record-role">{{ record.showName }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else class="verify-no-record">暂无兑换记录</div>
|
||||||
|
</template>
|
||||||
|
<div v-else class="verify-error-msg">{{ entry.error }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -249,6 +334,105 @@ const resultEntries = computed(() => Object.entries(results.value))
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 复验相关样式 */
|
||||||
|
.log-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 6px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-verify {
|
||||||
|
background: var(--accent-soft);
|
||||||
|
color: var(--accent);
|
||||||
|
border-color: var(--accent-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-verify:hover {
|
||||||
|
background: var(--accent);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-verify:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.verify-summary {
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
background: var(--surface);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.verify-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.verify-item {
|
||||||
|
padding: 10px 12px;
|
||||||
|
background: var(--surface);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
}
|
||||||
|
|
||||||
|
.verify-success {
|
||||||
|
border-color: var(--success);
|
||||||
|
background: var(--success-soft);
|
||||||
|
}
|
||||||
|
|
||||||
|
.verify-empty {
|
||||||
|
border-color: var(--warning);
|
||||||
|
background: var(--warning-soft);
|
||||||
|
}
|
||||||
|
|
||||||
|
.verify-error {
|
||||||
|
border-color: var(--danger);
|
||||||
|
background: var(--danger-soft);
|
||||||
|
}
|
||||||
|
|
||||||
|
.verify-account {
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-strong);
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.verify-records {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.verify-record {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.record-name {
|
||||||
|
color: var(--text);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.record-role {
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.verify-no-record {
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.verify-error-msg {
|
||||||
|
color: var(--danger);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 1180px) {
|
@media (max-width: 1180px) {
|
||||||
.log-wrap {
|
.log-wrap {
|
||||||
height: 360px;
|
height: 360px;
|
||||||
|
|||||||
@@ -153,3 +153,50 @@ export function useAccounts() {
|
|||||||
buildAccountProducts
|
buildAccountProducts
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function useVerifyExchange() {
|
||||||
|
const { accounts } = useAccounts()
|
||||||
|
const verifyResults = ref<Record<string, any>>({})
|
||||||
|
const verifying = ref(false)
|
||||||
|
|
||||||
|
const verifyAccount = async (accountName: string) => {
|
||||||
|
try {
|
||||||
|
const result = await App.VerifyExchange(accountName)
|
||||||
|
verifyResults.value[accountName] = result
|
||||||
|
return result
|
||||||
|
} catch (e: any) {
|
||||||
|
verifyResults.value[accountName] = { success: false, error: e.message }
|
||||||
|
return { success: false, error: e.message }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const verifyAll = async () => {
|
||||||
|
verifying.value = true
|
||||||
|
const results: Record<string, any> = {}
|
||||||
|
|
||||||
|
for (const account of accounts.value) {
|
||||||
|
try {
|
||||||
|
const result = await App.VerifyExchange(account.name)
|
||||||
|
results[account.name] = result
|
||||||
|
} catch (e: any) {
|
||||||
|
results[account.name] = { success: false, error: e.message }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
verifyResults.value = results
|
||||||
|
verifying.value = false
|
||||||
|
return results
|
||||||
|
}
|
||||||
|
|
||||||
|
const clearResults = () => {
|
||||||
|
verifyResults.value = {}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
verifyResults,
|
||||||
|
verifying,
|
||||||
|
verifyAccount,
|
||||||
|
verifyAll,
|
||||||
|
clearResults
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Vendored
+1
@@ -25,6 +25,7 @@ interface Window {
|
|||||||
GetVersion: () => Promise<string>
|
GetVersion: () => Promise<string>
|
||||||
GetSettings: () => Promise<any>
|
GetSettings: () => Promise<any>
|
||||||
SaveSettings: (cfg: any) => Promise<any>
|
SaveSettings: (cfg: any) => Promise<any>
|
||||||
|
VerifyExchange: (accountName: string) => Promise<any>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -406,6 +406,39 @@ func (c *DouyuClient) RoleName(cookie string) (string, error) {
|
|||||||
return result.Data.GameRole.RoleName, nil
|
return result.Data.GameRole.RoleName, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ExchangeRecord 兑换记录。
|
||||||
|
type ExchangeRecord struct {
|
||||||
|
CommodityID string `json:"commodityId"`
|
||||||
|
CommodityName string `json:"commodityName"`
|
||||||
|
NickName string `json:"nickName"`
|
||||||
|
ShowName string `json:"showName"`
|
||||||
|
ExchangeNum int `json:"exchangeNum"`
|
||||||
|
Time string `json:"time"`
|
||||||
|
Score string `json:"score"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExchangeListResponse 兑换列表响应。
|
||||||
|
type ExchangeListResponse struct {
|
||||||
|
Error int `json:"error"`
|
||||||
|
Msg string `json:"msg"`
|
||||||
|
Data struct {
|
||||||
|
List []ExchangeRecord `json:"list"`
|
||||||
|
Count int `json:"count"`
|
||||||
|
} `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExchangeList 查询兑换记录列表。
|
||||||
|
func (c *DouyuClient) ExchangeList(cookie string) ([]ExchangeRecord, error) {
|
||||||
|
var result ExchangeListResponse
|
||||||
|
if err := c.getJSON(constants.ExchangeListURL, cookie, "兑换记录", &result); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if result.Error != 0 {
|
||||||
|
return nil, fmt.Errorf("兑换记录查询失败: %s", result.Msg)
|
||||||
|
}
|
||||||
|
return result.Data.List, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (c *DouyuClient) getJSON(url, cookie, label string, out any) error {
|
func (c *DouyuClient) getJSON(url, cookie, label string, out any) error {
|
||||||
req, err := http.NewRequest("GET", url, nil)
|
req, err := http.NewRequest("GET", url, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ const (
|
|||||||
BatchLimitURL = BaseURL + "/wgapi/ordnc/activity/peace/batchExchangeLimit"
|
BatchLimitURL = BaseURL + "/wgapi/ordnc/activity/peace/batchExchangeLimit"
|
||||||
PreExchangeURL = BaseURL + "/wgapi/ordnc/activity/peace/preExchange"
|
PreExchangeURL = BaseURL + "/wgapi/ordnc/activity/peace/preExchange"
|
||||||
ExchangeURL = BaseURL + "/wgapi/ordnc/activity/peace/exchange"
|
ExchangeURL = BaseURL + "/wgapi/ordnc/activity/peace/exchange"
|
||||||
|
ExchangeListURL = BaseURL + "/wgapi/ordnc/activity/peace/exchangeList?manualID=" + ManualID + "&limit=50&offset=0"
|
||||||
BindInfoURL = BaseURL + "/japi/carnivalApi/v2/tencent/bindInfo?actAlias=" + ActAlias
|
BindInfoURL = BaseURL + "/japi/carnivalApi/v2/tencent/bindInfo?actAlias=" + ActAlias
|
||||||
ScoreURL = BaseURL + "/japi/oms/web/peace/credit/balance"
|
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"
|
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"
|
||||||
|
|||||||
Reference in New Issue
Block a user