财务口径

This commit is contained in:
zhx 2026-07-16 14:35:29 +08:00
parent 31804f5e34
commit ad4d4151f5
12 changed files with 492 additions and 22 deletions

View File

@ -4506,6 +4506,98 @@
"x-permissions": ["finance-withdrawal:audit"] "x-permissions": ["finance-withdrawal:audit"]
} }
}, },
"/admin/operations/withdrawal-applications": {
"get": {
"operationId": "listOperationsWithdrawalApplications",
"parameters": [
{
"in": "query",
"name": "app_code",
"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/FinanceWithdrawalApplicationPageResponse"
}
},
"x-permission": "operations-withdrawal:view",
"x-permissions": ["operations-withdrawal:view", "operations-withdrawal:audit"]
}
},
"/admin/operations/withdrawal-applications/{application_id}/approve": {
"post": {
"operationId": "approveOperationsWithdrawalApplication",
"parameters": [
{
"in": "path",
"name": "application_id",
"required": true,
"schema": {
"type": "integer"
}
}
],
"requestBody": {
"$ref": "#/components/requestBodies/OperationsWithdrawalAuditRequest"
},
"responses": {
"200": {
"$ref": "#/components/responses/FinanceWithdrawalApplicationResponse"
}
},
"x-permission": "operations-withdrawal:audit",
"x-permissions": ["operations-withdrawal:audit"]
}
},
"/admin/operations/withdrawal-applications/{application_id}/reject": {
"post": {
"operationId": "rejectOperationsWithdrawalApplication",
"parameters": [
{
"in": "path",
"name": "application_id",
"required": true,
"schema": {
"type": "integer"
}
}
],
"requestBody": {
"$ref": "#/components/requestBodies/OperationsWithdrawalAuditRequest"
},
"responses": {
"200": {
"$ref": "#/components/responses/FinanceWithdrawalApplicationResponse"
}
},
"x-permission": "operations-withdrawal:audit",
"x-permissions": ["operations-withdrawal:audit"]
}
},
"/admin/finance/orders/coin-seller-recharges": { "/admin/finance/orders/coin-seller-recharges": {
"get": { "get": {
"operationId": "listFinanceCoinSellerRechargeOrders", "operationId": "listFinanceCoinSellerRechargeOrders",
@ -8640,6 +8732,20 @@
} }
} }
}, },
"OperationsWithdrawalAuditRequest": {
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"auditRemark": {
"type": "string"
}
}
}
}
}
},
"MenuRequest": { "MenuRequest": {
"content": { "content": {
"application/json": { "application/json": {
@ -14055,6 +14161,27 @@
"status": { "status": {
"type": "string" "type": "string"
}, },
"operationsStatus": {
"enum": ["pending", "approved", "rejected"],
"type": "string"
},
"operationsReviewerUserId": {
"nullable": true,
"type": "integer"
},
"operationsReviewerName": {
"type": "string"
},
"operationsAuditRemark": {
"type": "string"
},
"operationsAuditTransactionId": {
"type": "string"
},
"operationsReviewedAtMs": {
"nullable": true,
"type": "integer"
},
"approverUserId": { "approverUserId": {
"nullable": true, "nullable": true,
"type": "integer" "type": "integer"

View File

@ -1,6 +1,7 @@
import { useCallback, useEffect, useMemo, useState } from "react"; import { useCallback, useEffect, useMemo, useState } from "react";
import { import {
approveFinanceWithdrawalApplication, approveFinanceWithdrawalApplication,
approveOperationsWithdrawalApplication,
createFinanceCoinSellerRechargeOrder, createFinanceCoinSellerRechargeOrder,
exportFinanceRechargeBills, exportFinanceRechargeBills,
fetchFinanceOptions, fetchFinanceOptions,
@ -15,9 +16,11 @@ import {
listFinanceRechargeDetails, listFinanceRechargeDetails,
listFinanceRechargeRegions, listFinanceRechargeRegions,
listFinanceWithdrawalApplications, listFinanceWithdrawalApplications,
listOperationsWithdrawalApplications,
refreshFinanceGoogleRechargePaid, refreshFinanceGoogleRechargePaid,
replaceFinanceCoinSellerRechargeExchangeRate, replaceFinanceCoinSellerRechargeExchangeRate,
rejectFinanceWithdrawalApplication, rejectFinanceWithdrawalApplication,
rejectOperationsWithdrawalApplication,
quoteFinanceCoinSellerRechargeOrder, quoteFinanceCoinSellerRechargeOrder,
upsertFinanceUSDTAddress, upsertFinanceUSDTAddress,
verifyFinanceCoinSellerRechargeReceipt, verifyFinanceCoinSellerRechargeReceipt,
@ -41,7 +44,63 @@ const emptyOptions = { apps: [] };
const pageSize = 50; const pageSize = 50;
const DAY_MS = 86_400_000; const DAY_MS = 86_400_000;
const CHINA_OFFSET_MS = 8 * 3_600_000; const CHINA_OFFSET_MS = 8 * 3_600_000;
const FINANCE_VIEWS = new Set(["overview", "rechargeDetails", "recon"]); const RECHARGE_VIEWS = new Set(["overview", "rechargeDetails", "recon"]);
const FINANCE_VIEW_ALIASES = new Map([["withdrawals", "withdrawalFinance"]]);
function requestedFinanceView() {
const requested = new URLSearchParams(window.location.search).get("view") || "";
return FINANCE_VIEW_ALIASES.get(requested) || requested;
}
function canAccessFinanceView(session, view) {
if (RECHARGE_VIEWS.has(view)) {
return Boolean(session?.canViewAppRechargeDetails);
}
if (view === "coinSellerRechargeOrders") {
return Boolean(session?.canViewCoinSellerRechargeOrders);
}
if (view === "withdrawalOperations") {
return Boolean(session?.canViewOperationsWithdrawalApplications);
}
if (view === "withdrawalFinance") {
return Boolean(session?.canViewWithdrawalApplications);
}
return false;
}
function defaultFinanceView(session) {
if (session?.canViewAppRechargeDetails) {
return "overview";
}
if (session?.canViewCoinSellerRechargeOrders) {
return "coinSellerRechargeOrders";
}
if (session?.canViewOperationsWithdrawalApplications) {
return "withdrawalOperations";
}
if (session?.canViewWithdrawalApplications) {
return "withdrawalFinance";
}
return "";
}
function resolvedFinanceView(session) {
const requested = requestedFinanceView();
return canAccessFinanceView(session, requested) ? requested : defaultFinanceView(session);
}
function syncFinanceView(view) {
if (!view) {
return;
}
const url = new URL(window.location.href);
if (url.searchParams.get("view") === view) {
return;
}
// HTML query hash
url.searchParams.set("view", view);
window.history.replaceState(window.history.state, "", `${url.pathname}${url.search}${url.hash}`);
}
function chinaDayStart(timestamp) { function chinaDayStart(timestamp) {
return Math.floor((timestamp + CHINA_OFFSET_MS) / DAY_MS) * DAY_MS - CHINA_OFFSET_MS; return Math.floor((timestamp + CHINA_OFFSET_MS) / DAY_MS) * DAY_MS - CHINA_OFFSET_MS;

View File

@ -29,6 +29,8 @@ export async function fetchFinanceSession() {
const permissions = Array.isArray(session.permissions) ? session.permissions : []; const permissions = Array.isArray(session.permissions) ? session.permissions : [];
const canAuditWithdrawalApplications = permissions.includes(FINANCE_PERMISSIONS.auditWithdrawalApplications); const canAuditWithdrawalApplications = permissions.includes(FINANCE_PERMISSIONS.auditWithdrawalApplications);
const canViewWithdrawalApplications = permissions.includes(FINANCE_PERMISSIONS.viewWithdrawalApplications) || canAuditWithdrawalApplications; const canViewWithdrawalApplications = permissions.includes(FINANCE_PERMISSIONS.viewWithdrawalApplications) || canAuditWithdrawalApplications;
const canAuditOperationsWithdrawalApplications = permissions.includes(FINANCE_PERMISSIONS.auditOperationsWithdrawalApplications);
const canViewOperationsWithdrawalApplications = permissions.includes(FINANCE_PERMISSIONS.viewOperationsWithdrawalApplications) || canAuditOperationsWithdrawalApplications;
const canViewAppRechargeDetails = permissions.includes(FINANCE_PERMISSIONS.viewAppRechargeDetails); const canViewAppRechargeDetails = permissions.includes(FINANCE_PERMISSIONS.viewAppRechargeDetails);
const canViewCoinSellerRechargeOrders = permissions.includes(FINANCE_PERMISSIONS.viewCoinSellerRechargeOrders); const canViewCoinSellerRechargeOrders = permissions.includes(FINANCE_PERMISSIONS.viewCoinSellerRechargeOrders);
const canCreateCoinSellerRechargeOrder = permissions.includes(FINANCE_PERMISSIONS.createCoinSellerRechargeOrder); const canCreateCoinSellerRechargeOrder = permissions.includes(FINANCE_PERMISSIONS.createCoinSellerRechargeOrder);
@ -41,8 +43,10 @@ export async function fetchFinanceSession() {
...session, ...session,
canAccessFinance: canAccessFinance:
canViewWithdrawalApplications || canViewWithdrawalApplications ||
canViewOperationsWithdrawalApplications ||
canViewAppRechargeDetails || canViewAppRechargeDetails ||
canViewCoinSellerRechargeOrders, canViewCoinSellerRechargeOrders,
canAuditOperationsWithdrawalApplications,
canAuditWithdrawalApplications, canAuditWithdrawalApplications,
canConfigureCoinSellerRechargeExchangeRate, canConfigureCoinSellerRechargeExchangeRate,
canCreateCoinSellerRechargeOrder, canCreateCoinSellerRechargeOrder,
@ -51,6 +55,7 @@ export async function fetchFinanceSession() {
canVerifyCoinSellerRechargeOrder, canVerifyCoinSellerRechargeOrder,
canViewAppRechargeDetails, canViewAppRechargeDetails,
canViewCoinSellerRechargeOrders, canViewCoinSellerRechargeOrders,
canViewOperationsWithdrawalApplications,
canViewWithdrawalApplications, canViewWithdrawalApplications,
permissions permissions
}; };
@ -82,6 +87,15 @@ export async function listFinanceWithdrawalApplications(query = {}) {
return normalizeWithdrawalApplicationPage(data); return normalizeWithdrawalApplicationPage(data);
} }
export async function listOperationsWithdrawalApplications(query = {}) {
const endpoint = API_ENDPOINTS.listOperationsWithdrawalApplications;
const data = await apiRequest(apiEndpointPath(API_OPERATIONS.listOperationsWithdrawalApplications), {
method: endpoint.method,
query
});
return normalizeWithdrawalApplicationPage(data);
}
export async function listFinanceCoinSellerRechargeOrders(query = {}) { export async function listFinanceCoinSellerRechargeOrders(query = {}) {
const endpoint = API_ENDPOINTS.listFinanceCoinSellerRechargeOrders; const endpoint = API_ENDPOINTS.listFinanceCoinSellerRechargeOrders;
const data = await apiRequest(apiEndpointPath(API_OPERATIONS.listFinanceCoinSellerRechargeOrders), { const data = await apiRequest(apiEndpointPath(API_OPERATIONS.listFinanceCoinSellerRechargeOrders), {
@ -305,6 +319,22 @@ export async function rejectFinanceWithdrawalApplication(applicationId, payload)
}); });
} }
export async function approveOperationsWithdrawalApplication(applicationId, payload) {
const endpoint = API_ENDPOINTS.approveOperationsWithdrawalApplication;
return apiRequest(apiEndpointPath(API_OPERATIONS.approveOperationsWithdrawalApplication, { application_id: applicationId }), {
body: payload,
method: endpoint.method
});
}
export async function rejectOperationsWithdrawalApplication(applicationId, payload) {
const endpoint = API_ENDPOINTS.rejectOperationsWithdrawalApplication;
return apiRequest(apiEndpointPath(API_OPERATIONS.rejectOperationsWithdrawalApplication, { application_id: applicationId }), {
body: payload,
method: endpoint.method
});
}
async function currentSession() { async function currentSession() {
try { try {
return await refreshSession(); return await refreshSession();

View File

@ -8,7 +8,17 @@ import { useEffect, useState } from "react";
import { AppIdentity } from "@/shared/ui/AppIdentity.jsx"; import { AppIdentity } from "@/shared/ui/AppIdentity.jsx";
import { UploadField } from "@/shared/ui/UploadField.jsx"; import { UploadField } from "@/shared/ui/UploadField.jsx";
export function FinanceAuditDialog({ application, apps = [], loading, onClose, onSubmit, open, requireApprovalImage = false, requireRejectRemark = false }) { export function FinanceAuditDialog({
application,
apps = [],
loading,
onClose,
onSubmit,
open,
requireApprovalImage = false,
requireRejectRemark = false,
reviewLabel = "财务",
}) {
const [remark, setRemark] = useState(""); const [remark, setRemark] = useState("");
const [auditImageUrl, setAuditImageUrl] = useState(""); const [auditImageUrl, setAuditImageUrl] = useState("");
const decision = application?.decision || "approved"; const decision = application?.decision || "approved";
@ -29,7 +39,7 @@ export function FinanceAuditDialog({ application, apps = [], loading, onClose, o
return ( return (
<Dialog fullWidth maxWidth="sm" open={open} onClose={loading ? undefined : onClose}> <Dialog fullWidth maxWidth="sm" open={open} onClose={loading ? undefined : onClose}>
<DialogTitle>{decision === "approved" ? "通过申请" : "拒绝申请"}</DialogTitle> <DialogTitle>{decision === "approved" ? `通过${reviewLabel}审核` : `拒绝${reviewLabel}审核`}</DialogTitle>
<DialogContent className="finance-audit-dialog"> <DialogContent className="finance-audit-dialog">
<div className="finance-audit-summary"> <div className="finance-audit-summary">
<AppIdentity <AppIdentity

View File

@ -19,7 +19,10 @@ const NAV_GROUPS = [
}, },
{ {
label: "审批", label: "审批",
views: [{ icon: PaymentsOutlined, id: "withdrawals", label: "用户提现申请" }], views: [
{ icon: PaymentsOutlined, id: "withdrawalOperations", label: "用户提现运营审核" },
{ icon: PaymentsOutlined, id: "withdrawalFinance", label: "用户提现财务审核" },
],
}, },
]; ];
@ -28,7 +31,8 @@ const VIEW_TITLES = {
overview: "财务概览", overview: "财务概览",
recon: "渠道对账", recon: "渠道对账",
rechargeDetails: "充值流水", rechargeDetails: "充值流水",
withdrawals: "用户提现申请", withdrawalFinance: "用户提现财务审核",
withdrawalOperations: "用户提现运营审核",
}; };
export function FinanceShell({ export function FinanceShell({
@ -36,7 +40,8 @@ export function FinanceShell({
appBar, appBar,
canViewCoinSellerRechargeOrders, canViewCoinSellerRechargeOrders,
canViewRechargeDetails, canViewRechargeDetails,
canViewWithdrawals, canViewFinanceWithdrawals,
canViewOperationsWithdrawals,
children, children,
loading, loading,
onViewChange, onViewChange,
@ -51,8 +56,11 @@ export function FinanceShell({
if (viewId === "coinSellerRechargeOrders") { if (viewId === "coinSellerRechargeOrders") {
return canViewCoinSellerRechargeOrders; return canViewCoinSellerRechargeOrders;
} }
if (viewId === "withdrawals") { if (viewId === "withdrawalFinance") {
return canViewWithdrawals; return canViewFinanceWithdrawals;
}
if (viewId === "withdrawalOperations") {
return canViewOperationsWithdrawals;
} }
return false; return false;
}; };

View File

@ -15,8 +15,20 @@ import { AppIdentity } from "@/shared/ui/AppIdentity.jsx";
import { useToast } from "@/shared/ui/ToastProvider.jsx"; import { useToast } from "@/shared/ui/ToastProvider.jsx";
import { formatAmount, formatTime, statusLabel } from "../format.js"; import { formatAmount, formatTime, statusLabel } from "../format.js";
export function FinanceWithdrawalApplicationList({ applications, canAudit, error, filters, loading, onAudit, onFiltersChange, onReload, options }) { export function FinanceWithdrawalApplicationList({
applications,
canAudit,
error,
filters,
loading,
onAudit,
onFiltersChange,
onReload,
options,
reviewStage = "finance",
}) {
const { showToast } = useToast(); const { showToast } = useToast();
const isOperationsReview = reviewStage === "operations";
const items = applications.items || []; const items = applications.items || [];
const page = Number(applications.page || filters.page || 1); const page = Number(applications.page || filters.page || 1);
const pageSize = Number(applications.pageSize || 50); const pageSize = Number(applications.pageSize || 50);
@ -66,9 +78,20 @@ export function FinanceWithdrawalApplicationList({ applications, canAudit, error
<TableCell>提现方式</TableCell> <TableCell>提现方式</TableCell>
<TableCell>提现地址</TableCell> <TableCell>提现地址</TableCell>
<TableCell className="finance-time-cell">申请时间</TableCell> <TableCell className="finance-time-cell">申请时间</TableCell>
<TableCell>审批人</TableCell> {isOperationsReview ? (
<TableCell className="finance-time-cell">审批时间</TableCell> <>
<TableCell>审核原因</TableCell> <TableCell>运营审核人</TableCell>
<TableCell className="finance-time-cell">运营审核时间</TableCell>
<TableCell>运营审核原因</TableCell>
</>
) : (
<>
<TableCell>运营审核</TableCell>
<TableCell>财务审核人</TableCell>
<TableCell className="finance-time-cell">财务审核时间</TableCell>
<TableCell>财务审核原因</TableCell>
</>
)}
<TableCell align="right">操作</TableCell> <TableCell align="right">操作</TableCell>
</TableRow> </TableRow>
</TableHead> </TableHead>
@ -104,30 +127,73 @@ export function FinanceWithdrawalApplicationList({ applications, canAudit, error
) : "-"} ) : "-"}
</TableCell> </TableCell>
<TableCell className="finance-time-cell">{formatTime(item.createdAtMs)}</TableCell> <TableCell className="finance-time-cell">{formatTime(item.createdAtMs)}</TableCell>
{isOperationsReview ? (
<>
<TableCell>
<Stacked
primary={item.operationsReviewerName || "-"}
secondary={item.operationsReviewerUserId || ""}
/>
</TableCell>
<TableCell className="finance-time-cell">{formatTime(item.operationsReviewedAtMs)}</TableCell>
<TableCell>{item.operationsAuditRemark || "-"}</TableCell>
</>
) : (
<>
<TableCell>
<ReviewContext application={item} />
</TableCell>
<TableCell> <TableCell>
<Stacked primary={item.approverName || "-"} secondary={item.approverUserId || ""} /> <Stacked primary={item.approverName || "-"} secondary={item.approverUserId || ""} />
</TableCell> </TableCell>
<TableCell className="finance-time-cell">{formatTime(item.approvedAtMs)}</TableCell> <TableCell className="finance-time-cell">{formatTime(item.approvedAtMs)}</TableCell>
<TableCell>{item.auditRemark || "-"}</TableCell> <TableCell>{item.auditRemark || "-"}</TableCell>
</>
)}
<TableCell align="right"> <TableCell align="right">
{item.status === "pending" && canAudit ? ( {(isOperationsReview ? item.operationsStatus : item.status) === "pending" && canAudit ? (
<span className="finance-row-actions"> <span className="finance-row-actions">
<Button size="small" variant="contained" onClick={() => onAudit({ ...item, decision: "approved", operation: "用户提现", targetUserId: item.userId })}> <Button
size="small"
variant="contained"
onClick={() =>
onAudit({
...item,
decision: "approved",
operation: isOperationsReview ? "用户提现运营审核" : "用户提现财务审核",
reviewStage,
targetUserId: item.userId,
})
}
>
通过 通过
</Button> </Button>
<Button color="error" size="small" variant="outlined" onClick={() => onAudit({ ...item, decision: "rejected", operation: "用户提现", targetUserId: item.userId })}> <Button
color="error"
size="small"
variant="outlined"
onClick={() =>
onAudit({
...item,
decision: "rejected",
operation: isOperationsReview ? "用户提现运营审核" : "用户提现财务审核",
reviewStage,
targetUserId: item.userId,
})
}
>
拒绝 拒绝
</Button> </Button>
</span> </span>
) : ( ) : (
statusLabel(item.status) statusLabel(isOperationsReview ? item.operationsStatus : item.status)
)} )}
</TableCell> </TableCell>
</TableRow> </TableRow>
)) ))
) : ( ) : (
<TableRow> <TableRow>
<TableCell align="center" colSpan={10}> <TableCell align="center" colSpan={isOperationsReview ? 10 : 11}>
当前无数据 当前无数据
</TableCell> </TableCell>
</TableRow> </TableRow>
@ -153,6 +219,19 @@ export function FinanceWithdrawalApplicationList({ applications, canAudit, error
); );
} }
function ReviewContext({ application }) {
return (
<span className="finance-review-context">
<Stacked
primary={application.operationsReviewerName || statusLabel(application.operationsStatus)}
secondary={application.operationsReviewerUserId || ""}
/>
<small>{formatTime(application.operationsReviewedAtMs)}</small>
<small title={application.operationsAuditRemark || ""}>{application.operationsAuditRemark || "-"}</small>
</span>
);
}
function Stacked({ primary, secondary }) { function Stacked({ primary, secondary }) {
return ( return (
<span className="finance-stack"> <span className="finance-stack">

View File

@ -1,5 +1,6 @@
export const FINANCE_PERMISSIONS = { export const FINANCE_PERMISSIONS = {
auditWithdrawalApplications: "finance-withdrawal:audit", auditWithdrawalApplications: "finance-withdrawal:audit",
auditOperationsWithdrawalApplications: "operations-withdrawal:audit",
configureCoinSellerRechargeExchangeRate: "coin-seller:exchange-rate", configureCoinSellerRechargeExchangeRate: "coin-seller:exchange-rate",
createCoinSellerRechargeOrder: "finance-order:coin-seller-recharge:create", createCoinSellerRechargeOrder: "finance-order:coin-seller-recharge:create",
grantCoinSellerRechargeOrder: "finance-order:coin-seller-recharge:grant", grantCoinSellerRechargeOrder: "finance-order:coin-seller-recharge:grant",
@ -7,6 +8,7 @@ export const FINANCE_PERMISSIONS = {
verifyCoinSellerRechargeOrder: "finance-order:coin-seller-recharge:verify", verifyCoinSellerRechargeOrder: "finance-order:coin-seller-recharge:verify",
viewCoinSellerRechargeOrders: "finance-order:coin-seller-recharge:view", viewCoinSellerRechargeOrders: "finance-order:coin-seller-recharge:view",
viewAppRechargeDetails: "payment-bill:view", viewAppRechargeDetails: "payment-bill:view",
viewOperationsWithdrawalApplications: "operations-withdrawal:view",
viewWithdrawalApplications: "finance-withdrawal:view", viewWithdrawalApplications: "finance-withdrawal:view",
}; };

View File

@ -22,6 +22,14 @@ export function normalizeWithdrawalApplication(item = {}) {
createdAtMs: numberOrNull(item.createdAtMs ?? item.created_at_ms), createdAtMs: numberOrNull(item.createdAtMs ?? item.created_at_ms),
freezeTransactionId: stringValue(item.freezeTransactionId ?? item.freeze_transaction_id), freezeTransactionId: stringValue(item.freezeTransactionId ?? item.freeze_transaction_id),
id: item.id ?? item.withdrawalApplicationId ?? item.withdrawal_application_id, id: item.id ?? item.withdrawalApplicationId ?? item.withdrawal_application_id,
operationsAuditRemark: stringValue(item.operationsAuditRemark ?? item.operations_audit_remark),
operationsAuditTransactionId: stringValue(
item.operationsAuditTransactionId ?? item.operations_audit_transaction_id,
),
operationsReviewedAtMs: numberOrNull(item.operationsReviewedAtMs ?? item.operations_reviewed_at_ms),
operationsReviewerName: stringValue(item.operationsReviewerName ?? item.operations_reviewer_name),
operationsReviewerUserId: item.operationsReviewerUserId ?? item.operations_reviewer_user_id,
operationsStatus: stringValue(item.operationsStatus ?? item.operations_status),
salaryAssetType: stringValue(item.salaryAssetType ?? item.salary_asset_type), salaryAssetType: stringValue(item.salaryAssetType ?? item.salary_asset_type),
status: stringValue(item.status), status: stringValue(item.status),
updatedAtMs: numberOrNull(item.updatedAtMs ?? item.updated_at_ms), updatedAtMs: numberOrNull(item.updatedAtMs ?? item.updated_at_ms),

View File

@ -462,6 +462,20 @@ a {
min-width: 0; min-width: 0;
} }
/* 财务审核必须能在同一行确认前置运营结论;上下文收敛在单元格内,避免为备注额外拉宽整张审批表。 */
.finance-review-context {
display: grid;
min-width: 180px;
gap: 3px;
}
.finance-review-context > small {
max-width: 220px;
overflow: hidden;
color: var(--text-tertiary);
text-overflow: ellipsis;
}
.finance-address-wrap { .finance-address-wrap {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;

View File

@ -51,6 +51,8 @@ export const PERMISSIONS = {
financeView: "finance:view", financeView: "finance:view",
financeWithdrawalView: "finance-withdrawal:view", financeWithdrawalView: "finance-withdrawal:view",
financeWithdrawalAudit: "finance-withdrawal:audit", financeWithdrawalAudit: "finance-withdrawal:audit",
operationsWithdrawalView: "operations-withdrawal:view",
operationsWithdrawalAudit: "operations-withdrawal:audit",
financeOrderCoinSellerRechargeView: "finance-order:coin-seller-recharge:view", financeOrderCoinSellerRechargeView: "finance-order:coin-seller-recharge:view",
financeOrderCoinSellerRechargeCreate: "finance-order:coin-seller-recharge:create", financeOrderCoinSellerRechargeCreate: "finance-order:coin-seller-recharge:create",
financeOrderCoinSellerRechargeVerify: "finance-order:coin-seller-recharge:verify", financeOrderCoinSellerRechargeVerify: "finance-order:coin-seller-recharge:verify",

View File

@ -27,6 +27,7 @@ export const API_OPERATIONS = {
appListUsers: "appListUsers", appListUsers: "appListUsers",
approveCoinSellerSubApplication: "approveCoinSellerSubApplication", approveCoinSellerSubApplication: "approveCoinSellerSubApplication",
approveFinanceWithdrawalApplication: "approveFinanceWithdrawalApplication", approveFinanceWithdrawalApplication: "approveFinanceWithdrawalApplication",
approveOperationsWithdrawalApplication: "approveOperationsWithdrawalApplication",
appSetPassword: "appSetPassword", appSetPassword: "appSetPassword",
appUnbanUser: "appUnbanUser", appUnbanUser: "appUnbanUser",
appUpdateUser: "appUpdateUser", appUpdateUser: "appUpdateUser",
@ -238,6 +239,7 @@ export const API_OPERATIONS = {
listLuckyGiftPoolBalances: "listLuckyGiftPoolBalances", listLuckyGiftPoolBalances: "listLuckyGiftPoolBalances",
listManagers: "listManagers", listManagers: "listManagers",
listOperationLogs: "listOperationLogs", listOperationLogs: "listOperationLogs",
listOperationsWithdrawalApplications: "listOperationsWithdrawalApplications",
listPermissions: "listPermissions", listPermissions: "listPermissions",
listPlatformGrantRecords: "listPlatformGrantRecords", listPlatformGrantRecords: "listPlatformGrantRecords",
listPlatformGrantUsers: "listPlatformGrantUsers", listPlatformGrantUsers: "listPlatformGrantUsers",
@ -300,6 +302,7 @@ export const API_OPERATIONS = {
refreshGoogleRechargePaid: "refreshGoogleRechargePaid", refreshGoogleRechargePaid: "refreshGoogleRechargePaid",
rejectCoinSellerSubApplication: "rejectCoinSellerSubApplication", rejectCoinSellerSubApplication: "rejectCoinSellerSubApplication",
rejectFinanceWithdrawalApplication: "rejectFinanceWithdrawalApplication", rejectFinanceWithdrawalApplication: "rejectFinanceWithdrawalApplication",
rejectOperationsWithdrawalApplication: "rejectOperationsWithdrawalApplication",
renameCountryCode: "renameCountryCode", renameCountryCode: "renameCountryCode",
replaceCoinSellerSalaryRates: "replaceCoinSellerSalaryRates", replaceCoinSellerSalaryRates: "replaceCoinSellerSalaryRates",
replaceFinanceCoinSellerRechargeExchangeRate: "replaceFinanceCoinSellerRechargeExchangeRate", replaceFinanceCoinSellerRechargeExchangeRate: "replaceFinanceCoinSellerRechargeExchangeRate",
@ -513,6 +516,13 @@ export const API_ENDPOINTS: Record<ApiOperationId, ApiEndpoint> = {
permission: "finance-withdrawal:audit", permission: "finance-withdrawal:audit",
permissions: ["finance-withdrawal:audit"] permissions: ["finance-withdrawal:audit"]
}, },
approveOperationsWithdrawalApplication: {
method: "POST",
operationId: API_OPERATIONS.approveOperationsWithdrawalApplication,
path: "/v1/admin/operations/withdrawal-applications/{application_id}/approve",
permission: "operations-withdrawal:audit",
permissions: ["operations-withdrawal:audit"]
},
appSetPassword: { appSetPassword: {
method: "POST", method: "POST",
operationId: API_OPERATIONS.appSetPassword, operationId: API_OPERATIONS.appSetPassword,
@ -1986,6 +1996,13 @@ export const API_ENDPOINTS: Record<ApiOperationId, ApiEndpoint> = {
permission: "log:view", permission: "log:view",
permissions: ["log:view"] permissions: ["log:view"]
}, },
listOperationsWithdrawalApplications: {
method: "GET",
operationId: API_OPERATIONS.listOperationsWithdrawalApplications,
path: "/v1/admin/operations/withdrawal-applications",
permission: "operations-withdrawal:view",
permissions: ["operations-withdrawal:view","operations-withdrawal:audit"]
},
listPermissions: { listPermissions: {
method: "GET", method: "GET",
operationId: API_OPERATIONS.listPermissions, operationId: API_OPERATIONS.listPermissions,
@ -2408,6 +2425,13 @@ export const API_ENDPOINTS: Record<ApiOperationId, ApiEndpoint> = {
permission: "finance-withdrawal:audit", permission: "finance-withdrawal:audit",
permissions: ["finance-withdrawal:audit"] permissions: ["finance-withdrawal:audit"]
}, },
rejectOperationsWithdrawalApplication: {
method: "POST",
operationId: API_OPERATIONS.rejectOperationsWithdrawalApplication,
path: "/v1/admin/operations/withdrawal-applications/{application_id}/reject",
permission: "operations-withdrawal:audit",
permissions: ["operations-withdrawal:audit"]
},
renameCountryCode: { renameCountryCode: {
method: "POST", method: "POST",
operationId: API_OPERATIONS.renameCountryCode, operationId: API_OPERATIONS.renameCountryCode,

View File

@ -2556,6 +2556,54 @@ export interface paths {
patch?: never; patch?: never;
trace?: never; trace?: never;
}; };
"/admin/operations/withdrawal-applications": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get: operations["listOperationsWithdrawalApplications"];
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/admin/operations/withdrawal-applications/{application_id}/approve": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get?: never;
put?: never;
post: operations["approveOperationsWithdrawalApplication"];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/admin/operations/withdrawal-applications/{application_id}/reject": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get?: never;
put?: never;
post: operations["rejectOperationsWithdrawalApplication"];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/admin/finance/orders/coin-seller-recharges": { "/admin/finance/orders/coin-seller-recharges": {
parameters: { parameters: {
query?: never; query?: never;
@ -6485,6 +6533,13 @@ export interface components {
freezeTransactionId?: string; freezeTransactionId?: string;
auditTransactionId?: string; auditTransactionId?: string;
status?: string; status?: string;
/** @enum {string} */
operationsStatus?: "pending" | "approved" | "rejected";
operationsReviewerUserId?: number | null;
operationsReviewerName?: string;
operationsAuditRemark?: string;
operationsAuditTransactionId?: string;
operationsReviewedAtMs?: number | null;
approverUserId?: number | null; approverUserId?: number | null;
approverName?: string; approverName?: string;
auditRemark?: string; auditRemark?: string;
@ -7928,6 +7983,13 @@ export interface components {
}; };
}; };
}; };
OperationsWithdrawalAuditRequest: {
content: {
"application/json": {
auditRemark?: string;
};
};
};
MenuRequest: { MenuRequest: {
content: { content: {
"application/json": components["schemas"]["MenuInput"]; "application/json": components["schemas"]["MenuInput"];
@ -11220,6 +11282,51 @@ export interface operations {
200: components["responses"]["FinanceWithdrawalApplicationResponse"]; 200: components["responses"]["FinanceWithdrawalApplicationResponse"];
}; };
}; };
listOperationsWithdrawalApplications: {
parameters: {
query?: {
app_code?: string;
keyword?: string;
page?: number;
page_size?: number;
};
header?: never;
path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
200: components["responses"]["FinanceWithdrawalApplicationPageResponse"];
};
};
approveOperationsWithdrawalApplication: {
parameters: {
query?: never;
header?: never;
path: {
application_id: number;
};
cookie?: never;
};
requestBody?: components["requestBodies"]["OperationsWithdrawalAuditRequest"];
responses: {
200: components["responses"]["FinanceWithdrawalApplicationResponse"];
};
};
rejectOperationsWithdrawalApplication: {
parameters: {
query?: never;
header?: never;
path: {
application_id: number;
};
cookie?: never;
};
requestBody?: components["requestBodies"]["OperationsWithdrawalAuditRequest"];
responses: {
200: components["responses"]["FinanceWithdrawalApplicationResponse"];
};
};
listFinanceCoinSellerRechargeOrders: { listFinanceCoinSellerRechargeOrders: {
parameters: { parameters: {
query?: { query?: {