From 8728fdce4ce06648002997604547c0f0cfbba7d3 Mon Sep 17 00:00:00 2001 From: hailin Date: Tue, 20 Jan 2026 04:07:52 -0800 Subject: [PATCH] =?UTF-8?q?feat(trading):=20=E6=88=90=E4=BA=A4=E6=98=8E?= =?UTF-8?q?=E7=BB=86=E6=98=BE=E7=A4=BA=E5=AE=8C=E6=95=B4=E5=8D=96=E5=87=BA?= =?UTF-8?q?=E4=BF=A1=E6=81=AF=EF=BC=88=E9=94=80=E6=AF=81=E5=80=8D=E6=95=B0?= =?UTF-8?q?=E3=80=81=E6=9C=89=E6=95=88=E7=A7=AF=E5=88=86=E8=82=A1=E3=80=81?= =?UTF-8?q?=E6=89=8B=E7=BB=AD=E8=B4=B9=E7=AD=89=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 后端Trade表新增originalQuantity字段存储原始卖出数量 - quantity字段改为存储有效积分股(含销毁倍数) - API返回完整明细:销毁倍数、有效积分股、交易总额、进入积分股池 - 前端成交明细页面显示完整卖出信息,类似确认卖出弹窗样式 Co-Authored-By: Claude Opus 4.5 --- .../trading-service/prisma/schema.prisma | 9 +- .../src/application/services/order.service.ts | 4 +- .../repositories/order.repository.ts | 81 +++++++++++------ .../lib/data/models/trade_order_model.dart | 12 ++- .../lib/domain/entities/trade_record.dart | 29 +++++-- .../pages/profile/trading_records_page.dart | 87 +++++++++++-------- 6 files changed, 144 insertions(+), 78 deletions(-) diff --git a/backend/services/trading-service/prisma/schema.prisma b/backend/services/trading-service/prisma/schema.prisma index 79591cd3..9f739f49 100644 --- a/backend/services/trading-service/prisma/schema.prisma +++ b/backend/services/trading-service/prisma/schema.prisma @@ -196,10 +196,11 @@ model Trade { sellOrderId String @map("sell_order_id") buyerSequence String @map("buyer_sequence") sellerSequence String @map("seller_sequence") - price Decimal @db.Decimal(30, 18) - quantity Decimal @db.Decimal(30, 8) // 实际成交量 - burnQuantity Decimal @default(0) @map("burn_quantity") @db.Decimal(30, 8) // 卖出销毁量 - effectiveQty Decimal @default(0) @map("effective_qty") @db.Decimal(30, 8) // 有效量(quantity + burnQuantity) + price Decimal @db.Decimal(30, 18) + quantity Decimal @db.Decimal(30, 8) // 有效积分股(含销毁倍数) + originalQuantity Decimal @default(0) @map("original_quantity") @db.Decimal(30, 8) // 原始卖出数量 + burnQuantity Decimal @default(0) @map("burn_quantity") @db.Decimal(30, 8) // 卖出销毁量 + effectiveQty Decimal @default(0) @map("effective_qty") @db.Decimal(30, 8) // 有效量(保留兼容,同quantity) amount Decimal @db.Decimal(30, 8) // 卖方实际收到金额(扣除手续费后) fee Decimal @default(0) @db.Decimal(30, 8) // 交易手续费(10%进入积分股池) // 交易来源标识 diff --git a/backend/services/trading-service/src/application/services/order.service.ts b/backend/services/trading-service/src/application/services/order.service.ts index 6fadb5c6..95ccb5f9 100644 --- a/backend/services/trading-service/src/application/services/order.service.ts +++ b/backend/services/trading-service/src/application/services/order.service.ts @@ -197,6 +197,7 @@ export class OrderService { const sellerReceiveAmount = new Money(sellerGrossAmount.value.minus(tradeFee.value)); // 保存成交记录(包含销毁信息、手续费和来源标识) + // quantity 存储有效积分股(含销毁倍数),originalQuantity 存储原始卖出数量 await this.prisma.trade.create({ data: { tradeNo: match.trade.tradeNo, @@ -205,7 +206,8 @@ export class OrderService { buyerSequence: match.buyOrder.accountSequence, sellerSequence: match.sellOrder.accountSequence, price: match.trade.price.value, - quantity: tradeQuantity.value, + quantity: effectiveQuantity.value, // 有效积分股(倍数后) + originalQuantity: tradeQuantity.value, // 原始卖出数量 burnQuantity: burnQuantity.value, effectiveQty: effectiveQuantity.value, amount: sellerReceiveAmount.value, 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 8ffcdaff..2f3431a8 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 @@ -215,6 +215,7 @@ export class OrderRepository { /** * 查询用户的成交记录(作为买方或卖方) + * 返回完整明细:包括销毁倍数、有效积分股、手续费(进入积分股池)等 */ async findTradesByAccountSequence( accountSequence: string, @@ -225,10 +226,13 @@ export class OrderRepository { tradeNo: string; type: 'BUY' | 'SELL'; price: string; - quantity: string; - amount: string; - fee: string; - netAmount: string; + quantity: string; // 有效积分股(含销毁倍数) + originalQuantity: string; // 原始卖出数量 + burnQuantity: string; // 销毁数量 + burnMultiplier: string; // 销毁倍数 + grossAmount: string; // 交易总额(扣除手续费前) + fee: string; // 手续费(进入积分股池) + netAmount: string; // 实际获得(扣除手续费后) counterparty: string; createdAt: Date; }>; @@ -253,30 +257,51 @@ export class OrderRepository { // 合并并转换格式 const allTrades = [ - ...buyerTrades.map((t) => ({ - id: t.id, - tradeNo: t.tradeNo, - type: 'BUY' as const, - price: t.price.toString(), - quantity: t.quantity.toString(), - amount: t.amount.toString(), - fee: '0', // 买方不支付手续费 - netAmount: t.amount.toString(), - counterparty: t.sellerSequence, - createdAt: t.createdAt, - })), - ...sellerTrades.map((t) => ({ - id: t.id, - tradeNo: t.tradeNo, - type: 'SELL' as const, - price: t.price.toString(), - quantity: t.quantity.toString(), - amount: t.amount.toString(), // 这是扣除手续费后的金额 - fee: t.fee.toString(), - netAmount: t.amount.toString(), // amount 已经是 net(扣除手续费后) - counterparty: t.buyerSequence, - createdAt: t.createdAt, - })), + ...buyerTrades.map((t) => { + // 买方:quantity 是有效积分股,originalQuantity 是原始数量 + // 买方支付金额 = 原始数量 × 价格 + const originalQty = t.originalQuantity?.toString() || t.quantity.toString(); + const buyerPayAmount = Number(originalQty) * Number(t.price); + return { + id: t.id, + tradeNo: t.tradeNo, + type: 'BUY' as const, + price: t.price.toString(), + quantity: t.quantity.toString(), // 有效积分股(买方获得的积分股) + originalQuantity: originalQty, // 原始数量 + burnQuantity: '0', + burnMultiplier: '1', // 买方无销毁倍数 + grossAmount: buyerPayAmount.toString(), // 买方支付总额 + fee: '0', // 买方不支付手续费 + netAmount: buyerPayAmount.toString(), // 买方支付总额 + counterparty: t.sellerSequence, + createdAt: t.createdAt, + }; + }), + ...sellerTrades.map((t) => { + // 卖方:计算销毁倍数 = 有效积分股 / 原始数量 + const effectiveQty = Number(t.quantity); + const originalQty = Number(t.originalQuantity || t.quantity); + const burnMultiplier = originalQty > 0 ? effectiveQty / originalQty : 1; + // 交易总额 = 有效积分股 × 价格 + const grossAmount = effectiveQty * Number(t.price); + // 实际获得 = 交易总额 - 手续费 (即 amount 字段) + return { + id: t.id, + tradeNo: t.tradeNo, + type: 'SELL' as const, + price: t.price.toString(), + quantity: t.quantity.toString(), // 有效积分股 + originalQuantity: (t.originalQuantity || t.quantity).toString(), // 原始卖出数量 + burnQuantity: t.burnQuantity.toString(), // 销毁数量 + burnMultiplier: burnMultiplier.toString(), // 销毁倍数 + grossAmount: grossAmount.toString(), // 交易总额 + fee: t.fee.toString(), // 手续费(进入积分股池) + netAmount: t.amount.toString(), // 实际获得(扣除手续费后) + counterparty: t.buyerSequence, + createdAt: t.createdAt, + }; + }), ]; // 按时间排序 diff --git a/frontend/mining-app/lib/data/models/trade_order_model.dart b/frontend/mining-app/lib/data/models/trade_order_model.dart index 6800e240..1325af9f 100644 --- a/frontend/mining-app/lib/data/models/trade_order_model.dart +++ b/frontend/mining-app/lib/data/models/trade_order_model.dart @@ -88,7 +88,7 @@ class OrdersPageModel { } } -/// 成交记录 Model +/// 成交记录 Model(包含完整交易明细) class TradeRecordModel extends TradeRecord { const TradeRecordModel({ required super.id, @@ -96,7 +96,10 @@ class TradeRecordModel extends TradeRecord { required super.type, required super.price, required super.quantity, - required super.amount, + required super.originalQuantity, + required super.burnQuantity, + required super.burnMultiplier, + required super.grossAmount, required super.fee, required super.netAmount, required super.counterparty, @@ -110,7 +113,10 @@ class TradeRecordModel extends TradeRecord { type: json['type'] == 'BUY' ? TradeType.buy : TradeType.sell, price: json['price']?.toString() ?? '0', quantity: json['quantity']?.toString() ?? '0', - amount: json['amount']?.toString() ?? '0', + originalQuantity: json['originalQuantity']?.toString() ?? json['quantity']?.toString() ?? '0', + burnQuantity: json['burnQuantity']?.toString() ?? '0', + burnMultiplier: json['burnMultiplier']?.toString() ?? '1', + grossAmount: json['grossAmount']?.toString() ?? '0', fee: json['fee']?.toString() ?? '0', netAmount: json['netAmount']?.toString() ?? '0', counterparty: json['counterparty']?.toString() ?? '', diff --git a/frontend/mining-app/lib/domain/entities/trade_record.dart b/frontend/mining-app/lib/domain/entities/trade_record.dart index 3c9dda06..709d8b0c 100644 --- a/frontend/mining-app/lib/domain/entities/trade_record.dart +++ b/frontend/mining-app/lib/domain/entities/trade_record.dart @@ -2,7 +2,7 @@ import 'package:equatable/equatable.dart'; enum TradeType { buy, sell } -/// 成交记录实体(包含手续费明细) +/// 成交记录实体(包含完整交易明细) class TradeRecord extends Equatable { /// 成交ID final String id; @@ -12,13 +12,19 @@ class TradeRecord extends Equatable { final TradeType type; /// 成交价格 final String price; - /// 成交数量 + /// 有效积分股(含销毁倍数) final String quantity; - /// 成交金额(卖方扣除手续费后的金额) - final String amount; - /// 手续费(卖方支付,买方为0) + /// 原始卖出数量 + final String originalQuantity; + /// 销毁数量 + final String burnQuantity; + /// 销毁倍数 + final String burnMultiplier; + /// 交易总额(扣除手续费前) + final String grossAmount; + /// 手续费(进入积分股池,卖方支付,买方为0) final String fee; - /// 净额(实际收到/支付金额) + /// 实际获得(扣除手续费后) final String netAmount; /// 对手方账号 final String counterparty; @@ -31,7 +37,10 @@ class TradeRecord extends Equatable { required this.type, required this.price, required this.quantity, - required this.amount, + required this.originalQuantity, + required this.burnQuantity, + required this.burnMultiplier, + required this.grossAmount, required this.fee, required this.netAmount, required this.counterparty, @@ -47,6 +56,12 @@ class TradeRecord extends Equatable { return feeValue > 0; } + /// 是否有销毁倍数(大于1表示有销毁) + bool get hasBurn { + final multiplier = double.tryParse(burnMultiplier) ?? 1; + return multiplier > 1; + } + @override List get props => [id, tradeNo, type, price, quantity, fee]; } diff --git a/frontend/mining-app/lib/presentation/pages/profile/trading_records_page.dart b/frontend/mining-app/lib/presentation/pages/profile/trading_records_page.dart index 941898eb..dcd6dad2 100644 --- a/frontend/mining-app/lib/presentation/pages/profile/trading_records_page.dart +++ b/frontend/mining-app/lib/presentation/pages/profile/trading_records_page.dart @@ -358,11 +358,6 @@ class _TradingRecordsPageState extends ConsumerState with Si final typeColor = isBuy ? _green : _red; final typeText = isBuy ? '买入' : '卖出'; - // 计算交易总额(卖出时:netAmount + fee) - final grossAmount = isBuy - ? double.tryParse(trade.amount) ?? 0 - : (double.tryParse(trade.netAmount) ?? 0) + (double.tryParse(trade.fee) ?? 0); - return Container( margin: const EdgeInsets.only(bottom: 12), padding: const EdgeInsets.all(16), @@ -373,7 +368,7 @@ class _TradingRecordsPageState extends ConsumerState with Si child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // 第一行:类型标签 + // 第一行:类型标签和时间 Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ @@ -396,35 +391,28 @@ class _TradingRecordsPageState extends ConsumerState with Si ), const SizedBox(height: 12), - // 第二行:价格和数量 - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - _buildInfoItem('价格', _formatPrice(trade.price)), - _buildInfoItem('数量', _formatQuantity(trade.quantity)), - ], - ), - const SizedBox(height: 8), - - // 第三行:交易总额 - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - _buildInfoItem('交易总额', '${formatAmount(grossAmount.toString())} 积分值'), - if (trade.hasFee) - _buildInfoItem('手续费(10%)', '${formatAmount(trade.fee)} 积分值', valueColor: _red), - ], - ), - - // 第四行:实际收到(仅卖出时显示) - if (trade.isSell && trade.hasFee) ...[ - const SizedBox(height: 8), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - _buildInfoItem('实际收到', '${formatAmount(trade.netAmount)} 积分值', valueColor: _green), - const SizedBox(), - ], + if (isBuy) ...[ + // 买入:显示简化信息 + _buildDetailRow('成交价格', '${_formatPrice(trade.price)} 积分值'), + _buildDetailRow('成交数量', '${_formatQuantity(trade.quantity)} 积分股'), + _buildDetailRow('支付总额', '${formatAmount(trade.grossAmount)} 积分值'), + ] else ...[ + // 卖出:显示完整明细(类似确认卖出弹窗) + _buildDetailRow('卖出数量', '${_formatQuantity(trade.originalQuantity)} 积分股'), + _buildDetailRow('卖出价格', '${_formatPrice(trade.price)} 积分值'), + _buildDetailRow('销毁倍数', _formatMultiplier(trade.burnMultiplier)), + _buildDetailRow('有效积分股', _formatQuantity(trade.quantity)), + _buildDetailRow('交易总额', '${formatAmount(trade.grossAmount)} 积分值'), + _buildDetailRow( + '进入积分股池', + '${formatAmount(trade.fee)} 积分值 (10%)', + valueColor: const Color(0xFFE67E22), + ), + _buildDetailRow( + '实际获得', + '${formatAmount(trade.netAmount)} 积分值', + valueColor: _green, + isBold: true, ), ], @@ -447,6 +435,35 @@ class _TradingRecordsPageState extends ConsumerState with Si ); } + Widget _buildDetailRow(String label, String value, {Color? valueColor, bool isBold = false}) { + return Padding( + padding: const EdgeInsets.only(bottom: 6), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + label, + style: TextStyle(fontSize: 13, color: _grayText), + ), + Text( + value, + style: TextStyle( + fontSize: 13, + color: valueColor ?? _darkText, + fontWeight: isBold ? FontWeight.w600 : FontWeight.w500, + fontFamily: 'monospace', + ), + ), + ], + ), + ); + } + + String _formatMultiplier(String multiplier) { + final value = double.tryParse(multiplier) ?? 1; + return value.toStringAsFixed(4); + } + Widget _buildInfoItem(String label, String value, {Color? valueColor}) { return Row( mainAxisSize: MainAxisSize.min,