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:
@@ -514,9 +514,63 @@ def test_query_all_fails_when_points_center_unreachable(monkeypatch):
|
||||
None, None, account, {"uid": UID, "cookie": COOKIE}, {"bind_act_id": "9271"}
|
||||
)
|
||||
|
||||
status, message, _result = marks[-1]
|
||||
status, message, result = marks[-1]
|
||||
assert status == "failed"
|
||||
assert "积分中心接口无响应" in message
|
||||
# 两条通道都没回包:CK 失效标记跟着失败结果一起落库(前端整行标红)
|
||||
assert result["ck_expired"] is True
|
||||
assert account.status == "ck_expired"
|
||||
|
||||
|
||||
def test_query_all_flags_ck_expired_when_role_lookup_has_no_response(monkeypatch):
|
||||
"""角色接口整包没有回包 = 这个 CK 已经不认了(真机:外部导入的过期 CK 就这表现)。
|
||||
|
||||
真机日志(logs/app-2026-10-15.log,hy_297838517)里
|
||||
``webActUI.checkUserBindGameAccount`` 的响应只有 ``newdata keys: ['']``(没有 tRsp),
|
||||
``_resolve_bind_status`` 因此返回 None;积分中心走另一条通道却还能返回 200,
|
||||
所以不能凭「积分查到了」就当成功,必须打 ck_expired 让页面标红。
|
||||
"""
|
||||
shop_client = _StubClient(redeem=_redeem(REDEEM_NOT_OPENED_HEX))
|
||||
runner, marks, _logs = _runner(monkeypatch, shop_client, activity_client=object())
|
||||
monkeypatch.setattr(
|
||||
runner, "_resolve_bind_status", lambda **kwargs: (None, {}), raising=False
|
||||
)
|
||||
account = HuyaAccount(uid=str(UID), cookie=COOKIE)
|
||||
|
||||
runner._execute_query_all(
|
||||
None, None, account, {"uid": UID, "cookie": COOKIE}, {"bind_act_id": "9271"}
|
||||
)
|
||||
|
||||
status, message, result = marks[-1]
|
||||
# 宝典/积分查到了,任务本身不算失败(结果照样展示),但 CK 失效标记必须带上
|
||||
assert status == "success"
|
||||
assert result["ck_expired"] is True
|
||||
assert result["ck_expired_reason"] == "角色信息接口无响应"
|
||||
assert account.status == "ck_expired"
|
||||
assert "角色信息接口无响应" in message
|
||||
assert "疑似 CK 已失效" in message
|
||||
|
||||
|
||||
def test_query_all_keeps_business_error_out_of_ck_expired(monkeypatch):
|
||||
"""接口有回包、只是业务码非 200(例如活动已下线)不算 CK 失效,避免整批误标红。"""
|
||||
shop_client = _StubClient(redeem=_redeem(REDEEM_NOT_OPENED_HEX))
|
||||
runner, marks, _logs = _runner(monkeypatch, shop_client, activity_client=object())
|
||||
monkeypatch.setattr(
|
||||
runner,
|
||||
"_resolve_bind_status",
|
||||
lambda **kwargs: (_FakeBindStatus(status=601, msg="活动已下线"), {}),
|
||||
raising=False,
|
||||
)
|
||||
account = HuyaAccount(uid=str(UID), cookie=COOKIE)
|
||||
|
||||
runner._execute_query_all(
|
||||
None, None, account, {"uid": UID, "cookie": COOKIE}, {"bind_act_id": "9271"}
|
||||
)
|
||||
|
||||
_status, message, result = marks[-1]
|
||||
assert result.get("ck_expired") is None
|
||||
assert account.status == "all_queried"
|
||||
assert "活动已下线" in message
|
||||
|
||||
|
||||
def test_shop_client_falls_back_to_http_when_wss_init_fails(monkeypatch):
|
||||
|
||||
@@ -10,6 +10,13 @@
|
||||
* 开通时间 —— ``revenueWebUI.queryUserOrderList`` 里已支付的宝典订单,取最近一次支付时间。
|
||||
* 有效期 —— 商城订单(orderDetailV5)与积分中心都**不返回**到期时间,所以留空,
|
||||
界面上「有就显示」。
|
||||
* CK 失效 —— 「一键查询」里角色走活动通道(``webActUI.checkUserBindGameAccount``),
|
||||
真机(2026-10-15,外部导入的过期 CK)表现为**整个请求没有 tRsp 回包**
|
||||
(日志里 ``响应 newdata keys: ['']``),于是 ``_resolve_bind_status`` 返回 None。
|
||||
这种情况按「CK 已失效」处理:任务 result 打 ``ck_expired`` 标记、账号状态置
|
||||
``ck_expired``,前端把该行标红提示重新登录/重新导入 Cookie。
|
||||
积分中心走另一条通道(baseinfo 里的 cookie 校验更松),过期 CK 仍可能返回 200,
|
||||
所以不能拿积分能不能查来判 CK 有效性。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -335,6 +342,9 @@ class EliteMixin:
|
||||
self.payload.get("bind_act_id") or config_info.get("bind_act_id")
|
||||
)
|
||||
role_name = ""
|
||||
# 角色查不到 = 这个 CK 已经不认了(模块头里有真机依据),整行要标红提醒
|
||||
ck_expired = False
|
||||
ck_expired_reason = ""
|
||||
if not b_act_id_int:
|
||||
failures.append("未配置绑定 bActId")
|
||||
else:
|
||||
@@ -346,7 +356,9 @@ class EliteMixin:
|
||||
b_act_id_int=b_act_id_int,
|
||||
)
|
||||
if bind_status is None:
|
||||
failures.append("角色信息接口无响应")
|
||||
ck_expired = True
|
||||
ck_expired_reason = "角色信息接口无响应"
|
||||
failures.append(ck_expired_reason)
|
||||
elif bind_status.status != 200:
|
||||
failures.append(
|
||||
bind_status.msg or f"角色信息查询失败: {bind_status.status}"
|
||||
@@ -372,9 +384,14 @@ class EliteMixin:
|
||||
message_parts.append("未绑定角色")
|
||||
|
||||
account.points = points if points is not None else account.points
|
||||
account.status = "all_queried"
|
||||
# CK 失效时账号状态单独标出来(前端「状态」列据此显示红色「CK 失效」)
|
||||
account.status = "ck_expired" if ck_expired else "all_queried"
|
||||
account.updated_at = datetime.now(UTC)
|
||||
result["failures"] = failures
|
||||
if ck_expired:
|
||||
result["ck_expired"] = True
|
||||
result["ck_expired_reason"] = ck_expired_reason
|
||||
ck_hint = ";疑似 CK 已失效,请重新登录或重新导入 Cookie" if ck_expired else ""
|
||||
|
||||
fetched_anything = points is not None or role_name or opened is not None
|
||||
if not fetched_anything:
|
||||
@@ -382,7 +399,7 @@ class EliteMixin:
|
||||
worker_db,
|
||||
task,
|
||||
"failed",
|
||||
";".join(failures) or "一键查询全部失败",
|
||||
(";".join(failures) or "一键查询全部失败") + ck_hint,
|
||||
result,
|
||||
)
|
||||
return
|
||||
@@ -390,4 +407,4 @@ class EliteMixin:
|
||||
message = "|".join(message_parts) or "一键查询完成"
|
||||
if failures:
|
||||
message += f";部分失败: {';'.join(failures)}"
|
||||
self._mark_task(worker_db, task, "success", message, result)
|
||||
self._mark_task(worker_db, task, "success", message + ck_hint, result)
|
||||
|
||||
@@ -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 = (
|
||||
|
||||
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user