feat: 优化支付码队列与支付超时
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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 超时")
|
||||
|
||||
@@ -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 ""
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -72,7 +72,7 @@ export const weiboApi = {
|
||||
api.post<WeiboAccountItem, WeiboAccountItem>(
|
||||
`/weibo/accounts/${accountId}/enable`,
|
||||
undefined,
|
||||
{ timeout: 120000 },
|
||||
{ timeout: 600000 },
|
||||
),
|
||||
refreshAccount: (accountId: number) =>
|
||||
api.post<WeiboAccountItem, WeiboAccountItem>(
|
||||
@@ -201,7 +201,7 @@ export const weiboApi = {
|
||||
api.post<WeiboPaymentStatusResult, WeiboPaymentStatusResult>(
|
||||
`/weibo/accounts/${accountId}/recharge/status`,
|
||||
{ pay_id: payId },
|
||||
{ timeout: 30000 },
|
||||
{ timeout: 45000 },
|
||||
),
|
||||
deleteAccount: (accountId: number) =>
|
||||
api.delete<MessageResponse, MessageResponse>(`/weibo/accounts/${accountId}`),
|
||||
|
||||
@@ -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 (
|
||||
<div style={{ padding: 28, textAlign: 'center' }}>
|
||||
<Text type="secondary">{emptyText}</Text>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const queuedCount = items.filter(
|
||||
(item) => item.phase === 'queued' || item.phase === 'generating',
|
||||
).length;
|
||||
return (
|
||||
<Space orientation="vertical" size={10} style={{ width: '100%' }}>
|
||||
{queuedCount > 0 && (
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
||||
{queuedText}({queuedCount} 个)
|
||||
</Text>
|
||||
)}
|
||||
<div
|
||||
style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(auto-fill, minmax(230px, 1fr))',
|
||||
gap: 12,
|
||||
maxHeight: '64vh',
|
||||
overflowY: 'auto',
|
||||
paddingRight: 4,
|
||||
}}
|
||||
>
|
||||
{items.map((item) => {
|
||||
const hasQr = Boolean(item.qrValue || item.qrImage);
|
||||
const copyValue = item.copyValue || item.qrValue || '';
|
||||
return (
|
||||
<div
|
||||
key={item.key}
|
||||
style={{
|
||||
border: '1px solid rgba(128, 128, 128, .24)',
|
||||
borderRadius: 8,
|
||||
padding: 12,
|
||||
textAlign: 'center',
|
||||
background: 'rgba(255, 255, 255, .015)',
|
||||
}}
|
||||
>
|
||||
<Text strong ellipsis title={item.title}>
|
||||
{item.title}
|
||||
</Text>
|
||||
{item.subtitle && (
|
||||
<div>
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
||||
{item.subtitle}
|
||||
</Text>
|
||||
</div>
|
||||
)}
|
||||
{item.phase === 'queued' && (
|
||||
<div style={{ padding: '42px 0' }}>
|
||||
<Spin size="small" />
|
||||
<div>
|
||||
<Text type="secondary">排队中</Text>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{item.phase === 'generating' && (
|
||||
<div style={{ padding: '42px 0' }}>
|
||||
<Spin />
|
||||
<div style={{ marginTop: 8 }}>
|
||||
<Text type="secondary">正在创建订单并解析二维码</Text>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{item.phase === 'error' && (
|
||||
<Alert
|
||||
type="error"
|
||||
showIcon
|
||||
style={{ marginTop: 10, textAlign: 'left' }}
|
||||
title="支付码不可用"
|
||||
description={item.error || '未知原因'}
|
||||
/>
|
||||
)}
|
||||
{item.phase === 'ready' && (
|
||||
<Space
|
||||
orientation="vertical"
|
||||
size={7}
|
||||
style={{ width: '100%', marginTop: 8 }}
|
||||
>
|
||||
{hasQr ? (
|
||||
<div
|
||||
style={{
|
||||
display: 'inline-block',
|
||||
background: '#fff',
|
||||
padding: 6,
|
||||
lineHeight: 0,
|
||||
borderRadius: 6,
|
||||
}}
|
||||
>
|
||||
{item.qrImage ? (
|
||||
<img
|
||||
src={item.qrImage}
|
||||
alt="支付二维码"
|
||||
style={{
|
||||
width: 160,
|
||||
height: 160,
|
||||
objectFit: 'contain',
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<QRCode
|
||||
value={item.qrValue || ''}
|
||||
size={160}
|
||||
bordered={false}
|
||||
color="#000"
|
||||
bgColor="#fff"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<Alert
|
||||
type="warning"
|
||||
showIcon
|
||||
title="订单已创建但未解析到二维码"
|
||||
description="可复制支付链接后手动打开"
|
||||
/>
|
||||
)}
|
||||
{item.description && (
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
||||
{item.description}
|
||||
</Text>
|
||||
)}
|
||||
{item.statusLabel && (
|
||||
<Tag color={item.statusColor || 'processing'}>
|
||||
{item.statusLabel}
|
||||
</Tag>
|
||||
)}
|
||||
{item.warning && (
|
||||
<Text type="warning" style={{ fontSize: 12 }}>
|
||||
{item.warning}
|
||||
</Text>
|
||||
)}
|
||||
{(onZoom || onCopy || onRefresh) && (
|
||||
<Space wrap style={{ justifyContent: 'center' }}>
|
||||
{onZoom && hasQr && (
|
||||
<Button
|
||||
size="small"
|
||||
icon={<ZoomInOutlined />}
|
||||
onClick={() => onZoom(item)}
|
||||
>
|
||||
放大扫码
|
||||
</Button>
|
||||
)}
|
||||
{onCopy && copyValue && (
|
||||
<Button
|
||||
size="small"
|
||||
icon={<CopyOutlined />}
|
||||
onClick={() => onCopy(item)}
|
||||
>
|
||||
复制链接
|
||||
</Button>
|
||||
)}
|
||||
{onRefresh && (
|
||||
<Button
|
||||
size="small"
|
||||
icon={<ReloadOutlined />}
|
||||
loading={item.refreshing}
|
||||
onClick={() => onRefresh(item)}
|
||||
>
|
||||
刷新状态
|
||||
</Button>
|
||||
)}
|
||||
</Space>
|
||||
)}
|
||||
</Space>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</Space>
|
||||
);
|
||||
}
|
||||
@@ -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 (
|
||||
<>
|
||||
<Text strong>助威充值</Text>
|
||||
<Space wrap style={{ marginTop: 8 }}>
|
||||
<Space.Compact>
|
||||
<InputNumber
|
||||
min={1}
|
||||
max={1000}
|
||||
precision={0}
|
||||
value={amount}
|
||||
onChange={(value) => onAmountChange(Number(value) || 1)}
|
||||
style={{ width: 110 }}
|
||||
/>
|
||||
<Button disabled>元</Button>
|
||||
</Space.Compact>
|
||||
<Button
|
||||
type="primary"
|
||||
loading={creating}
|
||||
disabled={!enabled}
|
||||
onClick={onCreate}
|
||||
>
|
||||
创建订单
|
||||
</Button>
|
||||
</Space>
|
||||
{order && (
|
||||
<Space
|
||||
orientation="vertical"
|
||||
size={8}
|
||||
style={{ width: '100%', marginTop: 10 }}
|
||||
>
|
||||
{paid ? (
|
||||
<Alert
|
||||
type="success"
|
||||
showIcon
|
||||
title="支付成功,二维码已自动收起"
|
||||
description="可继续刷新账号资料核对微豆到账。"
|
||||
/>
|
||||
) : failed ? (
|
||||
<Alert
|
||||
type="error"
|
||||
showIcon
|
||||
title={WEIBO_PAYMENT_STATUS_LABELS[paymentStatus?.status || ''] || '支付失败'}
|
||||
description="该二维码已不可继续使用。"
|
||||
/>
|
||||
) : (
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<QRCode
|
||||
value={order.alipay_qr_code}
|
||||
size={180}
|
||||
bordered={false}
|
||||
color="#000"
|
||||
bgColor="#fff"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
||||
{order.amount_yuan} 元 · 预计{' '}
|
||||
{order.single_reward.toLocaleString()} 微豆
|
||||
</Text>
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
||||
订单号{' '}
|
||||
<Text copyable style={{ fontSize: 12 }}>
|
||||
{order.order_id}
|
||||
</Text>
|
||||
</Text>
|
||||
<Space wrap>
|
||||
<Button
|
||||
size="small"
|
||||
icon={<ReloadOutlined />}
|
||||
loading={statusLoading}
|
||||
onClick={onRefreshStatus}
|
||||
>
|
||||
刷新支付状态
|
||||
</Button>
|
||||
{order.desktop_pay_url && !paid && !failed && (
|
||||
<Button
|
||||
size="small"
|
||||
href={order.desktop_pay_url}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
电脑端支付
|
||||
</Button>
|
||||
)}
|
||||
{onRefreshBalance && (
|
||||
<Button size="small" onClick={onRefreshBalance}>
|
||||
刷新到账
|
||||
</Button>
|
||||
)}
|
||||
</Space>
|
||||
{paymentStatus && (
|
||||
<Tag
|
||||
color={
|
||||
paid
|
||||
? 'success'
|
||||
: failed
|
||||
? 'error'
|
||||
: 'processing'
|
||||
}
|
||||
>
|
||||
{WEIBO_PAYMENT_STATUS_LABELS[paymentStatus.status]
|
||||
|| paymentStatus.status}
|
||||
</Tag>
|
||||
)}
|
||||
</Space>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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<PaymentQrQueueItem[]>(() => {
|
||||
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() {
|
||||
</Modal>
|
||||
<Modal
|
||||
open={payOpen}
|
||||
title={payTasks.length ? `支付码(${payPendingTasks.length} 个待扫码 · 已完成 ${payDoneTasks.length}/${payTasks.length})` : '支付码'}
|
||||
title={`支付码${payQueueItems.length ? `(${payTasks.length} 个待扫码${payQueuedCount ? ` · ${payQueuedCount} 个排队中` : ''}${payDoneCount ? ` · ${payDoneCount} 个已支付` : ''})` : ''}`}
|
||||
width={920}
|
||||
centered
|
||||
onCancel={() => setPayOpen(false)}
|
||||
footer={<Space><Text type="secondary">多选账号会同时生成多个码并排展示,各自独立等待支付;支付后自动确认开通状态</Text><Button onClick={() => setPayOpen(false)}>关闭</Button><Button onClick={() => { setPayBatchIds([]); setPayOpen(false); }}>清空列表</Button></Space>}
|
||||
>
|
||||
{payTasks.length === 0 ? <div style={{ padding: 24, textAlign: 'center' }}><Text type="secondary">正在为选中的账号生成支付码…</Text></div> : <div style={{ maxHeight: '62vh', overflowY: 'auto' }}>
|
||||
{payQueuedCount > 0 && <div style={{ marginBottom: 10, textAlign: 'left' }}><Text type="secondary" style={{ fontSize: 12 }}>还有 {payQueuedCount} 个账号在排队生成支付码(并发不够时会等已出的码付完/超时,可调大右侧「并发」)</Text></div>}
|
||||
{payPendingTasks.length > 0 && <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(230px, 1fr))', gap: 12 }}>
|
||||
{payPendingTasks.map((task) => {
|
||||
const qrValue = payQrValue(task);
|
||||
return (
|
||||
<div key={task.id} style={{ border: '1px solid rgba(128,128,128,.22)', borderRadius: 6, padding: 10, textAlign: 'center' }}>
|
||||
<div style={{ background: '#fff', padding: 6, borderRadius: 6, lineHeight: 0, display: 'inline-block' }}>{resultText(task, 'mini_qrcode_image') ? <img src={miniQrcodeSrc(resultText(task, 'mini_qrcode_image'))} alt="支付码" style={{ width: 150, height: 150, objectFit: 'contain' }} /> : <QRCode value={qrValue} size={150} type="svg" errorLevel="L" bordered={false} color="#000" bgColor="#fff" />}</div>
|
||||
<div><Text strong ellipsis>{payTaskLabel(task)}</Text></div>
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>UID {task.account_uid || '-'}</Text>
|
||||
<div><Text type="secondary" style={{ fontSize: 12 }}>{[resultText(task, 'product_name') || '精英宝典', Number(resultText(task, 'count')) > 1 ? `×${resultText(task, 'count')}` : '', resultText(task, 'amount_text')].filter(Boolean).join(' ')}</Text></div>
|
||||
<div style={{ marginTop: 4 }}><Tag color={TASK_COLORS[task.status] || 'default'}>{resultText(task, 'payment_status_label') || TASK_STATUS_LABELS[task.status] || task.status}</Tag></div>
|
||||
<Text type="secondary" style={{ fontSize: 12 }} ellipsis title={task.message}>{task.message || '-'}</Text>
|
||||
{isLoopbackUrl(qrValue) && <div><Text type="warning" style={{ fontSize: 12 }}>二维码是本机地址(localhost),手机扫不开</Text></div>}
|
||||
<div style={{ marginTop: 6 }}><Space size={6}><Button size="small" icon={<ZoomInOutlined />} onClick={() => setZoomPayTask(task)}>放大扫码</Button><Button size="small" icon={<CopyOutlined />} onClick={() => void copyPayLink(qrValue, '支付链接')}>复制链接</Button></Space></div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>}
|
||||
{payFailedTasks.length > 0 && <div style={{ marginTop: payPendingTasks.length ? 12 : 0, textAlign: 'left' }}>
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>生成失败(没有二维码)</Text>
|
||||
<div style={{ marginTop: 6, display: 'flex', flexWrap: 'wrap', gap: 6 }}>
|
||||
{payFailedTasks.map((task) => <Tag key={task.id} color="error" title={task.message}>{payTaskLabel(task)} · {task.message || '生成失败'}</Tag>)}
|
||||
</div>
|
||||
</div>}
|
||||
{payDoneTasks.length > 0 && <div style={{ marginTop: payPendingTasks.length ? 12 : 0, textAlign: 'left' }}>
|
||||
<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' : 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>}
|
||||
</div>}
|
||||
<PaymentQrQueue
|
||||
items={payQueueItems}
|
||||
emptyText={payDoneCount > 0 ? '待扫码队列已清空,支付完成的账号已自动移除' : '当前没有待扫码支付码'}
|
||||
queuedText="还有账号在排队生成支付码"
|
||||
onZoom={(item) => {
|
||||
const task = payTaskById.get(Number(item.key));
|
||||
if (task) setZoomPayTask(task);
|
||||
}}
|
||||
onCopy={(item) => void copyPayLink(item.copyValue || item.qrValue || '', '支付链接')}
|
||||
/>
|
||||
</Modal>
|
||||
<Modal open={zoomPayTask !== null} title={`扫码支付 · ${zoomPayTask ? payTaskLabel(zoomPayTask) : ''}`} footer={null} onCancel={() => setZoomPayTask(null)} width={680} centered>
|
||||
{zoomPayTask && <div style={{ textAlign: 'center' }}>
|
||||
|
||||
@@ -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<string, { label: string; color: string }> = {
|
||||
error: { label: '查询失败', color: 'warning' },
|
||||
};
|
||||
|
||||
const PAYMENT_STATUS_LABELS: Record<string, string> = {
|
||||
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,
|
||||
}}
|
||||
>
|
||||
<Text strong>助威充值</Text>
|
||||
<Space wrap style={{ marginTop: 8 }}>
|
||||
<Space.Compact>
|
||||
<InputNumber
|
||||
min={1}
|
||||
max={1000}
|
||||
precision={0}
|
||||
value={rechargeAmount}
|
||||
onChange={(value) =>
|
||||
setRechargeAmount(Number(value) || 1)
|
||||
}
|
||||
style={{ width: 110 }}
|
||||
/>
|
||||
<Button disabled>元</Button>
|
||||
</Space.Compact>
|
||||
<Button
|
||||
type="primary"
|
||||
loading={rechargeCreating}
|
||||
disabled={!featureAccount.is_enabled}
|
||||
onClick={() => void createRecharge()}
|
||||
>
|
||||
创建订单
|
||||
</Button>
|
||||
</Space>
|
||||
{rechargeOrder && (
|
||||
<Space
|
||||
orientation="vertical"
|
||||
size={8}
|
||||
style={{ width: '100%', marginTop: 10 }}
|
||||
>
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<QRCode
|
||||
value={rechargeOrder.alipay_qr_code}
|
||||
size={180}
|
||||
bordered={false}
|
||||
color="#000"
|
||||
bgColor="#fff"
|
||||
/>
|
||||
</div>
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
||||
{rechargeOrder.amount_yuan} 元 · 预计{' '}
|
||||
{rechargeOrder.single_reward.toLocaleString()} 微豆
|
||||
</Text>
|
||||
<Button
|
||||
size="small"
|
||||
icon={<ReloadOutlined />}
|
||||
loading={paymentStatusLoading}
|
||||
onClick={() => void refreshPaymentStatus()}
|
||||
>
|
||||
刷新支付状态
|
||||
</Button>
|
||||
{paymentStatus && (
|
||||
<Tag
|
||||
color={
|
||||
paymentStatus.status === 'PAY_STATUS_SUCCESS'
|
||||
? 'success'
|
||||
: 'processing'
|
||||
}
|
||||
>
|
||||
{PAYMENT_STATUS_LABELS[paymentStatus.status] ||
|
||||
paymentStatus.status}
|
||||
</Tag>
|
||||
)}
|
||||
</Space>
|
||||
)}
|
||||
<WeiboRechargePanel
|
||||
enabled={featureAccount.is_enabled}
|
||||
amount={rechargeAmount}
|
||||
onAmountChange={setRechargeAmount}
|
||||
creating={rechargeCreating}
|
||||
onCreate={() => void createRecharge()}
|
||||
order={rechargeOrder}
|
||||
paymentStatus={paymentStatus}
|
||||
statusLoading={paymentStatusLoading}
|
||||
onRefreshStatus={() => void refreshPaymentStatus()}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<Popconfirm
|
||||
@@ -2826,7 +2831,7 @@ export default function WeiboAccountsPage({
|
||||
type="warning"
|
||||
showIcon
|
||||
title="请使用支付宝逐个扫码支付"
|
||||
description="弹窗打开时每 8 秒自动查询待支付和待确认到账项;支付成功后继续按订单号核对微豆到账。可随时取消“自动查询”,弹窗不会因为部分订单支付成功而自动关闭。"
|
||||
description="弹窗打开时每 8 秒自动查询待支付状态;支付成功的二维码会立即从待扫码队列移除,后台继续按订单号核对微豆到账。"
|
||||
/>
|
||||
<Space wrap>
|
||||
<Tag color="processing">
|
||||
@@ -2845,181 +2850,43 @@ export default function WeiboAccountsPage({
|
||||
}
|
||||
</Tag>
|
||||
<Tag color="blue">
|
||||
已支付{' '}
|
||||
{
|
||||
batchRechargeEntries.filter(
|
||||
(entry) =>
|
||||
entry.paymentStatus?.status === 'PAY_STATUS_SUCCESS',
|
||||
).length
|
||||
}
|
||||
已支付 {batchRechargePaidCount}
|
||||
</Tag>
|
||||
</Space>
|
||||
<div
|
||||
style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(auto-fill, minmax(230px, 1fr))',
|
||||
gap: 12,
|
||||
maxHeight: '64vh',
|
||||
overflowY: 'auto',
|
||||
paddingRight: 4,
|
||||
<PaymentQrQueue
|
||||
items={batchRechargeQueueItems}
|
||||
emptyText={
|
||||
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 (
|
||||
<div
|
||||
key={entry.account.id}
|
||||
style={{
|
||||
border: `1px solid ${token.colorBorderSecondary}`,
|
||||
borderRadius: token.borderRadius,
|
||||
padding: 12,
|
||||
textAlign: 'center',
|
||||
background: token.colorBgContainer,
|
||||
}}
|
||||
>
|
||||
<Text strong ellipsis>
|
||||
{entry.account.nickname || entry.account.uid}
|
||||
</Text>
|
||||
<div>
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
||||
UID {entry.account.uid}
|
||||
</Text>
|
||||
</div>
|
||||
{entry.phase === 'queued' && (
|
||||
<div style={{ padding: '42px 0' }}>
|
||||
<Spin size="small" />
|
||||
<div>
|
||||
<Text type="secondary">排队中</Text>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{entry.phase === 'creating' && (
|
||||
<div style={{ padding: '42px 0' }}>
|
||||
<Spin />
|
||||
<div style={{ marginTop: 8 }}>
|
||||
<Text type="secondary">正在创建订单并解析二维码</Text>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{entry.phase === 'error' && (
|
||||
<Alert
|
||||
type="error"
|
||||
showIcon
|
||||
style={{ marginTop: 10, textAlign: 'left' }}
|
||||
title="下单失败"
|
||||
description={entry.error || '未知原因'}
|
||||
/>
|
||||
)}
|
||||
{entry.phase === 'ready' && order && (
|
||||
<Space
|
||||
orientation="vertical"
|
||||
size={8}
|
||||
style={{ width: '100%', marginTop: 8 }}
|
||||
>
|
||||
{order.alipay_qr_code ? (
|
||||
<div
|
||||
style={{
|
||||
display: 'inline-block',
|
||||
background: '#fff',
|
||||
padding: 6,
|
||||
lineHeight: 0,
|
||||
borderRadius: 6,
|
||||
}}
|
||||
>
|
||||
<QRCode
|
||||
value={order.alipay_qr_code}
|
||||
size={160}
|
||||
bordered={false}
|
||||
color="#000"
|
||||
bgColor="#fff"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<Alert
|
||||
type="warning"
|
||||
showIcon
|
||||
title="订单已创建但未解析到二维码"
|
||||
description="可复制支付链接后手动打开"
|
||||
/>
|
||||
)}
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
||||
{order.amount_yuan} 元 · 预计{' '}
|
||||
{order.single_reward.toLocaleString()} 微豆
|
||||
</Text>
|
||||
<Tag color={paymentColor}>
|
||||
{payment
|
||||
? PAYMENT_STATUS_LABELS[payment.status] || payment.status
|
||||
: '待检查支付状态'}
|
||||
</Tag>
|
||||
{payment?.status === 'PAY_STATUS_SUCCESS' && (
|
||||
<Tag
|
||||
color={
|
||||
entry.creditPhase === 'credited'
|
||||
? 'success'
|
||||
: entry.creditPhase === 'error'
|
||||
? 'error'
|
||||
: 'processing'
|
||||
}
|
||||
>
|
||||
{entry.creditPhase === 'credited'
|
||||
? `已到账 +${entry.creditPoints ?? 0}`
|
||||
: entry.creditPhase === 'pending'
|
||||
? '支付成功,微豆暂未查到'
|
||||
: entry.creditPhase === 'error'
|
||||
? '到账查询失败'
|
||||
: '正在核对到账'}
|
||||
</Tag>
|
||||
)}
|
||||
{entry.creditError && (
|
||||
<Text type="danger" style={{ fontSize: 12 }}>
|
||||
{entry.creditError}
|
||||
</Text>
|
||||
)}
|
||||
{entry.statusError && (
|
||||
<Text type="danger" style={{ fontSize: 12 }}>
|
||||
{entry.statusError}
|
||||
</Text>
|
||||
)}
|
||||
<Space wrap style={{ justifyContent: 'center' }}>
|
||||
{order.alipay_qr_code && (
|
||||
<Button
|
||||
size="small"
|
||||
icon={<ZoomInOutlined />}
|
||||
onClick={() => setBatchRechargeZoom(entry)}
|
||||
>
|
||||
放大
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
size="small"
|
||||
icon={<CopyOutlined />}
|
||||
onClick={() => void copyBatchRechargeLink(entry)}
|
||||
>
|
||||
复制链接
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
icon={<ReloadOutlined />}
|
||||
loading={entry.checkingStatus}
|
||||
onClick={() => void checkBatchRechargeEntry(entry)}
|
||||
>
|
||||
刷新状态
|
||||
</Button>
|
||||
</Space>
|
||||
</Space>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
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 && (
|
||||
<Alert
|
||||
type="error"
|
||||
showIcon
|
||||
title={`已支付但微豆到账确认失败(${batchRechargeCreditFailures.length} 个)`}
|
||||
description={batchRechargeCreditFailures
|
||||
.slice(0, 5)
|
||||
.map((entry) =>
|
||||
`${entry.account.nickname || entry.account.uid}:${entry.creditError || '未查询到到账记录'}`,
|
||||
)
|
||||
.join(';')}
|
||||
/>
|
||||
)}
|
||||
</Space>
|
||||
</Modal>
|
||||
|
||||
@@ -3916,116 +3783,18 @@ export default function WeiboAccountsPage({
|
||||
{!canRecharge ? (
|
||||
<Alert type="warning" showIcon title="没有充值权限" />
|
||||
) : (
|
||||
<Space wrap>
|
||||
<Space.Compact>
|
||||
<InputNumber
|
||||
min={1}
|
||||
max={1000}
|
||||
precision={0}
|
||||
value={rechargeAmount}
|
||||
onChange={(value) => setRechargeAmount(Number(value) || 1)}
|
||||
style={{ width: 110 }}
|
||||
/>
|
||||
<Button disabled>元</Button>
|
||||
</Space.Compact>
|
||||
<Button
|
||||
type="primary"
|
||||
loading={rechargeCreating}
|
||||
disabled={!featureAccount.is_enabled}
|
||||
onClick={() => void createRecharge()}
|
||||
>
|
||||
创建充值订单
|
||||
</Button>
|
||||
</Space>
|
||||
)}
|
||||
{rechargeOrder && (
|
||||
<Space
|
||||
align="start"
|
||||
size={24}
|
||||
wrap
|
||||
style={{ alignItems: 'flex-start' }}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: 240,
|
||||
padding: 12,
|
||||
background: '#fff',
|
||||
borderRadius: 6,
|
||||
textAlign: 'center',
|
||||
}}
|
||||
>
|
||||
<QRCode
|
||||
value={rechargeOrder.alipay_qr_code}
|
||||
size={216}
|
||||
bordered={false}
|
||||
color="#000"
|
||||
bgColor="#fff"
|
||||
/>
|
||||
</div>
|
||||
<Space orientation="vertical" size={10}>
|
||||
<Descriptions
|
||||
size="small"
|
||||
column={1}
|
||||
items={[
|
||||
{
|
||||
key: 'amount',
|
||||
label: '支付金额',
|
||||
children: `${rechargeOrder.amount_yuan} 元`,
|
||||
},
|
||||
{
|
||||
key: 'reward',
|
||||
label: '预计微豆',
|
||||
children: rechargeOrder.single_reward.toLocaleString(),
|
||||
},
|
||||
{
|
||||
key: 'order',
|
||||
label: '订单号',
|
||||
children: (
|
||||
<Text copyable style={{ fontSize: 12 }}>
|
||||
{rechargeOrder.order_id}
|
||||
</Text>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<Space wrap>
|
||||
<Button
|
||||
icon={<ReloadOutlined />}
|
||||
loading={paymentStatusLoading}
|
||||
onClick={() => void refreshPaymentStatus()}
|
||||
>
|
||||
刷新支付状态
|
||||
</Button>
|
||||
{rechargeOrder.desktop_pay_url && (
|
||||
<Button
|
||||
href={rechargeOrder.desktop_pay_url}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
电脑端支付
|
||||
</Button>
|
||||
)}
|
||||
<Button onClick={() => void refreshAfterRecharge()}>
|
||||
刷新到账
|
||||
</Button>
|
||||
</Space>
|
||||
{paymentStatus && (
|
||||
<Alert
|
||||
type={
|
||||
paymentStatus.status === 'PAY_STATUS_SUCCESS'
|
||||
? 'success'
|
||||
: 'info'
|
||||
}
|
||||
showIcon
|
||||
title={
|
||||
PAYMENT_STATUS_LABELS[paymentStatus.status] ||
|
||||
paymentStatus.status
|
||||
}
|
||||
description={paymentStatus.sub_status}
|
||||
/>
|
||||
)}
|
||||
</Space>
|
||||
</Space>
|
||||
<WeiboRechargePanel
|
||||
enabled={featureAccount.is_enabled}
|
||||
amount={rechargeAmount}
|
||||
onAmountChange={setRechargeAmount}
|
||||
creating={rechargeCreating}
|
||||
onCreate={() => void createRecharge()}
|
||||
order={rechargeOrder}
|
||||
paymentStatus={paymentStatus}
|
||||
statusLoading={paymentStatusLoading}
|
||||
onRefreshStatus={() => void refreshPaymentStatus()}
|
||||
onRefreshBalance={() => void refreshAfterRecharge()}
|
||||
/>
|
||||
)}
|
||||
</Space>
|
||||
),
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
export const WEIBO_PAYMENT_STATUS_LABELS: Record<string, string> = {
|
||||
PAY_STATUS_PENDING: '等待支付',
|
||||
PAY_STATUS_SUCCESS: '支付成功',
|
||||
PAY_STATUS_FAILED: '支付失败',
|
||||
PAY_STATUS_CLOSED: '订单关闭',
|
||||
};
|
||||
Reference in New Issue
Block a user