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 { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth, ApiQuery } from '@nestjs/swagger';
import { IsString } from 'class-validator'; import { IsString } from 'class-validator';
import { TransferService } from '../../application/services/transfer.service'; import { TransferService } from '../../application/services/transfer.service';
@ -19,7 +19,7 @@ export class TransferController {
async transferIn(@Body() dto: TransferDto, @Req() req: any) { async transferIn(@Body() dto: TransferDto, @Req() req: any) {
const accountSequence = req.user?.accountSequence; const accountSequence = req.user?.accountSequence;
if (!accountSequence) { if (!accountSequence) {
throw new Error('Unauthorized'); throw new UnauthorizedException('未登录');
} }
return this.transferService.transferIn(accountSequence, dto.amount); return this.transferService.transferIn(accountSequence, dto.amount);
@ -30,7 +30,7 @@ export class TransferController {
async transferOut(@Body() dto: TransferDto, @Req() req: any) { async transferOut(@Body() dto: TransferDto, @Req() req: any) {
const accountSequence = req.user?.accountSequence; const accountSequence = req.user?.accountSequence;
if (!accountSequence) { if (!accountSequence) {
throw new Error('Unauthorized'); throw new UnauthorizedException('未登录');
} }
return this.transferService.transferOut(accountSequence, dto.amount); return this.transferService.transferOut(accountSequence, dto.amount);
@ -47,7 +47,7 @@ export class TransferController {
) { ) {
const accountSequence = req.user?.accountSequence; const accountSequence = req.user?.accountSequence;
if (!accountSequence) { if (!accountSequence) {
throw new Error('Unauthorized'); throw new UnauthorizedException('未登录');
} }
return this.transferService.getTransferHistory(accountSequence, page ?? 1, pageSize ?? 50); 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 { ConfigService } from '@nestjs/config';
import { TradingAccountRepository } from '../../infrastructure/persistence/repositories/trading-account.repository'; import { TradingAccountRepository } from '../../infrastructure/persistence/repositories/trading-account.repository';
import { PrismaService } from '../../infrastructure/persistence/prisma/prisma.service'; import { PrismaService } from '../../infrastructure/persistence/prisma/prisma.service';
@ -27,7 +27,7 @@ export class TransferService {
const transferAmount = new Money(amount); const transferAmount = new Money(amount);
if (transferAmount.value.lessThan(this.minTransferAmount)) { if (transferAmount.value.lessThan(this.minTransferAmount)) {
throw new Error(`Minimum transfer amount is ${this.minTransferAmount}`); throw new BadRequestException(`最低划转数量为 ${this.minTransferAmount}`);
} }
const transferNo = this.generateTransferNo(); const transferNo = this.generateTransferNo();
@ -48,7 +48,7 @@ export class TransferService {
const response = await this.callMiningServiceTransferOut(accountSequence, amount, transferNo); const response = await this.callMiningServiceTransferOut(accountSequence, amount, transferNo);
if (!response.success) { 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); const transferAmount = new Money(amount);
if (transferAmount.value.lessThan(this.minTransferAmount)) { 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); const account = await this.accountRepository.findByAccountSequence(accountSequence);
if (!account) { if (!account) {
throw new Error('Trading account not found'); throw new BadRequestException('交易账户不存在');
} }
if (account.availableShares.isLessThan(transferAmount)) { if (account.availableShares.isLessThan(transferAmount)) {
throw new Error('Insufficient available shares'); throw new BadRequestException('可用积分股不足(部分积分股可能已被 C2C 订单冻结)');
} }
const transferNo = this.generateTransferNo(); const transferNo = this.generateTransferNo();
@ -131,7 +131,7 @@ export class TransferService {
// 回滚交易账户余额 // 回滚交易账户余额
account.transferSharesIn(transferAmount, `${transferNo}_rollback`); account.transferSharesIn(transferAmount, `${transferNo}_rollback`);
await this.accountRepository.save(account); 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, 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 { String get totalShareBalance {
final available = double.tryParse(availableShares) ?? 0; final available = double.tryParse(availableShares) ?? 0;

View File

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