feat(trading): 成交明细显示完整卖出信息(销毁倍数、有效积分股、手续费等)
- 后端Trade表新增originalQuantity字段存储原始卖出数量 - quantity字段改为存储有效积分股(含销毁倍数) - API返回完整明细:销毁倍数、有效积分股、交易总额、进入积分股池 - 前端成交明细页面显示完整卖出信息,类似确认卖出弹窗样式 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
7da98c248b
commit
8728fdce4c
|
|
@ -196,10 +196,11 @@ model Trade {
|
||||||
sellOrderId String @map("sell_order_id")
|
sellOrderId String @map("sell_order_id")
|
||||||
buyerSequence String @map("buyer_sequence")
|
buyerSequence String @map("buyer_sequence")
|
||||||
sellerSequence String @map("seller_sequence")
|
sellerSequence String @map("seller_sequence")
|
||||||
price Decimal @db.Decimal(30, 18)
|
price Decimal @db.Decimal(30, 18)
|
||||||
quantity Decimal @db.Decimal(30, 8) // 实际成交量
|
quantity Decimal @db.Decimal(30, 8) // 有效积分股(含销毁倍数)
|
||||||
burnQuantity Decimal @default(0) @map("burn_quantity") @db.Decimal(30, 8) // 卖出销毁量
|
originalQuantity Decimal @default(0) @map("original_quantity") @db.Decimal(30, 8) // 原始卖出数量
|
||||||
effectiveQty Decimal @default(0) @map("effective_qty") @db.Decimal(30, 8) // 有效量(quantity + burnQuantity)
|
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) // 卖方实际收到金额(扣除手续费后)
|
amount Decimal @db.Decimal(30, 8) // 卖方实际收到金额(扣除手续费后)
|
||||||
fee Decimal @default(0) @db.Decimal(30, 8) // 交易手续费(10%进入积分股池)
|
fee Decimal @default(0) @db.Decimal(30, 8) // 交易手续费(10%进入积分股池)
|
||||||
// 交易来源标识
|
// 交易来源标识
|
||||||
|
|
|
||||||
|
|
@ -197,6 +197,7 @@ export class OrderService {
|
||||||
const sellerReceiveAmount = new Money(sellerGrossAmount.value.minus(tradeFee.value));
|
const sellerReceiveAmount = new Money(sellerGrossAmount.value.minus(tradeFee.value));
|
||||||
|
|
||||||
// 保存成交记录(包含销毁信息、手续费和来源标识)
|
// 保存成交记录(包含销毁信息、手续费和来源标识)
|
||||||
|
// quantity 存储有效积分股(含销毁倍数),originalQuantity 存储原始卖出数量
|
||||||
await this.prisma.trade.create({
|
await this.prisma.trade.create({
|
||||||
data: {
|
data: {
|
||||||
tradeNo: match.trade.tradeNo,
|
tradeNo: match.trade.tradeNo,
|
||||||
|
|
@ -205,7 +206,8 @@ export class OrderService {
|
||||||
buyerSequence: match.buyOrder.accountSequence,
|
buyerSequence: match.buyOrder.accountSequence,
|
||||||
sellerSequence: match.sellOrder.accountSequence,
|
sellerSequence: match.sellOrder.accountSequence,
|
||||||
price: match.trade.price.value,
|
price: match.trade.price.value,
|
||||||
quantity: tradeQuantity.value,
|
quantity: effectiveQuantity.value, // 有效积分股(倍数后)
|
||||||
|
originalQuantity: tradeQuantity.value, // 原始卖出数量
|
||||||
burnQuantity: burnQuantity.value,
|
burnQuantity: burnQuantity.value,
|
||||||
effectiveQty: effectiveQuantity.value,
|
effectiveQty: effectiveQuantity.value,
|
||||||
amount: sellerReceiveAmount.value,
|
amount: sellerReceiveAmount.value,
|
||||||
|
|
|
||||||
|
|
@ -215,6 +215,7 @@ export class OrderRepository {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 查询用户的成交记录(作为买方或卖方)
|
* 查询用户的成交记录(作为买方或卖方)
|
||||||
|
* 返回完整明细:包括销毁倍数、有效积分股、手续费(进入积分股池)等
|
||||||
*/
|
*/
|
||||||
async findTradesByAccountSequence(
|
async findTradesByAccountSequence(
|
||||||
accountSequence: string,
|
accountSequence: string,
|
||||||
|
|
@ -225,10 +226,13 @@ export class OrderRepository {
|
||||||
tradeNo: string;
|
tradeNo: string;
|
||||||
type: 'BUY' | 'SELL';
|
type: 'BUY' | 'SELL';
|
||||||
price: string;
|
price: string;
|
||||||
quantity: string;
|
quantity: string; // 有效积分股(含销毁倍数)
|
||||||
amount: string;
|
originalQuantity: string; // 原始卖出数量
|
||||||
fee: string;
|
burnQuantity: string; // 销毁数量
|
||||||
netAmount: string;
|
burnMultiplier: string; // 销毁倍数
|
||||||
|
grossAmount: string; // 交易总额(扣除手续费前)
|
||||||
|
fee: string; // 手续费(进入积分股池)
|
||||||
|
netAmount: string; // 实际获得(扣除手续费后)
|
||||||
counterparty: string;
|
counterparty: string;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
}>;
|
}>;
|
||||||
|
|
@ -253,30 +257,51 @@ export class OrderRepository {
|
||||||
|
|
||||||
// 合并并转换格式
|
// 合并并转换格式
|
||||||
const allTrades = [
|
const allTrades = [
|
||||||
...buyerTrades.map((t) => ({
|
...buyerTrades.map((t) => {
|
||||||
id: t.id,
|
// 买方:quantity 是有效积分股,originalQuantity 是原始数量
|
||||||
tradeNo: t.tradeNo,
|
// 买方支付金额 = 原始数量 × 价格
|
||||||
type: 'BUY' as const,
|
const originalQty = t.originalQuantity?.toString() || t.quantity.toString();
|
||||||
price: t.price.toString(),
|
const buyerPayAmount = Number(originalQty) * Number(t.price);
|
||||||
quantity: t.quantity.toString(),
|
return {
|
||||||
amount: t.amount.toString(),
|
id: t.id,
|
||||||
fee: '0', // 买方不支付手续费
|
tradeNo: t.tradeNo,
|
||||||
netAmount: t.amount.toString(),
|
type: 'BUY' as const,
|
||||||
counterparty: t.sellerSequence,
|
price: t.price.toString(),
|
||||||
createdAt: t.createdAt,
|
quantity: t.quantity.toString(), // 有效积分股(买方获得的积分股)
|
||||||
})),
|
originalQuantity: originalQty, // 原始数量
|
||||||
...sellerTrades.map((t) => ({
|
burnQuantity: '0',
|
||||||
id: t.id,
|
burnMultiplier: '1', // 买方无销毁倍数
|
||||||
tradeNo: t.tradeNo,
|
grossAmount: buyerPayAmount.toString(), // 买方支付总额
|
||||||
type: 'SELL' as const,
|
fee: '0', // 买方不支付手续费
|
||||||
price: t.price.toString(),
|
netAmount: buyerPayAmount.toString(), // 买方支付总额
|
||||||
quantity: t.quantity.toString(),
|
counterparty: t.sellerSequence,
|
||||||
amount: t.amount.toString(), // 这是扣除手续费后的金额
|
createdAt: t.createdAt,
|
||||||
fee: t.fee.toString(),
|
};
|
||||||
netAmount: t.amount.toString(), // amount 已经是 net(扣除手续费后)
|
}),
|
||||||
counterparty: t.buyerSequence,
|
...sellerTrades.map((t) => {
|
||||||
createdAt: t.createdAt,
|
// 卖方:计算销毁倍数 = 有效积分股 / 原始数量
|
||||||
})),
|
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,
|
||||||
|
};
|
||||||
|
}),
|
||||||
];
|
];
|
||||||
|
|
||||||
// 按时间排序
|
// 按时间排序
|
||||||
|
|
|
||||||
|
|
@ -88,7 +88,7 @@ class OrdersPageModel {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 成交记录 Model
|
/// 成交记录 Model(包含完整交易明细)
|
||||||
class TradeRecordModel extends TradeRecord {
|
class TradeRecordModel extends TradeRecord {
|
||||||
const TradeRecordModel({
|
const TradeRecordModel({
|
||||||
required super.id,
|
required super.id,
|
||||||
|
|
@ -96,7 +96,10 @@ class TradeRecordModel extends TradeRecord {
|
||||||
required super.type,
|
required super.type,
|
||||||
required super.price,
|
required super.price,
|
||||||
required super.quantity,
|
required super.quantity,
|
||||||
required super.amount,
|
required super.originalQuantity,
|
||||||
|
required super.burnQuantity,
|
||||||
|
required super.burnMultiplier,
|
||||||
|
required super.grossAmount,
|
||||||
required super.fee,
|
required super.fee,
|
||||||
required super.netAmount,
|
required super.netAmount,
|
||||||
required super.counterparty,
|
required super.counterparty,
|
||||||
|
|
@ -110,7 +113,10 @@ class TradeRecordModel extends TradeRecord {
|
||||||
type: json['type'] == 'BUY' ? TradeType.buy : TradeType.sell,
|
type: json['type'] == 'BUY' ? TradeType.buy : TradeType.sell,
|
||||||
price: json['price']?.toString() ?? '0',
|
price: json['price']?.toString() ?? '0',
|
||||||
quantity: json['quantity']?.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',
|
fee: json['fee']?.toString() ?? '0',
|
||||||
netAmount: json['netAmount']?.toString() ?? '0',
|
netAmount: json['netAmount']?.toString() ?? '0',
|
||||||
counterparty: json['counterparty']?.toString() ?? '',
|
counterparty: json['counterparty']?.toString() ?? '',
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import 'package:equatable/equatable.dart';
|
||||||
|
|
||||||
enum TradeType { buy, sell }
|
enum TradeType { buy, sell }
|
||||||
|
|
||||||
/// 成交记录实体(包含手续费明细)
|
/// 成交记录实体(包含完整交易明细)
|
||||||
class TradeRecord extends Equatable {
|
class TradeRecord extends Equatable {
|
||||||
/// 成交ID
|
/// 成交ID
|
||||||
final String id;
|
final String id;
|
||||||
|
|
@ -12,13 +12,19 @@ class TradeRecord extends Equatable {
|
||||||
final TradeType type;
|
final TradeType type;
|
||||||
/// 成交价格
|
/// 成交价格
|
||||||
final String price;
|
final String price;
|
||||||
/// 成交数量
|
/// 有效积分股(含销毁倍数)
|
||||||
final String quantity;
|
final String quantity;
|
||||||
/// 成交金额(卖方扣除手续费后的金额)
|
/// 原始卖出数量
|
||||||
final String amount;
|
final String originalQuantity;
|
||||||
/// 手续费(卖方支付,买方为0)
|
/// 销毁数量
|
||||||
|
final String burnQuantity;
|
||||||
|
/// 销毁倍数
|
||||||
|
final String burnMultiplier;
|
||||||
|
/// 交易总额(扣除手续费前)
|
||||||
|
final String grossAmount;
|
||||||
|
/// 手续费(进入积分股池,卖方支付,买方为0)
|
||||||
final String fee;
|
final String fee;
|
||||||
/// 净额(实际收到/支付金额)
|
/// 实际获得(扣除手续费后)
|
||||||
final String netAmount;
|
final String netAmount;
|
||||||
/// 对手方账号
|
/// 对手方账号
|
||||||
final String counterparty;
|
final String counterparty;
|
||||||
|
|
@ -31,7 +37,10 @@ class TradeRecord extends Equatable {
|
||||||
required this.type,
|
required this.type,
|
||||||
required this.price,
|
required this.price,
|
||||||
required this.quantity,
|
required this.quantity,
|
||||||
required this.amount,
|
required this.originalQuantity,
|
||||||
|
required this.burnQuantity,
|
||||||
|
required this.burnMultiplier,
|
||||||
|
required this.grossAmount,
|
||||||
required this.fee,
|
required this.fee,
|
||||||
required this.netAmount,
|
required this.netAmount,
|
||||||
required this.counterparty,
|
required this.counterparty,
|
||||||
|
|
@ -47,6 +56,12 @@ class TradeRecord extends Equatable {
|
||||||
return feeValue > 0;
|
return feeValue > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 是否有销毁倍数(大于1表示有销毁)
|
||||||
|
bool get hasBurn {
|
||||||
|
final multiplier = double.tryParse(burnMultiplier) ?? 1;
|
||||||
|
return multiplier > 1;
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Object?> get props => [id, tradeNo, type, price, quantity, fee];
|
List<Object?> get props => [id, tradeNo, type, price, quantity, fee];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -358,11 +358,6 @@ class _TradingRecordsPageState extends ConsumerState<TradingRecordsPage> with Si
|
||||||
final typeColor = isBuy ? _green : _red;
|
final typeColor = isBuy ? _green : _red;
|
||||||
final typeText = isBuy ? '买入' : '卖出';
|
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(
|
return Container(
|
||||||
margin: const EdgeInsets.only(bottom: 12),
|
margin: const EdgeInsets.only(bottom: 12),
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
|
|
@ -373,7 +368,7 @@ class _TradingRecordsPageState extends ConsumerState<TradingRecordsPage> with Si
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
// 第一行:类型标签
|
// 第一行:类型标签和时间
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
|
|
@ -396,35 +391,28 @@ class _TradingRecordsPageState extends ConsumerState<TradingRecordsPage> with Si
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
|
|
||||||
// 第二行:价格和数量
|
if (isBuy) ...[
|
||||||
Row(
|
// 买入:显示简化信息
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
_buildDetailRow('成交价格', '${_formatPrice(trade.price)} 积分值'),
|
||||||
children: [
|
_buildDetailRow('成交数量', '${_formatQuantity(trade.quantity)} 积分股'),
|
||||||
_buildInfoItem('价格', _formatPrice(trade.price)),
|
_buildDetailRow('支付总额', '${formatAmount(trade.grossAmount)} 积分值'),
|
||||||
_buildInfoItem('数量', _formatQuantity(trade.quantity)),
|
] else ...[
|
||||||
],
|
// 卖出:显示完整明细(类似确认卖出弹窗)
|
||||||
),
|
_buildDetailRow('卖出数量', '${_formatQuantity(trade.originalQuantity)} 积分股'),
|
||||||
const SizedBox(height: 8),
|
_buildDetailRow('卖出价格', '${_formatPrice(trade.price)} 积分值'),
|
||||||
|
_buildDetailRow('销毁倍数', _formatMultiplier(trade.burnMultiplier)),
|
||||||
// 第三行:交易总额
|
_buildDetailRow('有效积分股', _formatQuantity(trade.quantity)),
|
||||||
Row(
|
_buildDetailRow('交易总额', '${formatAmount(trade.grossAmount)} 积分值'),
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
_buildDetailRow(
|
||||||
children: [
|
'进入积分股池',
|
||||||
_buildInfoItem('交易总额', '${formatAmount(grossAmount.toString())} 积分值'),
|
'${formatAmount(trade.fee)} 积分值 (10%)',
|
||||||
if (trade.hasFee)
|
valueColor: const Color(0xFFE67E22),
|
||||||
_buildInfoItem('手续费(10%)', '${formatAmount(trade.fee)} 积分值', valueColor: _red),
|
),
|
||||||
],
|
_buildDetailRow(
|
||||||
),
|
'实际获得',
|
||||||
|
'${formatAmount(trade.netAmount)} 积分值',
|
||||||
// 第四行:实际收到(仅卖出时显示)
|
valueColor: _green,
|
||||||
if (trade.isSell && trade.hasFee) ...[
|
isBold: true,
|
||||||
const SizedBox(height: 8),
|
|
||||||
Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
_buildInfoItem('实际收到', '${formatAmount(trade.netAmount)} 积分值', valueColor: _green),
|
|
||||||
const SizedBox(),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
||||||
|
|
@ -447,6 +435,35 @@ class _TradingRecordsPageState extends ConsumerState<TradingRecordsPage> 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}) {
|
Widget _buildInfoItem(String label, String value, {Color? valueColor}) {
|
||||||
return Row(
|
return Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue