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