diff --git a/backend/services/mining-admin-service/src/api/api.module.ts b/backend/services/mining-admin-service/src/api/api.module.ts index 0236438a..cbb8b7d2 100644 --- a/backend/services/mining-admin-service/src/api/api.module.ts +++ b/backend/services/mining-admin-service/src/api/api.module.ts @@ -9,6 +9,7 @@ import { UsersController } from './controllers/users.controller'; import { SystemAccountsController } from './controllers/system-accounts.controller'; import { ReportsController } from './controllers/reports.controller'; import { ManualMiningController } from './controllers/manual-mining.controller'; +import { PendingContributionsController } from './controllers/pending-contributions.controller'; @Module({ imports: [ApplicationModule], @@ -22,6 +23,7 @@ import { ManualMiningController } from './controllers/manual-mining.controller'; SystemAccountsController, ReportsController, ManualMiningController, + PendingContributionsController, ], }) export class ApiModule {} diff --git a/backend/services/mining-admin-service/src/api/controllers/pending-contributions.controller.ts b/backend/services/mining-admin-service/src/api/controllers/pending-contributions.controller.ts new file mode 100644 index 00000000..cf5f941a --- /dev/null +++ b/backend/services/mining-admin-service/src/api/controllers/pending-contributions.controller.ts @@ -0,0 +1,77 @@ +import { Controller, Get, Param, Query } from '@nestjs/common'; +import { + ApiTags, + ApiOperation, + ApiBearerAuth, + ApiParam, + ApiQuery, +} from '@nestjs/swagger'; +import { PendingContributionsService } from '../../application/services/pending-contributions.service'; + +@ApiTags('Pending Contributions') +@ApiBearerAuth() +@Controller('pending-contributions') +export class PendingContributionsController { + constructor( + private readonly pendingContributionsService: PendingContributionsService, + ) {} + + @Get() + @ApiOperation({ summary: '获取待解锁算力列表' }) + @ApiQuery({ name: 'page', required: false, type: Number }) + @ApiQuery({ name: 'pageSize', required: false, type: Number }) + @ApiQuery({ + name: 'contributionType', + required: false, + type: String, + description: '算力类型筛选', + }) + async getPendingContributions( + @Query('page') page?: number, + @Query('pageSize') pageSize?: number, + @Query('contributionType') contributionType?: string, + ) { + return this.pendingContributionsService.getPendingContributions( + page ?? 1, + pageSize ?? 20, + contributionType, + ); + } + + @Get('summary') + @ApiOperation({ summary: '获取待解锁算力汇总统计' }) + async getPendingContributionsSummary() { + return this.pendingContributionsService.getPendingContributionsSummary(); + } + + @Get('mining-records') + @ApiOperation({ summary: '获取所有待解锁算力的挖矿记录' }) + @ApiQuery({ name: 'page', required: false, type: Number }) + @ApiQuery({ name: 'pageSize', required: false, type: Number }) + async getAllPendingMiningRecords( + @Query('page') page?: number, + @Query('pageSize') pageSize?: number, + ) { + return this.pendingContributionsService.getAllPendingMiningRecords( + page ?? 1, + pageSize ?? 20, + ); + } + + @Get(':id/records') + @ApiOperation({ summary: '获取某条待解锁算力的挖矿记录' }) + @ApiParam({ name: 'id', type: String, description: '待解锁算力ID' }) + @ApiQuery({ name: 'page', required: false, type: Number }) + @ApiQuery({ name: 'pageSize', required: false, type: Number }) + async getPendingContributionMiningRecords( + @Param('id') id: string, + @Query('page') page?: number, + @Query('pageSize') pageSize?: number, + ) { + return this.pendingContributionsService.getPendingContributionMiningRecords( + id, + page ?? 1, + pageSize ?? 20, + ); + } +} diff --git a/backend/services/mining-admin-service/src/application/application.module.ts b/backend/services/mining-admin-service/src/application/application.module.ts index 313dfc74..da8826c7 100644 --- a/backend/services/mining-admin-service/src/application/application.module.ts +++ b/backend/services/mining-admin-service/src/application/application.module.ts @@ -7,6 +7,7 @@ import { UsersService } from './services/users.service'; import { SystemAccountsService } from './services/system-accounts.service'; import { DailyReportService } from './services/daily-report.service'; import { ManualMiningService } from './services/manual-mining.service'; +import { PendingContributionsService } from './services/pending-contributions.service'; @Module({ imports: [InfrastructureModule], @@ -18,6 +19,7 @@ import { ManualMiningService } from './services/manual-mining.service'; SystemAccountsService, DailyReportService, ManualMiningService, + PendingContributionsService, ], exports: [ AuthService, @@ -27,6 +29,7 @@ import { ManualMiningService } from './services/manual-mining.service'; SystemAccountsService, DailyReportService, ManualMiningService, + PendingContributionsService, ], }) export class ApplicationModule implements OnModuleInit { diff --git a/backend/services/mining-admin-service/src/application/services/pending-contributions.service.ts b/backend/services/mining-admin-service/src/application/services/pending-contributions.service.ts new file mode 100644 index 00000000..7279a3d7 --- /dev/null +++ b/backend/services/mining-admin-service/src/application/services/pending-contributions.service.ts @@ -0,0 +1,138 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { HttpService } from '@nestjs/axios'; +import { ConfigService } from '@nestjs/config'; +import { firstValueFrom } from 'rxjs'; + +@Injectable() +export class PendingContributionsService { + private readonly logger = new Logger(PendingContributionsService.name); + + constructor( + private readonly httpService: HttpService, + private readonly configService: ConfigService, + ) {} + + private getMiningServiceUrl(): string { + return this.configService.get( + 'MINING_SERVICE_URL', + 'http://localhost:3021', + ); + } + + /** + * 获取待解锁算力列表 + */ + async getPendingContributions( + page: number = 1, + pageSize: number = 20, + contributionType?: string, + ) { + const miningServiceUrl = this.getMiningServiceUrl(); + + try { + const params: any = { page, pageSize }; + if (contributionType) { + params.contributionType = contributionType; + } + + const response = await firstValueFrom( + this.httpService.get(`${miningServiceUrl}/admin/pending-contributions`, { + params, + }), + ); + + return response.data; + } catch (error) { + this.logger.warn( + `Failed to fetch pending contributions: ${error.message}`, + ); + return { contributions: [], total: 0, page, pageSize }; + } + } + + /** + * 获取待解锁算力汇总统计 + */ + async getPendingContributionsSummary() { + const miningServiceUrl = this.getMiningServiceUrl(); + + try { + const response = await firstValueFrom( + this.httpService.get( + `${miningServiceUrl}/admin/pending-contributions/summary`, + ), + ); + + return response.data; + } catch (error) { + this.logger.warn( + `Failed to fetch pending contributions summary: ${error.message}`, + ); + return { + byType: [], + total: { totalAmount: '0', count: 0 }, + totalMinedToHeadquarters: '0', + }; + } + } + + /** + * 获取某条待解锁算力的挖矿记录 + */ + async getPendingContributionMiningRecords( + id: string, + page: number = 1, + pageSize: number = 20, + ) { + const miningServiceUrl = this.getMiningServiceUrl(); + + try { + const response = await firstValueFrom( + this.httpService.get( + `${miningServiceUrl}/admin/pending-contributions/${id}/records`, + { + params: { page, pageSize }, + }, + ), + ); + + return response.data; + } catch (error) { + this.logger.warn( + `Failed to fetch pending contribution mining records: ${error.message}`, + ); + return { + pendingContribution: null, + records: [], + total: 0, + page, + pageSize, + }; + } + } + + /** + * 获取所有待解锁算力的挖矿记录 + */ + async getAllPendingMiningRecords(page: number = 1, pageSize: number = 20) { + const miningServiceUrl = this.getMiningServiceUrl(); + + try { + const response = await firstValueFrom( + this.httpService.get( + `${miningServiceUrl}/admin/pending-contributions/mining-records`, + { + params: { page, pageSize }, + }, + ), + ); + + return response.data; + } catch (error) { + this.logger.warn( + `Failed to fetch all pending mining records: ${error.message}`, + ); + return { records: [], total: 0, page, pageSize }; + } + } +} diff --git a/backend/services/mining-service/src/api/controllers/admin.controller.ts b/backend/services/mining-service/src/api/controllers/admin.controller.ts index 959bc973..32aac751 100644 --- a/backend/services/mining-service/src/api/controllers/admin.controller.ts +++ b/backend/services/mining-service/src/api/controllers/admin.controller.ts @@ -424,4 +424,213 @@ export class AdminController { await this.manualMiningService.markWalletSynced(recordId); return { success: true, message: '已标记为同步完成' }; } + + // ==================== 待解锁算力(Pending Contributions)==================== + + @Get('pending-contributions') + @Public() + @ApiOperation({ summary: '获取待解锁算力列表' }) + @ApiQuery({ name: 'page', required: false, type: Number }) + @ApiQuery({ name: 'pageSize', required: false, type: Number }) + @ApiQuery({ name: 'contributionType', required: false, type: String, description: '算力类型筛选' }) + async getPendingContributions( + @Query('page') page?: number, + @Query('pageSize') pageSize?: number, + @Query('contributionType') contributionType?: string, + ) { + const pageNum = page ?? 1; + const pageSizeNum = pageSize ?? 20; + const skip = (pageNum - 1) * pageSizeNum; + + const where: any = { isExpired: false }; + if (contributionType) { + where.contributionType = contributionType; + } + + const [contributions, total] = await Promise.all([ + this.prisma.pendingContributionMining.findMany({ + where, + orderBy: { createdAt: 'desc' }, + skip, + take: pageSizeNum, + }), + this.prisma.pendingContributionMining.count({ where }), + ]); + + return { + contributions: contributions.map((c) => ({ + id: c.id.toString(), + sourceAdoptionId: c.sourceAdoptionId.toString(), + sourceAccountSequence: c.sourceAccountSequence, + wouldBeAccountSequence: c.wouldBeAccountSequence, + contributionType: c.contributionType, + amount: c.amount.toString(), + reason: c.reason, + effectiveDate: c.effectiveDate, + expireDate: c.expireDate, + isExpired: c.isExpired, + lastSyncedAt: c.lastSyncedAt, + createdAt: c.createdAt, + })), + total, + page: pageNum, + pageSize: pageSizeNum, + }; + } + + @Get('pending-contributions/summary') + @Public() + @ApiOperation({ summary: '获取待解锁算力汇总统计' }) + async getPendingContributionsSummary() { + // 按类型分组统计 + const byType = await this.prisma.pendingContributionMining.groupBy({ + by: ['contributionType'], + where: { isExpired: false }, + _sum: { amount: true }, + _count: { id: true }, + }); + + // 总计 + const total = await this.prisma.pendingContributionMining.aggregate({ + where: { isExpired: false }, + _sum: { amount: true }, + _count: { id: true }, + }); + + // 总挖矿收益(归总部) + const totalMined = await this.prisma.pendingMiningRecord.aggregate({ + _sum: { minedAmount: true }, + }); + + return { + byType: byType.map((t) => ({ + contributionType: t.contributionType, + totalAmount: t._sum.amount?.toString() || '0', + count: t._count.id, + })), + total: { + totalAmount: total._sum.amount?.toString() || '0', + count: total._count.id, + }, + totalMinedToHeadquarters: totalMined._sum.minedAmount?.toString() || '0', + }; + } + + @Get('pending-contributions/:id/records') + @Public() + @ApiOperation({ summary: '获取某条待解锁算力的挖矿记录' }) + @ApiParam({ name: 'id', type: String, description: '待解锁算力ID' }) + @ApiQuery({ name: 'page', required: false, type: Number }) + @ApiQuery({ name: 'pageSize', required: false, type: Number }) + async getPendingContributionMiningRecords( + @Param('id') id: string, + @Query('page') page?: number, + @Query('pageSize') pageSize?: number, + ) { + const pageNum = page ?? 1; + const pageSizeNum = pageSize ?? 20; + const skip = (pageNum - 1) * pageSizeNum; + const pendingId = BigInt(id); + + // 获取待解锁算力信息 + const pending = await this.prisma.pendingContributionMining.findUnique({ + where: { id: pendingId }, + }); + + if (!pending) { + throw new HttpException('待解锁算力记录不存在', HttpStatus.NOT_FOUND); + } + + const [records, total] = await Promise.all([ + this.prisma.pendingMiningRecord.findMany({ + where: { pendingContributionId: pendingId }, + orderBy: { miningMinute: 'desc' }, + skip, + take: pageSizeNum, + }), + this.prisma.pendingMiningRecord.count({ + where: { pendingContributionId: pendingId }, + }), + ]); + + return { + pendingContribution: { + id: pending.id.toString(), + sourceAdoptionId: pending.sourceAdoptionId.toString(), + sourceAccountSequence: pending.sourceAccountSequence, + wouldBeAccountSequence: pending.wouldBeAccountSequence, + contributionType: pending.contributionType, + amount: pending.amount.toString(), + reason: pending.reason, + }, + records: records.map((r) => ({ + id: r.id.toString(), + miningMinute: r.miningMinute, + contributionAmount: r.contributionAmount.toString(), + networkTotalContribution: r.networkTotalContribution.toString(), + contributionRatio: r.contributionRatio.toString(), + secondDistribution: r.secondDistribution.toString(), + minedAmount: r.minedAmount.toString(), + allocatedTo: r.allocatedTo, + createdAt: r.createdAt, + })), + total, + page: pageNum, + pageSize: pageSizeNum, + }; + } + + @Get('pending-contributions/mining-records') + @Public() + @ApiOperation({ summary: '获取所有待解锁算力的挖矿记录(汇总视图)' }) + @ApiQuery({ name: 'page', required: false, type: Number }) + @ApiQuery({ name: 'pageSize', required: false, type: Number }) + async getAllPendingMiningRecords( + @Query('page') page?: number, + @Query('pageSize') pageSize?: number, + ) { + const pageNum = page ?? 1; + const pageSizeNum = pageSize ?? 20; + const skip = (pageNum - 1) * pageSizeNum; + + const [records, total] = await Promise.all([ + this.prisma.pendingMiningRecord.findMany({ + orderBy: { miningMinute: 'desc' }, + skip, + take: pageSizeNum, + include: { + pendingContribution: { + select: { + contributionType: true, + wouldBeAccountSequence: true, + reason: true, + }, + }, + }, + }), + this.prisma.pendingMiningRecord.count(), + ]); + + return { + records: records.map((r) => ({ + id: r.id.toString(), + pendingContributionId: r.pendingContributionId.toString(), + miningMinute: r.miningMinute, + sourceAccountSequence: r.sourceAccountSequence, + wouldBeAccountSequence: r.wouldBeAccountSequence, + contributionType: r.contributionType, + contributionAmount: r.contributionAmount.toString(), + networkTotalContribution: r.networkTotalContribution.toString(), + contributionRatio: r.contributionRatio.toString(), + secondDistribution: r.secondDistribution.toString(), + minedAmount: r.minedAmount.toString(), + allocatedTo: r.allocatedTo, + reason: r.pendingContribution?.reason, + createdAt: r.createdAt, + })), + total, + page: pageNum, + pageSize: pageSizeNum, + }; + } } diff --git a/frontend/mining-admin-web/src/app/(dashboard)/pending-contributions/page.tsx b/frontend/mining-admin-web/src/app/(dashboard)/pending-contributions/page.tsx new file mode 100644 index 00000000..589604a7 --- /dev/null +++ b/frontend/mining-admin-web/src/app/(dashboard)/pending-contributions/page.tsx @@ -0,0 +1,506 @@ +'use client'; + +import { useState } from 'react'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; +import { Alert, AlertDescription } from '@/components/ui/alert'; +import { Badge } from '@/components/ui/badge'; +import { Button } from '@/components/ui/button'; +import { Skeleton } from '@/components/ui/skeleton'; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from '@/components/ui/table'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; +import { + AlertCircle, + RefreshCw, + Pickaxe, + List, + ChevronLeft, + ChevronRight, +} from 'lucide-react'; +import { useQueryClient } from '@tanstack/react-query'; +import { format } from 'date-fns'; +import { zhCN } from 'date-fns/locale'; + +import { + usePendingContributions, + usePendingContributionsSummary, + useAllPendingMiningRecords, +} from '@/features/pending-contributions'; +import { formatDecimal } from '@/lib/utils/format'; +import { PageHeader } from '@/components/layout/page-header'; + +const CONTRIBUTION_TYPE_LABELS: Record = { + LEVEL_1: { label: '一级', color: 'bg-blue-100 text-blue-800' }, + LEVEL_2: { label: '二级', color: 'bg-green-100 text-green-800' }, + LEVEL_3: { label: '三级', color: 'bg-purple-100 text-purple-800' }, + BONUS_1: { label: '奖励一级', color: 'bg-orange-100 text-orange-800' }, + BONUS_2: { label: '奖励二级', color: 'bg-yellow-100 text-yellow-800' }, + BONUS_3: { label: '奖励三级', color: 'bg-red-100 text-red-800' }, +}; + +export default function PendingContributionsPage() { + const queryClient = useQueryClient(); + + const [listPage, setListPage] = useState(1); + const [miningPage, setMiningPage] = useState(1); + const [typeFilter, setTypeFilter] = useState('all'); + const pageSize = 20; + + // 获取汇总统计 + const { + data: summary, + isLoading: summaryLoading, + error: summaryError, + } = usePendingContributionsSummary(); + + // 获取待解锁算力列表 + const { + data: contributions, + isLoading: listLoading, + error: listError, + } = usePendingContributions( + listPage, + pageSize, + typeFilter !== 'all' ? typeFilter : undefined + ); + + // 获取所有挖矿记录 + const { + data: miningRecords, + isLoading: miningLoading, + error: miningError, + } = useAllPendingMiningRecords(miningPage, pageSize); + + const handleRefresh = () => { + queryClient.invalidateQueries({ queryKey: ['pending-contributions'] }); + }; + + const listTotalPages = contributions + ? Math.ceil(contributions.total / pageSize) + : 0; + const miningTotalPages = miningRecords + ? Math.ceil(miningRecords.total / pageSize) + : 0; + + return ( +
+ + + 刷新 + + } + /> + + {/* 汇总统计卡片 */} +
+ + + + 待解锁算力总量 + + + + {summaryLoading ? ( + + ) : summaryError ? ( + 加载失败 + ) : ( + <> +
+ {formatDecimal(summary?.total?.totalAmount || '0', 2)} +
+
+ 共 {summary?.total?.count || 0} 条记录 +
+ + )} +
+
+ + + + + 已归入总部积分股 + + + + {summaryLoading ? ( + + ) : summaryError ? ( + 加载失败 + ) : ( +
+ {formatDecimal(summary?.totalMinedToHeadquarters || '0', 8)} +
+ )} +
+
+ + + + + 挖矿记录数 + + + + {miningLoading ? ( + + ) : ( +
{miningRecords?.total || 0}
+ )} +
+
+
+ + {/* 按类型统计 */} + {summary?.byType && summary.byType.length > 0 && ( + + + 按类型统计 + + +
+ {summary.byType.map((item) => { + const typeInfo = CONTRIBUTION_TYPE_LABELS[item.contributionType] || { + label: item.contributionType, + color: 'bg-gray-100 text-gray-800', + }; + return ( +
+ + {typeInfo.label} + +
+
+ 算力: {formatDecimal(item.totalAmount, 2)} +
+
+ 记录数: {item.count} +
+
+
+ ); + })} +
+
+
+ )} + + {/* 分类账 Tabs */} + + + + + 算力列表 + + + + 挖矿记录 + + + + {/* 算力列表 Tab */} + + + +
+ + 待解锁算力列表 + {contributions && ( + + 共 {contributions.total} 条 + + )} + + +
+
+ + {listError ? ( + + + 加载列表失败 + + ) : ( + <> + + + + 来源用户 + 归属用户 + 类型 + 算力 + 原因 + 创建时间 + + + + {listLoading ? ( + [...Array(5)].map((_, i) => ( + + {[...Array(6)].map((_, j) => ( + + + + ))} + + )) + ) : !contributions?.contributions.length ? ( + + + 暂无数据 + + + ) : ( + contributions.contributions.map((item) => { + const typeInfo = CONTRIBUTION_TYPE_LABELS[item.contributionType] || { + label: item.contributionType, + color: 'bg-gray-100 text-gray-800', + }; + return ( + + + + {item.sourceAccountSequence} + + + + + {item.wouldBeAccountSequence} + + + + + {typeInfo.label} + + + + {formatDecimal(item.amount, 2)} + + + {item.reason || '-'} + + + {format( + new Date(item.createdAt), + 'yyyy-MM-dd HH:mm', + { locale: zhCN } + )} + + + ); + }) + )} + +
+ + {/* 分页 */} + {listTotalPages > 1 && ( +
+ + + 第 {listPage} / {listTotalPages} 页 + + +
+ )} + + )} +
+
+
+ + {/* 挖矿记录 Tab */} + + + + + 挖矿记录 + {miningRecords && ( + + 共 {miningRecords.total} 条 + + )} + + + + {miningError ? ( + + + 加载挖矿记录失败 + + ) : ( + <> + + + + 挖矿时间 + 类型 + 算力 + 算力占比 + 每秒分配量 + 挖得数量 + 归入 + + + + {miningLoading ? ( + [...Array(5)].map((_, i) => ( + + {[...Array(7)].map((_, j) => ( + + + + ))} + + )) + ) : !miningRecords?.records.length ? ( + + + 暂无挖矿记录 + + + ) : ( + miningRecords.records.map((record) => { + const typeInfo = CONTRIBUTION_TYPE_LABELS[record.contributionType || ''] || { + label: record.contributionType || '-', + color: 'bg-gray-100 text-gray-800', + }; + return ( + + + {format( + new Date(record.miningMinute), + 'yyyy-MM-dd HH:mm', + { locale: zhCN } + )} + + + + {typeInfo.label} + + + + {formatDecimal(record.contributionAmount, 2)} + + + {(Number(record.contributionRatio) * 100).toFixed(6)}% + + + {formatDecimal(record.secondDistribution, 8)} + + + +{formatDecimal(record.minedAmount, 8)} + + + + {record.allocatedTo === 'HEADQUARTERS' ? '总部' : record.allocatedTo} + + + + ); + }) + )} + +
+ + {/* 分页 */} + {miningTotalPages > 1 && ( +
+ + + 第 {miningPage} / {miningTotalPages} 页 + + +
+ )} + + )} +
+
+
+
+
+ ); +} diff --git a/frontend/mining-admin-web/src/features/dashboard/components/contribution-breakdown.tsx b/frontend/mining-admin-web/src/features/dashboard/components/contribution-breakdown.tsx index 24a598f3..848c92da 100644 --- a/frontend/mining-admin-web/src/features/dashboard/components/contribution-breakdown.tsx +++ b/frontend/mining-admin-web/src/features/dashboard/components/contribution-breakdown.tsx @@ -1,10 +1,12 @@ 'use client'; +import Link from 'next/link'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Button } from '@/components/ui/button'; import { useDashboardStats } from '../hooks/use-dashboard-stats'; import { formatCompactNumber } from '@/lib/utils/format'; import { Skeleton } from '@/components/ui/skeleton'; -import { Activity, Users, Building2, Landmark, Layers, Gift, TreePine } from 'lucide-react'; +import { Activity, Users, Building2, Landmark, Layers, Gift, TreePine, ExternalLink } from 'lucide-react'; function ContributionBreakdownSkeleton() { return ( @@ -132,10 +134,18 @@ export function ContributionBreakdown() { {/* 层级算力详情 */} - - - 层级算力详情 (7.5%) - +
+ + + 层级算力详情 (7.5%) + + + + +

理论: {formatCompactNumber(dc.level.theory)} | 已解锁: {formatCompactNumber(dc.level.unlocked)} | @@ -173,10 +183,18 @@ export function ContributionBreakdown() { {/* 团队奖励详情 */} - - - 团队奖励详情 (7.5%) - +

+ + + 团队奖励详情 (7.5%) + + + + +

理论: {formatCompactNumber(dc.bonus.theory)} | 已解锁: {formatCompactNumber(dc.bonus.unlocked)} | diff --git a/frontend/mining-admin-web/src/features/pending-contributions/api/pending-contributions.api.ts b/frontend/mining-admin-web/src/features/pending-contributions/api/pending-contributions.api.ts new file mode 100644 index 00000000..01bb202b --- /dev/null +++ b/frontend/mining-admin-web/src/features/pending-contributions/api/pending-contributions.api.ts @@ -0,0 +1,114 @@ +import { apiClient } from '@/lib/api/client'; + +// 待解锁算力记录(来自 mining-service 的 PendingContributionMining 表) +export interface PendingContribution { + id: string; + sourceAdoptionId: string; + sourceAccountSequence: string; + wouldBeAccountSequence: string; + contributionType: string; + amount: string; + reason: string; + effectiveDate: string; + expireDate: string; + isExpired: boolean; + lastSyncedAt: string | null; + createdAt: string; +} + +// 待解锁算力列表响应 +export interface PendingContributionsResponse { + contributions: PendingContribution[]; + total: number; + page: number; + pageSize: number; +} + +// 待解锁算力汇总统计 +export interface PendingContributionsSummary { + byType: Array<{ + contributionType: string; + totalAmount: string; + count: number; + }>; + total: { + totalAmount: string; + count: number; + }; + totalMinedToHeadquarters: string; +} + +// 待解锁算力的挖矿记录 +export interface PendingMiningRecord { + id: string; + pendingContributionId?: string; + miningMinute: string; + sourceAccountSequence?: string; + wouldBeAccountSequence?: string; + contributionType?: string; + contributionAmount: string; + networkTotalContribution: string; + contributionRatio: string; + secondDistribution: string; + minedAmount: string; + allocatedTo: string; + reason?: string; + createdAt: string; +} + +// 挖矿记录响应 +export interface PendingMiningRecordsResponse { + records: PendingMiningRecord[]; + total: number; + page: number; + pageSize: number; +} + +export const pendingContributionsApi = { + // 获取待解锁算力列表 + getList: async ( + page: number = 1, + pageSize: number = 20, + contributionType?: string + ): Promise => { + const params = new URLSearchParams(); + params.append('page', page.toString()); + params.append('pageSize', pageSize.toString()); + if (contributionType) { + params.append('contributionType', contributionType); + } + const response = await apiClient.get( + `/pending-contributions?${params.toString()}` + ); + return response.data.data; + }, + + // 获取汇总统计 + getSummary: async (): Promise => { + const response = await apiClient.get('/pending-contributions/summary'); + return response.data.data; + }, + + // 获取所有待解锁算力的挖矿记录 + getAllMiningRecords: async ( + page: number = 1, + pageSize: number = 20 + ): Promise => { + const response = await apiClient.get( + `/pending-contributions/mining-records?page=${page}&pageSize=${pageSize}` + ); + return response.data.data; + }, + + // 获取某条待解锁算力的挖矿记录 + getMiningRecords: async ( + id: string, + page: number = 1, + pageSize: number = 20 + ): Promise => { + const response = await apiClient.get( + `/pending-contributions/${id}/records?page=${page}&pageSize=${pageSize}` + ); + return response.data.data; + }, +}; diff --git a/frontend/mining-admin-web/src/features/pending-contributions/hooks/use-pending-contributions.ts b/frontend/mining-admin-web/src/features/pending-contributions/hooks/use-pending-contributions.ts new file mode 100644 index 00000000..c999170c --- /dev/null +++ b/frontend/mining-admin-web/src/features/pending-contributions/hooks/use-pending-contributions.ts @@ -0,0 +1,43 @@ +import { useQuery } from '@tanstack/react-query'; +import { pendingContributionsApi } from '../api/pending-contributions.api'; + +export function usePendingContributions( + page: number = 1, + pageSize: number = 20, + contributionType?: string +) { + return useQuery({ + queryKey: ['pending-contributions', 'list', page, pageSize, contributionType], + queryFn: () => pendingContributionsApi.getList(page, pageSize, contributionType), + staleTime: 30000, + }); +} + +export function usePendingContributionsSummary() { + return useQuery({ + queryKey: ['pending-contributions', 'summary'], + queryFn: () => pendingContributionsApi.getSummary(), + staleTime: 30000, + }); +} + +export function useAllPendingMiningRecords(page: number = 1, pageSize: number = 20) { + return useQuery({ + queryKey: ['pending-contributions', 'mining-records', 'all', page, pageSize], + queryFn: () => pendingContributionsApi.getAllMiningRecords(page, pageSize), + staleTime: 30000, + }); +} + +export function usePendingContributionMiningRecords( + id: string, + page: number = 1, + pageSize: number = 20 +) { + return useQuery({ + queryKey: ['pending-contributions', 'mining-records', id, page, pageSize], + queryFn: () => pendingContributionsApi.getMiningRecords(id, page, pageSize), + enabled: !!id, + staleTime: 30000, + }); +} diff --git a/frontend/mining-admin-web/src/features/pending-contributions/index.ts b/frontend/mining-admin-web/src/features/pending-contributions/index.ts new file mode 100644 index 00000000..7e20b66d --- /dev/null +++ b/frontend/mining-admin-web/src/features/pending-contributions/index.ts @@ -0,0 +1,17 @@ +// API +export { + pendingContributionsApi, + type PendingContribution, + type PendingContributionsResponse, + type PendingContributionsSummary, + type PendingMiningRecord, + type PendingMiningRecordsResponse, +} from './api/pending-contributions.api'; + +// Hooks +export { + usePendingContributions, + usePendingContributionsSummary, + useAllPendingMiningRecords, + usePendingContributionMiningRecords, +} from './hooks/use-pending-contributions';