去掉财务
This commit is contained in:
parent
0051ca4324
commit
85e2e76878
@ -4153,181 +4153,6 @@
|
||||
"x-permissions": ["finance:view"]
|
||||
}
|
||||
},
|
||||
"/admin/finance/applications": {
|
||||
"get": {
|
||||
"operationId": "listFinanceApplications",
|
||||
"parameters": [
|
||||
{
|
||||
"in": "query",
|
||||
"name": "app_code",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"in": "query",
|
||||
"name": "operation",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"in": "query",
|
||||
"name": "status",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"in": "query",
|
||||
"name": "keyword",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"in": "query",
|
||||
"name": "page",
|
||||
"schema": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
{
|
||||
"in": "query",
|
||||
"name": "page_size",
|
||||
"schema": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"$ref": "#/components/responses/FinanceApplicationPageResponse"
|
||||
}
|
||||
},
|
||||
"x-permission": "finance-application:audit",
|
||||
"x-permissions": ["finance-application:audit"]
|
||||
},
|
||||
"post": {
|
||||
"operationId": "createFinanceApplication",
|
||||
"requestBody": {
|
||||
"$ref": "#/components/requestBodies/FinanceApplicationRequest"
|
||||
},
|
||||
"responses": {
|
||||
"201": {
|
||||
"$ref": "#/components/responses/FinanceApplicationResponse"
|
||||
}
|
||||
},
|
||||
"x-permission": "finance-application:create",
|
||||
"x-permissions": ["finance-application:create"]
|
||||
}
|
||||
},
|
||||
"/admin/finance/applications/mine": {
|
||||
"get": {
|
||||
"operationId": "listMyFinanceApplications",
|
||||
"parameters": [
|
||||
{
|
||||
"in": "query",
|
||||
"name": "app_code",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"in": "query",
|
||||
"name": "operation",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"in": "query",
|
||||
"name": "status",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"in": "query",
|
||||
"name": "keyword",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"in": "query",
|
||||
"name": "page",
|
||||
"schema": {
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
{
|
||||
"in": "query",
|
||||
"name": "page_size",
|
||||
"schema": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"$ref": "#/components/responses/FinanceApplicationPageResponse"
|
||||
}
|
||||
},
|
||||
"x-permission": "finance-application:create",
|
||||
"x-permissions": ["finance-application:create"]
|
||||
}
|
||||
},
|
||||
"/admin/finance/applications/{application_id}/approve": {
|
||||
"post": {
|
||||
"operationId": "approveFinanceApplication",
|
||||
"parameters": [
|
||||
{
|
||||
"in": "path",
|
||||
"name": "application_id",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"$ref": "#/components/requestBodies/FinanceApplicationAuditRequest"
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"$ref": "#/components/responses/FinanceApplicationResponse"
|
||||
}
|
||||
},
|
||||
"x-permission": "finance-application:audit",
|
||||
"x-permissions": ["finance-application:audit"]
|
||||
}
|
||||
},
|
||||
"/admin/finance/applications/{application_id}/reject": {
|
||||
"post": {
|
||||
"operationId": "rejectFinanceApplication",
|
||||
"parameters": [
|
||||
{
|
||||
"in": "path",
|
||||
"name": "application_id",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"$ref": "#/components/requestBodies/FinanceApplicationAuditRequest"
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"$ref": "#/components/responses/FinanceApplicationResponse"
|
||||
}
|
||||
},
|
||||
"x-permission": "finance-application:audit",
|
||||
"x-permissions": ["finance-application:audit"]
|
||||
}
|
||||
},
|
||||
"/admin/finance/withdrawal-applications": {
|
||||
"get": {
|
||||
"operationId": "listFinanceWithdrawalApplications",
|
||||
@ -4367,7 +4192,7 @@
|
||||
}
|
||||
},
|
||||
"x-permission": "finance-withdrawal:view",
|
||||
"x-permissions": ["finance-withdrawal:view", "finance-withdrawal:audit", "finance-application:audit"]
|
||||
"x-permissions": ["finance-withdrawal:view", "finance-withdrawal:audit"]
|
||||
}
|
||||
},
|
||||
"/admin/finance/withdrawal-applications/{application_id}/approve": {
|
||||
@ -4384,7 +4209,7 @@
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"$ref": "#/components/requestBodies/FinanceApplicationAuditRequest"
|
||||
"$ref": "#/components/requestBodies/FinanceWithdrawalAuditRequest"
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
@ -4392,7 +4217,7 @@
|
||||
}
|
||||
},
|
||||
"x-permission": "finance-withdrawal:audit",
|
||||
"x-permissions": ["finance-withdrawal:audit", "finance-application:audit"]
|
||||
"x-permissions": ["finance-withdrawal:audit"]
|
||||
}
|
||||
},
|
||||
"/admin/finance/withdrawal-applications/{application_id}/reject": {
|
||||
@ -4409,7 +4234,7 @@
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"$ref": "#/components/requestBodies/FinanceApplicationAuditRequest"
|
||||
"$ref": "#/components/requestBodies/FinanceWithdrawalAuditRequest"
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
@ -4417,7 +4242,7 @@
|
||||
}
|
||||
},
|
||||
"x-permission": "finance-withdrawal:audit",
|
||||
"x-permissions": ["finance-withdrawal:audit", "finance-application:audit"]
|
||||
"x-permissions": ["finance-withdrawal:audit"]
|
||||
}
|
||||
},
|
||||
"/admin/finance/orders/coin-seller-recharges": {
|
||||
@ -7419,15 +7244,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"FinanceApplicationRequest": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/FinanceApplicationInput"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"FinanceCoinSellerRechargeOrderRequest": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
@ -7464,7 +7280,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"FinanceApplicationAuditRequest": {
|
||||
"FinanceWithdrawalAuditRequest": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
@ -7952,26 +7768,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"FinanceApplicationPageResponse": {
|
||||
"description": "OK",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ApiResponseFinanceApplicationPage"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"FinanceApplicationResponse": {
|
||||
"description": "OK",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ApiResponseFinanceApplication"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"FinanceCoinSellerRechargeOrderPageResponse": {
|
||||
"description": "OK",
|
||||
"content": {
|
||||
@ -9395,27 +9191,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"ApiPageFinanceApplication": {
|
||||
"type": "object",
|
||||
"required": ["items", "page", "pageSize", "total"],
|
||||
"properties": {
|
||||
"items": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/FinanceApplication"
|
||||
}
|
||||
},
|
||||
"page": {
|
||||
"type": "integer"
|
||||
},
|
||||
"pageSize": {
|
||||
"type": "integer"
|
||||
},
|
||||
"total": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"ApiPageFinanceCoinSellerRechargeOrder": {
|
||||
"type": "object",
|
||||
"required": ["items", "page", "pageSize", "total"],
|
||||
@ -10004,36 +9779,6 @@
|
||||
"region_id": { "type": "integer", "format": "int64" }
|
||||
}
|
||||
},
|
||||
"ApiResponseFinanceApplication": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/Envelope"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"$ref": "#/components/schemas/FinanceApplication"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"ApiResponseFinanceApplicationPage": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/Envelope"
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"$ref": "#/components/schemas/ApiPageFinanceApplication"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"ApiResponseFinanceCoinSellerRechargeOrder": {
|
||||
"allOf": [
|
||||
{
|
||||
@ -12057,94 +11802,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"FinanceApplication": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"id",
|
||||
"appCode",
|
||||
"operation",
|
||||
"targetUserId",
|
||||
"coinAmount",
|
||||
"rechargeAmount",
|
||||
"applicantUserId",
|
||||
"status",
|
||||
"createdAtMs",
|
||||
"updatedAtMs"
|
||||
],
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "integer"
|
||||
},
|
||||
"appCode": {
|
||||
"type": "string"
|
||||
},
|
||||
"appName": {
|
||||
"type": "string"
|
||||
},
|
||||
"operation": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"user_coin_credit",
|
||||
"user_coin_debit",
|
||||
"user_wallet_debit",
|
||||
"user_wallet_credit",
|
||||
"coin_seller_coin_credit",
|
||||
"coin_seller_coin_debit"
|
||||
]
|
||||
},
|
||||
"walletIdentity": {
|
||||
"type": "string"
|
||||
},
|
||||
"targetUserId": {
|
||||
"type": "string"
|
||||
},
|
||||
"coinAmount": {
|
||||
"type": "integer"
|
||||
},
|
||||
"rechargeAmount": {
|
||||
"type": "string"
|
||||
},
|
||||
"credentialImageUrl": {
|
||||
"type": "string"
|
||||
},
|
||||
"credentialText": {
|
||||
"type": "string"
|
||||
},
|
||||
"applicantUserId": {
|
||||
"type": "integer"
|
||||
},
|
||||
"applicantName": {
|
||||
"type": "string"
|
||||
},
|
||||
"auditorUserId": {
|
||||
"nullable": true,
|
||||
"type": "integer"
|
||||
},
|
||||
"auditorName": {
|
||||
"type": "string"
|
||||
},
|
||||
"status": {
|
||||
"type": "string",
|
||||
"enum": ["pending", "approved", "rejected"]
|
||||
},
|
||||
"auditRemark": {
|
||||
"type": "string"
|
||||
},
|
||||
"auditedAtMs": {
|
||||
"nullable": true,
|
||||
"type": "integer"
|
||||
},
|
||||
"createdAtMs": {
|
||||
"type": "integer"
|
||||
},
|
||||
"updatedAtMs": {
|
||||
"type": "integer"
|
||||
},
|
||||
"dingTalkNotified": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
"FinanceCoinSellerRechargeOrder": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
@ -12347,47 +12004,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"FinanceApplicationInput": {
|
||||
"type": "object",
|
||||
"required": ["appCode", "operation", "targetUserId", "coinAmount", "rechargeAmount"],
|
||||
"properties": {
|
||||
"appCode": {
|
||||
"type": "string"
|
||||
},
|
||||
"operation": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"user_coin_credit",
|
||||
"user_coin_debit",
|
||||
"user_wallet_debit",
|
||||
"user_wallet_credit",
|
||||
"coin_seller_coin_credit",
|
||||
"coin_seller_coin_debit"
|
||||
]
|
||||
},
|
||||
"walletIdentity": {
|
||||
"type": "string",
|
||||
"enum": ["host", "agency", "bd"]
|
||||
},
|
||||
"targetUserId": {
|
||||
"type": "string"
|
||||
},
|
||||
"coinAmount": {
|
||||
"type": "number",
|
||||
"minimum": 0
|
||||
},
|
||||
"rechargeAmount": {
|
||||
"type": "number",
|
||||
"minimum": 0
|
||||
},
|
||||
"credentialImageUrl": {
|
||||
"type": "string"
|
||||
},
|
||||
"credentialText": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"FinanceCoinSellerRechargeOrderInput": {
|
||||
"type": "object",
|
||||
"required": ["appCode", "targetUserId", "usdAmount", "providerCode", "externalOrderNo"],
|
||||
|
||||
@ -1,59 +1,41 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import Dialog from "@mui/material/Dialog";
|
||||
import DialogContent from "@mui/material/DialogContent";
|
||||
import DialogTitle from "@mui/material/DialogTitle";
|
||||
import {
|
||||
approveFinanceApplication,
|
||||
approveFinanceWithdrawalApplication,
|
||||
createFinanceApplication,
|
||||
createFinanceCoinSellerRechargeOrder,
|
||||
exportFinanceRechargeBills,
|
||||
fetchFinanceApplicationOptions,
|
||||
fetchFinanceOptions,
|
||||
fetchFinanceRechargeApps,
|
||||
fetchFinanceRechargeOverview,
|
||||
fetchFinanceRechargeSummaries,
|
||||
fetchFinanceSession,
|
||||
filterOperationsByPermissions,
|
||||
grantFinanceCoinSellerRechargeOrder,
|
||||
getFinanceCoinSellerRechargeExchangeRate,
|
||||
listFinanceApplications,
|
||||
listFinanceCoinSellerRechargeOrders,
|
||||
listFinanceRechargeDetails,
|
||||
listFinanceRechargeRegions,
|
||||
listMyFinanceApplications,
|
||||
listFinanceWithdrawalApplications,
|
||||
refreshFinanceGoogleRechargePaid,
|
||||
replaceFinanceCoinSellerRechargeExchangeRate,
|
||||
rejectFinanceApplication,
|
||||
rejectFinanceWithdrawalApplication,
|
||||
quoteFinanceCoinSellerRechargeOrder,
|
||||
verifyFinanceCoinSellerRechargeReceipt,
|
||||
verifyFinanceCoinSellerRechargeOrder,
|
||||
} from "./api.js";
|
||||
import { FinanceApplicationForm } from "./components/FinanceApplicationForm.jsx";
|
||||
import { FinanceApplicationList } from "./components/FinanceApplicationList.jsx";
|
||||
import { FinanceAuditDialog } from "./components/FinanceAuditDialog.jsx";
|
||||
import { FinanceCoinSellerRechargeOrderList } from "./components/FinanceCoinSellerRechargeOrderList.jsx";
|
||||
import { FinanceMyApplicationList } from "./components/FinanceMyApplicationList.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 { DEFAULT_APPLICATION_FORM } from "./constants.js";
|
||||
import {
|
||||
EMPTY_RECHARGE_OVERVIEW,
|
||||
EMPTY_RECHARGE_SUMMARY,
|
||||
buildApplicationPayload,
|
||||
validateApplicationPayload,
|
||||
} from "./format.js";
|
||||
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: [], operations: [], walletIdentities: [] };
|
||||
const emptyOptions = { apps: [] };
|
||||
const pageSize = 50;
|
||||
const DAY_MS = 86_400_000;
|
||||
const CHINA_OFFSET_MS = 8 * 3_600_000;
|
||||
@ -123,16 +105,6 @@ export function FinanceApp() {
|
||||
overview: EMPTY_RECHARGE_OVERVIEW,
|
||||
summary: EMPTY_RECHARGE_SUMMARY,
|
||||
});
|
||||
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: "",
|
||||
@ -144,7 +116,7 @@ export function FinanceApp() {
|
||||
loading: false,
|
||||
});
|
||||
const [coinSellerRechargeOrderActionLoading, setCoinSellerRechargeOrderActionLoading] = useState("");
|
||||
const [activeView, setActiveView] = useState("create");
|
||||
const [activeView, setActiveView] = useState("overview");
|
||||
const [rechargeDetailFilters, setRechargeDetailFilters] = useState(() => ({
|
||||
appCode: "",
|
||||
currency: "usd",
|
||||
@ -156,8 +128,6 @@ export function FinanceApp() {
|
||||
timeRange: timePresets().find((preset) => preset.key === "yesterday").range,
|
||||
}));
|
||||
const [rechargeExporting, setRechargeExporting] = useState(false);
|
||||
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 });
|
||||
const [coinSellerRechargeOrderFilters, setCoinSellerRechargeOrderFilters] = useState({
|
||||
appCode: "",
|
||||
@ -168,11 +138,6 @@ export function FinanceApp() {
|
||||
status: "",
|
||||
verifyStatus: "",
|
||||
});
|
||||
const [form, setForm] = useState(DEFAULT_APPLICATION_FORM);
|
||||
const [createDialogOpen, setCreateDialogOpen] = useState(false);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [auditTarget, setAuditTarget] = useState(null);
|
||||
const [auditLoading, setAuditLoading] = useState(false);
|
||||
const [withdrawalAuditTarget, setWithdrawalAuditTarget] = useState(null);
|
||||
const [withdrawalAuditLoading, setWithdrawalAuditLoading] = useState(false);
|
||||
|
||||
@ -189,11 +154,7 @@ export function FinanceApp() {
|
||||
? "overview"
|
||||
: session.canViewCoinSellerRechargeOrders
|
||||
? "coinSellerRechargeOrders"
|
||||
: session.canAuditApplication
|
||||
? "applications"
|
||||
: session.canCreateApplication
|
||||
? "create"
|
||||
: "withdrawals",
|
||||
: "withdrawals",
|
||||
);
|
||||
})
|
||||
.catch((err) => {
|
||||
@ -233,28 +194,6 @@ export function FinanceApp() {
|
||||
}),
|
||||
[rechargeBaseQuery, rechargeDetailFilters.page, rechargeDetailFilters.paidState],
|
||||
);
|
||||
const myApplicationQuery = useMemo(
|
||||
() => ({
|
||||
app_code: myFilters.appCode,
|
||||
keyword: myFilters.keyword,
|
||||
operation: myFilters.operation,
|
||||
page: myFilters.page,
|
||||
page_size: pageSize,
|
||||
status: myFilters.status,
|
||||
}),
|
||||
[myFilters],
|
||||
);
|
||||
const applicationQuery = useMemo(
|
||||
() => ({
|
||||
app_code: filters.appCode,
|
||||
keyword: filters.keyword,
|
||||
operation: filters.operation,
|
||||
page: filters.page,
|
||||
page_size: pageSize,
|
||||
status: filters.status,
|
||||
}),
|
||||
[filters],
|
||||
);
|
||||
const withdrawalQuery = useMemo(
|
||||
() => ({
|
||||
app_code: withdrawalFilters.appCode,
|
||||
@ -284,30 +223,13 @@ export function FinanceApp() {
|
||||
}
|
||||
setOptionsState((current) => ({ ...current, error: "", loading: true }));
|
||||
try {
|
||||
const data = await fetchFinanceApplicationOptions();
|
||||
const data = await fetchFinanceOptions();
|
||||
setOptionsState({ data, error: "", loading: false });
|
||||
} catch (err) {
|
||||
setOptionsState({ data: emptyOptions, error: err.message || "加载财务选项失败", loading: false });
|
||||
}
|
||||
}, [canLoadOptions]);
|
||||
|
||||
const loadMyApplications = useCallback(async () => {
|
||||
if (!session?.canCreateApplication) {
|
||||
return;
|
||||
}
|
||||
setMyApplicationsState((current) => ({ ...current, error: "", loading: true }));
|
||||
try {
|
||||
const data = await listMyFinanceApplications(myApplicationQuery);
|
||||
setMyApplicationsState({ data, error: "", loading: false });
|
||||
} catch (err) {
|
||||
setMyApplicationsState((current) => ({
|
||||
...current,
|
||||
error: err.message || "加载我的申请列表失败",
|
||||
loading: false,
|
||||
}));
|
||||
}
|
||||
}, [myApplicationQuery, session?.canCreateApplication]);
|
||||
|
||||
// 充值详情的 App 目录来自 recharge-apps(含 Yumi/Aslan 等 legacy 外部账单源);接口未就绪时回退到财务通用 App 列表。
|
||||
const rechargeApps = useMemo(
|
||||
() =>
|
||||
@ -474,23 +396,6 @@ export function FinanceApp() {
|
||||
}
|
||||
}, [rechargeAppCode, rechargeDetailQuery, rechargeExporting, showToast]);
|
||||
|
||||
const loadApplications = useCallback(async () => {
|
||||
if (!session?.canAuditApplication) {
|
||||
return;
|
||||
}
|
||||
setApplicationsState((current) => ({ ...current, error: "", loading: true }));
|
||||
try {
|
||||
const data = await listFinanceApplications(applicationQuery);
|
||||
setApplicationsState({ data, error: "", loading: false });
|
||||
} catch (err) {
|
||||
setApplicationsState((current) => ({
|
||||
...current,
|
||||
error: err.message || "加载申请列表失败",
|
||||
loading: false,
|
||||
}));
|
||||
}
|
||||
}, [applicationQuery, session?.canAuditApplication]);
|
||||
|
||||
const loadWithdrawals = useCallback(async () => {
|
||||
if (!session?.canViewWithdrawalApplications) {
|
||||
return;
|
||||
@ -529,10 +434,6 @@ export function FinanceApp() {
|
||||
loadOptions();
|
||||
}, [loadOptions]);
|
||||
|
||||
useEffect(() => {
|
||||
loadMyApplications();
|
||||
}, [loadMyApplications]);
|
||||
|
||||
useEffect(() => {
|
||||
loadRechargeApps();
|
||||
}, [loadRechargeApps]);
|
||||
@ -557,10 +458,6 @@ export function FinanceApp() {
|
||||
loadRechargeRegions();
|
||||
}, [loadRechargeRegions]);
|
||||
|
||||
useEffect(() => {
|
||||
loadApplications();
|
||||
}, [loadApplications]);
|
||||
|
||||
useEffect(() => {
|
||||
loadWithdrawals();
|
||||
}, [loadWithdrawals]);
|
||||
@ -569,63 +466,6 @@ export function FinanceApp() {
|
||||
loadCoinSellerRechargeOrders();
|
||||
}, [loadCoinSellerRechargeOrders]);
|
||||
|
||||
const operationOptions = useMemo(
|
||||
() => filterOperationsByPermissions(optionsState.data.operations, session?.permissions || []),
|
||||
[optionsState.data.operations, session?.permissions],
|
||||
);
|
||||
|
||||
const submitApplication = async (event) => {
|
||||
event.preventDefault();
|
||||
if (!session?.canCreateApplication) {
|
||||
showToast("无发起财务申请权限", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = buildApplicationPayload(form);
|
||||
const validationMessage = validateApplicationPayload(payload);
|
||||
if (validationMessage) {
|
||||
showToast(validationMessage, "warning");
|
||||
return;
|
||||
}
|
||||
|
||||
setSubmitting(true);
|
||||
try {
|
||||
const result = await createFinanceApplication(cleanPayload(payload));
|
||||
setForm({ ...DEFAULT_APPLICATION_FORM, appCode: payload.appCode });
|
||||
setCreateDialogOpen(false);
|
||||
showToast(
|
||||
result?.dingTalkNotified === false ? "申请已提交,钉钉通知未确认" : "申请已提交并通知财务",
|
||||
"success",
|
||||
);
|
||||
await loadMyApplications();
|
||||
if (session.canAuditApplication) {
|
||||
await loadApplications();
|
||||
}
|
||||
} catch (err) {
|
||||
showToast(err.message || "发起申请失败", "error");
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const submitAudit = async ({ decision, remark }) => {
|
||||
if (!auditTarget?.id || !session?.canAuditApplication) {
|
||||
return;
|
||||
}
|
||||
setAuditLoading(true);
|
||||
try {
|
||||
const submitter = decision === "approved" ? approveFinanceApplication : rejectFinanceApplication;
|
||||
await submitter(auditTarget.id, { auditRemark: remark });
|
||||
showToast(decision === "approved" ? "申请已通过" : "申请已拒绝", "success");
|
||||
setAuditTarget(null);
|
||||
await loadApplications();
|
||||
} catch (err) {
|
||||
showToast(err.message || "审核失败", "error");
|
||||
} finally {
|
||||
setAuditLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const submitWithdrawalAudit = async ({ auditImageUrl, decision, remark }) => {
|
||||
if (!withdrawalAuditTarget?.id || !session?.canAuditWithdrawalApplications) {
|
||||
return;
|
||||
@ -730,18 +570,10 @@ export function FinanceApp() {
|
||||
}
|
||||
};
|
||||
|
||||
const updateFilters = (nextFilters) => {
|
||||
setFilters((current) => ({ ...current, ...nextFilters, page: nextFilters.page || 1 }));
|
||||
};
|
||||
|
||||
const updateRechargeDetailFilters = (nextFilters) => {
|
||||
setRechargeDetailFilters((current) => ({ ...current, ...nextFilters, page: nextFilters.page || 1 }));
|
||||
};
|
||||
|
||||
const updateMyFilters = (nextFilters) => {
|
||||
setMyFilters((current) => ({ ...current, ...nextFilters, page: nextFilters.page || 1 }));
|
||||
};
|
||||
|
||||
const updateWithdrawalFilters = (nextFilters) => {
|
||||
setWithdrawalFilters((current) => ({ ...current, ...nextFilters, page: nextFilters.page || 1 }));
|
||||
};
|
||||
@ -750,13 +582,6 @@ export function FinanceApp() {
|
||||
setCoinSellerRechargeOrderFilters((current) => ({ ...current, ...nextFilters, page: nextFilters.page || 1 }));
|
||||
};
|
||||
|
||||
const closeCreateDialog = (_, reason) => {
|
||||
if (submitting || reason === "backdropClick") {
|
||||
return;
|
||||
}
|
||||
setCreateDialogOpen(false);
|
||||
};
|
||||
|
||||
if (sessionState.loading) {
|
||||
return <StateScreen title="正在进入财务系统" />;
|
||||
}
|
||||
@ -853,16 +678,12 @@ export function FinanceApp() {
|
||||
<FinanceShell
|
||||
activeView={activeView}
|
||||
appBar={appBar}
|
||||
canAudit={session.canAuditApplication}
|
||||
canCreate={session.canCreateApplication}
|
||||
canViewCoinSellerRechargeOrders={session.canViewCoinSellerRechargeOrders}
|
||||
canViewRechargeDetails={session.canViewAppRechargeDetails}
|
||||
canViewWithdrawals={session.canViewWithdrawalApplications}
|
||||
loading={
|
||||
optionsState.loading ||
|
||||
rechargeDetailsState.loading ||
|
||||
myApplicationsState.loading ||
|
||||
applicationsState.loading ||
|
||||
withdrawalsState.loading ||
|
||||
coinSellerRechargeOrdersState.loading
|
||||
}
|
||||
@ -944,18 +765,6 @@ export function FinanceApp() {
|
||||
loadRechargeOverview();
|
||||
}}
|
||||
/>
|
||||
) : activeView === "applications" && session.canAuditApplication ? (
|
||||
<FinanceApplicationList
|
||||
applications={applicationsState.data}
|
||||
error={applicationsState.error}
|
||||
filters={filters}
|
||||
loading={applicationsState.loading}
|
||||
operations={optionsState.data.operations}
|
||||
options={optionsState.data}
|
||||
onAudit={setAuditTarget}
|
||||
onFiltersChange={updateFilters}
|
||||
onReload={loadApplications}
|
||||
/>
|
||||
) : activeView === "withdrawals" && session.canViewWithdrawalApplications ? (
|
||||
<FinanceWithdrawalApplicationList
|
||||
applications={withdrawalsState.data}
|
||||
@ -968,56 +777,11 @@ export function FinanceApp() {
|
||||
onFiltersChange={updateWithdrawalFilters}
|
||||
onReload={loadWithdrawals}
|
||||
/>
|
||||
) : (
|
||||
<FinanceMyApplicationList
|
||||
applications={myApplicationsState.data}
|
||||
error={myApplicationsState.error}
|
||||
filters={myFilters}
|
||||
loading={myApplicationsState.loading}
|
||||
operations={optionsState.data.operations}
|
||||
options={optionsState.data}
|
||||
onCreate={() => setCreateDialogOpen(true)}
|
||||
onFiltersChange={updateMyFilters}
|
||||
onReload={loadMyApplications}
|
||||
/>
|
||||
)}
|
||||
<Dialog
|
||||
fullWidth
|
||||
maxWidth="md"
|
||||
open={createDialogOpen}
|
||||
slotProps={{ paper: { className: "finance-create-dialog-paper" } }}
|
||||
onClose={closeCreateDialog}
|
||||
>
|
||||
<DialogTitle>发起申请</DialogTitle>
|
||||
<DialogContent className="finance-create-dialog">
|
||||
<FinanceApplicationForm
|
||||
apps={optionsState.data.apps}
|
||||
disabled={!session.canCreateApplication}
|
||||
error={optionsState.error}
|
||||
form={form}
|
||||
loading={optionsState.loading || submitting}
|
||||
operations={operationOptions}
|
||||
walletIdentities={optionsState.data.walletIdentities}
|
||||
onCancel={closeCreateDialog}
|
||||
onChange={setForm}
|
||||
onSubmit={submitApplication}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
<FinanceAuditDialog
|
||||
application={auditTarget}
|
||||
apps={optionsState.data.apps}
|
||||
loading={auditLoading}
|
||||
operations={optionsState.data.operations}
|
||||
open={Boolean(auditTarget)}
|
||||
onClose={() => setAuditTarget(null)}
|
||||
onSubmit={submitAudit}
|
||||
/>
|
||||
) : null}
|
||||
<FinanceAuditDialog
|
||||
application={withdrawalAuditTarget}
|
||||
apps={optionsState.data.apps}
|
||||
loading={withdrawalAuditLoading}
|
||||
operations={optionsState.data.operations}
|
||||
open={Boolean(withdrawalAuditTarget)}
|
||||
requireApprovalImage
|
||||
requireRejectRemark
|
||||
|
||||
@ -1,43 +1,35 @@
|
||||
import { fireEvent, render, screen, waitFor, within } from "@testing-library/react";
|
||||
import { render, screen, waitFor } from "@testing-library/react";
|
||||
import { afterEach, beforeEach, expect, test, vi } from "vitest";
|
||||
import { ToastProvider } from "@/shared/ui/ToastProvider.jsx";
|
||||
import { FinanceApp } from "./FinanceApp.jsx";
|
||||
import {
|
||||
fetchFinanceApplicationOptions,
|
||||
fetchFinanceOptions,
|
||||
fetchFinanceRechargeApps,
|
||||
fetchFinanceRechargeOverview,
|
||||
fetchFinanceRechargeSummaries,
|
||||
fetchFinanceSession,
|
||||
listFinanceApplications,
|
||||
listFinanceCoinSellerRechargeOrders,
|
||||
listFinanceRechargeDetails,
|
||||
listFinanceRechargeRegions,
|
||||
listFinanceWithdrawalApplications,
|
||||
listMyFinanceApplications
|
||||
} from "./api.js";
|
||||
import { EMPTY_RECHARGE_OVERVIEW, EMPTY_RECHARGE_SUMMARY } from "./format.js";
|
||||
|
||||
vi.mock("./api.js", () => ({
|
||||
approveFinanceApplication: vi.fn(),
|
||||
approveFinanceWithdrawalApplication: vi.fn(),
|
||||
createFinanceApplication: vi.fn(),
|
||||
createFinanceCoinSellerRechargeOrder: vi.fn(),
|
||||
exportFinanceRechargeBills: vi.fn(),
|
||||
fetchFinanceApplicationOptions: vi.fn(),
|
||||
fetchFinanceOptions: vi.fn(),
|
||||
fetchFinanceRechargeApps: vi.fn(),
|
||||
fetchFinanceRechargeOverview: vi.fn(),
|
||||
fetchFinanceRechargeSummaries: vi.fn(),
|
||||
fetchFinanceSession: vi.fn(),
|
||||
filterOperationsByPermissions: vi.fn((operations, permissions) => operations.filter((item) => permissions.includes(item.permissionCode))),
|
||||
grantFinanceCoinSellerRechargeOrder: vi.fn(),
|
||||
getFinanceCoinSellerRechargeExchangeRate: vi.fn(),
|
||||
listFinanceApplications: vi.fn(),
|
||||
listFinanceCoinSellerRechargeOrders: vi.fn(),
|
||||
listFinanceRechargeDetails: vi.fn(),
|
||||
listFinanceRechargeRegions: vi.fn(),
|
||||
listFinanceWithdrawalApplications: vi.fn(),
|
||||
listMyFinanceApplications: vi.fn(),
|
||||
rejectFinanceApplication: vi.fn(),
|
||||
rejectFinanceWithdrawalApplication: vi.fn(),
|
||||
refreshFinanceGoogleRechargePaid: vi.fn(),
|
||||
replaceFinanceCoinSellerRechargeExchangeRate: vi.fn(),
|
||||
@ -48,35 +40,28 @@ vi.mock("./api.js", () => ({
|
||||
|
||||
const defaultSession = {
|
||||
canAccessFinance: true,
|
||||
canAuditApplication: false,
|
||||
canAuditWithdrawalApplications: false,
|
||||
canConfigureCoinSellerRechargeExchangeRate: false,
|
||||
canCreateApplication: true,
|
||||
canCreateCoinSellerRechargeOrder: false,
|
||||
canGrantCoinSellerRechargeOrder: false,
|
||||
canVerifyCoinSellerRechargeOrder: false,
|
||||
canViewAppRechargeDetails: false,
|
||||
canViewCoinSellerRechargeOrders: false,
|
||||
canViewWithdrawalApplications: false,
|
||||
permissions: ["finance-application:create", "finance-operation:user-coin-credit"],
|
||||
canViewWithdrawalApplications: true,
|
||||
permissions: ["finance-withdrawal:view"],
|
||||
user: { account: "admin", name: "admin" }
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vi.mocked(fetchFinanceApplicationOptions).mockResolvedValue({
|
||||
apps: [{ appCode: "lalu", appName: "lalu" }],
|
||||
operations: [{ label: "增加用户金币", permissionCode: "finance-operation:user-coin-credit", value: "user_coin_credit" }],
|
||||
walletIdentities: []
|
||||
});
|
||||
vi.mocked(fetchFinanceOptions).mockResolvedValue({ apps: [{ appCode: "lalu", appName: "lalu" }] });
|
||||
vi.mocked(fetchFinanceRechargeApps).mockResolvedValue([{ appCode: "lalu", appName: "Lalu" }]);
|
||||
vi.mocked(fetchFinanceRechargeOverview).mockResolvedValue(EMPTY_RECHARGE_OVERVIEW);
|
||||
vi.mocked(fetchFinanceRechargeSummaries).mockResolvedValue({ merged: EMPTY_RECHARGE_SUMMARY, perApp: [] });
|
||||
vi.mocked(fetchFinanceSession).mockResolvedValue(defaultSession);
|
||||
vi.mocked(listFinanceApplications).mockResolvedValue({ items: [], page: 1, pageSize: 50, total: 0 });
|
||||
vi.mocked(listFinanceCoinSellerRechargeOrders).mockResolvedValue({ items: [], page: 1, pageSize: 50, total: 0 });
|
||||
vi.mocked(listFinanceRechargeDetails).mockResolvedValue({ items: [], page: 1, pageSize: 50, total: 0 });
|
||||
vi.mocked(listFinanceRechargeRegions).mockResolvedValue([]);
|
||||
vi.mocked(listFinanceWithdrawalApplications).mockResolvedValue({ items: [], page: 1, pageSize: 50, total: 0 });
|
||||
vi.mocked(listMyFinanceApplications).mockResolvedValue({ items: [], page: 1, pageSize: 50, total: 0 });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@ -105,7 +90,6 @@ test("defaults recharge finance views to yesterday in China timezone", async ()
|
||||
vi.spyOn(Date, "now").mockReturnValue(Date.UTC(2026, 6, 8, 4));
|
||||
vi.mocked(fetchFinanceSession).mockResolvedValue({
|
||||
...defaultSession,
|
||||
canCreateApplication: false,
|
||||
canViewAppRechargeDetails: true,
|
||||
permissions: ["finance-recharge-detail:view"]
|
||||
});
|
||||
@ -125,26 +109,3 @@ test("defaults recharge finance views to yesterday in China timezone", async ()
|
||||
expect(query.start_at_ms).toBe(Date.UTC(2026, 6, 6, 16));
|
||||
expect(query.end_at_ms).toBe(Date.UTC(2026, 6, 7, 16));
|
||||
});
|
||||
|
||||
test("keeps create application dialog open when backdrop is clicked", async () => {
|
||||
render(
|
||||
<ToastProvider>
|
||||
<FinanceApp />
|
||||
</ToastProvider>
|
||||
);
|
||||
|
||||
await screen.findByRole("button", { name: "添加" });
|
||||
expect(document.querySelector(".finance-header")).toBeNull();
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: "添加" }));
|
||||
expect(within(screen.getByRole("dialog")).getByRole("heading", { name: "发起申请" })).toBeInTheDocument();
|
||||
|
||||
const backdrop = document.querySelector(".MuiBackdrop-root");
|
||||
expect(backdrop).toBeTruthy();
|
||||
fireEvent.mouseDown(backdrop);
|
||||
fireEvent.click(backdrop);
|
||||
expect(screen.getByRole("dialog")).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: "取消" }));
|
||||
await waitFor(() => expect(screen.queryByRole("dialog")).not.toBeInTheDocument());
|
||||
});
|
||||
|
||||
@ -1,11 +1,10 @@
|
||||
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 { FINANCE_PERMISSIONS } from "./constants.js";
|
||||
import {
|
||||
mergeRechargeOverviews,
|
||||
mergeRechargeSummaries,
|
||||
normalizeApplicationPage,
|
||||
normalizeCoinSellerRechargeOrder,
|
||||
normalizeCoinSellerRechargeOrderPage,
|
||||
normalizeRechargeBillPage,
|
||||
@ -20,10 +19,7 @@ const appCodeHeader = "X-App-Code";
|
||||
export async function fetchFinanceSession() {
|
||||
const session = await currentSession();
|
||||
const permissions = Array.isArray(session.permissions) ? session.permissions : [];
|
||||
const canCreateApplication = permissions.includes(FINANCE_PERMISSIONS.createApplication);
|
||||
const canAuditApplication = permissions.includes(FINANCE_PERMISSIONS.auditApplication);
|
||||
const canAuditWithdrawalApplications =
|
||||
permissions.includes(FINANCE_PERMISSIONS.auditWithdrawalApplications) || permissions.includes(FINANCE_PERMISSIONS.auditApplication);
|
||||
const canAuditWithdrawalApplications = permissions.includes(FINANCE_PERMISSIONS.auditWithdrawalApplications);
|
||||
const canViewWithdrawalApplications = permissions.includes(FINANCE_PERMISSIONS.viewWithdrawalApplications) || canAuditWithdrawalApplications;
|
||||
const canViewAppRechargeDetails = permissions.includes(FINANCE_PERMISSIONS.viewAppRechargeDetails);
|
||||
const canViewCoinSellerRechargeOrders = permissions.includes(FINANCE_PERMISSIONS.viewCoinSellerRechargeOrders);
|
||||
@ -35,15 +31,11 @@ export async function fetchFinanceSession() {
|
||||
return {
|
||||
...session,
|
||||
canAccessFinance:
|
||||
canCreateApplication ||
|
||||
canAuditApplication ||
|
||||
canViewWithdrawalApplications ||
|
||||
canViewAppRechargeDetails ||
|
||||
canViewCoinSellerRechargeOrders,
|
||||
canAuditApplication,
|
||||
canAuditWithdrawalApplications,
|
||||
canConfigureCoinSellerRechargeExchangeRate,
|
||||
canCreateApplication,
|
||||
canCreateCoinSellerRechargeOrder,
|
||||
canGrantCoinSellerRechargeOrder,
|
||||
canVerifyCoinSellerRechargeOrder,
|
||||
@ -54,7 +46,7 @@ export async function fetchFinanceSession() {
|
||||
};
|
||||
}
|
||||
|
||||
export async function fetchFinanceApplicationOptions() {
|
||||
export async function fetchFinanceOptions() {
|
||||
const endpoint = API_ENDPOINTS.listApps;
|
||||
const data = await apiRequest(apiEndpointPath(API_OPERATIONS.listApps), {
|
||||
method: endpoint.method
|
||||
@ -71,24 +63,6 @@ export async function fetchFinanceRechargeApps() {
|
||||
return normalizeApps(data);
|
||||
}
|
||||
|
||||
export async function listFinanceApplications(query = {}) {
|
||||
const endpoint = API_ENDPOINTS.listFinanceApplications;
|
||||
const data = await apiRequest(apiEndpointPath(API_OPERATIONS.listFinanceApplications), {
|
||||
method: endpoint.method,
|
||||
query
|
||||
});
|
||||
return normalizeApplicationPage(data);
|
||||
}
|
||||
|
||||
export async function listMyFinanceApplications(query = {}) {
|
||||
const endpoint = API_ENDPOINTS.listMyFinanceApplications;
|
||||
const data = await apiRequest(apiEndpointPath(API_OPERATIONS.listMyFinanceApplications), {
|
||||
method: endpoint.method,
|
||||
query
|
||||
});
|
||||
return normalizeApplicationPage(data);
|
||||
}
|
||||
|
||||
export async function listFinanceWithdrawalApplications(query = {}) {
|
||||
const endpoint = API_ENDPOINTS.listFinanceWithdrawalApplications;
|
||||
const data = await apiRequest(apiEndpointPath(API_OPERATIONS.listFinanceWithdrawalApplications), {
|
||||
@ -277,30 +251,6 @@ export async function refreshFinanceGoogleRechargePaid(appCode, transactionIds)
|
||||
return Array.isArray(data?.items) ? data.items : [];
|
||||
}
|
||||
|
||||
export async function createFinanceApplication(payload) {
|
||||
const endpoint = API_ENDPOINTS.createFinanceApplication;
|
||||
return apiRequest(apiEndpointPath(API_OPERATIONS.createFinanceApplication), {
|
||||
body: payload,
|
||||
method: endpoint.method
|
||||
});
|
||||
}
|
||||
|
||||
export async function approveFinanceApplication(applicationId, payload) {
|
||||
const endpoint = API_ENDPOINTS.approveFinanceApplication;
|
||||
return apiRequest(apiEndpointPath(API_OPERATIONS.approveFinanceApplication, { application_id: applicationId }), {
|
||||
body: payload,
|
||||
method: endpoint.method
|
||||
});
|
||||
}
|
||||
|
||||
export async function rejectFinanceApplication(applicationId, payload) {
|
||||
const endpoint = API_ENDPOINTS.rejectFinanceApplication;
|
||||
return apiRequest(apiEndpointPath(API_OPERATIONS.rejectFinanceApplication, { application_id: applicationId }), {
|
||||
body: payload,
|
||||
method: endpoint.method
|
||||
});
|
||||
}
|
||||
|
||||
export async function approveFinanceWithdrawalApplication(applicationId, payload) {
|
||||
const endpoint = API_ENDPOINTS.approveFinanceWithdrawalApplication;
|
||||
return apiRequest(apiEndpointPath(API_OPERATIONS.approveFinanceWithdrawalApplication, { application_id: applicationId }), {
|
||||
@ -317,11 +267,6 @@ export async function rejectFinanceWithdrawalApplication(applicationId, payload)
|
||||
});
|
||||
}
|
||||
|
||||
export function filterOperationsByPermissions(operations, permissions) {
|
||||
const allowed = new Set(permissions || []);
|
||||
return (operations || []).filter((operation) => !operation.permissionCode || allowed.has(operation.permissionCode));
|
||||
}
|
||||
|
||||
async function currentSession() {
|
||||
try {
|
||||
return await refreshSession();
|
||||
@ -365,9 +310,7 @@ async function listRechargeBillsForApp(appCode, query = {}) {
|
||||
|
||||
function normalizeOptions(data = {}) {
|
||||
return {
|
||||
apps: normalizeApps(data.apps),
|
||||
operations: FINANCE_OPERATIONS,
|
||||
walletIdentities: WALLET_IDENTITIES
|
||||
apps: normalizeApps(data.apps)
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -1,34 +1,53 @@
|
||||
import { afterEach, expect, test, vi } from "vitest";
|
||||
import { setAccessToken } from "@/shared/api/request";
|
||||
import {
|
||||
approveFinanceApplication,
|
||||
approveFinanceWithdrawalApplication,
|
||||
createFinanceApplication,
|
||||
createFinanceCoinSellerRechargeOrder,
|
||||
fetchFinanceApplicationOptions,
|
||||
fetchFinanceOptions,
|
||||
fetchFinanceSession,
|
||||
getFinanceCoinSellerRechargeExchangeRate,
|
||||
filterOperationsByPermissions,
|
||||
grantFinanceCoinSellerRechargeOrder,
|
||||
listFinanceApplications,
|
||||
listFinanceCoinSellerRechargeOrders,
|
||||
listFinanceRechargeDetails,
|
||||
listMyFinanceApplications,
|
||||
listFinanceWithdrawalApplications,
|
||||
rejectFinanceApplication,
|
||||
rejectFinanceWithdrawalApplication,
|
||||
quoteFinanceCoinSellerRechargeOrder,
|
||||
replaceFinanceCoinSellerRechargeExchangeRate,
|
||||
verifyFinanceCoinSellerRechargeReceipt,
|
||||
verifyFinanceCoinSellerRechargeOrder
|
||||
} from "./api.js";
|
||||
import { FINANCE_OPERATIONS } from "./constants.js";
|
||||
|
||||
afterEach(() => {
|
||||
setAccessToken("");
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
test("finance application APIs use generated endpoint paths", async () => {
|
||||
test("finance session no longer treats removed application permissions as withdrawal access", async () => {
|
||||
vi.stubGlobal(
|
||||
"fetch",
|
||||
vi.fn(async () =>
|
||||
new Response(
|
||||
JSON.stringify({
|
||||
code: 0,
|
||||
data: {
|
||||
permissions: ["finance-application:audit"],
|
||||
user: { account: "legacy-finance" }
|
||||
}
|
||||
})
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
const session = await fetchFinanceSession();
|
||||
|
||||
expect(session.canAccessFinance).toBe(false);
|
||||
expect(session.canAuditWithdrawalApplications).toBe(false);
|
||||
expect(session.canViewWithdrawalApplications).toBe(false);
|
||||
expect(session).not.toHaveProperty("canAuditApplication");
|
||||
expect(session).not.toHaveProperty("canCreateApplication");
|
||||
});
|
||||
|
||||
test("finance options and withdrawal APIs use generated endpoint paths", async () => {
|
||||
vi.stubGlobal(
|
||||
"fetch",
|
||||
vi.fn(async (url) => {
|
||||
@ -41,12 +60,6 @@ test("finance application APIs use generated endpoint paths", async () => {
|
||||
}
|
||||
}));
|
||||
}
|
||||
if (String(url).includes("/applications?")) {
|
||||
return new Response(JSON.stringify({ code: 0, data: { items: [], page: 1, pageSize: 50, total: 0 } }));
|
||||
}
|
||||
if (String(url).includes("/applications/mine?")) {
|
||||
return new Response(JSON.stringify({ code: 0, data: { items: [], page: 1, pageSize: 50, total: 0 } }));
|
||||
}
|
||||
if (String(url).includes("/withdrawal-applications?")) {
|
||||
return new Response(JSON.stringify({ code: 0, data: { items: [], page: 1, pageSize: 50, total: 0 } }));
|
||||
}
|
||||
@ -54,19 +67,8 @@ test("finance application APIs use generated endpoint paths", async () => {
|
||||
})
|
||||
);
|
||||
|
||||
const options = await fetchFinanceApplicationOptions();
|
||||
await listFinanceApplications({ page: 1, page_size: 50, status: "pending" });
|
||||
await listMyFinanceApplications({ page: 1, page_size: 50 });
|
||||
const options = await fetchFinanceOptions();
|
||||
await listFinanceWithdrawalApplications({ page: 1, page_size: 50 });
|
||||
await createFinanceApplication({
|
||||
appCode: "lalu",
|
||||
coinAmount: 100,
|
||||
operation: "coin_seller_coin_credit",
|
||||
rechargeAmount: 10,
|
||||
targetUserId: "10001"
|
||||
});
|
||||
await approveFinanceApplication(12, { auditRemark: "ok" });
|
||||
await rejectFinanceApplication(13, { auditRemark: "bad" });
|
||||
await approveFinanceWithdrawalApplication(14, { auditImageUrl: "https://cdn.example.com/pay.png", auditRemark: "ok" });
|
||||
await rejectFinanceWithdrawalApplication(15, { auditRemark: "bad" });
|
||||
|
||||
@ -74,38 +76,16 @@ test("finance application APIs use generated endpoint paths", async () => {
|
||||
expect(options.apps).toEqual([
|
||||
{ appCode: "lalu", appName: "Lalu", logoUrl: "https://media.example.com/lalu.png" }
|
||||
]);
|
||||
expect(options.operations.map((item) => item.label)).toEqual([
|
||||
"增加用户金币",
|
||||
"减少用户金币",
|
||||
"用户钱包扣减",
|
||||
"用户钱包增加",
|
||||
"增加币商金币",
|
||||
"减少币商金币"
|
||||
]);
|
||||
expect(String(calls[0][0])).toContain("/api/v1/admin/apps");
|
||||
expect(calls[0][1]?.method).toBe("GET");
|
||||
expect(String(calls[1][0])).toContain("/api/v1/admin/finance/applications?");
|
||||
expect(String(calls[1][0])).toContain("status=pending");
|
||||
expect(String(calls[1][0])).toContain("/api/v1/admin/finance/withdrawal-applications?");
|
||||
expect(calls[1][1]?.method).toBe("GET");
|
||||
expect(String(calls[2][0])).toContain("/api/v1/admin/finance/applications/mine?");
|
||||
expect(calls[2][1]?.method).toBe("GET");
|
||||
expect(String(calls[3][0])).toContain("/api/v1/admin/finance/withdrawal-applications?");
|
||||
expect(calls[3][1]?.method).toBe("GET");
|
||||
expect(String(calls[4][0])).toContain("/api/v1/admin/finance/applications");
|
||||
expect(calls[4][1]?.method).toBe("POST");
|
||||
expect(JSON.parse(String(calls[4][1]?.body))).toMatchObject({ appCode: "lalu", operation: "coin_seller_coin_credit" });
|
||||
expect(String(calls[5][0])).toContain("/api/v1/admin/finance/applications/12/approve");
|
||||
expect(calls[5][1]?.method).toBe("POST");
|
||||
expect(JSON.parse(String(calls[5][1]?.body))).toEqual({ auditRemark: "ok" });
|
||||
expect(String(calls[6][0])).toContain("/api/v1/admin/finance/applications/13/reject");
|
||||
expect(calls[6][1]?.method).toBe("POST");
|
||||
expect(JSON.parse(String(calls[6][1]?.body))).toEqual({ auditRemark: "bad" });
|
||||
expect(String(calls[7][0])).toContain("/api/v1/admin/finance/withdrawal-applications/14/approve");
|
||||
expect(calls[7][1]?.method).toBe("POST");
|
||||
expect(JSON.parse(String(calls[7][1]?.body))).toEqual({ auditImageUrl: "https://cdn.example.com/pay.png", auditRemark: "ok" });
|
||||
expect(String(calls[8][0])).toContain("/api/v1/admin/finance/withdrawal-applications/15/reject");
|
||||
expect(calls[8][1]?.method).toBe("POST");
|
||||
expect(JSON.parse(String(calls[8][1]?.body))).toEqual({ auditRemark: "bad" });
|
||||
expect(String(calls[2][0])).toContain("/api/v1/admin/finance/withdrawal-applications/14/approve");
|
||||
expect(calls[2][1]?.method).toBe("POST");
|
||||
expect(JSON.parse(String(calls[2][1]?.body))).toEqual({ auditImageUrl: "https://cdn.example.com/pay.png", auditRemark: "ok" });
|
||||
expect(String(calls[3][0])).toContain("/api/v1/admin/finance/withdrawal-applications/15/reject");
|
||||
expect(calls[3][1]?.method).toBe("POST");
|
||||
expect(JSON.parse(String(calls[3][1]?.body))).toEqual({ auditRemark: "bad" });
|
||||
});
|
||||
|
||||
test("finance coin seller recharge order APIs use generated endpoint paths", async () => {
|
||||
@ -194,15 +174,6 @@ test("finance coin seller quote and exchange rate APIs use generated endpoint pa
|
||||
expect(calls[2][1]?.method).toBe("PUT");
|
||||
});
|
||||
|
||||
test("filters operation options by backend-configured permissions", () => {
|
||||
const allowed = filterOperationsByPermissions(FINANCE_OPERATIONS, [
|
||||
"finance-operation:user-coin-credit",
|
||||
"finance-operation:coin-seller-coin-debit"
|
||||
]);
|
||||
|
||||
expect(allowed.map((item) => item.value)).toEqual(["user_coin_credit", "coin_seller_coin_debit"]);
|
||||
});
|
||||
|
||||
test("finance recharge details query every app with app header and merge by recharge time", async () => {
|
||||
vi.stubGlobal(
|
||||
"fetch",
|
||||
|
||||
@ -1,143 +0,0 @@
|
||||
import SendOutlined from "@mui/icons-material/SendOutlined";
|
||||
import Alert from "@mui/material/Alert";
|
||||
import Button from "@mui/material/Button";
|
||||
import InputAdornment from "@mui/material/InputAdornment";
|
||||
import MenuItem from "@mui/material/MenuItem";
|
||||
import TextField from "@mui/material/TextField";
|
||||
import { AppIdentity } from "@/shared/ui/AppIdentity.jsx";
|
||||
import { UploadField } from "@/shared/ui/UploadField.jsx";
|
||||
|
||||
export function FinanceApplicationForm({
|
||||
apps,
|
||||
disabled,
|
||||
error,
|
||||
form,
|
||||
loading,
|
||||
operations,
|
||||
walletIdentities,
|
||||
onChange,
|
||||
onCancel,
|
||||
onSubmit
|
||||
}) {
|
||||
const selectedOperation = operations.find((operation) => operation.value === form.operation);
|
||||
const needsWalletIdentity = Boolean(selectedOperation?.requiresWalletIdentity);
|
||||
const canSubmit = !disabled && !loading && apps.length > 0 && operations.length > 0;
|
||||
|
||||
const updateField = (key, value) => {
|
||||
if (key === "operation") {
|
||||
const nextOperation = operations.find((operation) => operation.value === value);
|
||||
onChange({ ...form, operation: value, walletIdentity: nextOperation?.requiresWalletIdentity ? form.walletIdentity : "" });
|
||||
return;
|
||||
}
|
||||
onChange({ ...form, [key]: value });
|
||||
};
|
||||
|
||||
return (
|
||||
<form className={onCancel ? "finance-form finance-form--dialog" : "finance-panel finance-form"} onSubmit={onSubmit}>
|
||||
{error ? <Alert severity="error">{error}</Alert> : null}
|
||||
{disabled ? <Alert severity="warning">无发起财务申请权限</Alert> : null}
|
||||
{!loading && !disabled && (!apps.length || !operations.length) ? <Alert severity="warning">暂无可用 APP 或操作权限</Alert> : null}
|
||||
<div className="finance-form-grid">
|
||||
<TextField
|
||||
disabled={disabled || loading}
|
||||
label="APP"
|
||||
required
|
||||
select
|
||||
value={form.appCode}
|
||||
onChange={(event) => updateField("appCode", event.target.value)}
|
||||
>
|
||||
{apps.map((app) => (
|
||||
<MenuItem key={app.appCode} value={app.appCode}>
|
||||
<AppIdentity app={app} size="small" />
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
<TextField
|
||||
disabled={disabled || loading}
|
||||
label="操作内容"
|
||||
required
|
||||
select
|
||||
value={form.operation}
|
||||
onChange={(event) => updateField("operation", event.target.value)}
|
||||
>
|
||||
{operations.map((operation) => (
|
||||
<MenuItem key={operation.value} value={operation.value}>
|
||||
{operation.label}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
{needsWalletIdentity ? (
|
||||
<TextField
|
||||
disabled={disabled || loading}
|
||||
label="钱包身份"
|
||||
required
|
||||
select
|
||||
value={form.walletIdentity}
|
||||
onChange={(event) => updateField("walletIdentity", event.target.value)}
|
||||
>
|
||||
{walletIdentities.map(([value, label]) => (
|
||||
<MenuItem key={value} value={value}>
|
||||
{label}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
) : null}
|
||||
<TextField
|
||||
disabled={disabled || loading}
|
||||
label="目标用户 ID"
|
||||
required
|
||||
value={form.targetUserId}
|
||||
onChange={(event) => updateField("targetUserId", event.target.value)}
|
||||
/>
|
||||
<TextField
|
||||
disabled={disabled || loading}
|
||||
inputProps={{ min: 0, step: "1" }}
|
||||
label="金币数量"
|
||||
required
|
||||
type="number"
|
||||
value={form.coinAmount}
|
||||
onChange={(event) => updateField("coinAmount", event.target.value)}
|
||||
/>
|
||||
<TextField
|
||||
disabled={disabled || loading}
|
||||
InputProps={{ startAdornment: <InputAdornment position="start">USD</InputAdornment> }}
|
||||
inputProps={{ min: 0, step: "0.01" }}
|
||||
label="充值金额"
|
||||
required
|
||||
type="number"
|
||||
value={form.rechargeAmount}
|
||||
onChange={(event) => updateField("rechargeAmount", event.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="finance-evidence-grid">
|
||||
<UploadField
|
||||
className="finance-credential-upload"
|
||||
disabled={disabled || loading}
|
||||
hideType
|
||||
label="凭证图片"
|
||||
previewClassName="finance-credential-preview"
|
||||
value={form.credentialImageUrl}
|
||||
onChange={(value) => updateField("credentialImageUrl", value)}
|
||||
/>
|
||||
<TextField
|
||||
disabled={disabled || loading}
|
||||
label="凭证文字"
|
||||
minRows={7}
|
||||
multiline
|
||||
value={form.credentialText}
|
||||
onChange={(event) => updateField("credentialText", event.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="finance-form-actions">
|
||||
{onCancel ? (
|
||||
<Button disabled={loading} variant="outlined" onClick={onCancel}>
|
||||
取消
|
||||
</Button>
|
||||
) : null}
|
||||
<Button disabled={!canSubmit} startIcon={<SendOutlined fontSize="small" />} type="submit" variant="contained">
|
||||
发起申请
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
@ -1,201 +0,0 @@
|
||||
import CheckCircleOutlineOutlined from "@mui/icons-material/CheckCircleOutlineOutlined";
|
||||
import CloseOutlined from "@mui/icons-material/CloseOutlined";
|
||||
import RefreshOutlined from "@mui/icons-material/RefreshOutlined";
|
||||
import Button from "@mui/material/Button";
|
||||
import IconButton from "@mui/material/IconButton";
|
||||
import MenuItem from "@mui/material/MenuItem";
|
||||
import Table from "@mui/material/Table";
|
||||
import TableBody from "@mui/material/TableBody";
|
||||
import TableCell from "@mui/material/TableCell";
|
||||
import TableContainer from "@mui/material/TableContainer";
|
||||
import TableHead from "@mui/material/TableHead";
|
||||
import TableRow from "@mui/material/TableRow";
|
||||
import TextField from "@mui/material/TextField";
|
||||
import { AppIdentity } from "@/shared/ui/AppIdentity.jsx";
|
||||
import Tooltip from "@mui/material/Tooltip";
|
||||
import { APPLICATION_STATUS } from "../constants.js";
|
||||
import { formatAmount, formatTime, operationLabel, statusLabel, walletIdentityLabel } from "../format.js";
|
||||
|
||||
export function FinanceApplicationList({ applications, error, filters, loading, onAudit, onFiltersChange, onReload, operations, options }) {
|
||||
const items = applications.items || [];
|
||||
const page = Number(applications.page || filters.page || 1);
|
||||
const pageSize = Number(applications.pageSize || 50);
|
||||
const total = Number(applications.total || 0);
|
||||
const hasNextPage = page * pageSize < total;
|
||||
|
||||
return (
|
||||
<section className="finance-panel finance-list">
|
||||
<div className="finance-toolbar">
|
||||
<TextField
|
||||
label="APP"
|
||||
select
|
||||
value={filters.appCode}
|
||||
onChange={(event) => onFiltersChange({ appCode: event.target.value })}
|
||||
>
|
||||
<MenuItem value="">全部 APP</MenuItem>
|
||||
{options.apps.map((app) => (
|
||||
<MenuItem key={app.appCode} value={app.appCode}>
|
||||
<AppIdentity app={app} size="small" />
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
<TextField
|
||||
label="操作内容"
|
||||
select
|
||||
value={filters.operation}
|
||||
onChange={(event) => onFiltersChange({ operation: event.target.value })}
|
||||
>
|
||||
<MenuItem value="">全部操作</MenuItem>
|
||||
{operations.map((operation) => (
|
||||
<MenuItem key={operation.value} value={operation.value}>
|
||||
{operation.label}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
<TextField label="状态" select value={filters.status} onChange={(event) => onFiltersChange({ status: event.target.value })}>
|
||||
<MenuItem value="">全部状态</MenuItem>
|
||||
{APPLICATION_STATUS.map(([value, label]) => (
|
||||
<MenuItem key={value} value={value}>
|
||||
{label}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
<TextField
|
||||
label="搜索"
|
||||
value={filters.keyword}
|
||||
onChange={(event) => onFiltersChange({ keyword: event.target.value })}
|
||||
/>
|
||||
<Button startIcon={<RefreshOutlined fontSize="small" />} variant="outlined" onClick={onReload}>
|
||||
刷新列表
|
||||
</Button>
|
||||
</div>
|
||||
{error ? <div className="finance-error">{error}</div> : null}
|
||||
{loading && !items.length ? <div className="finance-skeleton" /> : null}
|
||||
{!loading || items.length ? (
|
||||
<TableContainer className="finance-table-wrap">
|
||||
<Table className="finance-table" size="small" stickyHeader>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>APP</TableCell>
|
||||
<TableCell>操作</TableCell>
|
||||
<TableCell>目标用户</TableCell>
|
||||
<TableCell align="right">金币数量</TableCell>
|
||||
<TableCell align="right">充值金额</TableCell>
|
||||
<TableCell>凭证</TableCell>
|
||||
<TableCell>申请人</TableCell>
|
||||
<TableCell className="finance-time-cell">发起申请时间</TableCell>
|
||||
<TableCell>审批人</TableCell>
|
||||
<TableCell className="finance-time-cell">审核时间</TableCell>
|
||||
<TableCell>状态</TableCell>
|
||||
<TableCell align="right">审核</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{items.length ? (
|
||||
items.map((item) => (
|
||||
<TableRow key={item.id}>
|
||||
<TableCell>
|
||||
<AppIdentity
|
||||
app={options.apps.find((app) => app.appCode === item.appCode)}
|
||||
appCode={item.appCode}
|
||||
appName={item.appName}
|
||||
size="small"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Stacked primary={operationLabel(item.operation, operations)} secondary={walletIdentityLabel(item.walletIdentity)} />
|
||||
</TableCell>
|
||||
<TableCell>{item.targetUserId || "-"}</TableCell>
|
||||
<TableCell align="right">{formatAmount(item.coinAmount)}</TableCell>
|
||||
<TableCell align="right">{formatAmount(item.rechargeAmount)}</TableCell>
|
||||
<TableCell>
|
||||
<Evidence item={item} />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Stacked primary={item.applicantName || "-"} secondary={item.applicantUserId || ""} />
|
||||
</TableCell>
|
||||
<TableCell className="finance-time-cell">{formatTime(item.createdAtMs)}</TableCell>
|
||||
<TableCell>
|
||||
<Stacked primary={item.auditorName || "-"} secondary={item.auditorUserId || ""} />
|
||||
</TableCell>
|
||||
<TableCell className="finance-time-cell">{formatTime(item.auditedAtMs)}</TableCell>
|
||||
<TableCell>
|
||||
<StatusBadge status={item.status} />
|
||||
</TableCell>
|
||||
<TableCell align="right">
|
||||
{item.status === "pending" ? (
|
||||
<span className="finance-row-actions">
|
||||
<Tooltip arrow title="通过">
|
||||
<IconButton aria-label="通过" onClick={() => onAudit({ ...item, decision: "approved" })}>
|
||||
<CheckCircleOutlineOutlined fontSize="small" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip arrow title="拒绝">
|
||||
<IconButton aria-label="拒绝" onClick={() => onAudit({ ...item, decision: "rejected" })}>
|
||||
<CloseOutlined fontSize="small" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</span>
|
||||
) : (
|
||||
"-"
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell align="center" colSpan={12}>
|
||||
当前无数据
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
) : null}
|
||||
{total > pageSize ? (
|
||||
<div className="finance-pagination">
|
||||
<Button disabled={page <= 1 || loading} variant="outlined" onClick={() => onFiltersChange({ page: page - 1 })}>
|
||||
上一页
|
||||
</Button>
|
||||
<span>
|
||||
第 {page} 页 / 共 {total} 条
|
||||
</span>
|
||||
<Button disabled={!hasNextPage || loading} variant="outlined" onClick={() => onFiltersChange({ page: page + 1 })}>
|
||||
下一页
|
||||
</Button>
|
||||
</div>
|
||||
) : null}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function Evidence({ item }) {
|
||||
if (!item.credentialImageUrl && !item.credentialText) {
|
||||
return "-";
|
||||
}
|
||||
return (
|
||||
<span className="finance-evidence">
|
||||
{item.credentialImageUrl ? (
|
||||
<a href={item.credentialImageUrl} rel="noreferrer" target="_blank">
|
||||
图片
|
||||
</a>
|
||||
) : null}
|
||||
{item.credentialText ? <span title={item.credentialText}>{item.credentialText}</span> : null}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function Stacked({ primary, secondary }) {
|
||||
return (
|
||||
<span className="finance-stack">
|
||||
<span>{primary || "-"}</span>
|
||||
<small>{secondary || ""}</small>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function StatusBadge({ status }) {
|
||||
const tone = status === "approved" ? "success" : status === "rejected" ? "danger" : "warning";
|
||||
return <span className={`finance-status finance-status--${tone}`}>{statusLabel(status)}</span>;
|
||||
}
|
||||
@ -1,67 +0,0 @@
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { expect, test, vi } from "vitest";
|
||||
import { FinanceApplicationList } from "./FinanceApplicationList.jsx";
|
||||
|
||||
const baseProps = {
|
||||
applications: {
|
||||
items: [],
|
||||
page: 1,
|
||||
pageSize: 50,
|
||||
total: 0
|
||||
},
|
||||
error: "",
|
||||
filters: {
|
||||
appCode: "",
|
||||
keyword: "",
|
||||
operation: "",
|
||||
page: 1,
|
||||
status: ""
|
||||
},
|
||||
loading: false,
|
||||
onAudit: vi.fn(),
|
||||
onFiltersChange: vi.fn(),
|
||||
onReload: vi.fn(),
|
||||
operations: [{ label: "增加币商金币", value: "coin_seller_coin_credit" }],
|
||||
options: { apps: [] }
|
||||
};
|
||||
|
||||
test("renders finance audit table with explicit applicant and audit time columns", () => {
|
||||
render(
|
||||
<FinanceApplicationList
|
||||
{...baseProps}
|
||||
applications={{
|
||||
...baseProps.applications,
|
||||
items: [
|
||||
{
|
||||
appCode: "lalu",
|
||||
applicantName: "张三",
|
||||
auditedAtMs: 1760003600000,
|
||||
auditorName: "王财务",
|
||||
auditorUserId: 99,
|
||||
coinAmount: 200,
|
||||
createdAtMs: 1760000000000,
|
||||
id: 9,
|
||||
operation: "coin_seller_coin_credit",
|
||||
rechargeAmount: 20,
|
||||
status: "approved",
|
||||
targetUserId: "88"
|
||||
}
|
||||
],
|
||||
total: 1
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByRole("columnheader", { name: "发起申请时间" })).toBeInTheDocument();
|
||||
expect(screen.getByRole("columnheader", { name: "审批人" })).toBeInTheDocument();
|
||||
expect(screen.getByRole("columnheader", { name: "审核时间" })).toBeInTheDocument();
|
||||
expect(screen.getByText("王财务")).toBeInTheDocument();
|
||||
expect(screen.getByText("99")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("keeps empty finance audit table colspan aligned with visible columns", () => {
|
||||
const { container } = render(<FinanceApplicationList {...baseProps} />);
|
||||
|
||||
expect(container.querySelector("tbody td")?.colSpan).toBe(12);
|
||||
expect(screen.getByText("当前无数据")).toBeInTheDocument();
|
||||
});
|
||||
@ -7,9 +7,8 @@ import TextField from "@mui/material/TextField";
|
||||
import { useEffect, useState } from "react";
|
||||
import { AppIdentity } from "@/shared/ui/AppIdentity.jsx";
|
||||
import { UploadField } from "@/shared/ui/UploadField.jsx";
|
||||
import { operationLabel } from "../format.js";
|
||||
|
||||
export function FinanceAuditDialog({ application, apps = [], loading, onClose, onSubmit, open, operations, requireApprovalImage = false, requireRejectRemark = false }) {
|
||||
export function FinanceAuditDialog({ application, apps = [], loading, onClose, onSubmit, open, requireApprovalImage = false, requireRejectRemark = false }) {
|
||||
const [remark, setRemark] = useState("");
|
||||
const [auditImageUrl, setAuditImageUrl] = useState("");
|
||||
const decision = application?.decision || "approved";
|
||||
@ -39,8 +38,8 @@ export function FinanceAuditDialog({ application, apps = [], loading, onClose, o
|
||||
appName={application?.appName}
|
||||
size="small"
|
||||
/>
|
||||
<strong>{operationLabel(application?.operation, operations)}</strong>
|
||||
<span>{application?.targetUserId || "-"}</span>
|
||||
<strong>{application?.withdrawMethod || "用户提现"}</strong>
|
||||
<span>{application?.userId || "-"}</span>
|
||||
</div>
|
||||
<TextField
|
||||
disabled={loading}
|
||||
|
||||
@ -1,164 +0,0 @@
|
||||
import AddOutlined from "@mui/icons-material/AddOutlined";
|
||||
import RefreshOutlined from "@mui/icons-material/RefreshOutlined";
|
||||
import Button from "@mui/material/Button";
|
||||
import MenuItem from "@mui/material/MenuItem";
|
||||
import Table from "@mui/material/Table";
|
||||
import TableBody from "@mui/material/TableBody";
|
||||
import TableCell from "@mui/material/TableCell";
|
||||
import TableContainer from "@mui/material/TableContainer";
|
||||
import TableHead from "@mui/material/TableHead";
|
||||
import TableRow from "@mui/material/TableRow";
|
||||
import TextField from "@mui/material/TextField";
|
||||
import { AppIdentity } from "@/shared/ui/AppIdentity.jsx";
|
||||
import { APPLICATION_STATUS } from "../constants.js";
|
||||
import { formatAmount, formatTime, operationLabel, statusLabel, walletIdentityLabel } from "../format.js";
|
||||
|
||||
export function FinanceMyApplicationList({ applications, error, filters, loading, onCreate, onFiltersChange, onReload, operations, options }) {
|
||||
const items = applications.items || [];
|
||||
const page = Number(applications.page || filters.page || 1);
|
||||
const pageSize = Number(applications.pageSize || 50);
|
||||
const total = Number(applications.total || 0);
|
||||
const hasNextPage = page * pageSize < total;
|
||||
|
||||
return (
|
||||
<section className="finance-panel finance-list">
|
||||
<div className="finance-toolbar finance-toolbar--with-action">
|
||||
<TextField label="APP" select value={filters.appCode} onChange={(event) => onFiltersChange({ appCode: event.target.value })}>
|
||||
<MenuItem value="">全部 APP</MenuItem>
|
||||
{options.apps.map((app) => (
|
||||
<MenuItem key={app.appCode} value={app.appCode}>
|
||||
<AppIdentity app={app} size="small" />
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
<TextField label="操作内容" select value={filters.operation} onChange={(event) => onFiltersChange({ operation: event.target.value })}>
|
||||
<MenuItem value="">全部操作</MenuItem>
|
||||
{operations.map((operation) => (
|
||||
<MenuItem key={operation.value} value={operation.value}>
|
||||
{operation.label}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
<TextField label="状态" select value={filters.status} onChange={(event) => onFiltersChange({ status: event.target.value })}>
|
||||
<MenuItem value="">全部状态</MenuItem>
|
||||
{APPLICATION_STATUS.map(([value, label]) => (
|
||||
<MenuItem key={value} value={value}>
|
||||
{label}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
<TextField label="搜索" value={filters.keyword} onChange={(event) => onFiltersChange({ keyword: event.target.value })} />
|
||||
<Button startIcon={<RefreshOutlined fontSize="small" />} variant="outlined" onClick={onReload}>
|
||||
刷新列表
|
||||
</Button>
|
||||
<Button startIcon={<AddOutlined fontSize="small" />} variant="contained" onClick={onCreate}>
|
||||
添加
|
||||
</Button>
|
||||
</div>
|
||||
{error ? <div className="finance-error">{error}</div> : null}
|
||||
{loading && !items.length ? <div className="finance-skeleton" /> : null}
|
||||
{!loading || items.length ? (
|
||||
<TableContainer className="finance-table-wrap">
|
||||
<Table className="finance-table" size="small" stickyHeader>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>APP</TableCell>
|
||||
<TableCell>操作</TableCell>
|
||||
<TableCell>目标用户</TableCell>
|
||||
<TableCell align="right">金币数量</TableCell>
|
||||
<TableCell align="right">充值金额</TableCell>
|
||||
<TableCell>凭证</TableCell>
|
||||
<TableCell>状态</TableCell>
|
||||
<TableCell className="finance-time-cell">发起申请时间</TableCell>
|
||||
<TableCell>审批人</TableCell>
|
||||
<TableCell className="finance-time-cell">审核时间</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{items.length ? (
|
||||
items.map((item) => (
|
||||
<TableRow key={item.id}>
|
||||
<TableCell>
|
||||
<AppIdentity
|
||||
app={options.apps.find((app) => app.appCode === item.appCode)}
|
||||
appCode={item.appCode}
|
||||
appName={item.appName}
|
||||
size="small"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Stacked primary={operationLabel(item.operation, operations)} secondary={walletIdentityLabel(item.walletIdentity)} />
|
||||
</TableCell>
|
||||
<TableCell>{item.targetUserId || "-"}</TableCell>
|
||||
<TableCell align="right">{formatAmount(item.coinAmount)}</TableCell>
|
||||
<TableCell align="right">{formatAmount(item.rechargeAmount)}</TableCell>
|
||||
<TableCell>
|
||||
<Evidence item={item} />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<StatusBadge status={item.status} />
|
||||
</TableCell>
|
||||
<TableCell className="finance-time-cell">{formatTime(item.createdAtMs)}</TableCell>
|
||||
<TableCell>
|
||||
<Stacked primary={item.auditorName || "-"} secondary={item.auditorUserId || ""} />
|
||||
</TableCell>
|
||||
<TableCell className="finance-time-cell">{formatTime(item.auditedAtMs)}</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell align="center" colSpan={10}>
|
||||
当前无数据
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
) : null}
|
||||
{total > pageSize ? (
|
||||
<div className="finance-pagination">
|
||||
<Button disabled={page <= 1 || loading} variant="outlined" onClick={() => onFiltersChange({ page: page - 1 })}>
|
||||
上一页
|
||||
</Button>
|
||||
<span>
|
||||
第 {page} 页 / 共 {total} 条
|
||||
</span>
|
||||
<Button disabled={!hasNextPage || loading} variant="outlined" onClick={() => onFiltersChange({ page: page + 1 })}>
|
||||
下一页
|
||||
</Button>
|
||||
</div>
|
||||
) : null}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function Evidence({ item }) {
|
||||
if (!item.credentialImageUrl && !item.credentialText) {
|
||||
return "-";
|
||||
}
|
||||
return (
|
||||
<span className="finance-evidence">
|
||||
{item.credentialImageUrl ? (
|
||||
<a href={item.credentialImageUrl} rel="noreferrer" target="_blank">
|
||||
图片
|
||||
</a>
|
||||
) : null}
|
||||
{item.credentialText ? <span title={item.credentialText}>{item.credentialText}</span> : null}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function Stacked({ primary, secondary }) {
|
||||
return (
|
||||
<span className="finance-stack">
|
||||
<span>{primary || "-"}</span>
|
||||
<small>{secondary || ""}</small>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function StatusBadge({ status }) {
|
||||
const tone = status === "approved" ? "success" : status === "rejected" ? "danger" : "warning";
|
||||
return <span className={`finance-status finance-status--${tone}`}>{statusLabel(status)}</span>;
|
||||
}
|
||||
@ -1,81 +0,0 @@
|
||||
import { fireEvent, render, screen } from "@testing-library/react";
|
||||
import { expect, test, vi } from "vitest";
|
||||
import { FinanceMyApplicationList } from "./FinanceMyApplicationList.jsx";
|
||||
|
||||
const baseProps = {
|
||||
applications: {
|
||||
items: [],
|
||||
page: 1,
|
||||
pageSize: 50,
|
||||
total: 0
|
||||
},
|
||||
error: "",
|
||||
filters: {
|
||||
appCode: "",
|
||||
keyword: "",
|
||||
operation: "",
|
||||
page: 1,
|
||||
status: ""
|
||||
},
|
||||
loading: false,
|
||||
onCreate: vi.fn(),
|
||||
onFiltersChange: vi.fn(),
|
||||
onReload: vi.fn(),
|
||||
operations: [{ label: "增加用户金币", value: "user_coin_credit" }],
|
||||
options: { apps: [] }
|
||||
};
|
||||
|
||||
test("renders my finance application list and create action", () => {
|
||||
const onCreate = vi.fn();
|
||||
render(
|
||||
<FinanceMyApplicationList
|
||||
{...baseProps}
|
||||
applications={{
|
||||
...baseProps.applications,
|
||||
items: [
|
||||
{
|
||||
appCode: "lalu",
|
||||
auditedAtMs: 1760007200000,
|
||||
auditorName: "财务一",
|
||||
auditorUserId: 21,
|
||||
coinAmount: 100,
|
||||
createdAtMs: 1760000000000,
|
||||
credentialText: "receipt",
|
||||
id: 7,
|
||||
operation: "user_coin_credit",
|
||||
rechargeAmount: 12.5,
|
||||
status: "approved",
|
||||
targetUserId: "10001"
|
||||
}
|
||||
],
|
||||
total: 1
|
||||
}}
|
||||
onCreate={onCreate}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByRole("columnheader", { name: "APP" })).toBeInTheDocument();
|
||||
expect(screen.getByRole("columnheader", { name: "操作" })).toBeInTheDocument();
|
||||
expect(screen.getByRole("columnheader", { name: "目标用户" })).toBeInTheDocument();
|
||||
expect(screen.getByRole("columnheader", { name: "金币数量" })).toBeInTheDocument();
|
||||
expect(screen.getByRole("columnheader", { name: "充值金额" })).toBeInTheDocument();
|
||||
expect(screen.getByRole("columnheader", { name: "凭证" })).toBeInTheDocument();
|
||||
expect(screen.getByRole("columnheader", { name: "状态" })).toBeInTheDocument();
|
||||
expect(screen.getByRole("columnheader", { name: "发起申请时间" })).toBeInTheDocument();
|
||||
expect(screen.getByRole("columnheader", { name: "审批人" })).toBeInTheDocument();
|
||||
expect(screen.getByRole("columnheader", { name: "审核时间" })).toBeInTheDocument();
|
||||
expect(screen.getByText("lalu")).toBeInTheDocument();
|
||||
expect(screen.getByText("增加用户金币")).toBeInTheDocument();
|
||||
expect(screen.getByText("10001")).toBeInTheDocument();
|
||||
expect(screen.getByText("已通过")).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: "添加" }));
|
||||
expect(onCreate).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test("keeps empty my finance application table colspan aligned with visible columns", () => {
|
||||
const { container } = render(<FinanceMyApplicationList {...baseProps} />);
|
||||
|
||||
expect(container.querySelector("tbody td")?.colSpan).toBe(10);
|
||||
expect(screen.getByText("当前无数据")).toBeInTheDocument();
|
||||
});
|
||||
@ -1,5 +1,3 @@
|
||||
import AssignmentOutlined from "@mui/icons-material/AssignmentOutlined";
|
||||
import FactCheckOutlined from "@mui/icons-material/FactCheckOutlined";
|
||||
import InsightsOutlined from "@mui/icons-material/InsightsOutlined";
|
||||
import PaymentsOutlined from "@mui/icons-material/PaymentsOutlined";
|
||||
import ReceiptLongOutlined from "@mui/icons-material/ReceiptLongOutlined";
|
||||
@ -21,18 +19,12 @@ const NAV_GROUPS = [
|
||||
},
|
||||
{
|
||||
label: "审批",
|
||||
views: [
|
||||
{ icon: AssignmentOutlined, id: "create", label: "发起申请" },
|
||||
{ icon: FactCheckOutlined, id: "applications", label: "申请审核" },
|
||||
{ icon: PaymentsOutlined, id: "withdrawals", label: "用户提现申请" },
|
||||
],
|
||||
views: [{ icon: PaymentsOutlined, id: "withdrawals", label: "用户提现申请" }],
|
||||
},
|
||||
];
|
||||
|
||||
const VIEW_TITLES = {
|
||||
applications: "申请审核",
|
||||
coinSellerRechargeOrders: "币商充值",
|
||||
create: "发起申请",
|
||||
overview: "财务概览",
|
||||
recon: "渠道对账",
|
||||
rechargeDetails: "充值流水",
|
||||
@ -42,8 +34,6 @@ const VIEW_TITLES = {
|
||||
export function FinanceShell({
|
||||
activeView,
|
||||
appBar,
|
||||
canAudit,
|
||||
canCreate,
|
||||
canViewCoinSellerRechargeOrders,
|
||||
canViewRechargeDetails,
|
||||
canViewWithdrawals,
|
||||
@ -61,13 +51,10 @@ export function FinanceShell({
|
||||
if (viewId === "coinSellerRechargeOrders") {
|
||||
return canViewCoinSellerRechargeOrders;
|
||||
}
|
||||
if (viewId === "applications") {
|
||||
return canAudit;
|
||||
}
|
||||
if (viewId === "withdrawals") {
|
||||
return canViewWithdrawals;
|
||||
}
|
||||
return canCreate;
|
||||
return false;
|
||||
};
|
||||
return (
|
||||
<main className="finance-shell">
|
||||
|
||||
@ -1,8 +1,6 @@
|
||||
export const FINANCE_PERMISSIONS = {
|
||||
auditApplication: "finance-application:audit",
|
||||
auditWithdrawalApplications: "finance-withdrawal:audit",
|
||||
configureCoinSellerRechargeExchangeRate: "coin-seller:exchange-rate",
|
||||
createApplication: "finance-application:create",
|
||||
createCoinSellerRechargeOrder: "finance-order:coin-seller-recharge:create",
|
||||
grantCoinSellerRechargeOrder: "finance-order:coin-seller-recharge:grant",
|
||||
verifyCoinSellerRechargeOrder: "finance-order:coin-seller-recharge:verify",
|
||||
@ -11,50 +9,7 @@ export const FINANCE_PERMISSIONS = {
|
||||
viewWithdrawalApplications: "finance-withdrawal:view",
|
||||
};
|
||||
|
||||
export const FINANCE_OPERATIONS = [
|
||||
{
|
||||
allowsZeroRechargeAmount: true,
|
||||
label: "增加用户金币",
|
||||
permissionCode: "finance-operation:user-coin-credit",
|
||||
value: "user_coin_credit",
|
||||
},
|
||||
{
|
||||
allowsZeroRechargeAmount: true,
|
||||
label: "减少用户金币",
|
||||
permissionCode: "finance-operation:user-coin-debit",
|
||||
value: "user_coin_debit",
|
||||
},
|
||||
{
|
||||
label: "用户钱包扣减",
|
||||
permissionCode: "finance-operation:user-wallet-debit",
|
||||
requiresWalletIdentity: true,
|
||||
value: "user_wallet_debit",
|
||||
},
|
||||
{
|
||||
label: "用户钱包增加",
|
||||
permissionCode: "finance-operation:user-wallet-credit",
|
||||
requiresWalletIdentity: true,
|
||||
value: "user_wallet_credit",
|
||||
},
|
||||
{
|
||||
label: "增加币商金币",
|
||||
permissionCode: "finance-operation:coin-seller-coin-credit",
|
||||
value: "coin_seller_coin_credit",
|
||||
},
|
||||
{
|
||||
label: "减少币商金币",
|
||||
permissionCode: "finance-operation:coin-seller-coin-debit",
|
||||
value: "coin_seller_coin_debit",
|
||||
},
|
||||
];
|
||||
|
||||
export const WALLET_IDENTITIES = [
|
||||
["host", "Host 钱包"],
|
||||
["agency", "Agency 钱包"],
|
||||
["bd", "BD 钱包"],
|
||||
];
|
||||
|
||||
export const APPLICATION_STATUS = [
|
||||
export const WITHDRAWAL_STATUS = [
|
||||
["pending", "待审核"],
|
||||
["approved", "已通过"],
|
||||
["rejected", "已拒绝"],
|
||||
@ -96,17 +51,6 @@ export const COIN_SELLER_RECHARGE_GRANT_STATUS = [
|
||||
["failed", "发放失败"],
|
||||
];
|
||||
|
||||
export const DEFAULT_APPLICATION_FORM = {
|
||||
appCode: "",
|
||||
coinAmount: "",
|
||||
credentialImageUrl: "",
|
||||
credentialText: "",
|
||||
operation: "",
|
||||
rechargeAmount: "",
|
||||
targetUserId: "",
|
||||
walletIdentity: "",
|
||||
};
|
||||
|
||||
export const DEFAULT_COIN_SELLER_RECHARGE_ORDER_FORM = {
|
||||
appCode: "",
|
||||
chain: "",
|
||||
@ -121,11 +65,3 @@ export const DEFAULT_COIN_SELLER_RECHARGE_ORDER_FORM = {
|
||||
targetUserId: "",
|
||||
usdAmount: "",
|
||||
};
|
||||
|
||||
export function operationRequiresWalletIdentity(operation) {
|
||||
return FINANCE_OPERATIONS.some((item) => item.value === operation && item.requiresWalletIdentity);
|
||||
}
|
||||
|
||||
export function operationAllowsZeroRechargeAmount(operation) {
|
||||
return FINANCE_OPERATIONS.some((item) => item.value === operation && item.allowsZeroRechargeAmount);
|
||||
}
|
||||
|
||||
@ -1,96 +1,12 @@
|
||||
import {
|
||||
APPLICATION_STATUS,
|
||||
COIN_SELLER_RECHARGE_CHAINS,
|
||||
COIN_SELLER_RECHARGE_GRANT_STATUS,
|
||||
COIN_SELLER_RECHARGE_PROVIDERS,
|
||||
COIN_SELLER_RECHARGE_STATUS,
|
||||
COIN_SELLER_RECHARGE_VERIFY_STATUS,
|
||||
FINANCE_OPERATIONS,
|
||||
WALLET_IDENTITIES,
|
||||
operationAllowsZeroRechargeAmount,
|
||||
WITHDRAWAL_STATUS,
|
||||
} from "./constants.js";
|
||||
|
||||
export function buildApplicationPayload(form) {
|
||||
const operation = stringValue(form.operation);
|
||||
const payload = {
|
||||
appCode: stringValue(form.appCode),
|
||||
coinAmount: numberValue(form.coinAmount),
|
||||
credentialImageUrl: stringValue(form.credentialImageUrl),
|
||||
credentialText: stringValue(form.credentialText),
|
||||
operation,
|
||||
rechargeAmount: numberValue(form.rechargeAmount),
|
||||
targetUserId: stringValue(form.targetUserId),
|
||||
walletIdentity: requiresWalletIdentity(operation) ? stringValue(form.walletIdentity) : "",
|
||||
};
|
||||
|
||||
return payload;
|
||||
}
|
||||
|
||||
export function validateApplicationPayload(payload) {
|
||||
if (!payload.appCode) {
|
||||
return "请选择 APP";
|
||||
}
|
||||
if (!payload.operation) {
|
||||
return "请选择操作内容";
|
||||
}
|
||||
if (!payload.targetUserId) {
|
||||
return "请填写目标用户 ID";
|
||||
}
|
||||
if (!Number.isFinite(payload.coinAmount) || payload.coinAmount <= 0) {
|
||||
return "请填写大于 0 的金币数量";
|
||||
}
|
||||
const allowsZeroRechargeAmount = operationAllowsZeroRechargeAmount(payload.operation);
|
||||
if (
|
||||
!Number.isFinite(payload.rechargeAmount) ||
|
||||
payload.rechargeAmount < 0 ||
|
||||
(!allowsZeroRechargeAmount && payload.rechargeAmount <= 0)
|
||||
) {
|
||||
return allowsZeroRechargeAmount ? "请填写不小于 0 的充值金额" : "请填写大于 0 的充值金额";
|
||||
}
|
||||
if (requiresWalletIdentity(payload.operation) && !payload.walletIdentity) {
|
||||
return "请选择钱包身份";
|
||||
}
|
||||
if (!payload.credentialImageUrl && !payload.credentialText) {
|
||||
return "请上传凭证图片或填写凭证文字";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
export function normalizeApplication(item = {}) {
|
||||
return {
|
||||
appCode: stringValue(item.appCode ?? item.app_code),
|
||||
appName: stringValue(item.appName ?? item.app_name),
|
||||
applicantName: stringValue(
|
||||
item.applicantName ?? item.applicant_name ?? item.applicantAccount ?? item.applicant_account,
|
||||
),
|
||||
applicantUserId: item.applicantUserId ?? item.applicant_user_id,
|
||||
auditedAtMs: numberOrNull(item.auditedAtMs ?? item.audited_at_ms),
|
||||
auditorName: stringValue(item.auditorName ?? item.auditor_name ?? item.auditorAccount ?? item.auditor_account),
|
||||
auditorUserId: item.auditorUserId ?? item.auditor_user_id,
|
||||
auditRemark: stringValue(item.auditRemark ?? item.audit_remark),
|
||||
coinAmount: numberValue(item.coinAmount ?? item.coin_amount),
|
||||
createdAtMs: numberOrNull(item.createdAtMs ?? item.created_at_ms),
|
||||
credentialImageUrl: stringValue(item.credentialImageUrl ?? item.credential_image_url),
|
||||
credentialText: stringValue(item.credentialText ?? item.credential_text),
|
||||
id: item.id ?? item.applicationId ?? item.application_id,
|
||||
operation: stringValue(item.operation),
|
||||
rechargeAmount: numberValue(item.rechargeAmount ?? item.recharge_amount),
|
||||
status: stringValue(item.status) || "pending",
|
||||
targetUserId: stringValue(item.targetUserId ?? item.target_user_id),
|
||||
walletIdentity: stringValue(item.walletIdentity ?? item.wallet_identity),
|
||||
};
|
||||
}
|
||||
|
||||
export function normalizeApplicationPage(data = {}) {
|
||||
const items = Array.isArray(data.items) ? data.items : [];
|
||||
return {
|
||||
items: items.map(normalizeApplication),
|
||||
page: Number(data.page || 1),
|
||||
pageSize: Number(data.pageSize ?? data.page_size ?? items.length),
|
||||
total: Number(data.total ?? items.length),
|
||||
};
|
||||
}
|
||||
|
||||
export function normalizeWithdrawalApplication(item = {}) {
|
||||
return {
|
||||
appCode: stringValue(item.appCode ?? item.app_code),
|
||||
@ -538,10 +454,6 @@ export function normalizeRechargeRegions(data = {}) {
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
export function operationLabel(value, operations = FINANCE_OPERATIONS) {
|
||||
return operations.find((item) => item.value === value)?.label || value || "-";
|
||||
}
|
||||
|
||||
export function rechargeTypeLabel(value) {
|
||||
if (value === "coin_seller_transfer") {
|
||||
return "币商充值";
|
||||
@ -595,7 +507,7 @@ export function rechargeSourceTone(rechargeType) {
|
||||
}
|
||||
|
||||
export function statusLabel(value) {
|
||||
return APPLICATION_STATUS.find(([status]) => status === value)?.[1] || value || "-";
|
||||
return WITHDRAWAL_STATUS.find(([status]) => status === value)?.[1] || value || "-";
|
||||
}
|
||||
|
||||
export function coinSellerRechargeProviderLabel(value) {
|
||||
@ -642,10 +554,6 @@ export function isCoinSellerRechargeGranted(order) {
|
||||
);
|
||||
}
|
||||
|
||||
export function walletIdentityLabel(value) {
|
||||
return WALLET_IDENTITIES.find(([identity]) => identity === value)?.[1] || value || "-";
|
||||
}
|
||||
|
||||
export function formatAmount(value) {
|
||||
const number = Number(value);
|
||||
if (!Number.isFinite(number)) {
|
||||
@ -701,10 +609,6 @@ export function formatPercent(value) {
|
||||
return `${percent.toLocaleString("zh-CN", { maximumFractionDigits: 4 })}%`;
|
||||
}
|
||||
|
||||
export function requiresWalletIdentity(operation) {
|
||||
return FINANCE_OPERATIONS.some((item) => item.value === operation && item.requiresWalletIdentity);
|
||||
}
|
||||
|
||||
function numberValue(value) {
|
||||
if (value === "" || value === null || value === undefined) {
|
||||
return Number.NaN;
|
||||
|
||||
@ -1,120 +1,18 @@
|
||||
import { expect, test } from "vitest";
|
||||
import {
|
||||
buildApplicationPayload,
|
||||
buildCoinSellerRechargeReceiptPayload,
|
||||
buildCoinSellerRechargeExchangeRatePayload,
|
||||
buildCoinSellerRechargeOrderPayload,
|
||||
buildCoinSellerRechargeQuotePayload,
|
||||
coinSellerRechargeProviderLabel,
|
||||
normalizeApplicationPage,
|
||||
normalizeCoinSellerRechargeOrderPage,
|
||||
normalizeRechargeBillPage,
|
||||
normalizeWithdrawalApplicationPage,
|
||||
operationLabel,
|
||||
rechargeTypeLabel,
|
||||
statusLabel,
|
||||
validateApplicationPayload,
|
||||
validateCoinSellerRechargeOrderPayload,
|
||||
validateCoinSellerRechargeExchangeRatePayload,
|
||||
walletIdentityLabel,
|
||||
} from "./format.js";
|
||||
|
||||
test("builds and validates finance application payload", () => {
|
||||
const payload = buildApplicationPayload({
|
||||
appCode: " lalu ",
|
||||
coinAmount: "3000",
|
||||
credentialImageUrl: "",
|
||||
credentialText: "bank receipt",
|
||||
operation: "user_wallet_credit",
|
||||
rechargeAmount: "15.5",
|
||||
targetUserId: " 10001 ",
|
||||
walletIdentity: "agency",
|
||||
});
|
||||
|
||||
expect(payload).toEqual({
|
||||
appCode: "lalu",
|
||||
coinAmount: 3000,
|
||||
credentialImageUrl: "",
|
||||
credentialText: "bank receipt",
|
||||
operation: "user_wallet_credit",
|
||||
rechargeAmount: 15.5,
|
||||
targetUserId: "10001",
|
||||
walletIdentity: "agency",
|
||||
});
|
||||
expect(validateApplicationPayload(payload)).toBe("");
|
||||
});
|
||||
|
||||
test("requires wallet identity and credential for wallet operations", () => {
|
||||
const payload = buildApplicationPayload({
|
||||
appCode: "lalu",
|
||||
coinAmount: "100",
|
||||
operation: "user_wallet_debit",
|
||||
rechargeAmount: "1",
|
||||
targetUserId: "10001",
|
||||
});
|
||||
|
||||
expect(validateApplicationPayload(payload)).toBe("请选择钱包身份");
|
||||
expect(validateApplicationPayload({ ...payload, walletIdentity: "host" })).toBe("请上传凭证图片或填写凭证文字");
|
||||
});
|
||||
|
||||
test("allows zero recharge amount only for user coin operations", () => {
|
||||
const payload = buildApplicationPayload({
|
||||
appCode: "lalu",
|
||||
coinAmount: "1111",
|
||||
credentialText: "测试",
|
||||
operation: "user_coin_credit",
|
||||
rechargeAmount: "0",
|
||||
targetUserId: "163185",
|
||||
});
|
||||
|
||||
expect(validateApplicationPayload(payload)).toBe("");
|
||||
expect(validateApplicationPayload({ ...payload, operation: "user_wallet_credit", walletIdentity: "agency" })).toBe(
|
||||
"请填写大于 0 的充值金额",
|
||||
);
|
||||
expect(validateApplicationPayload({ ...payload, operation: "coin_seller_coin_credit" })).toBe(
|
||||
"请填写大于 0 的充值金额",
|
||||
);
|
||||
});
|
||||
|
||||
test("normalizes finance application list rows", () => {
|
||||
const page = normalizeApplicationPage({
|
||||
items: [
|
||||
{
|
||||
app_code: "lalu",
|
||||
applicant_name: "张三",
|
||||
audited_at_ms: 1760003600000,
|
||||
auditor_name: "李四",
|
||||
auditor_user_id: 99,
|
||||
coin_amount: 200,
|
||||
created_at_ms: 1760000000000,
|
||||
id: 9,
|
||||
operation: "coin_seller_coin_credit",
|
||||
recharge_amount: 20,
|
||||
status: "pending",
|
||||
target_user_id: 88,
|
||||
},
|
||||
],
|
||||
page: 1,
|
||||
page_size: 50,
|
||||
total: 1,
|
||||
});
|
||||
|
||||
expect(page.items[0]).toMatchObject({
|
||||
appCode: "lalu",
|
||||
applicantName: "张三",
|
||||
auditedAtMs: 1760003600000,
|
||||
auditorName: "李四",
|
||||
auditorUserId: 99,
|
||||
coinAmount: 200,
|
||||
operation: "coin_seller_coin_credit",
|
||||
targetUserId: "88",
|
||||
});
|
||||
expect(page.pageSize).toBe(50);
|
||||
expect(operationLabel("coin_seller_coin_credit")).toBe("增加币商金币");
|
||||
expect(statusLabel("pending")).toBe("待审核");
|
||||
expect(walletIdentityLabel("bd")).toBe("BD 钱包");
|
||||
});
|
||||
|
||||
test("normalizes finance withdrawal application list rows", () => {
|
||||
const page = normalizeWithdrawalApplicationPage({
|
||||
items: [
|
||||
|
||||
@ -49,20 +49,12 @@ export const PERMISSIONS = {
|
||||
pointWithdrawalConfigView: "point-withdrawal-config:view",
|
||||
pointWithdrawalConfigUpdate: "point-withdrawal-config:update",
|
||||
financeView: "finance:view",
|
||||
financeApplicationCreate: "finance-application:create",
|
||||
financeApplicationAudit: "finance-application:audit",
|
||||
financeWithdrawalView: "finance-withdrawal:view",
|
||||
financeWithdrawalAudit: "finance-withdrawal:audit",
|
||||
financeOrderCoinSellerRechargeView: "finance-order:coin-seller-recharge:view",
|
||||
financeOrderCoinSellerRechargeCreate: "finance-order:coin-seller-recharge:create",
|
||||
financeOrderCoinSellerRechargeVerify: "finance-order:coin-seller-recharge:verify",
|
||||
financeOrderCoinSellerRechargeGrant: "finance-order:coin-seller-recharge:grant",
|
||||
financeOperationUserCoinCredit: "finance-operation:user-coin-credit",
|
||||
financeOperationUserCoinDebit: "finance-operation:user-coin-debit",
|
||||
financeOperationUserWalletCredit: "finance-operation:user-wallet-credit",
|
||||
financeOperationUserWalletDebit: "finance-operation:user-wallet-debit",
|
||||
financeOperationCoinSellerCoinCredit: "finance-operation:coin-seller-coin-credit",
|
||||
financeOperationCoinSellerCoinDebit: "finance-operation:coin-seller-coin-debit",
|
||||
coinLedgerView: "coin-ledger:view",
|
||||
coinSellerLedgerView: "coin-seller-ledger:view",
|
||||
coinAdjustmentView: "coin-adjustment:view",
|
||||
|
||||
@ -21,6 +21,7 @@ import { ResourceGroupSelectField } from "@/shared/ui/ResourceGroupSelectDrawer.
|
||||
import { formatMillis } from "@/shared/utils/time.js";
|
||||
import { levelStatusLabels, levelTrackLabels, levelTrackOptions } from "@/features/level-config/constants.js";
|
||||
import { useLevelConfigPage } from "@/features/level-config/hooks/useLevelConfigPage.js";
|
||||
import { ResourceSelectField } from "@/features/resources/components/ResourceSelectDrawer.jsx";
|
||||
import styles from "@/features/level-config/level-config.module.css";
|
||||
|
||||
export function LevelConfigPage() {
|
||||
@ -467,32 +468,58 @@ function RuleFormDialog({ page }) {
|
||||
</AdminFormSection>
|
||||
<AdminFormSection title="展示与礼物">
|
||||
<AdminFormFieldGrid>
|
||||
<ResourceSelect
|
||||
<ResourceSelectField
|
||||
disabled={!page.abilities.canUpdate}
|
||||
drawerTitle="选择长徽章素材"
|
||||
label="长徽章素材"
|
||||
resources={levelBadgeResourcesForTrack(page.badgeResources, form.track)}
|
||||
placeholder="请选择长徽章素材"
|
||||
required={false}
|
||||
resources={withSelectedResource(
|
||||
levelBadgeResourcesForTrack(page.badgeResources, form.track),
|
||||
form.longBadgeResourceId,
|
||||
)}
|
||||
value={form.longBadgeResourceId}
|
||||
onChange={changeField(page.setRuleForm, "longBadgeResourceId")}
|
||||
onChange={(value) =>
|
||||
page.setRuleForm((current) => ({ ...current, longBadgeResourceId: value }))
|
||||
}
|
||||
/>
|
||||
<ResourceSelect
|
||||
<ResourceSelectField
|
||||
disabled={!page.abilities.canUpdate}
|
||||
drawerTitle="选择等级头像框素材"
|
||||
label="等级头像框素材"
|
||||
resources={levelAvatarFrameResourcesForTrack(page.avatarFrameResources, form.track)}
|
||||
placeholder="请选择等级头像框素材"
|
||||
required={false}
|
||||
resources={withSelectedResource(
|
||||
levelAvatarFrameResourcesForTrack(page.avatarFrameResources, form.track),
|
||||
form.avatarFrameResourceId,
|
||||
)}
|
||||
value={form.avatarFrameResourceId}
|
||||
onChange={changeField(page.setRuleForm, "avatarFrameResourceId")}
|
||||
onChange={(value) =>
|
||||
page.setRuleForm((current) => ({ ...current, avatarFrameResourceId: value }))
|
||||
}
|
||||
/>
|
||||
<ResourceSelect
|
||||
<ResourceSelectField
|
||||
disabled={!page.abilities.canUpdate}
|
||||
drawerTitle="选择短徽章素材"
|
||||
label="短徽章素材"
|
||||
resources={levelShortBadgeResourcesForTrack(page.badgeResources, form.track)}
|
||||
placeholder="请选择短徽章素材"
|
||||
required={false}
|
||||
resources={withSelectedResource(
|
||||
levelShortBadgeResourcesForTrack(page.badgeResources, form.track),
|
||||
form.shortBadgeResourceId,
|
||||
)}
|
||||
value={form.shortBadgeResourceId}
|
||||
onChange={changeField(page.setRuleForm, "shortBadgeResourceId")}
|
||||
onChange={(value) =>
|
||||
page.setRuleForm((current) => ({ ...current, shortBadgeResourceId: value }))
|
||||
}
|
||||
/>
|
||||
<ResourceGroupSelectField
|
||||
allowEmpty
|
||||
disabled={!page.abilities.canUpdate}
|
||||
drawerTitle="选择升级礼物"
|
||||
groups={page.resourceGroups}
|
||||
label="升级礼物"
|
||||
placeholder="请选择升级礼物"
|
||||
value={form.rewardResourceGroupId}
|
||||
onChange={(value) =>
|
||||
page.setRuleForm((current) => ({ ...current, rewardResourceGroupId: value }))
|
||||
@ -548,6 +575,7 @@ function TierFormDialog({ page }) {
|
||||
page.setTierForm((current) => ({
|
||||
...current,
|
||||
displayAvatarFrameResourceId: "",
|
||||
displayBadgeResourceId: "",
|
||||
track: event.target.value,
|
||||
}))
|
||||
}
|
||||
@ -584,25 +612,43 @@ function TierFormDialog({ page }) {
|
||||
</AdminFormSection>
|
||||
<AdminFormSection title="展示与礼物">
|
||||
<AdminFormFieldGrid>
|
||||
<ResourceSelect
|
||||
<ResourceSelectField
|
||||
disabled={!page.abilities.canUpdate}
|
||||
drawerTitle="选择头像框素材"
|
||||
label="头像框素材"
|
||||
resources={levelAvatarFrameResourcesForTrack(page.avatarFrameResources, form.track)}
|
||||
placeholder="请选择头像框素材"
|
||||
required={false}
|
||||
resources={withSelectedResource(
|
||||
levelAvatarFrameResourcesForTrack(page.avatarFrameResources, form.track),
|
||||
form.displayAvatarFrameResourceId,
|
||||
)}
|
||||
value={form.displayAvatarFrameResourceId}
|
||||
onChange={changeField(page.setTierForm, "displayAvatarFrameResourceId")}
|
||||
onChange={(value) =>
|
||||
page.setTierForm((current) => ({ ...current, displayAvatarFrameResourceId: value }))
|
||||
}
|
||||
/>
|
||||
<ResourceSelect
|
||||
<ResourceSelectField
|
||||
disabled={!page.abilities.canUpdate}
|
||||
drawerTitle="选择徽章素材"
|
||||
label="徽章素材"
|
||||
resources={page.badgeResources}
|
||||
placeholder="请选择徽章素材"
|
||||
required={false}
|
||||
resources={withSelectedResource(
|
||||
levelBadgeResourcesForTrack(page.badgeResources, form.track),
|
||||
form.displayBadgeResourceId,
|
||||
)}
|
||||
value={form.displayBadgeResourceId}
|
||||
onChange={changeField(page.setTierForm, "displayBadgeResourceId")}
|
||||
onChange={(value) =>
|
||||
page.setTierForm((current) => ({ ...current, displayBadgeResourceId: value }))
|
||||
}
|
||||
/>
|
||||
<ResourceGroupSelectField
|
||||
allowEmpty
|
||||
disabled={!page.abilities.canUpdate}
|
||||
drawerTitle="选择升级礼物"
|
||||
groups={page.resourceGroups}
|
||||
label="升级礼物"
|
||||
placeholder="请选择升级礼物"
|
||||
value={form.rewardResourceGroupId}
|
||||
onChange={(value) =>
|
||||
page.setTierForm((current) => ({ ...current, rewardResourceGroupId: value }))
|
||||
@ -668,20 +714,6 @@ function TrackFormDialog({ page }) {
|
||||
);
|
||||
}
|
||||
|
||||
function ResourceSelect({ disabled, label, onChange, resources, value }) {
|
||||
const options = withSelectedResource(resources, value);
|
||||
return (
|
||||
<TextField select disabled={disabled} label={label} value={String(value || "")} onChange={onChange}>
|
||||
<MenuItem value="">不配置</MenuItem>
|
||||
{options.map((resource) => (
|
||||
<MenuItem key={resource.resourceId} value={String(resource.resourceId)}>
|
||||
{resource.name || resource.resourceCode || resource.resourceId}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
);
|
||||
}
|
||||
|
||||
function ResourceLabel({ id, resources }) {
|
||||
if (!id) {
|
||||
return <span className={styles.emptyValue}>未配置</span>;
|
||||
|
||||
@ -25,7 +25,6 @@ export const API_OPERATIONS = {
|
||||
appListUserLoginLogs: "appListUserLoginLogs",
|
||||
appListUsers: "appListUsers",
|
||||
approveCoinSellerSubApplication: "approveCoinSellerSubApplication",
|
||||
approveFinanceApplication: "approveFinanceApplication",
|
||||
approveFinanceWithdrawalApplication: "approveFinanceWithdrawalApplication",
|
||||
appSetPassword: "appSetPassword",
|
||||
appUnbanUser: "appUnbanUser",
|
||||
@ -49,7 +48,6 @@ export const API_OPERATIONS = {
|
||||
createCountry: "createCountry",
|
||||
createEmojiPack: "createEmojiPack",
|
||||
createExploreTab: "createExploreTab",
|
||||
createFinanceApplication: "createFinanceApplication",
|
||||
createFinanceCoinSellerRechargeOrder: "createFinanceCoinSellerRechargeOrder",
|
||||
createFullServerNoticeFanout: "createFullServerNoticeFanout",
|
||||
createGift: "createGift",
|
||||
@ -196,7 +194,6 @@ export const API_OPERATIONS = {
|
||||
listEmojiPackCategories: "listEmojiPackCategories",
|
||||
listEmojiPacks: "listEmojiPacks",
|
||||
listExploreTabs: "listExploreTabs",
|
||||
listFinanceApplications: "listFinanceApplications",
|
||||
listFinanceCoinSellerRechargeOrders: "listFinanceCoinSellerRechargeOrders",
|
||||
listFinanceWithdrawalApplications: "listFinanceWithdrawalApplications",
|
||||
listFirstRechargeRewardClaims: "listFirstRechargeRewardClaims",
|
||||
@ -214,7 +211,6 @@ export const API_OPERATIONS = {
|
||||
listLuckyGiftConfigs: "listLuckyGiftConfigs",
|
||||
listLuckyGiftDraws: "listLuckyGiftDraws",
|
||||
listManagers: "listManagers",
|
||||
listMyFinanceApplications: "listMyFinanceApplications",
|
||||
listOperationLogs: "listOperationLogs",
|
||||
listPermissions: "listPermissions",
|
||||
listPlatformGrantRecords: "listPlatformGrantRecords",
|
||||
@ -275,7 +271,6 @@ export const API_OPERATIONS = {
|
||||
refresh: "refresh",
|
||||
refreshGoogleRechargePaid: "refreshGoogleRechargePaid",
|
||||
rejectCoinSellerSubApplication: "rejectCoinSellerSubApplication",
|
||||
rejectFinanceApplication: "rejectFinanceApplication",
|
||||
rejectFinanceWithdrawalApplication: "rejectFinanceWithdrawalApplication",
|
||||
renameCountryCode: "renameCountryCode",
|
||||
replaceCoinSellerSalaryRates: "replaceCoinSellerSalaryRates",
|
||||
@ -464,19 +459,12 @@ export const API_ENDPOINTS: Record<ApiOperationId, ApiEndpoint> = {
|
||||
permission: "coin-seller-sub-application:audit",
|
||||
permissions: ["coin-seller-sub-application:audit"]
|
||||
},
|
||||
approveFinanceApplication: {
|
||||
method: "POST",
|
||||
operationId: API_OPERATIONS.approveFinanceApplication,
|
||||
path: "/v1/admin/finance/applications/{application_id}/approve",
|
||||
permission: "finance-application:audit",
|
||||
permissions: ["finance-application:audit"]
|
||||
},
|
||||
approveFinanceWithdrawalApplication: {
|
||||
method: "POST",
|
||||
operationId: API_OPERATIONS.approveFinanceWithdrawalApplication,
|
||||
path: "/v1/admin/finance/withdrawal-applications/{application_id}/approve",
|
||||
permission: "finance-withdrawal:audit",
|
||||
permissions: ["finance-withdrawal:audit","finance-application:audit"]
|
||||
permissions: ["finance-withdrawal:audit"]
|
||||
},
|
||||
appSetPassword: {
|
||||
method: "POST",
|
||||
@ -630,13 +618,6 @@ export const API_ENDPOINTS: Record<ApiOperationId, ApiEndpoint> = {
|
||||
permission: "app-config:update",
|
||||
permissions: ["app-config:update"]
|
||||
},
|
||||
createFinanceApplication: {
|
||||
method: "POST",
|
||||
operationId: API_OPERATIONS.createFinanceApplication,
|
||||
path: "/v1/admin/finance/applications",
|
||||
permission: "finance-application:create",
|
||||
permissions: ["finance-application:create"]
|
||||
},
|
||||
createFinanceCoinSellerRechargeOrder: {
|
||||
method: "POST",
|
||||
operationId: API_OPERATIONS.createFinanceCoinSellerRechargeOrder,
|
||||
@ -1657,13 +1638,6 @@ export const API_ENDPOINTS: Record<ApiOperationId, ApiEndpoint> = {
|
||||
permission: "app-config:view",
|
||||
permissions: ["app-config:view"]
|
||||
},
|
||||
listFinanceApplications: {
|
||||
method: "GET",
|
||||
operationId: API_OPERATIONS.listFinanceApplications,
|
||||
path: "/v1/admin/finance/applications",
|
||||
permission: "finance-application:audit",
|
||||
permissions: ["finance-application:audit"]
|
||||
},
|
||||
listFinanceCoinSellerRechargeOrders: {
|
||||
method: "GET",
|
||||
operationId: API_OPERATIONS.listFinanceCoinSellerRechargeOrders,
|
||||
@ -1676,7 +1650,7 @@ export const API_ENDPOINTS: Record<ApiOperationId, ApiEndpoint> = {
|
||||
operationId: API_OPERATIONS.listFinanceWithdrawalApplications,
|
||||
path: "/v1/admin/finance/withdrawal-applications",
|
||||
permission: "finance-withdrawal:view",
|
||||
permissions: ["finance-withdrawal:view","finance-withdrawal:audit","finance-application:audit"]
|
||||
permissions: ["finance-withdrawal:view","finance-withdrawal:audit"]
|
||||
},
|
||||
listFirstRechargeRewardClaims: {
|
||||
method: "GET",
|
||||
@ -1783,13 +1757,6 @@ export const API_ENDPOINTS: Record<ApiOperationId, ApiEndpoint> = {
|
||||
permission: "bd:view",
|
||||
permissions: ["bd:view"]
|
||||
},
|
||||
listMyFinanceApplications: {
|
||||
method: "GET",
|
||||
operationId: API_OPERATIONS.listMyFinanceApplications,
|
||||
path: "/v1/admin/finance/applications/mine",
|
||||
permission: "finance-application:create",
|
||||
permissions: ["finance-application:create"]
|
||||
},
|
||||
listOperationLogs: {
|
||||
method: "GET",
|
||||
operationId: API_OPERATIONS.listOperationLogs,
|
||||
@ -2200,19 +2167,12 @@ export const API_ENDPOINTS: Record<ApiOperationId, ApiEndpoint> = {
|
||||
permission: "coin-seller-sub-application:audit",
|
||||
permissions: ["coin-seller-sub-application:audit"]
|
||||
},
|
||||
rejectFinanceApplication: {
|
||||
method: "POST",
|
||||
operationId: API_OPERATIONS.rejectFinanceApplication,
|
||||
path: "/v1/admin/finance/applications/{application_id}/reject",
|
||||
permission: "finance-application:audit",
|
||||
permissions: ["finance-application:audit"]
|
||||
},
|
||||
rejectFinanceWithdrawalApplication: {
|
||||
method: "POST",
|
||||
operationId: API_OPERATIONS.rejectFinanceWithdrawalApplication,
|
||||
path: "/v1/admin/finance/withdrawal-applications/{application_id}/reject",
|
||||
permission: "finance-withdrawal:audit",
|
||||
permissions: ["finance-withdrawal:audit","finance-application:audit"]
|
||||
permissions: ["finance-withdrawal:audit"]
|
||||
},
|
||||
renameCountryCode: {
|
||||
method: "POST",
|
||||
|
||||
219
src/shared/api/generated/schema.d.ts
vendored
219
src/shared/api/generated/schema.d.ts
vendored
@ -2380,70 +2380,6 @@ export interface paths {
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/admin/finance/applications": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
get: operations["listFinanceApplications"];
|
||||
put?: never;
|
||||
post: operations["createFinanceApplication"];
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/admin/finance/applications/mine": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
get: operations["listMyFinanceApplications"];
|
||||
put?: never;
|
||||
post?: never;
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/admin/finance/applications/{application_id}/approve": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
get?: never;
|
||||
put?: never;
|
||||
post: operations["approveFinanceApplication"];
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/admin/finance/applications/{application_id}/reject": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
get?: never;
|
||||
put?: never;
|
||||
post: operations["rejectFinanceApplication"];
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/admin/finance/withdrawal-applications": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
@ -4863,12 +4799,6 @@ export interface components {
|
||||
pageSize: number;
|
||||
total: number;
|
||||
};
|
||||
ApiPageFinanceApplication: {
|
||||
items: components["schemas"]["FinanceApplication"][];
|
||||
page: number;
|
||||
pageSize: number;
|
||||
total: number;
|
||||
};
|
||||
ApiPageFinanceCoinSellerRechargeOrder: {
|
||||
items: components["schemas"]["FinanceCoinSellerRechargeOrder"][];
|
||||
page: number;
|
||||
@ -5095,12 +5025,6 @@ export interface components {
|
||||
/** Format: int64 */
|
||||
region_id?: number;
|
||||
};
|
||||
ApiResponseFinanceApplication: components["schemas"]["Envelope"] & {
|
||||
data?: components["schemas"]["FinanceApplication"];
|
||||
};
|
||||
ApiResponseFinanceApplicationPage: components["schemas"]["Envelope"] & {
|
||||
data?: components["schemas"]["ApiPageFinanceApplication"];
|
||||
};
|
||||
ApiResponseFinanceCoinSellerRechargeOrder: components["schemas"]["Envelope"] & {
|
||||
data?: components["schemas"]["FinanceCoinSellerRechargeOrder"];
|
||||
};
|
||||
@ -5736,30 +5660,6 @@ export interface components {
|
||||
countryName?: string;
|
||||
countryDisplayName?: string;
|
||||
};
|
||||
FinanceApplication: {
|
||||
id: number;
|
||||
appCode: string;
|
||||
appName?: string;
|
||||
/** @enum {string} */
|
||||
operation: "user_coin_credit" | "user_coin_debit" | "user_wallet_debit" | "user_wallet_credit" | "coin_seller_coin_credit" | "coin_seller_coin_debit";
|
||||
walletIdentity?: string;
|
||||
targetUserId: string;
|
||||
coinAmount: number;
|
||||
rechargeAmount: string;
|
||||
credentialImageUrl?: string;
|
||||
credentialText?: string;
|
||||
applicantUserId: number;
|
||||
applicantName?: string;
|
||||
auditorUserId?: number | null;
|
||||
auditorName?: string;
|
||||
/** @enum {string} */
|
||||
status: "pending" | "approved" | "rejected";
|
||||
auditRemark?: string;
|
||||
auditedAtMs?: number | null;
|
||||
createdAtMs: number;
|
||||
updatedAtMs: number;
|
||||
dingTalkNotified?: boolean;
|
||||
};
|
||||
FinanceCoinSellerRechargeOrder: {
|
||||
id: string;
|
||||
appCode: string;
|
||||
@ -5824,18 +5724,6 @@ export interface components {
|
||||
createdAtMs: number;
|
||||
updatedAtMs?: number;
|
||||
};
|
||||
FinanceApplicationInput: {
|
||||
appCode: string;
|
||||
/** @enum {string} */
|
||||
operation: "user_coin_credit" | "user_coin_debit" | "user_wallet_debit" | "user_wallet_credit" | "coin_seller_coin_credit" | "coin_seller_coin_debit";
|
||||
/** @enum {string} */
|
||||
walletIdentity?: "host" | "agency" | "bd";
|
||||
targetUserId: string;
|
||||
coinAmount: number;
|
||||
rechargeAmount: number;
|
||||
credentialImageUrl?: string;
|
||||
credentialText?: string;
|
||||
};
|
||||
FinanceCoinSellerRechargeOrderInput: {
|
||||
appCode: string;
|
||||
targetUserId: string;
|
||||
@ -6236,24 +6124,6 @@ export interface components {
|
||||
};
|
||||
};
|
||||
/** @description OK */
|
||||
FinanceApplicationPageResponse: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["ApiResponseFinanceApplicationPage"];
|
||||
};
|
||||
};
|
||||
/** @description OK */
|
||||
FinanceApplicationResponse: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["ApiResponseFinanceApplication"];
|
||||
};
|
||||
};
|
||||
/** @description OK */
|
||||
FinanceCoinSellerRechargeOrderPageResponse: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
@ -6576,11 +6446,6 @@ export interface components {
|
||||
};
|
||||
};
|
||||
};
|
||||
FinanceApplicationRequest: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["FinanceApplicationInput"];
|
||||
};
|
||||
};
|
||||
FinanceCoinSellerRechargeOrderRequest: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["FinanceCoinSellerRechargeOrderInput"];
|
||||
@ -6601,7 +6466,7 @@ export interface components {
|
||||
"application/json": components["schemas"]["FinanceCoinSellerRechargeReceiptVerificationInput"];
|
||||
};
|
||||
};
|
||||
FinanceApplicationAuditRequest: {
|
||||
FinanceWithdrawalAuditRequest: {
|
||||
content: {
|
||||
"application/json": {
|
||||
auditRemark?: string;
|
||||
@ -9686,84 +9551,6 @@ export interface operations {
|
||||
200: components["responses"]["FinanceScopeCatalogResponse"];
|
||||
};
|
||||
};
|
||||
listFinanceApplications: {
|
||||
parameters: {
|
||||
query?: {
|
||||
app_code?: string;
|
||||
operation?: string;
|
||||
status?: string;
|
||||
keyword?: string;
|
||||
page?: number;
|
||||
page_size?: number;
|
||||
};
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
200: components["responses"]["FinanceApplicationPageResponse"];
|
||||
};
|
||||
};
|
||||
createFinanceApplication: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: components["requestBodies"]["FinanceApplicationRequest"];
|
||||
responses: {
|
||||
201: components["responses"]["FinanceApplicationResponse"];
|
||||
};
|
||||
};
|
||||
listMyFinanceApplications: {
|
||||
parameters: {
|
||||
query?: {
|
||||
app_code?: string;
|
||||
operation?: string;
|
||||
status?: string;
|
||||
keyword?: string;
|
||||
page?: number;
|
||||
page_size?: number;
|
||||
};
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
200: components["responses"]["FinanceApplicationPageResponse"];
|
||||
};
|
||||
};
|
||||
approveFinanceApplication: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path: {
|
||||
application_id: number;
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: components["requestBodies"]["FinanceApplicationAuditRequest"];
|
||||
responses: {
|
||||
200: components["responses"]["FinanceApplicationResponse"];
|
||||
};
|
||||
};
|
||||
rejectFinanceApplication: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path: {
|
||||
application_id: number;
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: components["requestBodies"]["FinanceApplicationAuditRequest"];
|
||||
responses: {
|
||||
200: components["responses"]["FinanceApplicationResponse"];
|
||||
};
|
||||
};
|
||||
listFinanceWithdrawalApplications: {
|
||||
parameters: {
|
||||
query?: {
|
||||
@ -9790,7 +9577,7 @@ export interface operations {
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: components["requestBodies"]["FinanceApplicationAuditRequest"];
|
||||
requestBody?: components["requestBodies"]["FinanceWithdrawalAuditRequest"];
|
||||
responses: {
|
||||
200: components["responses"]["FinanceWithdrawalApplicationResponse"];
|
||||
};
|
||||
@ -9804,7 +9591,7 @@ export interface operations {
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: components["requestBodies"]["FinanceApplicationAuditRequest"];
|
||||
requestBody?: components["requestBodies"]["FinanceWithdrawalAuditRequest"];
|
||||
responses: {
|
||||
200: components["responses"]["FinanceWithdrawalApplicationResponse"];
|
||||
};
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user