From 995dfa898e506352a9e5096727970c4eb30629d1 Mon Sep 17 00:00:00 2001 From: hailin Date: Tue, 20 Jan 2026 05:24:23 -0800 Subject: [PATCH] =?UTF-8?q?feat(trading):=20=E6=B7=BB=E5=8A=A0=E6=B6=A8?= =?UTF-8?q?=E8=B7=8C=E5=B9=85=E6=98=BE=E7=A4=BA=E5=8F=8A=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E6=88=90=E4=BA=A4=E6=98=8E=E7=BB=86=E6=95=B0=E6=8D=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 后端: - 添加 getFirstSnapshot() 获取上线首日价格 - PriceInfo 接口增加 priceChangePercent 和 initialPrice 字段 - 计算涨跌幅 = (当前价格 - 首日价格) / 首日价格 × 100% - 修复 originalQuantity 为0时的数据计算逻辑 2. 前端: - 交易页面涨跌幅移到价格下方单独显示 - 添加"较上线首日"说明文字 - 根据涨跌正负显示不同颜色和图标 Co-Authored-By: Claude Opus 4.5 --- .../src/application/services/price.service.ts | 28 +++++++++- .../repositories/order.repository.ts | 16 ++++-- .../repositories/price-snapshot.repository.ts | 13 +++++ .../lib/core/router/app_router.dart | 4 +- .../lib/data/models/price_info_model.dart | 4 ++ .../lib/domain/entities/market_overview.dart | 2 +- .../lib/domain/entities/price_info.dart | 10 +++- .../pages/splash/splash_page.dart | 2 +- .../pages/trading/trading_page.dart | 52 ++++++++++++++----- 9 files changed, 108 insertions(+), 23 deletions(-) diff --git a/backend/services/trading-service/src/application/services/price.service.ts b/backend/services/trading-service/src/application/services/price.service.ts index 395bc749..2f8ab934 100644 --- a/backend/services/trading-service/src/application/services/price.service.ts +++ b/backend/services/trading-service/src/application/services/price.service.ts @@ -17,6 +17,8 @@ export interface PriceInfo { burnMultiplier: string; minuteBurnRate: string; snapshotTime: Date; + priceChangePercent: string; // 较上线首日涨跌幅 + initialPrice: string; // 上线首日价格 } @Injectable() @@ -36,11 +38,12 @@ export class PriceService { * 获取当前价格信息 */ async getCurrentPrice(): Promise { - const [sharePool, blackHole, circulationPool, config] = await Promise.all([ + const [sharePool, blackHole, circulationPool, config, firstSnapshot] = await Promise.all([ this.sharePoolRepository.getPool(), this.blackHoleRepository.getBlackHole(), this.circulationPoolRepository.getPool(), this.tradingConfigRepository.getConfig(), + this.priceSnapshotRepository.getFirstSnapshot(), ]); const greenPoints = sharePool?.greenPoints || Money.zero(); @@ -65,6 +68,13 @@ export class PriceService { // 获取当前每分钟销毁率 const minuteBurnRate = config?.minuteBurnRate || Money.zero(); + // 计算较上线首日的涨跌幅 + const initialPrice = firstSnapshot?.price || price; + let priceChangePercent = new Decimal(0); + if (!initialPrice.isZero()) { + priceChangePercent = price.value.minus(initialPrice.value).dividedBy(initialPrice.value).times(100); + } + return { price: price.toFixed(18), greenPoints: greenPoints.toFixed(8), @@ -74,6 +84,8 @@ export class PriceService { burnMultiplier: burnMultiplier.toFixed(18), minuteBurnRate: minuteBurnRate.toFixed(18), snapshotTime: new Date(), + priceChangePercent: priceChangePercent.toFixed(2), + initialPrice: initialPrice.toFixed(18), }; } @@ -208,13 +220,23 @@ export class PriceService { * 获取最新价格快照 */ async getLatestSnapshot(): Promise { - const snapshot = await this.priceSnapshotRepository.getLatestSnapshot(); + const [snapshot, firstSnapshot] = await Promise.all([ + this.priceSnapshotRepository.getLatestSnapshot(), + this.priceSnapshotRepository.getFirstSnapshot(), + ]); if (!snapshot) { return null; } const burnMultiplier = await this.getCurrentBurnMultiplier(); + // 计算较上线首日的涨跌幅 + const initialPrice = firstSnapshot?.price || snapshot.price; + let priceChangePercent = new Decimal(0); + if (!initialPrice.isZero()) { + priceChangePercent = snapshot.price.value.minus(initialPrice.value).dividedBy(initialPrice.value).times(100); + } + return { price: snapshot.price.toFixed(18), greenPoints: snapshot.greenPoints.toFixed(8), @@ -224,6 +246,8 @@ export class PriceService { burnMultiplier: burnMultiplier.toFixed(18), minuteBurnRate: snapshot.minuteBurnRate.toFixed(18), snapshotTime: snapshot.snapshotTime, + priceChangePercent: priceChangePercent.toFixed(2), + initialPrice: initialPrice.toFixed(18), }; } diff --git a/backend/services/trading-service/src/infrastructure/persistence/repositories/order.repository.ts b/backend/services/trading-service/src/infrastructure/persistence/repositories/order.repository.ts index 2f3431a8..063343cc 100644 --- a/backend/services/trading-service/src/infrastructure/persistence/repositories/order.repository.ts +++ b/backend/services/trading-service/src/infrastructure/persistence/repositories/order.repository.ts @@ -279,9 +279,17 @@ export class OrderRepository { }; }), ...sellerTrades.map((t) => { - // 卖方:计算销毁倍数 = 有效积分股 / 原始数量 - const effectiveQty = Number(t.quantity); - const originalQty = Number(t.originalQuantity || t.quantity); + // 卖方:计算销毁倍数和原始数量 + const effectiveQty = Number(t.quantity); // 有效积分股(含销毁倍数) + const burnQty = Number(t.burnQuantity); + // 原始数量:优先使用数据库值,如果为0则通过 有效量 - 销毁量 计算 + // 注意:使用 != null 而不是 || 来正确处理0值 + let originalQty = t.originalQuantity != null && Number(t.originalQuantity) > 0 + ? Number(t.originalQuantity) + : effectiveQty - burnQty; + // 确保原始数量不为负数 + if (originalQty <= 0) originalQty = effectiveQty; + // 销毁倍数 = 有效积分股 / 原始数量 const burnMultiplier = originalQty > 0 ? effectiveQty / originalQty : 1; // 交易总额 = 有效积分股 × 价格 const grossAmount = effectiveQty * Number(t.price); @@ -292,7 +300,7 @@ export class OrderRepository { type: 'SELL' as const, price: t.price.toString(), quantity: t.quantity.toString(), // 有效积分股 - originalQuantity: (t.originalQuantity || t.quantity).toString(), // 原始卖出数量 + originalQuantity: originalQty.toString(), // 原始卖出数量 burnQuantity: t.burnQuantity.toString(), // 销毁数量 burnMultiplier: burnMultiplier.toString(), // 销毁倍数 grossAmount: grossAmount.toString(), // 交易总额 diff --git a/backend/services/trading-service/src/infrastructure/persistence/repositories/price-snapshot.repository.ts b/backend/services/trading-service/src/infrastructure/persistence/repositories/price-snapshot.repository.ts index fc762051..ff4faba3 100644 --- a/backend/services/trading-service/src/infrastructure/persistence/repositories/price-snapshot.repository.ts +++ b/backend/services/trading-service/src/infrastructure/persistence/repositories/price-snapshot.repository.ts @@ -28,6 +28,19 @@ export class PriceSnapshotRepository { return this.toDomain(record); } + /** + * 获取最早的价格快照(上线首日价格) + */ + async getFirstSnapshot(): Promise { + const record = await this.prisma.priceSnapshot.findFirst({ + orderBy: { snapshotTime: 'asc' }, + }); + if (!record) { + return null; + } + return this.toDomain(record); + } + async getSnapshotAt(time: Date): Promise { // 获取指定时间之前最近的快照 const record = await this.prisma.priceSnapshot.findFirst({ diff --git a/frontend/mining-app/lib/core/router/app_router.dart b/frontend/mining-app/lib/core/router/app_router.dart index f28d60e6..e7076c5c 100644 --- a/frontend/mining-app/lib/core/router/app_router.dart +++ b/frontend/mining-app/lib/core/router/app_router.dart @@ -83,9 +83,9 @@ final appRouterProvider = Provider((ref) { return Routes.login; } - // 已登录且访问登录页,重定向到首页 + // 已登录且访问登录页,重定向到兑换页 if (isLoggedIn && currentPath == Routes.login) { - return Routes.contribution; + return Routes.trading; } return null; diff --git a/frontend/mining-app/lib/data/models/price_info_model.dart b/frontend/mining-app/lib/data/models/price_info_model.dart index 15ef550b..bcb8f81e 100644 --- a/frontend/mining-app/lib/data/models/price_info_model.dart +++ b/frontend/mining-app/lib/data/models/price_info_model.dart @@ -10,6 +10,8 @@ class PriceInfoModel extends PriceInfo { required super.burnMultiplier, required super.minuteBurnRate, required super.snapshotTime, + required super.priceChangePercent, + required super.initialPrice, }); factory PriceInfoModel.fromJson(Map json) { @@ -24,6 +26,8 @@ class PriceInfoModel extends PriceInfo { snapshotTime: json['snapshotTime'] != null ? DateTime.parse(json['snapshotTime'].toString()) : DateTime.now(), + priceChangePercent: json['priceChangePercent']?.toString() ?? '0', + initialPrice: json['initialPrice']?.toString() ?? '0', ); } } diff --git a/frontend/mining-app/lib/domain/entities/market_overview.dart b/frontend/mining-app/lib/domain/entities/market_overview.dart index 18dfe2b5..d5a55522 100644 --- a/frontend/mining-app/lib/domain/entities/market_overview.dart +++ b/frontend/mining-app/lib/domain/entities/market_overview.dart @@ -4,7 +4,7 @@ import 'package:equatable/equatable.dart'; class MarketOverview extends Equatable { /// 当前价格 final String price; - /// 积分值池 + /// 积分股池 final String greenPoints; /// 黑洞销毁量 final String blackHoleAmount; diff --git a/frontend/mining-app/lib/domain/entities/price_info.dart b/frontend/mining-app/lib/domain/entities/price_info.dart index a5950406..a6844b03 100644 --- a/frontend/mining-app/lib/domain/entities/price_info.dart +++ b/frontend/mining-app/lib/domain/entities/price_info.dart @@ -4,7 +4,7 @@ import 'package:equatable/equatable.dart'; class PriceInfo extends Equatable { /// 当前价格 final String price; - /// 积分值池 + /// 积分股池 final String greenPoints; /// 黑洞销毁量 final String blackHoleAmount; @@ -18,6 +18,10 @@ class PriceInfo extends Equatable { final String minuteBurnRate; /// 快照时间 final DateTime snapshotTime; + /// 较上线首日涨跌幅(百分比) + final String priceChangePercent; + /// 上线首日价格 + final String initialPrice; const PriceInfo({ required this.price, @@ -28,6 +32,8 @@ class PriceInfo extends Equatable { required this.burnMultiplier, required this.minuteBurnRate, required this.snapshotTime, + required this.priceChangePercent, + required this.initialPrice, }); @override @@ -40,5 +46,7 @@ class PriceInfo extends Equatable { burnMultiplier, minuteBurnRate, snapshotTime, + priceChangePercent, + initialPrice, ]; } diff --git a/frontend/mining-app/lib/presentation/pages/splash/splash_page.dart b/frontend/mining-app/lib/presentation/pages/splash/splash_page.dart index 360ba57e..c61dc553 100644 --- a/frontend/mining-app/lib/presentation/pages/splash/splash_page.dart +++ b/frontend/mining-app/lib/presentation/pages/splash/splash_page.dart @@ -36,7 +36,7 @@ class _SplashPageState extends ConsumerState { // 已登录,直接跳转,不需要主动刷新 token // token 刷新只在 API 返回 401 时才触发 if (mounted) { - context.go(Routes.contribution); + context.go(Routes.trading); } } else { context.go(Routes.login); diff --git a/frontend/mining-app/lib/presentation/pages/trading/trading_page.dart b/frontend/mining-app/lib/presentation/pages/trading/trading_page.dart index 3b7d3b12..a3228600 100644 --- a/frontend/mining-app/lib/presentation/pages/trading/trading_page.dart +++ b/frontend/mining-app/lib/presentation/pages/trading/trading_page.dart @@ -167,7 +167,8 @@ class _TradingPageState extends ConsumerState { ), const SizedBox(height: 8), Row( - crossAxisAlignment: CrossAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.baseline, + textBaseline: TextBaseline.alphabetic, children: [ AmountText( amount: priceInfo != null ? formatPrice(price) : null, @@ -179,31 +180,58 @@ class _TradingPageState extends ConsumerState { letterSpacing: -0.75, ), ), - const SizedBox(width: 8), - Container( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), + const SizedBox(width: 4), + Text( + '积分值', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: AppColors.textSecondaryOf(context), + ), + ), + ], + ), + const SizedBox(height: 8), + Builder( + builder: (context) { + final changePercent = double.tryParse(priceInfo?.priceChangePercent ?? '0') ?? 0; + final isPositive = changePercent >= 0; + final color = isPositive ? _green : _red; + final icon = isPositive ? Icons.trending_up : Icons.trending_down; + final sign = isPositive ? '+' : ''; + return Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration( - color: _green.withValues(alpha: 0.1), + color: color.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(16), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ - const Icon(Icons.trending_up, size: 16, color: _green), + Icon(icon, size: 16, color: color), + const SizedBox(width: 4), + Text( + '较上线首日', + style: TextStyle( + fontSize: 12, + color: AppColors.textSecondaryOf(context), + ), + ), + const SizedBox(width: 4), DataText( - data: isLoading ? null : '+0.00%', + data: isLoading ? null : '$sign${changePercent.toStringAsFixed(2)}%', isLoading: isLoading, placeholder: '+--.--%', - style: const TextStyle( + style: TextStyle( fontSize: 14, fontWeight: FontWeight.bold, - color: _green, + color: color, ), ), ], ), - ), - ], + ); + }, ), ], ), @@ -313,7 +341,7 @@ class _TradingPageState extends ConsumerState { Row( children: [ _buildMarketDataItem( - '积分值池', + '积分股池', market != null ? formatCompact(market.greenPoints) : null, _orange, isLoading,