refactor: 移除下单失败存储数据库的功能

- 删除 FailedTrade 实体类和 FailedTradeRepository
- 从 CopyOrderTrackingService 中移除失败交易存储逻辑
- 移除 recordFailedTrade 方法
- 移除检查失败交易的代码
- 创建 Flyway migration V16 删除 failed_trade 表
- 从 V1__init_database.sql 中移除 failed_trade 表创建语句

下单失败时仅记录日志,不再存储到数据库
This commit is contained in:
WrBug
2026-01-01 22:24:47 +08:00
parent ec06003157
commit cc1a732984
6 changed files with 846 additions and 242 deletions
@@ -1,55 +0,0 @@
package com.wrbug.polymarketbot.entity
import jakarta.persistence.*
/**
* 失败交易实体
* 记录处理失败的交易信息
*/
@Entity
@Table(name = "failed_trade")
data class FailedTrade(
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long? = null,
@Column(name = "leader_id", nullable = false)
val leaderId: Long,
@Column(name = "leader_trade_id", nullable = false, length = 100)
val leaderTradeId: String, // Leader 的交易ID
@Column(name = "trade_type", nullable = false, length = 10)
val tradeType: String, // BUY 或 SELL
@Column(name = "copy_trading_id", nullable = false)
val copyTradingId: Long,
@Column(name = "account_id", nullable = false)
val accountId: Long,
@Column(name = "market_id", nullable = false, length = 100)
val marketId: String,
@Column(name = "side", nullable = false, length = 10)
val side: String, // YES/NO
@Column(name = "price", nullable = false, length = 50)
val price: String, // 价格(字符串格式)
@Column(name = "size", nullable = false, length = 50)
val size: String, // 数量(字符串格式)
@Column(name = "error_message", columnDefinition = "TEXT")
val errorMessage: String? = null, // 错误信息
@Column(name = "retry_count", nullable = false)
val retryCount: Int = 0, // 重试次数
@Column(name = "failed_at", nullable = false)
val failedAt: Long = System.currentTimeMillis(),
@Column(name = "created_at", nullable = false)
val createdAt: Long = System.currentTimeMillis()
)
@@ -1,23 +0,0 @@
package com.wrbug.polymarketbot.repository
import com.wrbug.polymarketbot.entity.FailedTrade
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.stereotype.Repository
/**
* 失败交易Repository
*/
@Repository
interface FailedTradeRepository : JpaRepository<FailedTrade, Long> {
/**
* 根据Leader ID和交易ID查询
*/
fun findByLeaderIdAndLeaderTradeId(leaderId: Long, leaderTradeId: String): FailedTrade?
/**
* 检查是否存在失败的交易
*/
fun existsByLeaderIdAndLeaderTradeId(leaderId: Long, leaderTradeId: String): Boolean
}
@@ -37,7 +37,6 @@ open class CopyOrderTrackingService(
private val sellMatchRecordRepository: SellMatchRecordRepository,
private val sellMatchDetailRepository: SellMatchDetailRepository,
private val processedTradeRepository: ProcessedTradeRepository,
private val failedTradeRepository: FailedTradeRepository,
private val filteredOrderRepository: FilteredOrderRepository,
private val copyTradingRepository: CopyTradingRepository,
private val accountRepository: AccountRepository,
@@ -135,12 +134,6 @@ open class CopyOrderTrackingService(
return@withLock Result.success(Unit)
}
// 检查是否已记录为失败交易
val failedTrade = failedTradeRepository.findByLeaderIdAndLeaderTradeId(leaderId, trade.id)
if (failedTrade != null) {
return@withLock Result.success(Unit)
}
// 2. 处理交易逻辑
val result = when (trade.side.uppercase()) {
"BUY" -> processBuyTrade(leaderId, trade)
@@ -491,27 +484,6 @@ open class CopyOrderTrackingService(
if (createOrderResult.isFailure) {
// 提取错误信息(只保留 code 和 errorBody
val exception = createOrderResult.exceptionOrNull()
val errorMsg = buildFullErrorMessage(
exception,
"BUY",
buyPrice.toString(),
finalBuyQuantity.toString(),
trade.id
)
// 记录失败交易到数据库
// retryCount = MAX_RETRY_ATTEMPTS - 1,表示已重试的次数
recordFailedTrade(
leaderId = leaderId,
trade = trade,
copyTradingId = copyTrading.id!!,
accountId = copyTrading.accountId,
side = "BUY",
price = buyPrice.toString(),
size = finalBuyQuantity.toString(),
errorMessage = errorMsg,
retryCount = MAX_RETRY_ATTEMPTS - 1 // 已重试次数
)
// 发送订单失败通知(异步,不阻塞,仅在 pushFailedOrders 为 true 时发送)
if (copyTrading.pushFailedOrders) {
@@ -971,26 +943,9 @@ open class CopyOrderTrackingService(
)
if (createOrderResult.isFailure) {
// 创建订单失败,记录到失败表
// 创建订单失败,记录错误日志
val exception = createOrderResult.exceptionOrNull()
val errorMsg = buildFullErrorMessage(
exception,
"SELL",
sellPrice.toString(),
totalMatched.toString(),
leaderSellTrade.id
)
recordFailedTrade(
leaderId = copyTrading.leaderId,
trade = leaderSellTrade,
copyTradingId = copyTrading.id!!,
accountId = copyTrading.accountId,
side = "SELL", // 订单方向是SELL
price = sellPrice.toString(),
size = totalMatched.toString(),
errorMessage = errorMsg,
retryCount = 1 // 已重试一次
)
logger.error("创建卖出订单失败: copyTradingId=${copyTrading.id}, tradeId=${leaderSellTrade.id}, error=${exception?.message}")
return
}
@@ -1248,98 +1203,6 @@ open class CopyOrderTrackingService(
return "code=$code, errorBody=$errorBody"
}
/**
* 记录失败交易到数据库
* 注意:此方法在 @Transactional 方法中被调用,会自动继承事务
*/
private suspend fun recordFailedTrade(
leaderId: Long,
trade: TradeResponse,
copyTradingId: Long,
accountId: Long,
side: String,
price: String,
size: String,
errorMessage: String,
retryCount: Int
) {
try {
// 确保错误信息不超过数据库字段限制(TEXT类型通常支持65535字符)
val maxErrorMessageLength = 50000 // 保留一些余量
val finalErrorMessage = if (errorMessage.length > maxErrorMessageLength) {
errorMessage.substring(0, maxErrorMessageLength) + "... (截断)"
} else {
errorMessage
}
val failedTrade = FailedTrade(
leaderId = leaderId,
leaderTradeId = trade.id,
tradeType = trade.side.uppercase(),
copyTradingId = copyTradingId,
accountId = accountId,
marketId = trade.market,
side = side,
price = price,
size = size,
errorMessage = finalErrorMessage,
retryCount = retryCount,
failedAt = System.currentTimeMillis()
)
failedTradeRepository.save(failedTrade)
// 记录日志,确认已保存到数据库
logger.info("失败交易已保存到数据库: leaderId=$leaderId, tradeId=${trade.id}, errorMessageLength=${finalErrorMessage.length}")
// 标记为已处理(失败状态),避免重复处理
// 注意:并发情况下可能多个请求同时处理同一笔交易,需要处理唯一约束冲突
try {
val processed = ProcessedTrade(
leaderId = leaderId,
leaderTradeId = trade.id,
tradeType = trade.side.uppercase(),
source = "polling",
status = "FAILED",
processedAt = System.currentTimeMillis()
)
processedTradeRepository.save(processed)
} catch (e: Exception) {
// 检查是否是唯一键冲突异常
if (isUniqueConstraintViolation(e)) {
// 唯一约束冲突,说明已经处理过了(可能是并发请求)
// 检查现有记录的状态
val existing = processedTradeRepository.findByLeaderIdAndLeaderTradeId(leaderId, trade.id)
if (existing != null) {
if (existing.status == "SUCCESS") {
logger.warn("交易已成功处理,但尝试记录为失败(并发冲突): leaderId=$leaderId, tradeId=${trade.id}")
} else {
logger.debug("交易已标记为失败(并发检测): leaderId=$leaderId, tradeId=${trade.id}")
}
} else {
// 如果查询不到,等待一下再查询(可能是事务隔离级别问题)
delay(100)
val existingAfterDelay =
processedTradeRepository.findByLeaderIdAndLeaderTradeId(leaderId, trade.id)
if (existingAfterDelay != null) {
logger.debug("延迟查询到记录(并发检测): leaderId=$leaderId, tradeId=${trade.id}, status=${existingAfterDelay.status}")
} else {
logger.warn(
"保存ProcessedTrade失败记录时发生唯一约束冲突,但查询不到记录: leaderId=$leaderId, tradeId=${trade.id}",
e
)
}
}
} else {
// 其他类型的异常,记录但不抛出(避免影响其他交易的处理)
logger.warn("保存ProcessedTrade失败记录时发生异常: leaderId=$leaderId, tradeId=${trade.id}", e)
}
}
logger.warn("已记录失败交易: leaderId=$leaderId, tradeId=${trade.id}, error=$errorMessage")
} catch (e: Exception) {
logger.error("记录失败交易异常: leaderId=$leaderId, tradeId=${trade.id}", e)
}
}
/**
* 更新订单状态
@@ -0,0 +1,8 @@
-- ============================================
-- V16: 删除失败交易记录表
-- 移除下单失败存储到数据库的功能
-- ============================================
-- 删除失败交易记录表
DROP TABLE IF EXISTS failed_trade;
@@ -172,32 +172,8 @@ CREATE TABLE IF NOT EXISTS processed_trade (
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='已处理交易表(用于去重)';
-- ============================================
-- 9. 创建失败交易记录表
-- ============================================
CREATE TABLE IF NOT EXISTS failed_trade (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
leader_id BIGINT NOT NULL COMMENT 'Leader ID',
leader_trade_id VARCHAR(100) NOT NULL COMMENT 'Leader 的交易ID',
trade_type VARCHAR(10) NOT NULL COMMENT '交易类型:BUY 或 SELL',
copy_trading_id BIGINT NOT NULL COMMENT '跟单关系ID',
account_id BIGINT NOT NULL COMMENT '账户ID',
market_id VARCHAR(100) NOT NULL COMMENT '市场地址',
side VARCHAR(10) NOT NULL COMMENT '方向:YES/NO',
price VARCHAR(50) NOT NULL COMMENT '价格',
size VARCHAR(50) NOT NULL COMMENT '数量',
error_message TEXT COMMENT '错误信息',
retry_count INT NOT NULL DEFAULT 0 COMMENT '重试次数',
failed_at BIGINT NOT NULL COMMENT '失败时间(毫秒时间戳)',
created_at BIGINT NOT NULL COMMENT '创建时间(毫秒时间戳)',
INDEX idx_leader_trade (leader_id, leader_trade_id),
INDEX idx_copy_trading (copy_trading_id),
INDEX idx_failed_at (failed_at),
FOREIGN KEY (copy_trading_id) REFERENCES copy_trading(id) ON DELETE CASCADE,
FOREIGN KEY (leader_id) REFERENCES copy_trading_leaders(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='失败交易记录表';
-- ============================================
-- 10. 创建用户表(用于JWT登录鉴权)
-- 9. 创建用户表(用于JWT登录鉴权)
-- ============================================
CREATE TABLE IF NOT EXISTS users (
id BIGINT AUTO_INCREMENT PRIMARY KEY,