From 9adef67bb832a4a8dc717c91f77e5f23f2b2357e Mon Sep 17 00:00:00 2001 From: hailin Date: Fri, 30 Jan 2026 10:41:55 -0800 Subject: [PATCH] =?UTF-8?q?feat(admin-web):=20P2P=E5=88=92=E8=BD=AC?= =?UTF-8?q?=E8=AE=B0=E5=BD=95=E7=AE=A1=E7=90=86=E9=A1=B5=E9=9D=A2=20+=20?= =?UTF-8?q?=E6=89=8B=E7=BB=AD=E8=B4=B9=E6=B1=87=E6=80=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 后端 — mining-admin-service - users.controller: 新增 GET /users/p2p-transfers 端点(搜索+分页) - 放在 :accountSequence 路由之前避免被 catch-all 拦截 - users.service: 新增 getP2pTransfers() 代理方法 - 调用 trading-service 的 /api/v2/p2p/internal/all-transfers - 返回划转记录列表 + 汇总统计 + 分页信息 ## 前端 — mining-admin-web - 新增 /p2p-transfers 页面: - 三个汇总卡片:累计手续费收入、累计划转金额、成功划转笔数 - 搜索框支持账号、手机号、转账单号搜索 - 记录表格:转账单号、发送方、接收方、转账金额、手续费、备注、时间 - 分页控件 - sidebar: 新增"P2P划转"导航项(位于"交易管理"下方) - users.api: 新增 getP2pTransfers API + P2pTransferRecord 类型 - use-users: 新增 useP2pTransfers hook Co-Authored-By: Claude Opus 4.5 --- .../src/api/controllers/users.controller.ts | 17 ++ .../src/application/services/users.service.ts | 36 ++++ .../app/(dashboard)/p2p-transfers/page.tsx | 186 ++++++++++++++++++ .../src/components/layout/sidebar.tsx | 2 + .../src/features/users/api/users.api.ts | 38 ++++ .../src/features/users/hooks/use-users.ts | 7 + 6 files changed, 286 insertions(+) create mode 100644 frontend/mining-admin-web/src/app/(dashboard)/p2p-transfers/page.tsx diff --git a/backend/services/mining-admin-service/src/api/controllers/users.controller.ts b/backend/services/mining-admin-service/src/api/controllers/users.controller.ts index ad12dff7..27c81e22 100644 --- a/backend/services/mining-admin-service/src/api/controllers/users.controller.ts +++ b/backend/services/mining-admin-service/src/api/controllers/users.controller.ts @@ -40,6 +40,23 @@ export class UsersController { }); } + @Get('p2p-transfers') + @ApiOperation({ summary: '获取全部P2P转账记录(含手续费汇总)' }) + @ApiQuery({ name: 'page', required: false, type: Number }) + @ApiQuery({ name: 'pageSize', required: false, type: Number }) + @ApiQuery({ name: 'search', required: false, type: String, description: '搜索账号/手机号/转账单号' }) + async getP2pTransfers( + @Query('page') page?: number, + @Query('pageSize') pageSize?: number, + @Query('search') search?: string, + ) { + return this.usersService.getP2pTransfers( + page ?? 1, + pageSize ?? 20, + search, + ); + } + @Get(':accountSequence') @ApiOperation({ summary: '获取用户详情' }) @ApiParam({ name: 'accountSequence', type: String }) diff --git a/backend/services/mining-admin-service/src/application/services/users.service.ts b/backend/services/mining-admin-service/src/application/services/users.service.ts index 532464ca..73d500f9 100644 --- a/backend/services/mining-admin-service/src/application/services/users.service.ts +++ b/backend/services/mining-admin-service/src/application/services/users.service.ts @@ -23,12 +23,14 @@ export interface GetOrdersQuery { export class UsersService { private readonly logger = new Logger(UsersService.name); private readonly miningServiceUrl: string; + private readonly tradingServiceUrl: string; constructor( private readonly prisma: PrismaService, private readonly configService: ConfigService, ) { this.miningServiceUrl = this.configService.get('MINING_SERVICE_URL', 'http://localhost:3021'); + this.tradingServiceUrl = this.configService.get('TRADING_SERVICE_URL', 'http://localhost:3022'); } /** @@ -1276,4 +1278,38 @@ export class UsersService { return result; } + + /** + * 获取全部P2P转账记录(代理 trading-service) + */ + async getP2pTransfers(page: number, pageSize: number, search?: string) { + try { + const params = new URLSearchParams({ + page: page.toString(), + pageSize: pageSize.toString(), + }); + if (search) params.set('search', search); + + const url = `${this.tradingServiceUrl}/api/v2/p2p/internal/all-transfers?${params}`; + const response = await fetch(url); + if (!response.ok) { + this.logger.warn(`Failed to fetch P2P transfers: ${response.status}`); + return { data: [], total: 0, summary: { totalFee: '0', totalAmount: '0', totalCount: 0 }, pagination: { page, pageSize, total: 0, totalPages: 0 } }; + } + const result = await response.json(); + const body = result.data || result; + return { + ...body, + pagination: { + page, + pageSize, + total: body.total, + totalPages: Math.ceil(body.total / pageSize), + }, + }; + } catch (error: any) { + this.logger.warn(`Failed to fetch P2P transfers: ${error.message}`); + return { data: [], total: 0, summary: { totalFee: '0', totalAmount: '0', totalCount: 0 }, pagination: { page, pageSize, total: 0, totalPages: 0 } }; + } + } } diff --git a/frontend/mining-admin-web/src/app/(dashboard)/p2p-transfers/page.tsx b/frontend/mining-admin-web/src/app/(dashboard)/p2p-transfers/page.tsx new file mode 100644 index 00000000..7630c0d2 --- /dev/null +++ b/frontend/mining-admin-web/src/app/(dashboard)/p2p-transfers/page.tsx @@ -0,0 +1,186 @@ +'use client'; + +import { useState } from 'react'; +import { PageHeader } from '@/components/layout/page-header'; +import { useP2pTransfers } from '@/features/users/hooks/use-users'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Skeleton } from '@/components/ui/skeleton'; +import { Badge } from '@/components/ui/badge'; +import { ChevronLeft, ChevronRight, Search, ArrowRightLeft, Coins, Hash } from 'lucide-react'; +import { formatDecimal } from '@/lib/utils/format'; +import { formatDateTime } from '@/lib/utils/date'; + +export default function P2pTransfersPage() { + const [page, setPage] = useState(1); + const [search, setSearch] = useState(''); + const [searchInput, setSearchInput] = useState(''); + const pageSize = 20; + + const { data, isLoading } = useP2pTransfers({ page, pageSize, search: search || undefined }); + + const handleSearch = () => { + setSearch(searchInput); + setPage(1); + }; + + return ( +
+ + + {/* 汇总统计 */} +
+ + + 累计手续费收入 + + + + {isLoading ? ( + + ) : ( +
+ {formatDecimal(data?.summary.totalFee || '0', 2)} +
+ )} +

积分值

+
+
+ + + 累计划转金额 + + + + {isLoading ? ( + + ) : ( +
+ {formatDecimal(data?.summary.totalAmount || '0', 2)} +
+ )} +

积分值

+
+
+ + + 成功划转笔数 + + + + {isLoading ? ( + + ) : ( +
+ {data?.summary.totalCount || 0} +
+ )} +

+
+
+
+ + {/* 搜索 */} + + +
+ setSearchInput(e.target.value)} + onKeyDown={(e) => e.key === 'Enter' && handleSearch()} + className="max-w-md" + /> + + {search && ( + + )} +
+
+
+ + {/* 记录列表 */} + + + + + + 转账单号 + 发送方 + 接收方 + 转账金额 + 手续费 + 备注 + 时间 + + + + {isLoading ? ( + [...Array(5)].map((_, i) => ( + + {[...Array(7)].map((_, j) => ( + + + + ))} + + )) + ) : !data?.items.length ? ( + + + 暂无划转记录 + + + ) : ( + data.items.map((record) => ( + + {record.transferNo} + +
+ {record.fromNickname || record.fromPhone || '-'} + {record.fromAccountSequence} +
+
+ +
+ {record.toNickname || record.toPhone} + {record.toAccountSequence} +
+
+ {formatDecimal(record.amount, 2)} + {formatDecimal(record.fee || '0', 2)} + {record.memo || '-'} + {formatDateTime(record.createdAt)} +
+ )) + )} +
+
+ + {data && data.totalPages > 1 && ( +
+

+ 共 {data.total} 条,第 {page} / {data.totalPages} 页 +

+
+ + +
+
+ )} +
+
+
+ ); +} diff --git a/frontend/mining-admin-web/src/components/layout/sidebar.tsx b/frontend/mining-admin-web/src/components/layout/sidebar.tsx index a2108b64..3126d5f9 100644 --- a/frontend/mining-admin-web/src/components/layout/sidebar.tsx +++ b/frontend/mining-admin-web/src/components/layout/sidebar.tsx @@ -17,6 +17,7 @@ import { Bot, HandCoins, FileSpreadsheet, + SendHorizontal, } from 'lucide-react'; import { Button } from '@/components/ui/button'; @@ -24,6 +25,7 @@ const menuItems = [ { name: '仪表盘', href: '/dashboard', icon: LayoutDashboard }, { name: '用户管理', href: '/users', icon: Users }, { name: '交易管理', href: '/trading', icon: ArrowLeftRight }, + { name: 'P2P划转', href: '/p2p-transfers', icon: SendHorizontal }, { name: '做市商管理', href: '/market-maker', icon: Bot }, { name: '手工补发', href: '/manual-mining', icon: HandCoins }, { name: '批量补发', href: '/batch-mining', icon: FileSpreadsheet }, diff --git a/frontend/mining-admin-web/src/features/users/api/users.api.ts b/frontend/mining-admin-web/src/features/users/api/users.api.ts index a25c7ccc..164ccbec 100644 --- a/frontend/mining-admin-web/src/features/users/api/users.api.ts +++ b/frontend/mining-admin-web/src/features/users/api/users.api.ts @@ -187,8 +187,46 @@ export const usersApi = { const response = await apiClient.get(`/users/${accountSequence}/batch-mining-records`, { params }); return response.data.data; }, + + getP2pTransfers: async ( + params: PaginationParams & { search?: string } + ): Promise<{ + items: P2pTransferRecord[]; + total: number; + totalPages: number; + page: number; + pageSize: number; + summary: { totalFee: string; totalAmount: string; totalCount: number }; + }> => { + const response = await apiClient.get('/users/p2p-transfers', { params }); + const result = response.data.data; + return { + items: result.data || [], + total: result.pagination?.total || 0, + page: result.pagination?.page || 1, + pageSize: result.pagination?.pageSize || 20, + totalPages: result.pagination?.totalPages || 0, + summary: result.summary || { totalFee: '0', totalAmount: '0', totalCount: 0 }, + }; + }, }; +// P2P转账记录类型 +export interface P2pTransferRecord { + transferNo: string; + fromAccountSequence: string; + fromPhone?: string | null; + fromNickname?: string | null; + toAccountSequence: string; + toPhone: string; + toNickname?: string | null; + amount: string; + fee?: string; + memo?: string | null; + status: string; + createdAt: string; +} + // 批量补发记录类型 export interface BatchMiningRecord { id: string; diff --git a/frontend/mining-admin-web/src/features/users/hooks/use-users.ts b/frontend/mining-admin-web/src/features/users/hooks/use-users.ts index 586e1cd1..3486b1df 100644 --- a/frontend/mining-admin-web/src/features/users/hooks/use-users.ts +++ b/frontend/mining-admin-web/src/features/users/hooks/use-users.ts @@ -77,3 +77,10 @@ export function useBatchMiningRecords(accountSequence: string, params: Paginatio enabled: !!accountSequence, }); } + +export function useP2pTransfers(params: PaginationParams & { search?: string }) { + return useQuery({ + queryKey: ['p2p-transfers', params], + queryFn: () => usersApi.getP2pTransfers(params), + }); +}