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