fix: 修复订单通知重复发送和时间显示问题

1. 修复并发导致的重复通知问题
   - 在 OrderStatusUpdateService 中实现双重检查机制
   - 先保存订单标记为已发送,再重新查询数据库检查
   - 防止定时任务并发时重复发送同一订单的通知
   - 同时修复买入订单和卖出订单的通知逻辑

2. Telegram 通知时间改为订单创建时间
   - TelegramNotificationService.sendOrderSuccessNotification 添加 orderTime 参数
   - 使用订单的 createdAt 时间戳作为通知显示时间
   - 而不是使用当前通知发送时间
   - 更准确反映订单的实际创建时间

修改文件:
- OrderStatusUpdateService.kt: 实现双重检查机制,防止并发重复通知
- TelegramNotificationService.kt: 添加 orderTime 参数支持订单时间显示
- AccountService.kt: 手动创建订单时传递订单创建时间
This commit is contained in:
WrBug
2026-01-11 14:33:30 +08:00
parent d624eaeb57
commit 6af76c4d80
3 changed files with 112 additions and 53 deletions
@@ -948,6 +948,9 @@ class AccountService(
java.util.Locale("zh", "CN") // 默认简体中文
}
// 使用当前时间作为订单创建时间
val orderTime = System.currentTimeMillis()
telegramNotificationService?.sendOrderSuccessNotification(
orderId = orderId,
marketTitle = marketTitle,
@@ -963,7 +966,8 @@ class AccountService(
apiSecret = try { cryptoUtils.decrypt(account.apiSecret!!) } catch (e: Exception) { null },
apiPassphrase = try { cryptoUtils.decrypt(account.apiPassphrase!!) } catch (e: Exception) { null },
walletAddressForApi = account.walletAddress,
locale = locale
locale = locale,
orderTime = orderTime // 使用订单创建时间
)
} catch (e: Exception) {
logger.warn("发送订单成功通知失败: ${e.message}", e)
@@ -344,7 +344,8 @@ class OrderStatusUpdateService(
copyTrading = copyTrading,
clobApi = clobApi,
apiSecret = apiSecret,
apiPassphrase = apiPassphrase
apiPassphrase = apiPassphrase,
orderCreatedAt = record.createdAt
)
} else {
logger.debug("自动生成的订单,跳过发送通知: orderId=${record.sellOrderId}")
@@ -429,19 +430,7 @@ class OrderStatusUpdateService(
totalRealizedPnl = totalRealizedPnl.add(updatedRealizedPnl)
}
// 发送通知(使用实际价格
sendSellOrderNotification(
record = record,
actualPrice = actualSellPrice.toString(),
actualSize = record.totalMatchedQuantity.toString(),
account = account,
copyTrading = copyTrading,
clobApi = clobApi,
apiSecret = apiSecret,
apiPassphrase = apiPassphrase
)
// 更新卖出记录
// 先更新卖出记录,标记 priceUpdated = true(在发送通知之前更新
// 注意:SellMatchRecord 的字段都是 val,需要创建新对象
val updatedRecord = SellMatchRecord(
id = record.id,
@@ -459,20 +448,31 @@ class OrderStatusUpdateService(
)
sellMatchRecordRepository.save(updatedRecord)
logger.info("更新卖出订单价格成功并已发送通知: orderId=${record.sellOrderId}, 原价格=${record.sellPrice}, 新价格=$actualSellPrice")
} else {
// 价格相同,但已经查询过,发送通知并标记为已处理
sendSellOrderNotification(
record = record,
actualPrice = actualSellPrice.toString(),
actualSize = record.totalMatchedQuantity.toString(),
account = account,
copyTrading = copyTrading,
clobApi = clobApi,
apiSecret = apiSecret,
apiPassphrase = apiPassphrase
)
logger.info("更新卖出订单价格成功: orderId=${record.sellOrderId}, 原价格=${record.sellPrice}, 新价格=$actualSellPrice")
// 重新从数据库查询最新的记录状态,检查是否已被其他任务标记为已处理
val latestRecord = sellMatchRecordRepository.findById(record.id!!).orElse(null)
if (latestRecord == null) {
logger.warn("卖出记录已被删除,跳过发送通知: orderId=${record.sellOrderId}, recordId=${record.id}")
} else if (latestRecord.priceUpdated) {
logger.debug("卖出记录已被标记为已处理,跳过重复发送通知: orderId=${record.sellOrderId}, recordId=${record.id}")
} else {
// 发送通知(使用实际价格)
sendSellOrderNotification(
record = updatedRecord,
actualPrice = actualSellPrice.toString(),
actualSize = record.totalMatchedQuantity.toString(),
account = account,
copyTrading = copyTrading,
clobApi = clobApi,
apiSecret = apiSecret,
apiPassphrase = apiPassphrase,
orderCreatedAt = record.createdAt
)
logger.info("卖出订单通知已发送: orderId=${record.sellOrderId}")
}
} else {
// 价格相同,但已经查询过,先标记为已处理再发送通知
val updatedRecord = SellMatchRecord(
id = record.id,
copyTradingId = record.copyTradingId,
@@ -488,7 +488,30 @@ class OrderStatusUpdateService(
createdAt = record.createdAt
)
sellMatchRecordRepository.save(updatedRecord)
logger.debug("卖出订单价格无需更新但已发送通知: orderId=${record.sellOrderId}, price=$actualSellPrice")
logger.debug("卖出订单价格无需更新: orderId=${record.sellOrderId}, price=$actualSellPrice")
// 重新从数据库查询最新的记录状态,检查是否已被其他任务标记为已处理
val latestRecord = sellMatchRecordRepository.findById(record.id!!).orElse(null)
if (latestRecord == null) {
logger.warn("卖出记录已被删除,跳过发送通知: orderId=${record.sellOrderId}, recordId=${record.id}")
} else if (latestRecord.priceUpdated) {
logger.debug("卖出记录已被标记为已处理,跳过重复发送通知: orderId=${record.sellOrderId}, recordId=${record.id}")
} else {
// 发送通知
sendSellOrderNotification(
record = updatedRecord,
actualPrice = actualSellPrice.toString(),
actualSize = record.totalMatchedQuantity.toString(),
account = account,
copyTrading = copyTrading,
clobApi = clobApi,
apiSecret = apiSecret,
apiPassphrase = apiPassphrase,
orderCreatedAt = record.createdAt
)
logger.info("卖出订单通知已发送: orderId=${record.sellOrderId}")
}
}
} catch (e: Exception) {
logger.warn("更新卖出订单价格失败: orderId=${record.sellOrderId}, error=${e.message}", e)
@@ -521,7 +544,7 @@ class OrderStatusUpdateService(
// 验证 orderId 格式(必须以 0x 开头的 16 进制)
if (!isValidOrderId(order.buyOrderId)) {
logger.warn("买入订单ID格式无效,直接标记为已发送通知: orderId=${order.buyOrderId}")
// 对于非 0x 开头的订单ID直接标记为已发送,使用临时数据发送通知
// 对于非 0x 开头的订单ID标记为已发送,使用临时数据发送通知
val updatedOrder = CopyOrderTracking(
id = order.id,
copyTradingId = order.copyTradingId,
@@ -542,7 +565,16 @@ class OrderStatusUpdateService(
updatedAt = System.currentTimeMillis()
)
copyOrderTrackingRepository.save(updatedOrder)
sendBuyOrderNotification(updatedOrder, useTemporaryData = true)
// 重新从数据库查询最新的订单状态,检查是否已被其他任务标记为已发送通知
val latestOrder = copyOrderTrackingRepository.findById(order.id!!).orElse(null)
if (latestOrder == null) {
logger.warn("订单已被删除,跳过发送通知: orderId=${order.buyOrderId}, copyOrderTrackingId=${order.id}")
} else if (latestOrder.notificationSent) {
logger.debug("订单已被标记为已发送通知,跳过重复发送: orderId=${order.buyOrderId}, copyOrderTrackingId=${order.id}")
} else {
sendBuyOrderNotification(updatedOrder, useTemporaryData = true, orderCreatedAt = order.createdAt)
}
continue
}
@@ -630,7 +662,8 @@ class OrderStatusUpdateService(
// 更新订单数据(如果实际数据与临时数据不同)
val needUpdate = actualPrice != order.price || actualSize != order.quantity
// 创建更新后的订单对象
// 先保存更新后的订单,标记 notificationSent = true
// 这样可以防止其他并发任务重复发送通知
val updatedOrder = CopyOrderTracking(
id = order.id,
copyTradingId = order.copyTradingId,
@@ -651,7 +684,7 @@ class OrderStatusUpdateService(
updatedAt = System.currentTimeMillis()
)
// 保存更新后的订单
// 保存更新后的订单(在发送通知之前保存)
copyOrderTrackingRepository.save(updatedOrder)
if (needUpdate) {
@@ -660,18 +693,27 @@ class OrderStatusUpdateService(
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
)
// 重新从数据库查询最新的订单状态,检查是否已被其他任务标记为已发送通知
val latestOrder = copyOrderTrackingRepository.findById(order.id!!).orElse(null)
if (latestOrder == null) {
logger.warn("订单已被删除,跳过发送通知: orderId=${order.buyOrderId}, copyOrderTrackingId=${order.id}")
} else if (latestOrder.notificationSent) {
logger.debug("订单已被标记为已发送通知,跳过重复发送: orderId=${order.buyOrderId}, copyOrderTrackingId=${order.id}")
} else {
// 发送通知(使用实际数据)
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)
// 继续处理下一条记录
@@ -695,7 +737,8 @@ class OrderStatusUpdateService(
copyTrading: CopyTrading? = null,
clobApi: PolymarketClobApi? = null,
apiSecret: String? = null,
apiPassphrase: String? = null
apiPassphrase: String? = null,
orderCreatedAt: Long? = null // 订单创建时间(毫秒时间戳)
) {
if (telegramNotificationService == null) {
return
@@ -762,7 +805,8 @@ class OrderStatusUpdateService(
walletAddressForApi = finalAccount.walletAddress,
locale = locale,
leaderName = leaderName,
configName = configName
configName = configName,
orderTime = orderCreatedAt // 使用订单创建时间
)
logger.info("买入订单通知已发送: orderId=${order.buyOrderId}, copyTradingId=${order.copyTradingId}")
@@ -785,7 +829,8 @@ class OrderStatusUpdateService(
copyTrading: CopyTrading? = null,
clobApi: PolymarketClobApi? = null,
apiSecret: String? = null,
apiPassphrase: String? = null
apiPassphrase: String? = null,
orderCreatedAt: Long? = null // 订单创建时间(毫秒时间戳)
) {
if (telegramNotificationService == null) {
return
@@ -852,7 +897,8 @@ class OrderStatusUpdateService(
walletAddressForApi = finalAccount.walletAddress,
locale = locale,
leaderName = leaderName,
configName = configName
configName = configName,
orderTime = orderCreatedAt // 使用订单创建时间
)
logger.info("卖出订单通知已发送: orderId=${record.sellOrderId}, copyTradingId=${record.copyTradingId}")
@@ -77,6 +77,7 @@ class TelegramNotificationService(
* @param apiPassphrase API Passphrase(可选,用于查询订单详情)
* @param walletAddressForApi 钱包地址(可选,用于查询订单详情)
* @param locale 语言设置(可选,如果提供则使用,否则使用 LocaleContextHolder 获取)
* @param orderTime 订单时间(可选,如果提供则使用订单创建时间,否则使用当前通知时间)
*/
suspend fun sendOrderSuccessNotification(
orderId: String?,
@@ -96,7 +97,8 @@ class TelegramNotificationService(
walletAddressForApi: String? = null,
locale: java.util.Locale? = null,
leaderName: String? = null, // Leader 名称(备注)
configName: String? = null // 跟单配置名
configName: String? = null, // 跟单配置名
orderTime: Long? = null // 订单创建时间(毫秒时间戳),用于通知中的时间显示
) {
// 1. 如果提供了 orderId,检查是否已发送过通知(去重)
if (orderId != null) {
@@ -189,7 +191,8 @@ class TelegramNotificationService(
walletAddress = walletAddress,
locale = currentLocale,
leaderName = leaderName,
configName = configName
configName = configName,
orderTime = orderTime
)
sendMessage(message)
}
@@ -699,7 +702,8 @@ class TelegramNotificationService(
walletAddress: String?,
locale: java.util.Locale,
leaderName: String? = null, // Leader 名称(备注)
configName: String? = null // 跟单配置名
configName: String? = null, // 跟单配置名
orderTime: Long? = null // 订单创建时间(毫秒时间戳)
): String {
// 获取多语言文本
@@ -749,7 +753,12 @@ class TelegramNotificationService(
""
}
val time = DateUtils.formatDateTime()
// 使用订单时间(如果提供),否则使用当前通知时间
val time = if (orderTime != null) {
DateUtils.formatDateTime(orderTime)
} else {
DateUtils.formatDateTime()
}
// 转义 HTML 特殊字符
val escapedMarketTitle = marketTitle.replace("<", "&lt;").replace(">", "&gt;")