修复虎牙宝典开通中状态与支付监听异常

This commit is contained in:
yml2213
2026-11-11 12:51:06 +08:00
parent b18e46d72d
commit 26809dd116
6 changed files with 282 additions and 19 deletions
+151 -8
View File
@@ -39,6 +39,12 @@ ELITE_HANDBOOK_SPU_IDS = frozenset(
ELITE_HANDBOOK_NAME_KEYWORD = "精英宝典"
ELITE_ORDER_PAGE_SIZE = 20
ELITE_ORDER_MAX_PAGES = 3
# 已支付但 redeem 还没翻成已开通时,在这个窗口内视为「开通中」而不是「未开通」。
ELITE_OPENING_WINDOW_MS = 24 * 60 * 60 * 1000
# 官方订单状态:10=待支付、1000=尾款待支付;90=已取消。
ELITE_PENDING_ORDER_STATUSES = frozenset({10, 1000})
ELITE_CANCELLED_ORDER_STATUSES = frozenset({90, 1001})
ELITE_PAID_ORDER_STATUSES = frozenset({20, 30, 40, 50, 55, 60, 70, 80})
# 赛季(福利季)节点的毫秒时间戳下限:2001-09 之后的值才认
ELITE_SEASON_MS_FLOOR = 1_000_000_000_000
@@ -126,6 +132,79 @@ class EliteMixin:
offset += len(orders)
return latest or None
def _latest_elite_order(self, client: Any, uid: int, cookie: str) -> dict | None:
"""最近一张宝典订单(不要求已支付),用于识别开通中/待支付/已取消。"""
latest: dict | None = None
latest_key = -1
offset = 0
for _page in range(ELITE_ORDER_MAX_PAGES):
resp = client.query_user_order_list(
uid=uid,
guid="",
cookie=cookie,
offset=offset,
page_size=ELITE_ORDER_PAGE_SIZE,
order_type=1,
status=0,
)
if resp is None:
break
orders = list(getattr(resp, "orders", None) or [])
for order in orders:
item = order.to_dict() if hasattr(order, "to_dict") else dict(order)
if not self._is_elite_order(item):
continue
key = int(item.get("create_time") or 0)
if key >= latest_key:
latest = item
latest_key = key
total = self._to_int(getattr(resp, "totalCount", 0))
if len(orders) < ELITE_ORDER_PAGE_SIZE or offset + len(orders) >= total:
break
offset += len(orders)
return latest
@staticmethod
def _elite_order_state(
order: dict | None, *, opened: bool | None, now_ms: int | None = None
) -> dict[str, Any]:
"""把「redeem 开通位 + 最近订单」解释成可展示状态。
订单状态 10 是待支付,不代表开通中;只有已支付/有 pay_time 且权益尚未到账,
才归为「开通中」。超出交付窗口后仍没开通,不再继续误导成开通中。
"""
if opened is True:
return {"status": "opened", "label": "已开通"}
if not order:
return {"status": "unopened", "label": "未开通"}
status = int(order.get("order_status") or 0)
pay_time = int(order.get("pay_time") or 0)
current_ms = int(now_ms if now_ms is not None else time.time() * 1000)
paid = pay_time > 0 or status in ELITE_PAID_ORDER_STATUSES
observed_ms = pay_time or int(order.get("create_time") or 0)
if paid and (
observed_ms <= 0 or current_ms - observed_ms <= ELITE_OPENING_WINDOW_MS
):
return {
"status": "opening",
"label": "开通中",
"order": order,
}
if status in ELITE_PENDING_ORDER_STATUSES:
return {
"status": "pending_payment",
"label": "待支付",
"order": order,
}
if status in ELITE_CANCELLED_ORDER_STATUSES:
return {
"status": "cancelled",
"label": "订单已取消",
"order": order,
}
return {"status": "unopened", "label": "未开通", "order": order}
@classmethod
def _walk_season_nodes(cls, node: Any, found: list[dict]) -> None:
"""递归找「赛季」节点:同时带 tag10/tag11 毫秒起止时间的结构。"""
@@ -210,9 +289,38 @@ class EliteMixin:
if redeem is not None and getattr(redeem, "code", 0) == 200:
state["redeem"] = redeem.to_dict()
state["redeem_points"] = self._to_int(redeem.value("tag2"))
latest_order = self._latest_elite_order(client, uid, cookie)
elite_state = self._elite_order_state(
latest_order,
opened=opened,
now_ms=pay_time_ms,
)
if latest_order:
state["elite_order"] = latest_order
def mark_opening() -> None:
state.update(
{
"elite_opened": None,
"elite_opening": True,
"elite_status": "opening",
"elite_status_label": "开通中",
}
)
if pay_time_ms:
state["elite_order_paid_time"] = int(pay_time_ms) // 1000
if opened is None:
if pay_time_ms or elite_state["status"] == "opening":
mark_opening()
return state
if not opened:
opening = bool(pay_time_ms) or elite_state["status"] == "opening"
if opening:
mark_opening()
return state
open_time: int | None = pay_time_ms or None
if opened and not open_time:
open_time = self._resolve_elite_open_time(client, uid, cookie)
@@ -220,10 +328,17 @@ class EliteMixin:
expire_time = self._to_int(season["end_ms"]) if season else None
self._apply_elite_state(account, opened, open_time, expire_time)
state["elite_opened"] = opened
state["elite_open_time"] = (open_time // 1000) if open_time else None
state["elite_open_time_text"] = (
self._format_local_time(open_time // 1000) if open_time else ""
state.update(
{
"elite_opened": opened,
"elite_opening": False,
"elite_status": elite_state["status"],
"elite_status_label": elite_state["label"],
"elite_open_time": (open_time // 1000) if open_time else None,
"elite_open_time_text": (
self._format_local_time(open_time // 1000) if open_time else ""
),
}
)
if season:
state["elite_season"] = season
@@ -379,17 +494,32 @@ class EliteMixin:
)
message_parts.append(f"积分 {points}")
latest_order = (
self._latest_elite_order(shop_client, uid, cookie)
if opened is not None
else None
)
elite_state = self._elite_order_state(latest_order, opened=opened)
open_time: int | None = None
if opened:
# 已开通才去翻订单:既省一次请求,也避免给未开通账号显示旧订单时间
open_time = self._resolve_elite_open_time(shop_client, uid, cookie)
season = (
self._resolve_elite_season(shop_client, uid, cookie) if opened else None
)
expire_time = self._to_int(season["end_ms"]) if season else None
if opened is not None:
self._apply_elite_state(account, opened, open_time, expire_time)
result["elite_opened"] = opened
opening = elite_state["status"] == "opening"
if not opening:
self._apply_elite_state(account, opened, open_time, expire_time)
result["elite_opened"] = None if opening else opened
result["elite_opening"] = opening
result["elite_status"] = elite_state["status"]
result["elite_status_label"] = elite_state["label"]
if latest_order:
result["elite_order"] = latest_order
paid_at = int(latest_order.get("pay_time") or 0)
if paid_at:
result["elite_order_paid_time"] = paid_at // 1000
result["elite_open_time"] = (open_time // 1000) if open_time else None
result["elite_open_time_text"] = (
self._format_local_time(open_time // 1000) if open_time else ""
@@ -408,8 +538,21 @@ class EliteMixin:
opened_text += (
f",有效期至 {self._format_local_time(expire_time // 1000)}"
)
elif opening:
order_id = (latest_order or {}).get("order_id") or ""
opened_text = "开通中宝典"
if order_id:
opened_text += f"(订单 #{order_id},等待权益到账)"
else:
opened_text = "未开通宝典"
order_id = (latest_order or {}).get("order_id") or ""
if elite_state["status"] == "pending_payment":
opened_text = (
f"宝典订单 #{order_id} 待支付" if order_id else "宝典待支付"
)
elif elite_state["status"] == "cancelled":
opened_text = "未开通宝典(最近订单已取消)"
else:
opened_text = "未开通宝典"
message_parts.append(opened_text)
account.points = points if points is not None else account.points
+8 -3
View File
@@ -115,7 +115,11 @@ function miniQrcodeSrc(image: string): string {
/** 开通支付码是否还需要扫码:已支付或已确认开通就不用再展示了 */
function needsScanPay(task: HuyaTaskItem): boolean {
const result = task.result || {};
return !(result.payment_paid === true || result.elite_opened === true);
return !(
result.payment_paid === true
|| result.elite_opened === true
|| result.elite_opening === true
);
}
/**
@@ -128,6 +132,7 @@ function needsScanPay(task: HuyaTaskItem): boolean {
function qrDoneLabel(task: HuyaTaskItem): string | null {
if (task.task_type !== 'create_recharge_order') return null;
if (needsScanPay(task)) return null;
if (task.result?.elite_opening === true) return '开通中';
return task.result?.elite_opened === true ? '已开通' : '已支付';
}
@@ -538,7 +543,7 @@ export default function HuyaElitePage() {
{ 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) => { 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: '精英宝典', width: '13%', align: 'center', ellipsis: true, render: (_value, row) => { const task = latestTask(tasks, row.id); const opening = task?.result?.elite_opening === true; return <Space direction="vertical" align="center" size={0} style={{ width: '100%' }}><Tag color={opening ? 'processing' : row.elite_opened === true ? 'success' : row.elite_opened === false ? 'default' : undefined}>{opening ? '开通中' : 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); 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 (task.task_type === 'get_bind_qr' && !image) { const hasMini = task.result?.has_mini_qrcode === true; if (!hasMini) return <Text type="secondary">-</Text>; return <div onClick={(event) => { event.stopPropagation(); void openQrTask(task); }} style={{ cursor: 'pointer', display: 'inline-block', lineHeight: 0, background: '#fff', padding: 6, borderRadius: 4 }} title="点击查看小程序码"><QrcodeOutlined style={{ fontSize: 26, color: '#000' }} /></div>; } 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={miniQrcodeSrc(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>; } },
@@ -612,7 +617,7 @@ export default function HuyaElitePage() {
<Text type="secondary" style={{ fontSize: 12 }}>已完成(二维码已收起)</Text>
<div style={{ marginTop: 6, display: 'flex', flexWrap: 'wrap', gap: 6 }}>
{payDoneTasks.map((task) => (
<Tag key={task.id} color={task.result?.elite_opened === true ? 'success' : 'blue'}>{payTaskLabel(task)} · {task.result?.elite_opened === true ? '已开通' : resultText(task, 'payment_status_label') || '已支付'}</Tag>
<Tag key={task.id} color={task.result?.elite_opened === true ? 'success' : task.result?.elite_opening === true ? 'processing' : 'blue'}>{payTaskLabel(task)} · {task.result?.elite_opened === true ? '已开通' : task.result?.elite_opening === true ? '开通中' : resultText(task, 'payment_status_label') || '已支付'}</Tag>
))}
</div>
</div>}