import { Controller, Get, Post, Param, Query, Body, NotFoundException, BadRequestException } from '@nestjs/common'; import { ApiTags, ApiOperation, ApiResponse, ApiParam, ApiQuery } from '@nestjs/swagger'; import { IsString, IsOptional } from 'class-validator'; import { GetMiningAccountQuery } from '../../application/queries/get-mining-account.query'; import { GetMiningStatsQuery } from '../../application/queries/get-mining-stats.query'; import { Public } from '../../shared/guards/jwt-auth.guard'; import { PrismaService } from '../../infrastructure/persistence/prisma/prisma.service'; import { Decimal } from '@prisma/client/runtime/library'; class TransferDto { @IsString() amount: string; @IsString() @IsOptional() transferNo?: string; } @ApiTags('Mining') @Controller('mining') @Public() // 服务间调用,不需要认证 export class MiningController { constructor( private readonly getAccountQuery: GetMiningAccountQuery, private readonly getStatsQuery: GetMiningStatsQuery, private readonly prisma: PrismaService, ) {} @Get('stats') @ApiOperation({ summary: '获取挖矿统计数据' }) @ApiResponse({ status: 200, description: '挖矿统计' }) async getStats() { return this.getStatsQuery.execute(); } @Get('progress') @ApiOperation({ summary: '获取挖矿进度状态(类似销毁进度)' }) @ApiResponse({ status: 200, description: '挖矿进度状态' }) async getMiningProgress() { const config = await this.prisma.miningConfig.findFirst(); if (!config) { return { totalDistributed: '0', distributionPool: '0', remainingDistribution: '0', miningProgress: '0', minuteDistribution: '0', remainingMinutes: 0, lastMiningMinute: null, isActive: false, currentEra: 0, }; } // 查询用户挖矿账户的总已挖数量 const userMiningStats = await this.prisma.miningAccount.aggregate({ _sum: { totalMined: true }, }); // 查询系统账户的总已挖数量 const systemMiningStats = await this.prisma.systemMiningAccount.aggregate({ _sum: { totalMined: true }, }); // 总已分配 = 用户已挖 + 系统已挖 const totalDistributedDecimal = Number(userMiningStats._sum.totalMined || 0) + Number(systemMiningStats._sum.totalMined || 0); // 查询最后分配时间 const lastMinuteStat = await this.prisma.minuteMiningStat.findFirst({ orderBy: { minute: 'desc' }, select: { minute: true }, }); // 计算每分钟分配量(每秒 × 60) const secondDistribution = Number(config.secondDistribution || 0); const minuteDistribution = secondDistribution * 60; // 计算挖矿进度(用实际已分配数量 / 分配池总量) const distributionPool = Number(config.distributionPool || 0); // 剩余分配池 = 总池 - 实际已分配(保持数据一致性) const remainingDistribution = Math.max(0, distributionPool - totalDistributedDecimal); const miningProgress = distributionPool > 0 ? (totalDistributedDecimal / distributionPool) * 100 : 0; // 计算剩余分钟数 const remainingMinutes = minuteDistribution > 0 ? Math.ceil(remainingDistribution / minuteDistribution) : 0; return { totalDistributed: totalDistributedDecimal.toFixed(8), distributionPool: distributionPool.toFixed(8), remainingDistribution: remainingDistribution.toFixed(8), miningProgress: miningProgress.toFixed(4), minuteDistribution: minuteDistribution.toFixed(8), remainingMinutes, lastMiningMinute: lastMinuteStat?.minute || null, isActive: config.isActive, currentEra: config.currentEra, }; } @Get('ranking') @ApiOperation({ summary: '获取挖矿排行榜' }) @ApiQuery({ name: 'limit', required: false, description: '返回数量限制', type: Number }) @ApiResponse({ status: 200, description: '挖矿排行榜' }) async getRanking(@Query('limit') limit?: number) { const ranking = await this.getStatsQuery.getRanking(limit ?? 100); return { data: ranking }; } @Get('accounts/:accountSequence') @ApiOperation({ summary: '获取挖矿账户信息' }) @ApiParam({ name: 'accountSequence', description: '账户序号' }) @ApiResponse({ status: 200, description: '挖矿账户信息' }) @ApiResponse({ status: 404, description: '账户不存在' }) async getAccount(@Param('accountSequence') accountSequence: string) { const account = await this.getAccountQuery.execute(accountSequence); if (!account) { throw new NotFoundException(`Account ${accountSequence} not found`); } return account; } @Get('accounts/:accountSequence/records') @ApiOperation({ summary: '获取挖矿记录' }) @ApiParam({ name: 'accountSequence', description: '账户序号' }) @ApiQuery({ name: 'page', required: false, type: Number }) @ApiQuery({ name: 'pageSize', required: false, type: Number }) @ApiResponse({ status: 200, description: '挖矿记录列表' }) async getRecords( @Param('accountSequence') accountSequence: string, @Query('page') page?: number, @Query('pageSize') pageSize?: number, ) { return this.getAccountQuery.getMiningRecords( accountSequence, page ?? 1, pageSize ?? 50, ); } @Get('accounts/:accountSequence/transactions') @ApiOperation({ summary: '获取交易流水' }) @ApiParam({ name: 'accountSequence', description: '账户序号' }) @ApiQuery({ name: 'page', required: false, type: Number }) @ApiQuery({ name: 'pageSize', required: false, type: Number }) @ApiResponse({ status: 200, description: '交易流水列表' }) async getTransactions( @Param('accountSequence') accountSequence: string, @Query('page') page?: number, @Query('pageSize') pageSize?: number, ) { return this.getAccountQuery.getTransactions( accountSequence, page ?? 1, pageSize ?? 50, ); } @Post('accounts/:accountSequence/transfer-out') @ApiOperation({ summary: '从挖矿账户划出积分股(到交易账户)' }) @ApiParam({ name: 'accountSequence', description: '账户序号' }) @ApiResponse({ status: 200, description: '划出成功' }) @ApiResponse({ status: 400, description: '余额不足或参数错误' }) async transferOut( @Param('accountSequence') accountSequence: string, @Body() dto: TransferDto, ) { const amount = new Decimal(dto.amount); if (amount.lessThanOrEqualTo(0)) { throw new BadRequestException('Amount must be positive'); } // 查找账户 const account = await this.prisma.miningAccount.findUnique({ where: { accountSequence }, }); if (!account) { throw new NotFoundException(`Account ${accountSequence} not found`); } // 检查余额 if (new Decimal(account.availableBalance).lessThan(amount)) { throw new BadRequestException('Insufficient balance'); } // 执行划转(使用事务) const txId = `TX${Date.now().toString(36)}${Math.random().toString(36).substring(2, 8)}`.toUpperCase(); await this.prisma.$transaction(async (tx) => { // 扣减余额 const newBalance = new Decimal(account.availableBalance).minus(amount); await tx.miningAccount.update({ where: { accountSequence }, data: { availableBalance: newBalance }, }); // 创建交易记录 await tx.miningTransaction.create({ data: { accountSequence, type: 'TRANSFER_OUT', amount: amount.negated(), balanceBefore: account.availableBalance, balanceAfter: newBalance, referenceId: dto.transferNo || txId, referenceType: 'TRADING_TRANSFER', memo: `划转到交易账户,金额: ${amount.toString()}`, }, }); }); return { success: true, txId, message: 'Transfer out successful', }; } @Post('accounts/:accountSequence/transfer-in') @ApiOperation({ summary: '划入积分股到挖矿账户(从交易账户)' }) @ApiParam({ name: 'accountSequence', description: '账户序号' }) @ApiResponse({ status: 200, description: '划入成功' }) @ApiResponse({ status: 400, description: '参数错误' }) async transferIn( @Param('accountSequence') accountSequence: string, @Body() dto: TransferDto, ) { const amount = new Decimal(dto.amount); if (amount.lessThanOrEqualTo(0)) { throw new BadRequestException('Amount must be positive'); } const txId = `TX${Date.now().toString(36)}${Math.random().toString(36).substring(2, 8)}`.toUpperCase(); await this.prisma.$transaction(async (tx) => { // 查找或创建账户 let account = await tx.miningAccount.findUnique({ where: { accountSequence }, }); if (!account) { // 创建账户 account = await tx.miningAccount.create({ data: { accountSequence, totalMined: new Decimal(0), availableBalance: new Decimal(0), frozenBalance: new Decimal(0), totalContribution: new Decimal(0), }, }); } // 增加余额 const newBalance = new Decimal(account.availableBalance).plus(amount); await tx.miningAccount.update({ where: { accountSequence }, data: { availableBalance: newBalance }, }); // 创建交易记录 await tx.miningTransaction.create({ data: { accountSequence, type: 'TRANSFER_IN', amount, balanceBefore: account.availableBalance, balanceAfter: newBalance, referenceId: dto.transferNo || txId, referenceType: 'TRADING_TRANSFER', memo: `从交易账户划入,金额: ${amount.toString()}`, }, }); }); return { success: true, txId, message: 'Transfer in successful', }; } }