rwadurian/backend/services/mining-service/src/api/controllers/mining.controller.ts

285 lines
9.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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',
};
}
}