账号作战表新增「兑换结果」列,移除已废弃的分析文档

- 账号作战表新增「兑换结果」列:明确展示 已抢到(含奖品名与订单号)/ 未成功(含原因与尝试次数)/ 已兑换(来自服务端兑换记录兜底)/ 抢购中 / 待抢购 / 已跳过重复兑换
- 抢到奖品的账号整行标绿并加左侧色条,一眼可见
- 复盘表时间列改为可读时间(秒级时间戳格式化)
- 按用户确认删除已废弃的 purchase_logic_analysis.md
This commit is contained in:
yml2213
2026-10-13 20:46:42 +08:00
parent 41565566c5
commit f96fa7d858
3 changed files with 147 additions and 1357 deletions
+138 -3
View File
@@ -210,6 +210,22 @@ type RecordRow = UserPrizeInfo & {
uid: number
}
// exchangeSummary 汇总某个账号本次运行里的兑换结果,供「兑换结果」列展示。
type ExchangeSummary = {
success?: MonitorResult
failed?: MonitorResult
attempts: number
skipped: number
}
function isExchangeAttempt(action?: string): boolean {
return action === 'exchange' || action === 'exchange_error'
}
function exchangeOrderID(exchange?: ExchangeResp): string {
return exchange?.order_id || exchange?.orderId || ''
}
type SavedConfig = Omit<UiConfig, 'cookieText' | 'selectedAccountLines'>
type ScheduleCountdown = {
@@ -303,6 +319,14 @@ function summarizeInventory(items: PrizeInfo[]): string {
return `剩余 ${leftKnown ? left : '-'} / 总量 ${totalKnown ? total : '-'}`
}
function formatUnixTime(value?: number): string {
const ts = Number(value || 0)
if (!ts) return ''
// 服务端返回秒级时间戳,兼容毫秒。
const ms = ts > 1e12 ? ts : ts * 1000
return new Date(ms).toLocaleString('zh-CN', { hour12: false })
}
function statusTag(status?: number, error?: string): ReactNode {
if (error) return <Tag color="error">失败</Tag>
if (status === 200) return <Tag color="success">成功</Tag>
@@ -615,6 +639,49 @@ export default function Dashboard() {
return map
}, [results])
// 本次运行里各账号的兑换结果(results 为最新在前)。
const exchangeByUID = useMemo(() => {
const map = new Map<number, ExchangeSummary>()
results.forEach((item) => {
if (item.action === 'skip_duplicate') {
const entry = map.get(item.uid) ?? { attempts: 0, skipped: 0 }
entry.skipped += 1
map.set(item.uid, entry)
return
}
if (!isExchangeAttempt(item.action)) return
const entry = map.get(item.uid) ?? { attempts: 0, skipped: 0 }
entry.attempts += 1
if (item.category === 'success') {
if (!entry.success) entry.success = item
} else if (!entry.failed) {
entry.failed = item
}
map.set(item.uid, entry)
})
return map
}, [results])
// 用服务端兑换记录兜底,界面刷新后也能看出该账号是否已抢到当前奖品。
const recordHitByUID = useMemo(() => {
const map = new Map<number, UserPrizeInfo>()
const pid = Number(watchedPID || 0)
const name = String(watchedName || '').trim()
if (pid <= 0 && !name) return map
records.forEach((item) => {
;(item.records || []).forEach((record) => {
const hitPID = pid > 0 && Number(record.prize_id || 0) === pid
const hitName = name !== '' && String(record.name || '').includes(name)
if (!hitPID && !hitName) return
const prev = map.get(item.uid)
if (!prev || Number(record.create_time || 0) > Number(prev.create_time || 0)) {
map.set(item.uid, record)
}
})
})
return map
}, [records, watchedPID, watchedName])
const successCount = useMemo(
() => new Set(results.filter((item) => item.category === 'success').map((item) => item.uid)).size,
[results],
@@ -983,6 +1050,63 @@ export default function Dashboard() {
width: 84,
render: (_, row) => scoreMap.get(row.uid)?.score ?? '-',
},
{
title: '兑换结果',
width: 190,
render: (_, row) => {
const exchange = exchangeByUID.get(row.uid)
if (exchange?.success) {
const orderID = exchangeOrderID(exchange.success.exchange)
return (
<div>
<Tag color="success">已抢到</Tag>
<div className="muted">{exchange.success.prize?.name || currentPrize?.name || ''}</div>
{orderID ? (
<div className="muted" title={orderID}>
单号 {orderID}
</div>
) : null}
</div>
)
}
if (exchange?.failed) {
return (
<div>
<Tag color="error">未成功</Tag>
<div className="muted">
{exchange.failed.error ||
exchange.failed.message ||
exchange.failed.exchange?.msg ||
'兑换失败'}
</div>
{exchange.attempts > 1 ? (
<div className="muted">已尝试 {exchange.attempts} 次</div>
) : null}
</div>
)
}
const record = recordHitByUID.get(row.uid)
if (record) {
return (
<div>
<Tag color="blue">已兑换</Tag>
<div className="muted">{record.name || ''}</div>
{record.order_id ? (
<div className="muted" title={record.order_id}>
单号 {record.order_id}
</div>
) : null}
</div>
)
}
return (
<div>
<Tag color={running ? 'processing' : 'default'}>{running ? '抢购中' : '待抢购'}</Tag>
{exchange?.skipped ? <div className="muted">已跳过重复兑换</div> : null}
</div>
)
},
},
{
title: '状态',
width: 98,
@@ -997,7 +1121,15 @@ export default function Dashboard() {
},
},
],
[currentPrize?.name, latestResultByUID, scoreMap, watchedName],
[
currentPrize?.name,
exchangeByUID,
latestResultByUID,
recordHitByUID,
running,
scoreMap,
watchedName,
],
)
const cookieColumns: ColumnsType<CookieEntry> = useMemo(
@@ -1086,7 +1218,7 @@ export default function Dashboard() {
{ title: '积分', width: 90, render: (_, row) => row.new_score || row.score || 0 },
{ title: '状态', dataIndex: 'status', width: 84 },
{ title: '订单号', dataIndex: 'order_id', minWidth: 200, ellipsis: true },
{ title: '时间', dataIndex: 'create_time', width: 130 },
{ title: '时间', width: 160, render: (_, row) => formatUnixTime(row.create_time) },
],
[],
)
@@ -1400,7 +1532,10 @@ export default function Dashboard() {
columns={accountColumns}
dataSource={accounts}
pagination={false}
scroll={{ x: 850, y: 560 }}
rowClassName={(row) =>
exchangeByUID.get(row.uid)?.success ? 'account-row-success' : ''
}
scroll={{ x: 1040, y: 560 }}
/>
</div>
+9
View File
@@ -369,6 +369,15 @@ body {
margin: 0;
}
/* 抢到奖品的账号整行标绿,方便一眼看出结果。 */
.account-panel .ant-table-tbody > tr.account-row-success > td {
background: #f6ffed !important;
}
.account-panel .ant-table-tbody > tr.account-row-success > td:first-child {
box-shadow: inset 3px 0 0 #52c41a;
}
.side-log-panel {
padding: 0 12px 12px;
}
File diff suppressed because it is too large Load Diff