-
-
-
提现审核
-
- 针对赚钱页面的账户。审核通过会立即发起微信提现,拒绝会退回用户现金余额。
-
-
- {canManage && (
- } onClick={reconcile}>
- 处理超时订单
-
- )}
-
-
- {(hasBlockingPayoutIssue || (ledger && !ledger.ok)) && (
-
- {health && hasBlockingPayoutIssue && (
-
- 微信支付配置不完整,点击“通过并打款”将失败。请联系部署管理员处理。
- {healthCheckedAt && (
-
- 最近检测:{dayjs(healthCheckedAt).format('HH:mm:ss')}
-
- )}
-
- 查看技术详情
-
- {health.issues.map((issue) => (
- -
- {issue}
-
- ))}
-
-
-
- }
- action={
-
}
- loading={healthLoading}
- onClick={() => void loadHealth(true)}
- >
- 重新检测
-
- }
- />
- )}
- {ledger && !ledger.ok && (
-
}
- loading={ledgerLoading}
- onClick={() => void loadLedger(true)}
- >
- 重新校验
-
- }
- />
- )}
-
- )}
-
- {(health || ledger) && (
-
- {health && payoutReady && 微信打款可用}
- {ledger?.ok && (
-
-
- 账本正常
- {ledgerCheckedAt && (
-
- · {dayjs(ledgerCheckedAt).format('HH:mm:ss')} 校验
-
- )}
- }
- loading={ledgerLoading}
- onClick={() => void loadLedger(true)}
- >
- 重新校验
-
-
- )}
-
- )}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {
- const count = summaryCount(summary, tab.key);
- return {
- key: tab.key,
- label:
- count == null ? (
- tab.label
- ) : (
-
- {tab.label}
- {count}
-
- ),
- };
- })}
- />
-
- }
- placeholder="用户手机号"
- value={searchDraft}
- style={{ width: 240 }}
- onChange={(e) => {
- const value = e.target.value;
- setSearchDraft(value);
- if (!value) setKeyword('');
- }}
- onSearch={(value) => setKeyword(value.trim())}
- />
- setDateRange(values as [Dayjs, Dayjs] | null)}
- />
- }
- options={[
- { value: 'desc', label: '倒序' },
- { value: 'asc', label: '正序' },
- ]}
- onChange={(value: 'asc' | 'desc') => setSortOrder(value)}
- />
- } onClick={resetFilters}>
- 重置
-
-
- {canManage && selectedOrders.length > 0 && (
-
-
- 已选 {selectedOrders.length} 笔,待审核 {selectedReviewing.length} 笔,打款中{' '}
- {selectedPending.length} 笔
-
-
- }
- disabled={!selectedReviewing.length}
- onClick={bulkApprove}
- >
- 批量通过
-
- }
- disabled={!selectedReviewing.length}
- onClick={() => {
- setRejecting(null);
- setBulkRejecting(selectedReviewing);
- setRejectReason('');
- }}
- >
- 批量拒绝
-
- }
- disabled={!selectedPending.length}
- onClick={bulkRefresh}
- >
- 批量刷新查单
-
-
-
-
- )}
- setSelectedRowKeys(keys),
- getCheckboxProps: (record) => ({
- disabled: record.status !== 'reviewing' && record.status !== 'pending',
- }),
- }
- : undefined
- }
- columns={columns}
- dataSource={items}
- loading={loading}
- pagination={{
- current: page,
- pageSize,
- total,
- showSizeChanger: true,
- pageSizeOptions: [20, 50, 100],
- showTotal: (t) => `共 ${t} 条`,
- onChange: onPageChange,
- }}
- scroll={{ x: 1350 }}
- onRow={(record) => ({
- onClick: () => openDetail(record),
- style: { cursor: 'pointer' },
- })}
- />
-
-
- setDrawerOpen(false)}
- extra={
- detail && (
- }
- onClick={() => void loadDetail(detail.order.out_bill_no)}
- >
- 刷新详情
-
- )
- }
- >
-
- {detail && (
-
-
- {statusTag(detail.order.status)}
-
- {yuan(detail.order.amount_cents)}
-
-
- {detail.order.user_name || '-'}
-
-
- {detail.order.wechat_state || '-'}
-
-
- {detail.order.out_bill_no}
-
-
- {detail.order.transfer_bill_no || '-'}
-
-
- {detail.order.fail_reason || '-'}
-
-
-
-
-
-
-
风险提示
- {risks.length ? (
-
- {risks.map((risk) => (
-
- {risk}
-
- ))}
-
- ) : (
- 暂无明显风险标签
- )}
-
-
-
-
处理时间线
-
-
-
- )}
-
-
-
- }}
- onOk={confirmReject}
- onCancel={() => {
- setRejecting(null);
- setBulkRejecting([]);
- setRejectReason('');
- }}
- >
- {!!rejectTargets.length && (
-
-
- 拒绝后会把 {yuan(rejectAmount)} 退回用户现金余额。
-
-
- {REJECT_TEMPLATES.map((tpl) => (
-
- ))}
-
- setRejectReason(e.target.value)}
- />
-
- )}
-
-
- );
+ return ;
}
diff --git a/src/app/page.tsx b/src/app/page.tsx
index 1e182e7..2babb5e 100644
--- a/src/app/page.tsx
+++ b/src/app/page.tsx
@@ -1,13 +1,26 @@
'use client';
import { useEffect } from 'react';
-import { useRouter } from 'next/navigation';
import { getToken } from '@/lib/auth';
export default function Home() {
- const router = useRouter();
useEffect(() => {
- router.replace(getToken() ? '/dashboard' : '/login');
- }, [router]);
- return null;
+ window.location.replace(getToken() ? '/dashboard' : '/login');
+ }, []);
+
+ return (
+
+ 正在进入运营后台…
+
+ );
}
diff --git a/src/components/UserRiskModal.tsx b/src/components/UserRiskModal.tsx
new file mode 100644
index 0000000..8ac5bc5
--- /dev/null
+++ b/src/components/UserRiskModal.tsx
@@ -0,0 +1,81 @@
+'use client';
+
+import { useEffect, useState } from 'react';
+import { App, Input, Modal } from 'antd';
+import { api, errMsg } from '@/lib/api';
+
+export type RiskUser = {
+ id: number;
+ phone: string;
+ is_high_risk: boolean;
+ high_risk_note: string | null;
+};
+
+type Props = {
+ user: RiskUser | null;
+ open: boolean;
+ onClose: () => void;
+ onDone: () => void | Promise;
+};
+
+export function UserRiskModal({ user, open, onClose, onDone }: Props) {
+ const { message } = App.useApp();
+ const [note, setNote] = useState('');
+ const [saving, setSaving] = useState(false);
+
+ useEffect(() => {
+ if (open) setNote(user?.high_risk_note ?? '');
+ }, [open, user]);
+
+ const save = async () => {
+ if (!user) return;
+ const trimmed = note.trim();
+ if (!trimmed) {
+ message.warning('请输入标记为高风险的原因');
+ return;
+ }
+ setSaving(true);
+ try {
+ await api.post(`/admin/api/users/${user.id}/risk`, {
+ is_high_risk: true,
+ note: trimmed,
+ });
+ message.success(user.is_high_risk ? '高风险备注已更新' : '已标记为高风险');
+ await onDone();
+ onClose();
+ } catch (error) {
+ message.error(errMsg(error));
+ } finally {
+ setSaving(false);
+ }
+ };
+
+ return (
+
+ setNote(event.target.value)}
+ />
+
+ );
+}
+
+export async function clearUserRisk(userId: number) {
+ await api.post(`/admin/api/users/${userId}/risk`, {
+ is_high_risk: false,
+ note: null,
+ });
+}
diff --git a/src/lib/reviewBadge.ts b/src/lib/reviewBadge.ts
index 78e95d4..d6d04c0 100644
--- a/src/lib/reviewBadge.ts
+++ b/src/lib/reviewBadge.ts
@@ -1,4 +1,8 @@
-export type ReviewBadgeKey = '/withdraws' | '/price-reports' | '/feedbacks';
+export type ReviewBadgeKey =
+ | '/invite-withdraws'
+ | '/withdraws'
+ | '/price-reports'
+ | '/feedbacks';
export const REVIEW_BADGE_REFRESH_EVENT = 'review-badge-refresh';
diff --git a/src/lib/types.ts b/src/lib/types.ts
index 26d8d95..e11b6a6 100644
--- a/src/lib/types.ts
+++ b/src/lib/types.ts
@@ -52,6 +52,8 @@ export interface UserListItem {
status: string;
// 调试链接权限:开了的用户在 App 比价结果页/记录页可复制 price.shaguabijia.com 的 trace 调试链接
debug_trace_enabled: boolean;
+ is_high_risk: boolean;
+ high_risk_note: string | null;
// 运营「一键开启新手引导」:true = 该用户下次打开 App 会被强制重走新手引导,走完自动取消
force_onboarding: boolean;
wechat_openid: string | null;
@@ -143,7 +145,6 @@ export interface WithdrawOrder {
amount_cents: number;
// 提现类型:coin_cash(福利页提现,金币兑换现金) / invite_cash(邀请提现,邀请奖励金)
source: string;
- user_name: string | null;
status: string; // reviewing / pending / success / failed / rejected
wechat_state: string | null;
transfer_bill_no: string | null;
@@ -157,6 +158,8 @@ export interface WithdrawOrder {
export interface WithdrawListItem extends WithdrawOrder {
phone: string | null;
nickname: string | null;
+ is_high_risk: boolean;
+ high_risk_note: string | null;
cumulative_success_cents: number; // 累计成功提现(分)
}
@@ -174,6 +177,8 @@ export interface WithdrawUserSnapshot {
id: number;
phone: string;
nickname: string | null;
+ is_high_risk: boolean;
+ high_risk_note: string | null;
status: string;
wechat_nickname: string | null;
wechat_avatar_url: string | null;
@@ -184,14 +189,34 @@ export interface WithdrawUserSnapshot {
withdraw_success_cents: number;
}
+export interface InviteeDetail {
+ user_id: number;
+ phone: string;
+ registered_at: string;
+ invite_success: boolean;
+ first_compare_store: string | null;
+ first_compare_products: string | null;
+ first_order_store: string | null;
+ first_order_products: string | null;
+ first_order_amount_cents: number | null;
+}
+
+export interface InviteOverview {
+ invite_total: number;
+ invite_success_total: number;
+ items: InviteeDetail[];
+}
+
export interface WithdrawDetail {
order: WithdrawOrder;
user: WithdrawUserSnapshot | null;
+ cumulative_success_cents: number;
risk_flags: string[];
risk_score: number;
recent_withdraws: WithdrawOrder[];
recent_cash_transactions: CashTxn[];
audit_logs: AuditLog[];
+ invite_overview: InviteOverview | null;
}
// 提现详情抽屉「用户统计区」:按时间窗口算的提现 + 看广告行为统计。
@@ -318,26 +343,6 @@ export interface WithdrawBulkResult {
items: WithdrawBulkItemResult[];
}
-export interface WithdrawLedgerCheck {
- ok: boolean;
- // 普通现金账(coin_cash)
- cash_balance_total_cents: number;
- cash_transaction_total_cents: number;
- balance_diff_cents: number;
- missing_withdraw_txn_count: number;
- missing_refund_txn_count: number;
- duplicate_refund_txn_count: number;
- refund_txn_on_non_terminal_count: number;
- // 邀请奖励金账(invite_cash:与普通现金物理隔离,各自对账)
- invite_cash_balance_total_cents: number;
- invite_cash_transaction_total_cents: number;
- invite_balance_diff_cents: number;
- invite_missing_withdraw_txn_count: number;
- invite_missing_refund_txn_count: number;
- invite_duplicate_refund_txn_count: number;
- invite_refund_txn_on_non_terminal_count: number;
-}
-
export interface WxpayHealthCheck {
ok: boolean;
wxpay_configured: boolean;
--
2.52.0
From a189c7df9a044a3eae38b5395e4c7180e993387a Mon Sep 17 00:00:00 2001
From: linkeyu <798648091@qq.com>
Date: Mon, 27 Jul 2026 10:20:04 +0800
Subject: [PATCH 2/2] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=EF=BC=9A=E5=90=8C?=
=?UTF-8?q?=E6=AD=A5=E6=8F=90=E7=8E=B0=E5=AE=A1=E6=A0=B8=E5=88=97=E8=A1=A8?=
=?UTF-8?q?=E4=B8=8E=E7=BB=9F=E8=AE=A1?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
邀请提现和其他提现页在窗口聚焦、恢复可见及每分钟自动刷新当前列表和统计。
---
.../(main)/withdraws/WithdrawReviewPage.tsx | 30 +++++++++++++++++--
1 file changed, 28 insertions(+), 2 deletions(-)
diff --git a/src/app/(main)/withdraws/WithdrawReviewPage.tsx b/src/app/(main)/withdraws/WithdrawReviewPage.tsx
index bed7a0f..bc2be6e 100644
--- a/src/app/(main)/withdraws/WithdrawReviewPage.tsx
+++ b/src/app/(main)/withdraws/WithdrawReviewPage.tsx
@@ -1,6 +1,6 @@
'use client';
-import { useCallback, useEffect, useState } from 'react';
+import { useCallback, useEffect, useRef, useState } from 'react';
import type { Key } from 'react';
import {
App,
@@ -61,6 +61,7 @@ import {
const { Text } = Typography;
const { RangePicker } = DatePicker;
+const REVIEW_PAGE_POLL_INTERVAL_MS = 60_000;
// 提现单 / 审计 / 用户均为 UTC 口径(server_default=func.now());现金流水为北京 wall-clock。
const dt = (v: string) => formatUtcTime(v);
@@ -194,6 +195,10 @@ export function WithdrawReviewPage({ mode = 'other' }: { mode?: WithdrawReviewMo
: {};
const { items, total, page, pageSize, loading, onChange: onPageChange, reload } =
usePagedList('/admin/api/withdraws', filters, 20);
+ const reloadRef = useRef(reload);
+ useEffect(() => {
+ reloadRef.current = reload;
+ }, [reload]);
const canManage = canDo(['finance']);
const selectedOrders = items.filter((item) => selectedRowKeys.includes(item.id));
const selectedReviewing = selectedOrders.filter((item) => item.status === 'reviewing');
@@ -257,10 +262,31 @@ export function WithdrawReviewPage({ mode = 'other' }: { mode?: WithdrawReviewMo
}
}, []);
+ const refreshReviewData = useCallback(() => {
+ void reloadRef.current();
+ void loadSummary();
+ }, [loadSummary]);
+
useEffect(() => {
void loadSummary();
void loadHealth();
- }, [loadHealth, loadSummary]);
+
+ const onWindowFocus = () => refreshReviewData();
+ const onVisibilityChange = () => {
+ if (document.visibilityState === 'visible') refreshReviewData();
+ };
+ const pollTimer = window.setInterval(() => {
+ if (document.visibilityState === 'visible') refreshReviewData();
+ }, REVIEW_PAGE_POLL_INTERVAL_MS);
+
+ window.addEventListener('focus', onWindowFocus);
+ document.addEventListener('visibilitychange', onVisibilityChange);
+ return () => {
+ window.clearInterval(pollTimer);
+ window.removeEventListener('focus', onWindowFocus);
+ document.removeEventListener('visibilitychange', onVisibilityChange);
+ };
+ }, [loadHealth, loadSummary, refreshReviewData]);
useEffect(() => {
setSelectedRowKeys([]);
--
2.52.0