fix: 修复K线图NaN错误并添加mining-service划转端点

- 修复K线图当价格范围为0时导致的NaN Offset错误
- 在mining-service添加transfer-out和transfer-in端点
- 划转操作会在mining_transactions表中记录明细

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
hailin 2026-01-18 07:12:32 -08:00
parent 4e181354f4
commit 2154d5752f
3 changed files with 148 additions and 4 deletions

View File

@ -1,9 +1,20 @@
import { Controller, Get, Param, Query, NotFoundException } from '@nestjs/common';
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')
@ -145,4 +156,129 @@ export class MiningController {
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',
};
}
}

View File

@ -703,11 +703,16 @@ class _KlineChartWidgetState extends State<KlineChartWidget> {
}
final range = maxPrice - minPrice;
final padding = range * 0.1;
// range 0 NaN
final safeRange = range > 0 ? range : (maxPrice > 0 ? maxPrice * 0.1 : 1.0);
final padding = safeRange * 0.1;
minPrice -= padding;
maxPrice += padding;
final y = 10 + ((maxPrice - price) / (maxPrice - minPrice)) * (chartHeight - 20);
final adjustedRange = maxPrice - minPrice;
if (adjustedRange <= 0) return chartHeight / 2;
final y = 10 + ((maxPrice - price) / adjustedRange) * (chartHeight - 20);
return y.clamp(0.0, chartHeight - 20);
}
}

View File

@ -100,13 +100,16 @@ class KlinePainter extends CustomPainter {
//
final priceRange = maxPrice - minPrice;
final padding = priceRange * 0.1;
// priceRange 0 NaN
final safePriceRange = priceRange > 0 ? priceRange : (maxPrice > 0 ? maxPrice * 0.1 : 1.0);
final padding = safePriceRange * 0.1;
minPrice -= padding;
maxPrice += padding;
final adjustedRange = maxPrice - minPrice;
// Y坐标转换函数
double priceToY(double price) {
if (adjustedRange <= 0) return topPadding + chartHeight / 2;
return topPadding + ((maxPrice - price) / adjustedRange) * chartHeight;
}