feat(douyu): 精英宝典支持查询开通状态,开通前先校验且不再误判到账
线上两个问题:工作台看不到账号有没有开通精英宝典;点开通时二维码会「闪一下 就变小」——大图弹窗 2~4 秒后自动关闭,只剩列表里 56px 的小二维码。 根因: - getUserInfo 的 manualType/expireTime 才是开通状态的权威口径,但只有兑换 流程用过一次,没有任务类型对外暴露;前端只看积分列,而「开通后积分花完」 和「从未开通」都是 0 分,区分不了(实测 13456 manualType=1/score=0); - 开通前不查状态,且到账判定写成「积分 >= 300」:账上本来就有 680 分的账号 在出码后第一轮轮询就被判成功,前端按 paymentArrived 自动关掉了弹窗。 改动: - 新增任务类型 query_elite_status:查 manualType/manualScore/expireTime 并 落库到 accounts.elite_opened / elite_expire_time(迁移 20261001_0036), 精英工作台加查询按钮与「宝典状态」列(已开通+到期 / 未开通); - create_elite_qr 开通前先查状态:已开通直接判成功、不再生成支付码(方案 A), 既消除二维码闪现,也避免给已开通账号开出真实支付单; - 到账判定改为「积分比开通前增长」,基线用与轮询同源的 credit/balance 取, 取不到基线才退回旧口径; - 支付弹窗不再自动关闭:到账/失败/停止都在弹窗里给结论 + 手动关闭;列表小 二维码在任务进终态后隐藏。 新增 9 条测试覆盖上述行为,回退 runner 时其中 8 条失败。
This commit is contained in:
@@ -340,6 +340,8 @@ export interface DouyuTaskAccountItem {
|
||||
exchange_balance: number | null;
|
||||
bind_status: string;
|
||||
change_role_wait_time: number | null;
|
||||
elite_opened: boolean | null;
|
||||
elite_expire_time: number | null;
|
||||
esports_points: number | null;
|
||||
esports_game_name: string;
|
||||
esports_game_channel: string;
|
||||
|
||||
@@ -43,7 +43,8 @@ import {
|
||||
resultText,
|
||||
taskPayloadText,
|
||||
resultFlag,
|
||||
taskQrUrl,
|
||||
activeTaskQrUrl,
|
||||
eliteStatusText,
|
||||
bindReadyForConfirm,
|
||||
hasBindQrcode,
|
||||
hasPaymentQrcode,
|
||||
@@ -74,6 +75,7 @@ function parseAccountNames(text: string): string[] {
|
||||
export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind }) {
|
||||
const isPeaceHandbook = handbook === 'peace';
|
||||
const isEsportsHandbook = handbook === 'esports';
|
||||
const isEliteHandbook = handbook === 'elite';
|
||||
const { title: handbookTitle, description: handbookDescription, taskTypes: activeTaskTypes, quickActions } = HANDBOOK_META[handbook];
|
||||
const { can } = usePermissions();
|
||||
const { token } = theme.useToken();
|
||||
@@ -665,10 +667,8 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind })
|
||||
if (!payTask) return;
|
||||
const latest = tasks.find((task) => task.id === payTask.id);
|
||||
if (!latest) return;
|
||||
if (paymentArrived(latest)) {
|
||||
setPayTask(null);
|
||||
return;
|
||||
}
|
||||
// 任务进终态后不再自动关闭弹窗:二维码刚弹出来就消失会让人以为"闪一下就没了",
|
||||
// 改成在弹窗里显示结果,由用户手动关闭。
|
||||
if (latest !== payTask) setPayTask(latest);
|
||||
}, [payTask, tasks]);
|
||||
|
||||
@@ -1239,6 +1239,18 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind })
|
||||
width: 80,
|
||||
render: (v) => v ?? <Text type="secondary">-</Text>,
|
||||
},
|
||||
// 精英宝典开通状态:积分区分不了「开通后积分花完」和「从未开通」,单独展示
|
||||
...(isEliteHandbook
|
||||
? [{
|
||||
title: '宝典状态', dataIndex: 'elite_opened', width: 150,
|
||||
render: (_: unknown, record: DouyuTaskAccountItem) => {
|
||||
const text = eliteStatusText(record);
|
||||
if (!text) return <Text type="secondary">未查</Text>;
|
||||
if (record.elite_opened) return <Tag color="green">{text}</Tag>;
|
||||
return <Tag color="default">未开通</Tag>;
|
||||
},
|
||||
}] as NonNullable<TableProps<DouyuTaskAccountItem>['columns']>
|
||||
: []),
|
||||
// 和平小店无鱼翅/限制兑换/换绑时间概念(可无限制换绑),展示扭蛋碎片
|
||||
...(isPeaceHandbook
|
||||
? [{
|
||||
@@ -1348,7 +1360,8 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind })
|
||||
title: '二维码', width: 76, align: 'center',
|
||||
render: (_, record) => {
|
||||
const task = latestTaskByAccount.get(record.id);
|
||||
const url = taskQrUrl(task);
|
||||
// 终态任务(成功/失败/停止)的二维码不再有用,不展示小图
|
||||
const url = activeTaskQrUrl(task);
|
||||
if (!url) return null;
|
||||
const openTask = () => {
|
||||
if (!task) return;
|
||||
@@ -1673,7 +1686,10 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind })
|
||||
<span>开通、充值与送礼</span>
|
||||
</div>
|
||||
<Space direction="vertical" style={{ width: '100%' }} size={6}>
|
||||
{renderActionButton('create_elite_qr', 'primary')}
|
||||
<div style={actionGridStyle}>
|
||||
{renderActionButton('query_elite_status')}
|
||||
{renderActionButton('create_elite_qr', 'primary')}
|
||||
</div>
|
||||
<Tag color={isSupplierGoldRecharge ? 'green' : 'blue'}>{`当前渠道:${goldRechargeChannelLabel}`}</Tag>
|
||||
<Space.Compact style={{ width: '100%' }}>
|
||||
<InputNumber
|
||||
@@ -2348,17 +2364,44 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind })
|
||||
{payAccountName && (
|
||||
<Text type="secondary" style={{ marginBottom: 12, display: 'block' }}>{payAccountName}</Text>
|
||||
)}
|
||||
<Space direction="vertical" align="center" size={12}>
|
||||
{payUrl ? (
|
||||
<div ref={payQrCanvasWrapRef}>
|
||||
<QRCode value={payUrl} size={220} bordered={false} />
|
||||
</div>
|
||||
) : (
|
||||
<Text type="secondary">支付码生成中...</Text>
|
||||
)}
|
||||
<QrActions wrapRef={payQrCanvasWrapRef} url={payUrl} disabled={!payUrl} />
|
||||
<Text>{payTask.message || '请扫码支付'}</Text>
|
||||
</Space>
|
||||
{paymentArrived(payTask) ? (
|
||||
// 已到账/已开通:二维码不再有意义,留在弹窗里给结论 + 手动关闭
|
||||
<Space direction="vertical" align="center" size={12}>
|
||||
<Alert
|
||||
type="success"
|
||||
showIcon
|
||||
message={payTask.task_type === 'create_elite_qr'
|
||||
? '精英宝典已处理完成'
|
||||
: payTask.task_type === 'create_esports_qr'
|
||||
? '电竞手册已处理完成'
|
||||
: '充值已到账'}
|
||||
description={payTask.message || ''}
|
||||
/>
|
||||
<Button type="primary" onClick={() => setPayTask(null)}>关闭</Button>
|
||||
</Space>
|
||||
) : ['failed', 'error', 'stopped'].includes(payTask.status) ? (
|
||||
<Space direction="vertical" align="center" size={12}>
|
||||
<Alert
|
||||
type={payTask.status === 'stopped' ? 'warning' : 'error'}
|
||||
showIcon
|
||||
message={payTask.status === 'stopped' ? '任务已停止' : '支付码任务未完成'}
|
||||
description={payTask.message || ''}
|
||||
/>
|
||||
<Button onClick={() => setPayTask(null)}>关闭</Button>
|
||||
</Space>
|
||||
) : (
|
||||
<Space direction="vertical" align="center" size={12}>
|
||||
{payUrl ? (
|
||||
<div ref={payQrCanvasWrapRef}>
|
||||
<QRCode value={payUrl} size={220} bordered={false} />
|
||||
</div>
|
||||
) : (
|
||||
<Text type="secondary">支付码生成中...</Text>
|
||||
)}
|
||||
<QrActions wrapRef={payQrCanvasWrapRef} url={payUrl} disabled={!payUrl} />
|
||||
<Text>{payTask.message || '请扫码支付'}</Text>
|
||||
</Space>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Modal>
|
||||
|
||||
@@ -30,6 +30,7 @@ export const TASK_STATUS_LABELS: Record<string, string> = {
|
||||
export const ELITE_QUICK_ACTIONS = [
|
||||
{ key: 'get_bind_qr', icon: <QrcodeOutlined /> },
|
||||
{ key: 'confirm_bind', icon: <CheckCircleOutlined /> },
|
||||
{ key: 'query_elite_status', icon: <SearchOutlined /> },
|
||||
{ key: 'create_elite_qr', icon: <CreditCardOutlined /> },
|
||||
{ key: 'query_points', icon: <SearchOutlined /> },
|
||||
{ key: 'query_game_name', icon: <SearchOutlined /> },
|
||||
@@ -73,6 +74,7 @@ export const ELITE_TASK_TYPES = new Set([
|
||||
'get_bind_qr',
|
||||
'confirm_bind',
|
||||
'create_elite_qr',
|
||||
'query_elite_status',
|
||||
'create_gold_qr',
|
||||
'donate_elite_gift',
|
||||
'query_points',
|
||||
@@ -154,6 +156,30 @@ export function taskQrUrl(task: DouyuTaskItem | null | undefined): string {
|
||||
return '';
|
||||
}
|
||||
|
||||
// 任务已进终态后二维码不再有意义(支付已完成/已失败/已停止),列表里不再展示,
|
||||
// 避免大图弹窗关闭后只剩一个小图,看起来像"二维码变小了还能继续付"。
|
||||
const QR_TERMINAL_STATUSES = new Set(['success', 'failed', 'error', 'stopped']);
|
||||
|
||||
export function activeTaskQrUrl(task: DouyuTaskItem | null | undefined): string {
|
||||
if (!task || QR_TERMINAL_STATUSES.has(task.status)) return '';
|
||||
return taskQrUrl(task);
|
||||
}
|
||||
|
||||
// 精英宝典开通状态展示:积分区分不了"开通后积分花完"和"从未开通",
|
||||
// 以后端落库的 manualType(elite_opened)为准。
|
||||
export function eliteStatusText(
|
||||
account: { elite_opened?: boolean | null; elite_expire_time?: number | null },
|
||||
): string {
|
||||
if (account.elite_opened == null) return '';
|
||||
if (!account.elite_opened) return '未开通';
|
||||
const expire = account.elite_expire_time;
|
||||
if (!expire || expire <= 0) return '已开通';
|
||||
const at = new Date(expire * 1000);
|
||||
if (Number.isNaN(at.getTime())) return '已开通';
|
||||
const pad = (n: number) => String(n).padStart(2, '0');
|
||||
return `已开通 ${at.getFullYear()}-${pad(at.getMonth() + 1)}-${pad(at.getDate())} 到期`;
|
||||
}
|
||||
|
||||
export function bindReadyForConfirm(task: DouyuTaskItem | null | undefined): boolean {
|
||||
if (!task) return false;
|
||||
if (task.task_type === 'get_xpd_bind_qr') {
|
||||
|
||||
Reference in New Issue
Block a user