feat(admin): 系统账户添加已挖积分股显示和分类账功能
## 后端改动 ### mining-service - 新增 GET /admin/system-accounts/:accountType/records - 获取系统账户挖矿记录(分钟级) - 新增 GET /admin/system-accounts/:accountType/transactions - 获取系统账户交易记录 ### mining-admin-service - 添加 @nestjs/axios 依赖用于 HTTP 调用 - 修改 SystemAccountsService,通过 HTTP 调用 mining-service 获取挖矿数据(totalMined, availableBalance) - 新增挖矿记录和交易记录的代理 API ## 前端改动 ### 类型定义 - SystemAccount 新增 totalMined, availableBalance, miningContribution, miningLastSyncedAt 字段 ### API 层 - 新增 getMiningRecords 和 getTransactions API 方法 - 新增 SystemMiningRecord, SystemTransaction 等类型定义 ### Hooks - 新增 useSystemAccountMiningRecords 和 useSystemAccountTransactions ### 组件 - AccountsTable 新增"已挖积分股"列,显示每个系统账户累计挖到的积分股 - AccountsTable 新增"分类账"按钮,可跳转到账户详情页 ### 新页面 - 新建 /system-accounts/[accountType] 详情页面 - 账户概览卡片:当前算力、已挖积分股、可用余额、挖矿记录数 - 挖矿记录 Tab:分钟级挖矿明细(时间、算力占比、全网算力、每秒分配量、挖得数量) - 交易记录 Tab:所有交易流水(时间、类型、金额、交易前后余额、备注) - 支持分页浏览 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
07498271d3
commit
d957e5a841
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<Map<string, MiningServiceSystemAccount>> {
|
||||
const miningServiceUrl = this.configService.get<string>(
|
||||
'MINING_SERVICE_URL',
|
||||
'http://localhost:3021',
|
||||
);
|
||||
|
||||
try {
|
||||
const response = await firstValueFrom(
|
||||
this.httpService.get<MiningServiceResponse>(
|
||||
`${miningServiceUrl}/admin/system-accounts`,
|
||||
),
|
||||
);
|
||||
|
||||
const miningDataMap = new Map<string, MiningServiceSystemAccount>();
|
||||
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<string, any>();
|
||||
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<string>(
|
||||
'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<string>(
|
||||
'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 };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {}
|
||||
|
|
|
|||
|
|
@ -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: '获取挖矿进度状态(类似销毁进度)' })
|
||||
|
|
|
|||
|
|
@ -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<string, { label: string; color: string }> = {
|
||||
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 (
|
||||
<div className="space-y-6">
|
||||
{/* 自定义头部 */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<Link href="/system-accounts">
|
||||
<Button variant="ghost" size="icon" className="h-8 w-8">
|
||||
<ChevronLeft className="h-4 w-4" />
|
||||
</Button>
|
||||
</Link>
|
||||
<div className="flex items-center gap-3">
|
||||
<span
|
||||
className={`inline-block w-3 h-3 rounded-full ${displayInfo.color.replace('text-', 'bg-')}`}
|
||||
/>
|
||||
<h1 className="text-2xl font-bold tracking-tight">{accountTitle}</h1>
|
||||
<code className="text-sm bg-muted px-2 py-1 rounded font-normal">
|
||||
{accountType}
|
||||
</code>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleRefresh}
|
||||
disabled={miningLoading || transactionsLoading}
|
||||
>
|
||||
<RefreshCw
|
||||
className={`h-4 w-4 mr-2 ${miningLoading || transactionsLoading ? 'animate-spin' : ''}`}
|
||||
/>
|
||||
刷新
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 账户概览卡片 */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-sm font-medium text-muted-foreground">
|
||||
当前算力
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{accountsLoading ? (
|
||||
<Skeleton className="h-8 w-32" />
|
||||
) : (
|
||||
<div className="text-2xl font-bold">
|
||||
{formatDecimal(
|
||||
currentAccount?.contributionBalance ||
|
||||
currentAccount?.totalContribution ||
|
||||
'0',
|
||||
2
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-sm font-medium text-muted-foreground">
|
||||
已挖积分股
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{accountsLoading ? (
|
||||
<Skeleton className="h-8 w-32" />
|
||||
) : (
|
||||
<div className="text-2xl font-bold text-green-600">
|
||||
{formatDecimal(currentAccount?.totalMined || '0', 8)}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-sm font-medium text-muted-foreground">
|
||||
可用余额
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{accountsLoading ? (
|
||||
<Skeleton className="h-8 w-32" />
|
||||
) : (
|
||||
<div className="text-2xl font-bold">
|
||||
{formatDecimal(currentAccount?.availableBalance || '0', 8)}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-sm font-medium text-muted-foreground">
|
||||
挖矿记录数
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{miningLoading ? (
|
||||
<Skeleton className="h-8 w-20" />
|
||||
) : (
|
||||
<div className="text-2xl font-bold">{miningRecords?.total || 0}</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* 分类账 Tabs */}
|
||||
<Tabs defaultValue="mining" className="space-y-4">
|
||||
<TabsList>
|
||||
<TabsTrigger value="mining" className="flex items-center gap-2">
|
||||
<Pickaxe className="h-4 w-4" />
|
||||
挖矿记录
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="transactions" className="flex items-center gap-2">
|
||||
<Receipt className="h-4 w-4" />
|
||||
交易记录
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
{/* 挖矿记录 Tab */}
|
||||
<TabsContent value="mining">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg flex items-center gap-2">
|
||||
挖矿记录
|
||||
{miningRecords && (
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
共 {miningRecords.total} 条
|
||||
</Badge>
|
||||
)}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="p-0">
|
||||
{miningError ? (
|
||||
<Alert variant="destructive" className="m-4">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertDescription>加载挖矿记录失败</AlertDescription>
|
||||
</Alert>
|
||||
) : (
|
||||
<>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>挖矿时间</TableHead>
|
||||
<TableHead className="text-right">算力占比</TableHead>
|
||||
<TableHead className="text-right">全网算力</TableHead>
|
||||
<TableHead className="text-right">每秒分配量</TableHead>
|
||||
<TableHead className="text-right">挖得数量</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{miningLoading ? (
|
||||
[...Array(5)].map((_, i) => (
|
||||
<TableRow key={i}>
|
||||
{[...Array(5)].map((_, j) => (
|
||||
<TableCell key={j}>
|
||||
<Skeleton className="h-4 w-full" />
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))
|
||||
) : !miningRecords?.records.length ? (
|
||||
<TableRow>
|
||||
<TableCell
|
||||
colSpan={5}
|
||||
className="text-center text-muted-foreground py-8"
|
||||
>
|
||||
暂无挖矿记录
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
miningRecords.records.map((record) => (
|
||||
<TableRow key={record.id}>
|
||||
<TableCell>
|
||||
{format(
|
||||
new Date(record.miningMinute),
|
||||
'yyyy-MM-dd HH:mm',
|
||||
{ locale: zhCN }
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className="text-right font-mono">
|
||||
{(Number(record.contributionRatio) * 100).toFixed(6)}%
|
||||
</TableCell>
|
||||
<TableCell className="text-right font-mono">
|
||||
{formatDecimal(record.totalContribution, 2)}
|
||||
</TableCell>
|
||||
<TableCell className="text-right font-mono">
|
||||
{formatDecimal(record.secondDistribution, 8)}
|
||||
</TableCell>
|
||||
<TableCell className="text-right font-mono text-green-600">
|
||||
+{formatDecimal(record.minedAmount, 8)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
|
||||
{/* 分页 */}
|
||||
{miningTotalPages > 1 && (
|
||||
<div className="flex items-center justify-center gap-2 py-4">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setMiningPage((p) => Math.max(1, p - 1))}
|
||||
disabled={miningPage <= 1}
|
||||
>
|
||||
<ChevronLeft className="h-4 w-4" />
|
||||
上一页
|
||||
</Button>
|
||||
<span className="text-sm text-muted-foreground px-4">
|
||||
第 {miningPage} / {miningTotalPages} 页
|
||||
</span>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() =>
|
||||
setMiningPage((p) => Math.min(miningTotalPages, p + 1))
|
||||
}
|
||||
disabled={miningPage >= miningTotalPages}
|
||||
>
|
||||
下一页
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
{/* 交易记录 Tab */}
|
||||
<TabsContent value="transactions">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg flex items-center gap-2">
|
||||
交易记录
|
||||
{transactions && (
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
共 {transactions.total} 条
|
||||
</Badge>
|
||||
)}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="p-0">
|
||||
{transactionsError ? (
|
||||
<Alert variant="destructive" className="m-4">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertDescription>加载交易记录失败</AlertDescription>
|
||||
</Alert>
|
||||
) : (
|
||||
<>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>时间</TableHead>
|
||||
<TableHead>类型</TableHead>
|
||||
<TableHead className="text-right">金额</TableHead>
|
||||
<TableHead className="text-right">交易前余额</TableHead>
|
||||
<TableHead className="text-right">交易后余额</TableHead>
|
||||
<TableHead>备注</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{transactionsLoading ? (
|
||||
[...Array(5)].map((_, i) => (
|
||||
<TableRow key={i}>
|
||||
{[...Array(6)].map((_, j) => (
|
||||
<TableCell key={j}>
|
||||
<Skeleton className="h-4 w-full" />
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))
|
||||
) : !transactions?.transactions.length ? (
|
||||
<TableRow>
|
||||
<TableCell
|
||||
colSpan={6}
|
||||
className="text-center text-muted-foreground py-8"
|
||||
>
|
||||
暂无交易记录
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
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 (
|
||||
<TableRow key={tx.id}>
|
||||
<TableCell>
|
||||
{format(
|
||||
new Date(tx.createdAt),
|
||||
'yyyy-MM-dd HH:mm:ss',
|
||||
{ locale: zhCN }
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge className={`${typeInfo.color} text-xs`}>
|
||||
{typeInfo.label}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell
|
||||
className={`text-right font-mono ${
|
||||
isPositive ? 'text-green-600' : 'text-red-600'
|
||||
}`}
|
||||
>
|
||||
{isPositive ? '+' : ''}
|
||||
{formatDecimal(tx.amount, 8)}
|
||||
</TableCell>
|
||||
<TableCell className="text-right font-mono">
|
||||
{formatDecimal(tx.balanceBefore, 8)}
|
||||
</TableCell>
|
||||
<TableCell className="text-right font-mono">
|
||||
{formatDecimal(tx.balanceAfter, 8)}
|
||||
</TableCell>
|
||||
<TableCell className="text-muted-foreground text-sm max-w-[200px] truncate">
|
||||
{tx.memo || '-'}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
|
||||
{/* 分页 */}
|
||||
{transactionTotalPages > 1 && (
|
||||
<div className="flex items-center justify-center gap-2 py-4">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() =>
|
||||
setTransactionPage((p) => Math.max(1, p - 1))
|
||||
}
|
||||
disabled={transactionPage <= 1}
|
||||
>
|
||||
<ChevronLeft className="h-4 w-4" />
|
||||
上一页
|
||||
</Button>
|
||||
<span className="text-sm text-muted-foreground px-4">
|
||||
第 {transactionPage} / {transactionTotalPages} 页
|
||||
</span>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() =>
|
||||
setTransactionPage((p) =>
|
||||
Math.min(transactionTotalPages, p + 1)
|
||||
)
|
||||
}
|
||||
disabled={transactionPage >= transactionTotalPages}
|
||||
>
|
||||
下一页
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -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<SystemMiningRecordsResponse> => {
|
||||
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<SystemTransactionsResponse> => {
|
||||
const response = await apiClient.get(
|
||||
`/system-accounts/${accountType}/transactions`,
|
||||
{ params: { page, pageSize } }
|
||||
);
|
||||
return response.data.data;
|
||||
},
|
||||
};
|
||||
|
||||
// Helper to categorize accounts for display
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
|
|
@ -45,19 +58,22 @@ export function AccountsTable({
|
|||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="w-[200px]">账户类型</TableHead>
|
||||
<TableHead className="w-[180px]">账户类型</TableHead>
|
||||
<TableHead>账户名称</TableHead>
|
||||
<TableHead className="text-right">余额/算力</TableHead>
|
||||
<TableHead className="text-right">算力</TableHead>
|
||||
{showMiningData && (
|
||||
<TableHead className="text-right">已挖积分股</TableHead>
|
||||
)}
|
||||
<TableHead>来源</TableHead>
|
||||
{showSyncInfo && <TableHead>同步时间</TableHead>}
|
||||
<TableHead>描述</TableHead>
|
||||
<TableHead className="w-[100px]">操作</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{isLoading ? (
|
||||
[...Array(3)].map((_, i) => (
|
||||
<TableRow key={i}>
|
||||
{[...Array(showSyncInfo ? 6 : 5)].map((_, j) => (
|
||||
{[...Array(getColumnCount())].map((_, j) => (
|
||||
<TableCell key={j}>
|
||||
<Skeleton className="h-4 w-full" />
|
||||
</TableCell>
|
||||
|
|
@ -67,7 +83,7 @@ export function AccountsTable({
|
|||
) : accounts.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell
|
||||
colSpan={showSyncInfo ? 6 : 5}
|
||||
colSpan={getColumnCount()}
|
||||
className="text-center text-muted-foreground py-8"
|
||||
>
|
||||
暂无数据
|
||||
|
|
@ -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 (
|
||||
<TableRow key={account.accountType}>
|
||||
|
|
@ -94,13 +111,20 @@ export function AccountsTable({
|
|||
{account.name || displayInfo.label}
|
||||
</TableCell>
|
||||
<TableCell className="text-right font-mono">
|
||||
{formatDecimal(balance, 8)}
|
||||
{formatDecimal(contribution, 8)}
|
||||
{account.contributionNeverExpires && (
|
||||
<Badge variant="outline" className="ml-2 text-xs">
|
||||
永久
|
||||
</Badge>
|
||||
)}
|
||||
</TableCell>
|
||||
{showMiningData && (
|
||||
<TableCell className="text-right font-mono">
|
||||
<span className={Number(totalMined) > 0 ? 'text-green-600' : 'text-muted-foreground'}>
|
||||
{formatDecimal(totalMined, 8)}
|
||||
</span>
|
||||
</TableCell>
|
||||
)}
|
||||
<TableCell>
|
||||
<Badge
|
||||
variant={account.source === 'synced' ? 'default' : 'secondary'}
|
||||
|
|
@ -119,8 +143,13 @@ export function AccountsTable({
|
|||
: '-'}
|
||||
</TableCell>
|
||||
)}
|
||||
<TableCell className="text-muted-foreground text-sm max-w-[200px] truncate">
|
||||
{account.description || displayInfo.description}
|
||||
<TableCell>
|
||||
<Link href={`/system-accounts/${account.accountType}`}>
|
||||
<Button variant="ghost" size="sm" className="h-8 px-2">
|
||||
<FileText className="h-4 w-4 mr-1" />
|
||||
分类账
|
||||
</Button>
|
||||
</Link>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in New Issue