diff --git a/contracts/admin-openapi.json b/contracts/admin-openapi.json index 5ed8487..545ef41 100644 --- a/contracts/admin-openapi.json +++ b/contracts/admin-openapi.json @@ -4506,6 +4506,98 @@ "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": { "get": { "operationId": "listFinanceCoinSellerRechargeOrders", @@ -8640,6 +8732,20 @@ } } }, + "OperationsWithdrawalAuditRequest": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "auditRemark": { + "type": "string" + } + } + } + } + } + }, "MenuRequest": { "content": { "application/json": { @@ -14055,6 +14161,27 @@ "status": { "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": { "nullable": true, "type": "integer" diff --git a/finance/src/FinanceApp.jsx b/finance/src/FinanceApp.jsx index d6efc24..bc90ce6 100644 --- a/finance/src/FinanceApp.jsx +++ b/finance/src/FinanceApp.jsx @@ -1,6 +1,7 @@ import { useCallback, useEffect, useMemo, useState } from "react"; import { approveFinanceWithdrawalApplication, + approveOperationsWithdrawalApplication, createFinanceCoinSellerRechargeOrder, exportFinanceRechargeBills, fetchFinanceOptions, @@ -15,9 +16,11 @@ import { listFinanceRechargeDetails, listFinanceRechargeRegions, listFinanceWithdrawalApplications, + listOperationsWithdrawalApplications, refreshFinanceGoogleRechargePaid, replaceFinanceCoinSellerRechargeExchangeRate, rejectFinanceWithdrawalApplication, + rejectOperationsWithdrawalApplication, quoteFinanceCoinSellerRechargeOrder, upsertFinanceUSDTAddress, verifyFinanceCoinSellerRechargeReceipt, @@ -41,7 +44,63 @@ const emptyOptions = { apps: [] }; const pageSize = 50; const DAY_MS = 86_400_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) { return Math.floor((timestamp + CHINA_OFFSET_MS) / DAY_MS) * DAY_MS - CHINA_OFFSET_MS; diff --git a/finance/src/api.js b/finance/src/api.js index 57c8aba..df63930 100644 --- a/finance/src/api.js +++ b/finance/src/api.js @@ -29,6 +29,8 @@ export async function fetchFinanceSession() { const permissions = Array.isArray(session.permissions) ? session.permissions : []; const canAuditWithdrawalApplications = permissions.includes(FINANCE_PERMISSIONS.auditWithdrawalApplications); 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 canViewCoinSellerRechargeOrders = permissions.includes(FINANCE_PERMISSIONS.viewCoinSellerRechargeOrders); const canCreateCoinSellerRechargeOrder = permissions.includes(FINANCE_PERMISSIONS.createCoinSellerRechargeOrder); @@ -41,8 +43,10 @@ export async function fetchFinanceSession() { ...session, canAccessFinance: canViewWithdrawalApplications || + canViewOperationsWithdrawalApplications || canViewAppRechargeDetails || canViewCoinSellerRechargeOrders, + canAuditOperationsWithdrawalApplications, canAuditWithdrawalApplications, canConfigureCoinSellerRechargeExchangeRate, canCreateCoinSellerRechargeOrder, @@ -51,6 +55,7 @@ export async function fetchFinanceSession() { canVerifyCoinSellerRechargeOrder, canViewAppRechargeDetails, canViewCoinSellerRechargeOrders, + canViewOperationsWithdrawalApplications, canViewWithdrawalApplications, permissions }; @@ -82,6 +87,15 @@ export async function listFinanceWithdrawalApplications(query = {}) { 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 = {}) { const endpoint = API_ENDPOINTS.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() { try { return await refreshSession(); diff --git a/finance/src/components/FinanceAuditDialog.jsx b/finance/src/components/FinanceAuditDialog.jsx index 3f2e9a4..ffdbcad 100644 --- a/finance/src/components/FinanceAuditDialog.jsx +++ b/finance/src/components/FinanceAuditDialog.jsx @@ -8,7 +8,17 @@ import { useEffect, useState } from "react"; import { AppIdentity } from "@/shared/ui/AppIdentity.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 [auditImageUrl, setAuditImageUrl] = useState(""); const decision = application?.decision || "approved"; @@ -29,7 +39,7 @@ export function FinanceAuditDialog({ application, apps = [], loading, onClose, o return ( - {decision === "approved" ? "通过申请" : "拒绝申请"} + {decision === "approved" ? `通过${reviewLabel}审核` : `拒绝${reviewLabel}审核`}
提现方式 提现地址 申请时间 - 审批人 - 审批时间 - 审核原因 + {isOperationsReview ? ( + <> + 运营审核人 + 运营审核时间 + 运营审核原因 + + ) : ( + <> + 运营审核 + 财务审核人 + 财务审核时间 + 财务审核原因 + + )} 操作 @@ -104,30 +127,73 @@ export function FinanceWithdrawalApplicationList({ applications, canAudit, error ) : "-"} {formatTime(item.createdAtMs)} - - - - {formatTime(item.approvedAtMs)} - {item.auditRemark || "-"} + {isOperationsReview ? ( + <> + + + + {formatTime(item.operationsReviewedAtMs)} + {item.operationsAuditRemark || "-"} + + ) : ( + <> + + + + + + + {formatTime(item.approvedAtMs)} + {item.auditRemark || "-"} + + )} - {item.status === "pending" && canAudit ? ( + {(isOperationsReview ? item.operationsStatus : item.status) === "pending" && canAudit ? ( - - ) : ( - statusLabel(item.status) + statusLabel(isOperationsReview ? item.operationsStatus : item.status) )} )) ) : ( - + 当前无数据 @@ -153,6 +219,19 @@ export function FinanceWithdrawalApplicationList({ applications, canAudit, error ); } +function ReviewContext({ application }) { + return ( + + + {formatTime(application.operationsReviewedAtMs)} + {application.operationsAuditRemark || "-"} + + ); +} + function Stacked({ primary, secondary }) { return ( diff --git a/finance/src/constants.js b/finance/src/constants.js index 8632bd1..baa72da 100644 --- a/finance/src/constants.js +++ b/finance/src/constants.js @@ -1,5 +1,6 @@ export const FINANCE_PERMISSIONS = { auditWithdrawalApplications: "finance-withdrawal:audit", + auditOperationsWithdrawalApplications: "operations-withdrawal:audit", configureCoinSellerRechargeExchangeRate: "coin-seller:exchange-rate", createCoinSellerRechargeOrder: "finance-order:coin-seller-recharge:create", grantCoinSellerRechargeOrder: "finance-order:coin-seller-recharge:grant", @@ -7,6 +8,7 @@ export const FINANCE_PERMISSIONS = { verifyCoinSellerRechargeOrder: "finance-order:coin-seller-recharge:verify", viewCoinSellerRechargeOrders: "finance-order:coin-seller-recharge:view", viewAppRechargeDetails: "payment-bill:view", + viewOperationsWithdrawalApplications: "operations-withdrawal:view", viewWithdrawalApplications: "finance-withdrawal:view", }; diff --git a/finance/src/format.js b/finance/src/format.js index 230a6a3..e6cd2a8 100644 --- a/finance/src/format.js +++ b/finance/src/format.js @@ -22,6 +22,14 @@ export function normalizeWithdrawalApplication(item = {}) { createdAtMs: numberOrNull(item.createdAtMs ?? item.created_at_ms), freezeTransactionId: stringValue(item.freezeTransactionId ?? item.freeze_transaction_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), status: stringValue(item.status), updatedAtMs: numberOrNull(item.updatedAtMs ?? item.updated_at_ms), diff --git a/finance/src/styles/index.css b/finance/src/styles/index.css index 50e8e3b..731ef6b 100644 --- a/finance/src/styles/index.css +++ b/finance/src/styles/index.css @@ -462,6 +462,20 @@ a { 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 { display: inline-flex; align-items: center; diff --git a/src/app/permissions.ts b/src/app/permissions.ts index cbe86f8..e7c990a 100644 --- a/src/app/permissions.ts +++ b/src/app/permissions.ts @@ -51,6 +51,8 @@ export const PERMISSIONS = { financeView: "finance:view", financeWithdrawalView: "finance-withdrawal:view", financeWithdrawalAudit: "finance-withdrawal:audit", + operationsWithdrawalView: "operations-withdrawal:view", + operationsWithdrawalAudit: "operations-withdrawal:audit", financeOrderCoinSellerRechargeView: "finance-order:coin-seller-recharge:view", financeOrderCoinSellerRechargeCreate: "finance-order:coin-seller-recharge:create", financeOrderCoinSellerRechargeVerify: "finance-order:coin-seller-recharge:verify", diff --git a/src/shared/api/generated/endpoints.ts b/src/shared/api/generated/endpoints.ts index 2fe5977..2411d5f 100644 --- a/src/shared/api/generated/endpoints.ts +++ b/src/shared/api/generated/endpoints.ts @@ -27,6 +27,7 @@ export const API_OPERATIONS = { appListUsers: "appListUsers", approveCoinSellerSubApplication: "approveCoinSellerSubApplication", approveFinanceWithdrawalApplication: "approveFinanceWithdrawalApplication", + approveOperationsWithdrawalApplication: "approveOperationsWithdrawalApplication", appSetPassword: "appSetPassword", appUnbanUser: "appUnbanUser", appUpdateUser: "appUpdateUser", @@ -238,6 +239,7 @@ export const API_OPERATIONS = { listLuckyGiftPoolBalances: "listLuckyGiftPoolBalances", listManagers: "listManagers", listOperationLogs: "listOperationLogs", + listOperationsWithdrawalApplications: "listOperationsWithdrawalApplications", listPermissions: "listPermissions", listPlatformGrantRecords: "listPlatformGrantRecords", listPlatformGrantUsers: "listPlatformGrantUsers", @@ -300,6 +302,7 @@ export const API_OPERATIONS = { refreshGoogleRechargePaid: "refreshGoogleRechargePaid", rejectCoinSellerSubApplication: "rejectCoinSellerSubApplication", rejectFinanceWithdrawalApplication: "rejectFinanceWithdrawalApplication", + rejectOperationsWithdrawalApplication: "rejectOperationsWithdrawalApplication", renameCountryCode: "renameCountryCode", replaceCoinSellerSalaryRates: "replaceCoinSellerSalaryRates", replaceFinanceCoinSellerRechargeExchangeRate: "replaceFinanceCoinSellerRechargeExchangeRate", @@ -513,6 +516,13 @@ export const API_ENDPOINTS: Record = { permission: "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: { method: "POST", operationId: API_OPERATIONS.appSetPassword, @@ -1986,6 +1996,13 @@ export const API_ENDPOINTS: Record = { permission: "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: { method: "GET", operationId: API_OPERATIONS.listPermissions, @@ -2408,6 +2425,13 @@ export const API_ENDPOINTS: Record = { permission: "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: { method: "POST", operationId: API_OPERATIONS.renameCountryCode, diff --git a/src/shared/api/generated/schema.d.ts b/src/shared/api/generated/schema.d.ts index 87c1e47..4b0ba30 100644 --- a/src/shared/api/generated/schema.d.ts +++ b/src/shared/api/generated/schema.d.ts @@ -2556,6 +2556,54 @@ export interface paths { patch?: 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": { parameters: { query?: never; @@ -6485,6 +6533,13 @@ export interface components { freezeTransactionId?: string; auditTransactionId?: string; status?: string; + /** @enum {string} */ + operationsStatus?: "pending" | "approved" | "rejected"; + operationsReviewerUserId?: number | null; + operationsReviewerName?: string; + operationsAuditRemark?: string; + operationsAuditTransactionId?: string; + operationsReviewedAtMs?: number | null; approverUserId?: number | null; approverName?: string; auditRemark?: string; @@ -7928,6 +7983,13 @@ export interface components { }; }; }; + OperationsWithdrawalAuditRequest: { + content: { + "application/json": { + auditRemark?: string; + }; + }; + }; MenuRequest: { content: { "application/json": components["schemas"]["MenuInput"]; @@ -11220,6 +11282,51 @@ export interface operations { 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: { parameters: { query?: {