Compare commits
No commits in common. "7711f2a9ee03f259d7f47fbcaa115666b71490ec" and "24e9bfc439a212d257efb8a851a9ea4a018a6082" have entirely different histories.
7711f2a9ee
...
24e9bfc439
@ -3578,148 +3578,6 @@
|
|||||||
"x-permissions": ["finance-withdrawal:audit", "finance-application:audit"]
|
"x-permissions": ["finance-withdrawal:audit", "finance-application:audit"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/admin/finance/orders/coin-seller-recharges": {
|
|
||||||
"get": {
|
|
||||||
"operationId": "listFinanceCoinSellerRechargeOrders",
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"in": "query",
|
|
||||||
"name": "app_code",
|
|
||||||
"schema": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"in": "query",
|
|
||||||
"name": "provider_code",
|
|
||||||
"schema": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"in": "query",
|
|
||||||
"name": "status",
|
|
||||||
"schema": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"in": "query",
|
|
||||||
"name": "verify_status",
|
|
||||||
"schema": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"in": "query",
|
|
||||||
"name": "grant_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/FinanceCoinSellerRechargeOrderPageResponse"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"x-permission": "finance-order:coin-seller-recharge:view",
|
|
||||||
"x-permissions": ["finance-order:coin-seller-recharge:view"]
|
|
||||||
},
|
|
||||||
"post": {
|
|
||||||
"operationId": "createFinanceCoinSellerRechargeOrder",
|
|
||||||
"requestBody": {
|
|
||||||
"$ref": "#/components/requestBodies/FinanceCoinSellerRechargeOrderRequest"
|
|
||||||
},
|
|
||||||
"responses": {
|
|
||||||
"201": {
|
|
||||||
"$ref": "#/components/responses/FinanceCoinSellerRechargeOrderResponse"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"x-permission": "finance-order:coin-seller-recharge:create",
|
|
||||||
"x-permissions": ["finance-order:coin-seller-recharge:create"]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/admin/finance/orders/coin-seller-recharges/receipt-verifications": {
|
|
||||||
"post": {
|
|
||||||
"operationId": "verifyFinanceCoinSellerRechargeReceipt",
|
|
||||||
"requestBody": {
|
|
||||||
"$ref": "#/components/requestBodies/FinanceCoinSellerRechargeReceiptVerificationRequest"
|
|
||||||
},
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"$ref": "#/components/responses/FinanceCoinSellerRechargeReceiptVerificationResponse"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"x-permission": "finance-order:coin-seller-recharge:verify",
|
|
||||||
"x-permissions": ["finance-order:coin-seller-recharge:verify"]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/admin/finance/orders/coin-seller-recharges/{order_id}/verify": {
|
|
||||||
"post": {
|
|
||||||
"operationId": "verifyFinanceCoinSellerRechargeOrder",
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"in": "path",
|
|
||||||
"name": "order_id",
|
|
||||||
"required": true,
|
|
||||||
"schema": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"$ref": "#/components/responses/FinanceCoinSellerRechargeOrderResponse"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"x-permission": "finance-order:coin-seller-recharge:verify",
|
|
||||||
"x-permissions": ["finance-order:coin-seller-recharge:verify"]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/admin/finance/orders/coin-seller-recharges/{order_id}/grant": {
|
|
||||||
"post": {
|
|
||||||
"operationId": "grantFinanceCoinSellerRechargeOrder",
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"in": "path",
|
|
||||||
"name": "order_id",
|
|
||||||
"required": true,
|
|
||||||
"schema": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"$ref": "#/components/responses/FinanceCoinSellerRechargeOrderResponse"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"x-permission": "finance-order:coin-seller-recharge:grant",
|
|
||||||
"x-permissions": ["finance-order:coin-seller-recharge:grant"]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/admin/payment/temporary-links": {
|
"/admin/payment/temporary-links": {
|
||||||
"get": {
|
"get": {
|
||||||
"operationId": "listTemporaryPaymentLinks",
|
"operationId": "listTemporaryPaymentLinks",
|
||||||
@ -6215,24 +6073,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"FinanceCoinSellerRechargeOrderRequest": {
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/FinanceCoinSellerRechargeOrderInput"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"FinanceCoinSellerRechargeReceiptVerificationRequest": {
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/FinanceCoinSellerRechargeReceiptVerificationInput"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"FinanceApplicationAuditRequest": {
|
"FinanceApplicationAuditRequest": {
|
||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
@ -6505,36 +6345,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"FinanceCoinSellerRechargeOrderPageResponse": {
|
|
||||||
"description": "OK",
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/ApiResponseFinanceCoinSellerRechargeOrderPage"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"FinanceCoinSellerRechargeOrderResponse": {
|
|
||||||
"description": "OK",
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/ApiResponseFinanceCoinSellerRechargeOrder"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"FinanceCoinSellerRechargeReceiptVerificationResponse": {
|
|
||||||
"description": "OK",
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/ApiResponseFinanceCoinSellerRechargeReceiptVerification"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"FinanceWithdrawalApplicationPageResponse": {
|
"FinanceWithdrawalApplicationPageResponse": {
|
||||||
"description": "OK",
|
"description": "OK",
|
||||||
"content": {
|
"content": {
|
||||||
@ -6884,27 +6694,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ApiPageFinanceCoinSellerRechargeOrder": {
|
|
||||||
"type": "object",
|
|
||||||
"required": ["items", "page", "pageSize", "total"],
|
|
||||||
"properties": {
|
|
||||||
"items": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"$ref": "#/components/schemas/FinanceCoinSellerRechargeOrder"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"page": {
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"pageSize": {
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"total": {
|
|
||||||
"type": "integer"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"ApiPageFinanceWithdrawalApplication": {
|
"ApiPageFinanceWithdrawalApplication": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": ["items", "page", "pageSize", "total"],
|
"required": ["items", "page", "pageSize", "total"],
|
||||||
@ -7107,51 +6896,6 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"ApiResponseFinanceCoinSellerRechargeOrder": {
|
|
||||||
"allOf": [
|
|
||||||
{
|
|
||||||
"$ref": "#/components/schemas/Envelope"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"data": {
|
|
||||||
"$ref": "#/components/schemas/FinanceCoinSellerRechargeOrder"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"ApiResponseFinanceCoinSellerRechargeReceiptVerification": {
|
|
||||||
"allOf": [
|
|
||||||
{
|
|
||||||
"$ref": "#/components/schemas/Envelope"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"data": {
|
|
||||||
"$ref": "#/components/schemas/FinanceCoinSellerRechargeReceiptVerification"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"ApiResponseFinanceCoinSellerRechargeOrderPage": {
|
|
||||||
"allOf": [
|
|
||||||
{
|
|
||||||
"$ref": "#/components/schemas/Envelope"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"data": {
|
|
||||||
"$ref": "#/components/schemas/ApiPageFinanceCoinSellerRechargeOrder"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"ApiResponseFinanceWithdrawalApplication": {
|
"ApiResponseFinanceWithdrawalApplication": {
|
||||||
"allOf": [
|
"allOf": [
|
||||||
{
|
{
|
||||||
@ -8332,133 +8076,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"FinanceCoinSellerRechargeOrder": {
|
|
||||||
"type": "object",
|
|
||||||
"required": [
|
|
||||||
"id",
|
|
||||||
"appCode",
|
|
||||||
"targetUserId",
|
|
||||||
"usdAmount",
|
|
||||||
"coinAmount",
|
|
||||||
"providerCode",
|
|
||||||
"externalOrderNo",
|
|
||||||
"status",
|
|
||||||
"verifyStatus",
|
|
||||||
"grantStatus",
|
|
||||||
"createdAtMs"
|
|
||||||
],
|
|
||||||
"properties": {
|
|
||||||
"id": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"appCode": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"targetUserId": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"targetDisplayUserId": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"usdAmount": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"usdMinorAmount": {
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"coinAmount": {
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"providerCode": {
|
|
||||||
"type": "string",
|
|
||||||
"enum": ["mifapay", "v5pay", "usdt"]
|
|
||||||
},
|
|
||||||
"chain": {
|
|
||||||
"type": "string",
|
|
||||||
"enum": ["TRON", "BSC"]
|
|
||||||
},
|
|
||||||
"externalOrderNo": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"providerCountryCode": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"providerCurrencyCode": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"providerAmountMinor": {
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"providerOrderId": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"providerStatus": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"providerPayType": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"receiveAddress": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"orderDate": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"status": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"verifyStatus": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"grantStatus": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"remark": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"failureReason": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"providerPayloadJson": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"walletCommandId": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"walletTransactionId": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"walletAssetType": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"walletAmountDelta": {
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"walletBalanceAfter": {
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"operatorName": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"verifiedByName": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"grantedByName": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"createdAtMs": {
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"verifiedAtMs": {
|
|
||||||
"nullable": true,
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"grantedAtMs": {
|
|
||||||
"nullable": true,
|
|
||||||
"type": "integer"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"FinanceWithdrawalApplication": {
|
"FinanceWithdrawalApplication": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": [
|
||||||
@ -8573,112 +8190,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"FinanceCoinSellerRechargeOrderInput": {
|
|
||||||
"type": "object",
|
|
||||||
"required": ["appCode", "targetUserId", "usdAmount", "coinAmount", "providerCode", "externalOrderNo"],
|
|
||||||
"properties": {
|
|
||||||
"appCode": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"targetUserId": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"usdAmount": {
|
|
||||||
"type": "number",
|
|
||||||
"minimum": 0
|
|
||||||
},
|
|
||||||
"coinAmount": {
|
|
||||||
"type": "integer",
|
|
||||||
"minimum": 0
|
|
||||||
},
|
|
||||||
"providerCode": {
|
|
||||||
"type": "string",
|
|
||||||
"enum": ["mifapay", "v5pay", "usdt"]
|
|
||||||
},
|
|
||||||
"externalOrderNo": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"chain": {
|
|
||||||
"type": "string",
|
|
||||||
"enum": ["TRON", "BSC"]
|
|
||||||
},
|
|
||||||
"remark": {
|
|
||||||
"type": "string",
|
|
||||||
"maxLength": 512
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"FinanceCoinSellerRechargeReceiptVerification": {
|
|
||||||
"type": "object",
|
|
||||||
"required": ["verified"],
|
|
||||||
"properties": {
|
|
||||||
"verified": {
|
|
||||||
"type": "boolean"
|
|
||||||
},
|
|
||||||
"providerCode": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"externalOrderNo": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"providerOrderId": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"status": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"currencyCode": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"providerAmountMinor": {
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"chain": {
|
|
||||||
"type": "string",
|
|
||||||
"enum": ["TRON", "BSC"]
|
|
||||||
},
|
|
||||||
"receiveAddress": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"verifiedAtMs": {
|
|
||||||
"type": "integer"
|
|
||||||
},
|
|
||||||
"failureReason": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"rawJson": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"FinanceCoinSellerRechargeReceiptVerificationInput": {
|
|
||||||
"type": "object",
|
|
||||||
"required": ["appCode", "providerCode", "externalOrderNo"],
|
|
||||||
"properties": {
|
|
||||||
"appCode": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"providerCode": {
|
|
||||||
"type": "string",
|
|
||||||
"enum": ["mifapay", "v5pay", "usdt"]
|
|
||||||
},
|
|
||||||
"externalOrderNo": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"chain": {
|
|
||||||
"type": "string",
|
|
||||||
"enum": ["TRON", "BSC"]
|
|
||||||
},
|
|
||||||
"usdAmount": {
|
|
||||||
"type": "number",
|
|
||||||
"minimum": 0
|
|
||||||
},
|
|
||||||
"usdMinorAmount": {
|
|
||||||
"type": "integer",
|
|
||||||
"minimum": 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"UserFinanceScope": {
|
"UserFinanceScope": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": ["appCode", "regionId"],
|
"required": ["appCode", "regionId"],
|
||||||
|
|||||||
@ -6,7 +6,6 @@ import {
|
|||||||
approveFinanceApplication,
|
approveFinanceApplication,
|
||||||
approveFinanceWithdrawalApplication,
|
approveFinanceWithdrawalApplication,
|
||||||
createFinanceApplication,
|
createFinanceApplication,
|
||||||
createFinanceCoinSellerRechargeOrder,
|
|
||||||
exportFinanceRechargeBills,
|
exportFinanceRechargeBills,
|
||||||
fetchFinanceApplicationOptions,
|
fetchFinanceApplicationOptions,
|
||||||
fetchFinanceRechargeApps,
|
fetchFinanceRechargeApps,
|
||||||
@ -14,9 +13,7 @@ import {
|
|||||||
fetchFinanceRechargeSummaries,
|
fetchFinanceRechargeSummaries,
|
||||||
fetchFinanceSession,
|
fetchFinanceSession,
|
||||||
filterOperationsByPermissions,
|
filterOperationsByPermissions,
|
||||||
grantFinanceCoinSellerRechargeOrder,
|
|
||||||
listFinanceApplications,
|
listFinanceApplications,
|
||||||
listFinanceCoinSellerRechargeOrders,
|
|
||||||
listFinanceRechargeDetails,
|
listFinanceRechargeDetails,
|
||||||
listFinanceRechargeRegions,
|
listFinanceRechargeRegions,
|
||||||
listMyFinanceApplications,
|
listMyFinanceApplications,
|
||||||
@ -24,13 +21,10 @@ import {
|
|||||||
refreshFinanceGoogleRechargePaid,
|
refreshFinanceGoogleRechargePaid,
|
||||||
rejectFinanceApplication,
|
rejectFinanceApplication,
|
||||||
rejectFinanceWithdrawalApplication,
|
rejectFinanceWithdrawalApplication,
|
||||||
verifyFinanceCoinSellerRechargeReceipt,
|
|
||||||
verifyFinanceCoinSellerRechargeOrder,
|
|
||||||
} from "./api.js";
|
} from "./api.js";
|
||||||
import { FinanceApplicationForm } from "./components/FinanceApplicationForm.jsx";
|
import { FinanceApplicationForm } from "./components/FinanceApplicationForm.jsx";
|
||||||
import { FinanceApplicationList } from "./components/FinanceApplicationList.jsx";
|
import { FinanceApplicationList } from "./components/FinanceApplicationList.jsx";
|
||||||
import { FinanceAuditDialog } from "./components/FinanceAuditDialog.jsx";
|
import { FinanceAuditDialog } from "./components/FinanceAuditDialog.jsx";
|
||||||
import { FinanceCoinSellerRechargeOrderList } from "./components/FinanceCoinSellerRechargeOrderList.jsx";
|
|
||||||
import { FinanceMyApplicationList } from "./components/FinanceMyApplicationList.jsx";
|
import { FinanceMyApplicationList } from "./components/FinanceMyApplicationList.jsx";
|
||||||
import { FinanceOverview } from "./components/FinanceOverview.jsx";
|
import { FinanceOverview } from "./components/FinanceOverview.jsx";
|
||||||
import { FinanceReconciliation } from "./components/FinanceReconciliation.jsx";
|
import { FinanceReconciliation } from "./components/FinanceReconciliation.jsx";
|
||||||
@ -130,12 +124,6 @@ export function FinanceApp() {
|
|||||||
error: "",
|
error: "",
|
||||||
loading: false,
|
loading: false,
|
||||||
});
|
});
|
||||||
const [coinSellerRechargeOrdersState, setCoinSellerRechargeOrdersState] = useState({
|
|
||||||
data: { items: [], page: 1, pageSize, total: 0 },
|
|
||||||
error: "",
|
|
||||||
loading: false,
|
|
||||||
});
|
|
||||||
const [coinSellerRechargeOrderActionLoading, setCoinSellerRechargeOrderActionLoading] = useState("");
|
|
||||||
const [activeView, setActiveView] = useState("create");
|
const [activeView, setActiveView] = useState("create");
|
||||||
const [rechargeDetailFilters, setRechargeDetailFilters] = useState(() => ({
|
const [rechargeDetailFilters, setRechargeDetailFilters] = useState(() => ({
|
||||||
appCode: "",
|
appCode: "",
|
||||||
@ -151,15 +139,6 @@ export function FinanceApp() {
|
|||||||
const [myFilters, setMyFilters] = useState({ appCode: "", keyword: "", operation: "", page: 1, status: "" });
|
const [myFilters, setMyFilters] = useState({ appCode: "", keyword: "", operation: "", page: 1, status: "" });
|
||||||
const [filters, setFilters] = useState({ appCode: "", keyword: "", operation: "", page: 1, status: "pending" });
|
const [filters, setFilters] = useState({ appCode: "", keyword: "", operation: "", page: 1, status: "pending" });
|
||||||
const [withdrawalFilters, setWithdrawalFilters] = useState({ appCode: "", keyword: "", page: 1 });
|
const [withdrawalFilters, setWithdrawalFilters] = useState({ appCode: "", keyword: "", page: 1 });
|
||||||
const [coinSellerRechargeOrderFilters, setCoinSellerRechargeOrderFilters] = useState({
|
|
||||||
appCode: "",
|
|
||||||
grantStatus: "",
|
|
||||||
keyword: "",
|
|
||||||
page: 1,
|
|
||||||
providerCode: "",
|
|
||||||
status: "",
|
|
||||||
verifyStatus: "",
|
|
||||||
});
|
|
||||||
const [form, setForm] = useState(DEFAULT_APPLICATION_FORM);
|
const [form, setForm] = useState(DEFAULT_APPLICATION_FORM);
|
||||||
const [createDialogOpen, setCreateDialogOpen] = useState(false);
|
const [createDialogOpen, setCreateDialogOpen] = useState(false);
|
||||||
const [submitting, setSubmitting] = useState(false);
|
const [submitting, setSubmitting] = useState(false);
|
||||||
@ -179,13 +158,11 @@ export function FinanceApp() {
|
|||||||
setActiveView(
|
setActiveView(
|
||||||
session.canViewAppRechargeDetails
|
session.canViewAppRechargeDetails
|
||||||
? "overview"
|
? "overview"
|
||||||
: session.canViewCoinSellerRechargeOrders
|
: session.canAuditApplication
|
||||||
? "coinSellerRechargeOrders"
|
? "applications"
|
||||||
: session.canAuditApplication
|
: session.canCreateApplication
|
||||||
? "applications"
|
? "create"
|
||||||
: session.canCreateApplication
|
: "withdrawals",
|
||||||
? "create"
|
|
||||||
: "withdrawals",
|
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
@ -256,19 +233,6 @@ export function FinanceApp() {
|
|||||||
}),
|
}),
|
||||||
[withdrawalFilters],
|
[withdrawalFilters],
|
||||||
);
|
);
|
||||||
const coinSellerRechargeOrderQuery = useMemo(
|
|
||||||
() => ({
|
|
||||||
app_code: coinSellerRechargeOrderFilters.appCode,
|
|
||||||
grant_status: coinSellerRechargeOrderFilters.grantStatus,
|
|
||||||
keyword: coinSellerRechargeOrderFilters.keyword,
|
|
||||||
page: coinSellerRechargeOrderFilters.page,
|
|
||||||
page_size: pageSize,
|
|
||||||
provider_code: coinSellerRechargeOrderFilters.providerCode,
|
|
||||||
status: coinSellerRechargeOrderFilters.status,
|
|
||||||
verify_status: coinSellerRechargeOrderFilters.verifyStatus,
|
|
||||||
}),
|
|
||||||
[coinSellerRechargeOrderFilters],
|
|
||||||
);
|
|
||||||
|
|
||||||
const loadOptions = useCallback(async () => {
|
const loadOptions = useCallback(async () => {
|
||||||
if (!canLoadOptions) {
|
if (!canLoadOptions) {
|
||||||
@ -500,23 +464,6 @@ export function FinanceApp() {
|
|||||||
}
|
}
|
||||||
}, [session?.canViewWithdrawalApplications, withdrawalQuery]);
|
}, [session?.canViewWithdrawalApplications, withdrawalQuery]);
|
||||||
|
|
||||||
const loadCoinSellerRechargeOrders = useCallback(async () => {
|
|
||||||
if (!session?.canViewCoinSellerRechargeOrders) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setCoinSellerRechargeOrdersState((current) => ({ ...current, error: "", loading: true }));
|
|
||||||
try {
|
|
||||||
const data = await listFinanceCoinSellerRechargeOrders(coinSellerRechargeOrderQuery);
|
|
||||||
setCoinSellerRechargeOrdersState({ data, error: "", loading: false });
|
|
||||||
} catch (err) {
|
|
||||||
setCoinSellerRechargeOrdersState((current) => ({
|
|
||||||
...current,
|
|
||||||
error: err.message || "加载币商充值订单失败",
|
|
||||||
loading: false,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}, [coinSellerRechargeOrderQuery, session?.canViewCoinSellerRechargeOrders]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadOptions();
|
loadOptions();
|
||||||
}, [loadOptions]);
|
}, [loadOptions]);
|
||||||
@ -557,10 +504,6 @@ export function FinanceApp() {
|
|||||||
loadWithdrawals();
|
loadWithdrawals();
|
||||||
}, [loadWithdrawals]);
|
}, [loadWithdrawals]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
loadCoinSellerRechargeOrders();
|
|
||||||
}, [loadCoinSellerRechargeOrders]);
|
|
||||||
|
|
||||||
const operationOptions = useMemo(
|
const operationOptions = useMemo(
|
||||||
() => filterOperationsByPermissions(optionsState.data.operations, session?.permissions || []),
|
() => filterOperationsByPermissions(optionsState.data.operations, session?.permissions || []),
|
||||||
[optionsState.data.operations, session?.permissions],
|
[optionsState.data.operations, session?.permissions],
|
||||||
@ -637,72 +580,6 @@ export function FinanceApp() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const submitCoinSellerRechargeOrder = async (payload) => {
|
|
||||||
if (!session?.canCreateCoinSellerRechargeOrder) {
|
|
||||||
showToast("无创建币商充值订单权限", "error");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
setCoinSellerRechargeOrderActionLoading("create");
|
|
||||||
try {
|
|
||||||
const order = await createFinanceCoinSellerRechargeOrder(cleanPayload(payload));
|
|
||||||
showToast("币商充值订单已创建", "success");
|
|
||||||
await loadCoinSellerRechargeOrders();
|
|
||||||
return order;
|
|
||||||
} catch (err) {
|
|
||||||
showToast(err.message || "创建币商充值订单失败", "error");
|
|
||||||
return null;
|
|
||||||
} finally {
|
|
||||||
setCoinSellerRechargeOrderActionLoading("");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const verifyCoinSellerRechargeReceipt = async (payload) => {
|
|
||||||
if (!session?.canVerifyCoinSellerRechargeOrder) {
|
|
||||||
showToast("无校验币商充值订单权限", "error");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const verification = await verifyFinanceCoinSellerRechargeReceipt(cleanPayload(payload));
|
|
||||||
showToast(verification?.verified ? "订单号校验通过" : verification?.failureReason || "订单号校验未通过", verification?.verified ? "success" : "error");
|
|
||||||
return verification;
|
|
||||||
} catch (err) {
|
|
||||||
showToast(err.message || "校验订单号失败", "error");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const verifyCoinSellerRechargeOrder = async (order) => {
|
|
||||||
if (!order?.id || !session?.canVerifyCoinSellerRechargeOrder) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setCoinSellerRechargeOrderActionLoading(`verify:${order.id}`);
|
|
||||||
try {
|
|
||||||
await verifyFinanceCoinSellerRechargeOrder(order.id);
|
|
||||||
showToast("订单校验已完成", "success");
|
|
||||||
await loadCoinSellerRechargeOrders();
|
|
||||||
} catch (err) {
|
|
||||||
showToast(err.message || "校验币商充值订单失败", "error");
|
|
||||||
} finally {
|
|
||||||
setCoinSellerRechargeOrderActionLoading("");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const grantCoinSellerRechargeOrder = async (order) => {
|
|
||||||
if (!order?.id || !session?.canGrantCoinSellerRechargeOrder) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setCoinSellerRechargeOrderActionLoading(`grant:${order.id}`);
|
|
||||||
try {
|
|
||||||
await grantFinanceCoinSellerRechargeOrder(order.id);
|
|
||||||
showToast("币商充值已发放", "success");
|
|
||||||
await loadCoinSellerRechargeOrders();
|
|
||||||
} catch (err) {
|
|
||||||
showToast(err.message || "发放币商充值失败", "error");
|
|
||||||
} finally {
|
|
||||||
setCoinSellerRechargeOrderActionLoading("");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateFilters = (nextFilters) => {
|
const updateFilters = (nextFilters) => {
|
||||||
setFilters((current) => ({ ...current, ...nextFilters, page: nextFilters.page || 1 }));
|
setFilters((current) => ({ ...current, ...nextFilters, page: nextFilters.page || 1 }));
|
||||||
};
|
};
|
||||||
@ -719,10 +596,6 @@ export function FinanceApp() {
|
|||||||
setWithdrawalFilters((current) => ({ ...current, ...nextFilters, page: nextFilters.page || 1 }));
|
setWithdrawalFilters((current) => ({ ...current, ...nextFilters, page: nextFilters.page || 1 }));
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateCoinSellerRechargeOrderFilters = (nextFilters) => {
|
|
||||||
setCoinSellerRechargeOrderFilters((current) => ({ ...current, ...nextFilters, page: nextFilters.page || 1 }));
|
|
||||||
};
|
|
||||||
|
|
||||||
const closeCreateDialog = (_, reason) => {
|
const closeCreateDialog = (_, reason) => {
|
||||||
if (submitting || reason === "backdropClick") {
|
if (submitting || reason === "backdropClick") {
|
||||||
return;
|
return;
|
||||||
@ -822,7 +695,6 @@ export function FinanceApp() {
|
|||||||
appBar={appBar}
|
appBar={appBar}
|
||||||
canAudit={session.canAuditApplication}
|
canAudit={session.canAuditApplication}
|
||||||
canCreate={session.canCreateApplication}
|
canCreate={session.canCreateApplication}
|
||||||
canViewCoinSellerRechargeOrders={session.canViewCoinSellerRechargeOrders}
|
|
||||||
canViewRechargeDetails={session.canViewAppRechargeDetails}
|
canViewRechargeDetails={session.canViewAppRechargeDetails}
|
||||||
canViewWithdrawals={session.canViewWithdrawalApplications}
|
canViewWithdrawals={session.canViewWithdrawalApplications}
|
||||||
loading={
|
loading={
|
||||||
@ -830,8 +702,7 @@ export function FinanceApp() {
|
|||||||
rechargeDetailsState.loading ||
|
rechargeDetailsState.loading ||
|
||||||
myApplicationsState.loading ||
|
myApplicationsState.loading ||
|
||||||
applicationsState.loading ||
|
applicationsState.loading ||
|
||||||
withdrawalsState.loading ||
|
withdrawalsState.loading
|
||||||
coinSellerRechargeOrdersState.loading
|
|
||||||
}
|
}
|
||||||
session={session}
|
session={session}
|
||||||
toolbar={financeToolbar}
|
toolbar={financeToolbar}
|
||||||
@ -859,24 +730,6 @@ export function FinanceApp() {
|
|||||||
}}
|
}}
|
||||||
onOpenWithdrawals={() => setActiveView("withdrawals")}
|
onOpenWithdrawals={() => setActiveView("withdrawals")}
|
||||||
/>
|
/>
|
||||||
) : activeView === "coinSellerRechargeOrders" && session.canViewCoinSellerRechargeOrders ? (
|
|
||||||
<FinanceCoinSellerRechargeOrderList
|
|
||||||
actionLoading={coinSellerRechargeOrderActionLoading}
|
|
||||||
apps={rechargeApps}
|
|
||||||
canCreate={session.canCreateCoinSellerRechargeOrder}
|
|
||||||
canGrant={session.canGrantCoinSellerRechargeOrder}
|
|
||||||
canVerify={session.canVerifyCoinSellerRechargeOrder}
|
|
||||||
error={coinSellerRechargeOrdersState.error}
|
|
||||||
filters={coinSellerRechargeOrderFilters}
|
|
||||||
loading={coinSellerRechargeOrdersState.loading}
|
|
||||||
orders={coinSellerRechargeOrdersState.data}
|
|
||||||
onCreate={submitCoinSellerRechargeOrder}
|
|
||||||
onFiltersChange={updateCoinSellerRechargeOrderFilters}
|
|
||||||
onGrant={grantCoinSellerRechargeOrder}
|
|
||||||
onReload={loadCoinSellerRechargeOrders}
|
|
||||||
onVerify={verifyCoinSellerRechargeOrder}
|
|
||||||
onVerifyReceipt={verifyCoinSellerRechargeReceipt}
|
|
||||||
/>
|
|
||||||
) : activeView === "recon" && session.canViewAppRechargeDetails ? (
|
) : activeView === "recon" && session.canViewAppRechargeDetails ? (
|
||||||
<FinanceReconciliation
|
<FinanceReconciliation
|
||||||
loading={reconState.loading}
|
loading={reconState.loading}
|
||||||
|
|||||||
@ -7,7 +7,6 @@ vi.mock("./api.js", () => ({
|
|||||||
approveFinanceApplication: vi.fn(),
|
approveFinanceApplication: vi.fn(),
|
||||||
approveFinanceWithdrawalApplication: vi.fn(),
|
approveFinanceWithdrawalApplication: vi.fn(),
|
||||||
createFinanceApplication: vi.fn(),
|
createFinanceApplication: vi.fn(),
|
||||||
createFinanceCoinSellerRechargeOrder: vi.fn(),
|
|
||||||
fetchFinanceApplicationOptions: vi.fn(async () => ({
|
fetchFinanceApplicationOptions: vi.fn(async () => ({
|
||||||
apps: [{ appCode: "lalu", appName: "lalu" }],
|
apps: [{ appCode: "lalu", appName: "lalu" }],
|
||||||
operations: [{ label: "增加用户金币", permissionCode: "finance-operation:user-coin-credit", value: "user_coin_credit" }],
|
operations: [{ label: "增加用户金币", permissionCode: "finance-operation:user-coin-credit", value: "user_coin_credit" }],
|
||||||
@ -17,25 +16,18 @@ vi.mock("./api.js", () => ({
|
|||||||
canAccessFinance: true,
|
canAccessFinance: true,
|
||||||
canAuditApplication: false,
|
canAuditApplication: false,
|
||||||
canCreateApplication: true,
|
canCreateApplication: true,
|
||||||
canCreateCoinSellerRechargeOrder: false,
|
|
||||||
canGrantCoinSellerRechargeOrder: false,
|
|
||||||
canVerifyCoinSellerRechargeOrder: false,
|
|
||||||
canViewAppRechargeDetails: false,
|
canViewAppRechargeDetails: false,
|
||||||
canViewCoinSellerRechargeOrders: false,
|
|
||||||
canViewWithdrawalApplications: false,
|
canViewWithdrawalApplications: false,
|
||||||
permissions: ["finance-application:create", "finance-operation:user-coin-credit"],
|
permissions: ["finance-application:create", "finance-operation:user-coin-credit"],
|
||||||
user: { account: "admin", name: "admin" }
|
user: { account: "admin", name: "admin" }
|
||||||
})),
|
})),
|
||||||
filterOperationsByPermissions: vi.fn((operations, permissions) => operations.filter((item) => permissions.includes(item.permissionCode))),
|
filterOperationsByPermissions: vi.fn((operations, permissions) => operations.filter((item) => permissions.includes(item.permissionCode))),
|
||||||
grantFinanceCoinSellerRechargeOrder: vi.fn(),
|
|
||||||
listFinanceApplications: vi.fn(),
|
listFinanceApplications: vi.fn(),
|
||||||
listFinanceCoinSellerRechargeOrders: vi.fn(async () => ({ items: [], page: 1, pageSize: 50, total: 0 })),
|
|
||||||
listFinanceRechargeDetails: vi.fn(),
|
listFinanceRechargeDetails: vi.fn(),
|
||||||
listFinanceWithdrawalApplications: vi.fn(),
|
listFinanceWithdrawalApplications: vi.fn(),
|
||||||
listMyFinanceApplications: vi.fn(async () => ({ items: [], page: 1, pageSize: 50, total: 0 })),
|
listMyFinanceApplications: vi.fn(async () => ({ items: [], page: 1, pageSize: 50, total: 0 })),
|
||||||
rejectFinanceApplication: vi.fn(),
|
rejectFinanceApplication: vi.fn(),
|
||||||
rejectFinanceWithdrawalApplication: vi.fn(),
|
rejectFinanceWithdrawalApplication: vi.fn()
|
||||||
verifyFinanceCoinSellerRechargeOrder: vi.fn()
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
|
|||||||
@ -6,8 +6,6 @@ import {
|
|||||||
mergeRechargeOverviews,
|
mergeRechargeOverviews,
|
||||||
mergeRechargeSummaries,
|
mergeRechargeSummaries,
|
||||||
normalizeApplicationPage,
|
normalizeApplicationPage,
|
||||||
normalizeCoinSellerRechargeOrder,
|
|
||||||
normalizeCoinSellerRechargeOrderPage,
|
|
||||||
normalizeRechargeBillPage,
|
normalizeRechargeBillPage,
|
||||||
normalizeRechargeOverview,
|
normalizeRechargeOverview,
|
||||||
normalizeRechargeRegions,
|
normalizeRechargeRegions,
|
||||||
@ -26,27 +24,14 @@ export async function fetchFinanceSession() {
|
|||||||
permissions.includes(FINANCE_PERMISSIONS.auditWithdrawalApplications) || permissions.includes(FINANCE_PERMISSIONS.auditApplication);
|
permissions.includes(FINANCE_PERMISSIONS.auditWithdrawalApplications) || permissions.includes(FINANCE_PERMISSIONS.auditApplication);
|
||||||
const canViewWithdrawalApplications = permissions.includes(FINANCE_PERMISSIONS.viewWithdrawalApplications) || canAuditWithdrawalApplications;
|
const canViewWithdrawalApplications = permissions.includes(FINANCE_PERMISSIONS.viewWithdrawalApplications) || canAuditWithdrawalApplications;
|
||||||
const canViewAppRechargeDetails = permissions.includes(FINANCE_PERMISSIONS.viewAppRechargeDetails);
|
const canViewAppRechargeDetails = permissions.includes(FINANCE_PERMISSIONS.viewAppRechargeDetails);
|
||||||
const canViewCoinSellerRechargeOrders = permissions.includes(FINANCE_PERMISSIONS.viewCoinSellerRechargeOrders);
|
|
||||||
const canCreateCoinSellerRechargeOrder = permissions.includes(FINANCE_PERMISSIONS.createCoinSellerRechargeOrder);
|
|
||||||
const canVerifyCoinSellerRechargeOrder = permissions.includes(FINANCE_PERMISSIONS.verifyCoinSellerRechargeOrder);
|
|
||||||
const canGrantCoinSellerRechargeOrder = permissions.includes(FINANCE_PERMISSIONS.grantCoinSellerRechargeOrder);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...session,
|
...session,
|
||||||
canAccessFinance:
|
canAccessFinance: canCreateApplication || canAuditApplication || canViewWithdrawalApplications || canViewAppRechargeDetails,
|
||||||
canCreateApplication ||
|
|
||||||
canAuditApplication ||
|
|
||||||
canViewWithdrawalApplications ||
|
|
||||||
canViewAppRechargeDetails ||
|
|
||||||
canViewCoinSellerRechargeOrders,
|
|
||||||
canAuditApplication,
|
canAuditApplication,
|
||||||
canAuditWithdrawalApplications,
|
canAuditWithdrawalApplications,
|
||||||
canCreateApplication,
|
canCreateApplication,
|
||||||
canCreateCoinSellerRechargeOrder,
|
|
||||||
canGrantCoinSellerRechargeOrder,
|
|
||||||
canVerifyCoinSellerRechargeOrder,
|
|
||||||
canViewAppRechargeDetails,
|
canViewAppRechargeDetails,
|
||||||
canViewCoinSellerRechargeOrders,
|
|
||||||
canViewWithdrawalApplications,
|
canViewWithdrawalApplications,
|
||||||
permissions
|
permissions
|
||||||
};
|
};
|
||||||
@ -96,48 +81,6 @@ export async function listFinanceWithdrawalApplications(query = {}) {
|
|||||||
return normalizeWithdrawalApplicationPage(data);
|
return normalizeWithdrawalApplicationPage(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function listFinanceCoinSellerRechargeOrders(query = {}) {
|
|
||||||
const endpoint = API_ENDPOINTS.listFinanceCoinSellerRechargeOrders;
|
|
||||||
const data = await apiRequest(apiEndpointPath(API_OPERATIONS.listFinanceCoinSellerRechargeOrders), {
|
|
||||||
method: endpoint.method,
|
|
||||||
query
|
|
||||||
});
|
|
||||||
return normalizeCoinSellerRechargeOrderPage(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function createFinanceCoinSellerRechargeOrder(payload) {
|
|
||||||
const endpoint = API_ENDPOINTS.createFinanceCoinSellerRechargeOrder;
|
|
||||||
const data = await apiRequest(apiEndpointPath(API_OPERATIONS.createFinanceCoinSellerRechargeOrder), {
|
|
||||||
body: payload,
|
|
||||||
method: endpoint.method
|
|
||||||
});
|
|
||||||
return normalizeCoinSellerRechargeOrder(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function verifyFinanceCoinSellerRechargeReceipt(payload) {
|
|
||||||
const endpoint = API_ENDPOINTS.verifyFinanceCoinSellerRechargeReceipt;
|
|
||||||
return apiRequest(apiEndpointPath(API_OPERATIONS.verifyFinanceCoinSellerRechargeReceipt), {
|
|
||||||
body: cleanPayload(payload),
|
|
||||||
method: endpoint.method
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function verifyFinanceCoinSellerRechargeOrder(orderId) {
|
|
||||||
const endpoint = API_ENDPOINTS.verifyFinanceCoinSellerRechargeOrder;
|
|
||||||
const data = await apiRequest(apiEndpointPath(API_OPERATIONS.verifyFinanceCoinSellerRechargeOrder, { order_id: orderId }), {
|
|
||||||
method: endpoint.method
|
|
||||||
});
|
|
||||||
return normalizeCoinSellerRechargeOrder(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function grantFinanceCoinSellerRechargeOrder(orderId) {
|
|
||||||
const endpoint = API_ENDPOINTS.grantFinanceCoinSellerRechargeOrder;
|
|
||||||
const data = await apiRequest(apiEndpointPath(API_OPERATIONS.grantFinanceCoinSellerRechargeOrder, { order_id: orderId }), {
|
|
||||||
method: endpoint.method
|
|
||||||
});
|
|
||||||
return normalizeCoinSellerRechargeOrder(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function listFinanceRechargeDetails(query = {}, apps = []) {
|
export async function listFinanceRechargeDetails(query = {}, apps = []) {
|
||||||
const page = positiveInteger(query.page, 1);
|
const page = positiveInteger(query.page, 1);
|
||||||
const pageSize = positiveInteger(query.page_size ?? query.pageSize, 50);
|
const pageSize = positiveInteger(query.page_size ?? query.pageSize, 50);
|
||||||
@ -383,12 +326,6 @@ function cleanRechargeBillQuery(query = {}) {
|
|||||||
return rest;
|
return rest;
|
||||||
}
|
}
|
||||||
|
|
||||||
function cleanPayload(payload = {}) {
|
|
||||||
return Object.fromEntries(
|
|
||||||
Object.entries(payload).filter(([, value]) => value !== undefined && value !== null && value !== "")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function positiveInteger(value, fallback) {
|
function positiveInteger(value, fallback) {
|
||||||
const number = Number(value);
|
const number = Number(value);
|
||||||
return Number.isInteger(number) && number > 0 ? number : fallback;
|
return Number.isInteger(number) && number > 0 ? number : fallback;
|
||||||
|
|||||||
@ -4,19 +4,14 @@ import {
|
|||||||
approveFinanceApplication,
|
approveFinanceApplication,
|
||||||
approveFinanceWithdrawalApplication,
|
approveFinanceWithdrawalApplication,
|
||||||
createFinanceApplication,
|
createFinanceApplication,
|
||||||
createFinanceCoinSellerRechargeOrder,
|
|
||||||
fetchFinanceApplicationOptions,
|
fetchFinanceApplicationOptions,
|
||||||
filterOperationsByPermissions,
|
filterOperationsByPermissions,
|
||||||
grantFinanceCoinSellerRechargeOrder,
|
|
||||||
listFinanceApplications,
|
listFinanceApplications,
|
||||||
listFinanceCoinSellerRechargeOrders,
|
|
||||||
listFinanceRechargeDetails,
|
listFinanceRechargeDetails,
|
||||||
listMyFinanceApplications,
|
listMyFinanceApplications,
|
||||||
listFinanceWithdrawalApplications,
|
listFinanceWithdrawalApplications,
|
||||||
rejectFinanceApplication,
|
rejectFinanceApplication,
|
||||||
rejectFinanceWithdrawalApplication,
|
rejectFinanceWithdrawalApplication
|
||||||
verifyFinanceCoinSellerRechargeReceipt,
|
|
||||||
verifyFinanceCoinSellerRechargeOrder
|
|
||||||
} from "./api.js";
|
} from "./api.js";
|
||||||
import { FINANCE_OPERATIONS } from "./constants.js";
|
import { FINANCE_OPERATIONS } from "./constants.js";
|
||||||
|
|
||||||
@ -97,56 +92,6 @@ test("finance application APIs use generated endpoint paths", async () => {
|
|||||||
expect(JSON.parse(String(calls[8][1]?.body))).toEqual({ auditRemark: "bad" });
|
expect(JSON.parse(String(calls[8][1]?.body))).toEqual({ auditRemark: "bad" });
|
||||||
});
|
});
|
||||||
|
|
||||||
test("finance coin seller recharge order APIs use generated endpoint paths", async () => {
|
|
||||||
vi.stubGlobal(
|
|
||||||
"fetch",
|
|
||||||
vi.fn(async (url) => {
|
|
||||||
if (String(url).includes("/coin-seller-recharges?")) {
|
|
||||||
return new Response(JSON.stringify({ code: 0, data: { items: [], page: 1, pageSize: 50, total: 0 } }));
|
|
||||||
}
|
|
||||||
return new Response(JSON.stringify({ code: 0, data: { id: "order-1", targetUserId: "10001" } }));
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
await listFinanceCoinSellerRechargeOrders({ page: 1, page_size: 50, provider_code: "mifapay" });
|
|
||||||
await createFinanceCoinSellerRechargeOrder({
|
|
||||||
appCode: "lalu",
|
|
||||||
coinAmount: 100000,
|
|
||||||
externalOrderNo: "MIFA-001",
|
|
||||||
providerCode: "mifapay",
|
|
||||||
targetUserId: "10001",
|
|
||||||
usdAmount: 10
|
|
||||||
});
|
|
||||||
await verifyFinanceCoinSellerRechargeReceipt({
|
|
||||||
appCode: "lalu",
|
|
||||||
externalOrderNo: "MIFA-001",
|
|
||||||
providerCode: "mifapay",
|
|
||||||
usdAmount: 10
|
|
||||||
});
|
|
||||||
await verifyFinanceCoinSellerRechargeOrder("order-1");
|
|
||||||
await grantFinanceCoinSellerRechargeOrder("order-1");
|
|
||||||
|
|
||||||
const calls = vi.mocked(fetch).mock.calls;
|
|
||||||
expect(String(calls[0][0])).toContain("/api/v1/admin/finance/orders/coin-seller-recharges?");
|
|
||||||
expect(String(calls[0][0])).toContain("provider_code=mifapay");
|
|
||||||
expect(calls[0][1]?.method).toBe("GET");
|
|
||||||
expect(String(calls[1][0])).toContain("/api/v1/admin/finance/orders/coin-seller-recharges");
|
|
||||||
expect(calls[1][1]?.method).toBe("POST");
|
|
||||||
expect(JSON.parse(String(calls[1][1]?.body))).toMatchObject({ externalOrderNo: "MIFA-001", providerCode: "mifapay" });
|
|
||||||
expect(String(calls[2][0])).toContain("/api/v1/admin/finance/orders/coin-seller-recharges/receipt-verifications");
|
|
||||||
expect(calls[2][1]?.method).toBe("POST");
|
|
||||||
expect(JSON.parse(String(calls[2][1]?.body))).toEqual({
|
|
||||||
appCode: "lalu",
|
|
||||||
externalOrderNo: "MIFA-001",
|
|
||||||
providerCode: "mifapay",
|
|
||||||
usdAmount: 10
|
|
||||||
});
|
|
||||||
expect(String(calls[3][0])).toContain("/api/v1/admin/finance/orders/coin-seller-recharges/order-1/verify");
|
|
||||||
expect(calls[3][1]?.method).toBe("POST");
|
|
||||||
expect(String(calls[4][0])).toContain("/api/v1/admin/finance/orders/coin-seller-recharges/order-1/grant");
|
|
||||||
expect(calls[4][1]?.method).toBe("POST");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("filters operation options by backend-configured permissions", () => {
|
test("filters operation options by backend-configured permissions", () => {
|
||||||
const allowed = filterOperationsByPermissions(FINANCE_OPERATIONS, [
|
const allowed = filterOperationsByPermissions(FINANCE_OPERATIONS, [
|
||||||
"finance-operation:user-coin-credit",
|
"finance-operation:user-coin-credit",
|
||||||
|
|||||||
@ -1,539 +0,0 @@
|
|||||||
import AddOutlined from "@mui/icons-material/AddOutlined";
|
|
||||||
import FactCheckOutlined from "@mui/icons-material/FactCheckOutlined";
|
|
||||||
import PaidOutlined from "@mui/icons-material/PaidOutlined";
|
|
||||||
import RefreshOutlined from "@mui/icons-material/RefreshOutlined";
|
|
||||||
import Alert from "@mui/material/Alert";
|
|
||||||
import Button from "@mui/material/Button";
|
|
||||||
import Dialog from "@mui/material/Dialog";
|
|
||||||
import DialogActions from "@mui/material/DialogActions";
|
|
||||||
import DialogContent from "@mui/material/DialogContent";
|
|
||||||
import DialogTitle from "@mui/material/DialogTitle";
|
|
||||||
import InputAdornment from "@mui/material/InputAdornment";
|
|
||||||
import MenuItem from "@mui/material/MenuItem";
|
|
||||||
import TextField from "@mui/material/TextField";
|
|
||||||
import { useState } from "react";
|
|
||||||
import {
|
|
||||||
COIN_SELLER_RECHARGE_CHAINS,
|
|
||||||
COIN_SELLER_RECHARGE_PROVIDERS,
|
|
||||||
COIN_SELLER_RECHARGE_STATUS,
|
|
||||||
DEFAULT_COIN_SELLER_RECHARGE_ORDER_FORM,
|
|
||||||
} from "../constants.js";
|
|
||||||
import {
|
|
||||||
buildCoinSellerRechargeReceiptPayload,
|
|
||||||
buildCoinSellerRechargeOrderPayload,
|
|
||||||
coinSellerRechargeChainLabel,
|
|
||||||
coinSellerRechargeGrantStatusLabel,
|
|
||||||
coinSellerRechargeProviderLabel,
|
|
||||||
coinSellerRechargeStatusLabel,
|
|
||||||
coinSellerRechargeStatusTone,
|
|
||||||
coinSellerRechargeVerifyStatusLabel,
|
|
||||||
formatAmount,
|
|
||||||
formatTime,
|
|
||||||
formatUsdMinor,
|
|
||||||
isCoinSellerRechargeGranted,
|
|
||||||
isCoinSellerRechargeVerified,
|
|
||||||
validateCoinSellerRechargeOrderPayload,
|
|
||||||
} from "../format.js";
|
|
||||||
|
|
||||||
export function FinanceCoinSellerRechargeOrderList({
|
|
||||||
actionLoading,
|
|
||||||
apps,
|
|
||||||
canCreate,
|
|
||||||
canGrant,
|
|
||||||
canVerify,
|
|
||||||
error,
|
|
||||||
filters,
|
|
||||||
loading,
|
|
||||||
onCreate,
|
|
||||||
onFiltersChange,
|
|
||||||
onGrant,
|
|
||||||
onReload,
|
|
||||||
onVerify,
|
|
||||||
onVerifyReceipt,
|
|
||||||
orders,
|
|
||||||
}) {
|
|
||||||
const [createOpen, setCreateOpen] = useState(false);
|
|
||||||
const [form, setForm] = useState(DEFAULT_COIN_SELLER_RECHARGE_ORDER_FORM);
|
|
||||||
const [formError, setFormError] = useState("");
|
|
||||||
const [receiptVerification, setReceiptVerification] = useState(null);
|
|
||||||
const [receiptLoading, setReceiptLoading] = useState(false);
|
|
||||||
const items = orders.items || [];
|
|
||||||
const page = Number(orders.page || filters.page || 1);
|
|
||||||
const pageSize = Number(orders.pageSize || 50);
|
|
||||||
const total = Number(orders.total || 0);
|
|
||||||
const hasNextPage = page * pageSize < total;
|
|
||||||
|
|
||||||
const openCreate = () => {
|
|
||||||
setForm({
|
|
||||||
...DEFAULT_COIN_SELLER_RECHARGE_ORDER_FORM,
|
|
||||||
appCode: filters.appCode || apps[0]?.appCode || "",
|
|
||||||
});
|
|
||||||
setFormError("");
|
|
||||||
setReceiptVerification(null);
|
|
||||||
setCreateOpen(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateForm = (key, value) => {
|
|
||||||
if (key === "providerCode") {
|
|
||||||
setForm((current) => ({
|
|
||||||
...current,
|
|
||||||
chain: "",
|
|
||||||
providerCode: value,
|
|
||||||
}));
|
|
||||||
setFormError("");
|
|
||||||
setReceiptVerification(null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setForm((current) => ({ ...current, [key]: value }));
|
|
||||||
setFormError("");
|
|
||||||
// 订单号校验结果绑定当前 APP、用户输入金额、渠道、链和订单号;任一字段变化都不能复用旧凭证。
|
|
||||||
setReceiptVerification(null);
|
|
||||||
};
|
|
||||||
|
|
||||||
const submitCreate = async (event) => {
|
|
||||||
event.preventDefault();
|
|
||||||
if (!receiptVerification?.verified) {
|
|
||||||
setFormError("请先校验订单号");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const payload = buildCoinSellerRechargeOrderPayload(form);
|
|
||||||
const validationMessage = validateCoinSellerRechargeOrderPayload(payload);
|
|
||||||
if (validationMessage) {
|
|
||||||
setFormError(validationMessage);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const created = await onCreate(payload);
|
|
||||||
if (created) {
|
|
||||||
setCreateOpen(false);
|
|
||||||
setReceiptVerification(null);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const verifyCreateReceipt = async () => {
|
|
||||||
const payload = buildCoinSellerRechargeReceiptPayload(form);
|
|
||||||
const validationMessage = validateCoinSellerRechargeOrderPayload({
|
|
||||||
...buildCoinSellerRechargeOrderPayload(form),
|
|
||||||
coinAmount: 1,
|
|
||||||
targetUserId: "verification-only",
|
|
||||||
});
|
|
||||||
if (validationMessage) {
|
|
||||||
setFormError(validationMessage);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setFormError("");
|
|
||||||
setReceiptLoading(true);
|
|
||||||
try {
|
|
||||||
const verification = await onVerifyReceipt(payload);
|
|
||||||
setReceiptVerification(verification);
|
|
||||||
if (!verification?.verified) {
|
|
||||||
setFormError(verification?.failureReason || "订单号校验未通过");
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
setReceiptLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const confirmGrant = (order) => {
|
|
||||||
const userText = order.targetDisplayUserId || order.targetUserId || "-";
|
|
||||||
const ok = window.confirm(
|
|
||||||
isZeroCoinMakeupOrder(order)
|
|
||||||
? `确认完成用户 ${userText} 的补单?该订单计入充值,不发放金币。`
|
|
||||||
: `确认给用户 ${userText} 发放 ${formatAmount(order.coinAmount)} 金币?`,
|
|
||||||
);
|
|
||||||
if (ok) {
|
|
||||||
onGrant(order);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<section className="finance-card finance-list">
|
|
||||||
<div className="finance-filterrow">
|
|
||||||
<TextField
|
|
||||||
className="finance-filterrow__region"
|
|
||||||
label="APP"
|
|
||||||
select
|
|
||||||
size="small"
|
|
||||||
value={filters.appCode}
|
|
||||||
onChange={(event) => onFiltersChange({ appCode: event.target.value })}
|
|
||||||
>
|
|
||||||
<MenuItem value="">全部 APP</MenuItem>
|
|
||||||
{apps.map((app) => (
|
|
||||||
<MenuItem key={app.appCode} value={app.appCode}>
|
|
||||||
{app.appName || app.appCode}
|
|
||||||
</MenuItem>
|
|
||||||
))}
|
|
||||||
</TextField>
|
|
||||||
<TextField
|
|
||||||
label="渠道"
|
|
||||||
select
|
|
||||||
size="small"
|
|
||||||
value={filters.providerCode}
|
|
||||||
onChange={(event) => onFiltersChange({ providerCode: event.target.value })}
|
|
||||||
>
|
|
||||||
<MenuItem value="">全部渠道</MenuItem>
|
|
||||||
{COIN_SELLER_RECHARGE_PROVIDERS.map(([value, label]) => (
|
|
||||||
<MenuItem key={value} value={value}>
|
|
||||||
{label}
|
|
||||||
</MenuItem>
|
|
||||||
))}
|
|
||||||
</TextField>
|
|
||||||
<TextField
|
|
||||||
label="订单状态"
|
|
||||||
select
|
|
||||||
size="small"
|
|
||||||
value={filters.status}
|
|
||||||
onChange={(event) => onFiltersChange({ status: event.target.value })}
|
|
||||||
>
|
|
||||||
<MenuItem value="">全部状态</MenuItem>
|
|
||||||
{COIN_SELLER_RECHARGE_STATUS.map(([value, label]) => (
|
|
||||||
<MenuItem key={value} value={value}>
|
|
||||||
{label}
|
|
||||||
</MenuItem>
|
|
||||||
))}
|
|
||||||
</TextField>
|
|
||||||
<TextField
|
|
||||||
label="搜索"
|
|
||||||
size="small"
|
|
||||||
value={filters.keyword}
|
|
||||||
onChange={(event) => onFiltersChange({ keyword: event.target.value })}
|
|
||||||
/>
|
|
||||||
<span className="finance-filterrow__spacer" />
|
|
||||||
<Button startIcon={<RefreshOutlined fontSize="small" />} variant="outlined" onClick={onReload}>
|
|
||||||
刷新
|
|
||||||
</Button>
|
|
||||||
<Button disabled={!canCreate} startIcon={<AddOutlined fontSize="small" />} variant="contained" onClick={openCreate}>
|
|
||||||
创建币商充值
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{error ? <div className="finance-error">{error}</div> : null}
|
|
||||||
{loading && !items.length ? <div className="finance-skeleton" /> : null}
|
|
||||||
{!loading || items.length ? (
|
|
||||||
<div className="finance-tablewrap">
|
|
||||||
<table className="finance-flat-table finance-flat-table--orders">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>订单</th>
|
|
||||||
<th>APP</th>
|
|
||||||
<th>目标用户</th>
|
|
||||||
<th className="r">金额</th>
|
|
||||||
<th>渠道</th>
|
|
||||||
<th>三方订单</th>
|
|
||||||
<th>备注</th>
|
|
||||||
<th>状态</th>
|
|
||||||
<th>钱包</th>
|
|
||||||
<th>操作人</th>
|
|
||||||
<th>时间</th>
|
|
||||||
<th className="r">操作</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{items.length ? (
|
|
||||||
items.map((item) => (
|
|
||||||
<tr className="finance-flat-table__row" key={item.id}>
|
|
||||||
<td className="finance-order-cell">{item.id || "-"}</td>
|
|
||||||
<td>{appName(item.appCode, apps)}</td>
|
|
||||||
<td>
|
|
||||||
<Stacked primary={item.targetDisplayUserId || item.targetUserId} secondary={item.targetUserId} />
|
|
||||||
</td>
|
|
||||||
<td className="r num">
|
|
||||||
<Stacked
|
|
||||||
primary={usdText(item)}
|
|
||||||
secondary={`${formatAmount(item.coinAmount)} 金币${isZeroCoinMakeupOrder(item) ? "(补单)" : ""}`}
|
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<Stacked primary={coinSellerRechargeProviderLabel(item.providerCode)} secondary={providerMeta(item)} />
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<Stacked primary={item.externalOrderNo || "-"} secondary={providerAmountText(item)} />
|
|
||||||
</td>
|
|
||||||
<td className="finance-remark-cell">{item.remark || "-"}</td>
|
|
||||||
<td>
|
|
||||||
<Stacked
|
|
||||||
primary={<StatusBadge status={item.status} />}
|
|
||||||
secondary={`${coinSellerRechargeVerifyStatusLabel(item.verifyStatus)} / ${coinSellerRechargeGrantStatusLabel(item.grantStatus)}`}
|
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<Stacked
|
|
||||||
primary={item.walletTransactionId || item.walletCommandId || "-"}
|
|
||||||
secondary={walletMeta(item)}
|
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<Stacked
|
|
||||||
primary={item.operatorName || "-"}
|
|
||||||
secondary={[
|
|
||||||
item.verifiedByName && `校验:${item.verifiedByName}`,
|
|
||||||
item.grantedByName && `${isZeroCoinMakeupOrder(item) ? "补单" : "发放"}:${item.grantedByName}`,
|
|
||||||
]
|
|
||||||
.filter(Boolean)
|
|
||||||
.join(" / ")}
|
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<Stacked
|
|
||||||
primary={formatTime(item.createdAtMs)}
|
|
||||||
secondary={[
|
|
||||||
item.verifiedAtMs && `校验 ${formatTime(item.verifiedAtMs)}`,
|
|
||||||
item.grantedAtMs && `${isZeroCoinMakeupOrder(item) ? "补单" : "发放"} ${formatTime(item.grantedAtMs)}`,
|
|
||||||
]
|
|
||||||
.filter(Boolean)
|
|
||||||
.join(" / ")}
|
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
<td className="r">
|
|
||||||
<span className="finance-row-actions">
|
|
||||||
<Button
|
|
||||||
disabled={
|
|
||||||
!canVerify ||
|
|
||||||
actionLoading === `verify:${item.id}` ||
|
|
||||||
isCoinSellerRechargeVerified(item) ||
|
|
||||||
isCoinSellerRechargeGranted(item)
|
|
||||||
}
|
|
||||||
size="small"
|
|
||||||
startIcon={<FactCheckOutlined fontSize="small" />}
|
|
||||||
variant="outlined"
|
|
||||||
onClick={() => onVerify(item)}
|
|
||||||
>
|
|
||||||
校验
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
disabled={
|
|
||||||
!canGrant ||
|
|
||||||
actionLoading === `grant:${item.id}` ||
|
|
||||||
!isCoinSellerRechargeVerified(item) ||
|
|
||||||
isCoinSellerRechargeGranted(item)
|
|
||||||
}
|
|
||||||
size="small"
|
|
||||||
startIcon={<PaidOutlined fontSize="small" />}
|
|
||||||
variant="contained"
|
|
||||||
onClick={() => confirmGrant(item)}
|
|
||||||
>
|
|
||||||
{isZeroCoinMakeupOrder(item) ? "完成补单" : "发放"}
|
|
||||||
</Button>
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
))
|
|
||||||
) : (
|
|
||||||
<tr>
|
|
||||||
<td colSpan={12} style={{ textAlign: "center" }}>
|
|
||||||
当前无数据
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
)}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
) : 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}
|
|
||||||
<CreateDialog
|
|
||||||
apps={apps}
|
|
||||||
form={form}
|
|
||||||
formError={formError}
|
|
||||||
loading={actionLoading === "create"}
|
|
||||||
open={createOpen}
|
|
||||||
receiptLoading={receiptLoading}
|
|
||||||
receiptVerification={receiptVerification}
|
|
||||||
onChange={updateForm}
|
|
||||||
onClose={() => {
|
|
||||||
setCreateOpen(false);
|
|
||||||
setReceiptVerification(null);
|
|
||||||
setFormError("");
|
|
||||||
}}
|
|
||||||
onSubmit={submitCreate}
|
|
||||||
onVerifyReceipt={verifyCreateReceipt}
|
|
||||||
/>
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function CreateDialog({
|
|
||||||
apps,
|
|
||||||
form,
|
|
||||||
formError,
|
|
||||||
loading,
|
|
||||||
onChange,
|
|
||||||
onClose,
|
|
||||||
onSubmit,
|
|
||||||
onVerifyReceipt,
|
|
||||||
open,
|
|
||||||
receiptLoading,
|
|
||||||
receiptVerification,
|
|
||||||
}) {
|
|
||||||
const disabled = loading || receiptLoading;
|
|
||||||
const canCreate = receiptVerification?.verified === true && !disabled;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dialog fullWidth maxWidth={false} open={open} slotProps={{ paper: { className: "finance-create-dialog-paper finance-create-dialog-paper--coin-seller" } }} onClose={disabled ? undefined : onClose}>
|
|
||||||
<DialogTitle>创建币商充值</DialogTitle>
|
|
||||||
<DialogContent className="finance-create-dialog">
|
|
||||||
<form className="finance-form finance-form--dialog" id="coin-seller-recharge-order-form" onSubmit={onSubmit}>
|
|
||||||
{formError ? <Alert severity="error">{formError}</Alert> : null}
|
|
||||||
<div className={form.providerCode === "usdt" ? "finance-form-grid finance-form-grid--coin-seller-create" : "finance-form-grid finance-form-grid--coin-seller-create finance-form-grid--coin-seller-no-chain"}>
|
|
||||||
{apps.length ? (
|
|
||||||
<TextField disabled={disabled} label="APP" required select value={form.appCode} onChange={(event) => onChange("appCode", event.target.value)}>
|
|
||||||
{apps.map((app) => (
|
|
||||||
<MenuItem key={app.appCode} value={app.appCode}>
|
|
||||||
{app.appName || app.appCode}
|
|
||||||
</MenuItem>
|
|
||||||
))}
|
|
||||||
</TextField>
|
|
||||||
) : (
|
|
||||||
<TextField disabled={disabled} label="APP" required value={form.appCode} onChange={(event) => onChange("appCode", event.target.value)} />
|
|
||||||
)}
|
|
||||||
<TextField disabled={disabled} label="目标用户 ID" required value={form.targetUserId} onChange={(event) => onChange("targetUserId", event.target.value)} />
|
|
||||||
<TextField
|
|
||||||
disabled={disabled}
|
|
||||||
InputProps={{ startAdornment: <InputAdornment position="start">USD</InputAdornment> }}
|
|
||||||
inputProps={{ min: 0, step: "0.01" }}
|
|
||||||
label="USD 金额"
|
|
||||||
required
|
|
||||||
type="number"
|
|
||||||
value={form.usdAmount}
|
|
||||||
onChange={(event) => onChange("usdAmount", event.target.value)}
|
|
||||||
/>
|
|
||||||
<TextField
|
|
||||||
disabled={disabled}
|
|
||||||
InputProps={{ endAdornment: <InputAdornment position="end">金币</InputAdornment> }}
|
|
||||||
inputProps={{ min: 0, step: "1" }}
|
|
||||||
label="充值金币"
|
|
||||||
required
|
|
||||||
type="number"
|
|
||||||
value={form.coinAmount}
|
|
||||||
onChange={(event) => onChange("coinAmount", event.target.value)}
|
|
||||||
/>
|
|
||||||
<TextField disabled={disabled} label="支付渠道" required select value={form.providerCode} onChange={(event) => onChange("providerCode", event.target.value)}>
|
|
||||||
{COIN_SELLER_RECHARGE_PROVIDERS.map(([value, label]) => (
|
|
||||||
<MenuItem key={value} value={value}>
|
|
||||||
{label}
|
|
||||||
</MenuItem>
|
|
||||||
))}
|
|
||||||
</TextField>
|
|
||||||
{form.providerCode === "usdt" ? (
|
|
||||||
<TextField disabled={disabled} label="USDT 链" required select value={form.chain} onChange={(event) => onChange("chain", event.target.value)}>
|
|
||||||
{COIN_SELLER_RECHARGE_CHAINS.map(([value, label]) => (
|
|
||||||
<MenuItem key={value} value={value}>
|
|
||||||
{label}
|
|
||||||
</MenuItem>
|
|
||||||
))}
|
|
||||||
</TextField>
|
|
||||||
) : null}
|
|
||||||
<div className="finance-receipt-field">
|
|
||||||
<TextField disabled={disabled} label="三方订单号" required value={form.externalOrderNo} onChange={(event) => onChange("externalOrderNo", event.target.value)} />
|
|
||||||
<Button disabled={disabled || !onVerifyReceipt} startIcon={<FactCheckOutlined fontSize="small" />} variant="outlined" onClick={onVerifyReceipt}>
|
|
||||||
校验
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<TextField
|
|
||||||
disabled={disabled}
|
|
||||||
inputProps={{ maxLength: 512 }}
|
|
||||||
label="备注"
|
|
||||||
value={form.remark}
|
|
||||||
onChange={(event) => onChange("remark", event.target.value)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{receiptVerification ? <ReceiptVerificationBanner verification={receiptVerification} /> : null}
|
|
||||||
</form>
|
|
||||||
</DialogContent>
|
|
||||||
<DialogActions>
|
|
||||||
<Button disabled={disabled} variant="outlined" onClick={onClose}>
|
|
||||||
取消
|
|
||||||
</Button>
|
|
||||||
<Button disabled={!canCreate} form="coin-seller-recharge-order-form" type="submit" variant="contained">
|
|
||||||
创建
|
|
||||||
</Button>
|
|
||||||
</DialogActions>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function isZeroCoinMakeupOrder(order) {
|
|
||||||
return Number(order?.coinAmount || 0) === 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
function ReceiptVerificationBanner({ verification }) {
|
|
||||||
const detail = [
|
|
||||||
verification.status && `状态 ${verification.status}`,
|
|
||||||
verification.providerOrderId && `三方单 ${verification.providerOrderId}`,
|
|
||||||
verification.currencyCode && verification.providerAmountMinor !== undefined
|
|
||||||
? formatUsdMinor(verification.providerAmountMinor, verification.currencyCode)
|
|
||||||
: "",
|
|
||||||
verification.failureReason,
|
|
||||||
]
|
|
||||||
.filter(Boolean)
|
|
||||||
.join(" / ");
|
|
||||||
return (
|
|
||||||
<Alert className="finance-receipt-result" severity={verification.verified ? "success" : "error"}>
|
|
||||||
{verification.verified ? "校验通过" : "校验未通过"}
|
|
||||||
{detail ? `:${detail}` : ""}
|
|
||||||
</Alert>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function StatusBadge({ status }) {
|
|
||||||
return <span className={`finance-status finance-status--${coinSellerRechargeStatusTone(status)}`}>{coinSellerRechargeStatusLabel(status)}</span>;
|
|
||||||
}
|
|
||||||
|
|
||||||
function Stacked({ primary, secondary }) {
|
|
||||||
return (
|
|
||||||
<span className="finance-stack">
|
|
||||||
<span>{primary || "-"}</span>
|
|
||||||
<small>{secondary || ""}</small>
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function appName(appCode, apps) {
|
|
||||||
return apps.find((app) => app.appCode === appCode)?.appName || appCode || "-";
|
|
||||||
}
|
|
||||||
|
|
||||||
function usdText(item) {
|
|
||||||
if (item.usdAmount !== undefined && item.usdAmount !== null && item.usdAmount !== "") {
|
|
||||||
return `USD ${item.usdAmount}`;
|
|
||||||
}
|
|
||||||
return formatUsdMinor(item.usdMinorAmount, "USD");
|
|
||||||
}
|
|
||||||
|
|
||||||
function providerAmountText(item) {
|
|
||||||
return item.providerAmountMinor === null || item.providerAmountMinor === undefined
|
|
||||||
? "-"
|
|
||||||
: formatUsdMinor(item.providerAmountMinor, item.providerCurrencyCode || "USD");
|
|
||||||
}
|
|
||||||
|
|
||||||
function providerMeta(item) {
|
|
||||||
if (item.providerCode === "usdt") {
|
|
||||||
return [coinSellerRechargeChainLabel(item.chain), item.providerStatus].filter(Boolean).join(" / ") || "-";
|
|
||||||
}
|
|
||||||
if (item.providerCode === "mifapay") {
|
|
||||||
return [item.providerStatus, item.providerOrderId].filter(Boolean).join(" / ") || "-";
|
|
||||||
}
|
|
||||||
if (item.providerCode === "v5pay") {
|
|
||||||
return [item.providerStatus, item.providerCurrencyCode, item.providerOrderId].filter(Boolean).join(" / ") || "-";
|
|
||||||
}
|
|
||||||
return "-";
|
|
||||||
}
|
|
||||||
|
|
||||||
function walletMeta(item) {
|
|
||||||
return [
|
|
||||||
item.walletAssetType,
|
|
||||||
item.walletAmountDelta === null || item.walletAmountDelta === undefined ? "" : `变动 ${formatAmount(item.walletAmountDelta)}`,
|
|
||||||
item.walletBalanceAfter === null || item.walletBalanceAfter === undefined ? "" : `余额 ${formatAmount(item.walletBalanceAfter)}`,
|
|
||||||
]
|
|
||||||
.filter(Boolean)
|
|
||||||
.join(" / ");
|
|
||||||
}
|
|
||||||
@ -1,170 +0,0 @@
|
|||||||
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
|
||||||
import { afterEach, expect, test, vi } from "vitest";
|
|
||||||
import { FinanceCoinSellerRechargeOrderList } from "./FinanceCoinSellerRechargeOrderList.jsx";
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
vi.unstubAllGlobals();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("coin seller recharge order create dialog verifies receipt before create", async () => {
|
|
||||||
const onCreate = vi.fn(async () => ({ id: "order-2" }));
|
|
||||||
const onVerifyReceipt = vi.fn(async () => ({
|
|
||||||
currencyCode: "USD",
|
|
||||||
providerAmountMinor: 1000,
|
|
||||||
providerOrderId: "V5-001",
|
|
||||||
status: "paid",
|
|
||||||
verified: true,
|
|
||||||
}));
|
|
||||||
render(<FinanceCoinSellerRechargeOrderList {...baseProps()} onCreate={onCreate} onVerifyReceipt={onVerifyReceipt} />);
|
|
||||||
|
|
||||||
fireEvent.click(screen.getByRole("button", { name: "创建币商充值" }));
|
|
||||||
fireEvent.mouseDown(screen.getByRole("combobox", { name: "支付渠道" }));
|
|
||||||
fireEvent.click(screen.getByRole("option", { name: "V5Pay" }));
|
|
||||||
|
|
||||||
expect(screen.queryByRole("textbox", { name: "三方国家码" })).not.toBeInTheDocument();
|
|
||||||
expect(screen.queryByRole("textbox", { name: "三方币种" })).not.toBeInTheDocument();
|
|
||||||
expect(screen.queryByRole("textbox", { name: "支付类型" })).not.toBeInTheDocument();
|
|
||||||
expect(screen.queryByRole("textbox", { name: "订单日期" })).not.toBeInTheDocument();
|
|
||||||
|
|
||||||
fireEvent.change(screen.getByRole("textbox", { name: "目标用户 ID" }), { target: { value: "10001" } });
|
|
||||||
fireEvent.change(screen.getByRole("spinbutton", { name: "USD 金额" }), { target: { value: "10" } });
|
|
||||||
fireEvent.change(screen.getByRole("spinbutton", { name: "充值金币" }), { target: { value: "0" } });
|
|
||||||
fireEvent.change(screen.getByRole("textbox", { name: "三方订单号" }), { target: { value: "V5-001" } });
|
|
||||||
fireEvent.change(screen.getByRole("textbox", { name: "备注" }), { target: { value: "补单,不发金币" } });
|
|
||||||
expect(screen.getByRole("button", { name: "创建" })).toBeDisabled();
|
|
||||||
|
|
||||||
fireEvent.click(screen.getByRole("button", { name: "校验" }));
|
|
||||||
await screen.findByText(/校验通过/);
|
|
||||||
await waitFor(() => expect(screen.getByRole("button", { name: "创建" })).not.toBeDisabled());
|
|
||||||
expect(onVerifyReceipt).toHaveBeenCalledWith({
|
|
||||||
appCode: "lalu",
|
|
||||||
externalOrderNo: "V5-001",
|
|
||||||
providerCode: "v5pay",
|
|
||||||
usdAmount: 10,
|
|
||||||
});
|
|
||||||
fireEvent.click(screen.getByRole("button", { name: "创建" }));
|
|
||||||
await waitFor(() =>
|
|
||||||
expect(onCreate).toHaveBeenCalledWith({
|
|
||||||
appCode: "lalu",
|
|
||||||
coinAmount: 0,
|
|
||||||
externalOrderNo: "V5-001",
|
|
||||||
providerCode: "v5pay",
|
|
||||||
remark: "补单,不发金币",
|
|
||||||
targetUserId: "10001",
|
|
||||||
usdAmount: 10,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("coin seller recharge order actions follow verify and grant status", () => {
|
|
||||||
const onVerify = vi.fn();
|
|
||||||
const onGrant = vi.fn();
|
|
||||||
vi.stubGlobal("confirm", vi.fn(() => true));
|
|
||||||
|
|
||||||
render(
|
|
||||||
<FinanceCoinSellerRechargeOrderList
|
|
||||||
{...baseProps({
|
|
||||||
orders: {
|
|
||||||
items: [
|
|
||||||
orderFixture({ id: "pending", verifyStatus: "pending", grantStatus: "pending" }),
|
|
||||||
orderFixture({ id: "verified", verifyStatus: "verified", grantStatus: "pending" }),
|
|
||||||
],
|
|
||||||
page: 1,
|
|
||||||
pageSize: 50,
|
|
||||||
total: 2,
|
|
||||||
},
|
|
||||||
})}
|
|
||||||
onGrant={onGrant}
|
|
||||||
onVerify={onVerify}
|
|
||||||
/>,
|
|
||||||
);
|
|
||||||
|
|
||||||
const verifyButtons = screen.getAllByRole("button", { name: "校验" });
|
|
||||||
const grantButtons = screen.getAllByRole("button", { name: "发放" });
|
|
||||||
fireEvent.click(verifyButtons[0]);
|
|
||||||
fireEvent.click(grantButtons[1]);
|
|
||||||
|
|
||||||
expect(onVerify).toHaveBeenCalledWith(expect.objectContaining({ id: "pending" }));
|
|
||||||
expect(onGrant).toHaveBeenCalledWith(expect.objectContaining({ id: "verified" }));
|
|
||||||
expect(screen.getAllByText("人工补单")[0]).toBeInTheDocument();
|
|
||||||
expect(grantButtons[0]).toBeDisabled();
|
|
||||||
expect(verifyButtons[1]).toBeDisabled();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("zero coin seller recharge order uses makeup completion action", () => {
|
|
||||||
const onGrant = vi.fn();
|
|
||||||
const confirm = vi.fn(() => true);
|
|
||||||
vi.stubGlobal("confirm", confirm);
|
|
||||||
|
|
||||||
render(
|
|
||||||
<FinanceCoinSellerRechargeOrderList
|
|
||||||
{...baseProps({
|
|
||||||
orders: {
|
|
||||||
items: [orderFixture({ coinAmount: 0, grantStatus: "pending", id: "makeup", verifyStatus: "verified" })],
|
|
||||||
page: 1,
|
|
||||||
pageSize: 50,
|
|
||||||
total: 1,
|
|
||||||
},
|
|
||||||
})}
|
|
||||||
onGrant={onGrant}
|
|
||||||
/>,
|
|
||||||
);
|
|
||||||
|
|
||||||
fireEvent.click(screen.getByRole("button", { name: "完成补单" }));
|
|
||||||
|
|
||||||
expect(confirm).toHaveBeenCalledWith(expect.stringContaining("不发放金币"));
|
|
||||||
expect(onGrant).toHaveBeenCalledWith(expect.objectContaining({ id: "makeup", coinAmount: 0 }));
|
|
||||||
});
|
|
||||||
|
|
||||||
function baseProps(overrides = {}) {
|
|
||||||
return {
|
|
||||||
actionLoading: "",
|
|
||||||
apps: [{ appCode: "lalu", appName: "lalu" }],
|
|
||||||
canCreate: true,
|
|
||||||
canGrant: true,
|
|
||||||
canVerify: true,
|
|
||||||
error: "",
|
|
||||||
filters: {
|
|
||||||
appCode: "",
|
|
||||||
grantStatus: "",
|
|
||||||
keyword: "",
|
|
||||||
page: 1,
|
|
||||||
providerCode: "",
|
|
||||||
status: "",
|
|
||||||
verifyStatus: "",
|
|
||||||
},
|
|
||||||
loading: false,
|
|
||||||
onCreate: vi.fn(),
|
|
||||||
onFiltersChange: vi.fn(),
|
|
||||||
onGrant: vi.fn(),
|
|
||||||
onReload: vi.fn(),
|
|
||||||
onVerify: vi.fn(),
|
|
||||||
onVerifyReceipt: vi.fn(),
|
|
||||||
orders: {
|
|
||||||
items: [orderFixture()],
|
|
||||||
page: 1,
|
|
||||||
pageSize: 50,
|
|
||||||
total: 1,
|
|
||||||
},
|
|
||||||
...overrides,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function orderFixture(overrides = {}) {
|
|
||||||
return {
|
|
||||||
appCode: "lalu",
|
|
||||||
coinAmount: 100000,
|
|
||||||
createdAtMs: 1760000000000,
|
|
||||||
externalOrderNo: "MIFA-001",
|
|
||||||
grantStatus: "pending",
|
|
||||||
id: "order-1",
|
|
||||||
providerCode: "mifapay",
|
|
||||||
remark: "人工补单",
|
|
||||||
status: "pending",
|
|
||||||
targetDisplayUserId: "165075",
|
|
||||||
targetUserId: "10001",
|
|
||||||
usdAmount: "10.00",
|
|
||||||
verifyStatus: "pending",
|
|
||||||
...overrides,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@ -1,12 +1,8 @@
|
|||||||
import { render, screen } from "@testing-library/react";
|
import { fireEvent, render, screen } from "@testing-library/react";
|
||||||
import { expect, test, vi } from "vitest";
|
import { expect, test, vi } from "vitest";
|
||||||
import { FinanceRechargeDetailList } from "./FinanceRechargeDetailList.jsx";
|
import { FinanceRechargeDetailList } from "./FinanceRechargeDetailList.jsx";
|
||||||
|
|
||||||
const baseProps = {
|
const baseProps = {
|
||||||
apps: [
|
|
||||||
{ appCode: "lalu", appName: "lalu" },
|
|
||||||
{ appCode: "aslan", appName: "Aslan" },
|
|
||||||
],
|
|
||||||
bills: {
|
bills: {
|
||||||
items: [],
|
items: [],
|
||||||
page: 1,
|
page: 1,
|
||||||
@ -24,6 +20,12 @@ const baseProps = {
|
|||||||
loading: false,
|
loading: false,
|
||||||
onFiltersChange: vi.fn(),
|
onFiltersChange: vi.fn(),
|
||||||
onReload: vi.fn(),
|
onReload: vi.fn(),
|
||||||
|
options: {
|
||||||
|
apps: [
|
||||||
|
{ appCode: "lalu", appName: "lalu" },
|
||||||
|
{ appCode: "aslan", appName: "Aslan" },
|
||||||
|
],
|
||||||
|
},
|
||||||
summary: {
|
summary: {
|
||||||
googlePlay: { billCount: 1, coinAmount: 90000, usdMinorAmount: 999 },
|
googlePlay: { billCount: 1, coinAmount: 90000, usdMinorAmount: 999 },
|
||||||
thirdParty: { billCount: 0, coinAmount: 0, usdMinorAmount: 0 },
|
thirdParty: { billCount: 0, coinAmount: 0, usdMinorAmount: 0 },
|
||||||
@ -44,14 +46,18 @@ test("hides coin recharge columns by default", () => {
|
|||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
expect(screen.getByRole("switch", { name: "金币显示" })).not.toBeChecked();
|
||||||
expect(screen.queryByRole("columnheader", { name: "充值金币" })).not.toBeInTheDocument();
|
expect(screen.queryByRole("columnheader", { name: "充值金币" })).not.toBeInTheDocument();
|
||||||
|
expect(screen.queryByRole("columnheader", { name: "金币换算" })).not.toBeInTheDocument();
|
||||||
expect(screen.queryByText("90,000 金币")).not.toBeInTheDocument();
|
expect(screen.queryByText("90,000 金币")).not.toBeInTheDocument();
|
||||||
|
expect(screen.queryByText("90,000 金币 = USD 9.99")).not.toBeInTheDocument();
|
||||||
expect(screen.queryByText("1 笔 · 90,000 金币")).not.toBeInTheDocument();
|
expect(screen.queryByText("1 笔 · 90,000 金币")).not.toBeInTheDocument();
|
||||||
|
expect(screen.getAllByText("1 笔")).toHaveLength(2);
|
||||||
expect(screen.getByRole("columnheader", { name: "账单金额" })).toBeInTheDocument();
|
expect(screen.getByRole("columnheader", { name: "账单金额" })).toBeInTheDocument();
|
||||||
expect(screen.getByRole("columnheader", { name: "用户实付" })).toBeInTheDocument();
|
expect(screen.getByRole("columnheader", { name: "用户实付" })).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("renders app recharge coin columns and values when coin currency is selected", () => {
|
test("renders app recharge coin columns and values when switch is enabled", () => {
|
||||||
render(
|
render(
|
||||||
<FinanceRechargeDetailList
|
<FinanceRechargeDetailList
|
||||||
{...baseProps}
|
{...baseProps}
|
||||||
@ -60,34 +66,39 @@ test("renders app recharge coin columns and values when coin currency is selecte
|
|||||||
items: [rechargeBillFixture()],
|
items: [rechargeBillFixture()],
|
||||||
total: 1,
|
total: 1,
|
||||||
}}
|
}}
|
||||||
currency="coin"
|
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByRole("switch", { name: "金币显示" }));
|
||||||
|
|
||||||
expect(screen.getByRole("columnheader", { name: "APP" })).toBeInTheDocument();
|
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.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("Aslan")).toBeInTheDocument();
|
expect(screen.getByText("Aslan")).toBeInTheDocument();
|
||||||
expect(screen.getByText("90,000 金币")).toBeInTheDocument();
|
expect(screen.getByText("90,000 金币")).toBeInTheDocument();
|
||||||
|
expect(screen.getAllByText("1 笔 · 90,000 金币")).toHaveLength(2);
|
||||||
expect(screen.getAllByText("谷歌充值").length).toBeGreaterThanOrEqual(1);
|
expect(screen.getAllByText("谷歌充值").length).toBeGreaterThanOrEqual(1);
|
||||||
expect(screen.getByText("tx-google")).toBeInTheDocument();
|
expect(screen.getByText("tx-google")).toBeInTheDocument();
|
||||||
expect(screen.getByText("cmd-1 / GPA.1")).toBeInTheDocument();
|
expect(screen.getByText("cmd-1 / GPA.1")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("90,000 金币 = USD 9.99")).toBeInTheDocument();
|
||||||
expect(screen.getAllByText("SAR 12.99")).toHaveLength(2);
|
expect(screen.getAllByText("SAR 12.99")).toHaveLength(2);
|
||||||
expect(screen.getByText("SAR 1.95 / 15%")).toBeInTheDocument();
|
expect(screen.getByText("SAR 1.95 / 15%")).toBeInTheDocument();
|
||||||
|
expect(screen.getByRole("button", { name: /充值时间 · 中国时区/ })).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("keeps empty recharge detail table colspan aligned with visible columns", () => {
|
test("keeps empty recharge detail table colspan aligned with visible columns", () => {
|
||||||
const { container } = render(<FinanceRechargeDetailList {...baseProps} />);
|
const { container } = render(<FinanceRechargeDetailList {...baseProps} />);
|
||||||
|
|
||||||
expect(container.querySelector("tbody td")?.colSpan).toBe(8);
|
expect(container.querySelector("tbody td")?.colSpan).toBe(7);
|
||||||
expect(screen.getByText("当前筛选条件下没有账单")).toBeInTheDocument();
|
expect(screen.getByText("当前无数据")).toBeInTheDocument();
|
||||||
|
|
||||||
const { container: coinContainer } = render(<FinanceRechargeDetailList {...baseProps} currency="coin" />);
|
fireEvent.click(screen.getByRole("switch", { name: "金币显示" }));
|
||||||
expect(coinContainer.querySelector("tbody td")?.colSpan).toBe(9);
|
expect(container.querySelector("tbody td")?.colSpan).toBe(9);
|
||||||
});
|
});
|
||||||
|
|
||||||
function rechargeBillFixture() {
|
function rechargeBillFixture() {
|
||||||
|
|||||||
@ -7,10 +7,6 @@ import RuleFolderOutlined from "@mui/icons-material/RuleFolderOutlined";
|
|||||||
import LinearProgress from "@mui/material/LinearProgress";
|
import LinearProgress from "@mui/material/LinearProgress";
|
||||||
|
|
||||||
const NAV_GROUPS = [
|
const NAV_GROUPS = [
|
||||||
{
|
|
||||||
label: "订单",
|
|
||||||
views: [{ icon: ReceiptLongOutlined, id: "coinSellerRechargeOrders", label: "币商充值" }],
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
label: "经营",
|
label: "经营",
|
||||||
views: [
|
views: [
|
||||||
@ -31,7 +27,6 @@ const NAV_GROUPS = [
|
|||||||
|
|
||||||
const VIEW_TITLES = {
|
const VIEW_TITLES = {
|
||||||
applications: "申请审核",
|
applications: "申请审核",
|
||||||
coinSellerRechargeOrders: "币商充值",
|
|
||||||
create: "发起申请",
|
create: "发起申请",
|
||||||
overview: "财务概览",
|
overview: "财务概览",
|
||||||
recon: "渠道对账",
|
recon: "渠道对账",
|
||||||
@ -44,7 +39,6 @@ export function FinanceShell({
|
|||||||
appBar,
|
appBar,
|
||||||
canAudit,
|
canAudit,
|
||||||
canCreate,
|
canCreate,
|
||||||
canViewCoinSellerRechargeOrders,
|
|
||||||
canViewRechargeDetails,
|
canViewRechargeDetails,
|
||||||
canViewWithdrawals,
|
canViewWithdrawals,
|
||||||
children,
|
children,
|
||||||
@ -58,9 +52,6 @@ export function FinanceShell({
|
|||||||
if (viewId === "overview" || viewId === "rechargeDetails" || viewId === "recon") {
|
if (viewId === "overview" || viewId === "rechargeDetails" || viewId === "recon") {
|
||||||
return canViewRechargeDetails;
|
return canViewRechargeDetails;
|
||||||
}
|
}
|
||||||
if (viewId === "coinSellerRechargeOrders") {
|
|
||||||
return canViewCoinSellerRechargeOrders;
|
|
||||||
}
|
|
||||||
if (viewId === "applications") {
|
if (viewId === "applications") {
|
||||||
return canAudit;
|
return canAudit;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,10 +2,6 @@ export const FINANCE_PERMISSIONS = {
|
|||||||
auditApplication: "finance-application:audit",
|
auditApplication: "finance-application:audit",
|
||||||
auditWithdrawalApplications: "finance-withdrawal:audit",
|
auditWithdrawalApplications: "finance-withdrawal:audit",
|
||||||
createApplication: "finance-application:create",
|
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",
|
|
||||||
viewCoinSellerRechargeOrders: "finance-order:coin-seller-recharge:view",
|
|
||||||
viewAppRechargeDetails: "payment-bill:view",
|
viewAppRechargeDetails: "payment-bill:view",
|
||||||
viewWithdrawalApplications: "finance-withdrawal:view",
|
viewWithdrawalApplications: "finance-withdrawal:view",
|
||||||
};
|
};
|
||||||
@ -59,37 +55,6 @@ export const APPLICATION_STATUS = [
|
|||||||
["rejected", "已拒绝"],
|
["rejected", "已拒绝"],
|
||||||
];
|
];
|
||||||
|
|
||||||
export const COIN_SELLER_RECHARGE_PROVIDERS = [
|
|
||||||
["mifapay", "MiFaPay"],
|
|
||||||
["v5pay", "V5Pay"],
|
|
||||||
["usdt", "USDT"],
|
|
||||||
];
|
|
||||||
|
|
||||||
export const COIN_SELLER_RECHARGE_CHAINS = [
|
|
||||||
["TRON", "TRON"],
|
|
||||||
["BSC", "BSC"],
|
|
||||||
];
|
|
||||||
|
|
||||||
export const COIN_SELLER_RECHARGE_STATUS = [
|
|
||||||
["pending", "待处理"],
|
|
||||||
["verified", "已校验"],
|
|
||||||
["granted", "已发放"],
|
|
||||||
["failed", "失败"],
|
|
||||||
["canceled", "已取消"],
|
|
||||||
];
|
|
||||||
|
|
||||||
export const COIN_SELLER_RECHARGE_VERIFY_STATUS = [
|
|
||||||
["pending", "待校验"],
|
|
||||||
["verified", "校验通过"],
|
|
||||||
["failed", "校验失败"],
|
|
||||||
];
|
|
||||||
|
|
||||||
export const COIN_SELLER_RECHARGE_GRANT_STATUS = [
|
|
||||||
["pending", "待发放"],
|
|
||||||
["granted", "已发放"],
|
|
||||||
["failed", "发放失败"],
|
|
||||||
];
|
|
||||||
|
|
||||||
export const DEFAULT_APPLICATION_FORM = {
|
export const DEFAULT_APPLICATION_FORM = {
|
||||||
appCode: "",
|
appCode: "",
|
||||||
coinAmount: "",
|
coinAmount: "",
|
||||||
@ -101,17 +66,6 @@ export const DEFAULT_APPLICATION_FORM = {
|
|||||||
walletIdentity: "",
|
walletIdentity: "",
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DEFAULT_COIN_SELLER_RECHARGE_ORDER_FORM = {
|
|
||||||
appCode: "",
|
|
||||||
chain: "",
|
|
||||||
coinAmount: "",
|
|
||||||
externalOrderNo: "",
|
|
||||||
providerCode: "mifapay",
|
|
||||||
remark: "",
|
|
||||||
targetUserId: "",
|
|
||||||
usdAmount: "",
|
|
||||||
};
|
|
||||||
|
|
||||||
export function operationRequiresWalletIdentity(operation) {
|
export function operationRequiresWalletIdentity(operation) {
|
||||||
return FINANCE_OPERATIONS.some((item) => item.value === operation && item.requiresWalletIdentity);
|
return FINANCE_OPERATIONS.some((item) => item.value === operation && item.requiresWalletIdentity);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,10 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
APPLICATION_STATUS,
|
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,
|
FINANCE_OPERATIONS,
|
||||||
WALLET_IDENTITIES,
|
WALLET_IDENTITIES,
|
||||||
operationAllowsZeroRechargeAmount,
|
operationAllowsZeroRechargeAmount,
|
||||||
@ -127,110 +122,6 @@ export function normalizeWithdrawalApplicationPage(data = {}) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildCoinSellerRechargeOrderPayload(form) {
|
|
||||||
const providerCode = stringValue(form.providerCode);
|
|
||||||
const payload = {
|
|
||||||
appCode: stringValue(form.appCode),
|
|
||||||
chain: providerCode === "usdt" ? stringValue(form.chain).toUpperCase() : "",
|
|
||||||
coinAmount: numberValue(form.coinAmount),
|
|
||||||
externalOrderNo: stringValue(form.externalOrderNo),
|
|
||||||
providerCode,
|
|
||||||
remark: stringValue(form.remark),
|
|
||||||
targetUserId: stringValue(form.targetUserId),
|
|
||||||
usdAmount: numberValue(form.usdAmount),
|
|
||||||
};
|
|
||||||
|
|
||||||
return Object.fromEntries(Object.entries(payload).filter(([, value]) => value !== ""));
|
|
||||||
}
|
|
||||||
|
|
||||||
export function buildCoinSellerRechargeReceiptPayload(form) {
|
|
||||||
const providerCode = stringValue(form.providerCode);
|
|
||||||
return Object.fromEntries(
|
|
||||||
Object.entries({
|
|
||||||
appCode: stringValue(form.appCode),
|
|
||||||
chain: providerCode === "usdt" ? stringValue(form.chain).toUpperCase() : "",
|
|
||||||
externalOrderNo: stringValue(form.externalOrderNo),
|
|
||||||
providerCode,
|
|
||||||
usdAmount: numberValue(form.usdAmount),
|
|
||||||
}).filter(([, value]) => value !== ""),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function validateCoinSellerRechargeOrderPayload(payload) {
|
|
||||||
if (!payload.appCode) {
|
|
||||||
return "请选择 APP";
|
|
||||||
}
|
|
||||||
if (!payload.targetUserId) {
|
|
||||||
return "请填写目标用户 ID";
|
|
||||||
}
|
|
||||||
if (!Number.isFinite(payload.usdAmount) || payload.usdAmount <= 0) {
|
|
||||||
return "请填写大于 0 的 USD 金额";
|
|
||||||
}
|
|
||||||
if (!Number.isInteger(payload.coinAmount) || payload.coinAmount < 0) {
|
|
||||||
return "请填写不小于 0 的整数金币";
|
|
||||||
}
|
|
||||||
if (!payload.providerCode) {
|
|
||||||
return "请选择支付渠道";
|
|
||||||
}
|
|
||||||
if (!payload.externalOrderNo) {
|
|
||||||
return "请填写三方订单号";
|
|
||||||
}
|
|
||||||
if (payload.providerCode === "usdt" && !payload.chain) {
|
|
||||||
return "请选择 USDT 链";
|
|
||||||
}
|
|
||||||
if (payload.remark && payload.remark.length > 512) {
|
|
||||||
return "备注不能超过 512 字";
|
|
||||||
}
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
export function normalizeCoinSellerRechargeOrder(item = {}) {
|
|
||||||
return {
|
|
||||||
appCode: stringValue(item.appCode ?? item.app_code),
|
|
||||||
chain: stringValue(item.chain),
|
|
||||||
coinAmount: numberValue(item.coinAmount ?? item.coin_amount),
|
|
||||||
createdAtMs: numberOrNull(item.createdAtMs ?? item.created_at_ms),
|
|
||||||
externalOrderNo: stringValue(item.externalOrderNo ?? item.external_order_no),
|
|
||||||
failureReason: stringValue(item.failureReason ?? item.failure_reason),
|
|
||||||
grantStatus: stringValue(item.grantStatus ?? item.grant_status) || "pending",
|
|
||||||
grantedAtMs: numberOrNull(item.grantedAtMs ?? item.granted_at_ms),
|
|
||||||
grantedByName: stringValue(item.grantedByName ?? item.granted_by_name),
|
|
||||||
id: stringValue(item.id ?? item.orderId ?? item.order_id),
|
|
||||||
operatorName: stringValue(item.operatorName ?? item.operator_name),
|
|
||||||
providerAmountMinor: numberOrNull(item.providerAmountMinor ?? item.provider_amount_minor),
|
|
||||||
providerCode: stringValue(item.providerCode ?? item.provider_code),
|
|
||||||
providerCurrencyCode: stringValue(item.providerCurrencyCode ?? item.provider_currency_code),
|
|
||||||
providerOrderId: stringValue(item.providerOrderId ?? item.provider_order_id),
|
|
||||||
providerPayloadJson: stringValue(item.providerPayloadJson ?? item.provider_payload_json),
|
|
||||||
providerStatus: stringValue(item.providerStatus ?? item.provider_status),
|
|
||||||
receiveAddress: stringValue(item.receiveAddress ?? item.receive_address),
|
|
||||||
remark: stringValue(item.remark),
|
|
||||||
status: stringValue(item.status) || "pending",
|
|
||||||
targetDisplayUserId: stringValue(item.targetDisplayUserId ?? item.target_display_user_id),
|
|
||||||
targetUserId: stringValue(item.targetUserId ?? item.target_user_id),
|
|
||||||
usdAmount: item.usdAmount ?? item.usd_amount ?? "",
|
|
||||||
usdMinorAmount: numberOrNull(item.usdMinorAmount ?? item.usd_minor_amount),
|
|
||||||
verifiedAtMs: numberOrNull(item.verifiedAtMs ?? item.verified_at_ms),
|
|
||||||
verifiedByName: stringValue(item.verifiedByName ?? item.verified_by_name),
|
|
||||||
verifyStatus: stringValue(item.verifyStatus ?? item.verify_status) || "pending",
|
|
||||||
walletAmountDelta: numberOrNull(item.walletAmountDelta ?? item.wallet_amount_delta),
|
|
||||||
walletAssetType: stringValue(item.walletAssetType ?? item.wallet_asset_type),
|
|
||||||
walletBalanceAfter: numberOrNull(item.walletBalanceAfter ?? item.wallet_balance_after),
|
|
||||||
walletCommandId: stringValue(item.walletCommandId ?? item.wallet_command_id),
|
|
||||||
walletTransactionId: stringValue(item.walletTransactionId ?? item.wallet_transaction_id),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function normalizeCoinSellerRechargeOrderPage(data = {}) {
|
|
||||||
const items = Array.isArray(data.items) ? data.items : [];
|
|
||||||
return {
|
|
||||||
items: items.map(normalizeCoinSellerRechargeOrder),
|
|
||||||
page: Number(data.page || 1),
|
|
||||||
pageSize: Number(data.pageSize ?? data.page_size ?? items.length),
|
|
||||||
total: Number(data.total ?? items.length),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function normalizeRechargeBill(item = {}) {
|
export function normalizeRechargeBill(item = {}) {
|
||||||
return {
|
return {
|
||||||
appCode: stringValue(item.appCode ?? item.app_code),
|
appCode: stringValue(item.appCode ?? item.app_code),
|
||||||
@ -523,50 +414,6 @@ export function statusLabel(value) {
|
|||||||
return APPLICATION_STATUS.find(([status]) => status === value)?.[1] || value || "-";
|
return APPLICATION_STATUS.find(([status]) => status === value)?.[1] || value || "-";
|
||||||
}
|
}
|
||||||
|
|
||||||
export function coinSellerRechargeProviderLabel(value) {
|
|
||||||
return COIN_SELLER_RECHARGE_PROVIDERS.find(([code]) => code === value)?.[1] || value || "-";
|
|
||||||
}
|
|
||||||
|
|
||||||
export function coinSellerRechargeChainLabel(value) {
|
|
||||||
return COIN_SELLER_RECHARGE_CHAINS.find(([code]) => code === value)?.[1] || value || "-";
|
|
||||||
}
|
|
||||||
|
|
||||||
export function coinSellerRechargeStatusLabel(value) {
|
|
||||||
return COIN_SELLER_RECHARGE_STATUS.find(([status]) => status === value)?.[1] || value || "-";
|
|
||||||
}
|
|
||||||
|
|
||||||
export function coinSellerRechargeVerifyStatusLabel(value) {
|
|
||||||
return COIN_SELLER_RECHARGE_VERIFY_STATUS.find(([status]) => status === value)?.[1] || value || "-";
|
|
||||||
}
|
|
||||||
|
|
||||||
export function coinSellerRechargeGrantStatusLabel(value) {
|
|
||||||
return COIN_SELLER_RECHARGE_GRANT_STATUS.find(([status]) => status === value)?.[1] || value || "-";
|
|
||||||
}
|
|
||||||
|
|
||||||
export function coinSellerRechargeStatusTone(value) {
|
|
||||||
if (["granted", "verified", "success", "succeeded", "completed", "passed"].includes(stringValue(value))) {
|
|
||||||
return "success";
|
|
||||||
}
|
|
||||||
if (["failed", "rejected", "canceled", "cancelled"].includes(stringValue(value))) {
|
|
||||||
return "danger";
|
|
||||||
}
|
|
||||||
return "warning";
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isCoinSellerRechargeVerified(order) {
|
|
||||||
return (
|
|
||||||
["verified", "success", "succeeded", "completed", "passed"].includes(stringValue(order?.verifyStatus)) ||
|
|
||||||
stringValue(order?.status) === "verified"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isCoinSellerRechargeGranted(order) {
|
|
||||||
return (
|
|
||||||
["granted", "success", "succeeded", "completed"].includes(stringValue(order?.grantStatus)) ||
|
|
||||||
stringValue(order?.status) === "granted"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function walletIdentityLabel(value) {
|
export function walletIdentityLabel(value) {
|
||||||
return WALLET_IDENTITIES.find(([identity]) => identity === value)?.[1] || value || "-";
|
return WALLET_IDENTITIES.find(([identity]) => identity === value)?.[1] || value || "-";
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,18 +1,13 @@
|
|||||||
import { expect, test } from "vitest";
|
import { expect, test } from "vitest";
|
||||||
import {
|
import {
|
||||||
buildApplicationPayload,
|
buildApplicationPayload,
|
||||||
buildCoinSellerRechargeReceiptPayload,
|
|
||||||
buildCoinSellerRechargeOrderPayload,
|
|
||||||
coinSellerRechargeProviderLabel,
|
|
||||||
normalizeApplicationPage,
|
normalizeApplicationPage,
|
||||||
normalizeCoinSellerRechargeOrderPage,
|
|
||||||
normalizeRechargeBillPage,
|
normalizeRechargeBillPage,
|
||||||
normalizeWithdrawalApplicationPage,
|
normalizeWithdrawalApplicationPage,
|
||||||
operationLabel,
|
operationLabel,
|
||||||
rechargeTypeLabel,
|
rechargeTypeLabel,
|
||||||
statusLabel,
|
statusLabel,
|
||||||
validateApplicationPayload,
|
validateApplicationPayload,
|
||||||
validateCoinSellerRechargeOrderPayload,
|
|
||||||
walletIdentityLabel,
|
walletIdentityLabel,
|
||||||
} from "./format.js";
|
} from "./format.js";
|
||||||
|
|
||||||
@ -165,84 +160,6 @@ test("normalizes finance withdrawal application list rows", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test("builds validates and normalizes coin seller recharge orders", () => {
|
|
||||||
const payload = buildCoinSellerRechargeOrderPayload({
|
|
||||||
appCode: " lalu ",
|
|
||||||
coinAmount: "100000",
|
|
||||||
externalOrderNo: " MIFA-001 ",
|
|
||||||
providerCode: "mifapay",
|
|
||||||
remark: " 人工补单 ",
|
|
||||||
targetUserId: " 10001 ",
|
|
||||||
usdAmount: "10.5",
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(payload).toEqual({
|
|
||||||
appCode: "lalu",
|
|
||||||
coinAmount: 100000,
|
|
||||||
externalOrderNo: "MIFA-001",
|
|
||||||
providerCode: "mifapay",
|
|
||||||
remark: "人工补单",
|
|
||||||
targetUserId: "10001",
|
|
||||||
usdAmount: 10.5,
|
|
||||||
});
|
|
||||||
expect(
|
|
||||||
buildCoinSellerRechargeReceiptPayload({
|
|
||||||
...payload,
|
|
||||||
coinAmount: "",
|
|
||||||
targetUserId: "",
|
|
||||||
}),
|
|
||||||
).toEqual({
|
|
||||||
appCode: "lalu",
|
|
||||||
externalOrderNo: "MIFA-001",
|
|
||||||
providerCode: "mifapay",
|
|
||||||
usdAmount: 10.5,
|
|
||||||
});
|
|
||||||
expect(validateCoinSellerRechargeOrderPayload(payload)).toBe("");
|
|
||||||
expect(validateCoinSellerRechargeOrderPayload({ ...payload, coinAmount: 0 })).toBe("");
|
|
||||||
expect(validateCoinSellerRechargeOrderPayload({ ...payload, coinAmount: -1 })).toBe("请填写不小于 0 的整数金币");
|
|
||||||
expect(validateCoinSellerRechargeOrderPayload({ ...payload, remark: "x".repeat(513) })).toBe("备注不能超过 512 字");
|
|
||||||
expect(validateCoinSellerRechargeOrderPayload({ ...payload, providerCode: "usdt", chain: "" })).toBe(
|
|
||||||
"请选择 USDT 链",
|
|
||||||
);
|
|
||||||
|
|
||||||
const page = normalizeCoinSellerRechargeOrderPage({
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
app_code: "lalu",
|
|
||||||
coin_amount: 100000,
|
|
||||||
created_at_ms: 1760000000000,
|
|
||||||
external_order_no: "MIFA-001",
|
|
||||||
grant_status: "pending",
|
|
||||||
id: "order-1",
|
|
||||||
provider_code: "mifapay",
|
|
||||||
remark: "人工补单",
|
|
||||||
target_display_user_id: "165075",
|
|
||||||
target_user_id: "10001",
|
|
||||||
usd_amount: "10.50",
|
|
||||||
verify_status: "verified",
|
|
||||||
wallet_transaction_id: "wallet-tx",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
page: 1,
|
|
||||||
page_size: 50,
|
|
||||||
total: 1,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(page.items[0]).toMatchObject({
|
|
||||||
appCode: "lalu",
|
|
||||||
coinAmount: 100000,
|
|
||||||
externalOrderNo: "MIFA-001",
|
|
||||||
id: "order-1",
|
|
||||||
providerCode: "mifapay",
|
|
||||||
remark: "人工补单",
|
|
||||||
targetDisplayUserId: "165075",
|
|
||||||
targetUserId: "10001",
|
|
||||||
verifyStatus: "verified",
|
|
||||||
walletTransactionId: "wallet-tx",
|
|
||||||
});
|
|
||||||
expect(coinSellerRechargeProviderLabel("mifapay")).toBe("MiFaPay");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("normalizes app recharge detail list rows with optional payment fields", () => {
|
test("normalizes app recharge detail list rows with optional payment fields", () => {
|
||||||
const page = normalizeRechargeBillPage({
|
const page = normalizeRechargeBillPage({
|
||||||
items: [
|
items: [
|
||||||
|
|||||||
@ -386,13 +386,6 @@ a {
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.finance-remark-cell {
|
|
||||||
max-width: 180px;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
white-space: normal;
|
|
||||||
word-break: break-word;
|
|
||||||
}
|
|
||||||
|
|
||||||
.finance-pagination__meta {
|
.finance-pagination__meta {
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
}
|
}
|
||||||
@ -535,11 +528,6 @@ a {
|
|||||||
border-radius: var(--radius-card) !important;
|
border-radius: var(--radius-card) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.finance-create-dialog-paper--coin-seller {
|
|
||||||
width: min(760px, calc(100vw - 64px));
|
|
||||||
max-width: min(760px, calc(100vw - 64px)) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.finance-create-dialog-paper .MuiDialogTitle-root {
|
.finance-create-dialog-paper .MuiDialogTitle-root {
|
||||||
padding: var(--space-5) var(--space-5) var(--space-2);
|
padding: var(--space-5) var(--space-5) var(--space-2);
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
@ -556,25 +544,6 @@ a {
|
|||||||
gap: var(--space-3);
|
gap: var(--space-3);
|
||||||
}
|
}
|
||||||
|
|
||||||
.finance-form--dialog .finance-form-grid--coin-seller-create {
|
|
||||||
grid-template-columns: minmax(0, 1fr);
|
|
||||||
}
|
|
||||||
|
|
||||||
.finance-form--dialog .finance-form-grid--coin-seller-no-chain {
|
|
||||||
grid-template-columns: minmax(0, 1fr);
|
|
||||||
}
|
|
||||||
|
|
||||||
.finance-receipt-field {
|
|
||||||
display: grid;
|
|
||||||
min-width: 0;
|
|
||||||
grid-template-columns: minmax(0, 1fr) auto;
|
|
||||||
gap: var(--space-2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.finance-receipt-result {
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.finance-form--dialog .finance-evidence-grid {
|
.finance-form--dialog .finance-evidence-grid {
|
||||||
grid-template-columns: 320px minmax(0, 1fr);
|
grid-template-columns: 320px minmax(0, 1fr);
|
||||||
gap: var(--space-4);
|
gap: var(--space-4);
|
||||||
@ -679,8 +648,6 @@ a {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.finance-form--dialog .finance-form-grid,
|
.finance-form--dialog .finance-form-grid,
|
||||||
.finance-form--dialog .finance-form-grid--coin-seller-create,
|
|
||||||
.finance-form--dialog .finance-form-grid--coin-seller-no-chain,
|
|
||||||
.finance-form--dialog .finance-evidence-grid {
|
.finance-form--dialog .finance-evidence-grid {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
@ -1105,10 +1072,6 @@ a {
|
|||||||
min-width: 1180px;
|
min-width: 1180px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.finance-flat-table--orders {
|
|
||||||
min-width: 1740px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.finance-views {
|
.finance-views {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 2px;
|
gap: 2px;
|
||||||
|
|||||||
@ -47,10 +47,6 @@ export const PERMISSIONS = {
|
|||||||
financeApplicationAudit: "finance-application:audit",
|
financeApplicationAudit: "finance-application:audit",
|
||||||
financeWithdrawalView: "finance-withdrawal:view",
|
financeWithdrawalView: "finance-withdrawal:view",
|
||||||
financeWithdrawalAudit: "finance-withdrawal:audit",
|
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",
|
financeOperationUserCoinCredit: "finance-operation:user-coin-credit",
|
||||||
financeOperationUserCoinDebit: "finance-operation:user-coin-debit",
|
financeOperationUserCoinDebit: "finance-operation:user-coin-debit",
|
||||||
financeOperationUserWalletCredit: "finance-operation:user-wallet-credit",
|
financeOperationUserWalletCredit: "finance-operation:user-wallet-credit",
|
||||||
|
|||||||
@ -44,8 +44,6 @@ export const resourceGrantStatusLabels = {
|
|||||||
export const resourceGrantSubjectLabels = {
|
export const resourceGrantSubjectLabels = {
|
||||||
resource: "资源",
|
resource: "资源",
|
||||||
resource_group: "资源组",
|
resource_group: "资源组",
|
||||||
vip: "VIP",
|
|
||||||
vip_level: "VIP等级",
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const emojiPackPricingLabels = {
|
export const emojiPackPricingLabels = {
|
||||||
|
|||||||
@ -1,206 +0,0 @@
|
|||||||
import { act, renderHook, waitFor } from "@testing-library/react";
|
|
||||||
import { afterEach, beforeEach, expect, test, vi } from "vitest";
|
|
||||||
import { useResourceGrantListPage } from "./useResourcePages.js";
|
|
||||||
|
|
||||||
const mocks = vi.hoisted(() => ({
|
|
||||||
can: vi.fn(),
|
|
||||||
getVipConfig: vi.fn(),
|
|
||||||
grantResource: vi.fn(),
|
|
||||||
grantResourceGroup: vi.fn(),
|
|
||||||
grantVip: vi.fn(),
|
|
||||||
listGiftTypes: vi.fn(),
|
|
||||||
listGifts: vi.fn(),
|
|
||||||
listResourceGroups: vi.fn(),
|
|
||||||
listResources: vi.fn(),
|
|
||||||
lookupResourceGrantTarget: vi.fn(),
|
|
||||||
reload: vi.fn(),
|
|
||||||
showToast: vi.fn(),
|
|
||||||
usePaginatedQuery: vi.fn(),
|
|
||||||
}));
|
|
||||||
|
|
||||||
vi.mock("@/app/auth/AuthProvider.jsx", () => ({
|
|
||||||
useAuth: () => ({ can: mocks.can }),
|
|
||||||
}));
|
|
||||||
|
|
||||||
vi.mock("@/shared/hooks/usePaginatedQuery.js", () => ({
|
|
||||||
usePaginatedQuery: mocks.usePaginatedQuery,
|
|
||||||
}));
|
|
||||||
|
|
||||||
vi.mock("@/shared/ui/ConfirmProvider.jsx", () => ({
|
|
||||||
useConfirm: () => vi.fn(),
|
|
||||||
}));
|
|
||||||
|
|
||||||
vi.mock("@/shared/ui/ToastProvider.jsx", () => ({
|
|
||||||
useToast: () => ({ showToast: mocks.showToast }),
|
|
||||||
}));
|
|
||||||
|
|
||||||
vi.mock("@/features/resources/api", async (importOriginal) => ({
|
|
||||||
...(await importOriginal()),
|
|
||||||
grantResource: mocks.grantResource,
|
|
||||||
grantResourceGroup: mocks.grantResourceGroup,
|
|
||||||
listGiftTypes: mocks.listGiftTypes,
|
|
||||||
listGifts: mocks.listGifts,
|
|
||||||
listResourceGroups: mocks.listResourceGroups,
|
|
||||||
listResources: mocks.listResources,
|
|
||||||
lookupResourceGrantTarget: mocks.lookupResourceGrantTarget,
|
|
||||||
}));
|
|
||||||
|
|
||||||
vi.mock("@/features/vip-config/api", () => ({
|
|
||||||
getVipConfig: mocks.getVipConfig,
|
|
||||||
grantVip: mocks.grantVip,
|
|
||||||
}));
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
mocks.can.mockReturnValue(true);
|
|
||||||
mocks.getVipConfig.mockResolvedValue({ levels: [{ level: 5, name: "VIP5", status: "active" }] });
|
|
||||||
mocks.grantResource.mockResolvedValue({});
|
|
||||||
mocks.grantResourceGroup.mockResolvedValue({});
|
|
||||||
mocks.grantVip.mockResolvedValue({ vip: { level: 5, name: "VIP5" } });
|
|
||||||
mocks.listGiftTypes.mockResolvedValue([]);
|
|
||||||
mocks.listGifts.mockResolvedValue({ items: [], page: 1, pageSize: 100, total: 0 });
|
|
||||||
mocks.listResourceGroups.mockResolvedValue({ items: [], page: 1, pageSize: 100, total: 0 });
|
|
||||||
mocks.listResources.mockResolvedValue({ items: [], page: 1, pageSize: 100, total: 0 });
|
|
||||||
mocks.lookupResourceGrantTarget.mockResolvedValue({ userId: "318705991371722752" });
|
|
||||||
mocks.reload.mockResolvedValue(undefined);
|
|
||||||
mocks.usePaginatedQuery.mockReturnValue({
|
|
||||||
data: { items: [], page: 1, pageSize: 50, total: 0 },
|
|
||||||
error: null,
|
|
||||||
loading: false,
|
|
||||||
reload: mocks.reload,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
vi.clearAllMocks();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("vip grant resolves target user before calling vip grant api", async () => {
|
|
||||||
const { result } = renderHook(() => useResourceGrantListPage());
|
|
||||||
|
|
||||||
await act(async () => {
|
|
||||||
result.current.setForm(grantFormFixture({ subjectType: "vip", vipLevel: "5" }));
|
|
||||||
});
|
|
||||||
|
|
||||||
await act(async () => {
|
|
||||||
await result.current.submitGrant({ preventDefault: vi.fn() });
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(mocks.lookupResourceGrantTarget).toHaveBeenCalledWith("163000");
|
|
||||||
expect(mocks.grantVip).toHaveBeenCalledWith({
|
|
||||||
level: 5,
|
|
||||||
reason: "manual",
|
|
||||||
targetUserId: "318705991371722752",
|
|
||||||
});
|
|
||||||
expect(mocks.lookupResourceGrantTarget.mock.invocationCallOrder[0]).toBeLessThan(
|
|
||||||
mocks.grantVip.mock.invocationCallOrder[0],
|
|
||||||
);
|
|
||||||
expect(mocks.grantResource).not.toHaveBeenCalled();
|
|
||||||
expect(mocks.grantResourceGroup).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("resource grant branch still calls resource grant api", async () => {
|
|
||||||
const { result } = renderHook(() => useResourceGrantListPage());
|
|
||||||
|
|
||||||
await act(async () => {
|
|
||||||
result.current.setForm(
|
|
||||||
grantFormFixture({
|
|
||||||
durationDays: "7",
|
|
||||||
quantity: "2",
|
|
||||||
resourceIds: ["11"],
|
|
||||||
subjectType: "resource",
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
await act(async () => {
|
|
||||||
await result.current.submitGrant({ preventDefault: vi.fn() });
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(mocks.lookupResourceGrantTarget).toHaveBeenCalledWith("163000");
|
|
||||||
expect(mocks.grantResource).toHaveBeenCalledWith(
|
|
||||||
expect.objectContaining({
|
|
||||||
durationMs: 7 * 24 * 60 * 60 * 1000,
|
|
||||||
quantity: 2,
|
|
||||||
reason: "manual",
|
|
||||||
resourceId: 11,
|
|
||||||
targetUserId: "318705991371722752",
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
expect(mocks.grantResource.mock.calls[0][0].commandId).toMatch(/^resource-grant-11-0-/);
|
|
||||||
expect(mocks.lookupResourceGrantTarget.mock.invocationCallOrder[0]).toBeLessThan(
|
|
||||||
mocks.grantResource.mock.invocationCallOrder[0],
|
|
||||||
);
|
|
||||||
expect(mocks.grantVip).not.toHaveBeenCalled();
|
|
||||||
expect(mocks.grantResourceGroup).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("resource group grant branch still calls resource group grant api", async () => {
|
|
||||||
const { result } = renderHook(() => useResourceGrantListPage());
|
|
||||||
|
|
||||||
await act(async () => {
|
|
||||||
result.current.setForm(grantFormFixture({ groupId: "22", subjectType: "resource_group" }));
|
|
||||||
});
|
|
||||||
|
|
||||||
await act(async () => {
|
|
||||||
await result.current.submitGrant({ preventDefault: vi.fn() });
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(mocks.lookupResourceGrantTarget).toHaveBeenCalledWith("163000");
|
|
||||||
expect(mocks.grantResourceGroup).toHaveBeenCalledWith(
|
|
||||||
expect.objectContaining({
|
|
||||||
groupId: 22,
|
|
||||||
reason: "manual",
|
|
||||||
targetUserId: "318705991371722752",
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
expect(mocks.grantResourceGroup.mock.calls[0][0].commandId).toMatch(/^resource-group-grant-/);
|
|
||||||
expect(mocks.lookupResourceGrantTarget.mock.invocationCallOrder[0]).toBeLessThan(
|
|
||||||
mocks.grantResourceGroup.mock.invocationCallOrder[0],
|
|
||||||
);
|
|
||||||
expect(mocks.grantVip).not.toHaveBeenCalled();
|
|
||||||
expect(mocks.grantResource).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("create dialog loads only vip levels when user only has vip grant permission", async () => {
|
|
||||||
mocks.can.mockImplementation((permission) => permission === "vip-config:grant");
|
|
||||||
const { result } = renderHook(() => useResourceGrantListPage());
|
|
||||||
|
|
||||||
await act(async () => {
|
|
||||||
result.current.openCreateGrant();
|
|
||||||
});
|
|
||||||
|
|
||||||
await waitFor(() => expect(mocks.getVipConfig).toHaveBeenCalledTimes(1));
|
|
||||||
expect(result.current.form.subjectType).toBe("vip");
|
|
||||||
expect(mocks.listResources).not.toHaveBeenCalled();
|
|
||||||
expect(mocks.listGifts).not.toHaveBeenCalled();
|
|
||||||
expect(mocks.listGiftTypes).not.toHaveBeenCalled();
|
|
||||||
expect(mocks.listResourceGroups).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("create dialog skips vip config when user lacks vip grant permission", async () => {
|
|
||||||
mocks.can.mockImplementation((permission) => permission !== "vip-config:grant");
|
|
||||||
const { result } = renderHook(() => useResourceGrantListPage());
|
|
||||||
|
|
||||||
await act(async () => {
|
|
||||||
result.current.openCreateGrant();
|
|
||||||
});
|
|
||||||
|
|
||||||
await waitFor(() => expect(mocks.listResources).toHaveBeenCalled());
|
|
||||||
expect(result.current.form.subjectType).toBe("resource");
|
|
||||||
expect(mocks.getVipConfig).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
function grantFormFixture(patch = {}) {
|
|
||||||
return {
|
|
||||||
durationDays: "7",
|
|
||||||
groupId: "",
|
|
||||||
quantity: "1",
|
|
||||||
reason: "manual",
|
|
||||||
resourceId: "",
|
|
||||||
resourceIds: [],
|
|
||||||
subjectType: "resource",
|
|
||||||
targetUserId: "163000",
|
|
||||||
vipLevel: "",
|
|
||||||
...patch,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@ -43,7 +43,6 @@ import {
|
|||||||
updateResourceGroup,
|
updateResourceGroup,
|
||||||
upsertResourceShopItems,
|
upsertResourceShopItems,
|
||||||
} from "@/features/resources/api";
|
} from "@/features/resources/api";
|
||||||
import { getVipConfig, grantVip } from "@/features/vip-config/api";
|
|
||||||
import {
|
import {
|
||||||
cpRelationTypeLabels,
|
cpRelationTypeLabels,
|
||||||
cpRelationTypeOptions,
|
cpRelationTypeOptions,
|
||||||
@ -314,15 +313,6 @@ export async function fetchAllOptionPages(fetcher, query = {}, pageSize = option
|
|||||||
throw new Error("资源选项分页过多,请收窄筛选条件");
|
throw new Error("资源选项分页过多,请收窄筛选条件");
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadResourceGrantOptions() {
|
|
||||||
return Promise.all([
|
|
||||||
fetchAllOptionPages(listResources, { status: "active" }),
|
|
||||||
fetchAllOptionPages(listGifts, { status: "active" }),
|
|
||||||
listGiftTypes(),
|
|
||||||
fetchAllOptionPages(listResourceGroups, { status: "active" }),
|
|
||||||
]).then(([resources, gifts, giftTypes, groups]) => ({ gifts, giftTypes, groups, resources }));
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseGiftPresentationObject(presentationJson) {
|
function parseGiftPresentationObject(presentationJson) {
|
||||||
const rawValue = String(presentationJson || "").trim();
|
const rawValue = String(presentationJson || "").trim();
|
||||||
if (!rawValue) {
|
if (!rawValue) {
|
||||||
@ -376,7 +366,6 @@ const emptyGrantForm = () => ({
|
|||||||
resourceIds: [],
|
resourceIds: [],
|
||||||
subjectType: "resource",
|
subjectType: "resource",
|
||||||
targetUserId: "",
|
targetUserId: "",
|
||||||
vipLevel: "",
|
|
||||||
});
|
});
|
||||||
|
|
||||||
function groupItemToForm(item) {
|
function groupItemToForm(item) {
|
||||||
@ -1851,7 +1840,6 @@ export function useResourceGrantListPage() {
|
|||||||
const [giftTypeOptions, setGiftTypeOptions] = useState(defaultGiftTypeOptions);
|
const [giftTypeOptions, setGiftTypeOptions] = useState(defaultGiftTypeOptions);
|
||||||
const [groupOptions, setGroupOptions] = useState([]);
|
const [groupOptions, setGroupOptions] = useState([]);
|
||||||
const [resourceOptions, setResourceOptions] = useState([]);
|
const [resourceOptions, setResourceOptions] = useState([]);
|
||||||
const [vipLevelOptions, setVipLevelOptions] = useState([]);
|
|
||||||
const [optionsLoading, setOptionsLoading] = useState(false);
|
const [optionsLoading, setOptionsLoading] = useState(false);
|
||||||
const [loadingAction, setLoadingAction] = useState("");
|
const [loadingAction, setLoadingAction] = useState("");
|
||||||
const filters = useMemo(
|
const filters = useMemo(
|
||||||
@ -1880,53 +1868,28 @@ export function useResourceGrantListPage() {
|
|||||||
}
|
}
|
||||||
let ignore = false;
|
let ignore = false;
|
||||||
setOptionsLoading(true);
|
setOptionsLoading(true);
|
||||||
const optionRequests = [];
|
Promise.all([
|
||||||
if (abilities.canCreateGrant) {
|
fetchAllOptionPages(listResources, { status: "active" }),
|
||||||
optionRequests.push(loadResourceGrantOptions().then((data) => ({ data, type: "resource" })));
|
fetchAllOptionPages(listGifts, { status: "active" }),
|
||||||
} else {
|
listGiftTypes(),
|
||||||
setResourceOptions([]);
|
fetchAllOptionPages(listResourceGroups, { status: "active" }),
|
||||||
setGiftOptions([]);
|
])
|
||||||
setGiftTypeOptions(defaultGiftTypeOptions);
|
.then(([resources, gifts, giftTypes, groups]) => {
|
||||||
setGroupOptions([]);
|
if (!ignore) {
|
||||||
}
|
setResourceOptions(
|
||||||
if (abilities.canGrantVip) {
|
(resources.items || []).filter(
|
||||||
optionRequests.push(getVipConfig().then((data) => ({ data, type: "vip" })));
|
(resource) => resource.status !== "disabled" && resource.grantable !== false,
|
||||||
} else {
|
),
|
||||||
setVipLevelOptions([]);
|
);
|
||||||
}
|
setGiftOptions((gifts.items || []).filter((gift) => gift.resourceId && gift.status !== "disabled"));
|
||||||
Promise.allSettled(optionRequests)
|
setGiftTypeOptions(mergeGiftTypeOptions(giftTypes));
|
||||||
.then((results) => {
|
setGroupOptions(groups.items || []);
|
||||||
if (ignore) {
|
}
|
||||||
return;
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
if (!ignore) {
|
||||||
|
showToast(err.message || "加载资源选项失败", "error");
|
||||||
}
|
}
|
||||||
results.forEach((result) => {
|
|
||||||
if (result.status === "rejected") {
|
|
||||||
showToast(result.reason?.message || "加载资源选项失败", "error");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (result.value.type === "resource") {
|
|
||||||
const { gifts, giftTypes, groups, resources } = result.value.data;
|
|
||||||
setResourceOptions(
|
|
||||||
(resources.items || []).filter(
|
|
||||||
(resource) => resource.status !== "disabled" && resource.grantable !== false,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
setGiftOptions(
|
|
||||||
(gifts.items || []).filter((gift) => gift.resourceId && gift.status !== "disabled"),
|
|
||||||
);
|
|
||||||
setGiftTypeOptions(mergeGiftTypeOptions(giftTypes));
|
|
||||||
setGroupOptions(groups.items || []);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const activeVipLevels = (result.value.data.levels || []).filter((level) => level.status === "active");
|
|
||||||
setVipLevelOptions(activeVipLevels);
|
|
||||||
setForm((current) => {
|
|
||||||
if (current.subjectType !== "vip" || current.vipLevel || !activeVipLevels.length) {
|
|
||||||
return current;
|
|
||||||
}
|
|
||||||
return { ...current, vipLevel: String(activeVipLevels[0].level) };
|
|
||||||
});
|
|
||||||
});
|
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
if (!ignore) {
|
if (!ignore) {
|
||||||
@ -1936,10 +1899,10 @@ export function useResourceGrantListPage() {
|
|||||||
return () => {
|
return () => {
|
||||||
ignore = true;
|
ignore = true;
|
||||||
};
|
};
|
||||||
}, [abilities.canCreateGrant, abilities.canGrantVip, activeAction, showToast]);
|
}, [activeAction, showToast]);
|
||||||
|
|
||||||
const openCreateGrant = () => {
|
const openCreateGrant = () => {
|
||||||
setForm({ ...emptyGrantForm(), subjectType: defaultGrantSubjectType(abilities) });
|
setForm(emptyGrantForm());
|
||||||
setActiveAction("create");
|
setActiveAction("create");
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -1949,15 +1912,12 @@ export function useResourceGrantListPage() {
|
|||||||
|
|
||||||
const submitGrant = async (event) => {
|
const submitGrant = async (event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
if (!abilities.canCreateGrant && !abilities.canGrantVip) {
|
if (!abilities.canCreateGrant) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setLoadingAction("grant-create");
|
setLoadingAction("grant-create");
|
||||||
try {
|
try {
|
||||||
const parsedForm = parseForm(resourceGrantFormSchema, form);
|
const parsedForm = parseForm(resourceGrantFormSchema, form);
|
||||||
if (!canSubmitGrantSubject(parsedForm.subjectType, abilities)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const targetUser = await lookupResourceGrantTarget(parsedForm.targetUserId);
|
const targetUser = await lookupResourceGrantTarget(parsedForm.targetUserId);
|
||||||
const targetUserId = normalizedResolvedUserId(targetUser);
|
const targetUserId = normalizedResolvedUserId(targetUser);
|
||||||
const payload = buildGrantPayload(parsedForm, targetUserId);
|
const payload = buildGrantPayload(parsedForm, targetUserId);
|
||||||
@ -1965,12 +1925,10 @@ export function useResourceGrantListPage() {
|
|||||||
for (const resourcePayload of payload.resources) {
|
for (const resourcePayload of payload.resources) {
|
||||||
await grantResource(resourcePayload);
|
await grantResource(resourcePayload);
|
||||||
}
|
}
|
||||||
} else if (payload.subjectType === "resource_group") {
|
|
||||||
await grantResourceGroup(payload.group);
|
|
||||||
} else {
|
} else {
|
||||||
await grantVip(payload.vip);
|
await grantResourceGroup(payload.group);
|
||||||
}
|
}
|
||||||
showToast(payload.subjectType === "vip" ? "VIP已赠送" : "资源已赠送", "success");
|
showToast("资源已赠送", "success");
|
||||||
closeAction();
|
closeAction();
|
||||||
setForm(emptyGrantForm());
|
setForm(emptyGrantForm());
|
||||||
await result.reload();
|
await result.reload();
|
||||||
@ -1986,7 +1944,7 @@ export function useResourceGrantListPage() {
|
|||||||
!abilities.canRevokeGrant ||
|
!abilities.canRevokeGrant ||
|
||||||
!grant?.grantId ||
|
!grant?.grantId ||
|
||||||
grant.status !== "succeeded" ||
|
grant.status !== "succeeded" ||
|
||||||
!isRevocableGrant(grant)
|
!isRevocableGrantSubject(grant.grantSubjectType)
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -2044,7 +2002,6 @@ export function useResourceGrantListPage() {
|
|||||||
status,
|
status,
|
||||||
submitGrant,
|
submitGrant,
|
||||||
targetFilter,
|
targetFilter,
|
||||||
vipLevelOptions,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2391,16 +2348,6 @@ function buildGrantPayload(form, resolvedTargetUserId) {
|
|||||||
subjectType: "resource",
|
subjectType: "resource",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (form.subjectType === "vip") {
|
|
||||||
return {
|
|
||||||
subjectType: "vip",
|
|
||||||
vip: {
|
|
||||||
level: Number(form.vipLevel),
|
|
||||||
reason,
|
|
||||||
targetUserId,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return {
|
return {
|
||||||
group: {
|
group: {
|
||||||
commandId: makeCommandId("resource-group-grant"),
|
commandId: makeCommandId("resource-group-grant"),
|
||||||
@ -2419,31 +2366,8 @@ function grantResourceIds(form) {
|
|||||||
return form.resourceId ? [form.resourceId] : [];
|
return form.resourceId ? [form.resourceId] : [];
|
||||||
}
|
}
|
||||||
|
|
||||||
function isRevocableGrant(grant) {
|
function isRevocableGrantSubject(subjectType) {
|
||||||
if (isVipGrantRecord(grant)) {
|
return subjectType === "resource" || subjectType === "resource_group";
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return grant?.grantSubjectType === "resource" || grant?.grantSubjectType === "resource_group";
|
|
||||||
}
|
|
||||||
|
|
||||||
function isVipGrantRecord(grant) {
|
|
||||||
const source = String(grant?.grantSource || "").trim();
|
|
||||||
const commandId = String(grant?.commandId || "").trim();
|
|
||||||
return source === "admin_grant" || source === "activity_grant" || source === "vip_purchase" || commandId.startsWith("vip_reward:");
|
|
||||||
}
|
|
||||||
|
|
||||||
function canSubmitGrantSubject(subjectType, abilities) {
|
|
||||||
if (subjectType === "vip") {
|
|
||||||
return Boolean(abilities.canGrantVip);
|
|
||||||
}
|
|
||||||
return Boolean(abilities.canCreateGrant);
|
|
||||||
}
|
|
||||||
|
|
||||||
function defaultGrantSubjectType(abilities) {
|
|
||||||
if (abilities.canCreateGrant) {
|
|
||||||
return "resource";
|
|
||||||
}
|
|
||||||
return abilities.canGrantVip ? "vip" : "resource";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function makeCommandId(prefix) {
|
function makeCommandId(prefix) {
|
||||||
|
|||||||
@ -34,7 +34,7 @@ const grantColumns = [
|
|||||||
width: "minmax(220px, 1fr)",
|
width: "minmax(220px, 1fr)",
|
||||||
render: (grant) => (
|
render: (grant) => (
|
||||||
<div className={styles.stack}>
|
<div className={styles.stack}>
|
||||||
<span className={styles.name}>{grantSourceLabel(grant.grantSource, grant)}</span>
|
<span className={styles.name}>{grantSourceLabel(grant.grantSource)}</span>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
@ -127,7 +127,7 @@ export function ResourceGrantListPage() {
|
|||||||
<AdminListPage>
|
<AdminListPage>
|
||||||
<AdminListToolbar
|
<AdminListToolbar
|
||||||
actions={
|
actions={
|
||||||
page.abilities.canCreateGrant || page.abilities.canGrantVip ? (
|
page.abilities.canCreateGrant ? (
|
||||||
<AdminActionIconButton label="资源赠送" primary onClick={page.openCreateGrant}>
|
<AdminActionIconButton label="资源赠送" primary onClick={page.openCreateGrant}>
|
||||||
<Add fontSize="small" />
|
<Add fontSize="small" />
|
||||||
</AdminActionIconButton>
|
</AdminActionIconButton>
|
||||||
@ -155,8 +155,7 @@ export function ResourceGrantListPage() {
|
|||||||
</AdminListBody>
|
</AdminListBody>
|
||||||
</DataState>
|
</DataState>
|
||||||
<ResourceGrantDialog
|
<ResourceGrantDialog
|
||||||
abilities={page.abilities}
|
disabled={!page.abilities.canCreateGrant}
|
||||||
disabled={!page.abilities.canCreateGrant && !page.abilities.canGrantVip}
|
|
||||||
form={page.form}
|
form={page.form}
|
||||||
giftOptions={page.giftOptions}
|
giftOptions={page.giftOptions}
|
||||||
giftTypeOptions={page.giftTypeOptions}
|
giftTypeOptions={page.giftTypeOptions}
|
||||||
@ -166,7 +165,6 @@ export function ResourceGrantListPage() {
|
|||||||
optionsLoading={page.optionsLoading}
|
optionsLoading={page.optionsLoading}
|
||||||
resourceOptions={page.resourceOptions}
|
resourceOptions={page.resourceOptions}
|
||||||
setForm={page.setForm}
|
setForm={page.setForm}
|
||||||
vipLevelOptions={page.vipLevelOptions}
|
|
||||||
onClose={page.closeAction}
|
onClose={page.closeAction}
|
||||||
onSubmit={page.submitGrant}
|
onSubmit={page.submitGrant}
|
||||||
/>
|
/>
|
||||||
@ -178,7 +176,7 @@ function ResourceGrantActions({ grant, page }) {
|
|||||||
const canRevoke =
|
const canRevoke =
|
||||||
page.abilities.canRevokeGrant &&
|
page.abilities.canRevokeGrant &&
|
||||||
grant?.status === "succeeded" &&
|
grant?.status === "succeeded" &&
|
||||||
isRevocableGrant(grant);
|
isRevocableGrantSubject(grant?.grantSubjectType);
|
||||||
if (!canRevoke) {
|
if (!canRevoke) {
|
||||||
return <span className={styles.meta}>-</span>;
|
return <span className={styles.meta}>-</span>;
|
||||||
}
|
}
|
||||||
@ -199,7 +197,6 @@ function ResourceGrantActions({ grant, page }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function ResourceGrantDialog({
|
function ResourceGrantDialog({
|
||||||
abilities,
|
|
||||||
disabled,
|
disabled,
|
||||||
form,
|
form,
|
||||||
giftOptions,
|
giftOptions,
|
||||||
@ -212,30 +209,8 @@ function ResourceGrantDialog({
|
|||||||
optionsLoading,
|
optionsLoading,
|
||||||
resourceOptions,
|
resourceOptions,
|
||||||
setForm,
|
setForm,
|
||||||
vipLevelOptions,
|
|
||||||
}) {
|
}) {
|
||||||
const resourceGrantAllowed = Boolean(abilities?.canCreateGrant);
|
const submitDisabled = disabled || loading || optionsLoading;
|
||||||
const vipGrantAllowed = Boolean(abilities?.canGrantVip);
|
|
||||||
const subjectAllowed = canEditGrantSubject(form.subjectType, abilities);
|
|
||||||
const hasVipLevels = (vipLevelOptions || []).length > 0;
|
|
||||||
const currentVipLevel = String(form.vipLevel || "");
|
|
||||||
const vipLevelValue = (vipLevelOptions || []).some((level) => String(level.level) === currentVipLevel)
|
|
||||||
? currentVipLevel
|
|
||||||
: "";
|
|
||||||
const vipUnavailable = form.subjectType === "vip" && (!vipGrantAllowed || !hasVipLevels);
|
|
||||||
const submitDisabled = disabled || loading || optionsLoading || !subjectAllowed || vipUnavailable;
|
|
||||||
const handleSubjectChange = (event) => {
|
|
||||||
const subjectType = event.target.value;
|
|
||||||
setForm({
|
|
||||||
...form,
|
|
||||||
groupId: "",
|
|
||||||
resourceId: "",
|
|
||||||
resourceIds: [],
|
|
||||||
subjectType,
|
|
||||||
vipLevel: subjectType === "vip" ? String((vipLevelOptions || [])[0]?.level || "") : "",
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AdminFormDialog
|
<AdminFormDialog
|
||||||
loading={loading}
|
loading={loading}
|
||||||
@ -247,7 +222,7 @@ function ResourceGrantDialog({
|
|||||||
>
|
>
|
||||||
<AdminFormSection title="赠送对象">
|
<AdminFormSection title="赠送对象">
|
||||||
<TextField
|
<TextField
|
||||||
disabled={disabled || loading || !subjectAllowed}
|
disabled={disabled}
|
||||||
inputProps={{ inputMode: "numeric", pattern: "[0-9]*" }}
|
inputProps={{ inputMode: "numeric", pattern: "[0-9]*" }}
|
||||||
label="用户 ID / 短号"
|
label="用户 ID / 短号"
|
||||||
required
|
required
|
||||||
@ -263,17 +238,24 @@ function ResourceGrantDialog({
|
|||||||
required
|
required
|
||||||
select
|
select
|
||||||
value={form.subjectType}
|
value={form.subjectType}
|
||||||
onChange={handleSubjectChange}
|
onChange={(event) =>
|
||||||
|
setForm({
|
||||||
|
...form,
|
||||||
|
groupId: "",
|
||||||
|
resourceId: "",
|
||||||
|
resourceIds: [],
|
||||||
|
subjectType: event.target.value,
|
||||||
|
})
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{resourceGrantAllowed ? <MenuItem value="resource">资源</MenuItem> : null}
|
<MenuItem value="resource">资源</MenuItem>
|
||||||
{resourceGrantAllowed ? <MenuItem value="resource_group">资源组</MenuItem> : null}
|
<MenuItem value="resource_group">资源组</MenuItem>
|
||||||
{vipGrantAllowed ? <MenuItem value="vip">VIP</MenuItem> : null}
|
|
||||||
</TextField>
|
</TextField>
|
||||||
{form.subjectType === "resource" ? (
|
{form.subjectType === "resource" ? (
|
||||||
<>
|
<>
|
||||||
<GiftResourceSelectField
|
<GiftResourceSelectField
|
||||||
drawerTitle="选择资源"
|
drawerTitle="选择资源"
|
||||||
disabled={disabled || optionsLoading || !resourceGrantAllowed}
|
disabled={disabled || optionsLoading}
|
||||||
giftTypes={giftTypeOptions}
|
giftTypes={giftTypeOptions}
|
||||||
gifts={giftOptions}
|
gifts={giftOptions}
|
||||||
label="资源"
|
label="资源"
|
||||||
@ -286,7 +268,7 @@ function ResourceGrantDialog({
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<TextField
|
<TextField
|
||||||
disabled={disabled || !resourceGrantAllowed}
|
disabled={disabled}
|
||||||
label="数量"
|
label="数量"
|
||||||
required
|
required
|
||||||
type="number"
|
type="number"
|
||||||
@ -294,17 +276,16 @@ function ResourceGrantDialog({
|
|||||||
onChange={(event) => setForm({ ...form, quantity: event.target.value })}
|
onChange={(event) => setForm({ ...form, quantity: event.target.value })}
|
||||||
/>
|
/>
|
||||||
<TextField
|
<TextField
|
||||||
disabled={disabled || !resourceGrantAllowed}
|
disabled={disabled}
|
||||||
label="有效天数"
|
label="有效天数"
|
||||||
type="number"
|
type="number"
|
||||||
value={form.durationDays}
|
value={form.durationDays}
|
||||||
onChange={(event) => setForm({ ...form, durationDays: event.target.value })}
|
onChange={(event) => setForm({ ...form, durationDays: event.target.value })}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
) : null}
|
) : (
|
||||||
{form.subjectType === "resource_group" ? (
|
|
||||||
<TextField
|
<TextField
|
||||||
disabled={disabled || optionsLoading || !resourceGrantAllowed}
|
disabled={disabled || optionsLoading}
|
||||||
label="资源组"
|
label="资源组"
|
||||||
required
|
required
|
||||||
select
|
select
|
||||||
@ -317,35 +298,12 @@ function ResourceGrantDialog({
|
|||||||
</MenuItem>
|
</MenuItem>
|
||||||
))}
|
))}
|
||||||
</TextField>
|
</TextField>
|
||||||
) : null}
|
)}
|
||||||
{form.subjectType === "vip" ? (
|
|
||||||
<TextField
|
|
||||||
disabled={disabled || optionsLoading || !vipGrantAllowed || !hasVipLevels}
|
|
||||||
helperText={!hasVipLevels && !optionsLoading ? "暂无启用VIP等级" : undefined}
|
|
||||||
label="VIP等级"
|
|
||||||
required
|
|
||||||
select
|
|
||||||
value={vipLevelValue}
|
|
||||||
onChange={(event) => setForm({ ...form, vipLevel: event.target.value })}
|
|
||||||
>
|
|
||||||
{hasVipLevels ? (
|
|
||||||
(vipLevelOptions || []).map((level) => (
|
|
||||||
<MenuItem key={level.level} value={String(level.level)}>
|
|
||||||
VIP{level.level} · {level.name || `VIP${level.level}`}
|
|
||||||
</MenuItem>
|
|
||||||
))
|
|
||||||
) : (
|
|
||||||
<MenuItem disabled value="">
|
|
||||||
暂无启用VIP等级
|
|
||||||
</MenuItem>
|
|
||||||
)}
|
|
||||||
</TextField>
|
|
||||||
) : null}
|
|
||||||
</AdminFormFieldGrid>
|
</AdminFormFieldGrid>
|
||||||
</AdminFormSection>
|
</AdminFormSection>
|
||||||
<AdminFormSection title="备注">
|
<AdminFormSection title="备注">
|
||||||
<TextField
|
<TextField
|
||||||
disabled={disabled || loading || !subjectAllowed}
|
disabled={disabled}
|
||||||
label="原因"
|
label="原因"
|
||||||
multiline
|
multiline
|
||||||
minRows={2}
|
minRows={2}
|
||||||
@ -417,10 +375,8 @@ function GrantItemMedia({ item }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function GrantSubject({ grant }) {
|
function GrantSubject({ grant }) {
|
||||||
const subjectLabel = isVipGrantRecord(grant)
|
const subjectLabel = resourceGrantSubjectLabels[grant.grantSubjectType] || grant.grantSubjectType || "发放";
|
||||||
? "VIP奖励资源组"
|
const sourceLabel = grantSourceLabel(grant.grantSource);
|
||||||
: resourceGrantSubjectLabels[grant.grantSubjectType] || grant.grantSubjectType || "发放";
|
|
||||||
const sourceLabel = grantSourceLabel(grant.grantSource, grant);
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.stack}>
|
<div className={styles.stack}>
|
||||||
<span className={styles.name}>{sourceLabel === "-" ? subjectLabel : `${subjectLabel} · ${sourceLabel}`}</span>
|
<span className={styles.name}>{sourceLabel === "-" ? subjectLabel : `${subjectLabel} · ${sourceLabel}`}</span>
|
||||||
@ -432,11 +388,10 @@ function GrantSubject({ grant }) {
|
|||||||
function GrantReason({ grant }) {
|
function GrantReason({ grant }) {
|
||||||
const label = reasonLabel(grant.reason);
|
const label = reasonLabel(grant.reason);
|
||||||
const raw = String(grant.reason || "").trim();
|
const raw = String(grant.reason || "").trim();
|
||||||
const showRawReason = raw && raw !== label && !grantReasonLabels[raw];
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.stack}>
|
<div className={styles.stack}>
|
||||||
<span className={styles.name}>{label}</span>
|
<span className={styles.name}>{label}</span>
|
||||||
{showRawReason ? <span className={styles.meta}>{raw}</span> : null}
|
{raw && raw !== label ? <span className={styles.meta}>{raw}</span> : null}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -472,9 +427,7 @@ function GrantOperator({ grant }) {
|
|||||||
const grantSourceLabels = {
|
const grantSourceLabels = {
|
||||||
achievement: "成就奖励",
|
achievement: "成就奖励",
|
||||||
admin: "后台发放",
|
admin: "后台发放",
|
||||||
admin_grant: "后台VIP赠送",
|
|
||||||
agency_opening: "Agency 开通",
|
agency_opening: "Agency 开通",
|
||||||
activity_grant: "活动VIP赠送",
|
|
||||||
cp_weekly_rank: "CP 周榜奖励",
|
cp_weekly_rank: "CP 周榜奖励",
|
||||||
cumulative_recharge_reward: "累充奖励",
|
cumulative_recharge_reward: "累充奖励",
|
||||||
first_recharge_reward: "首充奖励",
|
first_recharge_reward: "首充奖励",
|
||||||
@ -485,7 +438,6 @@ const grantSourceLabels = {
|
|||||||
resource_shop: "道具商店",
|
resource_shop: "道具商店",
|
||||||
room_rocket: "房间火箭",
|
room_rocket: "房间火箭",
|
||||||
seven_day_checkin: "七日签到",
|
seven_day_checkin: "七日签到",
|
||||||
vip_purchase: "VIP购买",
|
|
||||||
weekly_star: "周星奖励",
|
weekly_star: "周星奖励",
|
||||||
wheel_reward: "转盘奖励",
|
wheel_reward: "转盘奖励",
|
||||||
};
|
};
|
||||||
@ -496,12 +448,9 @@ const grantReasonLabels = {
|
|||||||
"resource shop purchase": "道具商店购买",
|
"resource shop purchase": "道具商店购买",
|
||||||
};
|
};
|
||||||
|
|
||||||
function grantSourceLabel(source, grant) {
|
function grantSourceLabel(source) {
|
||||||
const key = String(source || "").trim();
|
const key = String(source || "").trim();
|
||||||
if (grantSourceLabels[key]) {
|
return grantSourceLabels[key] || key || "-";
|
||||||
return grantSourceLabels[key];
|
|
||||||
}
|
|
||||||
return isVipRewardCommand(grant) ? "VIP奖励" : key || "-";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function reasonLabel(reason) {
|
function reasonLabel(reason) {
|
||||||
@ -510,44 +459,12 @@ function reasonLabel(reason) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function subjectMeta(grant) {
|
function subjectMeta(grant) {
|
||||||
if (isVipGrantRecord(grant)) {
|
|
||||||
return grant.grantSubjectId ? `奖励资源组 ID ${grant.grantSubjectId}` : "-";
|
|
||||||
}
|
|
||||||
if (grant.grantSubjectType === "vip_level") {
|
|
||||||
return grant.grantSubjectId ? `VIP等级 ${grant.grantSubjectId}` : "-";
|
|
||||||
}
|
|
||||||
const subjectLabel = resourceGrantSubjectLabels[grant.grantSubjectType] || grant.grantSubjectType || "对象";
|
const subjectLabel = resourceGrantSubjectLabels[grant.grantSubjectType] || grant.grantSubjectType || "对象";
|
||||||
return grant.grantSubjectId ? `${subjectLabel} ID ${grant.grantSubjectId}` : "-";
|
return grant.grantSubjectId ? `${subjectLabel} ID ${grant.grantSubjectId}` : "-";
|
||||||
}
|
}
|
||||||
|
|
||||||
function isRevocableGrant(grant) {
|
function isRevocableGrantSubject(subjectType) {
|
||||||
if (isVipGrantRecord(grant)) {
|
return subjectType === "resource" || subjectType === "resource_group";
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return grant?.grantSubjectType === "resource" || grant?.grantSubjectType === "resource_group";
|
|
||||||
}
|
|
||||||
|
|
||||||
function isVipGrantRecord(grant) {
|
|
||||||
const source = String(grant?.grantSource || "").trim();
|
|
||||||
return (
|
|
||||||
source === "admin_grant" ||
|
|
||||||
source === "activity_grant" ||
|
|
||||||
source === "vip_purchase" ||
|
|
||||||
isVipRewardCommand(grant)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function isVipRewardCommand(grant) {
|
|
||||||
return String(grant?.commandId || "")
|
|
||||||
.trim()
|
|
||||||
.startsWith("vip_reward:");
|
|
||||||
}
|
|
||||||
|
|
||||||
function canEditGrantSubject(subjectType, abilities) {
|
|
||||||
if (subjectType === "vip") {
|
|
||||||
return Boolean(abilities?.canGrantVip);
|
|
||||||
}
|
|
||||||
return Boolean(abilities?.canCreateGrant);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function grantItemLabel(item, snapshot = parseGrantItemSnapshot(item.resourceSnapshotJson)) {
|
function grantItemLabel(item, snapshot = parseGrantItemSnapshot(item.resourceSnapshotJson)) {
|
||||||
|
|||||||
@ -161,78 +161,11 @@ test("resource grant list keeps operator only for user initiated manager grants"
|
|||||||
expect(screen.getByText("Manager A")).toBeInTheDocument();
|
expect(screen.getByText("Manager A")).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("resource grant dialog shows vip subject and level selector", () => {
|
|
||||||
vi.mocked(useResourceGrantListPage).mockReturnValue(
|
|
||||||
pageFixture({
|
|
||||||
activeAction: "create",
|
|
||||||
form: grantFormFixture({ subjectType: "vip", vipLevel: "5" }),
|
|
||||||
vipLevelOptions: [{ level: 5, name: "黄金VIP", status: "active" }],
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
render(<ResourceGrantListPage />);
|
|
||||||
|
|
||||||
expect(screen.getByText("VIP等级")).toBeInTheDocument();
|
|
||||||
expect(screen.getByText("VIP5 · 黄金VIP")).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("resource grant dialog hides vip subject without vip grant permission", () => {
|
|
||||||
vi.mocked(useResourceGrantListPage).mockReturnValue(
|
|
||||||
pageFixture({
|
|
||||||
abilities: { canCreateGrant: true, canGrantVip: false },
|
|
||||||
activeAction: "create",
|
|
||||||
form: grantFormFixture({ subjectType: "resource" }),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
render(<ResourceGrantListPage />);
|
|
||||||
|
|
||||||
fireEvent.mouseDown(screen.getByRole("combobox", { name: "对象类型" }));
|
|
||||||
|
|
||||||
expect(screen.queryByRole("option", { name: "VIP" })).not.toBeInTheDocument();
|
|
||||||
expect(screen.queryByText("VIP等级")).not.toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("resource grant list renders vip source records and hides revoke", () => {
|
|
||||||
vi.mocked(useResourceGrantListPage).mockReturnValue(
|
|
||||||
pageFixture({
|
|
||||||
data: {
|
|
||||||
items: [
|
|
||||||
grantFixture({
|
|
||||||
commandId: "vip_reward:admin_vip_grant:rgr_vip",
|
|
||||||
grantId: "rgr_vip",
|
|
||||||
grantSource: "admin_grant",
|
|
||||||
grantSubjectId: "22",
|
|
||||||
grantSubjectType: "resource_group",
|
|
||||||
reason: "admin_grant",
|
|
||||||
status: "succeeded",
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
page: 1,
|
|
||||||
pageSize: 50,
|
|
||||||
total: 1,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
render(<ResourceGrantListPage />);
|
|
||||||
|
|
||||||
expect(screen.getAllByText("后台VIP赠送").length).toBeGreaterThanOrEqual(1);
|
|
||||||
expect(screen.getByText("VIP奖励资源组 · 后台VIP赠送")).toBeInTheDocument();
|
|
||||||
expect(screen.getByText("奖励资源组 ID 22")).toBeInTheDocument();
|
|
||||||
expect(screen.queryByLabelText("撤销")).not.toBeInTheDocument();
|
|
||||||
expect(screen.queryByText("admin_grant")).not.toBeInTheDocument();
|
|
||||||
expect(screen.queryByText("vip_reward:admin_vip_grant:rgr_vip")).not.toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
function pageFixture(patch = {}) {
|
function pageFixture(patch = {}) {
|
||||||
const { abilities: abilityPatch, form: formPatch, ...restPatch } = patch;
|
|
||||||
return {
|
return {
|
||||||
abilities: {
|
abilities: {
|
||||||
canCreateGrant: true,
|
canCreateGrant: true,
|
||||||
canGrantVip: true,
|
|
||||||
canRevokeGrant: true,
|
canRevokeGrant: true,
|
||||||
...abilityPatch,
|
|
||||||
},
|
},
|
||||||
activeAction: "",
|
activeAction: "",
|
||||||
changeQuery: vi.fn(),
|
changeQuery: vi.fn(),
|
||||||
@ -240,7 +173,15 @@ function pageFixture(patch = {}) {
|
|||||||
closeAction: vi.fn(),
|
closeAction: vi.fn(),
|
||||||
data: { items: [], page: 1, pageSize: 50, total: 0 },
|
data: { items: [], page: 1, pageSize: 50, total: 0 },
|
||||||
error: null,
|
error: null,
|
||||||
form: grantFormFixture(formPatch),
|
form: {
|
||||||
|
durationDays: "0",
|
||||||
|
giftIds: [],
|
||||||
|
groupId: "",
|
||||||
|
reason: "",
|
||||||
|
resourceIds: [],
|
||||||
|
subjectType: "resource",
|
||||||
|
targetUserId: "",
|
||||||
|
},
|
||||||
giftOptions: [],
|
giftOptions: [],
|
||||||
giftTypeOptions: [],
|
giftTypeOptions: [],
|
||||||
groupOptions: [],
|
groupOptions: [],
|
||||||
@ -258,23 +199,6 @@ function pageFixture(patch = {}) {
|
|||||||
setPage: vi.fn(),
|
setPage: vi.fn(),
|
||||||
status: "",
|
status: "",
|
||||||
submitGrant: vi.fn(),
|
submitGrant: vi.fn(),
|
||||||
vipLevelOptions: [],
|
|
||||||
...restPatch,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function grantFormFixture(patch = {}) {
|
|
||||||
return {
|
|
||||||
durationDays: "7",
|
|
||||||
giftIds: [],
|
|
||||||
groupId: "",
|
|
||||||
quantity: "1",
|
|
||||||
reason: "",
|
|
||||||
resourceId: "",
|
|
||||||
resourceIds: [],
|
|
||||||
subjectType: "resource",
|
|
||||||
targetUserId: "",
|
|
||||||
vipLevel: "",
|
|
||||||
...patch,
|
...patch,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,7 +9,6 @@ export function useResourceAbilities() {
|
|||||||
canCreateGift: can(PERMISSIONS.giftCreate),
|
canCreateGift: can(PERMISSIONS.giftCreate),
|
||||||
canCreateGrant: can(PERMISSIONS.resourceGrantCreate),
|
canCreateGrant: can(PERMISSIONS.resourceGrantCreate),
|
||||||
canCreateGroup: can(PERMISSIONS.resourceGroupCreate),
|
canCreateGroup: can(PERMISSIONS.resourceGroupCreate),
|
||||||
canGrantVip: can(PERMISSIONS.vipConfigGrant),
|
|
||||||
canRevokeGrant: can(PERMISSIONS.resourceGrantRevoke),
|
canRevokeGrant: can(PERMISSIONS.resourceGrantRevoke),
|
||||||
canCreateEmojiPack: can(PERMISSIONS.emojiPackCreate),
|
canCreateEmojiPack: can(PERMISSIONS.emojiPackCreate),
|
||||||
canDelete: can(PERMISSIONS.resourceDelete),
|
canDelete: can(PERMISSIONS.resourceDelete),
|
||||||
|
|||||||
@ -446,9 +446,8 @@ export const resourceGrantFormSchema = z
|
|||||||
reason: z.string().trim().min(1, "请输入原因").max(240, "原因不能超过 240 个字符"),
|
reason: z.string().trim().min(1, "请输入原因").max(240, "原因不能超过 240 个字符"),
|
||||||
resourceId: z.union([z.string(), z.number()]).optional(),
|
resourceId: z.union([z.string(), z.number()]).optional(),
|
||||||
resourceIds: z.array(z.union([z.string(), z.number()])).optional(),
|
resourceIds: z.array(z.union([z.string(), z.number()])).optional(),
|
||||||
subjectType: z.enum(["resource", "resource_group", "vip"]),
|
subjectType: z.enum(["resource", "resource_group"]),
|
||||||
targetUserId: z.union([z.string(), z.number()]),
|
targetUserId: z.union([z.string(), z.number()]),
|
||||||
vipLevel: z.union([z.string(), z.number()]).optional(),
|
|
||||||
})
|
})
|
||||||
.superRefine((value, context) => {
|
.superRefine((value, context) => {
|
||||||
const targetUserId = String(value.targetUserId || "").trim();
|
const targetUserId = String(value.targetUserId || "").trim();
|
||||||
@ -456,7 +455,6 @@ export const resourceGrantFormSchema = z
|
|||||||
const groupId = Number(value.groupId || 0);
|
const groupId = Number(value.groupId || 0);
|
||||||
const quantity = Number(value.quantity || 1);
|
const quantity = Number(value.quantity || 1);
|
||||||
const durationDays = Number(value.durationDays || 0);
|
const durationDays = Number(value.durationDays || 0);
|
||||||
const vipLevel = Number(value.vipLevel || 0);
|
|
||||||
|
|
||||||
if (!/^[1-9]\d*$/.test(targetUserId)) {
|
if (!/^[1-9]\d*$/.test(targetUserId)) {
|
||||||
context.addIssue({
|
context.addIssue({
|
||||||
@ -483,20 +481,6 @@ export const resourceGrantFormSchema = z
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
if (!Number.isInteger(quantity) || quantity <= 0) {
|
|
||||||
context.addIssue({
|
|
||||||
code: "custom",
|
|
||||||
message: "请输入数量",
|
|
||||||
path: ["quantity"],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (!Number.isFinite(durationDays) || durationDays < 0) {
|
|
||||||
context.addIssue({
|
|
||||||
code: "custom",
|
|
||||||
message: "请输入有效天数",
|
|
||||||
path: ["durationDays"],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (value.subjectType === "resource_group" && (!Number.isInteger(groupId) || groupId <= 0)) {
|
if (value.subjectType === "resource_group" && (!Number.isInteger(groupId) || groupId <= 0)) {
|
||||||
context.addIssue({
|
context.addIssue({
|
||||||
@ -505,11 +489,18 @@ export const resourceGrantFormSchema = z
|
|||||||
path: ["groupId"],
|
path: ["groupId"],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (value.subjectType === "vip" && (!Number.isInteger(vipLevel) || vipLevel <= 0)) {
|
if (!Number.isInteger(quantity) || quantity <= 0) {
|
||||||
context.addIssue({
|
context.addIssue({
|
||||||
code: "custom",
|
code: "custom",
|
||||||
message: "请选择VIP等级",
|
message: "请输入数量",
|
||||||
path: ["vipLevel"],
|
path: ["quantity"],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (!Number.isFinite(durationDays) || durationDays < 0) {
|
||||||
|
context.addIssue({
|
||||||
|
code: "custom",
|
||||||
|
message: "请输入有效天数",
|
||||||
|
path: ["durationDays"],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@ -302,31 +302,6 @@ describe("resource form schema", () => {
|
|||||||
expect(payload.resourceIds).toEqual(["11", "12"]);
|
expect(payload.resourceIds).toEqual(["11", "12"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("validates vip grant form fields", () => {
|
|
||||||
const payload = parseForm(resourceGrantFormSchema, {
|
|
||||||
reason: "manual",
|
|
||||||
subjectType: "vip",
|
|
||||||
targetUserId: "1001",
|
|
||||||
vipLevel: "5",
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(payload.subjectType).toBe("vip");
|
|
||||||
expect(payload.vipLevel).toBe("5");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("rejects invalid vip grant form fields", () => {
|
|
||||||
const basePayload = {
|
|
||||||
reason: "manual",
|
|
||||||
subjectType: "vip",
|
|
||||||
targetUserId: "1001",
|
|
||||||
vipLevel: "5",
|
|
||||||
};
|
|
||||||
|
|
||||||
expect(() => parseForm(resourceGrantFormSchema, { ...basePayload, vipLevel: "" })).toThrow(FormValidationError);
|
|
||||||
expect(() => parseForm(resourceGrantFormSchema, { ...basePayload, vipLevel: "bad" })).toThrow(FormValidationError);
|
|
||||||
expect(() => parseForm(resourceGrantFormSchema, { ...basePayload, reason: "" })).toThrow(FormValidationError);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("validates resource shop durations", () => {
|
test("validates resource shop durations", () => {
|
||||||
const payload = parseForm(resourceShopItemsFormSchema, {
|
const payload = parseForm(resourceShopItemsFormSchema, {
|
||||||
items: [
|
items: [
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
import { getVipConfig, updateVipConfig } from "@/features/vip-config/api";
|
import { getVipConfig, grantVip, updateVipConfig } from "@/features/vip-config/api";
|
||||||
import { useVipConfigAbilities } from "@/features/vip-config/permissions.js";
|
import { useVipConfigAbilities } from "@/features/vip-config/permissions.js";
|
||||||
import { listResourceGroups } from "@/features/resources/api";
|
import { listResourceGroups } from "@/features/resources/api";
|
||||||
import { useToast } from "@/shared/ui/ToastProvider.jsx";
|
import { useToast } from "@/shared/ui/ToastProvider.jsx";
|
||||||
@ -13,8 +13,11 @@ export function useVipConfigPage() {
|
|||||||
const [form, setForm] = useState({ levels: defaultLevels().map(levelToForm) });
|
const [form, setForm] = useState({ levels: defaultLevels().map(levelToForm) });
|
||||||
const [resourceGroups, setResourceGroups] = useState([]);
|
const [resourceGroups, setResourceGroups] = useState([]);
|
||||||
const [drawerOpen, setDrawerOpen] = useState(false);
|
const [drawerOpen, setDrawerOpen] = useState(false);
|
||||||
|
const [grantDrawerOpen, setGrantDrawerOpen] = useState(false);
|
||||||
|
const [grantForm, setGrantForm] = useState(defaultGrantForm());
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
|
const [grantSaving, setGrantSaving] = useState(false);
|
||||||
|
|
||||||
const activeCount = useMemo(
|
const activeCount = useMemo(
|
||||||
() => (config.levels || []).filter((level) => level.status === "active").length,
|
() => (config.levels || []).filter((level) => level.status === "active").length,
|
||||||
@ -56,6 +59,22 @@ export function useVipConfigPage() {
|
|||||||
setDrawerOpen(false);
|
setDrawerOpen(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const openGrantDrawer = () => {
|
||||||
|
setGrantForm((current) => ({
|
||||||
|
...defaultGrantForm(),
|
||||||
|
level: firstActiveLevel(config.levels || []) || current.level || "1",
|
||||||
|
}));
|
||||||
|
setGrantDrawerOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeGrantDrawer = () => {
|
||||||
|
if (grantSaving) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setGrantDrawerOpen(false);
|
||||||
|
setGrantForm(defaultGrantForm());
|
||||||
|
};
|
||||||
|
|
||||||
const updateLevel = (levelNumber, patch) => {
|
const updateLevel = (levelNumber, patch) => {
|
||||||
setForm((current) => ({
|
setForm((current) => ({
|
||||||
...current,
|
...current,
|
||||||
@ -63,6 +82,10 @@ export function useVipConfigPage() {
|
|||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const updateGrantForm = (patch) => {
|
||||||
|
setGrantForm((current) => ({ ...current, ...patch }));
|
||||||
|
};
|
||||||
|
|
||||||
const submit = async (event) => {
|
const submit = async (event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
if (!abilities.canUpdate) {
|
if (!abilities.canUpdate) {
|
||||||
@ -90,20 +113,53 @@ export function useVipConfigPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const submitGrant = async (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
if (!abilities.canGrant) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const targetUserId = String(grantForm.targetUserId || "").trim();
|
||||||
|
const reason = String(grantForm.reason || "").trim();
|
||||||
|
const level = Number(grantForm.level || 0);
|
||||||
|
if (!targetUserId || !Number.isInteger(level) || level <= 0 || !reason) {
|
||||||
|
showToast("VIP 赠送参数不正确", "error");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setGrantSaving(true);
|
||||||
|
try {
|
||||||
|
const result = await grantVip({ targetUserId, level, reason });
|
||||||
|
setGrantDrawerOpen(false);
|
||||||
|
setGrantForm(defaultGrantForm());
|
||||||
|
showToast(`已赠送 ${result.vip?.name || `VIP${level}`}`, "success");
|
||||||
|
await reload();
|
||||||
|
} catch (err) {
|
||||||
|
showToast(err.message || "赠送 VIP 失败", "error");
|
||||||
|
} finally {
|
||||||
|
setGrantSaving(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
abilities,
|
abilities,
|
||||||
activeCount,
|
activeCount,
|
||||||
closeDrawer,
|
closeDrawer,
|
||||||
|
closeGrantDrawer,
|
||||||
config,
|
config,
|
||||||
drawerOpen,
|
drawerOpen,
|
||||||
form,
|
form,
|
||||||
|
grantDrawerOpen,
|
||||||
|
grantForm,
|
||||||
|
grantSaving,
|
||||||
loading,
|
loading,
|
||||||
openDrawer,
|
openDrawer,
|
||||||
|
openGrantDrawer,
|
||||||
reload,
|
reload,
|
||||||
resourceGroups,
|
resourceGroups,
|
||||||
saving,
|
saving,
|
||||||
submit,
|
submit,
|
||||||
|
submitGrant,
|
||||||
updateLevel,
|
updateLevel,
|
||||||
|
updateGrantForm,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -129,6 +185,19 @@ function completeLevels(levels) {
|
|||||||
return defaultLevels().map((fallback) => ({ ...fallback, ...(byLevel.get(fallback.level) || {}) }));
|
return defaultLevels().map((fallback) => ({ ...fallback, ...(byLevel.get(fallback.level) || {}) }));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function defaultGrantForm() {
|
||||||
|
return {
|
||||||
|
targetUserId: "",
|
||||||
|
level: "1",
|
||||||
|
reason: "",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function firstActiveLevel(levels) {
|
||||||
|
const active = (levels || []).find((level) => level.status === "active");
|
||||||
|
return active ? String(active.level) : "";
|
||||||
|
}
|
||||||
|
|
||||||
function levelToForm(level) {
|
function levelToForm(level) {
|
||||||
return {
|
return {
|
||||||
level: Number(level.level || 0),
|
level: Number(level.level || 0),
|
||||||
|
|||||||
@ -1,7 +1,9 @@
|
|||||||
|
import CardGiftcardOutlined from "@mui/icons-material/CardGiftcardOutlined";
|
||||||
import EditOutlined from "@mui/icons-material/EditOutlined";
|
import EditOutlined from "@mui/icons-material/EditOutlined";
|
||||||
import RefreshOutlined from "@mui/icons-material/RefreshOutlined";
|
import RefreshOutlined from "@mui/icons-material/RefreshOutlined";
|
||||||
import SaveOutlined from "@mui/icons-material/SaveOutlined";
|
import SaveOutlined from "@mui/icons-material/SaveOutlined";
|
||||||
import Drawer from "@mui/material/Drawer";
|
import Drawer from "@mui/material/Drawer";
|
||||||
|
import MenuItem from "@mui/material/MenuItem";
|
||||||
import TextField from "@mui/material/TextField";
|
import TextField from "@mui/material/TextField";
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import { useVipConfigPage } from "@/features/vip-config/hooks/useVipConfigPage.js";
|
import { useVipConfigPage } from "@/features/vip-config/hooks/useVipConfigPage.js";
|
||||||
@ -46,6 +48,15 @@ export function VipConfigPage() {
|
|||||||
编辑配置
|
编辑配置
|
||||||
</Button>
|
</Button>
|
||||||
) : null}
|
) : null}
|
||||||
|
{page.abilities.canGrant ? (
|
||||||
|
<Button
|
||||||
|
disabled={page.loading}
|
||||||
|
startIcon={<CardGiftcardOutlined fontSize="small" />}
|
||||||
|
onClick={page.openGrantDrawer}
|
||||||
|
>
|
||||||
|
赠送VIP
|
||||||
|
</Button>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<DataState loading={page.loading} onRetry={page.reload}>
|
<DataState loading={page.loading} onRetry={page.reload}>
|
||||||
@ -54,10 +65,76 @@ export function VipConfigPage() {
|
|||||||
</AdminListBody>
|
</AdminListBody>
|
||||||
</DataState>
|
</DataState>
|
||||||
<VipConfigDrawer page={page} />
|
<VipConfigDrawer page={page} />
|
||||||
|
<VipGrantDrawer page={page} />
|
||||||
</AdminListPage>
|
</AdminListPage>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function VipGrantDrawer({ page }) {
|
||||||
|
const disabled = !page.abilities.canGrant || page.grantSaving || page.loading;
|
||||||
|
const activeLevels = (page.config.levels || []).filter((level) => level.status === "active");
|
||||||
|
return (
|
||||||
|
<Drawer
|
||||||
|
anchor="right"
|
||||||
|
open={page.grantDrawerOpen}
|
||||||
|
onClose={page.grantSaving ? undefined : page.closeGrantDrawer}
|
||||||
|
>
|
||||||
|
<form className="form-drawer" onSubmit={page.submitGrant}>
|
||||||
|
<h2>赠送VIP</h2>
|
||||||
|
<section className="form-drawer__section">
|
||||||
|
<div className="form-drawer__section-title">赠送信息</div>
|
||||||
|
<TextField
|
||||||
|
disabled={disabled}
|
||||||
|
inputProps={{ inputMode: "numeric" }}
|
||||||
|
label="用户ID"
|
||||||
|
value={page.grantForm.targetUserId}
|
||||||
|
onChange={(event) => page.updateGrantForm({ targetUserId: event.target.value })}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
select
|
||||||
|
disabled={disabled || activeLevels.length === 0}
|
||||||
|
label="VIP等级"
|
||||||
|
value={page.grantForm.level}
|
||||||
|
onChange={(event) => page.updateGrantForm({ level: event.target.value })}
|
||||||
|
>
|
||||||
|
{activeLevels.length === 0 ? (
|
||||||
|
<MenuItem value={page.grantForm.level || ""}>暂无启用等级</MenuItem>
|
||||||
|
) : (
|
||||||
|
activeLevels.map((level) => (
|
||||||
|
<MenuItem key={level.level} value={String(level.level)}>
|
||||||
|
VIP{level.level} · {level.name}
|
||||||
|
</MenuItem>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</TextField>
|
||||||
|
<TextField
|
||||||
|
multiline
|
||||||
|
disabled={disabled}
|
||||||
|
inputProps={{ maxLength: 512 }}
|
||||||
|
label="原因"
|
||||||
|
minRows={3}
|
||||||
|
value={page.grantForm.reason}
|
||||||
|
onChange={(event) => page.updateGrantForm({ reason: event.target.value })}
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
<div className="form-drawer__actions">
|
||||||
|
<Button disabled={page.grantSaving} type="button" onClick={page.closeGrantDrawer}>
|
||||||
|
取消
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
disabled={disabled || activeLevels.length === 0}
|
||||||
|
startIcon={<CardGiftcardOutlined fontSize="small" />}
|
||||||
|
type="submit"
|
||||||
|
variant="primary"
|
||||||
|
>
|
||||||
|
赠送
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Drawer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function VipConfigDrawer({ page }) {
|
function VipConfigDrawer({ page }) {
|
||||||
const disabled = !page.abilities.canUpdate || page.saving || page.loading;
|
const disabled = !page.abilities.canUpdate || page.saving || page.loading;
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -6,5 +6,6 @@ export function useVipConfigAbilities() {
|
|||||||
return {
|
return {
|
||||||
canView: can(PERMISSIONS.vipConfigView),
|
canView: can(PERMISSIONS.vipConfigView),
|
||||||
canUpdate: can(PERMISSIONS.vipConfigUpdate),
|
canUpdate: can(PERMISSIONS.vipConfigUpdate),
|
||||||
|
canGrant: can(PERMISSIONS.vipConfigGrant),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -45,7 +45,6 @@ export const API_OPERATIONS = {
|
|||||||
createEmojiPack: "createEmojiPack",
|
createEmojiPack: "createEmojiPack",
|
||||||
createExploreTab: "createExploreTab",
|
createExploreTab: "createExploreTab",
|
||||||
createFinanceApplication: "createFinanceApplication",
|
createFinanceApplication: "createFinanceApplication",
|
||||||
createFinanceCoinSellerRechargeOrder: "createFinanceCoinSellerRechargeOrder",
|
|
||||||
createFullServerNoticeFanout: "createFullServerNoticeFanout",
|
createFullServerNoticeFanout: "createFullServerNoticeFanout",
|
||||||
createGift: "createGift",
|
createGift: "createGift",
|
||||||
createH5Link: "createH5Link",
|
createH5Link: "createH5Link",
|
||||||
@ -152,7 +151,6 @@ export const API_OPERATIONS = {
|
|||||||
getWeeklyStarCycle: "getWeeklyStarCycle",
|
getWeeklyStarCycle: "getWeeklyStarCycle",
|
||||||
getWheelConfig: "getWheelConfig",
|
getWheelConfig: "getWheelConfig",
|
||||||
getWheelDrawSummary: "getWheelDrawSummary",
|
getWheelDrawSummary: "getWheelDrawSummary",
|
||||||
grantFinanceCoinSellerRechargeOrder: "grantFinanceCoinSellerRechargeOrder",
|
|
||||||
grantPrettyId: "grantPrettyId",
|
grantPrettyId: "grantPrettyId",
|
||||||
grantResource: "grantResource",
|
grantResource: "grantResource",
|
||||||
grantResourceGroup: "grantResourceGroup",
|
grantResourceGroup: "grantResourceGroup",
|
||||||
@ -179,7 +177,6 @@ export const API_OPERATIONS = {
|
|||||||
listEmojiPacks: "listEmojiPacks",
|
listEmojiPacks: "listEmojiPacks",
|
||||||
listExploreTabs: "listExploreTabs",
|
listExploreTabs: "listExploreTabs",
|
||||||
listFinanceApplications: "listFinanceApplications",
|
listFinanceApplications: "listFinanceApplications",
|
||||||
listFinanceCoinSellerRechargeOrders: "listFinanceCoinSellerRechargeOrders",
|
|
||||||
listFinanceWithdrawalApplications: "listFinanceWithdrawalApplications",
|
listFinanceWithdrawalApplications: "listFinanceWithdrawalApplications",
|
||||||
listFirstRechargeRewardClaims: "listFirstRechargeRewardClaims",
|
listFirstRechargeRewardClaims: "listFirstRechargeRewardClaims",
|
||||||
listGifts: "listGifts",
|
listGifts: "listGifts",
|
||||||
@ -342,9 +339,7 @@ export const API_OPERATIONS = {
|
|||||||
upsertLuckyGiftConfig: "upsertLuckyGiftConfig",
|
upsertLuckyGiftConfig: "upsertLuckyGiftConfig",
|
||||||
upsertResourceShopItems: "upsertResourceShopItems",
|
upsertResourceShopItems: "upsertResourceShopItems",
|
||||||
upsertRule: "upsertRule",
|
upsertRule: "upsertRule",
|
||||||
upsertWheelConfig: "upsertWheelConfig",
|
upsertWheelConfig: "upsertWheelConfig"
|
||||||
verifyFinanceCoinSellerRechargeOrder: "verifyFinanceCoinSellerRechargeOrder",
|
|
||||||
verifyFinanceCoinSellerRechargeReceipt: "verifyFinanceCoinSellerRechargeReceipt"
|
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export type ApiOperationId = (typeof API_OPERATIONS)[keyof typeof API_OPERATIONS];
|
export type ApiOperationId = (typeof API_OPERATIONS)[keyof typeof API_OPERATIONS];
|
||||||
@ -572,13 +567,6 @@ export const API_ENDPOINTS: Record<ApiOperationId, ApiEndpoint> = {
|
|||||||
permission: "finance-application:create",
|
permission: "finance-application:create",
|
||||||
permissions: ["finance-application:create"]
|
permissions: ["finance-application:create"]
|
||||||
},
|
},
|
||||||
createFinanceCoinSellerRechargeOrder: {
|
|
||||||
method: "POST",
|
|
||||||
operationId: API_OPERATIONS.createFinanceCoinSellerRechargeOrder,
|
|
||||||
path: "/v1/admin/finance/orders/coin-seller-recharges",
|
|
||||||
permission: "finance-order:coin-seller-recharge:create",
|
|
||||||
permissions: ["finance-order:coin-seller-recharge:create"]
|
|
||||||
},
|
|
||||||
createFullServerNoticeFanout: {
|
createFullServerNoticeFanout: {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
operationId: API_OPERATIONS.createFullServerNoticeFanout,
|
operationId: API_OPERATIONS.createFullServerNoticeFanout,
|
||||||
@ -1321,13 +1309,6 @@ export const API_ENDPOINTS: Record<ApiOperationId, ApiEndpoint> = {
|
|||||||
permission: "wheel:view",
|
permission: "wheel:view",
|
||||||
permissions: ["wheel:view"]
|
permissions: ["wheel:view"]
|
||||||
},
|
},
|
||||||
grantFinanceCoinSellerRechargeOrder: {
|
|
||||||
method: "POST",
|
|
||||||
operationId: API_OPERATIONS.grantFinanceCoinSellerRechargeOrder,
|
|
||||||
path: "/v1/admin/finance/orders/coin-seller-recharges/{order_id}/grant",
|
|
||||||
permission: "finance-order:coin-seller-recharge:grant",
|
|
||||||
permissions: ["finance-order:coin-seller-recharge:grant"]
|
|
||||||
},
|
|
||||||
grantPrettyId: {
|
grantPrettyId: {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
operationId: API_OPERATIONS.grantPrettyId,
|
operationId: API_OPERATIONS.grantPrettyId,
|
||||||
@ -1508,13 +1489,6 @@ export const API_ENDPOINTS: Record<ApiOperationId, ApiEndpoint> = {
|
|||||||
permission: "finance-application:audit",
|
permission: "finance-application:audit",
|
||||||
permissions: ["finance-application:audit"]
|
permissions: ["finance-application:audit"]
|
||||||
},
|
},
|
||||||
listFinanceCoinSellerRechargeOrders: {
|
|
||||||
method: "GET",
|
|
||||||
operationId: API_OPERATIONS.listFinanceCoinSellerRechargeOrders,
|
|
||||||
path: "/v1/admin/finance/orders/coin-seller-recharges",
|
|
||||||
permission: "finance-order:coin-seller-recharge:view",
|
|
||||||
permissions: ["finance-order:coin-seller-recharge:view"]
|
|
||||||
},
|
|
||||||
listFinanceWithdrawalApplications: {
|
listFinanceWithdrawalApplications: {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
operationId: API_OPERATIONS.listFinanceWithdrawalApplications,
|
operationId: API_OPERATIONS.listFinanceWithdrawalApplications,
|
||||||
@ -2643,20 +2617,6 @@ export const API_ENDPOINTS: Record<ApiOperationId, ApiEndpoint> = {
|
|||||||
path: "/v1/admin/activity/wheel/config",
|
path: "/v1/admin/activity/wheel/config",
|
||||||
permission: "wheel:update",
|
permission: "wheel:update",
|
||||||
permissions: ["wheel:update"]
|
permissions: ["wheel:update"]
|
||||||
},
|
|
||||||
verifyFinanceCoinSellerRechargeOrder: {
|
|
||||||
method: "POST",
|
|
||||||
operationId: API_OPERATIONS.verifyFinanceCoinSellerRechargeOrder,
|
|
||||||
path: "/v1/admin/finance/orders/coin-seller-recharges/{order_id}/verify",
|
|
||||||
permission: "finance-order:coin-seller-recharge:verify",
|
|
||||||
permissions: ["finance-order:coin-seller-recharge:verify"]
|
|
||||||
},
|
|
||||||
verifyFinanceCoinSellerRechargeReceipt: {
|
|
||||||
method: "POST",
|
|
||||||
operationId: API_OPERATIONS.verifyFinanceCoinSellerRechargeReceipt,
|
|
||||||
path: "/v1/admin/finance/orders/coin-seller-recharges/receipt-verifications",
|
|
||||||
permission: "finance-order:coin-seller-recharge:verify",
|
|
||||||
permissions: ["finance-order:coin-seller-recharge:verify"]
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
265
src/shared/api/generated/schema.d.ts
vendored
265
src/shared/api/generated/schema.d.ts
vendored
@ -2228,70 +2228,6 @@ export interface paths {
|
|||||||
patch?: never;
|
patch?: never;
|
||||||
trace?: never;
|
trace?: never;
|
||||||
};
|
};
|
||||||
"/admin/finance/orders/coin-seller-recharges": {
|
|
||||||
parameters: {
|
|
||||||
query?: never;
|
|
||||||
header?: never;
|
|
||||||
path?: never;
|
|
||||||
cookie?: never;
|
|
||||||
};
|
|
||||||
get: operations["listFinanceCoinSellerRechargeOrders"];
|
|
||||||
put?: never;
|
|
||||||
post: operations["createFinanceCoinSellerRechargeOrder"];
|
|
||||||
delete?: never;
|
|
||||||
options?: never;
|
|
||||||
head?: never;
|
|
||||||
patch?: never;
|
|
||||||
trace?: never;
|
|
||||||
};
|
|
||||||
"/admin/finance/orders/coin-seller-recharges/receipt-verifications": {
|
|
||||||
parameters: {
|
|
||||||
query?: never;
|
|
||||||
header?: never;
|
|
||||||
path?: never;
|
|
||||||
cookie?: never;
|
|
||||||
};
|
|
||||||
get?: never;
|
|
||||||
put?: never;
|
|
||||||
post: operations["verifyFinanceCoinSellerRechargeReceipt"];
|
|
||||||
delete?: never;
|
|
||||||
options?: never;
|
|
||||||
head?: never;
|
|
||||||
patch?: never;
|
|
||||||
trace?: never;
|
|
||||||
};
|
|
||||||
"/admin/finance/orders/coin-seller-recharges/{order_id}/verify": {
|
|
||||||
parameters: {
|
|
||||||
query?: never;
|
|
||||||
header?: never;
|
|
||||||
path?: never;
|
|
||||||
cookie?: never;
|
|
||||||
};
|
|
||||||
get?: never;
|
|
||||||
put?: never;
|
|
||||||
post: operations["verifyFinanceCoinSellerRechargeOrder"];
|
|
||||||
delete?: never;
|
|
||||||
options?: never;
|
|
||||||
head?: never;
|
|
||||||
patch?: never;
|
|
||||||
trace?: never;
|
|
||||||
};
|
|
||||||
"/admin/finance/orders/coin-seller-recharges/{order_id}/grant": {
|
|
||||||
parameters: {
|
|
||||||
query?: never;
|
|
||||||
header?: never;
|
|
||||||
path?: never;
|
|
||||||
cookie?: never;
|
|
||||||
};
|
|
||||||
get?: never;
|
|
||||||
put?: never;
|
|
||||||
post: operations["grantFinanceCoinSellerRechargeOrder"];
|
|
||||||
delete?: never;
|
|
||||||
options?: never;
|
|
||||||
head?: never;
|
|
||||||
patch?: never;
|
|
||||||
trace?: never;
|
|
||||||
};
|
|
||||||
"/admin/payment/temporary-links": {
|
"/admin/payment/temporary-links": {
|
||||||
parameters: {
|
parameters: {
|
||||||
query?: never;
|
query?: never;
|
||||||
@ -3954,12 +3890,6 @@ export interface components {
|
|||||||
pageSize: number;
|
pageSize: number;
|
||||||
total: number;
|
total: number;
|
||||||
};
|
};
|
||||||
ApiPageFinanceCoinSellerRechargeOrder: {
|
|
||||||
items: components["schemas"]["FinanceCoinSellerRechargeOrder"][];
|
|
||||||
page: number;
|
|
||||||
pageSize: number;
|
|
||||||
total: number;
|
|
||||||
};
|
|
||||||
ApiPageFinanceWithdrawalApplication: {
|
ApiPageFinanceWithdrawalApplication: {
|
||||||
items: components["schemas"]["FinanceWithdrawalApplication"][];
|
items: components["schemas"]["FinanceWithdrawalApplication"][];
|
||||||
page: number;
|
page: number;
|
||||||
@ -4008,15 +3938,6 @@ export interface components {
|
|||||||
ApiResponseFinanceApplicationPage: components["schemas"]["Envelope"] & {
|
ApiResponseFinanceApplicationPage: components["schemas"]["Envelope"] & {
|
||||||
data?: components["schemas"]["ApiPageFinanceApplication"];
|
data?: components["schemas"]["ApiPageFinanceApplication"];
|
||||||
};
|
};
|
||||||
ApiResponseFinanceCoinSellerRechargeOrder: components["schemas"]["Envelope"] & {
|
|
||||||
data?: components["schemas"]["FinanceCoinSellerRechargeOrder"];
|
|
||||||
};
|
|
||||||
ApiResponseFinanceCoinSellerRechargeReceiptVerification: components["schemas"]["Envelope"] & {
|
|
||||||
data?: components["schemas"]["FinanceCoinSellerRechargeReceiptVerification"];
|
|
||||||
};
|
|
||||||
ApiResponseFinanceCoinSellerRechargeOrderPage: components["schemas"]["Envelope"] & {
|
|
||||||
data?: components["schemas"]["ApiPageFinanceCoinSellerRechargeOrder"];
|
|
||||||
};
|
|
||||||
ApiResponseFinanceWithdrawalApplication: components["schemas"]["Envelope"] & {
|
ApiResponseFinanceWithdrawalApplication: components["schemas"]["Envelope"] & {
|
||||||
data?: components["schemas"]["FinanceWithdrawalApplication"];
|
data?: components["schemas"]["FinanceWithdrawalApplication"];
|
||||||
};
|
};
|
||||||
@ -4374,45 +4295,6 @@ export interface components {
|
|||||||
updatedAtMs: number;
|
updatedAtMs: number;
|
||||||
dingTalkNotified?: boolean;
|
dingTalkNotified?: boolean;
|
||||||
};
|
};
|
||||||
FinanceCoinSellerRechargeOrder: {
|
|
||||||
id: string;
|
|
||||||
appCode: string;
|
|
||||||
targetUserId: string;
|
|
||||||
targetDisplayUserId?: string;
|
|
||||||
usdAmount: string;
|
|
||||||
usdMinorAmount?: number;
|
|
||||||
coinAmount: number;
|
|
||||||
/** @enum {string} */
|
|
||||||
providerCode: "mifapay" | "v5pay" | "usdt";
|
|
||||||
/** @enum {string} */
|
|
||||||
chain?: "TRON" | "BSC";
|
|
||||||
externalOrderNo: string;
|
|
||||||
providerCountryCode?: string;
|
|
||||||
providerCurrencyCode?: string;
|
|
||||||
providerAmountMinor?: number;
|
|
||||||
providerOrderId?: string;
|
|
||||||
providerStatus?: string;
|
|
||||||
providerPayType?: string;
|
|
||||||
receiveAddress?: string;
|
|
||||||
orderDate?: string;
|
|
||||||
status: string;
|
|
||||||
verifyStatus: string;
|
|
||||||
grantStatus: string;
|
|
||||||
remark?: string;
|
|
||||||
failureReason?: string;
|
|
||||||
providerPayloadJson?: string;
|
|
||||||
walletCommandId?: string;
|
|
||||||
walletTransactionId?: string;
|
|
||||||
walletAssetType?: string;
|
|
||||||
walletAmountDelta?: number;
|
|
||||||
walletBalanceAfter?: number;
|
|
||||||
operatorName?: string;
|
|
||||||
verifiedByName?: string;
|
|
||||||
grantedByName?: string;
|
|
||||||
createdAtMs: number;
|
|
||||||
verifiedAtMs?: number | null;
|
|
||||||
grantedAtMs?: number | null;
|
|
||||||
};
|
|
||||||
FinanceWithdrawalApplication: {
|
FinanceWithdrawalApplication: {
|
||||||
id: number;
|
id: number;
|
||||||
appCode: string;
|
appCode: string;
|
||||||
@ -4446,43 +4328,6 @@ export interface components {
|
|||||||
credentialImageUrl?: string;
|
credentialImageUrl?: string;
|
||||||
credentialText?: string;
|
credentialText?: string;
|
||||||
};
|
};
|
||||||
FinanceCoinSellerRechargeOrderInput: {
|
|
||||||
appCode: string;
|
|
||||||
targetUserId: string;
|
|
||||||
usdAmount: number;
|
|
||||||
coinAmount: number;
|
|
||||||
/** @enum {string} */
|
|
||||||
providerCode: "mifapay" | "v5pay" | "usdt";
|
|
||||||
externalOrderNo: string;
|
|
||||||
/** @enum {string} */
|
|
||||||
chain?: "TRON" | "BSC";
|
|
||||||
remark?: string;
|
|
||||||
};
|
|
||||||
FinanceCoinSellerRechargeReceiptVerification: {
|
|
||||||
verified: boolean;
|
|
||||||
providerCode?: string;
|
|
||||||
externalOrderNo?: string;
|
|
||||||
providerOrderId?: string;
|
|
||||||
status?: string;
|
|
||||||
currencyCode?: string;
|
|
||||||
providerAmountMinor?: number;
|
|
||||||
/** @enum {string} */
|
|
||||||
chain?: "TRON" | "BSC";
|
|
||||||
receiveAddress?: string;
|
|
||||||
verifiedAtMs?: number;
|
|
||||||
failureReason?: string;
|
|
||||||
rawJson?: string;
|
|
||||||
};
|
|
||||||
FinanceCoinSellerRechargeReceiptVerificationInput: {
|
|
||||||
appCode: string;
|
|
||||||
/** @enum {string} */
|
|
||||||
providerCode: "mifapay" | "v5pay" | "usdt";
|
|
||||||
externalOrderNo: string;
|
|
||||||
/** @enum {string} */
|
|
||||||
chain?: "TRON" | "BSC";
|
|
||||||
usdAmount?: number;
|
|
||||||
usdMinorAmount?: number;
|
|
||||||
};
|
|
||||||
UserFinanceScope: {
|
UserFinanceScope: {
|
||||||
appCode: string;
|
appCode: string;
|
||||||
regionId: number;
|
regionId: number;
|
||||||
@ -4580,33 +4425,6 @@ export interface components {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
/** @description OK */
|
/** @description OK */
|
||||||
FinanceCoinSellerRechargeOrderPageResponse: {
|
|
||||||
headers: {
|
|
||||||
[name: string]: unknown;
|
|
||||||
};
|
|
||||||
content: {
|
|
||||||
"application/json": components["schemas"]["ApiResponseFinanceCoinSellerRechargeOrderPage"];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
/** @description OK */
|
|
||||||
FinanceCoinSellerRechargeOrderResponse: {
|
|
||||||
headers: {
|
|
||||||
[name: string]: unknown;
|
|
||||||
};
|
|
||||||
content: {
|
|
||||||
"application/json": components["schemas"]["ApiResponseFinanceCoinSellerRechargeOrder"];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
/** @description OK */
|
|
||||||
FinanceCoinSellerRechargeReceiptVerificationResponse: {
|
|
||||||
headers: {
|
|
||||||
[name: string]: unknown;
|
|
||||||
};
|
|
||||||
content: {
|
|
||||||
"application/json": components["schemas"]["ApiResponseFinanceCoinSellerRechargeReceiptVerification"];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
/** @description OK */
|
|
||||||
FinanceWithdrawalApplicationPageResponse: {
|
FinanceWithdrawalApplicationPageResponse: {
|
||||||
headers: {
|
headers: {
|
||||||
[name: string]: unknown;
|
[name: string]: unknown;
|
||||||
@ -4849,16 +4667,6 @@ export interface components {
|
|||||||
"application/json": components["schemas"]["FinanceApplicationInput"];
|
"application/json": components["schemas"]["FinanceApplicationInput"];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
FinanceCoinSellerRechargeOrderRequest: {
|
|
||||||
content: {
|
|
||||||
"application/json": components["schemas"]["FinanceCoinSellerRechargeOrderInput"];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
FinanceCoinSellerRechargeReceiptVerificationRequest: {
|
|
||||||
content: {
|
|
||||||
"application/json": components["schemas"]["FinanceCoinSellerRechargeReceiptVerificationInput"];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
FinanceApplicationAuditRequest: {
|
FinanceApplicationAuditRequest: {
|
||||||
content: {
|
content: {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
@ -7595,79 +7403,6 @@ export interface operations {
|
|||||||
200: components["responses"]["FinanceWithdrawalApplicationResponse"];
|
200: components["responses"]["FinanceWithdrawalApplicationResponse"];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
listFinanceCoinSellerRechargeOrders: {
|
|
||||||
parameters: {
|
|
||||||
query?: {
|
|
||||||
app_code?: string;
|
|
||||||
provider_code?: string;
|
|
||||||
status?: string;
|
|
||||||
verify_status?: string;
|
|
||||||
grant_status?: string;
|
|
||||||
keyword?: string;
|
|
||||||
page?: number;
|
|
||||||
page_size?: number;
|
|
||||||
};
|
|
||||||
header?: never;
|
|
||||||
path?: never;
|
|
||||||
cookie?: never;
|
|
||||||
};
|
|
||||||
requestBody?: never;
|
|
||||||
responses: {
|
|
||||||
200: components["responses"]["FinanceCoinSellerRechargeOrderPageResponse"];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
createFinanceCoinSellerRechargeOrder: {
|
|
||||||
parameters: {
|
|
||||||
query?: never;
|
|
||||||
header?: never;
|
|
||||||
path?: never;
|
|
||||||
cookie?: never;
|
|
||||||
};
|
|
||||||
requestBody?: components["requestBodies"]["FinanceCoinSellerRechargeOrderRequest"];
|
|
||||||
responses: {
|
|
||||||
201: components["responses"]["FinanceCoinSellerRechargeOrderResponse"];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
verifyFinanceCoinSellerRechargeReceipt: {
|
|
||||||
parameters: {
|
|
||||||
query?: never;
|
|
||||||
header?: never;
|
|
||||||
path?: never;
|
|
||||||
cookie?: never;
|
|
||||||
};
|
|
||||||
requestBody?: components["requestBodies"]["FinanceCoinSellerRechargeReceiptVerificationRequest"];
|
|
||||||
responses: {
|
|
||||||
200: components["responses"]["FinanceCoinSellerRechargeReceiptVerificationResponse"];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
verifyFinanceCoinSellerRechargeOrder: {
|
|
||||||
parameters: {
|
|
||||||
query?: never;
|
|
||||||
header?: never;
|
|
||||||
path: {
|
|
||||||
order_id: string;
|
|
||||||
};
|
|
||||||
cookie?: never;
|
|
||||||
};
|
|
||||||
requestBody?: never;
|
|
||||||
responses: {
|
|
||||||
200: components["responses"]["FinanceCoinSellerRechargeOrderResponse"];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
grantFinanceCoinSellerRechargeOrder: {
|
|
||||||
parameters: {
|
|
||||||
query?: never;
|
|
||||||
header?: never;
|
|
||||||
path: {
|
|
||||||
order_id: string;
|
|
||||||
};
|
|
||||||
cookie?: never;
|
|
||||||
};
|
|
||||||
requestBody?: never;
|
|
||||||
responses: {
|
|
||||||
200: components["responses"]["FinanceCoinSellerRechargeOrderResponse"];
|
|
||||||
};
|
|
||||||
};
|
|
||||||
listTemporaryPaymentLinks: {
|
listTemporaryPaymentLinks: {
|
||||||
parameters: {
|
parameters: {
|
||||||
query?: never;
|
query?: never;
|
||||||
|
|||||||
@ -585,79 +585,6 @@ export interface TemporaryPaymentLinkDto {
|
|||||||
usdMinorAmount: number;
|
usdMinorAmount: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FinanceCoinSellerRechargeOrderDto {
|
|
||||||
appCode?: string;
|
|
||||||
chain?: string;
|
|
||||||
coinAmount: number;
|
|
||||||
createdAtMs?: number;
|
|
||||||
externalOrderNo?: string;
|
|
||||||
failureReason?: string;
|
|
||||||
grantStatus?: string;
|
|
||||||
grantedAtMs?: number;
|
|
||||||
grantedByName?: string;
|
|
||||||
id: EntityId;
|
|
||||||
operatorName?: string;
|
|
||||||
orderDate?: string;
|
|
||||||
providerAmountMinor?: number;
|
|
||||||
providerCode?: string;
|
|
||||||
providerCountryCode?: string;
|
|
||||||
providerCurrencyCode?: string;
|
|
||||||
providerOrderId?: string;
|
|
||||||
providerPayloadJson?: string;
|
|
||||||
providerPayType?: string;
|
|
||||||
providerStatus?: string;
|
|
||||||
receiveAddress?: string;
|
|
||||||
remark?: string;
|
|
||||||
status?: string;
|
|
||||||
targetDisplayUserId?: string;
|
|
||||||
targetUserId: string;
|
|
||||||
usdAmount?: number | string;
|
|
||||||
usdMinorAmount?: number;
|
|
||||||
verifiedAtMs?: number;
|
|
||||||
verifiedByName?: string;
|
|
||||||
verifyStatus?: string;
|
|
||||||
walletAmountDelta?: number;
|
|
||||||
walletAssetType?: string;
|
|
||||||
walletBalanceAfter?: number;
|
|
||||||
walletCommandId?: string;
|
|
||||||
walletTransactionId?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface FinanceCoinSellerRechargeOrderPayload {
|
|
||||||
appCode: string;
|
|
||||||
chain?: string;
|
|
||||||
coinAmount: number;
|
|
||||||
externalOrderNo: string;
|
|
||||||
providerCode: "mifapay" | "v5pay" | "usdt";
|
|
||||||
remark?: string;
|
|
||||||
targetUserId: EntityId;
|
|
||||||
usdAmount: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface FinanceCoinSellerRechargeReceiptVerificationPayload {
|
|
||||||
appCode: string;
|
|
||||||
chain?: string;
|
|
||||||
externalOrderNo: string;
|
|
||||||
providerCode: "mifapay" | "v5pay" | "usdt";
|
|
||||||
usdAmount?: number;
|
|
||||||
usdMinorAmount?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface FinanceCoinSellerRechargeReceiptVerificationDto {
|
|
||||||
chain?: string;
|
|
||||||
currencyCode?: string;
|
|
||||||
externalOrderNo?: string;
|
|
||||||
failureReason?: string;
|
|
||||||
providerAmountMinor?: number;
|
|
||||||
providerCode?: string;
|
|
||||||
providerOrderId?: string;
|
|
||||||
rawJson?: string;
|
|
||||||
receiveAddress?: string;
|
|
||||||
status?: string;
|
|
||||||
verifiedAtMs?: number;
|
|
||||||
verified: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ThirdPartyPaymentRateSyncDto {
|
export interface ThirdPartyPaymentRateSyncDto {
|
||||||
missingCurrencies?: string[];
|
missingCurrencies?: string[];
|
||||||
rates?: Record<string, string>;
|
rates?: Record<string, string>;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user