feat: 订单列表按市场分组功能重构
- 后端实现按市场分组的订单列表接口 - 添加 MarketGroupedOrdersRequest、MarketOrderStats、MarketOrderGroup、MarketGroupedOrdersResponse DTO - 实现 getBuyOrderListGroupedByMarket 和 getSellOrderListGroupedByMarket 方法 - 添加 POST /api/copy-trading/orders/grouped-by-market 接口 - 支持分页和统计信息(订单数、总金额、盈亏、成交状态等) - 前端重构:移除前端分组逻辑,改为调用后端分组接口 - BuyOrdersTab 和 SellOrdersTab 使用后端分组接口 - 移除前端 useMemo 分组计算逻辑 - 添加分组数据状态管理和分页支持 - 分组默认折叠状态 - 移除桌面端自动展开逻辑 - 所有设备默认折叠,用户手动展开 - 修复 moneyline URL 逻辑 - getPolymarketUrl 函数添加 isMoneyline 参数 - 暂时不自动跳转到 moneyline 页面(需要后端提供标识) - 多语言支持 - 添加 markets 翻译键(中文、繁体中文、英文)
This commit is contained in:
+46
@@ -200,5 +200,51 @@ class CopyOrderTrackingController(
|
||||
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_ORDER_TRACKING_LIST_FETCH_FAILED, e.message, messageSource))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询按市场分组的订单列表(买入/卖出)
|
||||
* POST /api/copy-trading/orders/grouped-by-market
|
||||
*/
|
||||
@PostMapping("/grouped-by-market")
|
||||
fun getOrderListGroupedByMarket(@RequestBody request: MarketGroupedOrdersRequest): ResponseEntity<ApiResponse<MarketGroupedOrdersResponse>> {
|
||||
return try {
|
||||
if (request.copyTradingId <= 0) {
|
||||
return ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_COPY_TRADING_ID_INVALID, messageSource = messageSource))
|
||||
}
|
||||
|
||||
if (request.type.isBlank()) {
|
||||
return ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_EMPTY, "订单类型不能为空", messageSource))
|
||||
}
|
||||
|
||||
val validTypes = listOf("buy", "sell")
|
||||
if (!validTypes.contains(request.type.lowercase())) {
|
||||
return ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_ERROR, "订单类型必须是 buy 或 sell", messageSource))
|
||||
}
|
||||
|
||||
val result: Result<MarketGroupedOrdersResponse> = runBlocking {
|
||||
when (request.type.lowercase()) {
|
||||
"buy" -> statisticsService.getBuyOrderListGroupedByMarket(request)
|
||||
"sell" -> statisticsService.getSellOrderListGroupedByMarket(request)
|
||||
else -> Result.failure(IllegalArgumentException("不支持的订单类型: ${request.type}"))
|
||||
}
|
||||
}
|
||||
|
||||
result.fold(
|
||||
onSuccess = { response: MarketGroupedOrdersResponse ->
|
||||
ResponseEntity.ok(ApiResponse.success(response))
|
||||
},
|
||||
onFailure = { e: Throwable ->
|
||||
logger.error("查询按市场分组的订单列表失败: copyTradingId=${request.copyTradingId}, type=${request.type}", e)
|
||||
when (e) {
|
||||
is IllegalArgumentException -> ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_ERROR, e.message, messageSource))
|
||||
else -> ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_ORDER_TRACKING_LIST_FETCH_FAILED, e.message, messageSource))
|
||||
}
|
||||
}
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
logger.error("查询按市场分组的订单列表异常: copyTradingId=${request.copyTradingId}, type=${request.type}", e)
|
||||
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_ORDER_TRACKING_LIST_FETCH_FAILED, e.message, messageSource))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -113,6 +113,51 @@ data class OrderTrackingRequest(
|
||||
val buyOrderId: String? = null
|
||||
)
|
||||
|
||||
/**
|
||||
* 按市场分组的订单查询请求
|
||||
*/
|
||||
data class MarketGroupedOrdersRequest(
|
||||
val copyTradingId: Long,
|
||||
val type: String, // buy, sell, matched
|
||||
val page: Int? = 1,
|
||||
val limit: Int? = 20
|
||||
)
|
||||
|
||||
/**
|
||||
* 单个市场的订单统计信息
|
||||
*/
|
||||
data class MarketOrderStats(
|
||||
val count: Long,
|
||||
val totalAmount: String, // 总金额
|
||||
val totalPnl: String?, // 总盈亏(买入订单未实现盈亏,此字段为空)
|
||||
val fullyMatched: Boolean, // 是否全部成交
|
||||
val fullyMatchedCount: Long, // 完全成交的订单数
|
||||
val partiallyMatchedCount: Long, // 部分成交的订单数
|
||||
val filledCount: Long // 未成交的订单数
|
||||
)
|
||||
|
||||
/**
|
||||
* 单个市场分组的响应数据
|
||||
*/
|
||||
data class MarketOrderGroup(
|
||||
val marketId: String,
|
||||
val marketTitle: String?,
|
||||
val marketSlug: String?,
|
||||
val marketCategory: String?,
|
||||
val stats: MarketOrderStats,
|
||||
val orders: List<Any> // BuyOrderInfo, SellOrderInfo 或 MatchedOrderInfo 的列表
|
||||
)
|
||||
|
||||
/**
|
||||
* 按市场分组的订单列表响应
|
||||
*/
|
||||
data class MarketGroupedOrdersResponse(
|
||||
val list: List<MarketOrderGroup>,
|
||||
val total: Long, // 市场总数
|
||||
val page: Int,
|
||||
val limit: Int
|
||||
)
|
||||
|
||||
/**
|
||||
* 统计查询请求
|
||||
*/
|
||||
|
||||
+211
-1
@@ -727,5 +727,215 @@ class CopyTradingStatisticsService(
|
||||
val currentPositionQuantity: String,
|
||||
val totalRealizedPnl: String
|
||||
)
|
||||
|
||||
/**
|
||||
* 获取按市场分组的买入订单列表
|
||||
*/
|
||||
fun getBuyOrderListGroupedByMarket(request: MarketGroupedOrdersRequest): Result<MarketGroupedOrdersResponse> {
|
||||
return try {
|
||||
// 1. 验证跟单关系
|
||||
copyTradingRepository.findById(request.copyTradingId).orElse(null)
|
||||
?: return Result.failure(IllegalArgumentException("跟单关系不存在: ${request.copyTradingId}"))
|
||||
|
||||
// 2. 获取所有买入订单
|
||||
var orders = copyOrderTrackingRepository.findByCopyTradingId(request.copyTradingId)
|
||||
|
||||
// 3. 按市场ID分组
|
||||
val groups = mutableMapOf<String, MutableList<CopyOrderTracking>>()
|
||||
orders.forEach { order ->
|
||||
val marketId = order.marketId
|
||||
if (!groups.containsKey(marketId)) {
|
||||
groups[marketId] = mutableListOf()
|
||||
}
|
||||
groups[marketId]!!.add(order)
|
||||
}
|
||||
|
||||
// 4. 转换为分组数据并计算统计信息
|
||||
val marketIds = groups.keys.toList()
|
||||
val markets = marketService.getMarkets(marketIds)
|
||||
|
||||
val list = marketIds.map { marketId ->
|
||||
val marketOrders = groups[marketId] ?: mutableListOf()
|
||||
|
||||
// 计算统计信息
|
||||
val count = marketOrders.size.toLong()
|
||||
val totalAmount = marketOrders.sumOf { order ->
|
||||
order.quantity.toSafeBigDecimal().multi(order.price)
|
||||
}
|
||||
|
||||
// 计算订单状态统计
|
||||
val fullyMatchedCount = marketOrders.count { it.status == "fully_matched" }
|
||||
val partiallyMatchedCount = marketOrders.count { it.status == "partially_matched" }
|
||||
val filledCount = marketOrders.count { it.status == "filled" }
|
||||
val fullyMatched = fullyMatchedCount == marketOrders.size
|
||||
|
||||
val stats = MarketOrderStats(
|
||||
count = count,
|
||||
totalAmount = totalAmount.toString(),
|
||||
totalPnl = null, // 买入订单没有已实现盈亏
|
||||
fullyMatched = fullyMatched,
|
||||
fullyMatchedCount = fullyMatchedCount.toLong(),
|
||||
partiallyMatchedCount = partiallyMatchedCount.toLong(),
|
||||
filledCount = filledCount.toLong()
|
||||
)
|
||||
|
||||
// 排序(按创建时间倒序)
|
||||
marketOrders.sortByDescending { it.createdAt }
|
||||
|
||||
// 转换为 DTO
|
||||
val orderDtos = marketOrders.map { order ->
|
||||
val amount = order.quantity.toSafeBigDecimal().multi(order.price)
|
||||
val market = markets[order.marketId]
|
||||
BuyOrderInfo(
|
||||
orderId = order.buyOrderId,
|
||||
leaderTradeId = order.leaderBuyTradeId,
|
||||
marketId = order.marketId,
|
||||
marketTitle = market?.title,
|
||||
marketSlug = market?.slug,
|
||||
marketCategory = market?.category,
|
||||
side = order.side,
|
||||
quantity = order.quantity.toString(),
|
||||
price = order.price.toString(),
|
||||
amount = amount.toString(),
|
||||
matchedQuantity = order.matchedQuantity.toString(),
|
||||
remainingQuantity = order.remainingQuantity.toString(),
|
||||
status = order.status,
|
||||
createdAt = order.createdAt
|
||||
)
|
||||
}
|
||||
|
||||
MarketOrderGroup(
|
||||
marketId = marketId,
|
||||
marketTitle = markets[marketId]?.title,
|
||||
marketSlug = markets[marketId]?.slug,
|
||||
marketCategory = markets[marketId]?.category,
|
||||
stats = stats,
|
||||
orders = orderDtos as List<Any>
|
||||
)
|
||||
}.sortedByDescending { it.stats.count }
|
||||
|
||||
// 5. 分页
|
||||
val page = (request.page ?: 1)
|
||||
val limit = request.limit ?: 20
|
||||
val total = list.size.toLong()
|
||||
|
||||
val start = (page - 1) * limit
|
||||
val end = minOf(start + limit, list.size)
|
||||
val pagedList = if (start < list.size) list.subList(start, end) else emptyList()
|
||||
|
||||
val response = MarketGroupedOrdersResponse(
|
||||
list = pagedList,
|
||||
total = total,
|
||||
page = page,
|
||||
limit = limit
|
||||
)
|
||||
|
||||
Result.success(response)
|
||||
} catch (e: Exception) {
|
||||
logger.error("获取按市场分组的买入订单列表失败: copyTradingId=${request.copyTradingId}", e)
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取按市场分组的卖出订单列表
|
||||
*/
|
||||
fun getSellOrderListGroupedByMarket(request: MarketGroupedOrdersRequest): Result<MarketGroupedOrdersResponse> {
|
||||
return try {
|
||||
// 1. 验证跟单关系
|
||||
copyTradingRepository.findById(request.copyTradingId).orElse(null)
|
||||
?: return Result.failure(IllegalArgumentException("跟单关系不存在: ${request.copyTradingId}"))
|
||||
|
||||
// 2. 获取所有卖出记录
|
||||
val sellRecords = sellMatchRecordRepository.findByCopyTradingId(request.copyTradingId)
|
||||
|
||||
// 3. 按市场ID分组
|
||||
val groups = mutableMapOf<String, MutableList<SellMatchRecord>>()
|
||||
sellRecords.forEach { record ->
|
||||
val marketId = record.marketId
|
||||
if (!groups.containsKey(marketId)) {
|
||||
groups[marketId] = mutableListOf()
|
||||
}
|
||||
groups[marketId]!!.add(record)
|
||||
}
|
||||
|
||||
// 4. 转换为分组数据并计算统计信息
|
||||
val marketIds = groups.keys.toList()
|
||||
val markets = marketService.getMarkets(marketIds)
|
||||
|
||||
val list = marketIds.map { marketId ->
|
||||
val marketRecords = groups[marketId] ?: mutableListOf()
|
||||
|
||||
// 计算统计信息
|
||||
val count = marketRecords.size.toLong()
|
||||
val totalAmount = marketRecords.sumOf { record ->
|
||||
record.totalMatchedQuantity.toSafeBigDecimal().multi(record.sellPrice)
|
||||
}
|
||||
val totalPnl = marketRecords.sumOf { it.totalRealizedPnl.toSafeBigDecimal() }
|
||||
|
||||
val stats = MarketOrderStats(
|
||||
count = count,
|
||||
totalAmount = totalAmount.toString(),
|
||||
totalPnl = totalPnl.toString(),
|
||||
fullyMatched = true, // 卖出订单都是已成交的
|
||||
fullyMatchedCount = count, // 所有订单都是已成交的
|
||||
partiallyMatchedCount = 0L,
|
||||
filledCount = 0L
|
||||
)
|
||||
|
||||
// 排序(按创建时间倒序)
|
||||
marketRecords.sortByDescending { it.createdAt }
|
||||
|
||||
// 转换为 DTO
|
||||
val orderDtos = marketRecords.map { record ->
|
||||
val amount = record.totalMatchedQuantity.toSafeBigDecimal().multi(record.sellPrice)
|
||||
val market = markets[record.marketId]
|
||||
SellOrderInfo(
|
||||
orderId = record.sellOrderId,
|
||||
leaderTradeId = record.leaderSellTradeId,
|
||||
marketId = record.marketId,
|
||||
marketTitle = market?.title,
|
||||
marketSlug = market?.slug,
|
||||
marketCategory = market?.category,
|
||||
side = record.side,
|
||||
quantity = record.totalMatchedQuantity.toString(),
|
||||
price = record.sellPrice.toString(),
|
||||
amount = amount.toString(),
|
||||
realizedPnl = record.totalRealizedPnl.toString(),
|
||||
createdAt = record.createdAt
|
||||
)
|
||||
}
|
||||
|
||||
MarketOrderGroup(
|
||||
marketId = marketId,
|
||||
marketTitle = markets[marketId]?.title,
|
||||
marketSlug = markets[marketId]?.slug,
|
||||
marketCategory = markets[marketId]?.category,
|
||||
stats = stats,
|
||||
orders = orderDtos as List<Any>
|
||||
)
|
||||
}.sortedByDescending { it.stats.count }
|
||||
|
||||
// 5. 分页
|
||||
val page = (request.page ?: 1)
|
||||
val limit = request.limit ?: 20
|
||||
val total = list.size.toLong()
|
||||
|
||||
val start = (page - 1) * limit
|
||||
val end = minOf(start + limit, list.size)
|
||||
val pagedList = if (start < list.size) list.subList(start, end) else emptyList()
|
||||
|
||||
val response = MarketGroupedOrdersResponse(
|
||||
list = pagedList,
|
||||
total = total,
|
||||
page = page,
|
||||
limit = limit
|
||||
)
|
||||
|
||||
Result.success(response)
|
||||
} catch (e: Exception) {
|
||||
logger.error("获取按市场分组的卖出订单列表失败: copyTradingId=${request.copyTradingId}", e)
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user