285 lines
9.8 KiB
TypeScript
285 lines
9.8 KiB
TypeScript
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',
|
||
};
|
||
}
|
||
}
|