diff --git a/contracts/admin-openapi.json b/contracts/admin-openapi.json index d96c47a..fd71559 100644 --- a/contracts/admin-openapi.json +++ b/contracts/admin-openapi.json @@ -1488,6 +1488,28 @@ "x-permissions": ["coin-seller:stock-credit"] } }, + "/admin/coin-sellers/{user_id}/stock-debits": { + "post": { + "operationId": "debitCoinSellerStock", + "responses": { + "200": { + "$ref": "#/components/responses/EmptyResponse" + } + }, + "parameters": [ + { + "in": "path", + "name": "user_id", + "required": true, + "schema": { + "type": "integer" + } + } + ], + "x-permission": "coin-seller:stock-credit", + "x-permissions": ["coin-seller:stock-credit"] + } + }, "/admin/coin-seller-salary-rates/{region_id}": { "get": { "operationId": "getCoinSellerSalaryRates", diff --git a/src/features/host-org/api.test.ts b/src/features/host-org/api.test.ts index 585ef28..d8b2d27 100644 --- a/src/features/host-org/api.test.ts +++ b/src/features/host-org/api.test.ts @@ -92,6 +92,7 @@ test("host org list APIs use generated admin paths and filters", async () => { await debitCoinSellerStock("1001", { coinAmount: 1000, commandId: "coin-seller-stock-debit-test", + rechargeAmount: "1.25", reason: "deduct bad stock", }); await deleteAgency(7001, { commandId: "agency-delete-test", reason: "cleanup" }); diff --git a/src/features/host-org/api.ts b/src/features/host-org/api.ts index d67cab5..49b23dc 100644 --- a/src/features/host-org/api.ts +++ b/src/features/host-org/api.ts @@ -318,11 +318,12 @@ export function debitCoinSellerStock( userId: EntityId, payload: CoinSellerStockDebitPayload, ): Promise { + const endpoint = API_ENDPOINTS.debitCoinSellerStock; return apiRequest( - `/v1/admin/coin-sellers/${encodeURIComponent(String(userId))}/stock-debits`, + apiEndpointPath(API_OPERATIONS.debitCoinSellerStock, { user_id: userId }), { body: payload, - method: "POST", + method: endpoint.method, }, ); } diff --git a/src/features/host-org/hooks/useHostCoinSellersPage.js b/src/features/host-org/hooks/useHostCoinSellersPage.js index 3a6d3e9..48a68d0 100644 --- a/src/features/host-org/hooks/useHostCoinSellersPage.js +++ b/src/features/host-org/hooks/useHostCoinSellersPage.js @@ -41,6 +41,7 @@ const emptyStockDebitForm = () => ({ coinAmount: "", commandId: makeCommandId("coin-seller-stock-debit"), reason: "", + rechargeAmount: "", }); const emptyRateTier = (index = 0) => ({ coinPerUsd: "", diff --git a/src/features/host-org/pages/HostCoinSellersPage.jsx b/src/features/host-org/pages/HostCoinSellersPage.jsx index 5e430bc..8f58539 100644 --- a/src/features/host-org/pages/HostCoinSellersPage.jsx +++ b/src/features/host-org/pages/HostCoinSellersPage.jsx @@ -326,17 +326,30 @@ export function HostCoinSellersPage() { - - page.setStockDebitForm?.({ ...stockDebitForm, coinAmount: event.target.value }) - } - /> + + + page.setStockDebitForm?.({ ...stockDebitForm, coinAmount: event.target.value }) + } + /> + + page.setStockDebitForm?.({ ...stockDebitForm, rechargeAmount: event.target.value }) + } + /> + page.openStockCredit(item)} @@ -514,7 +527,7 @@ function SellerActions({ item, page }) { ) : null} {page.abilities.canStockCredit ? ( page.openStockDebit(item)} diff --git a/src/features/host-org/pages/HostCoinSellersPage.test.jsx b/src/features/host-org/pages/HostCoinSellersPage.test.jsx index 6cc99b8..b2ab9f3 100644 --- a/src/features/host-org/pages/HostCoinSellersPage.test.jsx +++ b/src/features/host-org/pages/HostCoinSellersPage.test.jsx @@ -134,6 +134,7 @@ function pageFixture(patch = {}) { setPage: vi.fn(), setStockForm: vi.fn(), status: "", + stockDebitForm: { coinAmount: "", reason: "", rechargeAmount: "" }, stockForm: { coinAmount: "", reason: "", rechargeAmount: "", type: "usdt_purchase" }, sortBy: "", sortDirection: "", diff --git a/src/features/host-org/schema.ts b/src/features/host-org/schema.ts index cabafa8..003c749 100644 --- a/src/features/host-org/schema.ts +++ b/src/features/host-org/schema.ts @@ -148,6 +148,11 @@ export const coinSellerStockCreditSchema = z export const coinSellerStockDebitSchema = z.object({ coinAmount: z.coerce.number().int().positive("请输入扣除金币"), commandId: z.string().trim().min(1, "请输入 Command ID").max(120, "Command ID 不能超过 120 个字符"), + rechargeAmount: z + .string() + .trim() + .regex(/^(0|[1-9]\d*)(\.\d{1,6})?$/, "请输入最多 6 位小数的 USDT 数量") + .refine((value) => Number(value) > 0, "请输入扣除USDT数量"), reason: z.string().trim().min(1, "请输入扣除原因").max(512, "原因不能超过 512 个字符"), }); diff --git a/src/features/operations/components/CoinSellerLedgerTable.jsx b/src/features/operations/components/CoinSellerLedgerTable.jsx index cb3794d..be16bd7 100644 --- a/src/features/operations/components/CoinSellerLedgerTable.jsx +++ b/src/features/operations/components/CoinSellerLedgerTable.jsx @@ -30,6 +30,7 @@ const ledgerTypeLabels = { const bizTypeLabels = { coin_seller_coin_compensation: "金币补偿", + coin_seller_stock_deduction: "USDT扣除", coin_seller_stock_purchase: "USDT进货", coin_seller_transfer: "币商转用户", manual_credit: "金币增加", @@ -280,11 +281,16 @@ function SalaryTransferAmount({ entry }) { function PaidUSDTAmount({ entry }) { const stockType = entry.stockType || entry.metadata?.stock_type; - if (entry.bizType !== "coin_seller_stock_purchase" && stockType !== "usdt_purchase") { + const isUSDTStock = + entry.bizType === "coin_seller_stock_purchase" || + entry.bizType === "coin_seller_stock_deduction" || + stockType === "usdt_purchase" || + stockType === "usdt_deduction"; + if (!isUSDTStock) { return "-"; } const amountMicro = Number(entry.paidAmountMicro || entry.metadata?.paid_amount_micro || 0); - return amountMicro > 0 ? formatUSDTMicro(amountMicro) : "-"; + return amountMicro !== 0 ? formatSignedUSDTMicro(amountMicro) : "-"; } function formatUSDMinor(value) { @@ -301,3 +307,12 @@ function formatUSDTMicro(value) { const trimmedFraction = String(fraction).padStart(6, "0").replace(/0+$/, ""); return trimmedFraction ? `${whole}.${trimmedFraction}` : String(whole); } + +function formatSignedUSDTMicro(value) { + const amount = Number(value || 0); + if (!Number.isFinite(amount) || amount === 0) { + return "0"; + } + const sign = amount < 0 ? "-" : ""; + return `${sign}${formatUSDTMicro(Math.abs(amount))}`; +} diff --git a/src/shared/api/generated/endpoints.ts b/src/shared/api/generated/endpoints.ts index cb25c99..e8d5ea1 100644 --- a/src/shared/api/generated/endpoints.ts +++ b/src/shared/api/generated/endpoints.ts @@ -64,6 +64,7 @@ export const API_OPERATIONS = { createWeeklyStarCycle: "createWeeklyStarCycle", creditCoinSellerStock: "creditCoinSellerStock", dashboardOverview: "dashboardOverview", + debitCoinSellerStock: "debitCoinSellerStock", deleteAgency: "deleteAgency", deleteAppVersion: "deleteAppVersion", deleteBanner: "deleteBanner", @@ -649,6 +650,13 @@ export const API_ENDPOINTS: Record = { permission: "overview:view", permissions: ["overview:view"] }, + debitCoinSellerStock: { + method: "POST", + operationId: API_OPERATIONS.debitCoinSellerStock, + path: "/v1/admin/coin-sellers/{user_id}/stock-debits", + permission: "coin-seller:stock-credit", + permissions: ["coin-seller:stock-credit"] + }, deleteAgency: { method: "POST", operationId: API_OPERATIONS.deleteAgency, diff --git a/src/shared/api/generated/schema.d.ts b/src/shared/api/generated/schema.d.ts index f373bfb..c0d0602 100644 --- a/src/shared/api/generated/schema.d.ts +++ b/src/shared/api/generated/schema.d.ts @@ -1044,6 +1044,22 @@ export interface paths { patch?: never; trace?: never; }; + "/admin/coin-sellers/{user_id}/stock-debits": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["debitCoinSellerStock"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/admin/coin-seller-salary-rates/{region_id}": { parameters: { query?: never; @@ -4959,6 +4975,20 @@ export interface operations { 200: components["responses"]["EmptyResponse"]; }; }; + debitCoinSellerStock: { + parameters: { + query?: never; + header?: never; + path: { + user_id: number; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: components["responses"]["EmptyResponse"]; + }; + }; getCoinSellerSalaryRates: { parameters: { query?: never; diff --git a/src/shared/api/types.ts b/src/shared/api/types.ts index 1c2b3cc..caaa54b 100644 --- a/src/shared/api/types.ts +++ b/src/shared/api/types.ts @@ -671,6 +671,7 @@ export interface CoinSellerStockDebitPayload { coinAmount: number; commandId: string; evidenceRef?: string; + rechargeAmount: string; reason: string; } @@ -678,7 +679,12 @@ export interface CoinSellerStockDebitDto { availableDelta: number; balanceAfter: number; coinAmount: number; + countsAsSellerRecharge?: boolean; + createdAtMs?: number; + paidAmountMicro?: number; + paidCurrencyCode?: string; sellerUserId: string; + stockType?: string; transactionId: string; }