From 8ba947e60f24f6344e730b8f66b8c6899508e87a Mon Sep 17 00:00:00 2001 From: yml Date: Fri, 13 Nov 2026 21:21:19 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=BC=98=E5=8C=96=E6=94=AF=E4=BB=98?= =?UTF-8?q?=E7=A0=81=E9=98=9F=E5=88=97=E4=B8=8E=E6=94=AF=E4=BB=98=E8=B6=85?= =?UTF-8?q?=E6=97=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/huya/http_client.py | 2 +- core/huya/wss_client.py | 2 +- core/weibo/client.py | 2 +- web/backend/services/huya_pay_link.py | 4 +- web/backend/services/huya_runner_recharge.py | 6 +- web/frontend/src/api/weibo.ts | 4 +- .../src/components/PaymentQrQueue.tsx | 229 +++++++++ .../src/components/WeiboRechargePanel.tsx | 155 ++++++ web/frontend/src/pages/HuyaElitePage.tsx | 110 ++-- web/frontend/src/pages/WeiboAccountsPage.tsx | 485 +++++------------- web/frontend/src/utils/weiboPayment.ts | 6 + 11 files changed, 592 insertions(+), 413 deletions(-) create mode 100644 web/frontend/src/components/PaymentQrQueue.tsx create mode 100644 web/frontend/src/components/WeiboRechargePanel.tsx create mode 100644 web/frontend/src/utils/weiboPayment.ts diff --git a/core/huya/http_client.py b/core/huya/http_client.py index 86c800b..a5ecce3 100644 --- a/core/huya/http_client.py +++ b/core/huya/http_client.py @@ -1277,7 +1277,7 @@ class HuyaHttpClient: }, ) try: - with self._urlopen(req, timeout=15) as resp: + with self._urlopen(req, timeout=30) as resp: resp_data = resp.read() if resp_data[:2] == b"\x1f\x8b": import gzip diff --git a/core/huya/wss_client.py b/core/huya/wss_client.py index 9963f1f..b77a13b 100644 --- a/core/huya/wss_client.py +++ b/core/huya/wss_client.py @@ -1416,7 +1416,7 @@ class HuyaWssClient: await self.ws.send(msg.encode()) try: - body = await asyncio.wait_for(future, 15.0) + body = await asyncio.wait_for(future, 30.0) except TimeoutError: self._pending.pop(req_id, None) self.logger("[✗] payOrderSubmitV5 超时") diff --git a/core/weibo/client.py b/core/weibo/client.py index 5b3613f..5176ffe 100644 --- a/core/weibo/client.py +++ b/core/weibo/client.py @@ -306,7 +306,7 @@ class WeiboClient: self, cookie: str = "", *, - timeout: float = 20.0, + timeout: float = 30.0, session: requests.Session | None = None, ): self.cookie = normalize_weibo_cookie(cookie) if cookie else "" diff --git a/web/backend/services/huya_pay_link.py b/web/backend/services/huya_pay_link.py index 1908bfc..632713f 100644 --- a/web/backend/services/huya_pay_link.py +++ b/web/backend/services/huya_pay_link.py @@ -99,7 +99,7 @@ def extract_pay_qr_code(html: str) -> str: return code if is_alipay_url(code) else "" -def resolve_pay_targets(pay_url: str, *, timeout: float = 6.0) -> PayTargets: +def resolve_pay_targets(pay_url: str, *, timeout: float = 15.0) -> PayTargets: """跟随支付宝网关的 302,取**收银台页面**和页面里**真正的扫码内容**。 真机实测(2026-10-15):`openapi.alipay.com/gateway.do?...` 会 302 到 @@ -139,6 +139,6 @@ def resolve_pay_targets(pay_url: str, *, timeout: float = 6.0) -> PayTargets: return PayTargets(cashier_url=final_url, qr_code=extract_pay_qr_code(html)) -def resolve_pay_entry_url(pay_url: str, *, timeout: float = 6.0) -> str: +def resolve_pay_entry_url(pay_url: str, *, timeout: float = 15.0) -> str: """只要收银台直达地址(约 93 字符);解析不到返回空串。""" return resolve_pay_targets(pay_url, timeout=timeout).cashier_url diff --git a/web/backend/services/huya_runner_recharge.py b/web/backend/services/huya_runner_recharge.py index b6dc114..18e9d51 100644 --- a/web/backend/services/huya_runner_recharge.py +++ b/web/backend/services/huya_runner_recharge.py @@ -2,6 +2,7 @@ from __future__ import annotations +import os import time from datetime import UTC, datetime from typing import TYPE_CHECKING, Any @@ -18,7 +19,10 @@ HUYA_RECHARGE_ACT_ID = 25135 # shopAppInfo.scene(2026-10-13 积分中心抓包实测 7;订单侧 scene 由 # core.huya.wss_client.ORDER_SCENE 决定,两者不是同一个值) HUYA_RECHARGE_APP_SCENE = SHOP_APP_SCENE -HUYA_PAYMENT_POLL_SECONDS = 180 +HUYA_PAYMENT_POLL_SECONDS = max( + 60, + int(os.getenv("HUYA_PAYMENT_POLL_SECONDS", "600")), +) HUYA_PAYMENT_POLL_INTERVAL = 3 # 内置开通商品候选。**必须同时满足** getGoodsInfoV5=200(能看到)与 # listPayChannelV5=200(能买到)——真机(2026-10-15,hy_300546644 / uid 1199667339402): diff --git a/web/frontend/src/api/weibo.ts b/web/frontend/src/api/weibo.ts index 6242530..3961cb5 100644 --- a/web/frontend/src/api/weibo.ts +++ b/web/frontend/src/api/weibo.ts @@ -72,7 +72,7 @@ export const weiboApi = { api.post( `/weibo/accounts/${accountId}/enable`, undefined, - { timeout: 120000 }, + { timeout: 600000 }, ), refreshAccount: (accountId: number) => api.post( @@ -201,7 +201,7 @@ export const weiboApi = { api.post( `/weibo/accounts/${accountId}/recharge/status`, { pay_id: payId }, - { timeout: 30000 }, + { timeout: 45000 }, ), deleteAccount: (accountId: number) => api.delete(`/weibo/accounts/${accountId}`), diff --git a/web/frontend/src/components/PaymentQrQueue.tsx b/web/frontend/src/components/PaymentQrQueue.tsx new file mode 100644 index 0000000..2969c3c --- /dev/null +++ b/web/frontend/src/components/PaymentQrQueue.tsx @@ -0,0 +1,229 @@ +import { + Alert, + Button, + QRCode, + Space, + Spin, + Tag, + Typography, +} from 'antd'; +import { + CopyOutlined, + ReloadOutlined, + ZoomInOutlined, +} from '@ant-design/icons'; + +const { Text } = Typography; + +export type PaymentQrQueuePhase = 'queued' | 'generating' | 'ready' | 'error'; + +export interface PaymentQrQueueItem { + key: string | number; + title: string; + subtitle?: string; + description?: string; + phase: PaymentQrQueuePhase; + qrValue?: string; + qrImage?: string; + copyValue?: string; + statusLabel?: string; + statusColor?: string; + warning?: string; + error?: string; + refreshing?: boolean; +} + +interface PaymentQrQueueProps { + items: PaymentQrQueueItem[]; + emptyText: string; + queuedText?: string; + onZoom?: (item: PaymentQrQueueItem) => void; + onCopy?: (item: PaymentQrQueueItem) => void; + onRefresh?: (item: PaymentQrQueueItem) => void; +} + +export default function PaymentQrQueue({ + items, + emptyText, + queuedText = '正在排队生成支付码', + onZoom, + onCopy, + onRefresh, +}: PaymentQrQueueProps) { + if (items.length === 0) { + return ( +
+ {emptyText} +
+ ); + } + + const queuedCount = items.filter( + (item) => item.phase === 'queued' || item.phase === 'generating', + ).length; + return ( + + {queuedCount > 0 && ( + + {queuedText}({queuedCount} 个) + + )} +
+ {items.map((item) => { + const hasQr = Boolean(item.qrValue || item.qrImage); + const copyValue = item.copyValue || item.qrValue || ''; + return ( +
+ + {item.title} + + {item.subtitle && ( +
+ + {item.subtitle} + +
+ )} + {item.phase === 'queued' && ( +
+ +
+ 排队中 +
+
+ )} + {item.phase === 'generating' && ( +
+ +
+ 正在创建订单并解析二维码 +
+
+ )} + {item.phase === 'error' && ( + + )} + {item.phase === 'ready' && ( + + {hasQr ? ( +
+ {item.qrImage ? ( + 支付二维码 + ) : ( + + )} +
+ ) : ( + + )} + {item.description && ( + + {item.description} + + )} + {item.statusLabel && ( + + {item.statusLabel} + + )} + {item.warning && ( + + {item.warning} + + )} + {(onZoom || onCopy || onRefresh) && ( + + {onZoom && hasQr && ( + + )} + {onCopy && copyValue && ( + + )} + {onRefresh && ( + + )} + + )} +
+ )} +
+ ); + })} +
+
+ ); +} diff --git a/web/frontend/src/components/WeiboRechargePanel.tsx b/web/frontend/src/components/WeiboRechargePanel.tsx new file mode 100644 index 0000000..f4c7c22 --- /dev/null +++ b/web/frontend/src/components/WeiboRechargePanel.tsx @@ -0,0 +1,155 @@ +import { + Alert, + Button, + InputNumber, + QRCode, + Space, + Tag, + Typography, +} from 'antd'; +import { ReloadOutlined } from '@ant-design/icons'; +import type { + WeiboPaymentStatusResult, + WeiboRechargeOrderResult, +} from '../api/modules'; +import { WEIBO_PAYMENT_STATUS_LABELS } from '../utils/weiboPayment'; + +const { Text } = Typography; + +interface WeiboRechargePanelProps { + enabled: boolean; + amount: number; + onAmountChange: (value: number) => void; + creating: boolean; + onCreate: () => void; + order: WeiboRechargeOrderResult | null; + paymentStatus: WeiboPaymentStatusResult | null; + statusLoading: boolean; + onRefreshStatus: () => void; + onRefreshBalance?: () => void; +} + +export default function WeiboRechargePanel({ + enabled, + amount, + onAmountChange, + creating, + onCreate, + order, + paymentStatus, + statusLoading, + onRefreshStatus, + onRefreshBalance, +}: WeiboRechargePanelProps) { + const paid = paymentStatus?.status === 'PAY_STATUS_SUCCESS'; + const failed = paymentStatus?.status === 'PAY_STATUS_FAILED' + || paymentStatus?.status === 'PAY_STATUS_CLOSED'; + return ( + <> + 助威充值 + + + onAmountChange(Number(value) || 1)} + style={{ width: 110 }} + /> + + + + + {order && ( + + {paid ? ( + + ) : failed ? ( + + ) : ( +
+ +
+ )} + + {order.amount_yuan} 元 · 预计{' '} + {order.single_reward.toLocaleString()} 微豆 + + + 订单号{' '} + + {order.order_id} + + + + + {order.desktop_pay_url && !paid && !failed && ( + + )} + {onRefreshBalance && ( + + )} + + {paymentStatus && ( + + {WEIBO_PAYMENT_STATUS_LABELS[paymentStatus.status] + || paymentStatus.status} + + )} +
+ )} + + ); +} diff --git a/web/frontend/src/pages/HuyaElitePage.tsx b/web/frontend/src/pages/HuyaElitePage.tsx index c948965..028d146 100644 --- a/web/frontend/src/pages/HuyaElitePage.tsx +++ b/web/frontend/src/pages/HuyaElitePage.tsx @@ -7,11 +7,12 @@ import { import type { TableProps } from 'antd'; import { CheckCircleOutlined, ColumnWidthOutlined, CopyOutlined, GiftOutlined, ImportOutlined, QrcodeOutlined, - ReloadOutlined, SearchOutlined, SettingOutlined, ShoppingOutlined, StopOutlined, SwapOutlined, ZoomInOutlined, + ReloadOutlined, SearchOutlined, SettingOutlined, ShoppingOutlined, StopOutlined, SwapOutlined, } from '@ant-design/icons'; import { huyaApi, type HuyaAccountItem, type HuyaConfig, type HuyaGoodsItem, type HuyaRechargeGoodsItem, type HuyaTaskItem } from '../api/modules'; import HuyaConfigFields from '../components/HuyaConfigFields'; import HuyaExternalCkImportModal from '../components/HuyaExternalCkImportModal'; +import PaymentQrQueue, { type PaymentQrQueueItem } from '../components/PaymentQrQueue'; import RealtimeLogPanel from '../components/RealtimeLogPanel'; import { usePermissions } from '../hooks/usePermissions'; import { useWebSocketLogs } from '../hooks/useWebSocketLogs'; @@ -189,6 +190,10 @@ function taskQrUrl(task: HuyaTaskItem | undefined): string { return resultText(task, 'bind_redirect_url') || resultText(task, 'url'); } +function payTaskLabel(task: HuyaTaskItem): string { + return task.account_nickname || task.account_uid || `#${task.account_id}`; +} + export default function HuyaElitePage() { const navigate = useNavigate(); const { can } = usePermissions(); @@ -569,30 +574,65 @@ export default function HuyaElitePage() { } }, [rechargeGoods, selectedRechargeSpu]); const exchangeCount = tasks.filter((task) => task.task_type === 'exchange_goods').length; - // 开通支付码:同一批次里每个账号一个码,未支付的排前面;已支付/已开通的只留一行状态 - const payTasks = useMemo(() => { - const list = tasks.filter((task) => ['create_recharge_order', 'exchange_goods'].includes(task.task_type) - && payBatchIds.includes(task.batch_id) - && Boolean(resultText(task, 'pay_url') || resultText(task, 'mini_qrcode_image'))); - return [...list].sort((a, b) => Number(needsScanPay(b)) - Number(needsScanPay(a)) || a.id - b.id); - }, [tasks, payBatchIds]); - const payPendingTasks = payTasks.filter(needsScanPay); - const payDoneTasks = payTasks.filter((task) => !needsScanPay(task)); + // 支付完成后立即从待扫码队列移除,只保留仍需处理的账号。 + const payAllTasks = useMemo(() => tasks.filter((task) => ['create_recharge_order', 'exchange_goods'].includes(task.task_type) + && payBatchIds.includes(task.batch_id) + && Boolean(resultText(task, 'pay_url') || resultText(task, 'mini_qrcode_image'))), [tasks, payBatchIds]); + const payTasks = useMemo(() => payAllTasks.filter(needsScanPay), [payAllTasks]); + const payDoneCount = payAllTasks.length - payTasks.length; + const payQrTaskIds = useMemo(() => new Set(payAllTasks.map((task) => task.id)), [payAllTasks]); // 多选账号时一次生成多个码:个别账号失败(限购、CK 失效…)要单独点名,否则少一个码看不出原因 const payFailedTasks = useMemo(() => tasks.filter((task) => ['create_recharge_order', 'exchange_goods'].includes(task.task_type) && payBatchIds.includes(task.batch_id) - && ['failed', 'error', 'stopped'].includes(task.status)), [tasks, payBatchIds]); + && !payQrTaskIds.has(task.id) + && ['failed', 'error', 'stopped'].includes(task.status)), [payBatchIds, payQrTaskIds, tasks]); // 还在排队/生成中的码(并发不够时,后面的码要等前面的码付完或超时才轮到) const payQueuedCount = useMemo(() => tasks.filter((task) => ['create_recharge_order', 'exchange_goods'].includes(task.task_type) && payBatchIds.includes(task.batch_id) && ['planned', 'pending', 'running'].includes(task.status) && !resultText(task, 'pay_url')).length, [tasks, payBatchIds]); + const payTaskById = useMemo(() => new Map(payAllTasks.map((task) => [task.id, task])), [payAllTasks]); + const payQueueItems = useMemo(() => { + const pending = payTasks + .map((task) => ({ + key: task.id, + title: payTaskLabel(task), + subtitle: `UID ${task.account_uid || '-'}`, + description: [resultText(task, 'product_name') || '精英宝典', Number(resultText(task, 'count')) > 1 ? `×${resultText(task, 'count')}` : '', resultText(task, 'amount_text')].filter(Boolean).join(' '), + phase: 'ready' as const, + qrValue: payQrValue(task), + qrImage: resultText(task, 'mini_qrcode_image') ? miniQrcodeSrc(resultText(task, 'mini_qrcode_image')) : '', + copyValue: payQrValue(task), + statusLabel: resultText(task, 'payment_status_label') || TASK_STATUS_LABELS[task.status] || task.status, + statusColor: TASK_COLORS[task.status] || 'default', + warning: isLoopbackUrl(payQrValue(task)) ? '二维码是本机地址(localhost),手机无法扫码' : '', + })) + .sort((a, b) => Number(a.key) - Number(b.key)); + const queued = tasks + .filter((task) => ['create_recharge_order', 'exchange_goods'].includes(task.task_type) + && payBatchIds.includes(task.batch_id) + && ['planned', 'pending', 'running'].includes(task.status) + && !resultText(task, 'pay_url')) + .map((task) => ({ + key: `queued-${task.id}`, + title: payTaskLabel(task), + subtitle: `UID ${task.account_uid || '-'}`, + phase: task.status === 'running' ? 'generating' as const : 'queued' as const, + })); + const failed = payFailedTasks.map((task) => ({ + key: `failed-${task.id}`, + title: payTaskLabel(task), + subtitle: `UID ${task.account_uid || '-'}`, + phase: 'error' as const, + error: task.message || '支付码生成失败', + })); + return [...pending, ...queued, ...failed]; + }, [payBatchIds, payFailedTasks, payTasks, tasks]); const openPayTasks = (batchId?: string) => { if (batchId) setPayBatchIds((prev) => (prev.includes(batchId) ? prev : [...prev, batchId])); setPayOpen(true); }; - const payTaskLabel = (task: HuyaTaskItem) => task.account_nickname || task.account_uid || `#${task.account_id}`; const copyPayLink = async (text: string, label: string) => { if (!text) { message.warning('没有可复制的链接'); return; } try { await navigator.clipboard.writeText(text); message.success(`${label}已复制`); } @@ -717,46 +757,22 @@ export default function HuyaElitePage() { setPayOpen(false)} footer={多选账号会同时生成多个码并排展示,各自独立等待支付;支付后自动确认开通状态} > - {payTasks.length === 0 ?
正在为选中的账号生成支付码…
:
- {payQueuedCount > 0 &&
还有 {payQueuedCount} 个账号在排队生成支付码(并发不够时会等已出的码付完/超时,可调大右侧「并发」)
} - {payPendingTasks.length > 0 &&
- {payPendingTasks.map((task) => { - const qrValue = payQrValue(task); - return ( -
-
{resultText(task, 'mini_qrcode_image') ? 支付码 : }
-
{payTaskLabel(task)}
- UID {task.account_uid || '-'} -
{[resultText(task, 'product_name') || '精英宝典', Number(resultText(task, 'count')) > 1 ? `×${resultText(task, 'count')}` : '', resultText(task, 'amount_text')].filter(Boolean).join(' ')}
-
{resultText(task, 'payment_status_label') || TASK_STATUS_LABELS[task.status] || task.status}
- {task.message || '-'} - {isLoopbackUrl(qrValue) &&
二维码是本机地址(localhost),手机扫不开
} -
-
- ); - })} -
} - {payFailedTasks.length > 0 &&
- 生成失败(没有二维码) -
- {payFailedTasks.map((task) => {payTaskLabel(task)} · {task.message || '生成失败'})} -
-
} - {payDoneTasks.length > 0 &&
- 已完成(二维码已收起) -
- {payDoneTasks.map((task) => ( - {payTaskLabel(task)} · {task.result?.elite_opened === true ? '已开通' : task.result?.elite_opening === true ? '开通中' : resultText(task, 'payment_status_label') || '已支付'} - ))} -
-
} -
} + 0 ? '待扫码队列已清空,支付完成的账号已自动移除' : '当前没有待扫码支付码'} + queuedText="还有账号在排队生成支付码" + onZoom={(item) => { + const task = payTaskById.get(Number(item.key)); + if (task) setZoomPayTask(task); + }} + onCopy={(item) => void copyPayLink(item.copyValue || item.qrValue || '', '支付链接')} + />
setZoomPayTask(null)} width={680} centered> {zoomPayTask &&
diff --git a/web/frontend/src/pages/WeiboAccountsPage.tsx b/web/frontend/src/pages/WeiboAccountsPage.tsx index cc80a3b..05cb971 100644 --- a/web/frontend/src/pages/WeiboAccountsPage.tsx +++ b/web/frontend/src/pages/WeiboAccountsPage.tsx @@ -40,7 +40,6 @@ import { SearchOutlined, StopOutlined, SwapOutlined, - ZoomInOutlined, } from '@ant-design/icons'; import { weiboApi, @@ -56,12 +55,15 @@ import { type WeiboRechargeOrderResult, type WeiboWorkbenchBatchImportResult, } from '../api/modules'; +import PaymentQrQueue, { type PaymentQrQueueItem } from '../components/PaymentQrQueue'; +import WeiboRechargePanel from '../components/WeiboRechargePanel'; import { usePermissions } from '../hooks/usePermissions'; import { formatTime } from '../utils/time'; import { getErrorMessage } from '../utils/error'; import { message } from '../utils/antdMessage'; import { copyTextToClipboard } from '../utils/clipboard'; import { runWithConcurrency } from '../utils/concurrency'; +import { WEIBO_PAYMENT_STATUS_LABELS as PAYMENT_STATUS_LABELS } from '../utils/weiboPayment'; const { Text, Title } = Typography; const { TextArea } = Input; @@ -72,13 +74,6 @@ const CK_STATUS: Record = { error: { label: '查询失败', color: 'warning' }, }; -const PAYMENT_STATUS_LABELS: Record = { - PAY_STATUS_PENDING: '等待支付', - PAY_STATUS_SUCCESS: '支付成功', - PAY_STATUS_FAILED: '支付失败', - PAY_STATUS_CLOSED: '订单关闭', -}; - const WORKBENCH_IMPORT_STATUS: Record< WeiboWorkbenchBatchImportResult['items'][number]['status'], { label: string; color: string } @@ -1378,6 +1373,11 @@ export default function WeiboAccountsPage({ paymentStatus: status, statusError: '', }); + if (status.status === 'PAY_STATUS_SUCCESS' || status.status === 'PAY_STATUS_FAILED' || status.status === 'PAY_STATUS_CLOSED') { + setBatchRechargeZoom((current) => ( + current?.account.id === accountId ? null : current + )); + } if (status.status !== 'PAY_STATUS_SUCCESS') { patchBatchRechargeEntry(accountId, { creditPhase: '', @@ -2054,10 +2054,17 @@ export default function WeiboAccountsPage({ () => knownTags.map((tag) => ({ value: tag })), [knownTags], ); - const batchRechargeReadyCount = batchRechargeEntries.filter( + const batchRechargePaidCount = batchRechargeEntries.filter( + (entry) => entry.paymentStatus?.status === 'PAY_STATUS_SUCCESS', + ).length; + // 支付成功后立即从待扫码队列移除;后台仍继续核对待到账。 + const batchRechargeVisibleEntries = batchRechargeEntries.filter( + (entry) => entry.paymentStatus?.status !== 'PAY_STATUS_SUCCESS', + ); + const batchRechargeReadyCount = batchRechargeVisibleEntries.filter( (entry) => entry.phase === 'ready', ).length; - const batchRechargePendingPaymentCount = batchRechargeEntries.filter( + const batchRechargePendingPaymentCount = batchRechargeVisibleEntries.filter( (entry) => entry.phase === 'ready' && (!entry.paymentStatus || @@ -2066,6 +2073,58 @@ export default function WeiboAccountsPage({ const batchRechargeCreditedCount = batchRechargeEntries.filter( (entry) => entry.creditPhase === 'credited', ).length; + const batchRechargeCreditFailures = batchRechargeEntries.filter( + (entry) => + entry.paymentStatus?.status === 'PAY_STATUS_SUCCESS' && + entry.creditPhase === 'error', + ); + const batchRechargeQueueItems = batchRechargeVisibleEntries.map( + (entry): PaymentQrQueueItem => { + const order = entry.order; + const payment = entry.paymentStatus; + const paymentFailed = payment?.status === 'PAY_STATUS_FAILED' + || payment?.status === 'PAY_STATUS_CLOSED'; + if (entry.phase === 'error' || paymentFailed) { + return { + key: entry.account.id, + title: entry.account.nickname || entry.account.uid, + subtitle: `UID ${entry.account.uid}`, + phase: 'error', + error: entry.error + || entry.statusError + || (payment ? PAYMENT_STATUS_LABELS[payment.status] || payment.status : '支付码生成失败'), + }; + } + if (entry.phase === 'queued' || entry.phase === 'creating') { + return { + key: entry.account.id, + title: entry.account.nickname || entry.account.uid, + subtitle: `UID ${entry.account.uid}`, + phase: entry.phase === 'creating' ? 'generating' : 'queued', + }; + } + return { + key: entry.account.id, + title: entry.account.nickname || entry.account.uid, + subtitle: `UID ${entry.account.uid}`, + description: order + ? `${order.amount_yuan} 元 · 预计 ${order.single_reward.toLocaleString()} 微豆` + : '', + phase: 'ready', + qrValue: order?.alipay_qr_code || '', + copyValue: order?.alipay_qr_code || order?.desktop_pay_url || '', + statusLabel: payment + ? PAYMENT_STATUS_LABELS[payment.status] || payment.status + : '等待支付', + statusColor: 'processing', + warning: entry.statusError || '', + refreshing: entry.checkingStatus, + }; + }, + ); + const batchRechargeEntryByAccountId = new Map( + batchRechargeVisibleEntries.map((entry) => [entry.account.id, entry]), + ); const detailPointIncome = detailPointResult?.items.reduce( (total, item) => total + Math.max(0, item.point_num), @@ -2576,71 +2635,17 @@ export default function WeiboAccountsPage({ paddingTop: 12, }} > - 助威充值 - - - - setRechargeAmount(Number(value) || 1) - } - style={{ width: 110 }} - /> - - - - - {rechargeOrder && ( - -
- -
- - {rechargeOrder.amount_yuan} 元 · 预计{' '} - {rechargeOrder.single_reward.toLocaleString()} 微豆 - - - {paymentStatus && ( - - {PAYMENT_STATUS_LABELS[paymentStatus.status] || - paymentStatus.status} - - )} -
- )} + void createRecharge()} + order={rechargeOrder} + paymentStatus={paymentStatus} + statusLoading={paymentStatusLoading} + onRefreshStatus={() => void refreshPaymentStatus()} + />
)} @@ -2845,181 +2850,43 @@ export default function WeiboAccountsPage({ } - 已支付{' '} - { - batchRechargeEntries.filter( - (entry) => - entry.paymentStatus?.status === 'PAY_STATUS_SUCCESS', - ).length - } + 已支付 {batchRechargePaidCount} -
0 + ? '待扫码队列已清空,支付成功的账号已自动移除' + : '当前没有待扫码支付码' + } + queuedText="正在排队生成微博支付码" + onZoom={(item) => { + const entry = batchRechargeEntryByAccountId.get(Number(item.key)); + if (entry) setBatchRechargeZoom(entry); }} - > - {batchRechargeEntries.map((entry) => { - const order = entry.order; - const payment = entry.paymentStatus; - const paymentColor = - payment?.status === 'PAY_STATUS_SUCCESS' - ? 'success' - : payment?.status === 'PAY_STATUS_FAILED' || - payment?.status === 'PAY_STATUS_CLOSED' - ? 'error' - : 'processing'; - return ( -
- - {entry.account.nickname || entry.account.uid} - -
- - UID {entry.account.uid} - -
- {entry.phase === 'queued' && ( -
- -
- 排队中 -
-
- )} - {entry.phase === 'creating' && ( -
- -
- 正在创建订单并解析二维码 -
-
- )} - {entry.phase === 'error' && ( - - )} - {entry.phase === 'ready' && order && ( - - {order.alipay_qr_code ? ( -
- -
- ) : ( - - )} - - {order.amount_yuan} 元 · 预计{' '} - {order.single_reward.toLocaleString()} 微豆 - - - {payment - ? PAYMENT_STATUS_LABELS[payment.status] || payment.status - : '待检查支付状态'} - - {payment?.status === 'PAY_STATUS_SUCCESS' && ( - - {entry.creditPhase === 'credited' - ? `已到账 +${entry.creditPoints ?? 0}` - : entry.creditPhase === 'pending' - ? '支付成功,微豆暂未查到' - : entry.creditPhase === 'error' - ? '到账查询失败' - : '正在核对到账'} - - )} - {entry.creditError && ( - - {entry.creditError} - - )} - {entry.statusError && ( - - {entry.statusError} - - )} - - {order.alipay_qr_code && ( - - )} - - - -
- )} -
- ); - })} -
+ onCopy={(item) => { + const entry = batchRechargeEntryByAccountId.get(Number(item.key)); + if (entry) void copyBatchRechargeLink(entry); + }} + onRefresh={(item) => { + const entry = batchRechargeEntryByAccountId.get(Number(item.key)); + if (entry) void checkBatchRechargeEntry(entry); + }} + /> + {batchRechargeCreditFailures.length > 0 && ( + + `${entry.account.nickname || entry.account.uid}:${entry.creditError || '未查询到到账记录'}`, + ) + .join(';')} + /> + )}
@@ -3916,116 +3783,18 @@ export default function WeiboAccountsPage({ {!canRecharge ? ( ) : ( - - - setRechargeAmount(Number(value) || 1)} - style={{ width: 110 }} - /> - - - - - )} - {rechargeOrder && ( - -
- -
- - - {rechargeOrder.order_id} - - ), - }, - ]} - /> - - - {rechargeOrder.desktop_pay_url && ( - - )} - - - {paymentStatus && ( - - )} - -
+ void createRecharge()} + order={rechargeOrder} + paymentStatus={paymentStatus} + statusLoading={paymentStatusLoading} + onRefreshStatus={() => void refreshPaymentStatus()} + onRefreshBalance={() => void refreshAfterRecharge()} + /> )} ), diff --git a/web/frontend/src/utils/weiboPayment.ts b/web/frontend/src/utils/weiboPayment.ts new file mode 100644 index 0000000..40e01fc --- /dev/null +++ b/web/frontend/src/utils/weiboPayment.ts @@ -0,0 +1,6 @@ +export const WEIBO_PAYMENT_STATUS_LABELS: Record = { + PAY_STATUS_PENDING: '等待支付', + PAY_STATUS_SUCCESS: '支付成功', + PAY_STATUS_FAILED: '支付失败', + PAY_STATUS_CLOSED: '订单关闭', +};