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 710c3785..ad12dff7 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 @@ -52,15 +52,21 @@ export class UsersController { @ApiParam({ name: 'accountSequence', type: String }) @ApiQuery({ name: 'page', required: false, type: Number }) @ApiQuery({ name: 'pageSize', required: false, type: Number }) + @ApiQuery({ name: 'sortBy', required: false, type: String, description: '排序字段: sourceType, levelDepth, amount, createdAt' }) + @ApiQuery({ name: 'sortOrder', required: false, type: String, description: '排序方向: asc, desc' }) async getUserContributions( @Param('accountSequence') accountSequence: string, @Query('page') page?: number, @Query('pageSize') pageSize?: number, + @Query('sortBy') sortBy?: string, + @Query('sortOrder') sortOrder?: string, ) { return this.usersService.getUserContributions( accountSequence, page ?? 1, pageSize ?? 20, + sortBy, + sortOrder as 'asc' | 'desc' | undefined, ); } 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 e1f36598..532464ca 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 @@ -322,6 +322,8 @@ export class UsersService { accountSequence: string, page: number, pageSize: number, + sortBy?: string, + sortOrder?: 'asc' | 'desc', ) { const user = await this.prisma.syncedUser.findUnique({ where: { accountSequence }, @@ -347,11 +349,32 @@ export class UsersService { unlockedBonusTiers: 0, }; + // 构建排序条件 + const allowedSortFields = ['sourceType', 'levelDepth', 'amount', 'createdAt']; + const order = sortOrder === 'asc' || sortOrder === 'desc' ? sortOrder : 'desc'; + let orderBy: any; + + if (sortBy && allowedSortFields.includes(sortBy)) { + // 按指定字段排序,levelDepth 需要特殊处理(null 值排到末尾) + if (sortBy === 'levelDepth') { + // 先按 sourceType 分组,再按 levelDepth/bonusTier 排序 + orderBy = [ + { sourceType: order }, + { levelDepth: { sort: order, nulls: 'last' } }, + { bonusTier: { sort: order, nulls: 'last' } }, + ]; + } else { + orderBy = { [sortBy]: order }; + } + } else { + orderBy = { createdAt: 'desc' }; + } + // 获取算力明细记录 const [records, total] = await Promise.all([ this.prisma.syncedContributionRecord.findMany({ where: { accountSequence }, - orderBy: { createdAt: 'desc' }, + orderBy, skip: (page - 1) * pageSize, take: pageSize, }), diff --git a/backend/services/trading-service/src/api/controllers/p2p-transfer.controller.ts b/backend/services/trading-service/src/api/controllers/p2p-transfer.controller.ts index c8d55cb1..061f8b48 100644 --- a/backend/services/trading-service/src/api/controllers/p2p-transfer.controller.ts +++ b/backend/services/trading-service/src/api/controllers/p2p-transfer.controller.ts @@ -2,6 +2,7 @@ import { Controller, Get, Post, Body, Query, Param, Req, Headers, BadRequestExce import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth, ApiQuery, ApiParam } from '@nestjs/swagger'; import { IsString, IsOptional, Length, Matches } from 'class-validator'; import { P2pTransferService } from '../../application/services/p2p-transfer.service'; +import { Public } from '../../shared/guards/jwt-auth.guard'; class P2pTransferDto { @IsString() @@ -81,4 +82,22 @@ export class P2pTransferController { // TransformInterceptor 会自动包装成 { success: true, data: ... } return result.data; } + + @Get('internal/all-transfers') + @Public() + @ApiOperation({ summary: '获取全部P2P转账记录(内部调用,管理后台用)' }) + @ApiQuery({ name: 'page', required: false, type: Number }) + @ApiQuery({ name: 'pageSize', required: false, type: Number }) + @ApiQuery({ name: 'search', required: false, type: String }) + async getAllTransfers( + @Query('page') page?: number, + @Query('pageSize') pageSize?: number, + @Query('search') search?: string, + ) { + return this.p2pTransferService.getAllTransfers( + page ?? 1, + pageSize ?? 20, + search, + ); + } } diff --git a/backend/services/trading-service/src/application/services/p2p-transfer.service.ts b/backend/services/trading-service/src/application/services/p2p-transfer.service.ts index f890bf58..b46e6a58 100644 --- a/backend/services/trading-service/src/application/services/p2p-transfer.service.ts +++ b/backend/services/trading-service/src/application/services/p2p-transfer.service.ts @@ -29,8 +29,11 @@ export interface P2pTransferResult { export interface P2pTransferHistoryItem { transferNo: string; fromAccountSequence: string; + fromPhone?: string | null; + fromNickname?: string | null; toAccountSequence: string; toPhone: string; + toNickname?: string | null; amount: string; fee?: string; memo?: string | null; @@ -410,6 +413,73 @@ export class P2pTransferService { return { data, total }; } + /** + * 获取全部P2P转账记录(管理后台用,不限用户) + */ + async getAllTransfers( + page: number = 1, + pageSize: number = 20, + search?: string, + ): Promise<{ + data: P2pTransferHistoryItem[]; + total: number; + summary: { totalFee: string; totalAmount: string; totalCount: number }; + }> { + const validPage = Math.max(1, Number(page) || 1); + const validPageSize = Math.max(1, Math.min(100, Number(pageSize) || 20)); + + const where: any = { status: 'COMPLETED' }; + if (search) { + where.OR = [ + { fromAccountSequence: { contains: search } }, + { toAccountSequence: { contains: search } }, + { toPhone: { contains: search } }, + { fromPhone: { contains: search } }, + { transferNo: { contains: search } }, + ]; + } + + const [records, total, agg] = await Promise.all([ + this.prisma.p2pTransfer.findMany({ + where, + orderBy: { createdAt: 'desc' }, + skip: (validPage - 1) * validPageSize, + take: validPageSize, + }), + this.prisma.p2pTransfer.count({ where }), + this.prisma.p2pTransfer.aggregate({ + where: { status: 'COMPLETED' }, + _sum: { fee: true, amount: true }, + _count: true, + }), + ]); + + const data: P2pTransferHistoryItem[] = records.map((record) => ({ + transferNo: record.transferNo, + fromAccountSequence: record.fromAccountSequence, + fromPhone: record.fromPhone, + fromNickname: record.fromNickname, + toAccountSequence: record.toAccountSequence, + toPhone: record.toPhone, + toNickname: record.toNickname, + amount: record.amount.toString(), + fee: record.fee.toString(), + memo: record.memo, + status: record.status, + createdAt: record.createdAt, + })); + + return { + data, + total, + summary: { + totalFee: agg._sum.fee?.toString() || '0', + totalAmount: agg._sum.amount?.toString() || '0', + totalCount: agg._count, + }, + }; + } + private generateTransferNo(): string { const timestamp = Date.now().toString(36); const random = Math.random().toString(36).substring(2, 8); 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 a7d49bc5..a25c7ccc 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 @@ -101,7 +101,7 @@ export const usersApi = { getContributionRecords: async ( accountSequence: string, - params: PaginationParams + params: PaginationParams & { sortBy?: string; sortOrder?: 'asc' | 'desc' } ): Promise> => { const response = await apiClient.get(`/users/${accountSequence}/contributions`, { params }); const result = response.data.data; diff --git a/frontend/mining-admin-web/src/features/users/components/contribution-records-list.tsx b/frontend/mining-admin-web/src/features/users/components/contribution-records-list.tsx index 84d32cb6..c4b33050 100644 --- a/frontend/mining-admin-web/src/features/users/components/contribution-records-list.tsx +++ b/frontend/mining-admin-web/src/features/users/components/contribution-records-list.tsx @@ -9,7 +9,10 @@ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@ import { Button } from '@/components/ui/button'; import { Skeleton } from '@/components/ui/skeleton'; import { Badge } from '@/components/ui/badge'; -import { ChevronLeft, ChevronRight } from 'lucide-react'; +import { ChevronLeft, ChevronRight, ArrowUpDown, ArrowUp, ArrowDown } from 'lucide-react'; + +type SortField = 'sourceType' | 'levelDepth' | 'amount' | 'createdAt'; +type SortOrder = 'asc' | 'desc'; const sourceTypeLabels: Record = { PERSONAL: '个人认种', @@ -23,6 +26,43 @@ const sourceTypeBadgeVariant: Record void; + className?: string; +}) { + const isActive = currentSort === field; + return ( + + + + ); +} + interface ContributionRecordsListProps { accountSequence: string; } @@ -30,8 +70,31 @@ interface ContributionRecordsListProps { export function ContributionRecordsList({ accountSequence }: ContributionRecordsListProps) { const [page, setPage] = useState(1); const pageSize = 10; + const [sortBy, setSortBy] = useState(undefined); + const [sortOrder, setSortOrder] = useState('asc'); - const { data, isLoading } = useContributionRecords(accountSequence, { page, pageSize }); + const handleSort = (field: SortField) => { + if (sortBy === field) { + // 同一字段:切换方向 → 第三次点击取消排序 + if (sortOrder === 'asc') { + setSortOrder('desc'); + } else { + setSortBy(undefined); + setSortOrder('asc'); + } + } else { + setSortBy(field); + setSortOrder('asc'); + } + setPage(1); + }; + + const { data, isLoading } = useContributionRecords(accountSequence, { + page, + pageSize, + sortBy, + sortOrder: sortBy ? sortOrder : undefined, + }); return ( @@ -39,14 +102,14 @@ export function ContributionRecordsList({ accountSequence }: ContributionRecords - 来源 + 来源用户 棵数 基础算力 分配比例 - 获得算力 - 层级/等级 - 生效日期 + + + 状态 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 aafab406..586e1cd1 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 @@ -17,7 +17,7 @@ export function useUserDetail(accountSequence: string) { }); } -export function useContributionRecords(accountSequence: string, params: PaginationParams) { +export function useContributionRecords(accountSequence: string, params: PaginationParams & { sortBy?: string; sortOrder?: 'asc' | 'desc' }) { return useQuery({ queryKey: ['users', accountSequence, 'contributions', params], queryFn: () => usersApi.getContributionRecords(accountSequence, params),