Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3c757aa838 | |||
| 45b9c7140d | |||
| e377234137 | |||
| d7c7d3e046 |
@@ -68,6 +68,25 @@ export default function UsersPage() {
|
||||
});
|
||||
};
|
||||
|
||||
const toggleDebugTrace = (u: UserListItem) => {
|
||||
const next = !u.debug_trace_enabled;
|
||||
Modal.confirm({
|
||||
title: `确认${next ? '开启' : '关闭'}用户 ${u.phone} 的调试链接权限?`,
|
||||
content: next
|
||||
? '开启后,该用户在 App 比价结果页/记录页可复制 trace 调试链接发给开发排障'
|
||||
: undefined,
|
||||
onOk: async () => {
|
||||
try {
|
||||
await api.post(`/admin/api/users/${u.id}/debug-trace`, { enabled: next });
|
||||
message.success('已更新调试链接权限');
|
||||
reload();
|
||||
} catch (e) {
|
||||
message.error(errMsg(e));
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const columns: ColumnsType<UserListItem> = [
|
||||
{ title: 'ID', dataIndex: 'id', width: 70 },
|
||||
{ title: '手机号', dataIndex: 'phone' },
|
||||
@@ -94,6 +113,11 @@ export default function UsersPage() {
|
||||
{canStatus && u.status !== 'deleted' && (
|
||||
<a onClick={() => toggleStatus(u)}>{u.status === 'active' ? '封禁' : '解封'}</a>
|
||||
)}
|
||||
{canStatus && u.status !== 'deleted' && (
|
||||
<a onClick={() => toggleDebugTrace(u)}>
|
||||
{u.debug_trace_enabled ? '关调试链接' : '开调试链接'}
|
||||
</a>
|
||||
)}
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
|
||||
+1011
-62
File diff suppressed because it is too large
Load Diff
+80
-1
@@ -27,6 +27,8 @@ export interface UserListItem {
|
||||
nickname: string | null;
|
||||
register_channel: string;
|
||||
status: string;
|
||||
// 调试链接权限:开了的用户在 App 比价结果页/记录页可复制 price.shaguabijia.com 的 trace 调试链接
|
||||
debug_trace_enabled: boolean;
|
||||
wechat_openid: string | null;
|
||||
created_at: string;
|
||||
last_login_at: string;
|
||||
@@ -71,7 +73,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 +82,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;
|
||||
|
||||
@@ -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]);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user