Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 09ada60e70 | |||
| d29dce91fa | |||
| 46587f6b88 |
@@ -142,7 +142,7 @@ function actionLabel(action: string) {
|
||||
'withdraw.approve': '审核通过',
|
||||
'withdraw.reject': '审核拒绝',
|
||||
'withdraw.refresh': '查单刷新',
|
||||
'withdraw.reconcile': '批量对账',
|
||||
'withdraw.reconcile': '处理超时订单',
|
||||
};
|
||||
return labels[action] || action;
|
||||
}
|
||||
@@ -182,8 +182,11 @@ export default function WithdrawsPage() {
|
||||
const [activeStatus, setActiveStatus] = useState('reviewing');
|
||||
const [summary, setSummary] = useState<WithdrawSummary | null>(null);
|
||||
const [health, setHealth] = useState<WxpayHealthCheck | null>(null);
|
||||
const [healthLoading, setHealthLoading] = useState(false);
|
||||
const [healthCheckedAt, setHealthCheckedAt] = useState<number | null>(null);
|
||||
const [ledger, setLedger] = useState<WithdrawLedgerCheck | null>(null);
|
||||
const [ledgerLoading, setLedgerLoading] = useState(false);
|
||||
const [ledgerCheckedAt, setLedgerCheckedAt] = useState<number | null>(null);
|
||||
const [drawerOpen, setDrawerOpen] = useState(false);
|
||||
const [detail, setDetail] = useState<WithdrawDetail | null>(null);
|
||||
const [detailLoading, setDetailLoading] = useState(false);
|
||||
@@ -255,21 +258,36 @@ export default function WithdrawsPage() {
|
||||
}
|
||||
}, []);
|
||||
|
||||
const loadHealth = useCallback(async () => {
|
||||
const loadHealth = useCallback(async (showFeedback = false) => {
|
||||
setHealthLoading(true);
|
||||
try {
|
||||
const { data } = await api.get<WxpayHealthCheck>('/admin/api/withdraws/health-check');
|
||||
setHealth(data);
|
||||
setHealthCheckedAt(Date.now());
|
||||
if (showFeedback) {
|
||||
message.success(data.ok ? '检测完成,微信提现配置正常' : '检测完成,仍有配置项需要处理');
|
||||
}
|
||||
} catch (e) {
|
||||
message.error(errMsg(e));
|
||||
} finally {
|
||||
setHealthLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 账本校验是全表对账、相对重,故只在进页时拉一次 + 提供手动按钮,不随每次写操作重算。
|
||||
const loadLedger = useCallback(async () => {
|
||||
const loadLedger = useCallback(async (showFeedback = false) => {
|
||||
setLedgerLoading(true);
|
||||
try {
|
||||
const { data } = await api.get<WithdrawLedgerCheck>('/admin/api/withdraws/ledger-check');
|
||||
setLedger(data);
|
||||
setLedgerCheckedAt(Date.now());
|
||||
if (showFeedback) {
|
||||
if (data.ok) {
|
||||
message.success('校验完成,现金账本正常');
|
||||
} else {
|
||||
message.warning('校验完成,发现现金账本异常');
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
message.error(errMsg(e));
|
||||
} finally {
|
||||
@@ -465,15 +483,16 @@ export default function WithdrawsPage() {
|
||||
|
||||
const reconcile = () => {
|
||||
modal.confirm({
|
||||
title: '批量对账?',
|
||||
content: '扫描超过 15 分钟仍处于打款中的提现单,逐单向微信查实并归一化。',
|
||||
okText: '开始对账',
|
||||
title: '处理超时打款订单?',
|
||||
content:
|
||||
'将检查超过 15 分钟仍处于“打款中”的订单:微信已成功则更新为“已到账”;失败则退款;仍待用户确认的老单将撤销并退款;仍在处理中则保持不变。',
|
||||
okText: '开始处理',
|
||||
onOk: async () => {
|
||||
try {
|
||||
const { data } = await api.post<{ checked: number; resolved: number }>(
|
||||
'/admin/api/withdraws/reconcile',
|
||||
);
|
||||
message.success(`对账完成: 检查 ${data.checked} 单,解决 ${data.resolved} 单`);
|
||||
message.success(`处理完成:检查 ${data.checked} 单,更新 ${data.resolved} 单`);
|
||||
await refreshAfterChange();
|
||||
} catch (e) {
|
||||
message.error(errMsg(e));
|
||||
@@ -652,6 +671,10 @@ export default function WithdrawsPage() {
|
||||
: '',
|
||||
].filter(Boolean)
|
||||
: [];
|
||||
const payoutReady = Boolean(
|
||||
health?.wxpay_configured && health.private_key_loadable && health.public_key_loadable,
|
||||
);
|
||||
const hasBlockingPayoutIssue = Boolean(health && !payoutReady);
|
||||
|
||||
return (
|
||||
<div>
|
||||
@@ -662,41 +685,62 @@ export default function WithdrawsPage() {
|
||||
</div>
|
||||
{canManage && (
|
||||
<Button danger icon={<ReloadOutlined />} onClick={reconcile}>
|
||||
批量对账
|
||||
处理超时订单
|
||||
</Button>
|
||||
)}
|
||||
</Space>
|
||||
|
||||
{(health || ledger) && (
|
||||
{(hasBlockingPayoutIssue || (ledger && !ledger.ok)) && (
|
||||
<Space direction="vertical" style={{ width: '100%', marginBottom: 16 }}>
|
||||
{health && (
|
||||
{health && hasBlockingPayoutIssue && (
|
||||
<Alert
|
||||
showIcon
|
||||
type={health.ok ? 'success' : 'warning'}
|
||||
message={health.ok ? '微信提现配置正常' : '微信提现配置需检查'}
|
||||
type="warning"
|
||||
message="当前环境无法执行微信打款"
|
||||
description={
|
||||
health.ok
|
||||
? `自动对账已${health.auto_reconcile_enabled ? '开启' : '关闭'},每 ${health.auto_reconcile_interval_sec}s 扫描 ${health.auto_reconcile_older_than_minutes} 分钟前的打款中订单`
|
||||
: health.issues.join(';')
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{ledger && (
|
||||
<Alert
|
||||
showIcon
|
||||
type={ledger.ok ? 'success' : 'error'}
|
||||
message={ledger.ok ? '现金账本校验正常' : '现金账本存在异常'}
|
||||
description={
|
||||
ledger.ok
|
||||
? `现金 余额 ${yuan(ledger.cash_balance_total_cents)} / 流水 ${yuan(ledger.cash_transaction_total_cents)};邀请金 余额 ${yuan(ledger.invite_cash_balance_total_cents)} / 流水 ${yuan(ledger.invite_cash_transaction_total_cents)}`
|
||||
: ledgerIssues.join(';')
|
||||
<Space direction="vertical" size={6}>
|
||||
<Text>微信支付配置不完整,点击“通过并打款”将失败。请联系部署管理员处理。</Text>
|
||||
{healthCheckedAt && (
|
||||
<Text type="secondary">
|
||||
最近检测:{dayjs(healthCheckedAt).format('HH:mm:ss')}
|
||||
</Text>
|
||||
)}
|
||||
<details>
|
||||
<summary style={{ color: '#1677ff', cursor: 'pointer' }}>查看技术详情</summary>
|
||||
<ul style={{ margin: '8px 0 0', paddingInlineStart: 20 }}>
|
||||
{health.issues.map((issue) => (
|
||||
<li key={issue} style={{ marginBottom: 4, overflowWrap: 'anywhere' }}>
|
||||
{issue}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</details>
|
||||
</Space>
|
||||
}
|
||||
action={
|
||||
<Button
|
||||
size="small"
|
||||
icon={<ReloadOutlined />}
|
||||
loading={healthLoading}
|
||||
onClick={() => void loadHealth(true)}
|
||||
>
|
||||
重新检测
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{ledger && !ledger.ok && (
|
||||
<Alert
|
||||
showIcon
|
||||
type="error"
|
||||
message="现金账本存在异常"
|
||||
description={ledgerIssues.join(';')}
|
||||
action={
|
||||
<Button
|
||||
size="small"
|
||||
icon={<ReloadOutlined />}
|
||||
loading={ledgerLoading}
|
||||
onClick={() => void loadLedger()}
|
||||
onClick={() => void loadLedger(true)}
|
||||
>
|
||||
重新校验
|
||||
</Button>
|
||||
@@ -706,6 +750,36 @@ export default function WithdrawsPage() {
|
||||
</Space>
|
||||
)}
|
||||
|
||||
{(health || ledger) && (
|
||||
<Space
|
||||
size={12}
|
||||
wrap
|
||||
style={{ width: '100%', justifyContent: 'flex-end', marginBottom: 12 }}
|
||||
>
|
||||
{health && payoutReady && <Tag color="success">微信打款可用</Tag>}
|
||||
{ledger?.ok && (
|
||||
<Space size={6}>
|
||||
<CheckCircleOutlined style={{ color: '#52c41a' }} />
|
||||
<Text strong>账本正常</Text>
|
||||
{ledgerCheckedAt && (
|
||||
<Text type="secondary">
|
||||
· {dayjs(ledgerCheckedAt).format('HH:mm:ss')} 校验
|
||||
</Text>
|
||||
)}
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
icon={<ReloadOutlined />}
|
||||
loading={ledgerLoading}
|
||||
onClick={() => void loadLedger(true)}
|
||||
>
|
||||
重新校验
|
||||
</Button>
|
||||
</Space>
|
||||
)}
|
||||
</Space>
|
||||
)}
|
||||
|
||||
<Row gutter={12} style={{ marginBottom: 16 }}>
|
||||
<Col xs={24} sm={12}>
|
||||
<Card size="small">
|
||||
|
||||
Reference in New Issue
Block a user