修复虎牙宝典开通中状态与支付监听异常
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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>}
|
||||
|
||||
Reference in New Issue
Block a user