import { useCallback, useEffect, useMemo, useState } from "react";
import {
approveFinanceWithdrawalApplication,
approveOperationsWithdrawalApplication,
createFinanceCoinSellerRechargeOrder,
exportFinanceRechargeBills,
fetchFinanceOptions,
fetchFinanceRechargeApps,
fetchFinanceRechargeOverview,
fetchFinanceRechargeSummaries,
fetchFinanceSession,
fetchFinanceWithdrawalOptions,
grantFinanceCoinSellerRechargeOrder,
getFinanceCoinSellerRechargeExchangeRate,
getFinanceUSDTAddresses,
listFinanceCoinSellerRechargeOrders,
listFinanceRechargeDetails,
listFinanceRechargeRegions,
listFinanceWithdrawalApplications,
listOperationsWithdrawalApplications,
refreshFinanceGoogleRechargePaid,
replaceFinanceCoinSellerRechargeExchangeRate,
rejectFinanceWithdrawalApplication,
rejectOperationsWithdrawalApplication,
quoteFinanceCoinSellerRechargeOrder,
upsertFinanceUSDTAddress,
verifyFinanceCoinSellerRechargeReceipt,
verifyFinanceCoinSellerRechargeOrder,
} from "./api.js";
import { FinanceAuditDialog } from "./components/FinanceAuditDialog.jsx";
import { FinanceCoinSellerRechargeOrderList } from "./components/FinanceCoinSellerRechargeOrderList.jsx";
import { FinanceOverview } from "./components/FinanceOverview.jsx";
import { FinanceReconciliation } from "./components/FinanceReconciliation.jsx";
import { FinanceRechargeDetailList } from "./components/FinanceRechargeDetailList.jsx";
import { FinanceShell } from "./components/FinanceShell.jsx";
import { FinanceWithdrawalApplicationList } from "./components/FinanceWithdrawalApplicationList.jsx";
import { EMPTY_RECHARGE_OVERVIEW, EMPTY_RECHARGE_SUMMARY } from "./format.js";
import { downloadCsv } from "@/shared/api/download";
import { buildLoginRedirectPath } from "@/features/auth/loginRedirect.js";
import { AppIdentity } from "@/shared/ui/AppIdentity.jsx";
import { TimeRangeFilter } from "@/shared/ui/TimeRangeFilter.jsx";
import { useToast } from "@/shared/ui/ToastProvider.jsx";
const emptyOptions = { apps: [] };
const pageSize = 50;
const DAY_MS = 86_400_000;
const CHINA_OFFSET_MS = 8 * 3_600_000;
const RECHARGE_VIEWS = new Set(["overview", "rechargeDetails", "recon"]);
const FINANCE_VIEW_ALIASES = new Map([["withdrawals", "withdrawalFinance"]]);
function requestedFinanceView() {
const requested = new URLSearchParams(window.location.search).get("view") || "";
return FINANCE_VIEW_ALIASES.get(requested) || requested;
}
function canAccessFinanceView(session, view) {
if (RECHARGE_VIEWS.has(view)) {
return Boolean(session?.canViewAppRechargeDetails);
}
if (view === "coinSellerRechargeOrders") {
return Boolean(session?.canViewCoinSellerRechargeOrders);
}
if (view === "withdrawalOperations") {
return Boolean(session?.canViewOperationsWithdrawalApplications);
}
if (view === "withdrawalFinance") {
return Boolean(session?.canViewWithdrawalApplications);
}
return false;
}
function defaultFinanceView(session) {
if (session?.canViewAppRechargeDetails) {
return "overview";
}
if (session?.canViewCoinSellerRechargeOrders) {
return "coinSellerRechargeOrders";
}
if (session?.canViewOperationsWithdrawalApplications) {
return "withdrawalOperations";
}
if (session?.canViewWithdrawalApplications) {
return "withdrawalFinance";
}
return "";
}
function resolvedFinanceView(session) {
const requested = requestedFinanceView();
return canAccessFinanceView(session, requested) ? requested : defaultFinanceView(session);
}
function syncFinanceView(view) {
if (!view) {
return;
}
const url = new URL(window.location.href);
if (url.searchParams.get("view") === view) {
return;
}
// 工作台是独立 HTML 入口,内部视图只同步 query;保留其他筛选参数和 hash,避免点击菜单破坏外部深链上下文。
url.searchParams.set("view", view);
window.history.replaceState(window.history.state, "", `${url.pathname}${url.search}${url.hash}`);
}
function chinaDayStart(timestamp) {
return Math.floor((timestamp + CHINA_OFFSET_MS) / DAY_MS) * DAY_MS - CHINA_OFFSET_MS;
}
function timePresets() {
const now = Date.now();
const todayStart = chinaDayStart(now);
const yesterdayStart = todayStart - DAY_MS;
const chinaNow = new Date(now + CHINA_OFFSET_MS);
const monthStart = Date.UTC(chinaNow.getUTCFullYear(), chinaNow.getUTCMonth(), 1) - CHINA_OFFSET_MS;
return [
{ key: "today", label: "今天", range: { endMs: todayStart + DAY_MS, startMs: todayStart } },
// 财务后台统计按中国自然日切分,昨天固定为昨日 00:00 到今日 00:00,避免滚动 24 小时影响对账口径。
{ key: "yesterday", label: "昨天", range: { endMs: todayStart, startMs: yesterdayStart } },
{ key: "7d", label: "近7天", range: { endMs: todayStart + DAY_MS, startMs: todayStart - 6 * DAY_MS } },
{ key: "30d", label: "近30天", range: { endMs: todayStart + DAY_MS, startMs: todayStart - 29 * DAY_MS } },
{ key: "month", label: "本月", range: { endMs: todayStart + DAY_MS, startMs: monthStart } },
{ key: "all", label: "不限", range: { endMs: "", startMs: "" } },
];
}
function reconMonthMeta(offset) {
const chinaNow = new Date(Date.now() + CHINA_OFFSET_MS);
const year = chinaNow.getUTCFullYear();
const month = chinaNow.getUTCMonth() + offset;
const startMs = Date.UTC(year, month, 1) - CHINA_OFFSET_MS;
const endMs = Date.UTC(year, month + 1, 1) - CHINA_OFFSET_MS;
const startDate = new Date(startMs + CHINA_OFFSET_MS);
return {
endMs,
isCurrent: offset === 0,
label: `${startDate.getUTCFullYear()}年${startDate.getUTCMonth() + 1}月`,
startMs,
};
}
export function FinanceApp() {
const { showToast } = useToast();
const [sessionState, setSessionState] = useState({ error: "", loading: true, session: null });
const [optionsState, setOptionsState] = useState({ data: emptyOptions, error: "", loading: false });
const [withdrawalOptionsState, setWithdrawalOptionsState] = useState({
data: emptyOptions,
error: "",
loading: false,
});
const [rechargeDetailsState, setRechargeDetailsState] = useState({
data: { items: [], page: 1, pageSize, total: 0 },
error: "",
loading: false,
});
const [rechargeSummaryState, setRechargeSummaryState] = useState({
merged: EMPTY_RECHARGE_SUMMARY,
loading: false,
perApp: [],
prev: null,
});
const [rechargeOverviewState, setRechargeOverviewState] = useState({
data: EMPTY_RECHARGE_OVERVIEW,
loading: false,
});
const [rechargeRegionsState, setRechargeRegionsState] = useState({ byApp: {}, loading: false });
const [rechargeAppsState, setRechargeAppsState] = useState({ items: [], loaded: false });
const [googlePaidRefreshing, setGooglePaidRefreshing] = useState(false);
const [reconMonthOffset, setReconMonthOffset] = useState(0);
const [reconState, setReconState] = useState({
loading: false,
overview: EMPTY_RECHARGE_OVERVIEW,
summary: EMPTY_RECHARGE_SUMMARY,
});
const [withdrawalsState, setWithdrawalsState] = useState({
data: { items: [], page: 1, pageSize, total: 0 },
error: "",
loading: false,
});
const [operationsWithdrawalsState, setOperationsWithdrawalsState] = useState({
data: { items: [], page: 1, pageSize, total: 0 },
error: "",
loading: false,
});
const [coinSellerRechargeOrdersState, setCoinSellerRechargeOrdersState] = useState({
data: { items: [], page: 1, pageSize, total: 0 },
error: "",
loading: false,
});
const [coinSellerRechargeOrderActionLoading, setCoinSellerRechargeOrderActionLoading] = useState("");
const [activeView, setActiveView] = useState("overview");
const [rechargeDetailFilters, setRechargeDetailFilters] = useState(() => ({
appCode: "",
currency: "usd",
keyword: "",
page: 1,
paidState: "",
rechargeType: "",
regionId: "",
timeRange: timePresets().find((preset) => preset.key === "yesterday").range,
}));
const [rechargeExporting, setRechargeExporting] = useState(false);
const [withdrawalFilters, setWithdrawalFilters] = useState({ appCode: "", keyword: "", page: 1 });
const [operationsWithdrawalFilters, setOperationsWithdrawalFilters] = useState({
appCode: "",
keyword: "",
page: 1,
});
const [coinSellerRechargeOrderFilters, setCoinSellerRechargeOrderFilters] = useState({
appCode: "",
grantStatus: "",
keyword: "",
page: 1,
providerCode: "",
status: "",
verifyStatus: "",
});
const [withdrawalAuditTarget, setWithdrawalAuditTarget] = useState(null);
const [withdrawalAuditLoading, setWithdrawalAuditLoading] = useState(false);
useEffect(() => {
let mounted = true;
fetchFinanceSession()
.then((session) => {
if (!mounted) {
return;
}
setSessionState({ error: "", loading: false, session });
const nextView = resolvedFinanceView(session);
setActiveView(nextView);
syncFinanceView(nextView);
})
.catch((err) => {
if (mounted) {
setSessionState({ error: err.message || "登录已失效", loading: false, session: null });
}
});
return () => {
mounted = false;
};
}, []);
const session = sessionState.session;
const canLoadOptions = Boolean(session?.canAccessFinance);
const canLoadWithdrawalOptions = Boolean(
session?.canViewOperationsWithdrawalApplications || session?.canViewWithdrawalApplications,
);
const canViewFinance = Boolean(session?.canViewAppRechargeDetails);
// 财务三页(概览/流水/对账)共享同一套 App × 时间 × 计价 全局筛选。
const rechargeBaseQuery = useMemo(
() => ({
app_code: rechargeDetailFilters.appCode,
end_at_ms: rechargeDetailFilters.timeRange.endMs,
keyword: rechargeDetailFilters.keyword,
recharge_type: rechargeDetailFilters.rechargeType,
region_id: rechargeDetailFilters.appCode ? rechargeDetailFilters.regionId : "",
start_at_ms: rechargeDetailFilters.timeRange.startMs,
stat_tz: "Asia/Shanghai",
status: "succeeded",
}),
[rechargeDetailFilters],
);
const rechargeDetailQuery = useMemo(
() => ({
...rechargeBaseQuery,
page: rechargeDetailFilters.page,
page_size: pageSize,
paid_state: rechargeDetailFilters.paidState,
}),
[rechargeBaseQuery, rechargeDetailFilters.page, rechargeDetailFilters.paidState],
);
const withdrawalQuery = useMemo(
() => ({
app_code: withdrawalFilters.appCode,
keyword: withdrawalFilters.keyword,
page: withdrawalFilters.page,
page_size: pageSize,
}),
[withdrawalFilters],
);
const operationsWithdrawalQuery = useMemo(
() => ({
app_code: operationsWithdrawalFilters.appCode,
keyword: operationsWithdrawalFilters.keyword,
page: operationsWithdrawalFilters.page,
page_size: pageSize,
}),
[operationsWithdrawalFilters],
);
const coinSellerRechargeOrderQuery = useMemo(
() => ({
app_code: coinSellerRechargeOrderFilters.appCode,
grant_status: coinSellerRechargeOrderFilters.grantStatus,
keyword: coinSellerRechargeOrderFilters.keyword,
page: coinSellerRechargeOrderFilters.page,
page_size: pageSize,
provider_code: coinSellerRechargeOrderFilters.providerCode,
status: coinSellerRechargeOrderFilters.status,
verify_status: coinSellerRechargeOrderFilters.verifyStatus,
}),
[coinSellerRechargeOrderFilters],
);
const loadOptions = useCallback(async () => {
if (!canLoadOptions) {
return;
}
setOptionsState((current) => ({ ...current, error: "", loading: true }));
try {
const data = await fetchFinanceOptions();
setOptionsState({ data, error: "", loading: false });
} catch (err) {
setOptionsState({ data: emptyOptions, error: err.message || "加载财务选项失败", loading: false });
}
}, [canLoadOptions]);
const loadWithdrawalOptions = useCallback(async () => {
if (!canLoadWithdrawalOptions) {
return;
}
setWithdrawalOptionsState((current) => ({ ...current, error: "", loading: true }));
try {
const data = await fetchFinanceWithdrawalOptions();
setWithdrawalOptionsState({ data, error: "", loading: false });
} catch (err) {
// 财务范围加载失败时必须保持空目录,不能回退全量 options 造成越权筛选入口。
setWithdrawalOptionsState({
data: emptyOptions,
error: err.message || "加载提现 App 范围失败",
loading: false,
});
}
}, [canLoadWithdrawalOptions]);
// 充值详情的 App 目录来自 recharge-apps(含 Yumi/Aslan 等 legacy 外部账单源);接口未就绪时回退到财务通用 App 列表。
const rechargeApps = useMemo(
() =>
rechargeAppsState.loaded && rechargeAppsState.items.length
? rechargeAppsState.items
: optionsState.data.apps,
[optionsState.data.apps, rechargeAppsState],
);
const loadRechargeApps = useCallback(async () => {
if (!canViewFinance) {
return;
}
try {
const items = await fetchFinanceRechargeApps();
setRechargeAppsState({ items, loaded: true });
} catch {
setRechargeAppsState({ items: [], loaded: false });
}
}, [canViewFinance]);
const loadRechargeDetails = useCallback(async () => {
if (!canViewFinance) {
return;
}
setRechargeDetailsState((current) => ({ ...current, error: "", loading: true }));
try {
const data = await listFinanceRechargeDetails(rechargeDetailQuery, rechargeApps);
setRechargeDetailsState({ data, error: "", loading: false });
} catch (err) {
setRechargeDetailsState((current) => ({
...current,
error: err.message || "加载充值流水失败",
loading: false,
}));
}
}, [canViewFinance, rechargeApps, rechargeDetailQuery]);
const loadRechargeSummary = useCallback(async () => {
if (!canViewFinance) {
return;
}
setRechargeSummaryState((current) => ({ ...current, loading: true }));
try {
const { merged, perApp } = await fetchFinanceRechargeSummaries(rechargeBaseQuery, rechargeApps);
// 环比:把当前时间范围整体前移一个周期再查一次;未选时间范围时不算环比。
let prev = null;
const { endMs, startMs } = rechargeDetailFilters.timeRange;
if (startMs && endMs && endMs > startMs) {
const length = endMs - startMs;
const prevQuery = { ...rechargeBaseQuery, end_at_ms: startMs, start_at_ms: startMs - length };
prev = (await fetchFinanceRechargeSummaries(prevQuery, rechargeApps)).merged;
}
setRechargeSummaryState({ merged, loading: false, perApp, prev });
} catch {
setRechargeSummaryState((current) => ({ ...current, loading: false }));
}
}, [canViewFinance, rechargeApps, rechargeBaseQuery, rechargeDetailFilters.timeRange]);
const loadRechargeOverview = useCallback(async () => {
if (!canViewFinance) {
return;
}
setRechargeOverviewState((current) => ({ ...current, loading: true }));
try {
const data = await fetchFinanceRechargeOverview(rechargeBaseQuery, rechargeApps);
setRechargeOverviewState({ data, loading: false });
} catch {
setRechargeOverviewState((current) => ({ ...current, loading: false }));
}
}, [canViewFinance, rechargeApps, rechargeBaseQuery]);
const loadRecon = useCallback(async () => {
if (!canViewFinance || activeView !== "recon") {
return;
}
const month = reconMonthMeta(reconMonthOffset);
const query = {
...rechargeBaseQuery,
end_at_ms: month.endMs,
keyword: "",
recharge_type: "",
start_at_ms: month.startMs,
};
setReconState((current) => ({ ...current, loading: true }));
try {
const [{ merged }, overview] = await Promise.all([
fetchFinanceRechargeSummaries(query, rechargeApps),
fetchFinanceRechargeOverview(query, rechargeApps),
]);
setReconState({ loading: false, overview, summary: merged });
} catch {
setReconState((current) => ({ ...current, loading: false }));
}
}, [activeView, canViewFinance, rechargeApps, rechargeBaseQuery, reconMonthOffset]);
const rechargeAppCode = rechargeDetailFilters.appCode;
// 一次拉齐所有 App 的区域目录:区域筛选下拉用当前 App 的,流水区域列在“全部 App”下要跨 App 显示名称。
const loadRechargeRegions = useCallback(async () => {
if (!canViewFinance || !rechargeApps.length) {
return;
}
setRechargeRegionsState((current) => ({ ...current, loading: true }));
try {
const entries = await Promise.all(
rechargeApps.map(async (app) => {
try {
return [app.appCode, await listFinanceRechargeRegions(app.appCode)];
} catch {
return [app.appCode, []];
}
}),
);
setRechargeRegionsState({ byApp: Object.fromEntries(entries), loading: false });
} catch {
setRechargeRegionsState((current) => ({ ...current, loading: false }));
}
}, [canViewFinance, rechargeApps]);
const refreshGooglePaid = useCallback(
async (transactionIds) => {
if (!rechargeAppCode || !transactionIds?.length || googlePaidRefreshing) {
return;
}
setGooglePaidRefreshing(true);
try {
const results = await refreshFinanceGoogleRechargePaid(rechargeAppCode, transactionIds);
const failed = results.filter((item) => item.error).length;
const succeeded = results.length - failed;
showToast(
failed
? `谷歌实付同步完成:成功 ${succeeded} 笔,失败 ${failed} 笔`
: `谷歌实付同步完成:${succeeded} 笔`,
failed ? "warning" : "success",
);
await Promise.all([loadRechargeDetails(), loadRechargeOverview()]);
} catch (err) {
showToast(err.message || "查询谷歌实付失败", "error");
} finally {
setGooglePaidRefreshing(false);
}
},
[googlePaidRefreshing, loadRechargeDetails, loadRechargeOverview, rechargeAppCode, showToast],
);
const exportRechargeBills = useCallback(async () => {
if (!rechargeAppCode || rechargeExporting) {
return;
}
setRechargeExporting(true);
try {
const timestamp = new Date()
.toLocaleString("sv-SE", { timeZone: "Asia/Shanghai" })
.replaceAll(/[: ]/g, "-");
await downloadCsv(
() => exportFinanceRechargeBills(rechargeAppCode, rechargeDetailQuery),
`充值明细-${rechargeAppCode}-${timestamp}.csv`,
);
showToast("充值明细已导出", "success");
} catch (err) {
showToast(err.message || "导出充值明细失败", "error");
} finally {
setRechargeExporting(false);
}
}, [rechargeAppCode, rechargeDetailQuery, rechargeExporting, showToast]);
const loadWithdrawals = useCallback(async () => {
if (!session?.canViewWithdrawalApplications) {
return;
}
setWithdrawalsState((current) => ({ ...current, error: "", loading: true }));
try {
const data = await listFinanceWithdrawalApplications(withdrawalQuery);
setWithdrawalsState({ data, error: "", loading: false });
} catch (err) {
setWithdrawalsState((current) => ({
...current,
error: err.message || "加载提现申请列表失败",
loading: false,
}));
}
}, [session?.canViewWithdrawalApplications, withdrawalQuery]);
const loadOperationsWithdrawals = useCallback(async () => {
if (!session?.canViewOperationsWithdrawalApplications) {
return;
}
setOperationsWithdrawalsState((current) => ({ ...current, error: "", loading: true }));
try {
const data = await listOperationsWithdrawalApplications(operationsWithdrawalQuery);
setOperationsWithdrawalsState({ data, error: "", loading: false });
} catch (err) {
setOperationsWithdrawalsState((current) => ({
...current,
error: err.message || "加载用户提现运营审核列表失败",
loading: false,
}));
}
}, [operationsWithdrawalQuery, session?.canViewOperationsWithdrawalApplications]);
const loadCoinSellerRechargeOrders = useCallback(async () => {
if (!session?.canViewCoinSellerRechargeOrders) {
return;
}
setCoinSellerRechargeOrdersState((current) => ({ ...current, error: "", loading: true }));
try {
const data = await listFinanceCoinSellerRechargeOrders(coinSellerRechargeOrderQuery);
setCoinSellerRechargeOrdersState({ data, error: "", loading: false });
} catch (err) {
setCoinSellerRechargeOrdersState((current) => ({
...current,
error: err.message || "加载币商充值订单失败",
loading: false,
}));
}
}, [coinSellerRechargeOrderQuery, session?.canViewCoinSellerRechargeOrders]);
useEffect(() => {
loadOptions();
}, [loadOptions]);
useEffect(() => {
loadWithdrawalOptions();
}, [loadWithdrawalOptions]);
useEffect(() => {
loadRechargeApps();
}, [loadRechargeApps]);
useEffect(() => {
loadRechargeDetails();
}, [loadRechargeDetails]);
useEffect(() => {
loadRechargeSummary();
}, [loadRechargeSummary]);
useEffect(() => {
loadRechargeOverview();
}, [loadRechargeOverview]);
useEffect(() => {
loadRecon();
}, [loadRecon]);
useEffect(() => {
loadRechargeRegions();
}, [loadRechargeRegions]);
useEffect(() => {
loadWithdrawals();
}, [loadWithdrawals]);
useEffect(() => {
loadOperationsWithdrawals();
}, [loadOperationsWithdrawals]);
useEffect(() => {
loadCoinSellerRechargeOrders();
}, [loadCoinSellerRechargeOrders]);
const submitWithdrawalAudit = async ({ auditImageUrl, decision, remark }) => {
const reviewStage = withdrawalAuditTarget?.reviewStage === "operations" ? "operations" : "finance";
const canAudit =
reviewStage === "operations"
? session?.canAuditOperationsWithdrawalApplications
: session?.canAuditWithdrawalApplications;
const isOperationsRejectRetry =
reviewStage === "operations" && withdrawalAuditTarget?.operationsStatus === "rejecting";
// rejecting 已锁定为拒绝方向,前端门禁不得再把该记录提交到通过接口。
if (!withdrawalAuditTarget?.id || !canAudit || (isOperationsRejectRetry && decision !== "rejected")) {
return;
}
setWithdrawalAuditLoading(true);
try {
const submitter =
reviewStage === "operations"
? decision === "approved"
? approveOperationsWithdrawalApplication
: rejectOperationsWithdrawalApplication
: decision === "approved"
? approveFinanceWithdrawalApplication
: rejectFinanceWithdrawalApplication;
const payload =
reviewStage === "operations"
? cleanPayload({ auditRemark: remark })
: cleanPayload({ auditImageUrl, auditRemark: remark });
await submitter(withdrawalAuditTarget.id, withdrawalAuditTarget.appCode, payload);
const stageLabel = reviewStage === "operations" ? "运营审核" : "财务审核";
showToast(`${stageLabel}${decision === "approved" ? "已通过" : "已拒绝"}`, "success");
setWithdrawalAuditTarget(null);
// 平台管理员可能同时打开两个队列;任一阶段落库后刷新两边,运营通过即可立即出现在财务队列。
await Promise.all([loadOperationsWithdrawals(), loadWithdrawals()]);
} catch (err) {
showToast(err.message || "审核用户提现失败", "error");
} finally {
setWithdrawalAuditLoading(false);
}
};
const submitCoinSellerRechargeOrder = async (payload) => {
if (!session?.canCreateCoinSellerRechargeOrder) {
showToast("无创建币商充值订单权限", "error");
return null;
}
setCoinSellerRechargeOrderActionLoading("create");
try {
const order = await createFinanceCoinSellerRechargeOrder(cleanPayload(payload));
showToast("币商充值订单已创建", "success");
await loadCoinSellerRechargeOrders();
return order;
} catch (err) {
showToast(err.message || "创建币商充值订单失败", "error");
return null;
} finally {
setCoinSellerRechargeOrderActionLoading("");
}
};
const quoteCoinSellerRechargeOrder = useCallback(
(payload) => quoteFinanceCoinSellerRechargeOrder(cleanPayload(payload)),
[],
);
const loadCoinSellerRechargeExchangeRate = useCallback(
(appCode) => getFinanceCoinSellerRechargeExchangeRate(appCode),
[],
);
const loadUSDTAddresses = useCallback((appCode) => getFinanceUSDTAddresses(appCode), []);
const saveUSDTAddress = useCallback(
async (appCode, chain, payload) => {
if (!session?.canUpdateUSDTAddress) {
throw new Error("无修改 USDT 收款地址权限");
}
const saved = await upsertFinanceUSDTAddress(appCode, chain, cleanPayload(payload));
showToast(`${chain} 收款地址已保存`, "success");
return saved;
},
[session?.canUpdateUSDTAddress, showToast],
);
const saveCoinSellerRechargeExchangeRate = useCallback(
async (appCode, payload) => {
const saved = await replaceFinanceCoinSellerRechargeExchangeRate(appCode, cleanPayload(payload));
showToast("金币汇率配置已保存", "success");
return saved;
},
[showToast],
);
const verifyCoinSellerRechargeReceipt = async (payload) => {
if (!session?.canVerifyCoinSellerRechargeOrder) {
showToast("无校验币商充值订单权限", "error");
return null;
}
try {
const verification = await verifyFinanceCoinSellerRechargeReceipt(cleanPayload(payload));
showToast(verification?.verified ? "订单号校验通过" : verification?.failureReason || "订单号校验未通过", verification?.verified ? "success" : "error");
return verification;
} catch (err) {
showToast(err.message || "校验订单号失败", "error");
return null;
}
};
const verifyCoinSellerRechargeOrder = async (order) => {
if (!order?.id || !session?.canVerifyCoinSellerRechargeOrder) {
return;
}
setCoinSellerRechargeOrderActionLoading(`verify:${order.id}`);
try {
await verifyFinanceCoinSellerRechargeOrder(order.id);
showToast("订单校验已完成", "success");
await loadCoinSellerRechargeOrders();
} catch (err) {
showToast(err.message || "校验币商充值订单失败", "error");
} finally {
setCoinSellerRechargeOrderActionLoading("");
}
};
const grantCoinSellerRechargeOrder = async (order) => {
if (!order?.id || !session?.canGrantCoinSellerRechargeOrder) {
return;
}
setCoinSellerRechargeOrderActionLoading(`grant:${order.id}`);
try {
await grantFinanceCoinSellerRechargeOrder(order.id);
showToast("币商充值已发放", "success");
await loadCoinSellerRechargeOrders();
} catch (err) {
showToast(err.message || "发放币商充值失败", "error");
} finally {
setCoinSellerRechargeOrderActionLoading("");
}
};
const updateRechargeDetailFilters = (nextFilters) => {
setRechargeDetailFilters((current) => ({ ...current, ...nextFilters, page: nextFilters.page || 1 }));
};
const updateWithdrawalFilters = (nextFilters) => {
setWithdrawalFilters((current) => ({ ...current, ...nextFilters, page: nextFilters.page || 1 }));
};
const updateOperationsWithdrawalFilters = (nextFilters) => {
setOperationsWithdrawalFilters((current) => ({ ...current, ...nextFilters, page: nextFilters.page || 1 }));
};
const changeActiveView = (nextView) => {
// URL 只能进入当前会话有权访问的视图;无权深链按初始化回退处理,菜单点击则直接忽略越权目标。
if (!canAccessFinanceView(session, nextView)) {
return;
}
setActiveView(nextView);
syncFinanceView(nextView);
};
const updateCoinSellerRechargeOrderFilters = (nextFilters) => {
setCoinSellerRechargeOrderFilters((current) => ({ ...current, ...nextFilters, page: nextFilters.page || 1 }));
};
if (sessionState.loading) {
return