查询扭蛋碎片+点券

This commit is contained in:
yml2213
2026-08-08 17:50:00 +08:00
parent 379a9d58d3
commit 395096e229
8 changed files with 183 additions and 9 deletions
@@ -2,6 +2,7 @@
import { computed } from 'vue'
import { useAccounts } from '@/composables/useAccounts'
import { useProducts } from '@/composables/useProducts'
import { useActivity } from '@/composables/useActivity'
const {
accounts,
@@ -9,10 +10,13 @@ const {
statusMap,
toggleAccount,
toggleAll,
getScore
getScore,
getXiaodianAssets
} = useAccounts()
const { selectedProductName } = useProducts()
const { currentActivity } = useActivity()
const isXiaodian = computed(() => currentActivity.value?.id === 'xiaodian')
const allSelected = computed(() =>
accounts.value.length > 0 && selectedIndices.value.size === accounts.value.length
@@ -81,14 +85,15 @@ const getResultMessage = (name: string) => {
<th>账号</th>
<th>凭据</th>
<th>商品</th>
<th>积分</th>
<th>{{ isXiaodian ? '点券' : '积分' }}</th>
<th v-if="isXiaodian">扭蛋碎片</th>
<th class="status-col">状态</th>
<th>最后结果</th>
</tr>
</thead>
<tbody>
<tr v-if="accounts.length === 0">
<td colspan="7" class="empty-state">暂无账号,请加载账号</td>
<td :colspan="isXiaodian ? 8 : 7" class="empty-state">暂无账号,请加载账号</td>
</tr>
<template v-else>
<tr
@@ -121,7 +126,15 @@ const getResultMessage = (name: string) => {
</span>
<span v-else>-</span>
</td>
<td>{{ getScore(account.name) }}</td>
<td>
<template v-if="isXiaodian">
{{ getXiaodianAssets(account.name)?.success ? getXiaodianAssets(account.name)?.coupons : (getXiaodianAssets(account.name)?.error || '-') }}
</template>
<template v-else>{{ getScore(account.name) }}</template>
</td>
<td v-if="isXiaodian">
{{ getXiaodianAssets(account.name)?.success ? getXiaodianAssets(account.name)?.fragments : '-' }}
</td>
<td class="status-td">
<span
class="status-badge"
@@ -1,10 +1,13 @@
<script setup lang="ts">
import { ref } from 'vue'
import { computed, ref } from 'vue'
import { useScheduler } from '@/composables/useScheduler'
import { useAccounts } from '@/composables/useAccounts'
import { useActivity } from '@/composables/useActivity'
const { scheduleActive, startNow, startSchedule, cancelSchedule, restoreSchedule } = useScheduler()
const { reloadAccounts, queryAllScore } = useAccounts()
const { reloadAccounts, queryAllScore, queryAllXiaodianAssets } = useAccounts()
const { currentActivity } = useActivity()
const isXiaodian = computed(() => currentActivity.value?.id === 'xiaodian')
// 从本地缓存恢复定时设置
const { savedTime } = restoreSchedule()
@@ -13,6 +16,14 @@ const targetTime = ref(savedTime || '18:00:00')
const handleSchedule = () => {
startSchedule(targetTime.value)
}
const queryAssets = () => {
if (isXiaodian.value) {
queryAllXiaodianAssets()
return
}
queryAllScore()
}
</script>
<template>
@@ -34,7 +45,7 @@ const handleSchedule = () => {
停止定时
</button>
<button class="btn" @click="reloadAccounts">加载账号</button>
<button class="btn" @click="queryAllScore">查询积分</button>
<button class="btn" @click="queryAssets">{{ isXiaodian ? '查询点券/碎片' : '查询积分' }}</button>
</div>
<div class="schedule-bar">
@@ -1,7 +1,11 @@
<script setup lang="ts">
import { computed } from 'vue'
import { useProducts } from '@/composables/useProducts'
import { useActivity } from '@/composables/useActivity'
const { selectedProductId, selectedProductName, selectedProduct } = useProducts()
const { currentActivity } = useActivity()
const priceLabel = computed(() => currentActivity.value?.id === 'xiaodian' ? '点券' : '积分')
</script>
<template>
@@ -10,7 +14,7 @@ const { selectedProductId, selectedProductName, selectedProduct } = useProducts(
<div class="selected-product-name">{{ selectedProductName || '未选' }}</div>
<div class="selected-product-meta">
<template v-if="selectedProduct">
<span class="chip chip-score">{{ selectedProduct.score }} 积分</span>
<span class="chip chip-score">{{ selectedProduct.score }} {{ priceLabel }}</span>
<span class="chip chip-stock" :class="{ low: selectedProduct.count < 500 }">
库存 {{ selectedProduct.count }}
</span>
@@ -2,8 +2,11 @@
import { useVirtualizer } from '@tanstack/vue-virtual'
import { ref, computed, watch } from 'vue'
import { useProducts } from '@/composables/useProducts'
import { useActivity } from '@/composables/useActivity'
const { filteredProducts, selectedProductId, selectProduct, searchQuery } = useProducts()
const { currentActivity } = useActivity()
const priceLabel = computed(() => currentActivity.value?.id === 'xiaodian' ? '点券' : '积分')
const parentRef = ref<HTMLElement | null>(null)
@@ -56,7 +59,7 @@ const handleSelect = (commodityId: string, commodityName: string) => {
<div class="pinfo">
<div class="pname">{{ filteredProducts[item.index].commodityName }}</div>
<div class="pmeta">
<span class="chip chip-score">{{ filteredProducts[item.index].score }} 积分</span>
<span class="chip chip-score">{{ filteredProducts[item.index].score }} {{ priceLabel }}</span>
<span
class="chip chip-stock"
:class="{ low: filteredProducts[item.index].count < 500 }"
+25
View File
@@ -2,6 +2,7 @@ import { ref } from 'vue'
import { useToast } from './useToast'
import {
GetAccounts,
QueryXiaodianAssets,
QueryScore,
ReloadAccounts,
VerifyAllExchange,
@@ -24,6 +25,13 @@ const accounts = ref<Account[]>([])
const selectedIndices = ref<Set<number>>(new Set())
const statusMap = ref<Record<string, AccountStatus | boolean>>({})
const scoreMap = ref<Record<string, string>>({})
export interface XiaodianAssets {
success: boolean
coupons: number
fragments: number
error?: string
}
const xiaodianAssetsMap = ref<Record<string, XiaodianAssets>>({})
export function useAccounts() {
const { showToast } = useToast()
@@ -96,10 +104,24 @@ export function useAccounts() {
showToast('积分查询完成', '', 'success')
}
const queryAllXiaodianAssets = async () => {
showToast('查询点券和碎片', '正在查询账号资产...', 'info')
try {
const data = await QueryXiaodianAssets()
xiaodianAssetsMap.value = data?.results || {}
const successCount = Object.values(xiaodianAssetsMap.value).filter(item => item.success).length
showToast('资产查询完成', `成功 ${successCount}/${accounts.value.length} 个账号`, successCount > 0 ? 'success' : 'warn')
} catch (e: any) {
showToast('资产查询失败', e?.message || '', 'error')
}
}
const getScore = (name: string): string => {
return scoreMap.value[name] || localStorage.getItem('accScore_' + name) || '-'
}
const getXiaodianAssets = (name: string): XiaodianAssets | undefined => xiaodianAssetsMap.value[name]
const resetStatus = () => {
statusMap.value = {}
localStorage.removeItem('statusMap')
@@ -173,13 +195,16 @@ export function useAccounts() {
selectedIndices,
statusMap,
scoreMap,
xiaodianAssetsMap,
loadAccounts,
reloadAccounts,
toggleAccount,
toggleAll,
restoreSelection,
queryAllScore,
queryAllXiaodianAssets,
getScore,
getXiaodianAssets,
resetStatus,
setStatuses,
mergeStatuses,
+21
View File
@@ -36,6 +36,27 @@ func (a *App) QueryScore(accountName string) types.ScoreResult {
return types.ScoreResult{Score: strconv.Itoa(score)}
}
// QueryXiaodianAssets 一次查询所有账号的和平小店点券和扭蛋碎片。
func (a *App) QueryXiaodianAssets() types.XiaodianAssetsResult {
results := make(map[string]types.XiaodianAssets)
if !a.getClient().Activity.IsXiaodian {
return types.XiaodianAssetsResult{Results: results}
}
for _, account := range a.scheduler.AccountSnapshot() {
if strings.TrimSpace(account.Cookie) == "" {
results[account.Name] = types.XiaodianAssets{Error: "Cookie 为空"}
continue
}
assets, err := a.getClient().XiaodianAssets(account.Cookie)
if err != nil {
results[account.Name] = types.XiaodianAssets{Error: "查询失败: " + err.Error()}
continue
}
results[account.Name] = assets
}
return types.XiaodianAssetsResult{Results: results}
}
// VerifyExchange 查询兑换记录,用于复验抢购结果。
func (a *App) VerifyExchange(accountName string) types.ExchangeRecordsResult {
if accountName == "" {
+84
View File
@@ -280,6 +280,7 @@ type xiaodianRoleContext struct {
RoleID xiaodianFlexibleString `json:"roleId"`
Type xiaodianFlexibleString `json:"type"`
Area xiaodianFlexibleString `json:"area"`
PlatID xiaodianFlexibleString `json:"platId"`
}
// xiaodianEmbedQuery 获取斗鱼为当前账号签发的短时道具城参数。
@@ -504,6 +505,89 @@ func (c *DouyuClient) xiaodianProductPage(endpoint *url.URL, query url.Values) (
return items, nil
}
// XiaodianAssets 查询一个账号的点券与扭蛋碎片,角色信息只解析一次。
func (c *DouyuClient) XiaodianAssets(cookie string) (types.XiaodianAssets, error) {
if !c.Activity.IsXiaodian {
return types.XiaodianAssets{}, errors.New("当前活动不是和平小店")
}
if strings.TrimSpace(cookie) == "" {
return types.XiaodianAssets{}, errors.New("Cookie 为空")
}
embedQuery, err := c.xiaodianEmbedQuery(cookie)
if err != nil {
return types.XiaodianAssets{}, err
}
role, err := c.xiaodianRole(embedQuery)
if err != nil {
return types.XiaodianAssets{}, err
}
if role.OpenID == "" || role.RoleID == "" {
return types.XiaodianAssets{}, errors.New("未获取到和平小店绑定角色")
}
areaID := parseXiaodianInt(string(role.Area))
if areaID <= 0 {
areaID = 1
}
platID := parseXiaodianInt(string(role.PlatID))
if platID <= 0 {
platID = 1
}
coupons, err := c.xiaodianAsset(cookie, role, areaID, platID, "banlanceInfo", "pay.midas.dq.get.ttpp", "balance", true)
if err != nil {
return types.XiaodianAssets{}, err
}
fragments, err := c.xiaodianAsset(cookie, role, areaID, platID, "jbInfos", "pay.idip.gamecoin.get", "jb2", false)
if err != nil {
return types.XiaodianAssets{}, err
}
return types.XiaodianAssets{Success: true, Coupons: coupons, Fragments: fragments}, nil
}
func (c *DouyuClient) xiaodianAsset(cookie string, role xiaodianRoleContext, areaID, platID int, variable, service, field string, withTime bool) (int, error) {
endpoint, err := url.Parse(strings.TrimRight(c.Activity.RoleURL, "/") + "/")
if err != nil {
return 0, err
}
query := endpoint.Query()
query.Set("_jsvar", variable)
query.Set("_service", service)
query.Set("_app_id", "2123")
query.Set("acctype", "ttpp")
query.Set("areaid", strconv.Itoa(areaID))
query.Set("eventid", "")
query.Set("interwork", "0")
query.Set("openid", string(role.OpenID))
query.Set("openkey", "openkey")
query.Set("partition", "0")
query.Set("pay_token", "")
query.Set("plat", strconv.Itoa(platID))
query.Set("plat_pc", "0")
query.Set("roleid", string(role.RoleID))
query.Set("_biz_code", "cjm")
query.Set("_act_id", c.Activity.ActID)
query.Set("_sid", "6")
if withTime {
query.Set("_time", strconv.FormatInt(time.Now().Unix(), 10))
} else {
query.Set("coin_type", "1")
}
endpoint.RawQuery = query.Encode()
req, err := http.NewRequest(http.MethodGet, endpoint.String(), nil)
if err != nil {
return 0, err
}
c.setXiaodianHeaders(req)
body, err := c.doText(req, "查询小店资产", true)
if err != nil {
return 0, err
}
var payload map[string]xiaodianFlexibleString
if err := parseXiaodianVariable(body, variable, &payload); err != nil {
return 0, fmt.Errorf("和平小店%s响应解析失败: %w", field, err)
}
return parseXiaodianInt(string(payload[field])), nil
}
// parseXiaodianInt 将 QQ 道具接口中的字符串数值转为界面商品字段。
func parseXiaodianInt(value string) int {
parsed, err := strconv.Atoi(strings.TrimSpace(value))
+13
View File
@@ -61,6 +61,19 @@ type ScoreResult struct {
Score string `json:"score"`
}
// XiaodianAssets 单个账号的和平小店点券与扭蛋碎片余额。
type XiaodianAssets struct {
Success bool `json:"success"`
Coupons int `json:"coupons"`
Fragments int `json:"fragments"`
Error string `json:"error,omitempty"`
}
// XiaodianAssetsResult 批量查询和平小店资产的结果。
type XiaodianAssetsResult struct {
Results map[string]XiaodianAssets `json:"results"`
}
// ExchangeRecordsResult 兑换记录查询结果。
type ExchangeRecordsResult struct {
Success bool `json:"success"`