财务模块
This commit is contained in:
parent
7025ae9e30
commit
88eae83dbe
@ -2825,6 +2825,103 @@
|
||||
"x-permissions": ["payment-bill:view"]
|
||||
}
|
||||
},
|
||||
"/admin/payment/recharge-bills/summary": {
|
||||
"get": {
|
||||
"operationId": "getRechargeBillSummary",
|
||||
"parameters": [
|
||||
{
|
||||
"in": "query",
|
||||
"name": "keyword",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"in": "query",
|
||||
"name": "recharge_type",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"in": "query",
|
||||
"name": "status",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"in": "query",
|
||||
"name": "region_id",
|
||||
"schema": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
{
|
||||
"in": "query",
|
||||
"name": "start_at_ms",
|
||||
"schema": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
{
|
||||
"in": "query",
|
||||
"name": "end_at_ms",
|
||||
"schema": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"$ref": "#/components/responses/EmptyResponse"
|
||||
}
|
||||
},
|
||||
"x-permission": "payment-bill:view",
|
||||
"x-permissions": ["payment-bill:view"]
|
||||
}
|
||||
},
|
||||
"/admin/payment/recharge-bills/google-paid/refresh": {
|
||||
"post": {
|
||||
"operationId": "refreshGoogleRechargePaid",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"transactionIds": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" }
|
||||
}
|
||||
},
|
||||
"required": ["transactionIds"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"$ref": "#/components/responses/EmptyResponse"
|
||||
}
|
||||
},
|
||||
"x-permission": "payment-bill:view",
|
||||
"x-permissions": ["payment-bill:view"]
|
||||
}
|
||||
},
|
||||
"/admin/payment/recharge-regions": {
|
||||
"get": {
|
||||
"operationId": "listRechargeRegions",
|
||||
"responses": {
|
||||
"200": {
|
||||
"$ref": "#/components/responses/EmptyResponse"
|
||||
}
|
||||
},
|
||||
"x-permission": "payment-bill:view",
|
||||
"x-permissions": ["payment-bill:view"]
|
||||
}
|
||||
},
|
||||
"/admin/payment/recharge-products": {
|
||||
"get": {
|
||||
"operationId": "listRechargeProducts",
|
||||
|
||||
@ -7,12 +7,15 @@ import {
|
||||
approveFinanceWithdrawalApplication,
|
||||
createFinanceApplication,
|
||||
fetchFinanceApplicationOptions,
|
||||
fetchFinanceRechargeSummary,
|
||||
fetchFinanceSession,
|
||||
filterOperationsByPermissions,
|
||||
listFinanceApplications,
|
||||
listFinanceRechargeDetails,
|
||||
listFinanceRechargeRegions,
|
||||
listMyFinanceApplications,
|
||||
listFinanceWithdrawalApplications,
|
||||
refreshFinanceGoogleRechargePaid,
|
||||
rejectFinanceApplication,
|
||||
rejectFinanceWithdrawalApplication
|
||||
} from "./api.js";
|
||||
@ -24,7 +27,7 @@ import { FinanceRechargeDetailList } from "./components/FinanceRechargeDetailLis
|
||||
import { FinanceShell } from "./components/FinanceShell.jsx";
|
||||
import { FinanceWithdrawalApplicationList } from "./components/FinanceWithdrawalApplicationList.jsx";
|
||||
import { DEFAULT_APPLICATION_FORM } from "./constants.js";
|
||||
import { buildApplicationPayload, validateApplicationPayload } from "./format.js";
|
||||
import { EMPTY_RECHARGE_SUMMARY, buildApplicationPayload, validateApplicationPayload } from "./format.js";
|
||||
import { useToast } from "@/shared/ui/ToastProvider.jsx";
|
||||
|
||||
const emptyOptions = { apps: [], operations: [], walletIdentities: [] };
|
||||
@ -35,11 +38,14 @@ export function FinanceApp() {
|
||||
const [sessionState, setSessionState] = useState({ error: "", loading: true, session: null });
|
||||
const [optionsState, setOptionsState] = useState({ data: emptyOptions, error: "", loading: false });
|
||||
const [rechargeDetailsState, setRechargeDetailsState] = useState({ data: { items: [], page: 1, pageSize, total: 0 }, error: "", loading: false });
|
||||
const [rechargeSummaryState, setRechargeSummaryState] = useState({ data: EMPTY_RECHARGE_SUMMARY, error: "", loading: false });
|
||||
const [rechargeRegionsState, setRechargeRegionsState] = useState({ appCode: "", items: [], loading: false });
|
||||
const [googlePaidRefreshing, setGooglePaidRefreshing] = useState(false);
|
||||
const [myApplicationsState, setMyApplicationsState] = useState({ data: { items: [], page: 1, pageSize, total: 0 }, error: "", loading: false });
|
||||
const [applicationsState, setApplicationsState] = useState({ data: { items: [], page: 1, pageSize, total: 0 }, error: "", loading: false });
|
||||
const [withdrawalsState, setWithdrawalsState] = useState({ data: { items: [], page: 1, pageSize, total: 0 }, error: "", loading: false });
|
||||
const [activeView, setActiveView] = useState("create");
|
||||
const [rechargeDetailFilters, setRechargeDetailFilters] = useState({ appCode: "", keyword: "", page: 1, timeRange: { endMs: "", startMs: "" } });
|
||||
const [rechargeDetailFilters, setRechargeDetailFilters] = useState({ appCode: "", keyword: "", page: 1, regionId: "", timeRange: { endMs: "", startMs: "" } });
|
||||
const [myFilters, setMyFilters] = useState({ appCode: "", keyword: "", operation: "", page: 1, status: "" });
|
||||
const [filters, setFilters] = useState({ appCode: "", keyword: "", operation: "", page: 1, status: "pending" });
|
||||
const [withdrawalFilters, setWithdrawalFilters] = useState({ appCode: "", keyword: "", page: 1 });
|
||||
@ -88,6 +94,7 @@ export function FinanceApp() {
|
||||
keyword: rechargeDetailFilters.keyword,
|
||||
page: rechargeDetailFilters.page,
|
||||
page_size: pageSize,
|
||||
region_id: rechargeDetailFilters.appCode ? rechargeDetailFilters.regionId : "",
|
||||
start_at_ms: rechargeDetailFilters.timeRange.startMs,
|
||||
stat_tz: "Asia/Shanghai",
|
||||
status: "succeeded"
|
||||
@ -165,6 +172,58 @@ export function FinanceApp() {
|
||||
}
|
||||
}, [optionsState.data.apps, rechargeDetailQuery, session?.canViewAppRechargeDetails]);
|
||||
|
||||
const loadRechargeSummary = useCallback(async () => {
|
||||
if (!session?.canViewAppRechargeDetails) {
|
||||
return;
|
||||
}
|
||||
setRechargeSummaryState((current) => ({ ...current, error: "", loading: true }));
|
||||
try {
|
||||
const data = await fetchFinanceRechargeSummary(rechargeDetailQuery, optionsState.data.apps);
|
||||
setRechargeSummaryState({ data, error: "", loading: false });
|
||||
} catch (err) {
|
||||
setRechargeSummaryState((current) => ({ ...current, error: err.message || "加载充值汇总失败", loading: false }));
|
||||
}
|
||||
}, [optionsState.data.apps, rechargeDetailQuery, session?.canViewAppRechargeDetails]);
|
||||
|
||||
const rechargeAppCode = rechargeDetailFilters.appCode;
|
||||
const loadRechargeRegions = useCallback(async () => {
|
||||
if (!session?.canViewAppRechargeDetails || !rechargeAppCode) {
|
||||
setRechargeRegionsState({ appCode: "", items: [], loading: false });
|
||||
return;
|
||||
}
|
||||
setRechargeRegionsState((current) => ({ ...current, appCode: rechargeAppCode, loading: true }));
|
||||
try {
|
||||
const items = await listFinanceRechargeRegions(rechargeAppCode);
|
||||
setRechargeRegionsState({ appCode: rechargeAppCode, items, loading: false });
|
||||
} catch {
|
||||
setRechargeRegionsState({ appCode: rechargeAppCode, items: [], loading: false });
|
||||
}
|
||||
}, [rechargeAppCode, session?.canViewAppRechargeDetails]);
|
||||
|
||||
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 loadRechargeDetails();
|
||||
} catch (err) {
|
||||
showToast(err.message || "查询谷歌实付失败", "error");
|
||||
} finally {
|
||||
setGooglePaidRefreshing(false);
|
||||
}
|
||||
},
|
||||
[googlePaidRefreshing, loadRechargeDetails, rechargeAppCode, showToast]
|
||||
);
|
||||
|
||||
const loadApplications = useCallback(async () => {
|
||||
if (!session?.canAuditApplication) {
|
||||
return;
|
||||
@ -203,6 +262,14 @@ export function FinanceApp() {
|
||||
loadRechargeDetails();
|
||||
}, [loadRechargeDetails]);
|
||||
|
||||
useEffect(() => {
|
||||
loadRechargeSummary();
|
||||
}, [loadRechargeSummary]);
|
||||
|
||||
useEffect(() => {
|
||||
loadRechargeRegions();
|
||||
}, [loadRechargeRegions]);
|
||||
|
||||
useEffect(() => {
|
||||
loadApplications();
|
||||
}, [loadApplications]);
|
||||
@ -330,6 +397,7 @@ export function FinanceApp() {
|
||||
onRefresh={() => {
|
||||
loadOptions();
|
||||
loadRechargeDetails();
|
||||
loadRechargeSummary();
|
||||
loadMyApplications();
|
||||
loadApplications();
|
||||
loadWithdrawals();
|
||||
@ -341,10 +409,18 @@ export function FinanceApp() {
|
||||
bills={rechargeDetailsState.data}
|
||||
error={rechargeDetailsState.error}
|
||||
filters={rechargeDetailFilters}
|
||||
googlePaidRefreshing={googlePaidRefreshing}
|
||||
loading={rechargeDetailsState.loading}
|
||||
options={optionsState.data}
|
||||
regions={rechargeRegionsState.items}
|
||||
summary={rechargeSummaryState.data}
|
||||
summaryLoading={rechargeSummaryState.loading}
|
||||
onFiltersChange={updateRechargeDetailFilters}
|
||||
onReload={loadRechargeDetails}
|
||||
onRefreshGooglePaid={refreshGooglePaid}
|
||||
onReload={() => {
|
||||
loadRechargeDetails();
|
||||
loadRechargeSummary();
|
||||
}}
|
||||
/>
|
||||
) : activeView === "applications" && session.canAuditApplication ? (
|
||||
<FinanceApplicationList
|
||||
|
||||
@ -2,7 +2,14 @@ import { getMe, refreshSession } from "@/features/auth/api";
|
||||
import { API_ENDPOINTS, API_OPERATIONS, apiEndpointPath } from "@/shared/api/generated/endpoints";
|
||||
import { apiRequest, getAccessToken } from "@/shared/api/request";
|
||||
import { FINANCE_OPERATIONS, FINANCE_PERMISSIONS, WALLET_IDENTITIES } from "./constants.js";
|
||||
import { normalizeApplicationPage, normalizeRechargeBillPage, normalizeWithdrawalApplicationPage } from "./format.js";
|
||||
import {
|
||||
mergeRechargeSummaries,
|
||||
normalizeApplicationPage,
|
||||
normalizeRechargeBillPage,
|
||||
normalizeRechargeRegions,
|
||||
normalizeRechargeSummary,
|
||||
normalizeWithdrawalApplicationPage
|
||||
} from "./format.js";
|
||||
|
||||
const appCodeHeader = "X-App-Code";
|
||||
|
||||
@ -96,6 +103,41 @@ export async function listFinanceRechargeDetails(query = {}, apps = []) {
|
||||
};
|
||||
}
|
||||
|
||||
export async function fetchFinanceRechargeSummary(query = {}, apps = []) {
|
||||
const selectedAppCode = stringValue(query.app_code ?? query.appCode);
|
||||
const cleanQuery = cleanRechargeBillQuery(query);
|
||||
|
||||
if (selectedAppCode) {
|
||||
return getRechargeSummaryForApp(selectedAppCode, cleanQuery);
|
||||
}
|
||||
|
||||
const appCodes = normalizeApps(apps).map((app) => app.appCode);
|
||||
if (!appCodes.length) {
|
||||
return mergeRechargeSummaries([]);
|
||||
}
|
||||
const summaries = await Promise.all(appCodes.map((appCode) => getRechargeSummaryForApp(appCode, cleanQuery)));
|
||||
return mergeRechargeSummaries(summaries);
|
||||
}
|
||||
|
||||
export async function listFinanceRechargeRegions(appCode) {
|
||||
const endpoint = API_ENDPOINTS.listRechargeRegions;
|
||||
const data = await apiRequest(apiEndpointPath(API_OPERATIONS.listRechargeRegions), {
|
||||
headers: { [appCodeHeader]: appCode },
|
||||
method: endpoint.method
|
||||
});
|
||||
return normalizeRechargeRegions(data);
|
||||
}
|
||||
|
||||
export async function refreshFinanceGoogleRechargePaid(appCode, transactionIds) {
|
||||
const endpoint = API_ENDPOINTS.refreshGoogleRechargePaid;
|
||||
const data = await apiRequest(apiEndpointPath(API_OPERATIONS.refreshGoogleRechargePaid), {
|
||||
body: { transactionIds },
|
||||
headers: { [appCodeHeader]: appCode },
|
||||
method: endpoint.method
|
||||
});
|
||||
return Array.isArray(data?.items) ? data.items : [];
|
||||
}
|
||||
|
||||
export async function createFinanceApplication(payload) {
|
||||
const endpoint = API_ENDPOINTS.createFinanceApplication;
|
||||
return apiRequest(apiEndpointPath(API_OPERATIONS.createFinanceApplication), {
|
||||
@ -152,6 +194,16 @@ async function currentSession() {
|
||||
}
|
||||
}
|
||||
|
||||
async function getRechargeSummaryForApp(appCode, query = {}) {
|
||||
const endpoint = API_ENDPOINTS.getRechargeBillSummary;
|
||||
const data = await apiRequest(apiEndpointPath(API_OPERATIONS.getRechargeBillSummary), {
|
||||
headers: { [appCodeHeader]: appCode },
|
||||
method: endpoint.method,
|
||||
query
|
||||
});
|
||||
return normalizeRechargeSummary(data);
|
||||
}
|
||||
|
||||
async function listRechargeBillsForApp(appCode, query = {}) {
|
||||
const endpoint = API_ENDPOINTS.listRechargeBills;
|
||||
const data = await apiRequest(apiEndpointPath(API_OPERATIONS.listRechargeBills), {
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import CloudSyncOutlined from "@mui/icons-material/CloudSyncOutlined";
|
||||
import RefreshOutlined from "@mui/icons-material/RefreshOutlined";
|
||||
import Button from "@mui/material/Button";
|
||||
import MenuItem from "@mui/material/MenuItem";
|
||||
@ -10,29 +11,48 @@ import TableRow from "@mui/material/TableRow";
|
||||
import TextField from "@mui/material/TextField";
|
||||
import { TimeRangeFilter } from "@/shared/ui/TimeRangeFilter.jsx";
|
||||
import {
|
||||
EMPTY_RECHARGE_SUMMARY,
|
||||
formatAmount,
|
||||
formatMicroMoney,
|
||||
formatTime,
|
||||
formatUsdMinor,
|
||||
formatPercent,
|
||||
rechargeTypeLabel,
|
||||
} from "../format.js";
|
||||
|
||||
export function FinanceRechargeDetailList({ bills, error, filters, loading, onFiltersChange, onReload, options }) {
|
||||
export function FinanceRechargeDetailList({
|
||||
bills,
|
||||
error,
|
||||
filters,
|
||||
googlePaidRefreshing,
|
||||
loading,
|
||||
onFiltersChange,
|
||||
onRefreshGooglePaid,
|
||||
onReload,
|
||||
options,
|
||||
regions,
|
||||
summary,
|
||||
summaryLoading,
|
||||
}) {
|
||||
const items = bills.items || [];
|
||||
const page = Number(bills.page || filters.page || 1);
|
||||
const pageSize = Number(bills.pageSize || 50);
|
||||
const total = Number(bills.total || 0);
|
||||
const hasNextPage = page * pageSize < total;
|
||||
const regionOptions = regions || [];
|
||||
const pendingGoogleTxIds = items
|
||||
.filter((item) => item.rechargeType === "google_play_recharge" && !item.paidSyncedAtMs)
|
||||
.map((item) => item.transactionId)
|
||||
.filter(Boolean);
|
||||
|
||||
return (
|
||||
<section className="finance-panel finance-list">
|
||||
<RechargeSummaryCards loading={summaryLoading} summary={summary} />
|
||||
<div className="finance-toolbar finance-toolbar--recharge-details">
|
||||
<TextField
|
||||
label="APP"
|
||||
select
|
||||
value={filters.appCode}
|
||||
onChange={(event) => onFiltersChange({ appCode: event.target.value })}
|
||||
onChange={(event) => onFiltersChange({ appCode: event.target.value, regionId: "" })}
|
||||
>
|
||||
<MenuItem value="">全部 APP</MenuItem>
|
||||
{options.apps.map((app) => (
|
||||
@ -41,6 +61,21 @@ export function FinanceRechargeDetailList({ bills, error, filters, loading, onFi
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
<TextField
|
||||
disabled={!filters.appCode}
|
||||
helperText={filters.appCode ? "" : "先选择 APP"}
|
||||
label="区域"
|
||||
select
|
||||
value={filters.regionId}
|
||||
onChange={(event) => onFiltersChange({ regionId: event.target.value })}
|
||||
>
|
||||
<MenuItem value="">全部区域</MenuItem>
|
||||
{regionOptions.map((region) => (
|
||||
<MenuItem key={region.regionId} value={String(region.regionId)}>
|
||||
{region.name || region.regionCode || region.regionId}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
<TimeRangeFilter
|
||||
label="充值时间 · 中国时区"
|
||||
value={filters.timeRange}
|
||||
@ -51,6 +86,15 @@ export function FinanceRechargeDetailList({ bills, error, filters, loading, onFi
|
||||
value={filters.keyword}
|
||||
onChange={(event) => onFiltersChange({ keyword: event.target.value })}
|
||||
/>
|
||||
<Button
|
||||
disabled={googlePaidRefreshing || !filters.appCode || !pendingGoogleTxIds.length}
|
||||
startIcon={<CloudSyncOutlined fontSize="small" />}
|
||||
title={filters.appCode ? "查询本页谷歌订单的用户实付币种和金额" : "先选择 APP 后可查询"}
|
||||
variant="outlined"
|
||||
onClick={() => onRefreshGooglePaid(pendingGoogleTxIds)}
|
||||
>
|
||||
{googlePaidRefreshing ? "查询中…" : "查询谷歌实付"}
|
||||
</Button>
|
||||
<Button startIcon={<RefreshOutlined fontSize="small" />} variant="outlined" onClick={onReload}>
|
||||
刷新列表
|
||||
</Button>
|
||||
@ -131,6 +175,32 @@ export function FinanceRechargeDetailList({ bills, error, filters, loading, onFi
|
||||
);
|
||||
}
|
||||
|
||||
function RechargeSummaryCards({ loading, summary }) {
|
||||
const data = summary || EMPTY_RECHARGE_SUMMARY;
|
||||
const cards = [
|
||||
{ bucket: data.total, key: "total", label: "充值总和" },
|
||||
{ bucket: data.thirdParty, key: "thirdParty", label: "三方充值" },
|
||||
{ bucket: data.googlePlay, key: "googlePlay", label: "谷歌充值" },
|
||||
];
|
||||
return (
|
||||
<div className="finance-summary-grid">
|
||||
{cards.map(({ bucket, key, label }) => (
|
||||
<div className="finance-summary-card" key={key}>
|
||||
<span className="finance-summary-card__label">{label}</span>
|
||||
<strong className="finance-summary-card__value">
|
||||
{loading ? "…" : formatUsdMinor(bucket.usdMinorAmount, "USD")}
|
||||
</strong>
|
||||
<small>
|
||||
{loading
|
||||
? "统计中"
|
||||
: `${formatAmount(bucket.billCount)} 笔 · ${formatAmount(bucket.coinAmount)} 金币`}
|
||||
</small>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Stacked({ primary, secondary }) {
|
||||
return (
|
||||
<span className="finance-stack">
|
||||
@ -176,27 +246,58 @@ function userPaidText(item) {
|
||||
if (item.userPaidText) {
|
||||
return item.userPaidText;
|
||||
}
|
||||
if (hasNumber(item.userPaidAmountMinor)) {
|
||||
return formatUsdMinor(item.userPaidAmountMinor, item.currencyCode || "USD");
|
||||
if (hasPositiveNumber(item.userPaidAmountMicro) && item.userPaidCurrencyCode) {
|
||||
return formatMicroMoney(item.userPaidAmountMicro, item.userPaidCurrencyCode);
|
||||
}
|
||||
if (hasNumber(item.providerAmountMinor)) {
|
||||
return formatUsdMinor(item.providerAmountMinor, item.currencyCode || "USD");
|
||||
if (isGoogleBill(item) && !item.paidSyncedAtMs) {
|
||||
return "未同步(点“查询谷歌实付”)";
|
||||
}
|
||||
if (hasNumber(item.userPaidAmountMicro)) {
|
||||
return formatMicroMoney(item.userPaidAmountMicro, item.currencyCode || "USD");
|
||||
if (hasPositiveNumber(item.userPaidAmountMinor)) {
|
||||
return formatUsdMinor(item.userPaidAmountMinor, item.userPaidCurrencyCode || item.currencyCode || "USD");
|
||||
}
|
||||
return "-";
|
||||
}
|
||||
|
||||
function taxDeductionText(item) {
|
||||
const rate = formatPercent(item.taxRate);
|
||||
const amount =
|
||||
item.taxDeductionText ||
|
||||
(hasNumber(item.taxDeductionMinor) ? formatUsdMinor(item.taxDeductionMinor, item.currencyCode || "USD") : "");
|
||||
if (amount && rate) {
|
||||
return `${amount} / ${rate}`;
|
||||
const currency = item.userPaidCurrencyCode || item.currencyCode || "USD";
|
||||
const fee = hasPositiveNumber(item.providerFeeMicro) ? item.providerFeeMicro : 0;
|
||||
const tax = hasPositiveNumber(item.providerTaxMicro) ? item.providerTaxMicro : 0;
|
||||
const deduction = fee + tax;
|
||||
if (deduction > 0) {
|
||||
const parts = [formatMicroMoney(deduction, currency)];
|
||||
if (hasPositiveNumber(item.userPaidAmountMicro)) {
|
||||
const percent = (deduction / item.userPaidAmountMicro) * 100;
|
||||
parts.push(`${percent.toLocaleString("zh-CN", { maximumFractionDigits: 2 })}%`);
|
||||
}
|
||||
return (
|
||||
<span className="finance-stack">
|
||||
<span>{parts.join(" / ")}</span>
|
||||
<small>{deductionBreakdown(fee, tax, currency)}</small>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
return amount || rate || "-";
|
||||
if (item.taxDeductionText) {
|
||||
return item.taxDeductionText;
|
||||
}
|
||||
if (isGoogleBill(item) && !item.paidSyncedAtMs) {
|
||||
return "未同步";
|
||||
}
|
||||
return "-";
|
||||
}
|
||||
|
||||
function deductionBreakdown(fee, tax, currency) {
|
||||
const parts = [];
|
||||
if (fee > 0) {
|
||||
parts.push(`手续费 ${formatMicroMoney(fee, currency)}`);
|
||||
}
|
||||
if (tax > 0) {
|
||||
parts.push(`税费 ${formatMicroMoney(tax, currency)}`);
|
||||
}
|
||||
return parts.join(" + ");
|
||||
}
|
||||
|
||||
function isGoogleBill(item) {
|
||||
return item.rechargeType === "google_play_recharge" || item.providerCode === "google_play";
|
||||
}
|
||||
|
||||
function hasNumber(value) {
|
||||
@ -205,3 +306,7 @@ function hasNumber(value) {
|
||||
}
|
||||
return Number.isFinite(Number(value));
|
||||
}
|
||||
|
||||
function hasPositiveNumber(value) {
|
||||
return hasNumber(value) && Number(value) > 0;
|
||||
}
|
||||
|
||||
@ -138,7 +138,12 @@ export function normalizeRechargeBill(item = {}) {
|
||||
exchangeUsdMinorAmount: numberValue(item.exchangeUsdMinorAmount ?? item.exchange_usd_minor_amount),
|
||||
externalRef: stringValue(item.externalRef ?? item.external_ref),
|
||||
id: stringValue(item.id ?? item.transactionId ?? item.transaction_id),
|
||||
paidSyncedAtMs: numberOrNull(item.paidSyncedAtMs ?? item.paid_synced_at_ms),
|
||||
providerAmountMinor: numberOrNull(item.providerAmountMinor ?? item.provider_amount_minor),
|
||||
providerCode: stringValue(item.providerCode ?? item.provider_code),
|
||||
providerFeeMicro: numberOrNull(item.providerFeeMicro ?? item.provider_fee_micro),
|
||||
providerNetMicro: numberOrNull(item.providerNetMicro ?? item.provider_net_micro),
|
||||
providerTaxMicro: numberOrNull(item.providerTaxMicro ?? item.provider_tax_micro),
|
||||
rechargeType: stringValue(item.rechargeType ?? item.recharge_type),
|
||||
status: stringValue(item.status),
|
||||
taxDeductionMinor: numberOrNull(
|
||||
@ -167,6 +172,7 @@ export function normalizeRechargeBill(item = {}) {
|
||||
item.providerAmountMinor ??
|
||||
item.provider_amount_minor,
|
||||
),
|
||||
userPaidCurrencyCode: stringValue(item.userPaidCurrencyCode ?? item.user_paid_currency_code),
|
||||
userPaidText: stringValue(
|
||||
item.userPaidText ??
|
||||
item.user_paid_text ??
|
||||
@ -188,6 +194,61 @@ export function normalizeRechargeBillPage(data = {}) {
|
||||
};
|
||||
}
|
||||
|
||||
const EMPTY_RECHARGE_SUMMARY_BUCKET = { billCount: 0, coinAmount: 0, usdMinorAmount: 0 };
|
||||
|
||||
export const EMPTY_RECHARGE_SUMMARY = {
|
||||
googlePlay: EMPTY_RECHARGE_SUMMARY_BUCKET,
|
||||
thirdParty: EMPTY_RECHARGE_SUMMARY_BUCKET,
|
||||
total: EMPTY_RECHARGE_SUMMARY_BUCKET,
|
||||
};
|
||||
|
||||
function normalizeRechargeSummaryBucket(bucket = {}) {
|
||||
return {
|
||||
billCount: numberValue(bucket.billCount ?? bucket.bill_count) || 0,
|
||||
coinAmount: numberValue(bucket.coinAmount ?? bucket.coin_amount) || 0,
|
||||
usdMinorAmount: numberValue(bucket.usdMinorAmount ?? bucket.usd_minor_amount) || 0,
|
||||
};
|
||||
}
|
||||
|
||||
export function normalizeRechargeSummary(data = {}) {
|
||||
return {
|
||||
googlePlay: normalizeRechargeSummaryBucket(data.googlePlay ?? data.google_play),
|
||||
thirdParty: normalizeRechargeSummaryBucket(data.thirdParty ?? data.third_party),
|
||||
total: normalizeRechargeSummaryBucket(data.total),
|
||||
};
|
||||
}
|
||||
|
||||
export function mergeRechargeSummaries(summaries = []) {
|
||||
const merged = normalizeRechargeSummary({});
|
||||
for (const summary of summaries) {
|
||||
for (const key of ["googlePlay", "thirdParty", "total"]) {
|
||||
merged[key] = {
|
||||
billCount: merged[key].billCount + summary[key].billCount,
|
||||
coinAmount: merged[key].coinAmount + summary[key].coinAmount,
|
||||
usdMinorAmount: merged[key].usdMinorAmount + summary[key].usdMinorAmount,
|
||||
};
|
||||
}
|
||||
}
|
||||
return merged;
|
||||
}
|
||||
|
||||
export function normalizeRechargeRegions(data = {}) {
|
||||
const items = Array.isArray(data.items) ? data.items : Array.isArray(data) ? data : [];
|
||||
return items
|
||||
.map((item) => {
|
||||
const regionId = numberOrNull(item.regionId ?? item.region_id);
|
||||
if (!regionId) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
name: stringValue(item.name),
|
||||
regionCode: stringValue(item.regionCode ?? item.region_code),
|
||||
regionId,
|
||||
};
|
||||
})
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
export function operationLabel(value, operations = FINANCE_OPERATIONS) {
|
||||
return operations.find((item) => item.value === value)?.label || value || "-";
|
||||
}
|
||||
@ -199,6 +260,15 @@ export function rechargeTypeLabel(value) {
|
||||
if (value === "google_play_recharge") {
|
||||
return "谷歌充值";
|
||||
}
|
||||
if (value === "mifapay") {
|
||||
return "三方充值 · MiFaPay";
|
||||
}
|
||||
if (value === "v5pay") {
|
||||
return "三方充值 · V5Pay";
|
||||
}
|
||||
if (value === "usdt_trc20") {
|
||||
return "三方充值 · USDT";
|
||||
}
|
||||
return value || "-";
|
||||
}
|
||||
|
||||
|
||||
@ -173,7 +173,36 @@ a {
|
||||
}
|
||||
|
||||
.finance-toolbar--recharge-details {
|
||||
grid-template-columns: minmax(170px, 1fr) minmax(280px, 1.35fr) minmax(180px, 1fr) auto;
|
||||
grid-template-columns: minmax(150px, 1fr) minmax(130px, 0.85fr) minmax(250px, 1.3fr) minmax(160px, 1fr) auto auto;
|
||||
}
|
||||
|
||||
.finance-summary-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(200px, 1fr));
|
||||
gap: var(--space-4);
|
||||
}
|
||||
|
||||
.finance-summary-card {
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-control);
|
||||
background: var(--bg-card-strong);
|
||||
padding: var(--space-4);
|
||||
}
|
||||
|
||||
.finance-summary-card__label {
|
||||
color: var(--text-secondary);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.finance-summary-card__value {
|
||||
font-size: 22px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.finance-summary-card small {
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
|
||||
.finance-error {
|
||||
@ -411,7 +440,8 @@ a {
|
||||
|
||||
.finance-form-grid,
|
||||
.finance-evidence-grid,
|
||||
.finance-toolbar {
|
||||
.finance-toolbar,
|
||||
.finance-summary-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
|
||||
@ -120,6 +120,7 @@ export const API_OPERATIONS = {
|
||||
getInviteActivityRewardConfig: "getInviteActivityRewardConfig",
|
||||
getLuckyGiftConfig: "getLuckyGiftConfig",
|
||||
getLuckyGiftDrawSummary: "getLuckyGiftDrawSummary",
|
||||
getRechargeBillSummary: "getRechargeBillSummary",
|
||||
getRedPacket: "getRedPacket",
|
||||
getRedPacketConfig: "getRedPacketConfig",
|
||||
getRegion: "getRegion",
|
||||
@ -193,6 +194,7 @@ export const API_OPERATIONS = {
|
||||
listPrettyIds: "listPrettyIds",
|
||||
listRechargeBills: "listRechargeBills",
|
||||
listRechargeProducts: "listRechargeProducts",
|
||||
listRechargeRegions: "listRechargeRegions",
|
||||
listRedPackets: "listRedPackets",
|
||||
listRegionBlocks: "listRegionBlocks",
|
||||
listRegions: "listRegions",
|
||||
@ -235,6 +237,7 @@ export const API_OPERATIONS = {
|
||||
publishHostAgencyPolicy: "publishHostAgencyPolicy",
|
||||
recyclePrettyId: "recyclePrettyId",
|
||||
refresh: "refresh",
|
||||
refreshGoogleRechargePaid: "refreshGoogleRechargePaid",
|
||||
rejectFinanceApplication: "rejectFinanceApplication",
|
||||
rejectFinanceWithdrawalApplication: "rejectFinanceWithdrawalApplication",
|
||||
replaceCoinSellerSalaryRates: "replaceCoinSellerSalaryRates",
|
||||
@ -1078,6 +1081,13 @@ export const API_ENDPOINTS: Record<ApiOperationId, ApiEndpoint> = {
|
||||
permission: "lucky-gift:view",
|
||||
permissions: ["lucky-gift:view"]
|
||||
},
|
||||
getRechargeBillSummary: {
|
||||
method: "GET",
|
||||
operationId: API_OPERATIONS.getRechargeBillSummary,
|
||||
path: "/v1/admin/payment/recharge-bills/summary",
|
||||
permission: "payment-bill:view",
|
||||
permissions: ["payment-bill:view"]
|
||||
},
|
||||
getRedPacket: {
|
||||
method: "GET",
|
||||
operationId: API_OPERATIONS.getRedPacket,
|
||||
@ -1587,6 +1597,13 @@ export const API_ENDPOINTS: Record<ApiOperationId, ApiEndpoint> = {
|
||||
permission: "payment-product:view",
|
||||
permissions: ["payment-product:view"]
|
||||
},
|
||||
listRechargeRegions: {
|
||||
method: "GET",
|
||||
operationId: API_OPERATIONS.listRechargeRegions,
|
||||
path: "/v1/admin/payment/recharge-regions",
|
||||
permission: "payment-bill:view",
|
||||
permissions: ["payment-bill:view"]
|
||||
},
|
||||
listRedPackets: {
|
||||
method: "GET",
|
||||
operationId: API_OPERATIONS.listRedPackets,
|
||||
@ -1871,6 +1888,13 @@ export const API_ENDPOINTS: Record<ApiOperationId, ApiEndpoint> = {
|
||||
operationId: API_OPERATIONS.refresh,
|
||||
path: "/v1/auth/refresh"
|
||||
},
|
||||
refreshGoogleRechargePaid: {
|
||||
method: "POST",
|
||||
operationId: API_OPERATIONS.refreshGoogleRechargePaid,
|
||||
path: "/v1/admin/payment/recharge-bills/google-paid/refresh",
|
||||
permission: "payment-bill:view",
|
||||
permissions: ["payment-bill:view"]
|
||||
},
|
||||
rejectFinanceApplication: {
|
||||
method: "POST",
|
||||
operationId: API_OPERATIONS.rejectFinanceApplication,
|
||||
|
||||
97
src/shared/api/generated/schema.d.ts
vendored
97
src/shared/api/generated/schema.d.ts
vendored
@ -1876,6 +1876,54 @@ export interface paths {
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/admin/payment/recharge-bills/summary": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
get: operations["getRechargeBillSummary"];
|
||||
put?: never;
|
||||
post?: never;
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/admin/payment/recharge-bills/google-paid/refresh": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
get?: never;
|
||||
put?: never;
|
||||
post: operations["refreshGoogleRechargePaid"];
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/admin/payment/recharge-regions": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
get: operations["listRechargeRegions"];
|
||||
put?: never;
|
||||
post?: never;
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/admin/payment/recharge-products": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
@ -6735,6 +6783,55 @@ export interface operations {
|
||||
200: components["responses"]["RechargeBillPageResponse"];
|
||||
};
|
||||
};
|
||||
getRechargeBillSummary: {
|
||||
parameters: {
|
||||
query?: {
|
||||
keyword?: string;
|
||||
recharge_type?: string;
|
||||
status?: string;
|
||||
region_id?: number;
|
||||
start_at_ms?: number;
|
||||
end_at_ms?: number;
|
||||
};
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
200: components["responses"]["EmptyResponse"];
|
||||
};
|
||||
};
|
||||
refreshGoogleRechargePaid: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody: {
|
||||
content: {
|
||||
"application/json": {
|
||||
transactionIds: string[];
|
||||
};
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
200: components["responses"]["EmptyResponse"];
|
||||
};
|
||||
};
|
||||
listRechargeRegions: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
200: components["responses"]["EmptyResponse"];
|
||||
};
|
||||
};
|
||||
listRechargeProducts: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user