修复无法扣除币商余额

This commit is contained in:
local 2026-06-23 23:29:21 +08:00
parent 81fa3a25bb
commit 6ab21a8baa
11 changed files with 120 additions and 17 deletions

View File

@ -1488,6 +1488,28 @@
"x-permissions": ["coin-seller:stock-credit"] "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}": { "/admin/coin-seller-salary-rates/{region_id}": {
"get": { "get": {
"operationId": "getCoinSellerSalaryRates", "operationId": "getCoinSellerSalaryRates",

View File

@ -92,6 +92,7 @@ test("host org list APIs use generated admin paths and filters", async () => {
await debitCoinSellerStock("1001", { await debitCoinSellerStock("1001", {
coinAmount: 1000, coinAmount: 1000,
commandId: "coin-seller-stock-debit-test", commandId: "coin-seller-stock-debit-test",
rechargeAmount: "1.25",
reason: "deduct bad stock", reason: "deduct bad stock",
}); });
await deleteAgency(7001, { commandId: "agency-delete-test", reason: "cleanup" }); await deleteAgency(7001, { commandId: "agency-delete-test", reason: "cleanup" });

View File

@ -318,11 +318,12 @@ export function debitCoinSellerStock(
userId: EntityId, userId: EntityId,
payload: CoinSellerStockDebitPayload, payload: CoinSellerStockDebitPayload,
): Promise<CoinSellerStockDebitDto> { ): Promise<CoinSellerStockDebitDto> {
const endpoint = API_ENDPOINTS.debitCoinSellerStock;
return apiRequest<CoinSellerStockDebitDto, CoinSellerStockDebitPayload>( return apiRequest<CoinSellerStockDebitDto, CoinSellerStockDebitPayload>(
`/v1/admin/coin-sellers/${encodeURIComponent(String(userId))}/stock-debits`, apiEndpointPath(API_OPERATIONS.debitCoinSellerStock, { user_id: userId }),
{ {
body: payload, body: payload,
method: "POST", method: endpoint.method,
}, },
); );
} }

View File

@ -41,6 +41,7 @@ const emptyStockDebitForm = () => ({
coinAmount: "", coinAmount: "",
commandId: makeCommandId("coin-seller-stock-debit"), commandId: makeCommandId("coin-seller-stock-debit"),
reason: "", reason: "",
rechargeAmount: "",
}); });
const emptyRateTier = (index = 0) => ({ const emptyRateTier = (index = 0) => ({
coinPerUsd: "", coinPerUsd: "",

View File

@ -326,17 +326,30 @@ export function HostCoinSellersPage() {
</AdminFormFieldGrid> </AdminFormFieldGrid>
</AdminFormSection> </AdminFormSection>
<AdminFormSection title="扣除信息"> <AdminFormSection title="扣除信息">
<AdminFormAmountField <AdminFormFieldGrid columns="repeat(2, minmax(0, 1fr))">
disabled={!page.abilities.canStockCredit} <AdminFormAmountField
inputProps={{ inputMode: "numeric", pattern: "[0-9]*" }} disabled={!page.abilities.canStockCredit}
label="扣除金币" inputProps={{ inputMode: "numeric", pattern: "[0-9]*" }}
required label="扣除金币"
unit="金币" required
value={stockDebitForm.coinAmount} unit="金币"
onChange={(event) => value={stockDebitForm.coinAmount}
page.setStockDebitForm?.({ ...stockDebitForm, coinAmount: event.target.value }) onChange={(event) =>
} page.setStockDebitForm?.({ ...stockDebitForm, coinAmount: event.target.value })
/> }
/>
<AdminFormAmountField
disabled={!page.abilities.canStockCredit}
inputProps={{ inputMode: "decimal" }}
label="扣除USDT数量"
required
unit="USDT"
value={stockDebitForm.rechargeAmount}
onChange={(event) =>
page.setStockDebitForm?.({ ...stockDebitForm, rechargeAmount: event.target.value })
}
/>
</AdminFormFieldGrid>
</AdminFormSection> </AdminFormSection>
<AdminFormSection title="备注信息"> <AdminFormSection title="备注信息">
<TextField <TextField
@ -504,7 +517,7 @@ function SellerActions({ item, page }) {
) : null} ) : null}
{page.abilities.canStockCredit ? ( {page.abilities.canStockCredit ? (
<AdminActionIconButton <AdminActionIconButton
disabled={item.status !== "active" || page.loadingAction === `coin-seller-stock-${item.userId}`} disabled={page.loadingAction === `coin-seller-stock-${item.userId}`}
label="加金币" label="加金币"
sx={coinCreditActionSx} sx={coinCreditActionSx}
onClick={() => page.openStockCredit(item)} onClick={() => page.openStockCredit(item)}
@ -514,7 +527,7 @@ function SellerActions({ item, page }) {
) : null} ) : null}
{page.abilities.canStockCredit ? ( {page.abilities.canStockCredit ? (
<AdminActionIconButton <AdminActionIconButton
disabled={item.status !== "active" || page.loadingAction === `coin-seller-stock-debit-${item.userId}`} disabled={page.loadingAction === `coin-seller-stock-debit-${item.userId}`}
label="扣金币" label="扣金币"
sx={coinDebitActionSx} sx={coinDebitActionSx}
onClick={() => page.openStockDebit(item)} onClick={() => page.openStockDebit(item)}

View File

@ -134,6 +134,7 @@ function pageFixture(patch = {}) {
setPage: vi.fn(), setPage: vi.fn(),
setStockForm: vi.fn(), setStockForm: vi.fn(),
status: "", status: "",
stockDebitForm: { coinAmount: "", reason: "", rechargeAmount: "" },
stockForm: { coinAmount: "", reason: "", rechargeAmount: "", type: "usdt_purchase" }, stockForm: { coinAmount: "", reason: "", rechargeAmount: "", type: "usdt_purchase" },
sortBy: "", sortBy: "",
sortDirection: "", sortDirection: "",

View File

@ -148,6 +148,11 @@ export const coinSellerStockCreditSchema = z
export const coinSellerStockDebitSchema = z.object({ export const coinSellerStockDebitSchema = z.object({
coinAmount: z.coerce.number().int().positive("请输入扣除金币"), coinAmount: z.coerce.number().int().positive("请输入扣除金币"),
commandId: z.string().trim().min(1, "请输入 Command ID").max(120, "Command ID 不能超过 120 个字符"), 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 个字符"), reason: z.string().trim().min(1, "请输入扣除原因").max(512, "原因不能超过 512 个字符"),
}); });

View File

@ -30,6 +30,7 @@ const ledgerTypeLabels = {
const bizTypeLabels = { const bizTypeLabels = {
coin_seller_coin_compensation: "金币补偿", coin_seller_coin_compensation: "金币补偿",
coin_seller_stock_deduction: "USDT扣除",
coin_seller_stock_purchase: "USDT进货", coin_seller_stock_purchase: "USDT进货",
coin_seller_transfer: "币商转用户", coin_seller_transfer: "币商转用户",
manual_credit: "金币增加", manual_credit: "金币增加",
@ -280,11 +281,16 @@ function SalaryTransferAmount({ entry }) {
function PaidUSDTAmount({ entry }) { function PaidUSDTAmount({ entry }) {
const stockType = entry.stockType || entry.metadata?.stock_type; 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 "-"; return "-";
} }
const amountMicro = Number(entry.paidAmountMicro || entry.metadata?.paid_amount_micro || 0); const amountMicro = Number(entry.paidAmountMicro || entry.metadata?.paid_amount_micro || 0);
return amountMicro > 0 ? formatUSDTMicro(amountMicro) : "-"; return amountMicro !== 0 ? formatSignedUSDTMicro(amountMicro) : "-";
} }
function formatUSDMinor(value) { function formatUSDMinor(value) {
@ -301,3 +307,12 @@ function formatUSDTMicro(value) {
const trimmedFraction = String(fraction).padStart(6, "0").replace(/0+$/, ""); const trimmedFraction = String(fraction).padStart(6, "0").replace(/0+$/, "");
return trimmedFraction ? `${whole}.${trimmedFraction}` : String(whole); 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))}`;
}

View File

@ -64,6 +64,7 @@ export const API_OPERATIONS = {
createWeeklyStarCycle: "createWeeklyStarCycle", createWeeklyStarCycle: "createWeeklyStarCycle",
creditCoinSellerStock: "creditCoinSellerStock", creditCoinSellerStock: "creditCoinSellerStock",
dashboardOverview: "dashboardOverview", dashboardOverview: "dashboardOverview",
debitCoinSellerStock: "debitCoinSellerStock",
deleteAgency: "deleteAgency", deleteAgency: "deleteAgency",
deleteAppVersion: "deleteAppVersion", deleteAppVersion: "deleteAppVersion",
deleteBanner: "deleteBanner", deleteBanner: "deleteBanner",
@ -649,6 +650,13 @@ export const API_ENDPOINTS: Record<ApiOperationId, ApiEndpoint> = {
permission: "overview:view", permission: "overview:view",
permissions: ["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: { deleteAgency: {
method: "POST", method: "POST",
operationId: API_OPERATIONS.deleteAgency, operationId: API_OPERATIONS.deleteAgency,

View File

@ -1044,6 +1044,22 @@ export interface paths {
patch?: never; patch?: never;
trace?: 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}": { "/admin/coin-seller-salary-rates/{region_id}": {
parameters: { parameters: {
query?: never; query?: never;
@ -4959,6 +4975,20 @@ export interface operations {
200: components["responses"]["EmptyResponse"]; 200: components["responses"]["EmptyResponse"];
}; };
}; };
debitCoinSellerStock: {
parameters: {
query?: never;
header?: never;
path: {
user_id: number;
};
cookie?: never;
};
requestBody?: never;
responses: {
200: components["responses"]["EmptyResponse"];
};
};
getCoinSellerSalaryRates: { getCoinSellerSalaryRates: {
parameters: { parameters: {
query?: never; query?: never;

View File

@ -671,6 +671,7 @@ export interface CoinSellerStockDebitPayload {
coinAmount: number; coinAmount: number;
commandId: string; commandId: string;
evidenceRef?: string; evidenceRef?: string;
rechargeAmount: string;
reason: string; reason: string;
} }
@ -678,7 +679,12 @@ export interface CoinSellerStockDebitDto {
availableDelta: number; availableDelta: number;
balanceAfter: number; balanceAfter: number;
coinAmount: number; coinAmount: number;
countsAsSellerRecharge?: boolean;
createdAtMs?: number;
paidAmountMicro?: number;
paidCurrencyCode?: string;
sellerUserId: string; sellerUserId: string;
stockType?: string;
transactionId: string; transactionId: string;
} }