feat: 添加市场信息管理和订单ID复制功能

- 新增市场信息表和服务,支持市场名称缓存和自动更新
  - 创建 Market 实体和 MarketRepository
  - 实现 MarketService 提供市场信息查询和缓存
  - 实现 MarketPollingService 每30秒自动检查并更新缺失的市场信息
  - 添加数据库迁移 V19 创建 markets 表

- 订单列表显示市场名称
  - 在 BuyOrderInfo、SellOrderInfo、MatchedOrderInfo 中添加 marketTitle 字段
  - 订单查询时自动查询并填充市场名称
  - 前端订单列表显示市场名称(优先显示名称,ID作为辅助信息)

- 前端订单ID复制功能
  - 添加 isAutoGeneratedOrderId 和 copyToClipboard 工具函数
  - 非自动生成的订单ID支持一键复制
  - 买入、卖出、匹配订单列表均支持复制功能

- 仓位检查延迟检测机制优化
  - 首次检测到仓位不存在时先记录,3分钟后再次检查
  - 避免因API延迟导致的误判
This commit is contained in:
WrBug
2026-01-08 11:56:52 +08:00
parent e75c93ddf4
commit d376a82ccc
19 changed files with 1122 additions and 101 deletions
@@ -40,6 +40,7 @@ data class BuyOrderInfo(
val orderId: String,
val leaderTradeId: String,
val marketId: String,
val marketTitle: String? = null, // 市场名称
val side: String,
val quantity: String,
val price: String,
@@ -57,6 +58,7 @@ data class SellOrderInfo(
val orderId: String,
val leaderTradeId: String,
val marketId: String,
val marketTitle: String? = null, // 市场名称
val side: String,
val quantity: String,
val price: String,
@@ -71,6 +73,8 @@ data class SellOrderInfo(
data class MatchedOrderInfo(
val sellOrderId: String,
val buyOrderId: String,
val marketId: String? = null, // 市场ID(从买入订单获取)
val marketTitle: String? = null, // 市场名称
val matchedQuantity: String,
val buyPrice: String,
val sellPrice: String,
@@ -0,0 +1,54 @@
package com.wrbug.polymarketbot.entity
import jakarta.persistence.*
/**
* 市场信息实体
* 用于缓存市场的基本信息(名称、slug等)
*/
@Entity
@Table(name = "markets", indexes = [
Index(name = "idx_market_id", columnList = "market_id", unique = true)
])
data class Market(
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long? = null,
@Column(name = "market_id", unique = true, nullable = false, length = 100)
val marketId: String, // 市场IDcondition ID
@Column(name = "title", nullable = false, length = 500)
val title: String, // 市场名称(question
@Column(name = "slug", length = 200)
val slug: String? = null, // 市场slug
@Column(name = "category", length = 50)
val category: String? = null, // 市场分类
@Column(name = "icon", length = 500)
val icon: String? = null, // 市场图标URL
@Column(name = "image", length = 500)
val image: String? = null, // 市场图片URL
@Column(name = "description", columnDefinition = "TEXT")
val description: String? = null, // 市场描述
@Column(name = "active", nullable = false)
val active: Boolean = true, // 是否活跃
@Column(name = "closed", nullable = false)
val closed: Boolean = false, // 是否已关闭
@Column(name = "archived", nullable = false)
val archived: Boolean = false, // 是否已归档
@Column(name = "created_at", nullable = false)
val createdAt: Long = System.currentTimeMillis(),
@Column(name = "updated_at", nullable = false)
var updatedAt: Long = System.currentTimeMillis()
)
@@ -0,0 +1,12 @@
package com.wrbug.polymarketbot.repository
import com.wrbug.polymarketbot.entity.Market
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.stereotype.Repository
@Repository
interface MarketRepository : JpaRepository<Market, Long> {
fun findByMarketId(marketId: String): Market?
fun findByMarketIdIn(marketIds: List<String>): List<Market>
}
@@ -62,6 +62,19 @@ class PositionCheckService(
// 记录已发送提示的配置(避免重复推送)
private val notifiedConfigs = ConcurrentHashMap<Long, Long>() // accountId/copyTradingId -> lastNotificationTime
// 待检查的仓位记录(延迟检测机制)
// key: "accountId_marketId_outcomeIndex_copyTradingId"
// value: PendingPositionCheck(包含订单列表和首次检测时间)
private data class PendingPositionCheck(
val accountId: Long,
val marketId: String,
val outcomeIndex: Int,
val copyTradingId: Long,
val orders: List<CopyOrderTracking>,
val firstDetectedTime: Long // 首次检测到仓位不存在的时间
)
private val pendingPositionChecks = ConcurrentHashMap<String, PendingPositionCheck>()
// 同步锁,确保订阅任务的启动和停止是线程安全的
private val lock = Any()
@@ -73,6 +86,7 @@ class PositionCheckService(
logger.info("PositionCheckService 初始化,订阅仓位轮训事件")
startSubscription()
startCacheCleanup()
startPendingPositionCheckTask()
}
/**
@@ -132,6 +146,121 @@ class PositionCheckService(
}
}
/**
* 启动待检查仓位的定期检查任务
* 每30秒检查一次,如果超过3分钟且确实不存在,则标记为已卖出
*/
private fun startPendingPositionCheckTask() {
scope.launch(Dispatchers.IO) {
while (isActive) {
try {
delay(30000) // 每30秒检查一次
checkPendingPositions()
} catch (e: Exception) {
logger.error("检查待检查仓位异常: ${e.message}", e)
}
}
}
}
/**
* 检查待检查的仓位
* 如果超过3分钟且确实不存在,则标记为已卖出
* 如果存在,则删除记录
*/
private suspend fun checkPendingPositions() {
if (pendingPositionChecks.isEmpty()) {
return
}
try {
// 获取最新的仓位数据
val result = accountService.getAllPositions()
if (result.isFailure) {
logger.warn("获取仓位数据失败,跳过待检查仓位验证: ${result.exceptionOrNull()?.message}")
return
}
val positionListResponse = result.getOrNull() ?: return
val currentPositions = positionListResponse.currentPositions
// 按账户和市场分组当前仓位
val positionsByAccountAndMarket = currentPositions.groupBy {
"${it.accountId}_${it.marketId}_${it.outcomeIndex ?: 0}"
}
val now = System.currentTimeMillis()
val checkDelay = 180000L // 3分钟 = 180000毫秒
val toRemove = mutableListOf<String>()
val toMarkAsSold = mutableListOf<PendingPositionCheck>()
// 遍历所有待检查的仓位
for ((key, pendingCheck) in pendingPositionChecks) {
// 先过滤出仍然有效的订单(remainingQuantity > 0
val validOrders = pendingCheck.orders.filter { order ->
// 重新从数据库查询订单状态,确保数据是最新的
val currentOrder = copyOrderTrackingRepository.findById(order.id!!).orElse(null)
currentOrder != null && currentOrder.remainingQuantity > BigDecimal.ZERO
}
// 如果没有有效订单了,删除记录
if (validOrders.isEmpty()) {
toRemove.add(key)
logger.info("待检查仓位的订单已全部处理,删除记录: marketId=${pendingCheck.marketId}, outcomeIndex=${pendingCheck.outcomeIndex}, accountId=${pendingCheck.accountId}, copyTradingId=${pendingCheck.copyTradingId}")
continue
}
val positionKey = "${pendingCheck.accountId}_${pendingCheck.marketId}_${pendingCheck.outcomeIndex}"
val position = positionsByAccountAndMarket[positionKey]?.firstOrNull()
if (position != null) {
// 仓位存在,删除记录
toRemove.add(key)
logger.info("待检查仓位已恢复,删除记录: marketId=${pendingCheck.marketId}, outcomeIndex=${pendingCheck.outcomeIndex}, accountId=${pendingCheck.accountId}, copyTradingId=${pendingCheck.copyTradingId}, elapsedTime=${now - pendingCheck.firstDetectedTime}ms")
} else {
// 仓位不存在,检查是否超过3分钟
val elapsedTime = now - pendingCheck.firstDetectedTime
if (elapsedTime >= checkDelay) {
// 超过3分钟且确实不存在,标记为已卖出(使用有效订单)
toMarkAsSold.add(pendingCheck.copy(orders = validOrders))
toRemove.add(key)
logger.info("待检查仓位超过3分钟仍不存在,标记为已卖出: marketId=${pendingCheck.marketId}, outcomeIndex=${pendingCheck.outcomeIndex}, accountId=${pendingCheck.accountId}, copyTradingId=${pendingCheck.copyTradingId}, elapsedTime=${elapsedTime}ms, validOrderCount=${validOrders.size}, originalOrderCount=${pendingCheck.orders.size}")
} else {
// 未超过3分钟,更新订单列表(移除已处理的订单)
if (validOrders.size < pendingCheck.orders.size) {
pendingPositionChecks[key] = pendingCheck.copy(orders = validOrders)
logger.debug("更新待检查仓位记录,移除已处理的订单: marketId=${pendingCheck.marketId}, outcomeIndex=${pendingCheck.outcomeIndex}, validOrderCount=${validOrders.size}, originalOrderCount=${pendingCheck.orders.size}")
}
logger.debug("待检查仓位仍不存在,继续等待: marketId=${pendingCheck.marketId}, outcomeIndex=${pendingCheck.outcomeIndex}, accountId=${pendingCheck.accountId}, copyTradingId=${pendingCheck.copyTradingId}, elapsedTime=${elapsedTime}ms, remainingTime=${checkDelay - elapsedTime}ms")
}
}
}
// 删除已恢复或已处理的记录
toRemove.forEach { key ->
pendingPositionChecks.remove(key)
}
// 标记为已卖出
for (pendingCheck in toMarkAsSold) {
try {
val currentPrice = getCurrentMarketPrice(pendingCheck.marketId, pendingCheck.outcomeIndex)
updateOrdersAsSold(
pendingCheck.orders,
currentPrice,
pendingCheck.copyTradingId,
pendingCheck.marketId,
pendingCheck.outcomeIndex
)
} catch (e: Exception) {
logger.error("标记待检查仓位为已卖出失败: marketId=${pendingCheck.marketId}, outcomeIndex=${pendingCheck.outcomeIndex}, error=${e.message}", e)
}
}
} catch (e: Exception) {
logger.error("检查待检查仓位异常: ${e.message}", e)
}
}
/**
* 清理过期的缓存条目(超过2小时的记录)
*/
@@ -163,8 +292,16 @@ class PositionCheckService(
notifiedConfigs.remove(key)
}
if (expiredPositions.isNotEmpty() || expiredProcessed.isNotEmpty() || expiredConfigs.isNotEmpty()) {
logger.debug("清理过期缓存: positions=${expiredPositions.size}, processed=${expiredProcessed.size}, configs=${expiredConfigs.size}")
// 清理过期的待检查仓位记录(超过1小时的记录,正常情况下应该在3分钟内处理完)
val expiredPendingChecks = pendingPositionChecks.entries.filter { (_, check) ->
(now - check.firstDetectedTime) > 3600000 // 1小时
}
expiredPendingChecks.forEach { (key, _) ->
pendingPositionChecks.remove(key)
}
if (expiredPositions.isNotEmpty() || expiredProcessed.isNotEmpty() || expiredConfigs.isNotEmpty() || expiredPendingChecks.isNotEmpty()) {
logger.debug("清理过期缓存: positions=${expiredPositions.size}, processed=${expiredProcessed.size}, configs=${expiredConfigs.size}, pendingChecks=${expiredPendingChecks.size}")
}
}
@@ -371,31 +508,48 @@ class PositionCheckService(
val position = positionsByAccountAndMarket[positionKey]?.firstOrNull()
if (position == null) {
// 仓位不存在,检查订单创建时间
// 只有当订单创建时间超过2分钟时,才认为仓位被出售了
// 这样可以避免刚创建的订单因为API延迟而被误判为已卖出
// 仓位不存在,使用延迟检测机制
// 先检查订单创建时间,只有超过2分钟的订单才进入延迟检测
val now = System.currentTimeMillis()
val ordersToMarkAsSold = orders.filter { order ->
val ordersToCheck = orders.filter { order ->
val orderAge = now - order.createdAt
orderAge > 120000 // 2分钟 = 120000毫秒
}
if (ordersToMarkAsSold.isNotEmpty()) {
// 有订单创建时间超过2分钟,认为仓位已被出售
try {
val currentPrice = getCurrentMarketPrice(marketId, outcomeIndex)
updateOrdersAsSold(ordersToMarkAsSold, currentPrice, copyTrading.id, marketId, outcomeIndex)
logger.debug("仓位不存在且订单创建时间超过2分钟,标记为已卖出: marketId=$marketId, outcomeIndex=$outcomeIndex, orderCount=${ordersToMarkAsSold.size}")
} catch (e: Exception) {
logger.warn("无法获取市场价格,跳过标记为已卖出: marketId=$marketId, outcomeIndex=$outcomeIndex, error=${e.message}")
// 无法获取价格时,跳过该市场的处理,等待下次检查时再试
continue
if (ordersToCheck.isNotEmpty()) {
// 有订单创建时间超过2分钟,记录到待检查列表
val checkKey = "${copyTrading.accountId}_${marketId}_${outcomeIndex}_${copyTrading.id}"
// 如果已经存在记录,更新订单列表(可能订单状态有变化)
val existingCheck = pendingPositionChecks[checkKey]
if (existingCheck == null) {
// 首次检测到,记录
pendingPositionChecks[checkKey] = PendingPositionCheck(
accountId = copyTrading.accountId,
marketId = marketId,
outcomeIndex = outcomeIndex,
copyTradingId = copyTrading.id!!,
orders = ordersToCheck,
firstDetectedTime = now
)
logger.info("首次检测到仓位不存在,记录待检查: marketId=$marketId, outcomeIndex=$outcomeIndex, accountId=${copyTrading.accountId}, copyTradingId=${copyTrading.id}, orderCount=${ordersToCheck.size}, positionKey=$positionKey")
} else {
// 已存在记录,更新订单列表(可能订单状态有变化)
pendingPositionChecks[checkKey] = existingCheck.copy(orders = ordersToCheck)
logger.debug("更新待检查仓位记录: marketId=$marketId, outcomeIndex=$outcomeIndex, accountId=${copyTrading.accountId}, copyTradingId=${copyTrading.id}, orderCount=${ordersToCheck.size}, elapsedTime=${now - existingCheck.firstDetectedTime}ms")
}
} else {
// 订单创建时间不足2分钟,可能是刚创建的订单,暂时不处理
logger.debug("仓位不存在但订单创建时间不足2分钟,暂不标记为已卖出: marketId=$marketId, outcomeIndex=$outcomeIndex, orderCount=${orders.size}, oldestOrderAge=${orders.minOfOrNull { now - it.createdAt }?.let { "${it}ms" } ?: "N/A"}")
logger.debug("仓位不存在但订单创建时间不足2分钟,暂不标记为已卖出: marketId=$marketId, outcomeIndex=$outcomeIndex, orderCount=${orders.size}, oldestOrderAge=${orders.minOfOrNull { now - it.createdAt }?.let { "${it}ms" } ?: "N/A"}, positionKey=$positionKey")
}
} else {
// 有仓位,先检查是否有对应的待检查记录,如果有则删除(仓位已恢复)
val checkKey = "${copyTrading.accountId}_${marketId}_${outcomeIndex}_${copyTrading.id}"
val pendingCheck = pendingPositionChecks.remove(checkKey)
if (pendingCheck != null) {
logger.info("仓位已恢复,删除待检查记录: marketId=$marketId, outcomeIndex=$outcomeIndex, accountId=${copyTrading.accountId}, copyTradingId=${copyTrading.id}, elapsedTime=${System.currentTimeMillis() - pendingCheck.firstDetectedTime}ms")
}
// 有仓位,按订单下单顺序(FIFO)更新状态
// 计算逻辑:
// 1. 总订单数量 = 所有未卖出订单的剩余数量总和
@@ -0,0 +1,173 @@
package com.wrbug.polymarketbot.service.common
import com.wrbug.polymarketbot.repository.CopyOrderTrackingRepository
import jakarta.annotation.PostConstruct
import jakarta.annotation.PreDestroy
import kotlinx.coroutines.*
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Value
import org.springframework.stereotype.Service
/**
* 市场信息轮询服务
* 定期检查买入订单的市场信息是否缺失,如果缺失则从API获取并更新
*/
@Service
class MarketPollingService(
private val copyOrderTrackingRepository: CopyOrderTrackingRepository,
private val marketService: MarketService
) {
private val logger = LoggerFactory.getLogger(MarketPollingService::class.java)
@Value("\${market.polling.interval:30000}")
private var pollingInterval: Long = 30000 // 轮询间隔(毫秒),默认30秒
@Value("\${market.polling.batch.size:50}")
private var batchSize: Int = 50 // 批量处理大小,每次最多处理50个市场
// 协程作用域和任务
private val scope = CoroutineScope(Dispatchers.Default + SupervisorJob())
private var pollingJob: Job? = null
// 同步锁,确保轮询任务的启动和停止是线程安全的
private val lock = Any()
/**
* 初始化服务(后端启动时直接启动轮询)
*/
@PostConstruct
fun init() {
logger.info("MarketPollingService 初始化,启动市场信息轮询任务,轮询间隔: ${pollingInterval}ms (${pollingInterval / 1000 / 60}分钟)")
startPolling()
}
/**
* 清理资源
*/
@PreDestroy
fun destroy() {
synchronized(lock) {
pollingJob?.cancel()
pollingJob = null
}
scope.cancel()
}
/**
* 启动轮询任务
*/
private fun startPolling() {
synchronized(lock) {
// 如果已经有轮询任务在运行,先取消
pollingJob?.cancel()
// 启动新的轮询任务
pollingJob = scope.launch(Dispatchers.IO) {
// 启动时立即执行一次
try {
checkAndUpdateMissingMarkets()
} catch (e: Exception) {
logger.error("初始检查市场信息失败: ${e.message}", e)
}
// 然后按间隔定期执行
while (isActive) {
try {
delay(pollingInterval)
checkAndUpdateMissingMarkets()
} catch (e: Exception) {
logger.error("轮询市场信息失败: ${e.message}", e)
}
}
}
}
}
/**
* 检查并更新缺失的市场信息
*/
private suspend fun checkAndUpdateMissingMarkets() {
try {
logger.debug("开始检查缺失的市场信息...")
// 1. 获取所有买入订单的市场ID(去重)
val allOrders = copyOrderTrackingRepository.findAll()
val marketIds = allOrders.map { it.marketId }.distinct()
if (marketIds.isEmpty()) {
logger.debug("没有找到任何订单,跳过市场信息检查")
return
}
logger.debug("找到 ${marketIds.size} 个不同的市场ID")
// 2. 检查哪些市场信息在数据库中缺失
val existingMarkets = marketService.marketRepository.findByMarketIdIn(marketIds)
val existingMarketIds = existingMarkets.map { it.marketId }.toSet()
val missingMarketIds = marketIds.filter { it !in existingMarketIds }
// 过滤掉空字符串和无效的市场ID
val validMissingMarketIds = missingMarketIds.filter {
it.isNotBlank() && it.startsWith("0x")
}
if (validMissingMarketIds.isEmpty()) {
logger.debug("所有市场信息都已存在,无需更新")
return
}
logger.info("发现 ${validMissingMarketIds.size} 个缺失的市场信息,开始批量更新...")
// 3. 批量从API获取缺失的市场信息(分批处理,避免一次性请求过多)
val batches = validMissingMarketIds.chunked(batchSize)
var successCount = 0
var failCount = 0
for ((index, batch) in batches.withIndex()) {
try {
logger.debug("处理第 ${index + 1}/${batches.size} 批,包含 ${batch.size} 个市场: ${batch.take(3).joinToString(", ")}${if (batch.size > 3) "..." else ""}")
// 使用 MarketService 的批量获取方法
// 这个方法会尝试从API获取并保存到数据库
val markets = marketService.getMarkets(batch)
val batchSuccessCount = markets.size
val batchFailCount = batch.size - batchSuccessCount
successCount += batchSuccessCount
failCount += batchFailCount
if (batchFailCount > 0) {
logger.warn("${index + 1} 批中有 ${batchFailCount} 个市场信息获取失败")
}
// 避免请求过于频繁,每批之间稍作延迟
if (index < batches.size - 1) {
delay(1000) // 延迟1秒,避免API限流
}
} catch (e: Exception) {
logger.error("批量获取市场信息失败: batch=${batch.take(5).joinToString(", ")}..., error=${e.message}", e)
failCount += batch.size
}
}
logger.info("市场信息更新完成: 成功=${successCount}, 失败=${failCount}, 总计=${validMissingMarketIds.size}")
} catch (e: Exception) {
logger.error("检查并更新市场信息异常: ${e.message}", e)
}
}
/**
* 手动触发检查(用于测试或手动刷新)
*/
fun triggerCheck() {
scope.launch(Dispatchers.IO) {
try {
checkAndUpdateMissingMarkets()
} catch (e: Exception) {
logger.error("手动触发检查市场信息失败: ${e.message}", e)
}
}
}
}
@@ -0,0 +1,200 @@
package com.wrbug.polymarketbot.service.common
import com.wrbug.polymarketbot.api.MarketResponse
import com.wrbug.polymarketbot.api.PolymarketGammaApi
import com.wrbug.polymarketbot.entity.Market
import com.wrbug.polymarketbot.repository.MarketRepository
import com.wrbug.polymarketbot.util.RetrofitFactory
import kotlinx.coroutines.runBlocking
import org.slf4j.LoggerFactory
import org.springframework.stereotype.Service
import java.util.concurrent.ConcurrentHashMap
/**
* 市场信息服务
* 负责缓存和管理市场信息
*/
@Service
class MarketService(
val marketRepository: MarketRepository, // 改为 public,供 MarketPollingService 使用
private val retrofitFactory: RetrofitFactory
) {
private val logger = LoggerFactory.getLogger(MarketService::class.java)
// 内存缓存(避免频繁查询数据库)
private val marketCache = ConcurrentHashMap<String, Market>()
/**
* 根据市场ID获取市场信息
* 优先从缓存获取,如果不存在则从数据库查询,如果数据库也没有则从API获取并保存
*/
fun getMarket(marketId: String): Market? {
// 1. 从缓存获取
marketCache[marketId]?.let { return it }
// 2. 从数据库查询
val market = marketRepository.findByMarketId(marketId)
if (market != null) {
marketCache[marketId] = market
return market
}
// 3. 从API获取(异步,不阻塞)
runBlocking {
try {
fetchAndSaveMarket(marketId)
} catch (e: Exception) {
logger.warn("获取市场信息失败: marketId=$marketId, error=${e.message}")
}
}
// 再次从数据库查询(API可能已经保存)
return marketRepository.findByMarketId(marketId)?.also {
marketCache[marketId] = it
}
}
/**
* 批量获取市场信息
*/
fun getMarkets(marketIds: List<String>): Map<String, Market> {
val result = mutableMapOf<String, Market>()
val missingIds = mutableListOf<String>()
// 1. 从缓存和数据库获取
for (marketId in marketIds) {
val market = getMarket(marketId)
if (market != null) {
result[marketId] = market
} else {
missingIds.add(marketId)
}
}
// 2. 批量从API获取缺失的市场信息
if (missingIds.isNotEmpty()) {
runBlocking {
try {
fetchAndSaveMarkets(missingIds)
} catch (e: Exception) {
logger.warn("批量获取市场信息失败: marketIds=$missingIds, error=${e.message}")
}
}
// 再次从数据库查询
val savedMarkets = marketRepository.findByMarketIdIn(missingIds)
for (market in savedMarkets) {
result[market.marketId] = market
marketCache[market.marketId] = market
}
}
return result
}
/**
* 从API获取市场信息并保存到数据库
*/
private suspend fun fetchAndSaveMarket(marketId: String): Market? {
return try {
val gammaApi = retrofitFactory.createGammaApi()
val response = gammaApi.listMarkets(conditionIds = listOf(marketId))
if (response.isSuccessful && response.body() != null) {
val markets = response.body()!!
if (markets.isNotEmpty()) {
val marketResponse = markets.first()
saveMarketFromResponse(marketId, marketResponse)
} else {
null
}
} else {
null
}
} catch (e: Exception) {
logger.error("从API获取市场信息失败: marketId=$marketId, error=${e.message}", e)
null
}
}
/**
* 批量从API获取市场信息并保存到数据库
*/
private suspend fun fetchAndSaveMarkets(marketIds: List<String>) {
if (marketIds.isEmpty()) return
try {
val gammaApi = retrofitFactory.createGammaApi()
val response = gammaApi.listMarkets(conditionIds = marketIds)
if (response.isSuccessful && response.body() != null) {
val markets = response.body()!!
val marketMap = markets.associateBy { it.conditionId ?: "" }
for (marketId in marketIds) {
val marketResponse = marketMap[marketId]
if (marketResponse != null) {
saveMarketFromResponse(marketId, marketResponse)
}
}
}
} catch (e: Exception) {
logger.error("批量从API获取市场信息失败: marketIds=$marketIds, error=${e.message}", e)
}
}
/**
* 从API响应保存市场信息到数据库
*/
private fun saveMarketFromResponse(marketId: String, marketResponse: MarketResponse): Market? {
return try {
val existingMarket = marketRepository.findByMarketId(marketId)
val market = if (existingMarket != null) {
// 更新现有市场信息
existingMarket.copy(
title = marketResponse.question ?: existingMarket.title,
slug = marketResponse.slug ?: existingMarket.slug,
category = marketResponse.category ?: existingMarket.category,
icon = marketResponse.icon ?: existingMarket.icon,
image = marketResponse.image ?: existingMarket.image,
description = marketResponse.description ?: existingMarket.description,
active = marketResponse.active ?: existingMarket.active,
closed = marketResponse.closed ?: existingMarket.closed,
archived = marketResponse.archived ?: existingMarket.archived,
updatedAt = System.currentTimeMillis()
)
} else {
// 创建新市场信息
Market(
marketId = marketId,
title = marketResponse.question ?: marketId,
slug = marketResponse.slug,
category = marketResponse.category,
icon = marketResponse.icon,
image = marketResponse.image,
description = marketResponse.description,
active = marketResponse.active ?: true,
closed = marketResponse.closed ?: false,
archived = marketResponse.archived ?: false
)
}
val savedMarket = marketRepository.save(market)
marketCache[marketId] = savedMarket
savedMarket
} catch (e: Exception) {
logger.error("保存市场信息失败: marketId=$marketId, error=${e.message}", e)
null
}
}
/**
* 清除缓存(用于测试或手动刷新)
*/
fun clearCache() {
marketCache.clear()
}
}
@@ -32,7 +32,8 @@ class CopyTradingStatisticsService(
private val accountRepository: AccountRepository,
private val leaderRepository: LeaderRepository,
private val accountService: AccountService,
private val blockchainService: BlockchainService
private val blockchainService: BlockchainService,
private val marketService: com.wrbug.polymarketbot.service.common.MarketService
) {
private val logger = LoggerFactory.getLogger(CopyTradingStatisticsService::class.java)
@@ -165,13 +166,19 @@ class CopyTradingStatisticsService(
val end = minOf(start + limit, orders.size)
val pagedOrders = if (start < orders.size) orders.subList(start, end) else emptyList()
// 批量获取市场信息
val marketIds = pagedOrders.map { it.marketId }.distinct()
val markets = marketService.getMarkets(marketIds)
// 转换为DTO
val list = pagedOrders.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,
side = order.side,
quantity = order.quantity.toString(),
price = order.price.toString(),
@@ -212,13 +219,19 @@ class CopyTradingStatisticsService(
val end = minOf(start + limit, records.size)
val pagedRecords = if (start < records.size) records.subList(start, end) else emptyList()
// 批量获取市场信息
val marketIds = pagedRecords.map { it.marketId }.distinct()
val markets = marketService.getMarkets(marketIds)
// 转换为DTO
val list = pagedRecords.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,
side = record.side,
quantity = record.totalMatchedQuantity.toString(),
price = record.sellPrice.toString(),
@@ -263,11 +276,23 @@ class CopyTradingStatisticsService(
val end = minOf(start + limit, filtered.size)
val pagedDetails = if (start < filtered.size) filtered.subList(start, end) else emptyList()
// 获取匹配记录以获取市场ID
val matchRecordIds = pagedDetails.map { it.matchRecordId }.distinct()
val matchRecords = matchRecordIds.mapNotNull { id ->
sellMatchRecordRepository.findById(id).orElse(null)
}
val marketIds = matchRecords.map { it.marketId }.distinct()
val markets = marketService.getMarkets(marketIds)
// 转换为DTO
val list = pagedDetails.map { detail ->
val matchRecord = matchRecords.find { it.id == detail.matchRecordId }
val market = matchRecord?.let { markets[it.marketId] }
MatchedOrderInfo(
sellOrderId = sellMatchRecordRepository.findById(detail.matchRecordId).orElse(null)?.sellOrderId ?: "",
sellOrderId = matchRecord?.sellOrderId ?: "",
buyOrderId = detail.buyOrderId,
marketId = matchRecord?.marketId,
marketTitle = market?.title,
matchedQuantity = detail.matchedQuantity.toString(),
buyPrice = detail.buyPrice.toString(),
sellPrice = detail.sellPrice.toString(),