feat: 添加推送已过滤订单功能并修复相关问题

- 新增推送已过滤订单功能(pushFilteredOrders),默认关闭
  - 数据库迁移:添加 pushFilteredOrders 字段到模板表和跟单配置表
  - 后端:在实体、DTO、Service 中添加 pushFilteredOrders 字段支持
  - 前端:在模板新增、编辑、推送页面和跟单配置新增、编辑页面添加开关
  - 多语言:添加中文、繁体中文、英文翻译
  - 通知逻辑:发送过滤订单通知时检查 pushFilteredOrders 字段

- 修复新建跟单配置时 pushFilteredOrders 字段未生效的问题
  - 修复手动输入模式下 pushFilteredOrders 被硬编码为 false 的问题
  - 修复从模板填充表单时未加载 pushFilteredOrders 的问题
  - 添加 CopyTradingTemplate 接口的 pushFilteredOrders 字段定义

- 修复新建和编辑页面的截止时间输入框交互问题
  - 修复删除后失去焦点自动填充1的问题
  - 优化 onChange 和 onBlur 处理逻辑,支持清空操作
This commit is contained in:
WrBug
2026-01-12 14:29:12 +08:00
parent cb8e46919f
commit 9c303e0a82
17 changed files with 183 additions and 40 deletions
@@ -43,6 +43,7 @@ data class CopyTradingCreateRequest(
// 新增配置字段
val configName: String? = null, // 配置名(可选)
val pushFailedOrders: Boolean? = null, // 推送失败订单(可选)
val pushFilteredOrders: Boolean? = null, // 推送已过滤订单(可选)
val maxMarketEndDate: Long? = null // 市场截止时间限制(毫秒时间戳),仅跟单截止时间小于此时间的订单,NULL表示不启用
)
@@ -81,6 +82,7 @@ data class CopyTradingUpdateRequest(
// 新增配置字段
val configName: String? = null, // 配置名(可选,但提供时必须非空)
val pushFailedOrders: Boolean? = null, // 推送失败订单(可选)
val pushFilteredOrders: Boolean? = null, // 推送已过滤订单(可选)
val maxMarketEndDate: Long? = null // 市场截止时间限制(毫秒时间戳),仅跟单截止时间小于此时间的订单,NULL表示不启用
)
@@ -156,6 +158,7 @@ data class CopyTradingDto(
// 新增配置字段
val configName: String? = null, // 配置名(可选)
val pushFailedOrders: Boolean = false, // 推送失败订单(默认关闭)
val pushFilteredOrders: Boolean = false, // 推送已过滤订单(默认关闭)
val maxMarketEndDate: Long? = null, // 市场截止时间限制(毫秒时间戳),仅跟单截止时间小于此时间的订单,NULL表示不启用
val createdAt: Long,
val updatedAt: Long
@@ -23,7 +23,8 @@ data class TemplateCreateRequest(
val minOrderDepth: String? = null, // 最小订单深度(USDC金额),NULL表示不启用
val maxSpread: String? = null, // 最大价差(绝对价格),NULL表示不启用
val minPrice: String? = null, // 最低价格(可选),NULL表示不限制最低价
val maxPrice: String? = null // 最高价格(可选),NULL表示不限制最高价
val maxPrice: String? = null, // 最高价格(可选),NULL表示不限制最高价
val pushFilteredOrders: Boolean? = null // 推送已过滤订单(默认关闭)
)
/**
@@ -50,7 +51,8 @@ data class TemplateUpdateRequest(
val minOrderDepth: String? = null, // 最小订单深度(USDC金额),NULL表示不启用
val maxSpread: String? = null, // 最大价差(绝对价格),NULL表示不启用
val minPrice: String? = null, // 最低价格(可选),NULL表示不限制最低价
val maxPrice: String? = null // 最高价格(可选),NULL表示不限制最高价
val maxPrice: String? = null, // 最高价格(可选),NULL表示不限制最高价
val pushFilteredOrders: Boolean? = null // 推送已过滤订单(默认关闭)
)
/**
@@ -84,7 +86,8 @@ data class TemplateCopyRequest(
val minOrderDepth: String? = null, // 最小订单深度(USDC金额),NULL表示不启用
val maxSpread: String? = null, // 最大价差(绝对价格),NULL表示不启用
val minPrice: String? = null, // 最低价格(可选),NULL表示不限制最低价
val maxPrice: String? = null // 最高价格(可选),NULL表示不限制最高价
val maxPrice: String? = null, // 最高价格(可选),NULL表示不限制最高价
val pushFilteredOrders: Boolean? = null // 推送已过滤订单(默认关闭)
)
/**
@@ -119,6 +122,7 @@ data class TemplateDto(
val maxSpread: String?,
val minPrice: String?, // 最低价格(可选),NULL表示不限制最低价
val maxPrice: String?, // 最高价格(可选),NULL表示不限制最高价
val pushFilteredOrders: Boolean, // 推送已过滤订单(默认关闭)
val createdAt: Long,
val updatedAt: Long
)
@@ -100,6 +100,9 @@ data class CopyTrading(
@Column(name = "push_failed_orders", nullable = false)
val pushFailedOrders: Boolean = false, // 推送失败订单(默认关闭)
@Column(name = "push_filtered_orders", nullable = false)
val pushFilteredOrders: Boolean = false, // 推送已过滤订单(默认关闭)
@Column(name = "max_market_end_date")
val maxMarketEndDate: Long? = null, // 市场截止时间限制(毫秒时间戳),仅跟单截止时间小于此时间的订单,NULL表示不启用
@@ -72,6 +72,9 @@ data class CopyTradingTemplate(
@Column(name = "max_price", precision = 20, scale = 8)
val maxPrice: BigDecimal? = null, // 最高价格(可选),NULL表示不限制最高价
@Column(name = "push_filtered_orders", nullable = false)
val pushFilteredOrders: Boolean = false, // 推送已过滤订单(默认关闭)
@Column(name = "created_at", nullable = false)
val createdAt: Long = System.currentTimeMillis(),
@@ -87,7 +87,8 @@ class CopyTradingService(
maxPositionCount = request.maxPositionCount,
keywordFilterMode = request.keywordFilterMode ?: "DISABLED",
keywords = convertKeywordsToJson(request.keywords),
maxMarketEndDate = request.maxMarketEndDate
maxMarketEndDate = request.maxMarketEndDate,
pushFilteredOrders = request.pushFilteredOrders ?: template.pushFilteredOrders
)
} else {
// 手动输入(所有字段必须提供)
@@ -118,7 +119,8 @@ class CopyTradingService(
maxPositionCount = request.maxPositionCount,
keywordFilterMode = request.keywordFilterMode ?: "DISABLED",
keywords = convertKeywordsToJson(request.keywords),
maxMarketEndDate = request.maxMarketEndDate
maxMarketEndDate = request.maxMarketEndDate,
pushFilteredOrders = request.pushFilteredOrders ?: false // 手动输入时使用请求中的值,默认为 false
)
}
@@ -151,7 +153,8 @@ class CopyTradingService(
keywords = config.keywords,
configName = configName,
pushFailedOrders = request.pushFailedOrders ?: false,
maxMarketEndDate = config.maxMarketEndDate
maxMarketEndDate = config.maxMarketEndDate,
pushFilteredOrders = config.pushFilteredOrders
)
val saved = copyTradingRepository.save(copyTrading)
@@ -283,6 +286,7 @@ class CopyTradingService(
},
configName = configName,
pushFailedOrders = request.pushFailedOrders ?: copyTrading.pushFailedOrders,
pushFilteredOrders = request.pushFilteredOrders ?: copyTrading.pushFilteredOrders,
// 处理 maxMarketEndDate-1 表示要清空(设置为 null),null 表示不更新
maxMarketEndDate = if (request.maxMarketEndDate != null) {
if (request.maxMarketEndDate == -1L) {
@@ -505,6 +509,7 @@ class CopyTradingService(
keywords = convertJsonToKeywords(copyTrading.keywords),
configName = copyTrading.configName,
pushFailedOrders = copyTrading.pushFailedOrders,
pushFilteredOrders = copyTrading.pushFilteredOrders,
maxMarketEndDate = copyTrading.maxMarketEndDate,
createdAt = copyTrading.createdAt,
updatedAt = copyTrading.updatedAt
@@ -567,6 +572,7 @@ class CopyTradingService(
val maxPositionCount: Int?,
val keywordFilterMode: String,
val keywords: String?, // JSON 字符串
val maxMarketEndDate: Long? // 市场截止时间限制(毫秒时间戳)
val maxMarketEndDate: Long?, // 市场截止时间限制(毫秒时间戳)
val pushFilteredOrders: Boolean // 推送已过滤订单(默认关闭)
)
}
@@ -347,27 +347,29 @@ open class CopyOrderTrackingService(
logger.error("保存被过滤订单失败: ${e.message}", e)
}
// 发送 Telegram 通知
val locale = try {
org.springframework.context.i18n.LocaleContextHolder.getLocale()
} catch (e: Exception) {
java.util.Locale("zh", "CN") // 默认简体中文
}
// 发送 Telegram 通知(仅在 pushFilteredOrders 为 true 时发送)
if (copyTrading.pushFilteredOrders) {
val locale = try {
org.springframework.context.i18n.LocaleContextHolder.getLocale()
} catch (e: Exception) {
java.util.Locale("zh", "CN") // 默认简体中文
}
telegramNotificationService?.sendOrderFilteredNotification(
marketTitle = marketTitle,
marketId = trade.market,
marketSlug = marketSlug,
side = "BUY",
outcome = trade.outcome,
price = trade.price,
size = trade.size,
filterReason = filterResult.reason,
filterType = filterType,
accountName = account.accountName,
walletAddress = account.walletAddress,
locale = locale
)
telegramNotificationService?.sendOrderFilteredNotification(
marketTitle = marketTitle,
marketId = trade.market,
marketSlug = marketSlug,
side = "BUY",
outcome = trade.outcome,
price = trade.price,
size = trade.size,
filterReason = filterResult.reason,
filterType = filterType,
accountName = account.accountName,
walletAddress = account.walletAddress,
locale = locale
)
}
} catch (e: Exception) {
logger.error("处理被过滤订单通知失败: ${e.message}", e)
}
@@ -62,7 +62,8 @@ class CopyTradingTemplateService(
minOrderDepth = request.minOrderDepth?.toSafeBigDecimal(),
maxSpread = request.maxSpread?.toSafeBigDecimal(),
minPrice = request.minPrice?.toSafeBigDecimal(),
maxPrice = request.maxPrice?.toSafeBigDecimal()
maxPrice = request.maxPrice?.toSafeBigDecimal(),
pushFilteredOrders = request.pushFilteredOrders ?: false
)
val saved = templateRepository.save(template)
@@ -121,6 +122,7 @@ class CopyTradingTemplateService(
maxSpread = request.maxSpread?.toSafeBigDecimal() ?: template.maxSpread,
minPrice = request.minPrice?.toSafeBigDecimal() ?: template.minPrice,
maxPrice = request.maxPrice?.toSafeBigDecimal() ?: template.maxPrice,
pushFilteredOrders = request.pushFilteredOrders ?: template.pushFilteredOrders,
updatedAt = System.currentTimeMillis()
)
@@ -186,7 +188,8 @@ class CopyTradingTemplateService(
minOrderDepth = request.minOrderDepth?.toSafeBigDecimal() ?: sourceTemplate.minOrderDepth,
maxSpread = request.maxSpread?.toSafeBigDecimal() ?: sourceTemplate.maxSpread,
minPrice = request.minPrice?.toSafeBigDecimal() ?: sourceTemplate.minPrice,
maxPrice = request.maxPrice?.toSafeBigDecimal() ?: sourceTemplate.maxPrice
maxPrice = request.maxPrice?.toSafeBigDecimal() ?: sourceTemplate.maxPrice,
pushFilteredOrders = request.pushFilteredOrders ?: sourceTemplate.pushFilteredOrders
)
val saved = templateRepository.save(newTemplate)
@@ -260,6 +263,7 @@ class CopyTradingTemplateService(
maxSpread = template.maxSpread?.toPlainString(),
minPrice = template.minPrice?.toPlainString(),
maxPrice = template.maxPrice?.toPlainString(),
pushFilteredOrders = template.pushFilteredOrders,
createdAt = template.createdAt,
updatedAt = template.updatedAt
)
@@ -0,0 +1,13 @@
-- ============================================
-- V24: 添加推送已过滤订单字段到模板表和跟单配置表
-- 用于配置是否推送被过滤的订单通知,默认关闭
-- ============================================
-- 添加推送已过滤订单字段到模板表
ALTER TABLE copy_trading_templates
ADD COLUMN push_filtered_orders BOOLEAN NOT NULL DEFAULT FALSE COMMENT '推送已过滤订单(默认关闭)';
-- 添加推送已过滤订单字段到跟单配置表
ALTER TABLE copy_trading
ADD COLUMN push_filtered_orders BOOLEAN NOT NULL DEFAULT FALSE COMMENT '推送已过滤订单(默认关闭)';