diff --git a/contracts/admin-openapi.json b/contracts/admin-openapi.json index 736a061..29bd213 100644 --- a/contracts/admin-openapi.json +++ b/contracts/admin-openapi.json @@ -4571,6 +4571,28 @@ "x-permissions": ["finance-order:coin-seller-recharge:grant"] } }, + "/admin/finance/usdt-addresses/{app_code}": { + "get": { + "operationId": "getFinanceUSDTAddresses", + "parameters": [ + { + "in": "path", + "name": "app_code", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "$ref": "#/components/responses/FinanceUSDTAddressesResponse" + } + }, + "x-permission": "finance-order:coin-seller-recharge:view", + "x-permissions": ["finance-order:coin-seller-recharge:view"] + } + }, "/admin/finance/coin-seller-recharge-exchange-rates/{app_code}": { "get": { "operationId": "getFinanceCoinSellerRechargeExchangeRate", @@ -8745,6 +8767,16 @@ } } }, + "FinanceUSDTAddressesResponse": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseFinanceUSDTAddresses" + } + } + } + }, "FinanceCoinSellerRechargeReceiptVerificationResponse": { "description": "OK", "content": { @@ -10972,6 +11004,21 @@ } ] }, + "ApiResponseFinanceUSDTAddresses": { + "allOf": [ + { + "$ref": "#/components/schemas/Envelope" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/FinanceUSDTAddresses" + } + } + } + ] + }, "ApiResponseFinanceCoinSellerRechargeReceiptVerification": { "allOf": [ { @@ -13400,6 +13447,38 @@ } } }, + "FinanceUSDTAddress": { + "type": "object", + "required": ["chain", "address", "updatedAtMs"], + "properties": { + "chain": { + "type": "string", + "enum": ["TRON", "BSC"] + }, + "address": { + "type": "string" + }, + "updatedAtMs": { + "type": "integer", + "format": "int64" + } + } + }, + "FinanceUSDTAddresses": { + "type": "object", + "required": ["appCode", "items"], + "properties": { + "appCode": { + "type": "string" + }, + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/FinanceUSDTAddress" + } + } + } + }, "FinanceCoinSellerRechargeExchangeRateInput": { "type": "object", "required": ["tiers", "whitelist"], diff --git a/finance/src/FinanceApp.jsx b/finance/src/FinanceApp.jsx index eae89de..aab42e2 100644 --- a/finance/src/FinanceApp.jsx +++ b/finance/src/FinanceApp.jsx @@ -10,6 +10,7 @@ import { fetchFinanceSession, grantFinanceCoinSellerRechargeOrder, getFinanceCoinSellerRechargeExchangeRate, + getFinanceUSDTAddresses, listFinanceCoinSellerRechargeOrders, listFinanceRechargeDetails, listFinanceRechargeRegions, @@ -514,6 +515,8 @@ export function FinanceApp() { [], ); + const loadUSDTAddresses = useCallback((appCode) => getFinanceUSDTAddresses(appCode), []); + const saveCoinSellerRechargeExchangeRate = useCallback( async (appCode, payload) => { const saved = await replaceFinanceCoinSellerRechargeExchangeRate(appCode, cleanPayload(payload)); @@ -727,6 +730,7 @@ export function FinanceApp() { orders={coinSellerRechargeOrdersState.data} onCreate={submitCoinSellerRechargeOrder} onLoadExchangeRate={loadCoinSellerRechargeExchangeRate} + onLoadUSDTAddresses={loadUSDTAddresses} onFiltersChange={updateCoinSellerRechargeOrderFilters} onGrant={grantCoinSellerRechargeOrder} onReload={loadCoinSellerRechargeOrders} diff --git a/finance/src/FinanceApp.test.jsx b/finance/src/FinanceApp.test.jsx index a1dbd46..1f3a503 100644 --- a/finance/src/FinanceApp.test.jsx +++ b/finance/src/FinanceApp.test.jsx @@ -26,6 +26,7 @@ vi.mock("./api.js", () => ({ fetchFinanceSession: vi.fn(), grantFinanceCoinSellerRechargeOrder: vi.fn(), getFinanceCoinSellerRechargeExchangeRate: vi.fn(), + getFinanceUSDTAddresses: vi.fn(), listFinanceCoinSellerRechargeOrders: vi.fn(), listFinanceRechargeDetails: vi.fn(), listFinanceRechargeRegions: vi.fn(), diff --git a/finance/src/api.js b/finance/src/api.js index ec4303e..f5ac3a9 100644 --- a/finance/src/api.js +++ b/finance/src/api.js @@ -105,6 +105,23 @@ export async function getFinanceCoinSellerRechargeExchangeRate(appCode) { }); } +export async function getFinanceUSDTAddresses(appCode) { + const endpoint = API_ENDPOINTS.getFinanceUSDTAddresses; + const data = await apiRequest(apiEndpointPath(API_OPERATIONS.getFinanceUSDTAddresses, { app_code: appCode }), { + method: endpoint.method + }); + return { + appCode: stringValue(data?.appCode ?? appCode), + items: listItems(data?.items) + .map((item) => ({ + address: stringValue(item?.address), + chain: stringValue(item?.chain).toUpperCase(), + updatedAtMs: Number(item?.updatedAtMs || 0) + })) + .filter((item) => item.address && item.chain) + }; +} + export async function replaceFinanceCoinSellerRechargeExchangeRate(appCode, payload) { const endpoint = API_ENDPOINTS.replaceFinanceCoinSellerRechargeExchangeRate; return apiRequest(apiEndpointPath(API_OPERATIONS.replaceFinanceCoinSellerRechargeExchangeRate, { app_code: appCode }), { diff --git a/finance/src/api.test.js b/finance/src/api.test.js index 816be65..0d6b616 100644 --- a/finance/src/api.test.js +++ b/finance/src/api.test.js @@ -6,6 +6,7 @@ import { fetchFinanceOptions, fetchFinanceSession, getFinanceCoinSellerRechargeExchangeRate, + getFinanceUSDTAddresses, grantFinanceCoinSellerRechargeOrder, listFinanceCoinSellerRechargeOrders, listFinanceRechargeDetails, @@ -174,6 +175,33 @@ test("finance coin seller quote and exchange rate APIs use generated endpoint pa expect(calls[2][1]?.method).toBe("PUT"); }); +test("finance USDT address API uses the selected app path and normalizes SQL rows", async () => { + vi.stubGlobal( + "fetch", + vi.fn(async () => + new Response( + JSON.stringify({ + code: 0, + data: { + appCode: "aslan", + items: [{ address: "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t", chain: "TRON", updatedAtMs: 123 }] + } + }) + ) + ) + ); + + const result = await getFinanceUSDTAddresses("aslan"); + + const [url, init] = vi.mocked(fetch).mock.calls[0]; + expect(String(url)).toContain("/api/v1/admin/finance/usdt-addresses/aslan"); + expect(init?.method).toBe("GET"); + expect(result).toEqual({ + appCode: "aslan", + items: [{ address: "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t", chain: "TRON", updatedAtMs: 123 }] + }); +}); + test("finance recharge details query every app with app header and merge by recharge time", async () => { vi.stubGlobal( "fetch", diff --git a/finance/src/components/FinanceCoinSellerRechargeOrderList.jsx b/finance/src/components/FinanceCoinSellerRechargeOrderList.jsx index 38fe601..3de2bb1 100644 --- a/finance/src/components/FinanceCoinSellerRechargeOrderList.jsx +++ b/finance/src/components/FinanceCoinSellerRechargeOrderList.jsx @@ -1,4 +1,5 @@ import AddOutlined from "@mui/icons-material/AddOutlined"; +import AccountBalanceWalletOutlined from "@mui/icons-material/AccountBalanceWalletOutlined"; import FactCheckOutlined from "@mui/icons-material/FactCheckOutlined"; import PaidOutlined from "@mui/icons-material/PaidOutlined"; import RefreshOutlined from "@mui/icons-material/RefreshOutlined"; @@ -33,6 +34,7 @@ import { import { FinanceCoinSellerRechargeCreateDialog } from "./FinanceCoinSellerRechargeCreateDialog.jsx"; import { FinanceCoinSellerExchangeRateDialog } from "./FinanceCoinSellerExchangeRateDialog.jsx"; import { FinanceCoinSellerGrantDialog } from "./FinanceCoinSellerGrantDialog.jsx"; +import { FinanceUSDTAddressDialog } from "./FinanceUSDTAddressDialog.jsx"; export function FinanceCoinSellerRechargeOrderList({ actionLoading, @@ -46,6 +48,7 @@ export function FinanceCoinSellerRechargeOrderList({ loading, onCreate, onLoadExchangeRate, + onLoadUSDTAddresses, onFiltersChange, onGrant, onReload, @@ -62,6 +65,7 @@ export function FinanceCoinSellerRechargeOrderList({ const [quoteError, setQuoteError] = useState(""); const [quoteLoading, setQuoteLoading] = useState(false); const [rateConfigOpen, setRateConfigOpen] = useState(false); + const [usdtAddressOpen, setUSDTAddressOpen] = useState(false); const [receiptVerification, setReceiptVerification] = useState(null); const [receiptLoading, setReceiptLoading] = useState(false); const [grantTarget, setGrantTarget] = useState(null); @@ -281,6 +285,13 @@ export function FinanceCoinSellerRechargeOrderList({ + {canConfigureExchangeRate ? ( + + + ))} + + ) : null} + {!loading && appCode && !data.items.length && !error ? ( +
当前 APP 未配置 USDT 收款地址
+ ) : null} + + + + + + ); +} + +function chainLabel(chain) { + if (chain === "TRON") { + return "TRON · TRC20"; + } + if (chain === "BSC") { + return "BSC · BEP20"; + } + return chain; +} diff --git a/finance/src/components/FinanceUSDTAddressDialog.test.jsx b/finance/src/components/FinanceUSDTAddressDialog.test.jsx new file mode 100644 index 0000000..b70a68d --- /dev/null +++ b/finance/src/components/FinanceUSDTAddressDialog.test.jsx @@ -0,0 +1,43 @@ +import { fireEvent, render, screen, waitFor } from "@testing-library/react"; +import { afterEach, expect, test, vi } from "vitest"; +import { FinanceUSDTAddressDialog } from "./FinanceUSDTAddressDialog.jsx"; + +afterEach(() => { + vi.unstubAllGlobals(); +}); + +test("loads the selected app USDT address and copies the exact SQL value", async () => { + const writeText = vi.fn(async () => undefined); + vi.stubGlobal("navigator", { clipboard: { writeText } }); + const onLoad = vi.fn(async (appCode) => ({ + appCode, + items: [ + { + address: "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t", + chain: "TRON", + updatedAtMs: 1760000000000, + }, + ], + })); + + render( + , + ); + + expect(await screen.findByText("TRON · TRC20")).toBeInTheDocument(); + expect(screen.getByText("TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t")).toBeInTheDocument(); + expect(onLoad).toHaveBeenCalledWith("aslan"); + + fireEvent.click(screen.getByRole("button", { name: "复制 TRON 地址" })); + await waitFor(() => expect(writeText).toHaveBeenCalledWith("TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t")); + expect(screen.getByRole("button", { name: "复制 TRON 地址" })).toHaveTextContent("已复制"); +}); diff --git a/finance/src/styles/index.css b/finance/src/styles/index.css index 55ee2af..8e6c042 100644 --- a/finance/src/styles/index.css +++ b/finance/src/styles/index.css @@ -1850,6 +1850,59 @@ a { margin-left: auto; } +.finance-usdt-address-dialog { + display: grid; + gap: var(--space-4); + padding-top: var(--space-2) !important; +} + +.finance-usdt-address-list { + display: grid; + gap: var(--space-3); +} + +.finance-usdt-address-item { + display: grid; + gap: var(--space-2); + border-bottom: 1px solid var(--border); + padding-bottom: var(--space-3); +} + +.finance-usdt-address-item:last-child { + border-bottom: 0; + padding-bottom: 0; +} + +.finance-usdt-address-item__meta, +.finance-usdt-address-item__value { + display: flex; + align-items: center; + justify-content: space-between; + gap: var(--space-3); +} + +.finance-usdt-address-item__meta small { + color: var(--text-tertiary); +} + +.finance-usdt-address-item__value code { + min-width: 0; + overflow-wrap: anywhere; + color: var(--text-primary); + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; + font-size: 13px; +} + +.finance-usdt-address-item__value .MuiButton-root { + flex: 0 0 auto; +} + +.finance-usdt-address-empty { + color: var(--text-secondary); + padding: var(--space-5) 0; + text-align: center; +} + .finance-fchip { display: inline-flex; align-items: center; diff --git a/src/shared/api/generated/endpoints.ts b/src/shared/api/generated/endpoints.ts index ba3782a..a75c051 100644 --- a/src/shared/api/generated/endpoints.ts +++ b/src/shared/api/generated/endpoints.ts @@ -137,6 +137,7 @@ export const API_OPERATIONS = { getCumulativeRechargeRewardConfig: "getCumulativeRechargeRewardConfig", getFinanceCoinSellerRechargeExchangeRate: "getFinanceCoinSellerRechargeExchangeRate", getFinanceScope: "getFinanceScope", + getFinanceUSDTAddresses: "getFinanceUSDTAddresses", getFirstRechargeRewardConfig: "getFirstRechargeRewardConfig", getHumanRoomRobotConfig: "getHumanRoomRobotConfig", getInviteActivityRewardConfig: "getInviteActivityRewardConfig", @@ -1267,6 +1268,13 @@ export const API_ENDPOINTS: Record = { permission: "finance:view", permissions: ["finance:view"] }, + getFinanceUSDTAddresses: { + method: "GET", + operationId: API_OPERATIONS.getFinanceUSDTAddresses, + path: "/v1/admin/finance/usdt-addresses/{app_code}", + permission: "finance-order:coin-seller-recharge:view", + permissions: ["finance-order:coin-seller-recharge:view"] + }, getFirstRechargeRewardConfig: { method: "GET", operationId: API_OPERATIONS.getFirstRechargeRewardConfig, diff --git a/src/shared/api/generated/schema.d.ts b/src/shared/api/generated/schema.d.ts index bc8034e..cff1343 100644 --- a/src/shared/api/generated/schema.d.ts +++ b/src/shared/api/generated/schema.d.ts @@ -2588,6 +2588,22 @@ export interface paths { patch?: never; trace?: never; }; + "/admin/finance/usdt-addresses/{app_code}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["getFinanceUSDTAddresses"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/admin/finance/coin-seller-recharge-exchange-rates/{app_code}": { parameters: { query?: never; @@ -5424,6 +5440,9 @@ export interface components { ApiResponseFinanceCoinSellerRechargeExchangeRate: components["schemas"]["Envelope"] & { data?: components["schemas"]["FinanceCoinSellerRechargeExchangeRate"]; }; + ApiResponseFinanceUSDTAddresses: components["schemas"]["Envelope"] & { + data?: components["schemas"]["FinanceUSDTAddresses"]; + }; ApiResponseFinanceCoinSellerRechargeReceiptVerification: components["schemas"]["Envelope"] & { data?: components["schemas"]["FinanceCoinSellerRechargeReceiptVerification"]; }; @@ -6200,6 +6219,17 @@ export interface components { whitelist: components["schemas"]["FinanceCoinSellerRechargeExchangeRateWhitelist"][]; updatedAtMs: number; }; + FinanceUSDTAddress: { + /** @enum {string} */ + chain: "TRON" | "BSC"; + address: string; + /** Format: int64 */ + updatedAtMs: number; + }; + FinanceUSDTAddresses: { + appCode: string; + items: components["schemas"]["FinanceUSDTAddress"][]; + }; FinanceCoinSellerRechargeExchangeRateInput: { tiers: { minUsdAmount: number; @@ -7056,6 +7086,15 @@ export interface components { }; }; /** @description OK */ + FinanceUSDTAddressesResponse: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ApiResponseFinanceUSDTAddresses"]; + }; + }; + /** @description OK */ FinanceCoinSellerRechargeReceiptVerificationResponse: { headers: { [name: string]: unknown; @@ -10780,6 +10819,20 @@ export interface operations { 200: components["responses"]["FinanceCoinSellerRechargeOrderResponse"]; }; }; + getFinanceUSDTAddresses: { + parameters: { + query?: never; + header?: never; + path: { + app_code: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: components["responses"]["FinanceUSDTAddressesResponse"]; + }; + }; getFinanceCoinSellerRechargeExchangeRate: { parameters: { query?: never;