修复无法扣除币商余额

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"]
}
},
"/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",

View File

@ -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" });

View File

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

View File

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

View File

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

View File

@ -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: "",

View File

@ -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 个字符"),
});

View File

@ -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))}`;
}

View File

@ -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<ApiOperationId, ApiEndpoint> = {
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,

View File

@ -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;

View File

@ -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;
}