feat(asset): aggregate mining and trading account balances in asset display

- Modify AssetService to fetch mining account balance from mining-service
- Sum mining balance + trading balance for total share display
- Add miningShareBalance and tradingShareBalance fields to AssetDisplay
- Update frontend entity and model to support new fields

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
hailin 2026-01-15 19:53:31 -08:00
parent ed715111ae
commit 1e2d8d1df7
3 changed files with 118 additions and 24 deletions

View File

@ -1,4 +1,5 @@
import { Injectable, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { TradingCalculatorService } from '../../domain/services/trading-calculator.service';
import { TradingAccountRepository } from '../../infrastructure/persistence/repositories/trading-account.repository';
import { BlackHoleRepository } from '../../infrastructure/persistence/repositories/black-hole.repository';
@ -8,16 +9,31 @@ import { PriceService } from './price.service';
import { Money } from '../../domain/value-objects/money.vo';
import Decimal from 'decimal.js';
// mining-service 返回的账户数据
interface MiningAccountDto {
accountSequence: string;
totalMined: string;
availableBalance: string;
frozenBalance: string;
totalBalance: string;
totalContribution: string;
lastSyncedAt: string | null;
}
export interface AssetDisplay {
// 账户积分股余额
// 挖矿账户积分股余额
miningShareBalance: string;
// 交易账户积分股余额
tradingShareBalance: string;
// 总积分股余额(挖矿 + 交易)
shareBalance: string;
// 账户现金余额
// 账户现金余额(交易账户)
cashBalance: string;
// 冻结积分股
// 冻结积分股(挖矿冻结 + 交易冻结)
frozenShares: string;
// 冻结现金
frozenCash: string;
// 可用积分股
// 可用积分股(挖矿可用 + 交易可用)
availableShares: string;
// 可用现金
availableCash: string;
@ -41,6 +57,7 @@ export interface AssetDisplay {
export class AssetService {
private readonly logger = new Logger(AssetService.name);
private readonly calculator = new TradingCalculatorService();
private readonly miningServiceUrl: string;
constructor(
private readonly tradingAccountRepository: TradingAccountRepository,
@ -48,11 +65,17 @@ export class AssetService {
private readonly circulationPoolRepository: CirculationPoolRepository,
private readonly sharePoolRepository: SharePoolRepository,
private readonly priceService: PriceService,
) {}
private readonly configService: ConfigService,
) {
this.miningServiceUrl = this.configService.get<string>(
'MINING_SERVICE_URL',
'http://localhost:3021',
);
}
/**
*
* = ( + × ) ×
* +
* = ( + × ) ×
*
* @param accountSequence
* @param dailyAllocation
@ -61,8 +84,14 @@ export class AssetService {
accountSequence: string,
dailyAllocation?: string,
): Promise<AssetDisplay | null> {
const account = await this.tradingAccountRepository.findByAccountSequence(accountSequence);
if (!account) {
// 并行获取交易账户和挖矿账户数据
const [tradingAccount, miningAccount] = await Promise.all([
this.tradingAccountRepository.findByAccountSequence(accountSequence),
this.getMiningAccountBalance(accountSequence),
]);
// 如果两个账户都不存在,返回 null
if (!tradingAccount && !miningAccount) {
return null;
}
@ -71,13 +100,32 @@ export class AssetService {
const price = new Money(priceInfo.price);
const burnMultiplier = new Decimal(priceInfo.burnMultiplier);
// 计算有效积分股 = 余额 × (1 + 倍数)
const multiplierFactor = new Decimal(1).plus(burnMultiplier);
const effectiveShares = account.shareBalance.value.times(multiplierFactor);
// 挖矿账户余额
const miningShareBalance = new Money(miningAccount?.availableBalance || '0');
const miningFrozenShares = new Money(miningAccount?.frozenBalance || '0');
// 计算资产显示值
// 交易账户余额
const tradingShareBalance = tradingAccount?.shareBalance || Money.zero();
const tradingFrozenShares = tradingAccount?.frozenShares || Money.zero();
const cashBalance = tradingAccount?.cashBalance || Money.zero();
const frozenCash = tradingAccount?.frozenCash || Money.zero();
const totalBought = tradingAccount?.totalBought || Money.zero();
const totalSold = tradingAccount?.totalSold || Money.zero();
// 汇总余额
const totalShareBalance = miningShareBalance.add(tradingShareBalance);
const totalFrozenShares = miningFrozenShares.add(tradingFrozenShares);
const totalAvailableShares = miningShareBalance.add(
tradingAccount?.availableShares || Money.zero(),
);
// 计算有效积分股 = 总余额 × (1 + 倍数)
const multiplierFactor = new Decimal(1).plus(burnMultiplier);
const effectiveShares = totalShareBalance.value.times(multiplierFactor);
// 计算资产显示值(使用汇总后的余额)
const displayAssetValue = this.calculator.calculateDisplayAssetValue(
account.shareBalance,
totalShareBalance,
burnMultiplier,
price,
);
@ -90,22 +138,51 @@ export class AssetService {
}
return {
shareBalance: account.shareBalance.toFixed(8),
cashBalance: account.cashBalance.toFixed(8),
frozenShares: account.frozenShares.toFixed(8),
frozenCash: account.frozenCash.toFixed(8),
availableShares: account.availableShares.toFixed(8),
availableCash: account.availableCash.toFixed(8),
miningShareBalance: miningShareBalance.toFixed(8),
tradingShareBalance: tradingShareBalance.toFixed(8),
shareBalance: totalShareBalance.toFixed(8),
cashBalance: cashBalance.toFixed(8),
frozenShares: totalFrozenShares.toFixed(8),
frozenCash: frozenCash.toFixed(8),
availableShares: totalAvailableShares.toFixed(8),
availableCash: (tradingAccount?.availableCash || Money.zero()).toFixed(8),
currentPrice: price.toFixed(18),
burnMultiplier: burnMultiplier.toFixed(18),
effectiveShares: new Money(effectiveShares).toFixed(8),
displayAssetValue: displayAssetValue.toFixed(8),
assetGrowthPerSecond: assetGrowthPerSecond.toFixed(18),
totalBought: account.totalBought.toFixed(8),
totalSold: account.totalSold.toFixed(8),
totalBought: totalBought.toFixed(8),
totalSold: totalSold.toFixed(8),
};
}
/**
* mining-service
*/
private async getMiningAccountBalance(
accountSequence: string,
): Promise<MiningAccountDto | null> {
try {
const response = await fetch(
`${this.miningServiceUrl}/api/v2/mining/accounts/${accountSequence}`,
);
if (!response.ok) {
return null;
}
const result = await response.json();
if (!result.success || !result.data) {
return null;
}
return result.data as MiningAccountDto;
} catch (error) {
this.logger.warn(`Failed to fetch mining account ${accountSequence}: ${error}`);
return null;
}
}
/**
*
* = ÷ 24 ÷ 60 ÷ 60

View File

@ -2,6 +2,8 @@ import '../../domain/entities/asset_display.dart';
class AssetDisplayModel extends AssetDisplay {
const AssetDisplayModel({
required super.miningShareBalance,
required super.tradingShareBalance,
required super.shareBalance,
required super.cashBalance,
required super.frozenShares,
@ -19,6 +21,8 @@ class AssetDisplayModel extends AssetDisplay {
factory AssetDisplayModel.fromJson(Map<String, dynamic> json) {
return AssetDisplayModel(
miningShareBalance: json['miningShareBalance']?.toString() ?? '0',
tradingShareBalance: json['tradingShareBalance']?.toString() ?? '0',
shareBalance: json['shareBalance']?.toString() ?? '0',
cashBalance: json['cashBalance']?.toString() ?? '0',
frozenShares: json['frozenShares']?.toString() ?? '0',
@ -37,6 +41,8 @@ class AssetDisplayModel extends AssetDisplay {
Map<String, dynamic> toJson() {
return {
'miningShareBalance': miningShareBalance,
'tradingShareBalance': tradingShareBalance,
'shareBalance': shareBalance,
'cashBalance': cashBalance,
'frozenShares': frozenShares,

View File

@ -2,11 +2,18 @@ import 'package:equatable/equatable.dart';
///
/// trading-service /asset/my /asset/account/:accountSequence
///
class AssetDisplay extends Equatable {
///
///
final String miningShareBalance;
///
final String tradingShareBalance;
/// +
final String shareBalance;
///
///
final String cashBalance;
///
@ -43,6 +50,8 @@ class AssetDisplay extends Equatable {
final String totalSold;
const AssetDisplay({
required this.miningShareBalance,
required this.tradingShareBalance,
required this.shareBalance,
required this.cashBalance,
required this.frozenShares,
@ -74,6 +83,8 @@ class AssetDisplay extends Equatable {
@override
List<Object?> get props => [
miningShareBalance,
tradingShareBalance,
shareBalance,
cashBalance,
frozenShares,