-
-
-
提现审核
-
- 针对赚钱页面的账户。审核通过会立即发起微信提现,拒绝会退回用户现金余额。
-
-
- {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;