From 9045de6ad26e05b2cbd559c527da0bb59e93c2fc Mon Sep 17 00:00:00 2001 From: zhx Date: Wed, 15 Jul 2026 13:09:53 +0800 Subject: [PATCH] feat(finance): edit app USDT receive addresses --- contracts/admin-openapi.json | 105 ++++++++++- finance/src/FinanceApp.jsx | 15 ++ finance/src/FinanceApp.test.jsx | 2 + finance/src/api.js | 31 +++- finance/src/api.test.js | 31 ++++ .../FinanceCoinSellerRechargeOrderList.jsx | 4 + ...inanceCoinSellerRechargeOrderList.test.jsx | 14 ++ .../components/FinanceUSDTAddressDialog.jsx | 167 +++++++++++++++--- .../FinanceUSDTAddressDialog.test.jsx | 49 +++++ finance/src/constants.js | 4 +- finance/src/styles/index.css | 18 ++ src/shared/api/generated/endpoints.ts | 8 + src/shared/api/generated/schema.d.ts | 67 ++++++- 13 files changed, 474 insertions(+), 41 deletions(-) diff --git a/contracts/admin-openapi.json b/contracts/admin-openapi.json index 29bd213..837af5f 100644 --- a/contracts/admin-openapi.json +++ b/contracts/admin-openapi.json @@ -4593,6 +4593,40 @@ "x-permissions": ["finance-order:coin-seller-recharge:view"] } }, + "/admin/finance/usdt-addresses/{app_code}/{chain}": { + "put": { + "operationId": "upsertFinanceUSDTAddress", + "parameters": [ + { + "in": "path", + "name": "app_code", + "required": true, + "schema": { + "type": "string" + } + }, + { + "in": "path", + "name": "chain", + "required": true, + "schema": { + "type": "string", + "enum": ["TRON", "BSC"] + } + } + ], + "requestBody": { + "$ref": "#/components/requestBodies/FinanceUSDTAddressRequest" + }, + "responses": { + "200": { + "$ref": "#/components/responses/FinanceUSDTAddressResponse" + } + }, + "x-permission": "finance-order:usdt-address:update", + "x-permissions": ["finance-order:usdt-address:update"] + } + }, "/admin/finance/coin-seller-recharge-exchange-rates/{app_code}": { "get": { "operationId": "getFinanceCoinSellerRechargeExchangeRate", @@ -8229,6 +8263,15 @@ } } }, + "FinanceUSDTAddressRequest": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FinanceUSDTAddressInput" + } + } + } + }, "FinanceWithdrawalAuditRequest": { "content": { "application/json": { @@ -8777,6 +8820,16 @@ } } }, + "FinanceUSDTAddressResponse": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponseFinanceUSDTAddressUpsert" + } + } + } + }, "FinanceCoinSellerRechargeReceiptVerificationResponse": { "description": "OK", "content": { @@ -11019,6 +11072,21 @@ } ] }, + "ApiResponseFinanceUSDTAddressUpsert": { + "allOf": [ + { + "$ref": "#/components/schemas/Envelope" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/FinanceUSDTAddressUpsert" + } + } + } + ] + }, "ApiResponseFinanceCoinSellerRechargeReceiptVerification": { "allOf": [ { @@ -13137,7 +13205,7 @@ }, "chain": { "type": "string", - "enum": ["TRON", "BSC"] + "enum": ["C2C", "OFFCHAIN", "TRON", "BSC"] }, "externalOrderNo": { "type": "string" @@ -13335,7 +13403,7 @@ }, "chain": { "type": "string", - "enum": ["TRON", "BSC"] + "enum": ["C2C", "OFFCHAIN", "TRON", "BSC"] }, "remark": { "type": "string", @@ -13479,6 +13547,35 @@ } } }, + "FinanceUSDTAddressInput": { + "type": "object", + "required": ["address"], + "properties": { + "address": { + "type": "string" + } + } + }, + "FinanceUSDTAddressUpsert": { + "type": "object", + "required": ["appCode", "chain", "address", "updatedAtMs"], + "properties": { + "appCode": { + "type": "string" + }, + "chain": { + "type": "string", + "enum": ["TRON", "BSC"] + }, + "address": { + "type": "string" + }, + "updatedAtMs": { + "type": "integer", + "format": "int64" + } + } + }, "FinanceCoinSellerRechargeExchangeRateInput": { "type": "object", "required": ["tiers", "whitelist"], @@ -13550,7 +13647,7 @@ }, "chain": { "type": "string", - "enum": ["TRON", "BSC"] + "enum": ["C2C", "OFFCHAIN", "TRON", "BSC"] }, "receiveAddress": { "type": "string" @@ -13582,7 +13679,7 @@ }, "chain": { "type": "string", - "enum": ["TRON", "BSC"] + "enum": ["C2C", "OFFCHAIN", "TRON", "BSC"] }, "usdAmount": { "type": "number", diff --git a/finance/src/FinanceApp.jsx b/finance/src/FinanceApp.jsx index aab42e2..d6efc24 100644 --- a/finance/src/FinanceApp.jsx +++ b/finance/src/FinanceApp.jsx @@ -19,6 +19,7 @@ import { replaceFinanceCoinSellerRechargeExchangeRate, rejectFinanceWithdrawalApplication, quoteFinanceCoinSellerRechargeOrder, + upsertFinanceUSDTAddress, verifyFinanceCoinSellerRechargeReceipt, verifyFinanceCoinSellerRechargeOrder, } from "./api.js"; @@ -517,6 +518,18 @@ export function FinanceApp() { const loadUSDTAddresses = useCallback((appCode) => getFinanceUSDTAddresses(appCode), []); + const saveUSDTAddress = useCallback( + async (appCode, chain, payload) => { + if (!session?.canUpdateUSDTAddress) { + throw new Error("无修改 USDT 收款地址权限"); + } + const saved = await upsertFinanceUSDTAddress(appCode, chain, cleanPayload(payload)); + showToast(`${chain} 收款地址已保存`, "success"); + return saved; + }, + [session?.canUpdateUSDTAddress, showToast], + ); + const saveCoinSellerRechargeExchangeRate = useCallback( async (appCode, payload) => { const saved = await replaceFinanceCoinSellerRechargeExchangeRate(appCode, cleanPayload(payload)); @@ -723,6 +736,7 @@ export function FinanceApp() { canCreate={session.canCreateCoinSellerRechargeOrder} canConfigureExchangeRate={session.canConfigureCoinSellerRechargeExchangeRate} canGrant={session.canGrantCoinSellerRechargeOrder} + canUpdateUSDTAddress={session.canUpdateUSDTAddress} canVerify={session.canVerifyCoinSellerRechargeOrder} error={coinSellerRechargeOrdersState.error} filters={coinSellerRechargeOrderFilters} @@ -736,6 +750,7 @@ export function FinanceApp() { onReload={loadCoinSellerRechargeOrders} onQuote={quoteCoinSellerRechargeOrder} onSaveExchangeRate={saveCoinSellerRechargeExchangeRate} + onSaveUSDTAddress={saveUSDTAddress} onVerify={verifyCoinSellerRechargeOrder} onVerifyReceipt={verifyCoinSellerRechargeReceipt} /> diff --git a/finance/src/FinanceApp.test.jsx b/finance/src/FinanceApp.test.jsx index 1f3a503..0742e57 100644 --- a/finance/src/FinanceApp.test.jsx +++ b/finance/src/FinanceApp.test.jsx @@ -34,6 +34,7 @@ vi.mock("./api.js", () => ({ rejectFinanceWithdrawalApplication: vi.fn(), refreshFinanceGoogleRechargePaid: vi.fn(), replaceFinanceCoinSellerRechargeExchangeRate: vi.fn(), + upsertFinanceUSDTAddress: vi.fn(), quoteFinanceCoinSellerRechargeOrder: vi.fn(), verifyFinanceCoinSellerRechargeOrder: vi.fn(), verifyFinanceCoinSellerRechargeReceipt: vi.fn() @@ -45,6 +46,7 @@ const defaultSession = { canConfigureCoinSellerRechargeExchangeRate: false, canCreateCoinSellerRechargeOrder: false, canGrantCoinSellerRechargeOrder: false, + canUpdateUSDTAddress: false, canVerifyCoinSellerRechargeOrder: false, canViewAppRechargeDetails: false, canViewCoinSellerRechargeOrders: false, diff --git a/finance/src/api.js b/finance/src/api.js index f5ac3a9..57c8aba 100644 --- a/finance/src/api.js +++ b/finance/src/api.js @@ -16,6 +16,14 @@ import { const appCodeHeader = "X-App-Code"; +function normalizeUSDTAddress(item = {}) { + return { + address: stringValue(item?.address), + chain: stringValue(item?.chain).toUpperCase(), + updatedAtMs: Number(item?.updatedAtMs || 0) + }; +} + export async function fetchFinanceSession() { const session = await currentSession(); const permissions = Array.isArray(session.permissions) ? session.permissions : []; @@ -26,6 +34,7 @@ export async function fetchFinanceSession() { const canCreateCoinSellerRechargeOrder = permissions.includes(FINANCE_PERMISSIONS.createCoinSellerRechargeOrder); const canVerifyCoinSellerRechargeOrder = permissions.includes(FINANCE_PERMISSIONS.verifyCoinSellerRechargeOrder); const canGrantCoinSellerRechargeOrder = permissions.includes(FINANCE_PERMISSIONS.grantCoinSellerRechargeOrder); + const canUpdateUSDTAddress = permissions.includes(FINANCE_PERMISSIONS.updateUSDTAddress); const canConfigureCoinSellerRechargeExchangeRate = permissions.includes(FINANCE_PERMISSIONS.configureCoinSellerRechargeExchangeRate); return { @@ -38,6 +47,7 @@ export async function fetchFinanceSession() { canConfigureCoinSellerRechargeExchangeRate, canCreateCoinSellerRechargeOrder, canGrantCoinSellerRechargeOrder, + canUpdateUSDTAddress, canVerifyCoinSellerRechargeOrder, canViewAppRechargeDetails, canViewCoinSellerRechargeOrders, @@ -113,15 +123,26 @@ export async function getFinanceUSDTAddresses(appCode) { 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) - })) + .map(normalizeUSDTAddress) .filter((item) => item.address && item.chain) }; } +export async function upsertFinanceUSDTAddress(appCode, chain, payload) { + const endpoint = API_ENDPOINTS.upsertFinanceUSDTAddress; + const data = await apiRequest(apiEndpointPath(API_OPERATIONS.upsertFinanceUSDTAddress, { + app_code: appCode, + chain + }), { + body: { address: stringValue(payload?.address) }, + method: endpoint.method + }); + return { + ...normalizeUSDTAddress(data), + appCode: stringValue(data?.appCode ?? appCode) + }; +} + 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 0d6b616..e0cf3af 100644 --- a/finance/src/api.test.js +++ b/finance/src/api.test.js @@ -14,6 +14,7 @@ import { rejectFinanceWithdrawalApplication, quoteFinanceCoinSellerRechargeOrder, replaceFinanceCoinSellerRechargeExchangeRate, + upsertFinanceUSDTAddress, verifyFinanceCoinSellerRechargeReceipt, verifyFinanceCoinSellerRechargeOrder } from "./api.js"; @@ -202,6 +203,36 @@ test("finance USDT address API uses the selected app path and normalizes SQL row }); }); +test("finance USDT address update uses the selected app and chain path", async () => { + vi.stubGlobal( + "fetch", + vi.fn(async () => + new Response(JSON.stringify({ + code: 0, + data: { + address: "TLv7wERsM42ZeobMrrKVC5C72d3gFUa44J", + appCode: "aslan", + chain: "TRON", + updatedAtMs: 456 + } + })) + ) + ); + + const result = await upsertFinanceUSDTAddress("aslan", "TRON", { address: "TLv7wERsM42ZeobMrrKVC5C72d3gFUa44J" }); + const [url, init] = vi.mocked(fetch).mock.calls[0]; + + expect(String(url)).toContain("/api/v1/admin/finance/usdt-addresses/aslan/TRON"); + expect(init?.method).toBe("PUT"); + expect(JSON.parse(String(init?.body))).toEqual({ address: "TLv7wERsM42ZeobMrrKVC5C72d3gFUa44J" }); + expect(result).toEqual({ + address: "TLv7wERsM42ZeobMrrKVC5C72d3gFUa44J", + appCode: "aslan", + chain: "TRON", + updatedAtMs: 456 + }); +}); + 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 3de2bb1..7103805 100644 --- a/finance/src/components/FinanceCoinSellerRechargeOrderList.jsx +++ b/finance/src/components/FinanceCoinSellerRechargeOrderList.jsx @@ -42,6 +42,7 @@ export function FinanceCoinSellerRechargeOrderList({ canCreate, canConfigureExchangeRate, canGrant, + canUpdateUSDTAddress, canVerify, error, filters, @@ -54,6 +55,7 @@ export function FinanceCoinSellerRechargeOrderList({ onReload, onQuote, onSaveExchangeRate, + onSaveUSDTAddress, onVerify, onVerifyReceipt, orders, @@ -508,10 +510,12 @@ export function FinanceCoinSellerRechargeOrderList({ /> setUSDTAddressOpen(false)} onLoad={onLoadUSDTAddresses} + onSave={onSaveUSDTAddress} /> { + render(); + + fireEvent.click(screen.getByRole("button", { name: "创建币商充值" })); + fireEvent.mouseDown(screen.getByRole("combobox", { name: "支付渠道" })); + fireEvent.click(screen.getByRole("option", { name: "USDT" })); + fireEvent.mouseDown(screen.getByRole("combobox", { name: "USDT 链" })); + + expect(screen.getByRole("option", { name: "Binance 站内转账 / Off-chain" })).toBeInTheDocument(); + expect(screen.getByRole("option", { name: "Binance Pay / C2C" })).toBeInTheDocument(); +}); + test("coin seller recharge order actions follow verify and grant status", () => { const onVerify = vi.fn(); const onGrant = vi.fn(); @@ -260,6 +272,7 @@ function baseProps(overrides = {}) { canCreate: true, canConfigureExchangeRate: false, canGrant: true, + canUpdateUSDTAddress: false, canVerify: true, error: "", filters: { @@ -280,6 +293,7 @@ function baseProps(overrides = {}) { onReload: vi.fn(), onQuote: vi.fn(async () => ({ coinAmount: 800000, coinsPerUsd: 80000, rateSource: "tier" })), onSaveExchangeRate: vi.fn(), + onSaveUSDTAddress: vi.fn(), onVerify: vi.fn(), onVerifyReceipt: vi.fn(), orders: { diff --git a/finance/src/components/FinanceUSDTAddressDialog.jsx b/finance/src/components/FinanceUSDTAddressDialog.jsx index b926e8c..6f93393 100644 --- a/finance/src/components/FinanceUSDTAddressDialog.jsx +++ b/finance/src/components/FinanceUSDTAddressDialog.jsx @@ -1,4 +1,6 @@ +import AddOutlined from "@mui/icons-material/AddOutlined"; import ContentCopyOutlined from "@mui/icons-material/ContentCopyOutlined"; +import EditOutlined from "@mui/icons-material/EditOutlined"; import Alert from "@mui/material/Alert"; import Button from "@mui/material/Button"; import Dialog from "@mui/material/Dialog"; @@ -8,18 +10,22 @@ import DialogTitle from "@mui/material/DialogTitle"; import LinearProgress from "@mui/material/LinearProgress"; import MenuItem from "@mui/material/MenuItem"; import TextField from "@mui/material/TextField"; -import { useEffect, useState } from "react"; +import { useEffect, useMemo, useState } from "react"; import { AppIdentity } from "@/shared/ui/AppIdentity.jsx"; import { formatTime } from "../format.js"; const EMPTY_DATA = { appCode: "", items: [] }; +const ADDRESS_CHAINS = ["TRON", "BSC"]; -export function FinanceUSDTAddressDialog({ apps, initialAppCode, open, onClose, onLoad }) { +export function FinanceUSDTAddressDialog({ apps, canUpdate = false, initialAppCode, open, onClose, onLoad, onSave }) { const [appCode, setAppCode] = useState(""); const [data, setData] = useState(EMPTY_DATA); const [error, setError] = useState(""); const [loading, setLoading] = useState(false); const [copiedChain, setCopiedChain] = useState(""); + const [editingChain, setEditingChain] = useState(""); + const [addressDraft, setAddressDraft] = useState(""); + const [saving, setSaving] = useState(false); useEffect(() => { if (!open) { @@ -27,6 +33,8 @@ export function FinanceUSDTAddressDialog({ apps, initialAppCode, open, onClose, } setAppCode(initialAppCode || apps[0]?.appCode || ""); setCopiedChain(""); + setEditingChain(""); + setAddressDraft(""); setError(""); }, [apps, initialAppCode, open]); @@ -40,6 +48,8 @@ export function FinanceUSDTAddressDialog({ apps, initialAppCode, open, onClose, setLoading(true); setError(""); setCopiedChain(""); + setEditingChain(""); + setAddressDraft(""); onLoad(appCode) .then((result) => { if (active) { @@ -62,6 +72,13 @@ export function FinanceUSDTAddressDialog({ apps, initialAppCode, open, onClose, }; }, [appCode, onLoad, open]); + const itemsByChain = useMemo( + () => new Map((data.items || []).map((item) => [item.chain, item])), + [data.items], + ); + // 有写权限时始终展示 TRON/BSC 两个可维护槽位,未配置项直接提供“添加”;只读账号仍只看到真实配置。 + const visibleChains = canUpdate ? ADDRESS_CHAINS : ADDRESS_CHAINS.filter((chain) => itemsByChain.has(chain)); + const copyAddress = async (item) => { try { if (!navigator.clipboard?.writeText) { @@ -77,13 +94,53 @@ export function FinanceUSDTAddressDialog({ apps, initialAppCode, open, onClose, } }; + const beginEdit = (chain) => { + setEditingChain(chain); + setAddressDraft(itemsByChain.get(chain)?.address || ""); + setCopiedChain(""); + setError(""); + }; + + const saveAddress = async () => { + const address = addressDraft.trim(); + if (!validUSDTAddress(editingChain, address)) { + setError(`${chainLabel(editingChain)} 收款地址格式不正确`); + return; + } + if (!onSave) { + setError("收款地址保存接口未配置"); + return; + } + setSaving(true); + setError(""); + try { + const saved = await onSave(appCode, editingChain, { address }); + const savedItem = { + address: saved?.address || address, + chain: saved?.chain || editingChain, + updatedAtMs: Number(saved?.updatedAtMs || Date.now()), + }; + // 保存成功后直接用服务端回包更新当前行;下一次校验也会由 admin-server 重新读同一 SQL 行。 + setData((current) => ({ + ...current, + items: [...current.items.filter((item) => item.chain !== savedItem.chain), savedItem], + })); + setEditingChain(""); + setAddressDraft(""); + } catch (saveError) { + setError(saveError.message || "保存 USDT 收款地址失败"); + } finally { + setSaving(false); + } + }; + return ( - + USDT 收款地址 {error ? {error} : null} {loading ? : null} - {!loading && data.items.length ? ( + {!loading && visibleChains.length ? (
- {data.items.map((item) => ( -
-
- {chainLabel(item.chain)} - {item.updatedAtMs ? `更新于 ${formatTime(item.updatedAtMs)}` : ""} -
-
- {item.address} - -
-
- ))} + {visibleChains.map((chain) => { + const item = itemsByChain.get(chain); + const editing = editingChain === chain; + return ( +
+
+ {chainLabel(chain)} + {item?.updatedAtMs ? `更新于 ${formatTime(item.updatedAtMs)}` : "未配置"} +
+ {editing ? ( +
+ setAddressDraft(event.target.value)} + /> +
+ + +
+
+ ) : ( +
+ {item ? {item.address} : 尚未添加} +
+ {item ? ( + + ) : null} + {canUpdate ? ( + + ) : null} +
+
+ )} +
+ ); + })}
) : null} - {!loading && appCode && !data.items.length && !error ? ( + {!loading && appCode && !visibleChains.length && !error ? (
当前 APP 未配置 USDT 收款地址
) : null}
- @@ -142,3 +245,13 @@ function chainLabel(chain) { } return chain; } + +function validUSDTAddress(chain, address) { + if (chain === "TRON") { + return /^T[1-9A-HJ-NP-Za-km-z]{33}$/.test(address); + } + if (chain === "BSC") { + return /^0x[0-9a-fA-F]{40}$/.test(address); + } + return false; +} diff --git a/finance/src/components/FinanceUSDTAddressDialog.test.jsx b/finance/src/components/FinanceUSDTAddressDialog.test.jsx index b70a68d..bf92dd6 100644 --- a/finance/src/components/FinanceUSDTAddressDialog.test.jsx +++ b/finance/src/components/FinanceUSDTAddressDialog.test.jsx @@ -41,3 +41,52 @@ test("loads the selected app USDT address and copies the exact SQL value", async await waitFor(() => expect(writeText).toHaveBeenCalledWith("TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t")); expect(screen.getByRole("button", { name: "复制 TRON 地址" })).toHaveTextContent("已复制"); }); + +test("modifies an existing TRON address and keeps the saved SQL value in the row", async () => { + const originalAddress = "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t"; + const updatedAddress = "TLv7wERsM42ZeobMrrKVC5C72d3gFUa44J"; + const onSave = vi.fn(async (appCode, chain, payload) => ({ + address: payload.address, + appCode, + chain, + updatedAtMs: 200, + })); + + render( + ({ appCode: "aslan", items: [{ address: originalAddress, chain: "TRON" }] }))} + onSave={onSave} + />, + ); + + await screen.findByText(originalAddress); + fireEvent.click(screen.getByRole("button", { name: "修改 TRON 地址" })); + fireEvent.change(screen.getByRole("textbox", { name: "TRON · TRC20 收款地址" }), { target: { value: updatedAddress } }); + fireEvent.click(screen.getByRole("button", { name: "保存 TRON 地址" })); + + await waitFor(() => expect(onSave).toHaveBeenCalledWith("aslan", "TRON", { address: updatedAddress })); + expect(await screen.findByText(updatedAddress)).toBeInTheDocument(); + expect(screen.getByRole("button", { name: "修改 TRON 地址" })).toBeInTheDocument(); +}); + +test("shows an add action when the selected app has no TRON address", async () => { + render( + ({ appCode: "aslan", items: [] }))} + onSave={vi.fn()} + />, + ); + + expect(await screen.findByRole("button", { name: "添加 TRON 地址" })).toBeInTheDocument(); + expect(screen.getByRole("button", { name: "添加 BSC 地址" })).toBeInTheDocument(); +}); diff --git a/finance/src/constants.js b/finance/src/constants.js index 54905a1..440d65b 100644 --- a/finance/src/constants.js +++ b/finance/src/constants.js @@ -3,6 +3,7 @@ export const FINANCE_PERMISSIONS = { configureCoinSellerRechargeExchangeRate: "coin-seller:exchange-rate", createCoinSellerRechargeOrder: "finance-order:coin-seller-recharge:create", grantCoinSellerRechargeOrder: "finance-order:coin-seller-recharge:grant", + updateUSDTAddress: "finance-order:usdt-address:update", verifyCoinSellerRechargeOrder: "finance-order:coin-seller-recharge:verify", viewCoinSellerRechargeOrders: "finance-order:coin-seller-recharge:view", viewAppRechargeDetails: "payment-bill:view", @@ -22,7 +23,8 @@ export const COIN_SELLER_RECHARGE_PROVIDERS = [ ]; export const COIN_SELLER_RECHARGE_CHAINS = [ - ["C2C", "C2C"], + ["C2C", "Binance Pay / C2C"], + ["OFFCHAIN", "Binance 站内转账 / Off-chain"], ["TRON", "TRON"], ["BSC", "BSC"], ]; diff --git a/finance/src/styles/index.css b/finance/src/styles/index.css index 8e6c042..8536b0f 100644 --- a/finance/src/styles/index.css +++ b/finance/src/styles/index.css @@ -1897,6 +1897,24 @@ a { flex: 0 0 auto; } +.finance-usdt-address-item__editor { + display: grid; + gap: var(--space-2); +} + +.finance-usdt-address-item__actions { + display: flex; + flex: 0 0 auto; + align-items: center; + justify-content: flex-end; + gap: var(--space-2); +} + +.finance-usdt-address-item__missing { + color: var(--text-tertiary); + font-size: 13px; +} + .finance-usdt-address-empty { color: var(--text-secondary); padding: var(--space-5) 0; diff --git a/src/shared/api/generated/endpoints.ts b/src/shared/api/generated/endpoints.ts index a75c051..bf7e8e2 100644 --- a/src/shared/api/generated/endpoints.ts +++ b/src/shared/api/generated/endpoints.ts @@ -390,6 +390,7 @@ export const API_OPERATIONS = { updateWeeklyStarCycle: "updateWeeklyStarCycle", uploadFile: "uploadFile", uploadImage: "uploadImage", + upsertFinanceUSDTAddress: "upsertFinanceUSDTAddress", upsertLuckyGiftConfig: "upsertLuckyGiftConfig", upsertResourceShopItems: "upsertResourceShopItems", upsertRule: "upsertRule", @@ -3023,6 +3024,13 @@ export const API_ENDPOINTS: Record = { permission: "upload:create", permissions: ["upload:create"] }, + upsertFinanceUSDTAddress: { + method: "PUT", + operationId: API_OPERATIONS.upsertFinanceUSDTAddress, + path: "/v1/admin/finance/usdt-addresses/{app_code}/{chain}", + permission: "finance-order:usdt-address:update", + permissions: ["finance-order:usdt-address:update"] + }, upsertLuckyGiftConfig: { method: "PUT", operationId: API_OPERATIONS.upsertLuckyGiftConfig, diff --git a/src/shared/api/generated/schema.d.ts b/src/shared/api/generated/schema.d.ts index cff1343..0dadb3f 100644 --- a/src/shared/api/generated/schema.d.ts +++ b/src/shared/api/generated/schema.d.ts @@ -2604,6 +2604,22 @@ export interface paths { patch?: never; trace?: never; }; + "/admin/finance/usdt-addresses/{app_code}/{chain}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put: operations["upsertFinanceUSDTAddress"]; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/admin/finance/coin-seller-recharge-exchange-rates/{app_code}": { parameters: { query?: never; @@ -5443,6 +5459,9 @@ export interface components { ApiResponseFinanceUSDTAddresses: components["schemas"]["Envelope"] & { data?: components["schemas"]["FinanceUSDTAddresses"]; }; + ApiResponseFinanceUSDTAddressUpsert: components["schemas"]["Envelope"] & { + data?: components["schemas"]["FinanceUSDTAddressUpsert"]; + }; ApiResponseFinanceCoinSellerRechargeReceiptVerification: components["schemas"]["Envelope"] & { data?: components["schemas"]["FinanceCoinSellerRechargeReceiptVerification"]; }; @@ -6113,7 +6132,7 @@ export interface components { /** @enum {string} */ providerCode: "mifapay" | "v5pay" | "usdt"; /** @enum {string} */ - chain?: "TRON" | "BSC"; + chain?: "C2C" | "OFFCHAIN" | "TRON" | "BSC"; externalOrderNo: string; providerCountryCode?: string; providerCurrencyCode?: string; @@ -6183,7 +6202,7 @@ export interface components { providerCurrencyCode?: string; providerAmountMinor?: number; /** @enum {string} */ - chain?: "TRON" | "BSC"; + chain?: "C2C" | "OFFCHAIN" | "TRON" | "BSC"; remark?: string; }; FinanceCoinSellerRechargeQuoteInput: { @@ -6230,6 +6249,17 @@ export interface components { appCode: string; items: components["schemas"]["FinanceUSDTAddress"][]; }; + FinanceUSDTAddressInput: { + address: string; + }; + FinanceUSDTAddressUpsert: { + appCode: string; + /** @enum {string} */ + chain: "TRON" | "BSC"; + address: string; + /** Format: int64 */ + updatedAtMs: number; + }; FinanceCoinSellerRechargeExchangeRateInput: { tiers: { minUsdAmount: number; @@ -6250,7 +6280,7 @@ export interface components { currencyCode?: string; providerAmountMinor?: number; /** @enum {string} */ - chain?: "TRON" | "BSC"; + chain?: "C2C" | "OFFCHAIN" | "TRON" | "BSC"; receiveAddress?: string; verifiedAtMs?: number; failureReason?: string; @@ -6262,7 +6292,7 @@ export interface components { providerCode: "mifapay" | "v5pay" | "usdt"; externalOrderNo: string; /** @enum {string} */ - chain?: "TRON" | "BSC"; + chain?: "C2C" | "OFFCHAIN" | "TRON" | "BSC"; usdAmount?: number; usdMinorAmount?: number; /** @@ -7095,6 +7125,15 @@ export interface components { }; }; /** @description OK */ + FinanceUSDTAddressResponse: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ApiResponseFinanceUSDTAddressUpsert"]; + }; + }; + /** @description OK */ FinanceCoinSellerRechargeReceiptVerificationResponse: { headers: { [name: string]: unknown; @@ -7514,6 +7553,11 @@ export interface components { "application/json": components["schemas"]["FinanceCoinSellerRechargeReceiptVerificationInput"]; }; }; + FinanceUSDTAddressRequest: { + content: { + "application/json": components["schemas"]["FinanceUSDTAddressInput"]; + }; + }; FinanceWithdrawalAuditRequest: { content: { "application/json": { @@ -10833,6 +10877,21 @@ export interface operations { 200: components["responses"]["FinanceUSDTAddressesResponse"]; }; }; + upsertFinanceUSDTAddress: { + parameters: { + query?: never; + header?: never; + path: { + app_code: string; + chain: "TRON" | "BSC"; + }; + cookie?: never; + }; + requestBody?: components["requestBodies"]["FinanceUSDTAddressRequest"]; + responses: { + 200: components["responses"]["FinanceUSDTAddressResponse"]; + }; + }; getFinanceCoinSellerRechargeExchangeRate: { parameters: { query?: never;