Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f6f5866118 | |||
| b1e69135b8 | |||
| 0c7f34a28a | |||
| c53fcde5d7 | |||
| 81a620af12 | |||
| 5f44a0ca20 | |||
| 5d2cf945f3 | |||
| 227a38fa89 | |||
| abcc004606 | |||
| cc40493ec6 | |||
| 692cbd9a80 | |||
| 43de0104e2 | |||
| 2ccab42894 | |||
| 3008cbcb50 | |||
| 42a318b501 | |||
| ecb737ec67 | |||
| 0f3baec6cb | |||
| 764d684846 |
@@ -58,7 +58,10 @@ data class CopyOrderTracking(
|
||||
|
||||
@Column(name = "notification_sent", nullable = false)
|
||||
var notificationSent: Boolean = false, // 是否已发送通知(从订单详情获取实际数据后发送)
|
||||
|
||||
|
||||
@Column(name = "source", nullable = false, length = 20)
|
||||
val source: String, // 订单来源:activity-ws(Polymarket WebSocket)、onchain-ws(OnChain WebSocket)
|
||||
|
||||
@Column(name = "created_at", nullable = false)
|
||||
val createdAt: Long = System.currentTimeMillis(),
|
||||
|
||||
|
||||
+16
-18
@@ -21,29 +21,29 @@ import java.util.concurrent.CopyOnWriteArrayList
|
||||
class PositionPollingService(
|
||||
private val accountService: AccountService
|
||||
) {
|
||||
|
||||
|
||||
private val logger = LoggerFactory.getLogger(PositionPollingService::class.java)
|
||||
|
||||
|
||||
@Value("\${position.polling.interval:2000}")
|
||||
private var pollingInterval: Long = 2000 // 轮训间隔(毫秒),默认2秒
|
||||
|
||||
|
||||
// 订阅者列表(支持多个订阅者)
|
||||
private val subscribers = CopyOnWriteArrayList<(PositionListResponse) -> Unit>()
|
||||
|
||||
|
||||
// 最新仓位数据(用于丢弃机制)
|
||||
@Volatile
|
||||
private var latestPositions: PositionListResponse? = null
|
||||
|
||||
|
||||
// 协程作用域和任务
|
||||
private val scope = CoroutineScope(Dispatchers.Default + SupervisorJob())
|
||||
private var pollingJob: Job? = null
|
||||
|
||||
|
||||
// 事件分发协程(使用专门的线程,避免阻塞轮训)
|
||||
private val eventDispatcherScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||
|
||||
|
||||
// 同步锁,确保轮询任务的启动和停止是线程安全的
|
||||
private val lock = Any()
|
||||
|
||||
|
||||
/**
|
||||
* 初始化服务(后端启动时直接启动轮训)
|
||||
*/
|
||||
@@ -52,7 +52,7 @@ class PositionPollingService(
|
||||
logger.info("PositionPollingService 初始化,启动仓位轮训任务,轮训间隔: ${pollingInterval}ms")
|
||||
startPolling()
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 清理资源
|
||||
*/
|
||||
@@ -66,7 +66,7 @@ class PositionPollingService(
|
||||
scope.cancel()
|
||||
eventDispatcherScope.cancel()
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 订阅仓位事件
|
||||
* @param callback 回调函数,接收最新的仓位数据
|
||||
@@ -78,7 +78,7 @@ class PositionPollingService(
|
||||
latestPositions?.let { callback(it) }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 取消订阅仓位事件
|
||||
*/
|
||||
@@ -87,7 +87,7 @@ class PositionPollingService(
|
||||
subscribers.remove(callback)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 启动轮训任务
|
||||
*/
|
||||
@@ -95,7 +95,7 @@ class PositionPollingService(
|
||||
synchronized(lock) {
|
||||
// 如果已经有轮训任务在运行,先取消
|
||||
pollingJob?.cancel()
|
||||
|
||||
|
||||
// 启动新的轮训任务
|
||||
pollingJob = scope.launch {
|
||||
while (isActive) {
|
||||
@@ -109,7 +109,7 @@ class PositionPollingService(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 轮训仓位数据并发布事件
|
||||
* 使用专门的线程分发事件,避免阻塞轮训
|
||||
@@ -123,7 +123,7 @@ class PositionPollingService(
|
||||
if (positions != null) {
|
||||
// 更新最新数据(丢弃旧数据,只保留最新的)
|
||||
latestPositions = positions
|
||||
|
||||
|
||||
// 在专门的线程中分发事件,避免阻塞轮训
|
||||
eventDispatcherScope.launch {
|
||||
try {
|
||||
@@ -131,7 +131,7 @@ class PositionPollingService(
|
||||
val currentSubscribers = synchronized(lock) {
|
||||
subscribers.toList() // 复制列表,避免并发修改
|
||||
}
|
||||
|
||||
|
||||
currentSubscribers.forEach { callback ->
|
||||
try {
|
||||
callback(positions)
|
||||
@@ -139,8 +139,6 @@ class PositionPollingService(
|
||||
logger.error("通知订阅者失败: ${e.message}", e)
|
||||
}
|
||||
}
|
||||
|
||||
logger.debug("发布仓位数据事件: currentPositions=${positions.currentPositions.size}, historyPositions=${positions.historyPositions.size}, subscribers=${currentSubscribers.size}")
|
||||
} catch (e: Exception) {
|
||||
logger.error("分发仓位数据事件失败: ${e.message}", e)
|
||||
}
|
||||
|
||||
+2
-2
@@ -114,10 +114,10 @@ class AccountOnChainMonitorService(
|
||||
}
|
||||
|
||||
val receiptRpcResponse = receiptResponse.body()!!
|
||||
if (receiptRpcResponse.error != null || receiptRpcResponse.result == null) {
|
||||
if (receiptRpcResponse.error != null || receiptRpcResponse.result == null || receiptRpcResponse.result.isJsonNull) {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
// 使用 Gson 解析 receipt JSON
|
||||
val receiptJson = receiptRpcResponse.result.asJsonObject
|
||||
|
||||
|
||||
+1
-1
@@ -213,7 +213,7 @@ class CopyTradingWebSocketService(
|
||||
// 处理交易
|
||||
scope.launch {
|
||||
try {
|
||||
copyOrderTrackingService.processTrade(leaderId, trade, "websocket")
|
||||
copyOrderTrackingService.processTrade(leaderId, trade, "activity-ws")
|
||||
} catch (e: Exception) {
|
||||
logger.error("处理交易失败: leaderId=$leaderId, tradeId=${trade.id}", e)
|
||||
}
|
||||
|
||||
+16
@@ -1,5 +1,7 @@
|
||||
package com.wrbug.polymarketbot.service.copytrading.monitor
|
||||
|
||||
import com.github.benmanes.caffeine.cache.Cache
|
||||
import com.github.benmanes.caffeine.cache.Caffeine
|
||||
import com.google.gson.JsonNull
|
||||
import com.wrbug.polymarketbot.api.*
|
||||
import com.wrbug.polymarketbot.entity.Leader
|
||||
@@ -12,6 +14,7 @@ import okhttp3.OkHttpClient
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.stereotype.Service
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
/**
|
||||
* 链上 WebSocket 监听服务
|
||||
@@ -30,6 +33,11 @@ class OnChainWsService(
|
||||
// 存储需要监听的Leader:leaderId -> Leader
|
||||
private val monitoredLeaders = ConcurrentHashMap<Long, Leader>()
|
||||
|
||||
// 存储已处理的交易哈希,用于去重(LRU 缓存,保留最近 100 条)
|
||||
private val processedTxHashes: Cache<String, Long> = Caffeine.newBuilder()
|
||||
.maximumSize(100)
|
||||
.build()
|
||||
|
||||
/**
|
||||
* 启动链上 WebSocket 监听
|
||||
* 通过统一服务订阅所有 Leader
|
||||
@@ -95,6 +103,14 @@ class OnChainWsService(
|
||||
) {
|
||||
val leader = monitoredLeaders[leaderId] ?: return
|
||||
|
||||
// 根据 txHash 去重(使用原子操作避免竞态条件)
|
||||
val currentTime = System.currentTimeMillis()
|
||||
val existingTimestamp = processedTxHashes.asMap().putIfAbsent(txHash, currentTime)
|
||||
if (existingTimestamp != null) {
|
||||
logger.debug("交易已处理过,跳过: leaderId=$leaderId, txHash=$txHash, firstProcessedAt=$existingTimestamp")
|
||||
return
|
||||
}
|
||||
|
||||
logger.debug("开始处理 Leader 交易: leaderId=$leaderId, txHash=$txHash, leaderAddress=${leader.leaderAddress}")
|
||||
|
||||
try {
|
||||
|
||||
+123
-22
@@ -1,5 +1,7 @@
|
||||
package com.wrbug.polymarketbot.service.copytrading.monitor
|
||||
|
||||
import com.github.benmanes.caffeine.cache.Cache
|
||||
import com.github.benmanes.caffeine.cache.Caffeine
|
||||
import com.wrbug.polymarketbot.api.TradeResponse
|
||||
import com.wrbug.polymarketbot.dto.ActivityTradeMessage
|
||||
import com.wrbug.polymarketbot.dto.ActivityTradePayload
|
||||
@@ -15,10 +17,11 @@ import org.slf4j.LoggerFactory
|
||||
import org.springframework.stereotype.Service
|
||||
import java.math.BigDecimal
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
/**
|
||||
* Polymarket Activity WebSocket 监听服务
|
||||
* 通过订阅全局 activity 交易流,客户端过滤 Leader 地址,实现实时交易检测
|
||||
* 通过订阅全局 activity 交易流(trades + orders_matched),客户端过滤 Leader 地址,实现实时交易检测
|
||||
* 延迟 < 100ms,适合快速跟单场景
|
||||
*/
|
||||
@Service
|
||||
@@ -39,6 +42,13 @@ class PolymarketActivityWsService(
|
||||
// 要监听的 Leader 地址集合(小写地址 -> leaderId)
|
||||
private val monitoredAddresses = ConcurrentHashMap<String, Long>()
|
||||
|
||||
// 存储已处理的交易哈希,用于去重(LRU 缓存,保留最近 100 条)
|
||||
// 因为同时订阅 trades 和 orders_matched,同一个交易可能被推送两次
|
||||
private val processedTxHashes: Cache<String, Long> = Caffeine.newBuilder()
|
||||
.maximumSize(100)
|
||||
.expireAfterWrite(10, TimeUnit.MINUTES)
|
||||
.build()
|
||||
|
||||
// 是否已订阅
|
||||
@Volatile
|
||||
private var isSubscribed = false
|
||||
@@ -50,6 +60,12 @@ class PolymarketActivityWsService(
|
||||
// Activity 消息超时检测任务
|
||||
private var activityTimeoutJob: Job? = null
|
||||
|
||||
// 性能统计
|
||||
private var totalMessagesProcessed = 0L
|
||||
private var addressMatchMessages = 0L
|
||||
private var jsonParseMessages = 0L
|
||||
private var duplicateTxHashMessages = 0L
|
||||
|
||||
/**
|
||||
* 启动监听
|
||||
*/
|
||||
@@ -68,7 +84,7 @@ class PolymarketActivityWsService(
|
||||
return
|
||||
}
|
||||
|
||||
logger.info("启动 Activity WebSocket 监听,监控 ${monitoredAddresses.size} 个 Leader 地址")
|
||||
logger.info("启动 Activity WebSocket 监听(trades + orders_matched),监控 ${monitoredAddresses.size} 个 Leader 地址")
|
||||
connectAndSubscribe()
|
||||
}
|
||||
|
||||
@@ -165,6 +181,7 @@ class PolymarketActivityWsService(
|
||||
* 订阅全局 activity
|
||||
* 根据 @polymarket/real-time-data-client 的协议格式
|
||||
* 使用 "action": "subscribe" 而不是 "type": "subscribe"
|
||||
* 同时订阅 trades 和 orders_matched 两种类型
|
||||
*/
|
||||
private fun subscribeAllActivity() {
|
||||
val client = wsClient
|
||||
@@ -176,6 +193,7 @@ class PolymarketActivityWsService(
|
||||
try {
|
||||
// 根据 real-time-data-client 的协议格式
|
||||
// 订阅消息应包含 "action": "subscribe" 和 "subscriptions" 数组
|
||||
// 同时订阅 trades 和 orders_matched 两种类型
|
||||
val subscribeMessage = """
|
||||
{
|
||||
"action": "subscribe",
|
||||
@@ -183,6 +201,10 @@ class PolymarketActivityWsService(
|
||||
{
|
||||
"topic": "activity",
|
||||
"type": "trades"
|
||||
},
|
||||
{
|
||||
"topic": "activity",
|
||||
"type": "orders_matched"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -193,8 +215,8 @@ class PolymarketActivityWsService(
|
||||
// 重置最后一次收到 activity 消息的时间
|
||||
lastActivityTime = System.currentTimeMillis()
|
||||
// 启动 Activity 消息超时检测
|
||||
startActivityTimeoutCheck()
|
||||
logger.info("Activity WebSocket 订阅成功(全局交易流)")
|
||||
// startActivityTimeoutCheck()
|
||||
logger.info("Activity WebSocket 订阅成功(全局交易流: trades + orders_matched)")
|
||||
} catch (e: Exception) {
|
||||
logger.error("订阅 Activity WebSocket 失败", e)
|
||||
isSubscribed = false
|
||||
@@ -208,24 +230,24 @@ class PolymarketActivityWsService(
|
||||
private fun startActivityTimeoutCheck() {
|
||||
// 先停止之前的检测任务
|
||||
stopActivityTimeoutCheck()
|
||||
|
||||
|
||||
activityTimeoutJob = scope.launch {
|
||||
while (isActive && isSubscribed) {
|
||||
delay(30000) // 每30秒检查一次
|
||||
|
||||
|
||||
// 如果已经取消订阅,停止检测
|
||||
if (!isSubscribed) {
|
||||
break
|
||||
}
|
||||
|
||||
|
||||
// 如果 lastActivityTime 为 0,说明还没有收到过消息,跳过本次检测
|
||||
if (lastActivityTime == 0L) {
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
val currentTime = System.currentTimeMillis()
|
||||
val timeSinceLastActivity = currentTime - lastActivityTime
|
||||
|
||||
|
||||
// 如果超过30秒没有收到activity消息,触发重连
|
||||
if (timeSinceLastActivity >= 30000) {
|
||||
logger.warn("超过30秒未收到 Activity 消息,触发重连。距离上次消息: ${timeSinceLastActivity}ms")
|
||||
@@ -240,7 +262,7 @@ class PolymarketActivityWsService(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 停止 Activity 消息超时检测
|
||||
*/
|
||||
@@ -249,42 +271,95 @@ class PolymarketActivityWsService(
|
||||
activityTimeoutJob = null
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查消息是否包含监听的 Leader 地址
|
||||
* 快速过滤,避免不必要的 JSON 解析
|
||||
* 只需要检查 "proxyWallet":"0x..." 或 "trader":{"address":"0x..."} 格式
|
||||
*/
|
||||
private fun containsMonitoredAddress(message: String): Boolean {
|
||||
// 快速检查:如果消息很短,不可能包含地址
|
||||
if (message.length < 50) {
|
||||
return false
|
||||
}
|
||||
|
||||
// 遍历所有监听的地址
|
||||
for ((address, leaderId) in monitoredAddresses) {
|
||||
// 检查 proxyWallet:格式为 "proxyWallet":"0x..."
|
||||
if (message.contains("\"proxyWallet\":\"$address\"", ignoreCase = true)) {
|
||||
addressMatchMessages++
|
||||
return true
|
||||
}
|
||||
|
||||
// 检查 trader.address:格式为 "trader":{"address":"0x..."}
|
||||
if (message.contains("\"trader\"", ignoreCase = true) &&
|
||||
message.contains("\"address\":\"$address\"", ignoreCase = true)
|
||||
) {
|
||||
addressMatchMessages++
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理消息
|
||||
*/
|
||||
private fun handleMessage(message: String) {
|
||||
try {
|
||||
totalMessagesProcessed++
|
||||
|
||||
// 处理 PONG 响应
|
||||
if (message.trim() == "PONG" || message.trim() == "pong") {
|
||||
return
|
||||
}
|
||||
|
||||
// 使用扩展函数解析消息
|
||||
|
||||
// 快速预检查:检查是否包含监听地址
|
||||
// 绝大部分消息会在这一步被过滤掉,避免不必要的 JSON 解析
|
||||
if (!containsMonitoredAddress(message)) {
|
||||
return
|
||||
}
|
||||
logger.info("发现leader交易:${message}")
|
||||
// 使用扩展函数解析消息(只对包含监听地址的消息)
|
||||
val tradeMessage = message.fromJson<ActivityTradeMessage>() ?: run {
|
||||
// 不是有效的 JSON 或格式不匹配,跳过
|
||||
logger.warn("无法解析为 ActivityTradeMessage,可能不是 activity 消息: ${message.take(200)}")
|
||||
logger.warn("无法解析为 ActivityTradeMessage: ${message.take(200)}")
|
||||
return
|
||||
}
|
||||
|
||||
// 检查是否是 activity trade 消息
|
||||
if (tradeMessage.topic != "activity" || tradeMessage.type != "trades") {
|
||||
// 不是我们关心的消息,直接返回
|
||||
|
||||
jsonParseMessages++
|
||||
|
||||
// 检查是否是 activity 消息(trades 或 orders_matched)
|
||||
if (tradeMessage.topic != "activity" ||
|
||||
(tradeMessage.type != "trades" && tradeMessage.type != "orders_matched")) {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
// 更新最后一次收到 activity 消息的时间(即使不是我们监听的 Leader 的交易)
|
||||
lastActivityTime = System.currentTimeMillis()
|
||||
|
||||
|
||||
val payload = tradeMessage.payload
|
||||
|
||||
|
||||
// 根据 txHash 去重(使用原子操作避免竞态条件)
|
||||
val txHash = payload.transactionHash
|
||||
if (txHash != null && txHash.isNotBlank()) {
|
||||
val currentTime = System.currentTimeMillis()
|
||||
val existingTimestamp = processedTxHashes.asMap().putIfAbsent(txHash, currentTime)
|
||||
if (existingTimestamp != null) {
|
||||
duplicateTxHashMessages++
|
||||
logger.debug("交易已处理过,跳过: txHash=$txHash, firstProcessedAt=$existingTimestamp, type=${tradeMessage.type}")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 提取交易者地址
|
||||
val traderAddress = extractTraderAddress(payload) ?: run {
|
||||
// 没有交易者地址,跳过
|
||||
logger.warn("Activity Trade 消息中没有交易者地址: trader=${payload.trader}, proxyWallet=${payload.proxyWallet}, asset=${payload.asset}")
|
||||
return
|
||||
}
|
||||
|
||||
// 检查是否是我们监听的 Leader
|
||||
|
||||
// 二次验证:确认地址匹配
|
||||
val normalizedAddress = traderAddress.lowercase()
|
||||
val leaderId = monitoredAddresses[normalizedAddress] ?: run {
|
||||
return
|
||||
@@ -449,6 +524,7 @@ class PolymarketActivityWsService(
|
||||
wsClient = null
|
||||
isSubscribed = false
|
||||
monitoredAddresses.clear()
|
||||
processedTxHashes.invalidateAll() // 清空去重缓存
|
||||
lastActivityTime = 0
|
||||
}
|
||||
|
||||
@@ -466,8 +542,33 @@ class PolymarketActivityWsService(
|
||||
return monitoredAddresses.size
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取性能统计信息
|
||||
*/
|
||||
fun getPerformanceStats(): Map<String, Any> {
|
||||
val jsonParseRate = if (totalMessagesProcessed > 0) {
|
||||
(jsonParseMessages.toDouble() / totalMessagesProcessed * 100).toInt()
|
||||
} else {
|
||||
0
|
||||
}
|
||||
|
||||
return mapOf(
|
||||
"totalMessages" to totalMessagesProcessed,
|
||||
"addressMatches" to addressMatchMessages,
|
||||
"jsonParses" to jsonParseMessages,
|
||||
"duplicateTxHashes" to duplicateTxHashMessages,
|
||||
"jsonParseRate" to "$jsonParseRate%",
|
||||
"filteringEfficiency" to if (totalMessagesProcessed > 0) {
|
||||
((1.0 - jsonParseMessages.toDouble() / totalMessagesProcessed) * 100).toInt()
|
||||
} else {
|
||||
0
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@PreDestroy
|
||||
fun destroy() {
|
||||
logger.info("Activity WS 性能统计: ${getPerformanceStats()}")
|
||||
stop()
|
||||
scope.cancel()
|
||||
}
|
||||
|
||||
+4
-3
@@ -140,7 +140,7 @@ open class CopyOrderTrackingService(
|
||||
|
||||
// 2. 处理交易逻辑
|
||||
val result = when (trade.side.uppercase()) {
|
||||
"BUY" -> processBuyTrade(leaderId, trade)
|
||||
"BUY" -> processBuyTrade(leaderId, trade, source)
|
||||
"SELL" -> processSellTrade(leaderId, trade)
|
||||
else -> {
|
||||
logger.warn("未知的交易方向: ${trade.side}")
|
||||
@@ -213,7 +213,7 @@ open class CopyOrderTrackingService(
|
||||
* 创建跟单买入订单并记录到跟踪表
|
||||
*/
|
||||
@Transactional
|
||||
suspend fun processBuyTrade(leaderId: Long, trade: TradeResponse): Result<Unit> {
|
||||
suspend fun processBuyTrade(leaderId: Long, trade: TradeResponse, source: String): Result<Unit> {
|
||||
return try {
|
||||
// 1. 查找所有启用且支持该Leader的跟单关系
|
||||
val copyTradings = copyTradingRepository.findByLeaderIdAndEnabledTrue(leaderId)
|
||||
@@ -622,7 +622,8 @@ open class CopyOrderTrackingService(
|
||||
price = buyPrice, // 使用下单价格,临时值
|
||||
remainingQuantity = finalBuyQuantity,
|
||||
status = "filled",
|
||||
notificationSent = false // 标记为未发送通知,等待轮询任务获取实际数据后发送
|
||||
notificationSent = false, // 标记为未发送通知,等待轮询任务获取实际数据后发送
|
||||
source = source // 订单来源
|
||||
)
|
||||
|
||||
copyOrderTrackingRepository.save(tracking)
|
||||
|
||||
+21
-13
@@ -611,22 +611,22 @@ class CopyTradingStatisticsService(
|
||||
|
||||
// 4. 转换为分组数据并计算统计信息
|
||||
val marketIds = groups.keys.toList()
|
||||
|
||||
|
||||
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(),
|
||||
@@ -636,10 +636,10 @@ class CopyTradingStatisticsService(
|
||||
partiallyMatchedCount = partiallyMatchedCount.toLong(),
|
||||
filledCount = filledCount.toLong()
|
||||
)
|
||||
|
||||
|
||||
// 排序(按创建时间倒序)
|
||||
marketOrders.sortByDescending { it.createdAt }
|
||||
|
||||
|
||||
// 转换为 DTO
|
||||
val orderDtos = marketOrders.map { order ->
|
||||
val amount = order.quantity.toSafeBigDecimal().multi(order.price)
|
||||
@@ -662,7 +662,7 @@ class CopyTradingStatisticsService(
|
||||
createdAt = order.createdAt
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
MarketOrderGroup(
|
||||
marketId = marketId,
|
||||
marketTitle = markets[marketId]?.title,
|
||||
@@ -672,27 +672,35 @@ class CopyTradingStatisticsService(
|
||||
stats = stats,
|
||||
orders = orderDtos as List<Any>
|
||||
)
|
||||
}.sortedByDescending { it.stats.count }
|
||||
|
||||
}.sortedByDescending { group ->
|
||||
// 找出该市场最近的买入订单时间
|
||||
group.orders.mapNotNull { order ->
|
||||
when (order) {
|
||||
is BuyOrderInfo -> order.createdAt
|
||||
else -> null
|
||||
}
|
||||
}.maxOrNull() ?: 0L
|
||||
}
|
||||
|
||||
// 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)
|
||||
logger.error("获取按市场分组的卖出订单列表失败: copyTradingId=${request.copyTradingId}", e)
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
|
||||
+282
-179
@@ -18,6 +18,7 @@ import org.springframework.scheduling.annotation.Scheduled
|
||||
import org.springframework.stereotype.Service
|
||||
import org.springframework.transaction.annotation.Transactional
|
||||
import java.math.BigDecimal
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
/**
|
||||
* 订单状态更新服务
|
||||
@@ -37,16 +38,25 @@ class OrderStatusUpdateService(
|
||||
private val marketService: MarketService, // 市场信息服务
|
||||
private val telegramNotificationService: TelegramNotificationService?
|
||||
) {
|
||||
|
||||
|
||||
private val logger = LoggerFactory.getLogger(OrderStatusUpdateService::class.java)
|
||||
|
||||
|
||||
private val updateScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||
|
||||
|
||||
// 缓存首次检测到订单详情为 null 的时间戳(订单ID -> 首次检测时间)
|
||||
private val orderNullDetectionTime = ConcurrentHashMap<String, Long>()
|
||||
|
||||
// 订单详情为 null 的重试时间窗口(1分钟)
|
||||
private val ORDER_NULL_RETRY_WINDOW_MS = 60000L
|
||||
|
||||
// 订单详情为 null 但已部分卖出的清理时间窗口(1小时)
|
||||
private val PARTIAL_SOLD_CLEANUP_WINDOW_MS = 3600000L
|
||||
|
||||
@EventListener(ApplicationReadyEvent::class)
|
||||
fun onApplicationReady() {
|
||||
logger.info("订单状态更新服务已启动,将每5秒轮询一次")
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 定时更新订单状态
|
||||
* 每5秒执行一次
|
||||
@@ -57,13 +67,13 @@ class OrderStatusUpdateService(
|
||||
try {
|
||||
// 1. 清理已删除账户的订单
|
||||
cleanupDeletedAccountOrders()
|
||||
|
||||
|
||||
// 2. 检查30秒前创建的订单,如果未成交则删除
|
||||
checkAndDeleteUnfilledOrders()
|
||||
|
||||
|
||||
// 3. 更新卖出订单的实际成交价并发送通知(priceUpdated 共用字段)
|
||||
updatePendingSellOrderPrices()
|
||||
|
||||
|
||||
// 4. 更新买入订单的实际数据并发送通知
|
||||
updatePendingBuyOrders()
|
||||
} catch (e: Exception) {
|
||||
@@ -71,11 +81,11 @@ class OrderStatusUpdateService(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 验证订单ID格式
|
||||
* 订单ID必须以 0x 开头,且是有效的 16 进制字符串
|
||||
*
|
||||
*
|
||||
* @param orderId 订单ID
|
||||
* @return 如果格式有效返回 true,否则返回 false
|
||||
*/
|
||||
@@ -91,7 +101,7 @@ class OrderStatusUpdateService(
|
||||
// 检查是否只包含 0-9, a-f, A-F
|
||||
return hexPart.all { it in '0'..'9' || it in 'a'..'f' || it in 'A'..'F' }
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 清理已删除账户的订单
|
||||
*/
|
||||
@@ -100,41 +110,41 @@ class OrderStatusUpdateService(
|
||||
try {
|
||||
// 查询所有卖出记录
|
||||
val allRecords = sellMatchRecordRepository.findAll()
|
||||
|
||||
|
||||
// 查询所有有效的账户ID
|
||||
val validAccountIds = accountRepository.findAll().mapNotNull { it.id }.toSet()
|
||||
|
||||
|
||||
// 查询所有有效的跟单关系
|
||||
val validCopyTradingIds = copyTradingRepository.findAll()
|
||||
.filter { it.accountId in validAccountIds }
|
||||
.mapNotNull { it.id }
|
||||
.toSet()
|
||||
|
||||
|
||||
// 找出需要删除的记录(关联的跟单关系已不存在或账户已删除)
|
||||
val recordsToDelete = allRecords.filter { record ->
|
||||
val copyTrading = copyTradingRepository.findById(record.copyTradingId).orElse(null)
|
||||
copyTrading == null || copyTrading.accountId !in validAccountIds
|
||||
}
|
||||
|
||||
|
||||
if (recordsToDelete.isNotEmpty()) {
|
||||
logger.info("清理已删除账户的订单: ${recordsToDelete.size} 条记录")
|
||||
|
||||
|
||||
// 删除匹配明细
|
||||
for (record in recordsToDelete) {
|
||||
val details = sellMatchDetailRepository.findByMatchRecordId(record.id!!)
|
||||
sellMatchDetailRepository.deleteAll(details)
|
||||
}
|
||||
|
||||
|
||||
// 删除卖出记录
|
||||
sellMatchRecordRepository.deleteAll(recordsToDelete)
|
||||
|
||||
|
||||
logger.info("已清理 ${recordsToDelete.size} 条已删除账户的订单记录")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logger.error("清理已删除账户订单异常: ${e.message}", e)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 检查30秒前创建的订单,如果未成交则删除
|
||||
* 首次检测但加入缓存中30s后还没有成交,则删除
|
||||
@@ -144,24 +154,24 @@ class OrderStatusUpdateService(
|
||||
try {
|
||||
// 计算30秒前的时间戳
|
||||
val thirtySecondsAgo = System.currentTimeMillis() - 30000
|
||||
|
||||
|
||||
// 查询30秒前创建的订单,并过滤掉已经完全匹配的订单
|
||||
// 已经完全匹配的订单(status = "fully_matched")不需要再检查
|
||||
// 使用数据库查询过滤,避免加载过多数据
|
||||
val ordersToCheck = copyOrderTrackingRepository.findByCreatedAtBeforeAndStatusNot(
|
||||
thirtySecondsAgo,
|
||||
thirtySecondsAgo,
|
||||
"fully_matched"
|
||||
)
|
||||
|
||||
|
||||
if (ordersToCheck.isEmpty()) {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
logger.debug("检查 ${ordersToCheck.size} 个30秒前创建的订单是否成交")
|
||||
|
||||
|
||||
// 按账户分组,避免重复创建 API 客户端
|
||||
val ordersByAccount = ordersToCheck.groupBy { it.accountId }
|
||||
|
||||
|
||||
for ((accountId, orders) in ordersByAccount) {
|
||||
try {
|
||||
// 获取账户
|
||||
@@ -170,13 +180,13 @@ class OrderStatusUpdateService(
|
||||
logger.warn("账户不存在,跳过检查: accountId=$accountId")
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
// 检查账户是否配置了 API 凭证
|
||||
if (account.apiKey == null || account.apiSecret == null || account.apiPassphrase == null) {
|
||||
logger.debug("账户未配置 API 凭证,跳过检查: accountId=${account.id}")
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
// 解密 API 凭证
|
||||
val apiSecret = try {
|
||||
cryptoUtils.decrypt(account.apiSecret!!)
|
||||
@@ -184,14 +194,14 @@ class OrderStatusUpdateService(
|
||||
logger.warn("解密 API Secret 失败: accountId=${account.id}, error=${e.message}")
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
val apiPassphrase = try {
|
||||
cryptoUtils.decrypt(account.apiPassphrase!!)
|
||||
} catch (e: Exception) {
|
||||
logger.warn("解密 API Passphrase 失败: accountId=${account.id}, error=${e.message}")
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
// 创建带认证的 CLOB API 客户端
|
||||
val clobApi = retrofitFactory.createClobApi(
|
||||
account.apiKey!!,
|
||||
@@ -199,13 +209,13 @@ class OrderStatusUpdateService(
|
||||
apiPassphrase,
|
||||
account.walletAddress
|
||||
)
|
||||
|
||||
|
||||
// 检查每个订单
|
||||
for (order in orders) {
|
||||
try {
|
||||
// 查询订单详情
|
||||
val orderResponse = clobApi.getOrder(order.buyOrderId)
|
||||
|
||||
|
||||
// 先检查 HTTP 状态码,非 200 的都跳过
|
||||
if (orderResponse.code() != 200) {
|
||||
// HTTP 非 200,记录日志并跳过,等待下次轮询
|
||||
@@ -214,30 +224,72 @@ class OrderStatusUpdateService(
|
||||
logger.debug("订单查询失败(HTTP非200),等待下次轮询: orderId=${order.buyOrderId}, copyOrderTrackingId=${order.id}, code=${orderResponse.code()}, errorBody=$errorBody")
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
// HTTP 200,检查响应体
|
||||
// 响应体也可能返回字符串 "null",Gson 解析时会返回 null
|
||||
val orderDetail = orderResponse.body()
|
||||
if (orderDetail == null) {
|
||||
// HTTP 200 且响应体为 null(或字符串 "null"),表示订单不存在
|
||||
// HTTP 200 且响应体为 null(或字符串 "null"),可能是网络异常或 API 暂时不可用
|
||||
// 使用兜底逻辑:首次检测不删除,1分钟后仍为 null 才删除
|
||||
val firstDetectionTime =
|
||||
orderNullDetectionTime.getOrPut(order.buyOrderId) { System.currentTimeMillis() }
|
||||
val currentTime = System.currentTimeMillis()
|
||||
|
||||
// 检查订单是否已部分卖出,如果已部分卖出则保留订单用于统计
|
||||
val hasMatchedDetails = sellMatchDetailRepository.findByTrackingId(order.id!!).isNotEmpty()
|
||||
val hasMatchedDetails =
|
||||
sellMatchDetailRepository.findByTrackingId(order.id!!).isNotEmpty()
|
||||
if (hasMatchedDetails || order.matchedQuantity > BigDecimal.ZERO) {
|
||||
logger.debug("订单不存在但已部分卖出,保留订单用于统计: orderId=${order.buyOrderId}, copyOrderTrackingId=${order.id}, matchedQuantity=${order.matchedQuantity}")
|
||||
// 检查是否超过清理时间窗口(1小时)
|
||||
val orderAge = currentTime - order.createdAt
|
||||
if (orderAge >= PARTIAL_SOLD_CLEANUP_WINDOW_MS) {
|
||||
logger.warn("订单详情为 null 且已部分卖出,但超过清理时间窗口,删除本地订单: orderId=${order.buyOrderId}, copyOrderTrackingId=${order.id}, matchedQuantity=${order.matchedQuantity}, 已等待=${orderAge / 1000}s")
|
||||
try {
|
||||
copyOrderTrackingRepository.deleteById(order.id!!)
|
||||
logger.info("已删除本地订单(超时清理): orderId=${order.buyOrderId}, copyOrderTrackingId=${order.id}")
|
||||
// 清除缓存
|
||||
orderNullDetectionTime.remove(order.buyOrderId)
|
||||
} catch (e: Exception) {
|
||||
logger.error(
|
||||
"删除本地订单失败: orderId=${order.buyOrderId}, copyOrderTrackingId=${order.id}, error=${e.message}",
|
||||
e
|
||||
)
|
||||
}
|
||||
continue
|
||||
} else {
|
||||
logger.debug("订单详情为 null 但已部分卖出,保留订单用于统计: orderId=${order.buyOrderId}, copyOrderTrackingId=${order.id}, matchedQuantity=${order.matchedQuantity}, 已等待=${orderAge / 1000}s")
|
||||
// 清除缓存,下次重新检测
|
||||
orderNullDetectionTime.remove(order.buyOrderId)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// 检查是否超过重试时间窗口
|
||||
if (currentTime - firstDetectionTime < ORDER_NULL_RETRY_WINDOW_MS) {
|
||||
// 未超过重试窗口,记录日志并等待下次轮询
|
||||
val elapsedSeconds = ((currentTime - firstDetectionTime) / 1000).toInt()
|
||||
logger.debug("订单详情为 null(可能是网络异常),等待重试: orderId=${order.buyOrderId}, copyOrderTrackingId=${order.id}, 已等待=${elapsedSeconds}s, 重试窗口=${ORDER_NULL_RETRY_WINDOW_MS / 1000}s")
|
||||
continue
|
||||
}
|
||||
|
||||
// 订单不存在且未部分卖出,删除本地订单
|
||||
logger.info("订单不存在(HTTP 200 但响应体为空),删除本地订单: orderId=${order.buyOrderId}, copyOrderTrackingId=${order.id}")
|
||||
|
||||
// 超过重试窗口,删除本地订单
|
||||
logger.warn("订单详情为 null 超过重试窗口,删除本地订单: orderId=${order.buyOrderId}, copyOrderTrackingId=${order.id}, 已等待=$((currentTime - firstDetectionTime) / 1000}s")
|
||||
try {
|
||||
copyOrderTrackingRepository.deleteById(order.id!!)
|
||||
logger.info("已删除本地订单: orderId=${order.buyOrderId}, copyOrderTrackingId=${order.id}")
|
||||
// 清除缓存
|
||||
orderNullDetectionTime.remove(order.buyOrderId)
|
||||
} catch (e: Exception) {
|
||||
logger.error("删除本地订单失败: orderId=${order.buyOrderId}, copyOrderTrackingId=${order.id}, error=${e.message}", e)
|
||||
logger.error(
|
||||
"删除本地订单失败: orderId=${order.buyOrderId}, copyOrderTrackingId=${order.id}, error=${e.message}",
|
||||
e
|
||||
)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
// 订单详情不为 null,清除缓存
|
||||
orderNullDetectionTime.remove(order.buyOrderId)
|
||||
|
||||
// 检查订单是否成交
|
||||
// 如果订单状态不是 FILLED 且已成交数量为0,说明未成交,删除
|
||||
val sizeMatched = orderDetail.sizeMatched?.toSafeBigDecimal() ?: BigDecimal.ZERO
|
||||
@@ -247,10 +299,11 @@ class OrderStatusUpdateService(
|
||||
copyOrderTrackingRepository.deleteById(order.id!!)
|
||||
logger.info("已删除未成交订单: orderId=${order.buyOrderId}, copyOrderTrackingId=${order.id}")
|
||||
} catch (e: Exception) {
|
||||
logger.error("删除未成交订单失败: orderId=${order.buyOrderId}, copyOrderTrackingId=${order.id}, error=${e.message}", e)
|
||||
logger.error(
|
||||
"删除未成交订单失败: orderId=${order.buyOrderId}, copyOrderTrackingId=${order.id}, error=${e.message}",
|
||||
e
|
||||
)
|
||||
}
|
||||
} else {
|
||||
logger.debug("订单已成交或部分成交,保留: orderId=${order.buyOrderId}, status=${orderDetail.status}, sizeMatched=$sizeMatched")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logger.error("检查订单失败: orderId=${order.buyOrderId}, error=${e.message}", e)
|
||||
@@ -264,7 +317,7 @@ class OrderStatusUpdateService(
|
||||
logger.error("检查未成交订单异常: ${e.message}", e)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 更新待更新的卖出订单价格
|
||||
* 注意:priceUpdated 现在同时表示价格已更新和通知已发送(共用字段)
|
||||
@@ -274,13 +327,13 @@ class OrderStatusUpdateService(
|
||||
try {
|
||||
// 查询所有价格未更新的卖出记录(priceUpdated = false 表示未处理)
|
||||
val pendingRecords = sellMatchRecordRepository.findByPriceUpdatedFalse()
|
||||
|
||||
|
||||
if (pendingRecords.isEmpty()) {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
logger.debug("找到 ${pendingRecords.size} 条待更新价格的卖出订单")
|
||||
|
||||
|
||||
for (record in pendingRecords) {
|
||||
try {
|
||||
// 获取跟单关系
|
||||
@@ -289,20 +342,20 @@ class OrderStatusUpdateService(
|
||||
logger.warn("跟单关系不存在,跳过更新: copyTradingId=${record.copyTradingId}")
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
// 获取账户
|
||||
val account = accountRepository.findById(copyTrading.accountId).orElse(null)
|
||||
if (account == null) {
|
||||
logger.warn("账户不存在,跳过更新: accountId=${copyTrading.accountId}")
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
// 检查账户是否配置了 API 凭证
|
||||
if (account.apiKey == null || account.apiSecret == null || account.apiPassphrase == null) {
|
||||
logger.debug("账户未配置 API 凭证,跳过更新: accountId=${account.id}")
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
// 解密 API 凭证
|
||||
val apiSecret = try {
|
||||
cryptoUtils.decrypt(account.apiSecret!!)
|
||||
@@ -310,14 +363,14 @@ class OrderStatusUpdateService(
|
||||
logger.warn("解密 API Secret 失败: accountId=${account.id}, error=${e.message}")
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
val apiPassphrase = try {
|
||||
cryptoUtils.decrypt(account.apiPassphrase!!)
|
||||
} catch (e: Exception) {
|
||||
logger.warn("解密 API Passphrase 失败: accountId=${account.id}, error=${e.message}")
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
// 创建带认证的 CLOB API 客户端
|
||||
val clobApi = retrofitFactory.createClobApi(
|
||||
account.apiKey!!,
|
||||
@@ -325,16 +378,16 @@ class OrderStatusUpdateService(
|
||||
apiPassphrase,
|
||||
account.walletAddress
|
||||
)
|
||||
|
||||
|
||||
// 如果 orderId 不是 0x 开头,直接标记为已处理(priceUpdated = true 表示已处理,包括价格更新和通知发送)
|
||||
if (!record.sellOrderId.startsWith("0x", ignoreCase = true)) {
|
||||
logger.debug("卖出订单ID非0x开头,直接标记为已处理: orderId=${record.sellOrderId}")
|
||||
|
||||
|
||||
// 检查是否为自动生成的订单(AUTO_ 或 AUTO_FIFO_ 开头),如果是则不发送通知
|
||||
val isAutoOrder = record.sellOrderId.startsWith("AUTO_", ignoreCase = true) ||
|
||||
record.sellOrderId.startsWith("AUTO_FIFO_", ignoreCase = true) ||
|
||||
record.sellOrderId.startsWith("AUTO_WS_", ignoreCase = true)
|
||||
|
||||
val isAutoOrder = record.sellOrderId.startsWith("AUTO_", ignoreCase = true) ||
|
||||
record.sellOrderId.startsWith("AUTO_FIFO_", ignoreCase = true) ||
|
||||
record.sellOrderId.startsWith("AUTO_WS_", ignoreCase = true)
|
||||
|
||||
if (!isAutoOrder) {
|
||||
// 非自动订单,发送通知(使用临时数据)
|
||||
sendSellOrderNotification(
|
||||
@@ -350,7 +403,7 @@ class OrderStatusUpdateService(
|
||||
} else {
|
||||
logger.debug("自动生成的订单,跳过发送通知: orderId=${record.sellOrderId}")
|
||||
}
|
||||
|
||||
|
||||
// 标记为已处理(priceUpdated = true 同时表示价格已更新和通知已发送)
|
||||
val updatedRecord = SellMatchRecord(
|
||||
id = record.id,
|
||||
@@ -369,12 +422,12 @@ class OrderStatusUpdateService(
|
||||
sellMatchRecordRepository.save(updatedRecord)
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
// 检查是否为自动生成的订单(AUTO_ 或 AUTO_FIFO_ 开头),如果是则跳过发送通知
|
||||
val isAutoOrder = record.sellOrderId.startsWith("AUTO_", ignoreCase = true) ||
|
||||
record.sellOrderId.startsWith("AUTO_FIFO_", ignoreCase = true) ||
|
||||
record.sellOrderId.startsWith("AUTO_WS_", ignoreCase = true)
|
||||
|
||||
val isAutoOrder = record.sellOrderId.startsWith("AUTO_", ignoreCase = true) ||
|
||||
record.sellOrderId.startsWith("AUTO_FIFO_", ignoreCase = true) ||
|
||||
record.sellOrderId.startsWith("AUTO_WS_", ignoreCase = true)
|
||||
|
||||
if (isAutoOrder) {
|
||||
logger.debug("自动生成的订单,跳过发送通知并直接标记为已处理: orderId=${record.sellOrderId}")
|
||||
// 直接标记为已处理,不发送通知
|
||||
@@ -395,23 +448,24 @@ class OrderStatusUpdateService(
|
||||
sellMatchRecordRepository.save(updatedRecord)
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
// 查询订单详情,获取实际成交价
|
||||
val actualSellPrice = trackingService.getActualExecutionPrice(
|
||||
orderId = record.sellOrderId,
|
||||
clobApi = clobApi,
|
||||
fallbackPrice = record.sellPrice
|
||||
)
|
||||
|
||||
|
||||
// 如果价格已更新(与当前价格不同),更新数据库
|
||||
if (actualSellPrice != record.sellPrice) {
|
||||
// 重新计算盈亏
|
||||
val details = sellMatchDetailRepository.findByMatchRecordId(record.id!!)
|
||||
var totalRealizedPnl = BigDecimal.ZERO
|
||||
|
||||
|
||||
for (detail in details) {
|
||||
val updatedRealizedPnl = actualSellPrice.subtract(detail.buyPrice).multi(detail.matchedQuantity)
|
||||
|
||||
val updatedRealizedPnl =
|
||||
actualSellPrice.subtract(detail.buyPrice).multi(detail.matchedQuantity)
|
||||
|
||||
// 更新明细的卖出价格和盈亏
|
||||
// 注意:SellMatchDetail 的字段都是 val,需要创建新对象
|
||||
val updatedDetail = SellMatchDetail(
|
||||
@@ -426,10 +480,10 @@ class OrderStatusUpdateService(
|
||||
createdAt = detail.createdAt
|
||||
)
|
||||
sellMatchDetailRepository.save(updatedDetail)
|
||||
|
||||
|
||||
totalRealizedPnl = totalRealizedPnl.add(updatedRealizedPnl)
|
||||
}
|
||||
|
||||
|
||||
// 先更新卖出记录,标记 priceUpdated = true(在发送通知之前更新)
|
||||
// 注意:SellMatchRecord 的字段都是 val,需要创建新对象
|
||||
val updatedRecord = SellMatchRecord(
|
||||
@@ -447,9 +501,9 @@ class OrderStatusUpdateService(
|
||||
createdAt = record.createdAt
|
||||
)
|
||||
sellMatchRecordRepository.save(updatedRecord)
|
||||
|
||||
|
||||
logger.info("更新卖出订单价格成功: orderId=${record.sellOrderId}, 原价格=${record.sellPrice}, 新价格=$actualSellPrice")
|
||||
|
||||
|
||||
// 发送通知(使用实际价格)
|
||||
sendSellOrderNotification(
|
||||
record = updatedRecord,
|
||||
@@ -480,9 +534,9 @@ class OrderStatusUpdateService(
|
||||
createdAt = record.createdAt
|
||||
)
|
||||
sellMatchRecordRepository.save(updatedRecord)
|
||||
|
||||
|
||||
logger.debug("卖出订单价格无需更新: orderId=${record.sellOrderId}, price=$actualSellPrice")
|
||||
|
||||
|
||||
// 发送通知
|
||||
sendSellOrderNotification(
|
||||
record = updatedRecord,
|
||||
@@ -506,7 +560,7 @@ class OrderStatusUpdateService(
|
||||
logger.error("更新待更新卖出订单价格异常: ${e.message}", e)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 更新待发送通知的买入订单
|
||||
* 查询订单详情获取实际价格和数量,然后发送通知并更新数据库
|
||||
@@ -516,13 +570,13 @@ class OrderStatusUpdateService(
|
||||
try {
|
||||
// 查询所有未发送通知的买入订单
|
||||
val pendingOrders = copyOrderTrackingRepository.findByNotificationSentFalse()
|
||||
|
||||
|
||||
if (pendingOrders.isEmpty()) {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
logger.debug("找到 ${pendingOrders.size} 条待发送通知的买入订单")
|
||||
|
||||
|
||||
for (order in pendingOrders) {
|
||||
try {
|
||||
// 验证 orderId 格式(必须以 0x 开头的 16 进制)
|
||||
@@ -545,35 +599,40 @@ class OrderStatusUpdateService(
|
||||
remainingQuantity = order.remainingQuantity,
|
||||
status = order.status,
|
||||
notificationSent = true, // 标记为已发送通知
|
||||
source = order.source, // 保留原始订单来源
|
||||
createdAt = order.createdAt,
|
||||
updatedAt = System.currentTimeMillis()
|
||||
)
|
||||
copyOrderTrackingRepository.save(updatedOrder)
|
||||
|
||||
sendBuyOrderNotification(updatedOrder, useTemporaryData = true, orderCreatedAt = order.createdAt)
|
||||
|
||||
sendBuyOrderNotification(
|
||||
updatedOrder,
|
||||
useTemporaryData = true,
|
||||
orderCreatedAt = order.createdAt
|
||||
)
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
// 获取跟单关系
|
||||
val copyTrading = copyTradingRepository.findById(order.copyTradingId).orElse(null)
|
||||
if (copyTrading == null) {
|
||||
logger.warn("跟单关系不存在,跳过更新: copyTradingId=${order.copyTradingId}")
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
// 获取账户
|
||||
val account = accountRepository.findById(order.accountId).orElse(null)
|
||||
if (account == null) {
|
||||
logger.warn("账户不存在,跳过更新: accountId=${order.accountId}")
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
// 检查账户是否配置了 API 凭证
|
||||
if (account.apiKey == null || account.apiSecret == null || account.apiPassphrase == null) {
|
||||
logger.debug("账户未配置 API 凭证,跳过更新: accountId=${account.id}")
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
// 解密 API 凭证
|
||||
val apiSecret = try {
|
||||
cryptoUtils.decrypt(account.apiSecret!!)
|
||||
@@ -581,14 +640,14 @@ class OrderStatusUpdateService(
|
||||
logger.warn("解密 API Secret 失败: accountId=${account.id}, error=${e.message}")
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
val apiPassphrase = try {
|
||||
cryptoUtils.decrypt(account.apiPassphrase!!)
|
||||
} catch (e: Exception) {
|
||||
logger.warn("解密 API Passphrase 失败: accountId=${account.id}, error=${e.message}")
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
// 创建带认证的 CLOB API 客户端
|
||||
val clobApi = retrofitFactory.createClobApi(
|
||||
account.apiKey!!,
|
||||
@@ -596,92 +655,134 @@ class OrderStatusUpdateService(
|
||||
apiPassphrase,
|
||||
account.walletAddress
|
||||
)
|
||||
|
||||
|
||||
// 查询订单详情
|
||||
val orderResponse = clobApi.getOrder(order.buyOrderId)
|
||||
|
||||
|
||||
// 先检查 HTTP 状态码,非 200 的都跳过
|
||||
if (orderResponse.code() != 200) {
|
||||
val errorBody = orderResponse.errorBody()?.string()?.take(200) ?: "无错误详情"
|
||||
logger.debug("查询订单详情失败(HTTP非200),等待下次轮询: orderId=${order.buyOrderId}, copyOrderTrackingId=${order.id}, code=${orderResponse.code()}, errorBody=$errorBody")
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
// HTTP 200,检查响应体
|
||||
// 响应体也可能返回字符串 "null",Gson 解析时会返回 null
|
||||
val orderDetail = orderResponse.body()
|
||||
if (orderDetail == null) {
|
||||
// HTTP 200 且响应体为 null(或字符串 "null"),表示订单不存在
|
||||
// HTTP 200 且响应体为 null(或字符串 "null"),可能是网络异常或 API 暂时不可用
|
||||
// 使用兜底逻辑:首次检测不删除,1分钟后仍为 null 才删除
|
||||
val firstDetectionTime =
|
||||
orderNullDetectionTime.getOrPut(order.buyOrderId) { System.currentTimeMillis() }
|
||||
val currentTime = System.currentTimeMillis()
|
||||
|
||||
// 检查订单是否已部分卖出,如果已部分卖出则保留订单用于统计
|
||||
val hasMatchedDetails = sellMatchDetailRepository.findByTrackingId(order.id!!).isNotEmpty()
|
||||
if (hasMatchedDetails || order.matchedQuantity > BigDecimal.ZERO) {
|
||||
logger.debug("订单不存在但已部分卖出,保留订单用于统计: orderId=${order.buyOrderId}, copyOrderTrackingId=${order.id}, matchedQuantity=${order.matchedQuantity}")
|
||||
// 检查是否超过清理时间窗口(1小时)
|
||||
val orderAge = currentTime - order.createdAt
|
||||
if (orderAge >= PARTIAL_SOLD_CLEANUP_WINDOW_MS) {
|
||||
logger.warn("订单详情为 null 且已部分卖出,但超过清理时间窗口,删除本地订单: orderId=${order.buyOrderId}, copyOrderTrackingId=${order.id}, matchedQuantity=${order.matchedQuantity}, 已等待=${orderAge / 1000}s")
|
||||
try {
|
||||
copyOrderTrackingRepository.deleteById(order.id!!)
|
||||
logger.info("已删除本地订单(超时清理): orderId=${order.buyOrderId}, copyOrderTrackingId=${order.id}")
|
||||
// 清除缓存
|
||||
orderNullDetectionTime.remove(order.buyOrderId)
|
||||
} catch (e: Exception) {
|
||||
logger.error(
|
||||
"删除本地订单失败: orderId=${order.buyOrderId}, copyOrderTrackingId=${order.id}, error=${e.message}",
|
||||
e
|
||||
)
|
||||
}
|
||||
continue
|
||||
} else {
|
||||
logger.debug("订单详情为 null 但已部分卖出,保留订单用于统计: orderId=${order.buyOrderId}, copyOrderTrackingId=${order.id}, matchedQuantity=${order.matchedQuantity}, 已等待=${orderAge / 1000}s")
|
||||
// 清除缓存,下次重新检测
|
||||
orderNullDetectionTime.remove(order.buyOrderId)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// 检查是否超过重试时间窗口
|
||||
if (currentTime - firstDetectionTime < ORDER_NULL_RETRY_WINDOW_MS) {
|
||||
// 未超过重试窗口,记录日志并等待下次轮询
|
||||
val elapsedSeconds = ((currentTime - firstDetectionTime) / 1000).toInt()
|
||||
logger.debug("订单详情为 null(可能是网络异常),等待重试: orderId=${order.buyOrderId}, copyOrderTrackingId=${order.id}, 已等待=${elapsedSeconds}s, 重试窗口=${ORDER_NULL_RETRY_WINDOW_MS / 1000}s")
|
||||
continue
|
||||
}
|
||||
|
||||
// 订单不存在且未部分卖出,删除本地订单
|
||||
logger.info("订单不存在(HTTP 200 但响应体为空),删除本地订单: orderId=${order.buyOrderId}, copyOrderTrackingId=${order.id}")
|
||||
|
||||
// 超过重试窗口,删除本地订单
|
||||
logger.warn("订单详情为 null 超过重试窗口,删除本地订单: orderId=${order.buyOrderId}, copyOrderTrackingId=${order.id}, 已等待=$((currentTime - firstDetectionTime) / 1000)s")
|
||||
try {
|
||||
copyOrderTrackingRepository.deleteById(order.id!!)
|
||||
logger.info("已删除本地订单: orderId=${order.buyOrderId}, copyOrderTrackingId=${order.id}")
|
||||
// 清除缓存
|
||||
orderNullDetectionTime.remove(order.buyOrderId)
|
||||
} catch (e: Exception) {
|
||||
logger.error("删除本地订单失败: orderId=${order.buyOrderId}, copyOrderTrackingId=${order.id}, error=${e.message}", e)
|
||||
logger.error(
|
||||
"删除本地订单失败: orderId=${order.buyOrderId}, copyOrderTrackingId=${order.id}, error=${e.message}",
|
||||
e
|
||||
)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
// 订单详情不为 null,清除缓存
|
||||
orderNullDetectionTime.remove(order.buyOrderId)
|
||||
|
||||
// 获取实际价格和数量
|
||||
val actualPrice = orderDetail.price?.toSafeBigDecimal() ?: order.price
|
||||
val actualSize = orderDetail.originalSize?.toSafeBigDecimal() ?: order.quantity
|
||||
val actualOutcome = orderDetail.outcome
|
||||
|
||||
|
||||
// 更新订单数据(如果实际数据与临时数据不同)
|
||||
val needUpdate = actualPrice != order.price || actualSize != order.quantity
|
||||
|
||||
// 先保存更新后的订单,标记 notificationSent = true
|
||||
// 这样可以防止其他并发任务重复发送通知
|
||||
val updatedOrder = CopyOrderTracking(
|
||||
id = order.id,
|
||||
copyTradingId = order.copyTradingId,
|
||||
accountId = order.accountId,
|
||||
leaderId = order.leaderId,
|
||||
marketId = order.marketId,
|
||||
side = order.side,
|
||||
outcomeIndex = order.outcomeIndex,
|
||||
buyOrderId = order.buyOrderId,
|
||||
leaderBuyTradeId = order.leaderBuyTradeId,
|
||||
quantity = actualSize, // 使用实际数量
|
||||
price = actualPrice, // 使用实际价格
|
||||
matchedQuantity = order.matchedQuantity,
|
||||
remainingQuantity = order.remainingQuantity,
|
||||
status = order.status,
|
||||
notificationSent = true, // 标记为已发送通知
|
||||
createdAt = order.createdAt,
|
||||
updatedAt = System.currentTimeMillis()
|
||||
)
|
||||
|
||||
// 保存更新后的订单(在发送通知之前保存)
|
||||
copyOrderTrackingRepository.save(updatedOrder)
|
||||
|
||||
if (needUpdate) {
|
||||
logger.info("更新买入订单数据成功: orderId=${order.buyOrderId}, 原价格=${order.price}, 新价格=$actualPrice, 原数量=${order.quantity}, 新数量=$actualSize")
|
||||
} else {
|
||||
logger.debug("买入订单数据无需更新: orderId=${order.buyOrderId}")
|
||||
}
|
||||
|
||||
// 发送通知(使用实际数据)
|
||||
sendBuyOrderNotification(
|
||||
order = updatedOrder,
|
||||
actualPrice = actualPrice.toString(),
|
||||
actualSize = actualSize.toString(),
|
||||
actualOutcome = actualOutcome,
|
||||
account = account,
|
||||
copyTrading = copyTrading,
|
||||
clobApi = clobApi,
|
||||
apiSecret = apiSecret,
|
||||
apiPassphrase = apiPassphrase,
|
||||
orderCreatedAt = order.createdAt
|
||||
)
|
||||
|
||||
// 先保存更新后的订单,标记 notificationSent = true
|
||||
// 这样可以防止其他并发任务重复发送通知
|
||||
val updatedOrder = CopyOrderTracking(
|
||||
id = order.id,
|
||||
copyTradingId = order.copyTradingId,
|
||||
accountId = order.accountId,
|
||||
leaderId = order.leaderId,
|
||||
marketId = order.marketId,
|
||||
side = order.side,
|
||||
outcomeIndex = order.outcomeIndex,
|
||||
buyOrderId = order.buyOrderId,
|
||||
leaderBuyTradeId = order.leaderBuyTradeId,
|
||||
quantity = actualSize, // 使用实际数量
|
||||
price = actualPrice, // 使用实际价格
|
||||
matchedQuantity = order.matchedQuantity,
|
||||
remainingQuantity = order.remainingQuantity,
|
||||
status = order.status,
|
||||
notificationSent = true, // 标记为已发送通知
|
||||
source = order.source, // 保留原始订单来源
|
||||
createdAt = order.createdAt,
|
||||
updatedAt = System.currentTimeMillis()
|
||||
)
|
||||
|
||||
// 保存更新后的订单(在发送通知之前保存)
|
||||
copyOrderTrackingRepository.save(updatedOrder)
|
||||
|
||||
if (needUpdate) {
|
||||
logger.info("更新买入订单数据成功: orderId=${order.buyOrderId}, 原价格=${order.price}, 新价格=$actualPrice, 原数量=${order.quantity}, 新数量=$actualSize")
|
||||
} else {
|
||||
logger.debug("买入订单数据无需更新: orderId=${order.buyOrderId}")
|
||||
}
|
||||
|
||||
// 发送通知(使用实际数据)
|
||||
sendBuyOrderNotification(
|
||||
order = updatedOrder,
|
||||
actualPrice = actualPrice.toString(),
|
||||
actualSize = actualSize.toString(),
|
||||
actualOutcome = actualOutcome,
|
||||
account = account,
|
||||
copyTrading = copyTrading,
|
||||
clobApi = clobApi,
|
||||
apiSecret = apiSecret,
|
||||
apiPassphrase = apiPassphrase,
|
||||
orderCreatedAt = order.createdAt
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
logger.warn("更新买入订单失败: orderId=${order.buyOrderId}, error=${e.message}", e)
|
||||
// 继续处理下一条记录
|
||||
@@ -691,7 +792,7 @@ class OrderStatusUpdateService(
|
||||
logger.error("更新待发送通知买入订单异常: ${e.message}", e)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 发送买入订单通知
|
||||
*/
|
||||
@@ -711,7 +812,7 @@ class OrderStatusUpdateService(
|
||||
if (telegramNotificationService == null) {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
// 获取跟单关系和账户信息(如果未提供)
|
||||
val finalCopyTrading = copyTrading ?: copyTradingRepository.findById(order.copyTradingId).orElse(null)
|
||||
@@ -719,7 +820,7 @@ class OrderStatusUpdateService(
|
||||
logger.warn("跟单关系不存在,跳过发送通知: copyTradingId=${order.copyTradingId}")
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
val finalAccount = account ?: accountRepository.findById(order.accountId).orElse(null)
|
||||
if (finalAccount == null) {
|
||||
logger.warn("账户不存在,跳过发送通知: accountId=${order.accountId}")
|
||||
@@ -729,31 +830,32 @@ class OrderStatusUpdateService(
|
||||
// 获取市场信息
|
||||
val market = marketService.getMarket(order.marketId)
|
||||
val marketTitle = market?.title ?: order.marketId
|
||||
|
||||
|
||||
// 获取 Leader 和跟单配置信息
|
||||
val leader = leaderRepository.findById(order.leaderId).orElse(null)
|
||||
val leaderName = leader?.leaderName
|
||||
val configName = finalCopyTrading.configName
|
||||
|
||||
|
||||
// 获取当前语言设置
|
||||
val locale = try {
|
||||
LocaleContextHolder.getLocale()
|
||||
} catch (e: Exception) {
|
||||
java.util.Locale("zh", "CN") // 默认简体中文
|
||||
}
|
||||
|
||||
|
||||
// 创建 CLOB API 客户端(如果未提供)
|
||||
val finalClobApi = clobApi ?: if (finalAccount.apiKey != null && apiSecret != null && apiPassphrase != null) {
|
||||
retrofitFactory.createClobApi(
|
||||
finalAccount.apiKey!!,
|
||||
apiSecret,
|
||||
apiPassphrase,
|
||||
finalAccount.walletAddress
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
val finalClobApi =
|
||||
clobApi ?: if (finalAccount.apiKey != null && apiSecret != null && apiPassphrase != null) {
|
||||
retrofitFactory.createClobApi(
|
||||
finalAccount.apiKey!!,
|
||||
apiSecret,
|
||||
apiPassphrase,
|
||||
finalAccount.walletAddress
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
// 发送通知
|
||||
telegramNotificationService.sendOrderSuccessNotification(
|
||||
orderId = order.buyOrderId,
|
||||
@@ -776,14 +878,14 @@ class OrderStatusUpdateService(
|
||||
configName = configName,
|
||||
orderTime = orderCreatedAt // 使用订单创建时间
|
||||
)
|
||||
|
||||
|
||||
logger.info("买入订单通知已发送: orderId=${order.buyOrderId}, copyTradingId=${order.copyTradingId}")
|
||||
} catch (e: Exception) {
|
||||
logger.warn("发送买入订单通知失败: orderId=${order.buyOrderId}, error=${e.message}", e)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 发送卖出订单通知
|
||||
*/
|
||||
@@ -803,7 +905,7 @@ class OrderStatusUpdateService(
|
||||
if (telegramNotificationService == null) {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
// 获取跟单关系和账户信息(如果未提供)
|
||||
val finalCopyTrading = copyTrading ?: copyTradingRepository.findById(record.copyTradingId).orElse(null)
|
||||
@@ -811,7 +913,7 @@ class OrderStatusUpdateService(
|
||||
logger.warn("跟单关系不存在,跳过发送通知: copyTradingId=${record.copyTradingId}")
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
val finalAccount = account ?: accountRepository.findById(finalCopyTrading.accountId).orElse(null)
|
||||
if (finalAccount == null) {
|
||||
logger.warn("账户不存在,跳过发送通知: accountId=${finalCopyTrading.accountId}")
|
||||
@@ -821,31 +923,32 @@ class OrderStatusUpdateService(
|
||||
// 获取市场信息
|
||||
val market = marketService.getMarket(record.marketId)
|
||||
val marketTitle = market?.title ?: record.marketId
|
||||
|
||||
|
||||
// 获取 Leader 和跟单配置信息
|
||||
val leader = leaderRepository.findById(finalCopyTrading.leaderId).orElse(null)
|
||||
val leaderName = leader?.leaderName
|
||||
val configName = finalCopyTrading.configName
|
||||
|
||||
|
||||
// 获取当前语言设置
|
||||
val locale = try {
|
||||
LocaleContextHolder.getLocale()
|
||||
} catch (e: Exception) {
|
||||
java.util.Locale("zh", "CN") // 默认简体中文
|
||||
}
|
||||
|
||||
|
||||
// 创建 CLOB API 客户端(如果未提供)
|
||||
val finalClobApi = clobApi ?: if (finalAccount.apiKey != null && apiSecret != null && apiPassphrase != null) {
|
||||
retrofitFactory.createClobApi(
|
||||
finalAccount.apiKey!!,
|
||||
apiSecret,
|
||||
apiPassphrase,
|
||||
finalAccount.walletAddress
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
val finalClobApi =
|
||||
clobApi ?: if (finalAccount.apiKey != null && apiSecret != null && apiPassphrase != null) {
|
||||
retrofitFactory.createClobApi(
|
||||
finalAccount.apiKey!!,
|
||||
apiSecret,
|
||||
apiPassphrase,
|
||||
finalAccount.walletAddress
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
// 发送通知
|
||||
telegramNotificationService.sendOrderSuccessNotification(
|
||||
orderId = record.sellOrderId,
|
||||
@@ -868,7 +971,7 @@ class OrderStatusUpdateService(
|
||||
configName = configName,
|
||||
orderTime = orderCreatedAt // 使用订单创建时间
|
||||
)
|
||||
|
||||
|
||||
logger.info("卖出订单通知已发送: orderId=${record.sellOrderId}, copyTradingId=${record.copyTradingId}")
|
||||
} catch (e: Exception) {
|
||||
logger.warn("发送卖出订单通知失败: orderId=${record.sellOrderId}, error=${e.message}", e)
|
||||
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
package com.wrbug.polymarketbot.service.system
|
||||
|
||||
import com.wrbug.polymarketbot.repository.ProcessedTradeRepository
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.scheduling.annotation.Scheduled
|
||||
import org.springframework.stereotype.Service
|
||||
import org.springframework.transaction.annotation.Transactional
|
||||
|
||||
/**
|
||||
* 已处理交易清理服务
|
||||
* 定期清理过期的去重记录
|
||||
*/
|
||||
@Service
|
||||
class ProcessedTradeCleanupService(
|
||||
private val processedTradeRepository: ProcessedTradeRepository
|
||||
) {
|
||||
|
||||
companion object {
|
||||
private val logger = LoggerFactory.getLogger(ProcessedTradeCleanupService::class.java)
|
||||
|
||||
// 保留时间:1小时(3600000毫秒)
|
||||
// 说明:重复订单通常10秒后就不会再出现,保留10分钟是为了安全起见
|
||||
private const val RETENTION_MS = 600_000L
|
||||
|
||||
// 定时清理间隔:10分钟(600000毫秒)
|
||||
private const val CLEANUP_INTERVAL_MS = 600_000L
|
||||
}
|
||||
|
||||
/**
|
||||
* 定时清理过期记录
|
||||
* 每10分钟执行一次
|
||||
*/
|
||||
@Scheduled(fixedDelay = CLEANUP_INTERVAL_MS)
|
||||
@Transactional
|
||||
fun cleanupExpiredProcessedTrades() {
|
||||
try {
|
||||
val expireTime = System.currentTimeMillis() - RETENTION_MS
|
||||
val deletedCount = processedTradeRepository.deleteByProcessedAtBefore(expireTime)
|
||||
|
||||
if (deletedCount > 0) {
|
||||
logger.info("清理过期已处理交易记录: deletedCount=$deletedCount, expireTime=$expireTime")
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logger.error("清理过期已处理交易记录失败", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
-- ============================================
|
||||
-- V25: 添加订单来源字段到跟单订单跟踪表
|
||||
-- 用于记录订单是从哪个数据源接收到的(activity-ws 或 onchain-ws)
|
||||
-- ============================================
|
||||
|
||||
-- 添加订单来源字段
|
||||
ALTER TABLE copy_order_tracking
|
||||
ADD COLUMN source VARCHAR(20) NOT NULL DEFAULT 'unknown' COMMENT '订单来源:activity-ws(Polymarket WebSocket)、onchain-ws(OnChain WebSocket)';
|
||||
|
||||
-- 对于已有数据,设置为默认值 unknown(不影响现有功能)
|
||||
-- 新创建的记录会在创建时自动填充此字段
|
||||
|
||||
Reference in New Issue
Block a user