From ac6ba0d98c0145c424791fcbd0014d3c41afd241 Mon Sep 17 00:00:00 2001 From: yml2213 Date: Sat, 3 Oct 2026 20:13:07 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8A=A2=E8=B4=AD=E6=8E=A7=E5=88=B6=E5=8F=B0?= =?UTF-8?q?=E6=94=AF=E6=8C=81=E6=9F=A5=E7=9C=8B=E7=B2=BE=E8=8B=B1=E6=89=8B?= =?UTF-8?q?=E5=86=8C=E5=85=91=E6=8D=A2=E8=AE=B0=E5=BD=95=EF=BC=8C=E5=8F=91?= =?UTF-8?q?=E5=B8=830.3.7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 此前只有和平小店能在控制台查兑换记录,精英/电竞手册只能去别的页面或客户端看。 现在账号作战表直接带上"兑换记录"列:查询一次就能看到每个账号兑换了什么、什么时候、 花多少积分,点"记录"看该账号全部明细。 - 新增 QueryExchangeRecords 批量查询(手册类活动):复用既有的并发 lookupExchangeRecords, 记录不做任何过滤("看我的兑换记录"不该藏东西),记录自带商品名与积分 - 商品筛选为可选项:不选=看全部记录;选了则额外标记该账号是否兑换过它(已兑换/未兑换) - 手册的商品筛选用本地状态,不写回全局商品选择——免得只是想看记录却把抢购目标改掉; 和平小店沿用原来的全局商品选择 - 账号表"兑换记录"列对所有活动可见;手册侧展示条数与最近一条(商品名 + 时间), 小店的"已兑换/未兑换 + 订单状态"保持原样 - 每行"记录"弹窗:若已批量查询过就直接复用结果不再打接口;否则手册走 VerifyExchange, 小店走原有订单查询。弹窗表格按活动切换列(手册:商品/数量/兑换时间/积分/昵称) - 聚合逻辑抽成纯函数 buildExchangeRecordsResult 并补测试 (记录不过滤、命中按商品统计、空 Cookie 与查询失败分开计数、未选商品不做命中判断) --- .../src/components/account/AccountTable.vue | 179 +++++++++++++++--- internal/app/app_exchange_records_test.go | 70 +++++++ internal/app/app_verify.go | 60 ++++++ internal/constants/constants.go | 2 +- internal/types/responses.go | 22 +++ wails.json | 4 +- 6 files changed, 306 insertions(+), 31 deletions(-) create mode 100644 internal/app/app_exchange_records_test.go diff --git a/frontend/src/components/account/AccountTable.vue b/frontend/src/components/account/AccountTable.vue index 7536faf..23b96c3 100644 --- a/frontend/src/components/account/AccountTable.vue +++ b/frontend/src/components/account/AccountTable.vue @@ -8,8 +8,10 @@ import { useActivity } from '@/composables/useActivity' import { useToast } from '@/composables/useToast' import { ExportLatestXiaodianReport, + QueryExchangeRecords, QueryXiaodianAccountExchangeRecords, - QueryXiaodianExchangeRecords + QueryXiaodianExchangeRecords, + VerifyExchange } from '@wails/go/app/App' const { @@ -56,6 +58,11 @@ watch(selectedProductId, () => { exchangeLookup.value = null }) +watch(currentActivity, () => { + exchangeLookup.value = null + lookupProductId.value = '' +}) + const handleDocumentClick = (event: MouseEvent) => { const target = event.target if (!(target instanceof Element) || !target.closest('.account-product-picker')) { @@ -168,6 +175,21 @@ const selectedLookupProduct = computed(() => products.value.find(product => product.commodityId === selectedProductId.value) ) +// lookupProductId 只用于"手册类活动的兑换记录筛选",刻意不写回全局商品选择—— +// 免得只是想看看记录,却把抢购目标商品改掉了。 +const lookupProductId = ref('') + +const lookupCommodity = computed(() => { + if (isXiaodian.value) { + return { + id: selectedLookupProduct.value?.commodityId || '', + name: selectedLookupProduct.value?.commodityName || '' + } + } + const product = products.value.find(item => item.commodityId === lookupProductId.value) + return { id: lookupProductId.value, name: product?.commodityName || '' } +}) + const handleLookupProductChange = (event: Event) => { const productID = (event.target as HTMLSelectElement).value const product = products.value.find(item => item.commodityId === productID) @@ -179,15 +201,19 @@ const handleLookupProductChange = (event: Event) => { } const querySelectedExchangeRecords = async () => { - if (!selectedLookupProduct.value) { + // 和平小店必须选商品(记录按商品归属);手册类活动的记录自带商品名,商品可不选 + if (isXiaodian.value && !selectedLookupProduct.value) { showToast('提示', '请先选择要查询的商品', 'warn') return } - const { commodityId, commodityName } = selectedLookupProduct.value + const commodityId = lookupCommodity.value.id + const commodityName = lookupCommodity.value.name exchangeLookup.value = { productId: commodityId, productName: commodityName, loading: true, results: {} } try { - const result = await QueryXiaodianExchangeRecords(commodityId, commodityName) + const result = isXiaodian.value + ? await QueryXiaodianExchangeRecords(commodityId, commodityName) + : await QueryExchangeRecords(commodityId, commodityName) if (!exchangeLookup.value || exchangeLookup.value.productId !== commodityId) return exchangeLookup.value.loading = false exchangeLookup.value.results = result.results || {} @@ -195,15 +221,44 @@ const querySelectedExchangeRecords = async () => { showToast('查询失败', result.error || '兑换记录接口未返回成功', 'error') return } - const matched = Object.values(result.results || {}).filter((item: any) => item.success && item.matched).length - const failed = Object.values(result.results || {}).filter((item: any) => !item.success).length - showToast('查询完成', `${commodityName}:${matched} 个账号有兑换记录${failed ? `,${failed} 个查询失败` : ''}`, failed ? 'warn' : 'success') + const items = Object.values(result.results || {}) as any[] + const failed = items.filter((item) => !item.success).length + const total = items.reduce((sum, item) => sum + (item.records?.length || 0), 0) + if (commodityId) { + const matched = items.filter((item) => item.success && item.matched).length + showToast('查询完成', `${commodityName}:${matched} 个账号有兑换记录${failed ? `,${failed} 个查询失败` : ''}`, failed ? 'warn' : 'success') + } else { + showToast('查询完成', `共 ${total} 条兑换记录${failed ? `,${failed} 个账号查询失败` : ''}`, failed ? 'warn' : 'success') + } } catch (e: any) { if (exchangeLookup.value?.productId === commodityId) exchangeLookup.value.loading = false showToast('查询失败', e?.message || '兑换记录查询失败', 'error') } } +// recordSummary 取账号最新的兑换记录,用于表格里的紧凑展示。 +const recordSummary = (accountName: string) => { + const records = getLookupResult(accountName)?.records || [] + if (!records.length) return '' + const latest = records[0] + const name = latest.commodityName || latest.commodityId || '-' + const time = formatShortTime(latest.time || latest.buyTime || '') + return time ? `${name} · ${time}` : name +} + +const formatShortTime = (value: string) => { + if (!value) return '' + const text = String(value) + const match = text.match(/(\d{2})[-/](\d{2})\s+(\d{2}:\d{2})/) + if (match) return `${match[1]}-${match[2]} ${match[3]}` + const numeric = Number(text) + if (Number.isFinite(numeric) && numeric > 0) { + const date = new Date(numeric < 10_000_000_000 ? numeric * 1000 : numeric) + return `${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')} ${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}` + } + return text.replace('T', ' ').slice(0, 16) +} + const getLookupResult = (accountName: string) => exchangeLookup.value?.results[accountName] const getMatchedRecord = (accountName: string) => { @@ -264,9 +319,18 @@ const orderStatus = (status: string) => { } const openExchangeRecords = async (accountName: string) => { + // 已经批量查过就直接用现成结果,避免同一个账号再打一次接口 + const cached = getLookupResult(accountName) + if (cached?.success && Array.isArray(cached.records)) { + recordsModal.value = { accountName, loading: false, error: '', records: cached.records } + return + } + recordsModal.value = { accountName, loading: true, error: '', records: [] } try { - const result = await QueryXiaodianAccountExchangeRecords(accountName) + const result = isXiaodian.value + ? await QueryXiaodianAccountExchangeRecords(accountName) + : await VerifyExchange(accountName) if (!recordsModal.value || recordsModal.value.accountName !== accountName) return recordsModal.value.loading = false recordsModal.value.error = result.error || '' @@ -292,8 +356,23 @@ const closeExchangeRecords = () => { 账号作战表 {{ accounts.length }} 个账号,已选 {{ selectedIndices.size }} 个 -
+
+ + + - - +
@@ -343,7 +424,7 @@ const closeExchangeRecords = () => { {{ isXiaodian ? '点券' : '积分' }} 扭蛋碎片 角色信息 - 兑换记录 + 兑换记录 状态 最后结果 操作 @@ -351,7 +432,7 @@ const closeExchangeRecords = () => { - 暂无账号,请加载账号 + 暂无账号,请加载账号