feat: 运营后台完善微信提现审核列表和批量处理

新增提现审核列表的关键词搜索、日期范围筛选、快捷筛选、排序控件和重置筛选能力。

新增提现详情抽屉,展示用户快照、风险提示、处理时间线、最近提现和现金流水。

支持批量通过、批量拒绝、批量刷新查单,并优化批量操作确认与结果提示。

修复筛选请求竞态,避免旧请求晚返回覆盖新的筛选结果。
This commit is contained in:
OuYingJun1024
2026-06-08 17:31:13 +08:00
parent d7c7d3e046
commit e377234137
3 changed files with 1097 additions and 65 deletions
File diff suppressed because it is too large Load Diff
+78 -1
View File
@@ -71,7 +71,8 @@ export interface WithdrawOrder {
user_id: number;
out_bill_no: string;
amount_cents: number;
status: string; // pending / success / failed
user_name: string | null;
status: string; // reviewing / pending / success / failed / rejected
wechat_state: string | null;
transfer_bill_no: string | null;
fail_reason: string | null;
@@ -79,6 +80,82 @@ export interface WithdrawOrder {
updated_at: string;
}
export interface WithdrawSummary {
reviewing_count: number;
reviewing_amount_cents: number;
pending_count: number;
failed_count: number;
today_success_count: number;
today_success_amount_cents: number;
today_rejected_count: number;
}
export interface WithdrawUserSnapshot {
id: number;
phone: string;
nickname: string | null;
status: string;
wechat_nickname: string | null;
wechat_avatar_url: string | null;
created_at: string;
last_login_at: string;
cash_balance_cents: number;
withdraw_total: number;
withdraw_success_cents: number;
}
export interface WithdrawDetail {
order: WithdrawOrder;
user: WithdrawUserSnapshot | null;
risk_flags: string[];
risk_score: number;
recent_withdraws: WithdrawOrder[];
recent_cash_transactions: CashTxn[];
audit_logs: AuditLog[];
}
export interface WithdrawBulkItemResult {
out_bill_no: string;
ok: boolean;
status: string | null;
error: string | null;
}
export interface WithdrawBulkResult {
total: number;
success: number;
failed: number;
items: WithdrawBulkItemResult[];
}
export interface WithdrawLedgerCheck {
ok: boolean;
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;
}
export interface WxpayHealthCheck {
ok: boolean;
wxpay_configured: boolean;
wxpay_auth_configured: boolean;
private_key_path: string;
private_key_exists: boolean;
private_key_loadable: boolean;
public_key_path: string;
public_key_exists: boolean;
public_key_loadable: boolean;
auth_notify_url_configured: boolean;
auto_reconcile_enabled: boolean;
auto_reconcile_interval_sec: number;
auto_reconcile_older_than_minutes: number;
issues: string[];
}
export interface Feedback {
id: number;
user_id: number;
+8 -2
View File
@@ -1,6 +1,6 @@
'use client';
import { useCallback, useEffect, useState } from 'react';
import { useCallback, useEffect, useRef, useState } from 'react';
import { api } from './api';
import type { CursorPage } from './types';
@@ -12,26 +12,32 @@ export function useCursorList<T>(url: string, filters: Record<string, unknown>,
const [items, setItems] = useState<T[]>([]);
const [nextCursor, setNextCursor] = useState<number | null>(null);
const [loading, setLoading] = useState(false);
const requestSeq = useRef(0);
const filtersKey = JSON.stringify(filters);
const load = useCallback(
async (cursor: number | null) => {
const seq = requestSeq.current + 1;
requestSeq.current = seq;
setLoading(true);
try {
const params: Record<string, unknown> = { ...JSON.parse(filtersKey), limit };
if (cursor != null) params.cursor = cursor;
const { data } = await api.get<CursorPage<T>>(url, { params });
if (requestSeq.current !== seq) return;
setItems((prev) => (cursor == null ? data.items : [...prev, ...data.items]));
setNextCursor(data.next_cursor);
} finally {
setLoading(false);
if (requestSeq.current === seq) setLoading(false);
}
},
[url, filtersKey, limit],
);
useEffect(() => {
setItems([]);
setNextCursor(null);
load(null);
}, [load]);