feat(huya): 一键查询查不到角色标记 CK 失效并前端标红

外部导入的 CK 过期时,角色走的 webActUI.checkUserBindGameAccount 整包没有 tRsp 回包(真机日志 newdata keys: ['']),而积分中心通道仍能返回 200,于是页面显示绿色「成功」+「角色信息接口无响应」,看不出 CK 已经挂了。

- 后端 _execute_query_all:bind_status is None 时打 result.ck_expired / ck_expired_reason、account.status=ck_expired,message 追加「疑似 CK 已失效,请重新登录或重新导入 Cookie」;业务码非 200(活动已下线等)不算 CK 失效,避免整批误标红
- 前端 HuyaElitePage:状态列红色「CK 失效」(tooltip 带原因)、最近操作消息红色、游戏名空值红色「角色未查到」;HuyaTasksPage 账号状态映射补 ck_expired
- 测试:角色接口无回包 → ck_expired + account.status;业务码非 200 不误标
This commit is contained in:
yml2213
2026-10-15 20:02:28 +08:00
parent bf1e2e9361
commit 82294aba80
4 changed files with 91 additions and 8 deletions
+13 -3
View File
@@ -101,6 +101,16 @@ function isLoopbackUrl(url: string): boolean {
return /^https?:\/\/(localhost|127\.0\.0\.1|0\.0\.0\.0|\[::1\])(:|\/|$)/i.test(url);
}
/**
* CK 失效:一键查询里角色走活动通道,外部导入的 CK 过期后这个接口直接没有回包,
* 后端会打 `result.ck_expired` 标记并把账号状态置为 `ck_expired`(见
* backend/services/huya_runner_elite.py 模块头)。这种行必须红色提醒重新登录/重新导入,
* 不能因为积分、宝典查到了就显示绿色「成功」。
*/
function ckExpired(task: HuyaTaskItem | undefined, row: HuyaAccountItem): boolean {
return task?.result?.ck_expired === true || row.status === 'ck_expired';
}
function latestTask(tasks: HuyaTaskItem[], accountId: number): HuyaTaskItem | undefined {
return tasks.filter((task) => task.account_id === accountId).sort((a, b) => b.id - a.id)[0];
}
@@ -329,12 +339,12 @@ export default function HuyaElitePage() {
{ title: '#', width: '4%', align: 'center', render: (_value, _row, index) => <Text type="secondary">{index + 1}</Text> },
{ title: '账号', width: '15%', align: 'center', ellipsis: true, render: (_value, row) => <Space direction="vertical" align="center" size={0} style={{ width: '100%' }}><Text strong ellipsis title={row.nickname || row.username || row.uid}>{row.nickname || row.username || row.uid}</Text><Text type="secondary" style={{ fontSize: 12 }} ellipsis title={row.username}>UID {row.uid || '-'}</Text></Space> },
{ title: '手机号', width: '10%', align: 'center', ellipsis: true, render: (_value, row) => row.game_phone || <Text type="secondary">-</Text> },
{ title: '游戏名', width: '13%', align: 'center', ellipsis: true, render: (_value, row) => <Space direction="vertical" align="center" size={0} style={{ width: '100%' }}><Text ellipsis title={row.game_name || row.game_channel}>{row.game_name || <Text type="secondary">未查询</Text>}</Text>{row.game_channel && <Text type="secondary" style={{ fontSize: 12 }} ellipsis>{row.game_channel}</Text>}</Space> },
{ title: '游戏名', width: '13%', align: 'center', ellipsis: true, render: (_value, row) => { const expired = ckExpired(latestTask(tasks, row.id), row); return <Space direction="vertical" align="center" size={0} style={{ width: '100%' }}><Text ellipsis title={row.game_name || row.game_channel}>{row.game_name || <Text type={expired ? 'danger' : 'secondary'}>{expired ? '角色未查到' : '未查询'}</Text>}</Text>{row.game_channel && <Text type="secondary" style={{ fontSize: 12 }} ellipsis>{row.game_channel}</Text>}</Space>; } },
{ title: '精英宝典', width: '13%', align: 'center', ellipsis: true, render: (_value, row) => <Space direction="vertical" align="center" size={0} style={{ width: '100%' }}><Tag color={row.elite_opened === true ? 'success' : row.elite_opened === false ? 'default' : undefined}>{row.elite_opened === true ? '已开通' : row.elite_opened === false ? '未开通' : '未查询'}</Tag>{row.elite_opened === true && row.elite_open_time ? <Text type="secondary" style={{ fontSize: 12 }} ellipsis>{`开通 ${formatUnixSeconds(row.elite_open_time)}`}</Text> : null}{row.elite_expire_time ? <Text type="secondary" style={{ fontSize: 12 }} ellipsis>{`有效期至 ${formatUnixSeconds(row.elite_expire_time)}`}</Text> : null}</Space> },
{ title: '积分', dataIndex: 'points', width: '7%', align: 'center', render: (value: number | null) => value ?? <Text type="secondary">-</Text> },
{ title: '最近操作', width: '25%', align: 'center', ellipsis: true, render: (_value, row) => { const task = latestTask(tasks, row.id); return task ? <Space direction="vertical" align="center" size={1} style={{ width: '100%' }}><Text strong ellipsis>{TASK_LABELS[task.task_type] || task.task_type}</Text><Text type="secondary" ellipsis title={task.message}>{task.message || '-'}</Text><Text type="secondary" style={{ fontSize: 12 }}>{formatTime(task.finished_at || task.created_at)}</Text></Space> : <Text type="secondary">暂无操作</Text>; } },
{ title: '最近操作', width: '25%', align: 'center', ellipsis: true, render: (_value, row) => { const task = latestTask(tasks, row.id); const expired = ckExpired(task, row); return task ? <Space direction="vertical" align="center" size={1} style={{ width: '100%' }}><Text strong ellipsis>{TASK_LABELS[task.task_type] || task.task_type}</Text><Text type={expired ? 'danger' : 'secondary'} ellipsis title={task.message}>{task.message || '-'}</Text><Text type="secondary" style={{ fontSize: 12 }}>{formatTime(task.finished_at || task.created_at)}</Text></Space> : <Text type="secondary">暂无操作</Text>; } },
{ title: '二维码', width: '6%', align: 'center', render: (_value, row) => { const task = latestTask(tasks, row.id); if (!task) return <Text type="secondary">-</Text>; const url = taskQrUrl(task); const image = resultText(task, 'mini_qrcode_image'); if (!url && !image) return <Text type="secondary">-</Text>; const doneLabel = qrDoneLabel(task); if (doneLabel) return <Text type="secondary">{doneLabel}</Text>; return <div onClick={(event) => { event.stopPropagation(); if (!task) return; if (task.task_type === 'create_recharge_order') setZoomPayTask(task); else void openQrTask(task); }} style={{ cursor: 'pointer', display: 'inline-block', lineHeight: 0, background: '#fff', padding: 3, borderRadius: 4 }} title="点击放大扫码">{image ? <img src={`data:image/png;base64,${image}`} alt="二维码" style={{ width: 52, height: 52, objectFit: 'contain' }} /> : <QRCode value={url} size={52} type="svg" errorLevel="L" bordered={false} color="#000" bgColor="#fff" />}</div>; } },
{ title: '状态', width: '7%', align: 'center', render: (_value, row) => { const task = latestTask(tasks, row.id); const status = task?.status || row.status || ''; return <Tag color={TASK_COLORS[status] || 'default'}>{TASK_STATUS_LABELS[status] || status || '待操作'}</Tag>; } },
{ title: '状态', width: '7%', align: 'center', render: (_value, row) => { const task = latestTask(tasks, row.id); if (ckExpired(task, row)) return <Tooltip title={`${resultText(task, 'ck_expired_reason') || '角色信息查不到'} —— 请重新登录或重新导入 Cookie`}><Tag color="error">CK 失效</Tag></Tooltip>; const status = task?.status || row.status || ''; return <Tag color={TASK_COLORS[status] || 'default'}>{TASK_STATUS_LABELS[status] || status || '待操作'}</Tag>; } },
];
const operationBody = (
+2
View File
@@ -94,6 +94,7 @@ const ACCOUNT_STATUS_LABELS: Record<string, string> = {
bind_confirmed: '已绑定',
goods_exchanged: '已兑换',
recharge_order_created: '待支付',
ck_expired: 'CK 失效',
};
const ACCOUNT_STATUS_COLORS: Record<string, string> = {
@@ -112,6 +113,7 @@ const ACCOUNT_STATUS_COLORS: Record<string, string> = {
bind_confirmed: 'success',
goods_exchanged: 'success',
recharge_order_created: 'processing',
ck_expired: 'error',
};
const HUYA_WORKBENCH_ACCOUNT_IDS_KEY = 'huya_task_workbench_account_ids';