feat(trading): 添加涨跌幅显示及修复成交明细数据

1. 后端:
   - 添加 getFirstSnapshot() 获取上线首日价格
   - PriceInfo 接口增加 priceChangePercent 和 initialPrice 字段
   - 计算涨跌幅 = (当前价格 - 首日价格) / 首日价格 × 100%
   - 修复 originalQuantity 为0时的数据计算逻辑

2. 前端:
   - 交易页面涨跌幅移到价格下方单独显示
   - 添加"较上线首日"说明文字
   - 根据涨跌正负显示不同颜色和图标

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
hailin 2026-01-20 05:24:23 -08:00
parent 7ff7157115
commit 995dfa898e
9 changed files with 108 additions and 23 deletions

View File

@ -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<PriceInfo> {
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<PriceInfo | null> {
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),
};
}

View File

@ -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(), // 交易总额

View File

@ -28,6 +28,19 @@ export class PriceSnapshotRepository {
return this.toDomain(record);
}
/**
* 线
*/
async getFirstSnapshot(): Promise<PriceSnapshotEntity | null> {
const record = await this.prisma.priceSnapshot.findFirst({
orderBy: { snapshotTime: 'asc' },
});
if (!record) {
return null;
}
return this.toDomain(record);
}
async getSnapshotAt(time: Date): Promise<PriceSnapshotEntity | null> {
// 获取指定时间之前最近的快照
const record = await this.prisma.priceSnapshot.findFirst({

View File

@ -83,9 +83,9 @@ final appRouterProvider = Provider<GoRouter>((ref) {
return Routes.login;
}
// 访
// 访
if (isLoggedIn && currentPath == Routes.login) {
return Routes.contribution;
return Routes.trading;
}
return null;

View File

@ -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<String, dynamic> 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',
);
}
}

View File

@ -4,7 +4,7 @@ import 'package:equatable/equatable.dart';
class MarketOverview extends Equatable {
///
final String price;
///
///
final String greenPoints;
///
final String blackHoleAmount;

View File

@ -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,
];
}

View File

@ -36,7 +36,7 @@ class _SplashPageState extends ConsumerState<SplashPage> {
// token
// token API 401
if (mounted) {
context.go(Routes.contribution);
context.go(Routes.trading);
}
} else {
context.go(Routes.login);

View File

@ -167,7 +167,8 @@ class _TradingPageState extends ConsumerState<TradingPage> {
),
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<TradingPage> {
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<TradingPage> {
Row(
children: [
_buildMarketDataItem(
'积分',
'积分',
market != null ? formatCompact(market.greenPoints) : null,
_orange,
isLoading,