diff --git a/backend/services/mining-admin-service/package-lock.json b/backend/services/mining-admin-service/package-lock.json index 3cadd715..accbb9ec 100644 --- a/backend/services/mining-admin-service/package-lock.json +++ b/backend/services/mining-admin-service/package-lock.json @@ -8,12 +8,14 @@ "name": "mining-admin-service", "version": "1.0.0", "dependencies": { + "@nestjs/axios": "^3.1.3", "@nestjs/common": "^10.3.0", "@nestjs/config": "^3.1.1", "@nestjs/core": "^10.3.0", "@nestjs/platform-express": "^10.3.0", "@nestjs/swagger": "^7.1.17", "@prisma/client": "^5.7.1", + "axios": "^1.13.2", "bcrypt": "^5.1.1", "class-transformer": "^0.5.1", "class-validator": "^0.14.0", @@ -627,6 +629,17 @@ "integrity": "sha512-4aErSrCR/On/e5G2hDP0wjooqDdauzEbIq8hIkIe5pXV0rtWJZvdCEKL0ykZxex+IxIwBp0eGeV48hQN07dXtw==", "license": "MIT" }, + "node_modules/@nestjs/axios": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@nestjs/axios/-/axios-3.1.3.tgz", + "integrity": "sha512-RZ/63c1tMxGLqyG3iOCVt7A72oy4x1eM6QEhd4KzCYpaVWW0igq0WSREeRoEZhIxRcZfDfIIkvsOMiM7yfVGZQ==", + "license": "MIT", + "peerDependencies": { + "@nestjs/common": "^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0", + "axios": "^1.3.1", + "rxjs": "^6.0.0 || ^7.0.0" + } + }, "node_modules/@nestjs/cli": { "version": "10.4.9", "resolved": "https://registry.npmjs.org/@nestjs/cli/-/cli-10.4.9.tgz", @@ -1734,6 +1747,24 @@ "dev": true, "license": "MIT" }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz", + "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", + "license": "MIT", + "peer": true, + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -2212,6 +2243,18 @@ "color-support": "bin.js" } }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/commander": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", @@ -2433,6 +2476,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/delegates": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", @@ -2629,6 +2681,21 @@ "node": ">= 0.4" } }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -3136,6 +3203,26 @@ "dev": true, "license": "ISC" }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, "node_modules/foreground-child": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", @@ -3182,6 +3269,22 @@ "webpack": "^5.11.0" } }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -3493,6 +3596,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/has-unicode": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", @@ -4878,6 +4996,12 @@ "node": ">= 0.10" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", diff --git a/backend/services/mining-admin-service/package.json b/backend/services/mining-admin-service/package.json index 9d3c7aa7..e453f614 100644 --- a/backend/services/mining-admin-service/package.json +++ b/backend/services/mining-admin-service/package.json @@ -15,12 +15,14 @@ "prisma:migrate": "prisma migrate dev" }, "dependencies": { + "@nestjs/axios": "^3.1.3", "@nestjs/common": "^10.3.0", "@nestjs/config": "^3.1.1", "@nestjs/core": "^10.3.0", "@nestjs/platform-express": "^10.3.0", "@nestjs/swagger": "^7.1.17", "@prisma/client": "^5.7.1", + "axios": "^1.13.2", "bcrypt": "^5.1.1", "class-transformer": "^0.5.1", "class-validator": "^0.14.0", diff --git a/backend/services/mining-admin-service/src/api/controllers/system-accounts.controller.ts b/backend/services/mining-admin-service/src/api/controllers/system-accounts.controller.ts index 934bdda4..590ccf3a 100644 --- a/backend/services/mining-admin-service/src/api/controllers/system-accounts.controller.ts +++ b/backend/services/mining-admin-service/src/api/controllers/system-accounts.controller.ts @@ -1,5 +1,5 @@ -import { Controller, Get } from '@nestjs/common'; -import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger'; +import { Controller, Get, Param, Query } from '@nestjs/common'; +import { ApiTags, ApiOperation, ApiBearerAuth, ApiParam, ApiQuery } from '@nestjs/swagger'; import { SystemAccountsService } from '../../application/services/system-accounts.service'; @ApiTags('System Accounts') @@ -19,4 +19,38 @@ export class SystemAccountsController { async getSystemAccountsSummary() { return this.systemAccountsService.getSystemAccountsSummary(); } + + @Get(':accountType/records') + @ApiOperation({ summary: '获取系统账户挖矿记录' }) + @ApiParam({ name: 'accountType', type: String, description: '系统账户类型' }) + @ApiQuery({ name: 'page', required: false, type: Number }) + @ApiQuery({ name: 'pageSize', required: false, type: Number }) + async getSystemAccountMiningRecords( + @Param('accountType') accountType: string, + @Query('page') page?: number, + @Query('pageSize') pageSize?: number, + ) { + return this.systemAccountsService.getSystemAccountMiningRecords( + accountType, + page ?? 1, + pageSize ?? 20, + ); + } + + @Get(':accountType/transactions') + @ApiOperation({ summary: '获取系统账户交易记录' }) + @ApiParam({ name: 'accountType', type: String, description: '系统账户类型' }) + @ApiQuery({ name: 'page', required: false, type: Number }) + @ApiQuery({ name: 'pageSize', required: false, type: Number }) + async getSystemAccountTransactions( + @Param('accountType') accountType: string, + @Query('page') page?: number, + @Query('pageSize') pageSize?: number, + ) { + return this.systemAccountsService.getSystemAccountTransactions( + accountType, + page ?? 1, + pageSize ?? 20, + ); + } } diff --git a/backend/services/mining-admin-service/src/application/services/system-accounts.service.ts b/backend/services/mining-admin-service/src/application/services/system-accounts.service.ts index fadae736..bd7cbc74 100644 --- a/backend/services/mining-admin-service/src/application/services/system-accounts.service.ts +++ b/backend/services/mining-admin-service/src/application/services/system-accounts.service.ts @@ -1,13 +1,66 @@ -import { Injectable } from '@nestjs/common'; +import { Injectable, Logger } from '@nestjs/common'; +import { HttpService } from '@nestjs/axios'; +import { ConfigService } from '@nestjs/config'; +import { firstValueFrom } from 'rxjs'; import { PrismaService } from '../../infrastructure/persistence/prisma/prisma.service'; +interface MiningServiceSystemAccount { + accountType: string; + name: string; + totalMined: string; + availableBalance: string; + totalContribution: string; + lastSyncedAt: string | null; +} + +interface MiningServiceResponse { + accounts: MiningServiceSystemAccount[]; + total: number; +} + @Injectable() export class SystemAccountsService { - constructor(private readonly prisma: PrismaService) {} + private readonly logger = new Logger(SystemAccountsService.name); + + constructor( + private readonly prisma: PrismaService, + private readonly httpService: HttpService, + private readonly configService: ConfigService, + ) {} + + /** + * 从 mining-service 获取系统账户挖矿数据 + */ + private async fetchMiningServiceSystemAccounts(): Promise> { + const miningServiceUrl = this.configService.get( + 'MINING_SERVICE_URL', + 'http://localhost:3021', + ); + + try { + const response = await firstValueFrom( + this.httpService.get( + `${miningServiceUrl}/admin/system-accounts`, + ), + ); + + const miningDataMap = new Map(); + for (const account of response.data.accounts) { + miningDataMap.set(account.accountType, account); + } + + return miningDataMap; + } catch (error) { + this.logger.warn( + `Failed to fetch mining service system accounts: ${error.message}`, + ); + return new Map(); + } + } /** * 获取系统账户列表 - * 从 CDC 同步的钱包系统账户表读取数据 + * 从 CDC 同步的钱包系统账户表读取数据,并合并挖矿数据 */ async getSystemAccounts() { // 从 CDC 同步的 SyncedWalletSystemAccount 表获取数据 @@ -19,6 +72,9 @@ export class SystemAccountsService { const syncedContributions = await this.prisma.syncedSystemContribution.findMany(); + // 从 mining-service 获取挖矿数据 + const miningDataMap = await this.fetchMiningServiceSystemAccounts(); + // 构建算力数据映射 const contributionMap = new Map(); for (const contrib of syncedContributions) { @@ -28,6 +84,8 @@ export class SystemAccountsService { // 构建返回数据 const accounts = syncedAccounts.map((account) => { const contrib = contributionMap.get(account.accountType); + const miningData = miningDataMap.get(account.accountType); + return { id: account.originalId, accountType: account.accountType, @@ -46,6 +104,11 @@ export class SystemAccountsService { isActive: account.isActive, contributionBalance: contrib?.contributionBalance?.toString() || '0', contributionNeverExpires: contrib?.contributionNeverExpires || false, + // 挖矿数据 + totalMined: miningData?.totalMined || '0', + availableBalance: miningData?.availableBalance || '0', + miningContribution: miningData?.totalContribution || '0', + miningLastSyncedAt: miningData?.lastSyncedAt || null, syncedAt: account.syncedAt, source: 'cdc', }; @@ -75,6 +138,15 @@ export class SystemAccountsService { this.prisma.syncedCirculationPool.findFirst(), ]); + // 从 mining-service 获取挖矿数据汇总 + const miningDataMap = await this.fetchMiningServiceSystemAccounts(); + + // 计算总挖矿积分股 + let totalMined = 0; + for (const miningData of miningDataMap.values()) { + totalMined += Number(miningData.totalMined || 0); + } + // 计算总算力 let totalSyncedContribution = 0n; for (const contrib of syncedContributions) { @@ -90,6 +162,7 @@ export class SystemAccountsService { (sum, acc) => sum + Number(acc.shareBalance), 0, ).toFixed(8), + totalMined: totalMined.toFixed(8), }, poolAccounts: { count: syncedPoolAccounts.length, @@ -123,4 +196,68 @@ export class SystemAccountsService { : null, }; } + + /** + * 获取系统账户挖矿记录 + */ + async getSystemAccountMiningRecords( + accountType: string, + page: number = 1, + pageSize: number = 20, + ) { + const miningServiceUrl = this.configService.get( + 'MINING_SERVICE_URL', + 'http://localhost:3021', + ); + + try { + const response = await firstValueFrom( + this.httpService.get( + `${miningServiceUrl}/admin/system-accounts/${accountType}/records`, + { + params: { page, pageSize }, + }, + ), + ); + + return response.data; + } catch (error) { + this.logger.warn( + `Failed to fetch system account mining records: ${error.message}`, + ); + return { records: [], total: 0, page, pageSize }; + } + } + + /** + * 获取系统账户交易记录 + */ + async getSystemAccountTransactions( + accountType: string, + page: number = 1, + pageSize: number = 20, + ) { + const miningServiceUrl = this.configService.get( + 'MINING_SERVICE_URL', + 'http://localhost:3021', + ); + + try { + const response = await firstValueFrom( + this.httpService.get( + `${miningServiceUrl}/admin/system-accounts/${accountType}/transactions`, + { + params: { page, pageSize }, + }, + ), + ); + + return response.data; + } catch (error) { + this.logger.warn( + `Failed to fetch system account transactions: ${error.message}`, + ); + return { transactions: [], total: 0, page, pageSize }; + } + } } diff --git a/backend/services/mining-admin-service/src/infrastructure/infrastructure.module.ts b/backend/services/mining-admin-service/src/infrastructure/infrastructure.module.ts index 50d4bf24..31c1c077 100644 --- a/backend/services/mining-admin-service/src/infrastructure/infrastructure.module.ts +++ b/backend/services/mining-admin-service/src/infrastructure/infrastructure.module.ts @@ -1,12 +1,20 @@ import { Module, Global } from '@nestjs/common'; import { ConfigModule, ConfigService } from '@nestjs/config'; +import { HttpModule } from '@nestjs/axios'; import { PrismaModule } from './persistence/prisma/prisma.module'; import { RedisService } from './redis/redis.service'; import { KafkaModule } from './kafka/kafka.module'; @Global() @Module({ - imports: [PrismaModule, KafkaModule], + imports: [ + PrismaModule, + KafkaModule, + HttpModule.register({ + timeout: 10000, + maxRedirects: 5, + }), + ], providers: [ { provide: 'REDIS_OPTIONS', @@ -20,6 +28,6 @@ import { KafkaModule } from './kafka/kafka.module'; }, RedisService, ], - exports: [PrismaModule, RedisService, KafkaModule], + exports: [PrismaModule, RedisService, KafkaModule, HttpModule], }) export class InfrastructureModule {} 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 e01c5a9f..959bc973 100644 --- a/backend/services/mining-service/src/api/controllers/admin.controller.ts +++ b/backend/services/mining-service/src/api/controllers/admin.controller.ts @@ -1,6 +1,7 @@ import { Controller, Get, Post, Body, Query, Param, HttpException, HttpStatus } from '@nestjs/common'; import { ApiTags, ApiOperation, ApiBody, ApiQuery, ApiParam } from '@nestjs/swagger'; import { ConfigService } from '@nestjs/config'; +import { SystemAccountType } from '@prisma/client'; import { PrismaService } from '../../infrastructure/persistence/prisma/prisma.service'; import { NetworkSyncService } from '../../application/services/network-sync.service'; import { ManualMiningService } from '../../application/services/manual-mining.service'; @@ -179,6 +180,98 @@ export class AdminController { }; } + @Get('system-accounts/:accountType/records') + @Public() + @ApiOperation({ summary: '获取系统账户挖矿记录' }) + @ApiParam({ name: 'accountType', type: String, description: '系统账户类型 (OPERATION, PROVINCE, CITY, HEADQUARTERS)' }) + @ApiQuery({ name: 'page', required: false, type: Number }) + @ApiQuery({ name: 'pageSize', required: false, type: Number }) + async getSystemAccountMiningRecords( + @Param('accountType') accountType: string, + @Query('page') page?: number, + @Query('pageSize') pageSize?: number, + ) { + const pageNum = page ?? 1; + const pageSizeNum = pageSize ?? 20; + const skip = (pageNum - 1) * pageSizeNum; + const accountTypeEnum = accountType as SystemAccountType; + + const [records, total] = await Promise.all([ + this.prisma.systemMiningRecord.findMany({ + where: { accountType: accountTypeEnum }, + orderBy: { miningMinute: 'desc' }, + skip, + take: pageSizeNum, + }), + this.prisma.systemMiningRecord.count({ + where: { accountType: accountTypeEnum }, + }), + ]); + + return { + records: records.map((record) => ({ + id: record.id, + accountType: record.accountType, + miningMinute: record.miningMinute, + contributionRatio: record.contributionRatio.toString(), + totalContribution: record.totalContribution.toString(), + secondDistribution: record.secondDistribution.toString(), + minedAmount: record.minedAmount.toString(), + createdAt: record.createdAt, + })), + total, + page: pageNum, + pageSize: pageSizeNum, + }; + } + + @Get('system-accounts/:accountType/transactions') + @Public() + @ApiOperation({ summary: '获取系统账户交易记录' }) + @ApiParam({ name: 'accountType', type: String, description: '系统账户类型 (OPERATION, PROVINCE, CITY, HEADQUARTERS)' }) + @ApiQuery({ name: 'page', required: false, type: Number }) + @ApiQuery({ name: 'pageSize', required: false, type: Number }) + async getSystemAccountTransactions( + @Param('accountType') accountType: string, + @Query('page') page?: number, + @Query('pageSize') pageSize?: number, + ) { + const pageNum = page ?? 1; + const pageSizeNum = pageSize ?? 20; + const skip = (pageNum - 1) * pageSizeNum; + const accountTypeEnum = accountType as SystemAccountType; + + const [transactions, total] = await Promise.all([ + this.prisma.systemMiningTransaction.findMany({ + where: { accountType: accountTypeEnum }, + orderBy: { createdAt: 'desc' }, + skip, + take: pageSizeNum, + }), + this.prisma.systemMiningTransaction.count({ + where: { accountType: accountTypeEnum }, + }), + ]); + + return { + transactions: transactions.map((tx) => ({ + id: tx.id, + accountType: tx.accountType, + type: tx.type, + amount: tx.amount.toString(), + balanceBefore: tx.balanceBefore.toString(), + balanceAfter: tx.balanceAfter.toString(), + referenceId: tx.referenceId, + referenceType: tx.referenceType, + memo: tx.memo, + createdAt: tx.createdAt, + })), + total, + page: pageNum, + pageSize: pageSizeNum, + }; + } + @Get('mining/status') @Public() @ApiOperation({ summary: '获取挖矿进度状态(类似销毁进度)' }) diff --git a/frontend/mining-admin-web/src/app/(dashboard)/system-accounts/[accountType]/page.tsx b/frontend/mining-admin-web/src/app/(dashboard)/system-accounts/[accountType]/page.tsx new file mode 100644 index 00000000..21e8ad29 --- /dev/null +++ b/frontend/mining-admin-web/src/app/(dashboard)/system-accounts/[accountType]/page.tsx @@ -0,0 +1,464 @@ +'use client'; + +import { useState } from 'react'; +import { useParams } from 'next/navigation'; +import Link from 'next/link'; +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 { + AlertCircle, + RefreshCw, + Pickaxe, + Receipt, + ChevronLeft, + ChevronRight, +} from 'lucide-react'; +import { useQueryClient } from '@tanstack/react-query'; +import { format } from 'date-fns'; +import { zhCN } from 'date-fns/locale'; + +import { + useSystemAccounts, + useSystemAccountMiningRecords, + useSystemAccountTransactions, +} from '@/features/system-accounts'; +import { getAccountDisplayInfo } from '@/types/system-account'; +import { formatDecimal } from '@/lib/utils/format'; + +const TRANSACTION_TYPE_LABELS: Record = { + MINE: { label: '挖矿收益', color: 'bg-green-100 text-green-800' }, + TRANSFER_OUT: { label: '转出', color: 'bg-red-100 text-red-800' }, + TRANSFER_IN: { label: '转入', color: 'bg-blue-100 text-blue-800' }, + ADJUSTMENT: { label: '调整', color: 'bg-yellow-100 text-yellow-800' }, +}; + +export default function SystemAccountDetailPage() { + const params = useParams(); + const queryClient = useQueryClient(); + const accountType = params.accountType as string; + + const [miningPage, setMiningPage] = useState(1); + const [transactionPage, setTransactionPage] = useState(1); + const pageSize = 20; + + // 获取账户列表以找到当前账户信息 + const { data: accountsData, isLoading: accountsLoading } = useSystemAccounts(); + const currentAccount = accountsData?.accounts.find( + (a) => a.accountType === accountType + ); + + // 获取挖矿记录 + const { + data: miningRecords, + isLoading: miningLoading, + error: miningError, + } = useSystemAccountMiningRecords(accountType, miningPage, pageSize); + + // 获取交易记录 + const { + data: transactions, + isLoading: transactionsLoading, + error: transactionsError, + } = useSystemAccountTransactions(accountType, transactionPage, pageSize); + + const displayInfo = getAccountDisplayInfo(accountType); + + const handleRefresh = () => { + queryClient.invalidateQueries({ + queryKey: ['system-accounts', accountType], + }); + }; + + const miningTotalPages = miningRecords + ? Math.ceil(miningRecords.total / pageSize) + : 0; + const transactionTotalPages = transactions + ? Math.ceil(transactions.total / pageSize) + : 0; + + const accountTitle = currentAccount?.name || displayInfo.label; + + return ( +
+ {/* 自定义头部 */} +
+
+ + + +
+ +

{accountTitle}

+ + {accountType} + +
+
+ +
+ + {/* 账户概览卡片 */} +
+ + + + 当前算力 + + + + {accountsLoading ? ( + + ) : ( +
+ {formatDecimal( + currentAccount?.contributionBalance || + currentAccount?.totalContribution || + '0', + 2 + )} +
+ )} +
+
+ + + + + 已挖积分股 + + + + {accountsLoading ? ( + + ) : ( +
+ {formatDecimal(currentAccount?.totalMined || '0', 8)} +
+ )} +
+
+ + + + + 可用余额 + + + + {accountsLoading ? ( + + ) : ( +
+ {formatDecimal(currentAccount?.availableBalance || '0', 8)} +
+ )} +
+
+ + + + + 挖矿记录数 + + + + {miningLoading ? ( + + ) : ( +
{miningRecords?.total || 0}
+ )} +
+
+
+ + {/* 分类账 Tabs */} + + + + + 挖矿记录 + + + + 交易记录 + + + + {/* 挖矿记录 Tab */} + + + + + 挖矿记录 + {miningRecords && ( + + 共 {miningRecords.total} 条 + + )} + + + + {miningError ? ( + + + 加载挖矿记录失败 + + ) : ( + <> + + + + 挖矿时间 + 算力占比 + 全网算力 + 每秒分配量 + 挖得数量 + + + + {miningLoading ? ( + [...Array(5)].map((_, i) => ( + + {[...Array(5)].map((_, j) => ( + + + + ))} + + )) + ) : !miningRecords?.records.length ? ( + + + 暂无挖矿记录 + + + ) : ( + miningRecords.records.map((record) => ( + + + {format( + new Date(record.miningMinute), + 'yyyy-MM-dd HH:mm', + { locale: zhCN } + )} + + + {(Number(record.contributionRatio) * 100).toFixed(6)}% + + + {formatDecimal(record.totalContribution, 2)} + + + {formatDecimal(record.secondDistribution, 8)} + + + +{formatDecimal(record.minedAmount, 8)} + + + )) + )} + +
+ + {/* 分页 */} + {miningTotalPages > 1 && ( +
+ + + 第 {miningPage} / {miningTotalPages} 页 + + +
+ )} + + )} +
+
+
+ + {/* 交易记录 Tab */} + + + + + 交易记录 + {transactions && ( + + 共 {transactions.total} 条 + + )} + + + + {transactionsError ? ( + + + 加载交易记录失败 + + ) : ( + <> + + + + 时间 + 类型 + 金额 + 交易前余额 + 交易后余额 + 备注 + + + + {transactionsLoading ? ( + [...Array(5)].map((_, i) => ( + + {[...Array(6)].map((_, j) => ( + + + + ))} + + )) + ) : !transactions?.transactions.length ? ( + + + 暂无交易记录 + + + ) : ( + transactions.transactions.map((tx) => { + const typeInfo = TRANSACTION_TYPE_LABELS[tx.type] || { + label: tx.type, + color: 'bg-gray-100 text-gray-800', + }; + const isPositive = Number(tx.amount) > 0; + + return ( + + + {format( + new Date(tx.createdAt), + 'yyyy-MM-dd HH:mm:ss', + { locale: zhCN } + )} + + + + {typeInfo.label} + + + + {isPositive ? '+' : ''} + {formatDecimal(tx.amount, 8)} + + + {formatDecimal(tx.balanceBefore, 8)} + + + {formatDecimal(tx.balanceAfter, 8)} + + + {tx.memo || '-'} + + + ); + }) + )} + +
+ + {/* 分页 */} + {transactionTotalPages > 1 && ( +
+ + + 第 {transactionPage} / {transactionTotalPages} 页 + + +
+ )} + + )} +
+
+
+
+
+ ); +} diff --git a/frontend/mining-admin-web/src/features/system-accounts/api/system-accounts.api.ts b/frontend/mining-admin-web/src/features/system-accounts/api/system-accounts.api.ts index 7f068fa4..259d962e 100644 --- a/frontend/mining-admin-web/src/features/system-accounts/api/system-accounts.api.ts +++ b/frontend/mining-admin-web/src/features/system-accounts/api/system-accounts.api.ts @@ -5,6 +5,44 @@ import type { SystemAccountsSummary, } from '@/types/system-account'; +export interface SystemMiningRecord { + id: string; + accountType: string; + miningMinute: string; + contributionRatio: string; + totalContribution: string; + secondDistribution: string; + minedAmount: string; + createdAt: string; +} + +export interface SystemMiningRecordsResponse { + records: SystemMiningRecord[]; + total: number; + page: number; + pageSize: number; +} + +export interface SystemTransaction { + id: string; + accountType: string; + type: string; + amount: string; + balanceBefore: string; + balanceAfter: string; + referenceId?: string; + referenceType?: string; + memo?: string; + createdAt: string; +} + +export interface SystemTransactionsResponse { + transactions: SystemTransaction[]; + total: number; + page: number; + pageSize: number; +} + export const systemAccountsApi = { /** * Get all system accounts (merged local + synced data) @@ -21,6 +59,36 @@ export const systemAccountsApi = { const response = await apiClient.get('/system-accounts/summary'); return response.data.data; }, + + /** + * Get system account mining records + */ + getMiningRecords: async ( + accountType: string, + page: number = 1, + pageSize: number = 20 + ): Promise => { + const response = await apiClient.get( + `/system-accounts/${accountType}/records`, + { params: { page, pageSize } } + ); + return response.data.data; + }, + + /** + * Get system account transactions + */ + getTransactions: async ( + accountType: string, + page: number = 1, + pageSize: number = 20 + ): Promise => { + const response = await apiClient.get( + `/system-accounts/${accountType}/transactions`, + { params: { page, pageSize } } + ); + return response.data.data; + }, }; // Helper to categorize accounts for display diff --git a/frontend/mining-admin-web/src/features/system-accounts/components/accounts-table.tsx b/frontend/mining-admin-web/src/features/system-accounts/components/accounts-table.tsx index 2702f610..acb26614 100644 --- a/frontend/mining-admin-web/src/features/system-accounts/components/accounts-table.tsx +++ b/frontend/mining-admin-web/src/features/system-accounts/components/accounts-table.tsx @@ -1,5 +1,6 @@ 'use client'; +import Link from 'next/link'; import { Table, TableBody, @@ -11,16 +12,19 @@ import { import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Badge } from '@/components/ui/badge'; import { Skeleton } from '@/components/ui/skeleton'; +import { Button } from '@/components/ui/button'; import { formatDecimal } from '@/lib/utils/format'; import { getAccountDisplayInfo, type SystemAccount } from '@/types/system-account'; import { formatDistanceToNow } from 'date-fns'; import { zhCN } from 'date-fns/locale'; +import { FileText } from 'lucide-react'; interface AccountsTableProps { title: string; accounts: SystemAccount[]; isLoading?: boolean; showSyncInfo?: boolean; + showMiningData?: boolean; } export function AccountsTable({ @@ -28,7 +32,16 @@ export function AccountsTable({ accounts, isLoading = false, showSyncInfo = false, + showMiningData = true, }: AccountsTableProps) { + // 计算列数 + const getColumnCount = () => { + let count = 5; // 基础列: 类型, 名称, 算力, 来源, 操作 + if (showMiningData) count += 1; // 已挖积分股 + if (showSyncInfo) count += 1; // 同步时间 + return count; + }; + return ( @@ -45,19 +58,22 @@ export function AccountsTable({ - 账户类型 + 账户类型 账户名称 - 余额/算力 + 算力 + {showMiningData && ( + 已挖积分股 + )} 来源 {showSyncInfo && 同步时间} - 描述 + 操作 {isLoading ? ( [...Array(3)].map((_, i) => ( - {[...Array(showSyncInfo ? 6 : 5)].map((_, j) => ( + {[...Array(getColumnCount())].map((_, j) => ( @@ -67,7 +83,7 @@ export function AccountsTable({ ) : accounts.length === 0 ? ( 暂无数据 @@ -76,7 +92,8 @@ export function AccountsTable({ ) : ( accounts.map((account) => { const displayInfo = getAccountDisplayInfo(account.accountType); - const balance = account.contributionBalance || account.totalContribution || '0'; + const contribution = account.contributionBalance || account.totalContribution || '0'; + const totalMined = account.totalMined || '0'; return ( @@ -94,13 +111,20 @@ export function AccountsTable({ {account.name || displayInfo.label} - {formatDecimal(balance, 8)} + {formatDecimal(contribution, 8)} {account.contributionNeverExpires && ( 永久 )} + {showMiningData && ( + + 0 ? 'text-green-600' : 'text-muted-foreground'}> + {formatDecimal(totalMined, 8)} + + + )} )} - - {account.description || displayInfo.description} + + + + ); diff --git a/frontend/mining-admin-web/src/features/system-accounts/hooks/use-system-accounts.ts b/frontend/mining-admin-web/src/features/system-accounts/hooks/use-system-accounts.ts index d70ac08e..ef98f888 100644 --- a/frontend/mining-admin-web/src/features/system-accounts/hooks/use-system-accounts.ts +++ b/frontend/mining-admin-web/src/features/system-accounts/hooks/use-system-accounts.ts @@ -46,3 +46,33 @@ export function useCategorizedAccounts() { total: data?.total ?? 0, }; } + +/** + * Hook to fetch system account mining records + */ +export function useSystemAccountMiningRecords( + accountType: string, + page: number = 1, + pageSize: number = 20 +) { + return useQuery({ + queryKey: ['system-accounts', accountType, 'records', page, pageSize], + queryFn: () => systemAccountsApi.getMiningRecords(accountType, page, pageSize), + enabled: !!accountType, + }); +} + +/** + * Hook to fetch system account transactions + */ +export function useSystemAccountTransactions( + accountType: string, + page: number = 1, + pageSize: number = 20 +) { + return useQuery({ + queryKey: ['system-accounts', accountType, 'transactions', page, pageSize], + queryFn: () => systemAccountsApi.getTransactions(accountType, page, pageSize), + enabled: !!accountType, + }); +} diff --git a/frontend/mining-admin-web/src/features/system-accounts/index.ts b/frontend/mining-admin-web/src/features/system-accounts/index.ts index b2e3c43b..a7ea11fd 100644 --- a/frontend/mining-admin-web/src/features/system-accounts/index.ts +++ b/frontend/mining-admin-web/src/features/system-accounts/index.ts @@ -1,11 +1,20 @@ // API -export { systemAccountsApi, categorizeAccounts } from './api/system-accounts.api'; +export { + systemAccountsApi, + categorizeAccounts, + type SystemMiningRecord, + type SystemMiningRecordsResponse, + type SystemTransaction, + type SystemTransactionsResponse, +} from './api/system-accounts.api'; // Hooks export { useSystemAccounts, useSystemAccountsSummary, useCategorizedAccounts, + useSystemAccountMiningRecords, + useSystemAccountTransactions, } from './hooks/use-system-accounts'; // Components diff --git a/frontend/mining-admin-web/src/types/system-account.ts b/frontend/mining-admin-web/src/types/system-account.ts index 066a6f8c..5b5cc223 100644 --- a/frontend/mining-admin-web/src/types/system-account.ts +++ b/frontend/mining-admin-web/src/types/system-account.ts @@ -21,6 +21,11 @@ export interface SystemAccount { totalContribution?: string; contributionBalance?: string; contributionNeverExpires?: boolean; + // 挖矿数据 + totalMined?: string; + availableBalance?: string; + miningContribution?: string; + miningLastSyncedAt?: string; syncedAt?: string; createdAt?: string; source: AccountSource;