fix(transfer): 修复积分股划转到分配账户失败问题

后端:
- transfer.service.ts: Error → BadRequestException,业务错误返回HTTP 400而非500
- transfer.controller.ts: Error → UnauthorizedException,正确返回HTTP 401
- 错误信息改为中文:余额不足、账户不存在等提示更明确

前端:
- asset_display.dart: 新增 tradingAvailableShares 计算属性(总可用 - 挖矿可用)
- trading_page.dart: 划转弹窗显示可用余额(扣除冻结)而非总余额
- trading_page.dart: 划转失败时显示后端具体错误信息而非通用提示

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
hailin 2026-01-31 06:24:35 -08:00
parent 3cbb874503
commit 8e63547a3e
4 changed files with 26 additions and 15 deletions

View File

@ -1,4 +1,4 @@
import { Controller, Get, Post, Param, Query, Body, Req } from '@nestjs/common';
import { Controller, Get, Post, Param, Query, Body, Req, UnauthorizedException } from '@nestjs/common';
import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth, ApiQuery } from '@nestjs/swagger';
import { IsString } from 'class-validator';
import { TransferService } from '../../application/services/transfer.service';
@ -19,7 +19,7 @@ export class TransferController {
async transferIn(@Body() dto: TransferDto, @Req() req: any) {
const accountSequence = req.user?.accountSequence;
if (!accountSequence) {
throw new Error('Unauthorized');
throw new UnauthorizedException('未登录');
}
return this.transferService.transferIn(accountSequence, dto.amount);
@ -30,7 +30,7 @@ export class TransferController {
async transferOut(@Body() dto: TransferDto, @Req() req: any) {
const accountSequence = req.user?.accountSequence;
if (!accountSequence) {
throw new Error('Unauthorized');
throw new UnauthorizedException('未登录');
}
return this.transferService.transferOut(accountSequence, dto.amount);
@ -47,7 +47,7 @@ export class TransferController {
) {
const accountSequence = req.user?.accountSequence;
if (!accountSequence) {
throw new Error('Unauthorized');
throw new UnauthorizedException('未登录');
}
return this.transferService.getTransferHistory(accountSequence, page ?? 1, pageSize ?? 50);

View File

@ -1,4 +1,4 @@
import { Injectable, Logger } from '@nestjs/common';
import { Injectable, Logger, BadRequestException } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { TradingAccountRepository } from '../../infrastructure/persistence/repositories/trading-account.repository';
import { PrismaService } from '../../infrastructure/persistence/prisma/prisma.service';
@ -27,7 +27,7 @@ export class TransferService {
const transferAmount = new Money(amount);
if (transferAmount.value.lessThan(this.minTransferAmount)) {
throw new Error(`Minimum transfer amount is ${this.minTransferAmount}`);
throw new BadRequestException(`最低划转数量为 ${this.minTransferAmount}`);
}
const transferNo = this.generateTransferNo();
@ -48,7 +48,7 @@ export class TransferService {
const response = await this.callMiningServiceTransferOut(accountSequence, amount, transferNo);
if (!response.success) {
throw new Error(response.message || 'Mining service transfer failed');
throw new BadRequestException(response.message || '挖矿服务划转失败');
}
// 增加交易账户余额
@ -94,16 +94,16 @@ export class TransferService {
const transferAmount = new Money(amount);
if (transferAmount.value.lessThan(this.minTransferAmount)) {
throw new Error(`Minimum transfer amount is ${this.minTransferAmount}`);
throw new BadRequestException(`最低划转数量为 ${this.minTransferAmount}`);
}
const account = await this.accountRepository.findByAccountSequence(accountSequence);
if (!account) {
throw new Error('Trading account not found');
throw new BadRequestException('交易账户不存在');
}
if (account.availableShares.isLessThan(transferAmount)) {
throw new Error('Insufficient available shares');
throw new BadRequestException('可用积分股不足(部分积分股可能已被 C2C 订单冻结)');
}
const transferNo = this.generateTransferNo();
@ -131,7 +131,7 @@ export class TransferService {
// 回滚交易账户余额
account.transferSharesIn(transferAmount, `${transferNo}_rollback`);
await this.accountRepository.save(account);
throw new Error(response.message || 'Mining service transfer failed');
throw new BadRequestException(response.message || '挖矿服务划转失败');
}
// 更新划转记录

View File

@ -67,6 +67,14 @@ class AssetDisplay extends Equatable {
required this.totalSold,
});
/// = -
String get tradingAvailableShares {
final totalAvailable = double.tryParse(availableShares) ?? 0;
final miningAvailable = double.tryParse(miningShareBalance) ?? 0;
final result = totalAvailable - miningAvailable;
return result < 0 ? '0' : result.toStringAsFixed(8);
}
/// = +
String get totalShareBalance {
final available = double.tryParse(availableShares) ?? 0;

View File

@ -402,8 +402,10 @@ class _TradingPageState extends ConsumerState<TradingPage> {
//
final miningShareBalance = asset?.miningShareBalance ?? '0';
//
//
final tradingShareBalance = asset?.tradingShareBalance ?? '0';
//
final tradingAvailableShares = asset?.tradingAvailableShares ?? '0';
// +
final availableShares = asset?.availableShares ?? '0';
//
@ -612,7 +614,7 @@ class _TradingPageState extends ConsumerState<TradingPage> {
Align(
alignment: Alignment.centerRight,
child: GestureDetector(
onTap: () => _showTransferDialog(miningShareBalance, tradingShareBalance),
onTap: () => _showTransferDialog(miningShareBalance, tradingAvailableShares),
child: const Text(
'划转',
style: TextStyle(
@ -1626,9 +1628,10 @@ class _TransferBottomSheetState extends ConsumerState<_TransferBottomSheet> {
),
);
} else {
final errorMsg = ref.read(tradingNotifierProvider).error ?? '划转失败,请稍后重试';
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('划转失败,请稍后重试'),
SnackBar(
content: Text(errorMsg),
backgroundColor: Colors.red,
),
);