feat: 完善 Telegram 推送通知功能
- 推送模板优化: - 优先使用账户名称而不是钱包地址 - 使用市场标题而不是16进制ID - 添加可点击的市场链接(支持 slug 和 conditionId) - 添加市场方向(outcome)显示 - 数量和价格从订单详情API获取实际值 - 失败通知只显示后端返回的msg,不显示完整堆栈 - 多语言支持: - 后端推送消息支持多语言(使用前端最后请求的语言) - 添加所有 ErrorCode 的多语言资源文件(中文简体、繁体、英文) - 通知消息文本全部使用多语言资源 - 功能改进: - 从订单详情获取实际的 side、price、size、outcome - 支持买入/卖出方向的多语言显示 - 错误信息优化,只显示后端返回的错误消息
This commit is contained in:
@@ -0,0 +1,386 @@
|
||||
package com.wrbug.polymarketbot.controller
|
||||
|
||||
import com.wrbug.polymarketbot.dto.*
|
||||
import com.wrbug.polymarketbot.enums.ErrorCode
|
||||
import com.wrbug.polymarketbot.service.NotificationConfigService
|
||||
import com.wrbug.polymarketbot.service.TelegramNotificationService
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.context.MessageSource
|
||||
import org.springframework.http.ResponseEntity
|
||||
import org.springframework.web.bind.annotation.*
|
||||
|
||||
/**
|
||||
* 消息推送配置控制器
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/notifications")
|
||||
class NotificationController(
|
||||
private val notificationConfigService: NotificationConfigService,
|
||||
private val telegramNotificationService: TelegramNotificationService,
|
||||
private val messageSource: MessageSource
|
||||
) {
|
||||
|
||||
private val logger = LoggerFactory.getLogger(NotificationController::class.java)
|
||||
|
||||
/**
|
||||
* 获取所有配置
|
||||
*/
|
||||
@PostMapping("/configs/list")
|
||||
fun getConfigs(@RequestBody request: NotificationConfigListRequest?): ResponseEntity<ApiResponse<List<NotificationConfigDto>>> {
|
||||
return try {
|
||||
val configs = runBlocking {
|
||||
if (request?.type != null) {
|
||||
notificationConfigService.getConfigsByType(request.type)
|
||||
} else {
|
||||
notificationConfigService.getAllConfigs()
|
||||
}
|
||||
}
|
||||
ResponseEntity.ok(ApiResponse.success(configs))
|
||||
} catch (e: Exception) {
|
||||
logger.error("获取通知配置列表失败: ${e.message}", e)
|
||||
ResponseEntity.ok(ApiResponse.error(
|
||||
ErrorCode.NOTIFICATION_CONFIG_FETCH_FAILED,
|
||||
customMsg = "获取配置列表失败:${e.message}",
|
||||
messageSource = messageSource
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取配置详情
|
||||
*/
|
||||
@PostMapping("/configs/detail")
|
||||
fun getConfigDetail(@RequestBody request: NotificationConfigDetailRequest): ResponseEntity<ApiResponse<NotificationConfigDto>> {
|
||||
return try {
|
||||
if (request.id == null) {
|
||||
return ResponseEntity.ok(ApiResponse.error(
|
||||
ErrorCode.NOTIFICATION_CONFIG_ID_EMPTY,
|
||||
messageSource = messageSource
|
||||
))
|
||||
}
|
||||
|
||||
val config = runBlocking {
|
||||
notificationConfigService.getConfigById(request.id)
|
||||
}
|
||||
|
||||
if (config == null) {
|
||||
ResponseEntity.ok(ApiResponse.error(ErrorCode.NOT_FOUND, messageSource = messageSource))
|
||||
} else {
|
||||
ResponseEntity.ok(ApiResponse.success(config))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logger.error("获取通知配置详情失败: ${e.message}", e)
|
||||
ResponseEntity.ok(ApiResponse.error(
|
||||
ErrorCode.NOTIFICATION_CONFIG_FETCH_FAILED,
|
||||
customMsg = "获取配置详情失败:${e.message}",
|
||||
messageSource = messageSource
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建配置
|
||||
*/
|
||||
@PostMapping("/configs/create")
|
||||
fun createConfig(@RequestBody request: NotificationConfigRequest): ResponseEntity<ApiResponse<NotificationConfigDto>> {
|
||||
return try {
|
||||
if (request.type.isBlank()) {
|
||||
return ResponseEntity.ok(ApiResponse.paramError("推送类型不能为空"))
|
||||
}
|
||||
if (request.name.isBlank()) {
|
||||
return ResponseEntity.ok(ApiResponse.paramError("配置名称不能为空"))
|
||||
}
|
||||
if (request.config.isEmpty()) {
|
||||
return ResponseEntity.ok(ApiResponse.paramError("配置信息不能为空"))
|
||||
}
|
||||
|
||||
val result = runBlocking {
|
||||
notificationConfigService.createConfig(request)
|
||||
}
|
||||
|
||||
result.fold(
|
||||
onSuccess = { config ->
|
||||
ResponseEntity.ok(ApiResponse.success(config))
|
||||
},
|
||||
onFailure = { e ->
|
||||
logger.error("创建通知配置失败: ${e.message}", e)
|
||||
ResponseEntity.ok(ApiResponse.error(
|
||||
ErrorCode.NOTIFICATION_CONFIG_CREATE_FAILED,
|
||||
customMsg = "创建配置失败:${e.message}",
|
||||
messageSource = messageSource
|
||||
))
|
||||
}
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
logger.error("创建通知配置异常: ${e.message}", e)
|
||||
ResponseEntity.ok(ApiResponse.error(
|
||||
ErrorCode.NOTIFICATION_CONFIG_CREATE_FAILED,
|
||||
customMsg = "创建配置失败:${e.message}",
|
||||
messageSource = messageSource
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新配置
|
||||
*/
|
||||
@PostMapping("/configs/update")
|
||||
fun updateConfig(@RequestBody request: NotificationConfigUpdateRequest): ResponseEntity<ApiResponse<NotificationConfigDto>> {
|
||||
return try {
|
||||
if (request.id == null) {
|
||||
return ResponseEntity.ok(ApiResponse.error(
|
||||
ErrorCode.NOTIFICATION_CONFIG_ID_EMPTY,
|
||||
messageSource = messageSource
|
||||
))
|
||||
}
|
||||
if (request.type.isBlank()) {
|
||||
return ResponseEntity.ok(ApiResponse.error(
|
||||
ErrorCode.NOTIFICATION_CONFIG_TYPE_EMPTY,
|
||||
messageSource = messageSource
|
||||
))
|
||||
}
|
||||
if (request.name.isBlank()) {
|
||||
return ResponseEntity.ok(ApiResponse.error(
|
||||
ErrorCode.NOTIFICATION_CONFIG_NAME_EMPTY,
|
||||
messageSource = messageSource
|
||||
))
|
||||
}
|
||||
if (request.config.isEmpty()) {
|
||||
return ResponseEntity.ok(ApiResponse.error(
|
||||
ErrorCode.NOTIFICATION_CONFIG_DATA_EMPTY,
|
||||
messageSource = messageSource
|
||||
))
|
||||
}
|
||||
|
||||
val configRequest = NotificationConfigRequest(
|
||||
type = request.type,
|
||||
name = request.name,
|
||||
enabled = request.enabled,
|
||||
config = request.config
|
||||
)
|
||||
|
||||
val result = runBlocking {
|
||||
notificationConfigService.updateConfig(request.id, configRequest)
|
||||
}
|
||||
|
||||
result.fold(
|
||||
onSuccess = { config ->
|
||||
ResponseEntity.ok(ApiResponse.success(config))
|
||||
},
|
||||
onFailure = { e ->
|
||||
logger.error("更新通知配置失败: ${e.message}", e)
|
||||
ResponseEntity.ok(ApiResponse.error(
|
||||
ErrorCode.NOTIFICATION_CONFIG_UPDATE_FAILED,
|
||||
customMsg = "更新配置失败:${e.message}",
|
||||
messageSource = messageSource
|
||||
))
|
||||
}
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
logger.error("更新通知配置异常: ${e.message}", e)
|
||||
ResponseEntity.ok(ApiResponse.error(
|
||||
ErrorCode.NOTIFICATION_CONFIG_UPDATE_FAILED,
|
||||
customMsg = "更新配置失败:${e.message}",
|
||||
messageSource = messageSource
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新启用状态
|
||||
*/
|
||||
@PostMapping("/configs/update-enabled")
|
||||
fun updateEnabled(@RequestBody request: NotificationConfigUpdateEnabledRequest): ResponseEntity<ApiResponse<NotificationConfigDto>> {
|
||||
return try {
|
||||
if (request.id == null) {
|
||||
return ResponseEntity.ok(ApiResponse.error(
|
||||
ErrorCode.NOTIFICATION_CONFIG_ID_EMPTY,
|
||||
messageSource = messageSource
|
||||
))
|
||||
}
|
||||
|
||||
val result = runBlocking {
|
||||
notificationConfigService.updateEnabled(request.id, request.enabled ?: true)
|
||||
}
|
||||
|
||||
result.fold(
|
||||
onSuccess = { config ->
|
||||
ResponseEntity.ok(ApiResponse.success(config))
|
||||
},
|
||||
onFailure = { e ->
|
||||
logger.error("更新通知配置启用状态失败: ${e.message}", e)
|
||||
ResponseEntity.ok(ApiResponse.error(
|
||||
ErrorCode.NOTIFICATION_CONFIG_UPDATE_ENABLED_FAILED,
|
||||
customMsg = "更新启用状态失败:${e.message}",
|
||||
messageSource = messageSource
|
||||
))
|
||||
}
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
logger.error("更新通知配置启用状态异常: ${e.message}", e)
|
||||
ResponseEntity.ok(ApiResponse.error(
|
||||
ErrorCode.NOTIFICATION_CONFIG_UPDATE_ENABLED_FAILED,
|
||||
customMsg = "更新启用状态失败:${e.message}",
|
||||
messageSource = messageSource
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除配置
|
||||
*/
|
||||
@PostMapping("/configs/delete")
|
||||
fun deleteConfig(@RequestBody request: NotificationConfigDeleteRequest): ResponseEntity<ApiResponse<Unit>> {
|
||||
return try {
|
||||
if (request.id == null) {
|
||||
return ResponseEntity.ok(ApiResponse.error(
|
||||
ErrorCode.NOTIFICATION_CONFIG_ID_EMPTY,
|
||||
messageSource = messageSource
|
||||
))
|
||||
}
|
||||
|
||||
val result = runBlocking {
|
||||
notificationConfigService.deleteConfig(request.id)
|
||||
}
|
||||
|
||||
result.fold(
|
||||
onSuccess = {
|
||||
ResponseEntity.ok(ApiResponse.success(Unit))
|
||||
},
|
||||
onFailure = { e ->
|
||||
logger.error("删除通知配置失败: ${e.message}", e)
|
||||
ResponseEntity.ok(ApiResponse.error(
|
||||
ErrorCode.NOTIFICATION_CONFIG_DELETE_FAILED,
|
||||
customMsg = "删除配置失败:${e.message}",
|
||||
messageSource = messageSource
|
||||
))
|
||||
}
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
logger.error("删除通知配置异常: ${e.message}", e)
|
||||
ResponseEntity.ok(ApiResponse.error(
|
||||
ErrorCode.NOTIFICATION_CONFIG_DELETE_FAILED,
|
||||
customMsg = "删除配置失败:${e.message}",
|
||||
messageSource = messageSource
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试通知
|
||||
*/
|
||||
@PostMapping("/test")
|
||||
fun testNotification(@RequestBody request: TestNotificationRequest?): ResponseEntity<ApiResponse<Boolean>> {
|
||||
return try {
|
||||
val message = request?.message ?: "这是一条测试消息"
|
||||
val success = runBlocking {
|
||||
telegramNotificationService.sendTestMessage(message)
|
||||
}
|
||||
|
||||
if (success) {
|
||||
ResponseEntity.ok(ApiResponse.success(true))
|
||||
} else {
|
||||
ResponseEntity.ok(ApiResponse.error(
|
||||
ErrorCode.NOTIFICATION_TEST_FAILED,
|
||||
messageSource = messageSource
|
||||
))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logger.error("测试通知失败: ${e.message}", e)
|
||||
ResponseEntity.ok(ApiResponse.error(
|
||||
ErrorCode.NOTIFICATION_TEST_FAILED,
|
||||
customMsg = "测试通知失败:${e.message}",
|
||||
messageSource = messageSource
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 Telegram Chat IDs
|
||||
*/
|
||||
@PostMapping("/telegram/get-chat-ids")
|
||||
fun getTelegramChatIds(@RequestBody request: GetTelegramChatIdsRequest): ResponseEntity<ApiResponse<List<String>>> {
|
||||
return try {
|
||||
if (request.botToken.isBlank()) {
|
||||
return ResponseEntity.ok(ApiResponse.error(
|
||||
ErrorCode.NOTIFICATION_CONFIG_BOT_TOKEN_EMPTY,
|
||||
messageSource = messageSource
|
||||
))
|
||||
}
|
||||
|
||||
val result = runBlocking {
|
||||
telegramNotificationService.getChatIds(request.botToken)
|
||||
}
|
||||
|
||||
result.fold(
|
||||
onSuccess = { chatIds ->
|
||||
ResponseEntity.ok(ApiResponse.success(chatIds))
|
||||
},
|
||||
onFailure = { e ->
|
||||
logger.error("获取 Chat IDs 失败: ${e.message}", e)
|
||||
ResponseEntity.ok(ApiResponse.error(
|
||||
ErrorCode.NOTIFICATION_GET_CHAT_IDS_FAILED,
|
||||
customMsg = "获取 Chat IDs 失败:${e.message}",
|
||||
messageSource = messageSource
|
||||
))
|
||||
}
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
logger.error("获取 Chat IDs 异常: ${e.message}", e)
|
||||
ResponseEntity.ok(ApiResponse.error(
|
||||
ErrorCode.NOTIFICATION_GET_CHAT_IDS_FAILED,
|
||||
customMsg = "获取 Chat IDs 失败:${e.message}",
|
||||
messageSource = messageSource
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 Telegram Chat IDs 请求
|
||||
*/
|
||||
data class GetTelegramChatIdsRequest(
|
||||
val botToken: String
|
||||
)
|
||||
|
||||
/**
|
||||
* 配置列表请求
|
||||
*/
|
||||
data class NotificationConfigListRequest(
|
||||
val type: String? = null // 可选,按类型筛选
|
||||
)
|
||||
|
||||
/**
|
||||
* 配置详情请求
|
||||
*/
|
||||
data class NotificationConfigDetailRequest(
|
||||
val id: Long
|
||||
)
|
||||
|
||||
/**
|
||||
* 配置更新请求
|
||||
*/
|
||||
data class NotificationConfigUpdateRequest(
|
||||
val id: Long,
|
||||
val type: String,
|
||||
val name: String,
|
||||
val enabled: Boolean? = null,
|
||||
val config: Map<String, Any>
|
||||
)
|
||||
|
||||
/**
|
||||
* 更新启用状态请求
|
||||
*/
|
||||
data class NotificationConfigUpdateEnabledRequest(
|
||||
val id: Long,
|
||||
val enabled: Boolean? = true
|
||||
)
|
||||
|
||||
/**
|
||||
* 删除配置请求
|
||||
*/
|
||||
data class NotificationConfigDeleteRequest(
|
||||
val id: Long
|
||||
)
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
package com.wrbug.polymarketbot.dto
|
||||
|
||||
/**
|
||||
* 消息推送配置 DTO
|
||||
*/
|
||||
data class NotificationConfigDto(
|
||||
val id: Long? = null,
|
||||
val type: String, // telegram、discord、slack 等
|
||||
val name: String, // 配置名称
|
||||
val enabled: Boolean, // 是否启用
|
||||
val config: NotificationConfigData, // 配置信息(根据类型不同而不同)
|
||||
val createdAt: Long? = null,
|
||||
val updatedAt: Long? = null
|
||||
)
|
||||
|
||||
/**
|
||||
* Telegram 配置数据
|
||||
*/
|
||||
data class TelegramConfigData(
|
||||
val botToken: String,
|
||||
val chatIds: List<String> // 多个 Chat ID,逗号分隔或数组
|
||||
)
|
||||
|
||||
/**
|
||||
* 通用配置数据(用于未来扩展)
|
||||
*/
|
||||
sealed class NotificationConfigData {
|
||||
data class Telegram(val data: TelegramConfigData) : NotificationConfigData()
|
||||
// 未来可以添加其他类型
|
||||
// data class Discord(val data: DiscordConfigData) : NotificationConfigData()
|
||||
// data class Slack(val data: SlackConfigData) : NotificationConfigData()
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建/更新配置请求
|
||||
*/
|
||||
data class NotificationConfigRequest(
|
||||
val type: String,
|
||||
val name: String,
|
||||
val enabled: Boolean? = true,
|
||||
val config: Map<String, Any> // 配置信息(JSON 对象)
|
||||
)
|
||||
|
||||
/**
|
||||
* 测试通知请求
|
||||
*/
|
||||
data class TestNotificationRequest(
|
||||
val configId: Long? = null, // 如果提供,使用指定配置;否则使用所有启用的配置
|
||||
val message: String? = null // 测试消息内容
|
||||
)
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.wrbug.polymarketbot.entity
|
||||
|
||||
import jakarta.persistence.*
|
||||
|
||||
/**
|
||||
* 消息推送配置实体
|
||||
* 支持多种推送方式(Telegram、Discord、Slack 等)
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "notification_configs")
|
||||
data class NotificationConfig(
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
val id: Long? = null,
|
||||
|
||||
@Column(name = "type", nullable = false, length = 50)
|
||||
val type: String, // telegram、discord、slack 等
|
||||
|
||||
@Column(name = "name", nullable = false, length = 100)
|
||||
val name: String, // 配置名称(用于显示)
|
||||
|
||||
@Column(name = "enabled", nullable = false)
|
||||
var enabled: Boolean = true, // 是否启用
|
||||
|
||||
@Column(name = "config_json", nullable = false, columnDefinition = "TEXT")
|
||||
val configJson: String, // 配置信息(JSON格式)
|
||||
|
||||
@Column(name = "created_at", nullable = false)
|
||||
val createdAt: Long = System.currentTimeMillis(),
|
||||
|
||||
@Column(name = "updated_at", nullable = false)
|
||||
var updatedAt: Long = System.currentTimeMillis()
|
||||
)
|
||||
|
||||
@@ -132,19 +132,34 @@ enum class ErrorCode(
|
||||
POSITION_INSUFFICIENT(4503, "仓位不足", "error.position_insufficient"),
|
||||
POSITION_ALREADY_REDEEMED(4504, "仓位已赎回", "error.position_already_redeemed"),
|
||||
|
||||
// 通知配置相关 (4601-4699)
|
||||
NOTIFICATION_CONFIG_NOT_FOUND(4601, "通知配置不存在", "error.notification_config_not_found"),
|
||||
NOTIFICATION_CONFIG_ID_EMPTY(4602, "配置ID不能为空", "error.notification_config_id_empty"),
|
||||
NOTIFICATION_CONFIG_TYPE_EMPTY(4603, "推送类型不能为空", "error.notification_config_type_empty"),
|
||||
NOTIFICATION_CONFIG_NAME_EMPTY(4604, "配置名称不能为空", "error.notification_config_name_empty"),
|
||||
NOTIFICATION_CONFIG_DATA_EMPTY(4605, "配置信息不能为空", "error.notification_config_data_empty"),
|
||||
NOTIFICATION_CONFIG_BOT_TOKEN_EMPTY(4606, "Bot Token 不能为空", "error.notification_config_bot_token_empty"),
|
||||
NOTIFICATION_CONFIG_CREATE_FAILED(4607, "创建配置失败", "error.notification_config_create_failed"),
|
||||
NOTIFICATION_CONFIG_UPDATE_FAILED(4608, "更新配置失败", "error.notification_config_update_failed"),
|
||||
NOTIFICATION_CONFIG_DELETE_FAILED(4609, "删除配置失败", "error.notification_config_delete_failed"),
|
||||
NOTIFICATION_CONFIG_UPDATE_ENABLED_FAILED(4610, "更新启用状态失败", "error.notification_config_update_enabled_failed"),
|
||||
NOTIFICATION_CONFIG_FETCH_FAILED(4611, "获取配置失败", "error.notification_config_fetch_failed"),
|
||||
NOTIFICATION_TEST_FAILED(4612, "发送测试消息失败,请检查配置", "error.notification_test_failed"),
|
||||
NOTIFICATION_GET_CHAT_IDS_FAILED(4613, "获取 Chat IDs 失败", "error.notification_get_chat_ids_failed"),
|
||||
|
||||
// 账户相关业务错误 (4601-4699)
|
||||
ACCOUNT_ALREADY_EXISTS(4601, "账户已存在", "error.account_already_exists"),
|
||||
ACCOUNT_IS_DEFAULT(4602, "账户已是默认账户", "error.account_is_default"),
|
||||
ACCOUNT_HAS_ACTIVE_ORDERS(4603, "账户有活跃订单", "error.account_has_active_orders"),
|
||||
ACCOUNT_IS_LAST_ONE(4604, "不能删除最后一个账户", "error.account_is_last_one"),
|
||||
ACCOUNT_API_KEY_CREATE_FAILED(4605, "自动获取 API Key 失败", "error.account_api_key_create_failed"),
|
||||
ACCOUNT_PROXY_ADDRESS_FETCH_FAILED(4606, "获取代理地址失败", "error.account_proxy_address_fetch_failed"),
|
||||
ACCOUNT_BALANCE_FETCH_FAILED(4607, "查询账户余额失败", "error.account_balance_fetch_failed"),
|
||||
ACCOUNT_POSITIONS_FETCH_FAILED(4608, "查询仓位列表失败", "error.account_positions_fetch_failed"),
|
||||
ACCOUNT_IS_DEFAULT(4702, "账户已是默认账户", "error.account_is_default"),
|
||||
ACCOUNT_HAS_ACTIVE_ORDERS(4703, "账户有活跃订单", "error.account_has_active_orders"),
|
||||
ACCOUNT_IS_LAST_ONE(4704, "不能删除最后一个账户", "error.account_is_last_one"),
|
||||
ACCOUNT_API_KEY_CREATE_FAILED(4705, "自动获取 API Key 失败", "error.account_api_key_create_failed"),
|
||||
ACCOUNT_PROXY_ADDRESS_FETCH_FAILED(4706, "获取代理地址失败", "error.account_proxy_address_fetch_failed"),
|
||||
ACCOUNT_BALANCE_FETCH_FAILED(4707, "查询账户余额失败", "error.account_balance_fetch_failed"),
|
||||
ACCOUNT_POSITIONS_FETCH_FAILED(4708, "查询仓位列表失败", "error.account_positions_fetch_failed"),
|
||||
|
||||
// 统计相关 (4701-4799)
|
||||
STATISTICS_FETCH_FAILED(4701, "获取统计信息失败", "error.statistics_fetch_failed"),
|
||||
ORDER_LIST_FETCH_FAILED(4702, "查询订单列表失败", "error.order_list_fetch_failed"),
|
||||
// 统计相关 (4801-4899)
|
||||
STATISTICS_FETCH_FAILED(4801, "获取统计信息失败", "error.statistics_fetch_failed"),
|
||||
ORDER_LIST_FETCH_FAILED(4802, "查询订单列表失败", "error.order_list_fetch_failed"),
|
||||
|
||||
// ==================== 服务器内部错误 (5001-5999) ====================
|
||||
SERVER_ERROR(5001, "服务器内部错误", "error.server.error"),
|
||||
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
package com.wrbug.polymarketbot.repository
|
||||
|
||||
import com.wrbug.polymarketbot.entity.NotificationConfig
|
||||
import org.springframework.data.jpa.repository.JpaRepository
|
||||
import org.springframework.stereotype.Repository
|
||||
|
||||
@Repository
|
||||
interface NotificationConfigRepository : JpaRepository<NotificationConfig, Long> {
|
||||
fun findByType(type: String): List<NotificationConfig>
|
||||
fun findByTypeAndEnabled(type: String, enabled: Boolean): List<NotificationConfig>
|
||||
fun findByIdAndType(id: Long, type: String): NotificationConfig?
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import com.wrbug.polymarketbot.util.RetrofitFactory
|
||||
import com.wrbug.polymarketbot.util.toSafeBigDecimal
|
||||
import com.wrbug.polymarketbot.util.eq
|
||||
import com.wrbug.polymarketbot.util.JsonUtils
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.*
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.stereotype.Service
|
||||
import org.springframework.transaction.annotation.Transactional
|
||||
@@ -27,10 +27,14 @@ class AccountService(
|
||||
private val apiKeyService: PolymarketApiKeyService,
|
||||
private val orderPushService: OrderPushService,
|
||||
private val orderSigningService: OrderSigningService,
|
||||
private val cryptoUtils: com.wrbug.polymarketbot.util.CryptoUtils
|
||||
private val cryptoUtils: com.wrbug.polymarketbot.util.CryptoUtils,
|
||||
private val telegramNotificationService: TelegramNotificationService? = null // 可选,避免循环依赖
|
||||
) {
|
||||
|
||||
private val logger = LoggerFactory.getLogger(AccountService::class.java)
|
||||
|
||||
// 协程作用域(用于异步发送通知)
|
||||
private val notificationScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||
|
||||
// 市价单价格调整系数(在最优价基础上调整,确保更快成交)
|
||||
// 市价买单:bestAsk + BUY_PRICE_ADJUSTMENT(加价,确保能立即成交)
|
||||
@@ -859,9 +863,60 @@ class AccountService(
|
||||
if (orderResponse.isSuccessful && orderResponse.body() != null) {
|
||||
val response = orderResponse.body()!!
|
||||
if (response.success) {
|
||||
val orderId = response.orderId ?: ""
|
||||
|
||||
// 发送订单成功通知(异步,不阻塞)
|
||||
notificationScope.launch {
|
||||
try {
|
||||
// 获取市场信息(标题和slug)
|
||||
val marketInfo = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val gammaApi = retrofitFactory.createGammaApi()
|
||||
val marketResponse = gammaApi.listMarkets(conditionIds = listOf(request.marketId))
|
||||
if (marketResponse.isSuccessful && marketResponse.body() != null) {
|
||||
marketResponse.body()!!.firstOrNull()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logger.warn("获取市场信息失败: ${e.message}", e)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
val marketTitle = marketInfo?.question ?: request.marketId
|
||||
val marketSlug = marketInfo?.slug
|
||||
|
||||
// 获取当前语言设置(从 LocaleContextHolder)
|
||||
val locale = try {
|
||||
org.springframework.context.i18n.LocaleContextHolder.getLocale()
|
||||
} catch (e: Exception) {
|
||||
java.util.Locale("zh", "CN") // 默认简体中文
|
||||
}
|
||||
|
||||
telegramNotificationService?.sendOrderSuccessNotification(
|
||||
orderId = orderId,
|
||||
marketTitle = marketTitle,
|
||||
marketId = request.marketId,
|
||||
marketSlug = marketSlug,
|
||||
side = request.side,
|
||||
accountName = account.accountName,
|
||||
walletAddress = account.walletAddress,
|
||||
clobApi = clobApi,
|
||||
apiKey = account.apiKey,
|
||||
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
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
logger.warn("发送订单成功通知失败: ${e.message}", e)
|
||||
}
|
||||
}
|
||||
|
||||
Result.success(
|
||||
PositionSellResponse(
|
||||
orderId = response.orderId ?: "",
|
||||
orderId = orderId,
|
||||
marketId = request.marketId,
|
||||
side = request.side,
|
||||
orderType = request.orderType,
|
||||
@@ -875,6 +930,54 @@ class AccountService(
|
||||
val errorMsg = response.errorMsg ?: "未知错误"
|
||||
val fullErrorMsg = "创建订单失败: accountId=${account.id}, marketId=${request.marketId}, side=${request.side}, orderType=${request.orderType}, price=${if (request.orderType == "LIMIT") sellPrice else "MARKET"}, quantity=${sellQuantity.toPlainString()}, errorMsg=$errorMsg"
|
||||
logger.error(fullErrorMsg)
|
||||
|
||||
// 发送订单失败通知(异步,不阻塞)
|
||||
notificationScope.launch {
|
||||
try {
|
||||
// 获取市场信息(标题和slug)
|
||||
val marketInfo = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val gammaApi = retrofitFactory.createGammaApi()
|
||||
val marketResponse = gammaApi.listMarkets(conditionIds = listOf(request.marketId))
|
||||
if (marketResponse.isSuccessful && marketResponse.body() != null) {
|
||||
marketResponse.body()!!.firstOrNull()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logger.warn("获取市场信息失败: ${e.message}", e)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
val marketTitle = marketInfo?.question ?: request.marketId
|
||||
val marketSlug = marketInfo?.slug
|
||||
|
||||
// 获取当前语言设置(从 LocaleContextHolder)
|
||||
val locale = try {
|
||||
org.springframework.context.i18n.LocaleContextHolder.getLocale()
|
||||
} catch (e: Exception) {
|
||||
java.util.Locale("zh", "CN") // 默认简体中文
|
||||
}
|
||||
|
||||
telegramNotificationService?.sendOrderFailureNotification(
|
||||
marketTitle = marketTitle,
|
||||
marketId = request.marketId,
|
||||
marketSlug = marketSlug,
|
||||
side = request.side,
|
||||
outcome = null, // 失败时可能没有 outcome
|
||||
price = if (request.orderType == "LIMIT") sellPrice.toString() else "MARKET",
|
||||
size = sellQuantity.toString(),
|
||||
errorMessage = errorMsg, // 只传递后端返回的 msg
|
||||
accountName = account.accountName,
|
||||
walletAddress = account.walletAddress,
|
||||
locale = locale
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
logger.warn("发送订单失败通知失败: ${e.message}", e)
|
||||
}
|
||||
}
|
||||
|
||||
Result.failure(Exception(fullErrorMsg))
|
||||
}
|
||||
} else {
|
||||
@@ -885,6 +988,57 @@ class AccountService(
|
||||
}
|
||||
val fullErrorMsg = "创建订单失败: accountId=${account.id}, marketId=${request.marketId}, side=${request.side}, orderType=${request.orderType}, price=${if (request.orderType == "LIMIT") sellPrice else "MARKET"}, quantity=${sellQuantity.toPlainString()}, code=${orderResponse.code()}, message=${orderResponse.message()}${if (errorBody != null) ", errorBody=$errorBody" else ""}"
|
||||
logger.error(fullErrorMsg)
|
||||
|
||||
// 发送订单失败通知(异步,不阻塞)
|
||||
notificationScope.launch {
|
||||
try {
|
||||
// 获取市场信息(标题和slug)
|
||||
val marketInfo = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val gammaApi = retrofitFactory.createGammaApi()
|
||||
val marketResponse = gammaApi.listMarkets(conditionIds = listOf(request.marketId))
|
||||
if (marketResponse.isSuccessful && marketResponse.body() != null) {
|
||||
marketResponse.body()!!.firstOrNull()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logger.warn("获取市场信息失败: ${e.message}", e)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
val marketTitle = marketInfo?.question ?: request.marketId
|
||||
val marketSlug = marketInfo?.slug
|
||||
|
||||
// 获取当前语言设置(从 LocaleContextHolder)
|
||||
val locale = try {
|
||||
org.springframework.context.i18n.LocaleContextHolder.getLocale()
|
||||
} catch (e: Exception) {
|
||||
java.util.Locale("zh", "CN") // 默认简体中文
|
||||
}
|
||||
|
||||
// 只传递后端返回的 msg,不传递完整堆栈
|
||||
val errorMsg = orderResponse.body()?.errorMsg ?: "创建订单失败"
|
||||
|
||||
telegramNotificationService?.sendOrderFailureNotification(
|
||||
marketTitle = marketTitle,
|
||||
marketId = request.marketId,
|
||||
marketSlug = marketSlug,
|
||||
side = request.side,
|
||||
outcome = null, // 失败时可能没有 outcome
|
||||
price = if (request.orderType == "LIMIT") sellPrice.toString() else "MARKET",
|
||||
size = sellQuantity.toString(),
|
||||
errorMessage = errorMsg, // 只传递后端返回的 msg
|
||||
accountName = account.accountName,
|
||||
walletAddress = account.walletAddress,
|
||||
locale = locale
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
logger.warn("发送订单失败通知失败: ${e.message}", e)
|
||||
}
|
||||
}
|
||||
|
||||
Result.failure(Exception(fullErrorMsg))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
|
||||
+128
-2
@@ -7,7 +7,7 @@ import com.wrbug.polymarketbot.entity.*
|
||||
import com.wrbug.polymarketbot.repository.*
|
||||
import com.wrbug.polymarketbot.util.RetrofitFactory
|
||||
import com.wrbug.polymarketbot.util.*
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.*
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.dao.DataIntegrityViolationException
|
||||
import org.springframework.stereotype.Service
|
||||
@@ -34,11 +34,15 @@ class CopyOrderTrackingService(
|
||||
private val orderSigningService: OrderSigningService,
|
||||
private val blockchainService: BlockchainService,
|
||||
private val retrofitFactory: RetrofitFactory,
|
||||
private val cryptoUtils: com.wrbug.polymarketbot.util.CryptoUtils
|
||||
private val cryptoUtils: com.wrbug.polymarketbot.util.CryptoUtils,
|
||||
private val telegramNotificationService: TelegramNotificationService? = null // 可选,避免循环依赖
|
||||
) {
|
||||
|
||||
private val logger = LoggerFactory.getLogger(CopyOrderTrackingService::class.java)
|
||||
|
||||
// 协程作用域(用于异步发送通知)
|
||||
private val notificationScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||
|
||||
/**
|
||||
* 解密账户私钥
|
||||
*/
|
||||
@@ -301,6 +305,54 @@ class CopyOrderTrackingService(
|
||||
errorMessage = errorMsg,
|
||||
retryCount = 1 // 已重试一次
|
||||
)
|
||||
|
||||
// 发送订单失败通知(异步,不阻塞)
|
||||
notificationScope.launch {
|
||||
try {
|
||||
// 获取市场信息(标题和slug)
|
||||
val marketInfo = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val gammaApi = retrofitFactory.createGammaApi()
|
||||
val marketResponse = gammaApi.listMarkets(conditionIds = listOf(trade.market))
|
||||
if (marketResponse.isSuccessful && marketResponse.body() != null) {
|
||||
marketResponse.body()!!.firstOrNull()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logger.warn("获取市场信息失败: ${e.message}", e)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
val marketTitle = marketInfo?.question ?: trade.market
|
||||
val marketSlug = marketInfo?.slug
|
||||
|
||||
// 获取当前语言设置(从 LocaleContextHolder)
|
||||
val locale = try {
|
||||
org.springframework.context.i18n.LocaleContextHolder.getLocale()
|
||||
} catch (e: Exception) {
|
||||
java.util.Locale("zh", "CN") // 默认简体中文
|
||||
}
|
||||
|
||||
telegramNotificationService?.sendOrderFailureNotification(
|
||||
marketTitle = marketTitle,
|
||||
marketId = trade.market,
|
||||
marketSlug = marketSlug,
|
||||
side = "BUY",
|
||||
outcome = null, // 失败时可能没有 outcome
|
||||
price = buyPrice.toString(),
|
||||
size = finalBuyQuantity.toString(),
|
||||
errorMessage = errorMsg, // 只传递后端返回的 msg
|
||||
accountName = account.accountName,
|
||||
walletAddress = account.walletAddress,
|
||||
locale = locale
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
logger.warn("发送订单失败通知失败: ${e.message}", e)
|
||||
}
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -324,6 +376,80 @@ class CopyOrderTrackingService(
|
||||
)
|
||||
|
||||
copyOrderTrackingRepository.save(tracking)
|
||||
|
||||
// 发送订单成功通知(异步,不阻塞)
|
||||
notificationScope.launch {
|
||||
try {
|
||||
// 获取市场信息(标题和slug)
|
||||
val marketInfo = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val gammaApi = retrofitFactory.createGammaApi()
|
||||
val marketResponse = gammaApi.listMarkets(conditionIds = listOf(trade.market))
|
||||
if (marketResponse.isSuccessful && marketResponse.body() != null) {
|
||||
marketResponse.body()!!.firstOrNull()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logger.warn("获取市场信息失败: ${e.message}", e)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
val marketTitle = marketInfo?.question ?: trade.market
|
||||
val marketSlug = marketInfo?.slug
|
||||
|
||||
// 重新创建 CLOB API 客户端用于查询订单详情
|
||||
val apiSecret = try {
|
||||
decryptApiSecret(account)
|
||||
} catch (e: Exception) {
|
||||
logger.warn("解密 API Secret 失败: ${e.message}", e)
|
||||
null
|
||||
}
|
||||
val apiPassphrase = try {
|
||||
decryptApiPassphrase(account)
|
||||
} catch (e: Exception) {
|
||||
logger.warn("解密 API Passphrase 失败: ${e.message}", e)
|
||||
null
|
||||
}
|
||||
|
||||
val clobApiForQuery = if (account.apiKey != null && apiSecret != null && apiPassphrase != null) {
|
||||
retrofitFactory.createClobApi(
|
||||
account.apiKey!!,
|
||||
apiSecret,
|
||||
apiPassphrase,
|
||||
account.walletAddress
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
// 获取当前语言设置(从 LocaleContextHolder)
|
||||
val locale = try {
|
||||
org.springframework.context.i18n.LocaleContextHolder.getLocale()
|
||||
} catch (e: Exception) {
|
||||
java.util.Locale("zh", "CN") // 默认简体中文
|
||||
}
|
||||
|
||||
telegramNotificationService?.sendOrderSuccessNotification(
|
||||
orderId = realOrderId,
|
||||
marketTitle = marketTitle,
|
||||
marketId = trade.market,
|
||||
marketSlug = marketSlug,
|
||||
side = "BUY",
|
||||
accountName = account.accountName,
|
||||
walletAddress = account.walletAddress,
|
||||
clobApi = clobApiForQuery,
|
||||
apiKey = account.apiKey,
|
||||
apiSecret = apiSecret,
|
||||
apiPassphrase = apiPassphrase,
|
||||
walletAddressForApi = account.walletAddress,
|
||||
locale = locale
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
logger.warn("发送订单成功通知失败: ${e.message}", e)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logger.error("处理买入交易失败: copyTradingId=${copyTrading.id}, tradeId=${trade.id}", e)
|
||||
// 继续处理下一个跟单关系
|
||||
|
||||
@@ -0,0 +1,236 @@
|
||||
package com.wrbug.polymarketbot.service
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import com.wrbug.polymarketbot.dto.*
|
||||
import com.wrbug.polymarketbot.entity.NotificationConfig
|
||||
import com.wrbug.polymarketbot.repository.NotificationConfigRepository
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.stereotype.Service
|
||||
import org.springframework.transaction.annotation.Transactional
|
||||
|
||||
/**
|
||||
* 消息推送配置服务
|
||||
*/
|
||||
@Service
|
||||
class NotificationConfigService(
|
||||
private val notificationConfigRepository: NotificationConfigRepository,
|
||||
private val objectMapper: ObjectMapper
|
||||
) {
|
||||
|
||||
private val logger = LoggerFactory.getLogger(NotificationConfigService::class.java)
|
||||
|
||||
/**
|
||||
* 获取所有配置
|
||||
*/
|
||||
suspend fun getAllConfigs(): List<NotificationConfigDto> {
|
||||
return withContext(Dispatchers.IO) {
|
||||
notificationConfigRepository.findAll().map { entityToDto(it) }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据类型获取配置
|
||||
*/
|
||||
suspend fun getConfigsByType(type: String): List<NotificationConfigDto> {
|
||||
return withContext(Dispatchers.IO) {
|
||||
notificationConfigRepository.findByType(type).map { entityToDto(it) }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取启用的配置(按类型)
|
||||
*/
|
||||
suspend fun getEnabledConfigsByType(type: String): List<NotificationConfigDto> {
|
||||
return withContext(Dispatchers.IO) {
|
||||
notificationConfigRepository.findByTypeAndEnabled(type, true).map { entityToDto(it) }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 ID 获取配置
|
||||
*/
|
||||
suspend fun getConfigById(id: Long): NotificationConfigDto? {
|
||||
return withContext(Dispatchers.IO) {
|
||||
notificationConfigRepository.findById(id).orElse(null)?.let { entityToDto(it) }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建配置
|
||||
*/
|
||||
@Transactional
|
||||
suspend fun createConfig(request: NotificationConfigRequest): Result<NotificationConfigDto> {
|
||||
return try {
|
||||
// 验证配置数据
|
||||
validateConfig(request.type, request.config)
|
||||
|
||||
val configJson = objectMapper.writeValueAsString(request.config)
|
||||
val config = NotificationConfig(
|
||||
type = request.type,
|
||||
name = request.name,
|
||||
enabled = request.enabled ?: true,
|
||||
configJson = configJson
|
||||
)
|
||||
|
||||
val saved = withContext(Dispatchers.IO) {
|
||||
notificationConfigRepository.save(config)
|
||||
}
|
||||
|
||||
Result.success(entityToDto(saved))
|
||||
} catch (e: Exception) {
|
||||
logger.error("创建通知配置失败: ${e.message}", e)
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新配置
|
||||
*/
|
||||
@Transactional
|
||||
suspend fun updateConfig(id: Long, request: NotificationConfigRequest): Result<NotificationConfigDto> {
|
||||
return try {
|
||||
val existing = withContext(Dispatchers.IO) {
|
||||
notificationConfigRepository.findById(id).orElse(null)
|
||||
} ?: return Result.failure(IllegalArgumentException("配置不存在"))
|
||||
|
||||
// 验证配置数据
|
||||
validateConfig(request.type, request.config)
|
||||
|
||||
val configJson = objectMapper.writeValueAsString(request.config)
|
||||
val updated = existing.copy(
|
||||
type = request.type,
|
||||
name = request.name,
|
||||
enabled = request.enabled ?: existing.enabled,
|
||||
configJson = configJson,
|
||||
updatedAt = System.currentTimeMillis()
|
||||
)
|
||||
|
||||
val saved = withContext(Dispatchers.IO) {
|
||||
notificationConfigRepository.save(updated)
|
||||
}
|
||||
|
||||
Result.success(entityToDto(saved))
|
||||
} catch (e: Exception) {
|
||||
logger.error("更新通知配置失败: ${e.message}", e)
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新启用状态
|
||||
*/
|
||||
@Transactional
|
||||
suspend fun updateEnabled(id: Long, enabled: Boolean): Result<NotificationConfigDto> {
|
||||
return try {
|
||||
val existing = withContext(Dispatchers.IO) {
|
||||
notificationConfigRepository.findById(id).orElse(null)
|
||||
} ?: return Result.failure(IllegalArgumentException("配置不存在"))
|
||||
|
||||
val updated = existing.copy(
|
||||
enabled = enabled,
|
||||
updatedAt = System.currentTimeMillis()
|
||||
)
|
||||
|
||||
val saved = withContext(Dispatchers.IO) {
|
||||
notificationConfigRepository.save(updated)
|
||||
}
|
||||
|
||||
Result.success(entityToDto(saved))
|
||||
} catch (e: Exception) {
|
||||
logger.error("更新通知配置启用状态失败: ${e.message}", e)
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除配置
|
||||
*/
|
||||
@Transactional
|
||||
suspend fun deleteConfig(id: Long): Result<Unit> {
|
||||
return try {
|
||||
withContext(Dispatchers.IO) {
|
||||
notificationConfigRepository.deleteById(id)
|
||||
}
|
||||
Result.success(Unit)
|
||||
} catch (e: Exception) {
|
||||
logger.error("删除通知配置失败: ${e.message}", e)
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证配置数据
|
||||
*/
|
||||
private fun validateConfig(type: String, config: Map<String, Any>) {
|
||||
when (type.lowercase()) {
|
||||
"telegram" -> {
|
||||
val botToken = config["botToken"] as? String
|
||||
val chatIds = config["chatIds"]
|
||||
|
||||
if (botToken.isNullOrBlank()) {
|
||||
throw IllegalArgumentException("Telegram Bot Token 不能为空")
|
||||
}
|
||||
|
||||
if (chatIds == null) {
|
||||
throw IllegalArgumentException("Telegram Chat IDs 不能为空")
|
||||
}
|
||||
|
||||
// 支持数组或逗号分隔的字符串
|
||||
val chatIdList = when (chatIds) {
|
||||
is List<*> -> chatIds.mapNotNull { it?.toString() }.filter { it.isNotBlank() }
|
||||
is String -> chatIds.split(",").map { it.trim() }.filter { it.isNotBlank() }
|
||||
else -> throw IllegalArgumentException("Chat IDs 格式错误,应为数组或逗号分隔的字符串")
|
||||
}
|
||||
|
||||
if (chatIdList.isEmpty()) {
|
||||
throw IllegalArgumentException("至少需要一个 Chat ID")
|
||||
}
|
||||
}
|
||||
// 未来可以添加其他类型的验证
|
||||
else -> {
|
||||
// 其他类型暂时不验证,允许扩展
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 实体转 DTO
|
||||
*/
|
||||
private fun entityToDto(entity: NotificationConfig): NotificationConfigDto {
|
||||
val configMap = try {
|
||||
objectMapper.readValue(entity.configJson, Map::class.java) as Map<String, Any>
|
||||
} catch (e: Exception) {
|
||||
logger.error("解析配置 JSON 失败: ${e.message}", e)
|
||||
emptyMap()
|
||||
}
|
||||
|
||||
val configData = when (entity.type.lowercase()) {
|
||||
"telegram" -> {
|
||||
val botToken = configMap["botToken"]?.toString() ?: ""
|
||||
val chatIds = when (val ids = configMap["chatIds"]) {
|
||||
is List<*> -> ids.mapNotNull { it?.toString() }
|
||||
is String -> ids.split(",").map { it.trim() }
|
||||
else -> emptyList()
|
||||
}
|
||||
NotificationConfigData.Telegram(TelegramConfigData(botToken, chatIds))
|
||||
}
|
||||
else -> {
|
||||
// 其他类型暂时不支持,返回空配置
|
||||
NotificationConfigData.Telegram(TelegramConfigData("", emptyList()))
|
||||
}
|
||||
}
|
||||
|
||||
return NotificationConfigDto(
|
||||
id = entity.id,
|
||||
type = entity.type,
|
||||
name = entity.name,
|
||||
enabled = entity.enabled,
|
||||
config = configData,
|
||||
createdAt = entity.createdAt,
|
||||
updatedAt = entity.updatedAt
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+636
@@ -0,0 +1,636 @@
|
||||
package com.wrbug.polymarketbot.service
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import com.wrbug.polymarketbot.dto.NotificationConfigData
|
||||
import com.wrbug.polymarketbot.dto.TelegramConfigData
|
||||
import com.wrbug.polymarketbot.util.createClient
|
||||
import com.wrbug.polymarketbot.util.toSafeBigDecimal
|
||||
import kotlinx.coroutines.*
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.context.MessageSource
|
||||
import org.springframework.context.i18n.LocaleContextHolder
|
||||
import org.springframework.stereotype.Service
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
/**
|
||||
* Telegram 通知服务
|
||||
* 负责发送 Telegram 消息
|
||||
*/
|
||||
@Service
|
||||
class TelegramNotificationService(
|
||||
private val notificationConfigService: NotificationConfigService,
|
||||
private val objectMapper: ObjectMapper,
|
||||
private val messageSource: MessageSource
|
||||
) {
|
||||
|
||||
private val logger = LoggerFactory.getLogger(TelegramNotificationService::class.java)
|
||||
|
||||
private val okHttpClient = createClient()
|
||||
.connectTimeout(5, TimeUnit.SECONDS)
|
||||
.readTimeout(5, TimeUnit.SECONDS)
|
||||
.writeTimeout(5, TimeUnit.SECONDS)
|
||||
.build()
|
||||
|
||||
private val apiBaseUrl = "https://api.telegram.org/bot"
|
||||
|
||||
// 协程作用域
|
||||
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||
|
||||
/**
|
||||
* 发送订单成功通知
|
||||
* @param orderId 订单ID(用于查询订单详情获取实际价格和数量)
|
||||
* @param marketTitle 市场标题
|
||||
* @param marketId 市场ID(conditionId),用于生成链接
|
||||
* @param marketSlug 市场slug,用于生成链接
|
||||
* @param side 订单方向(BUY/SELL),用于多语言显示
|
||||
* @param accountName 账户名称
|
||||
* @param walletAddress 钱包地址
|
||||
* @param clobApi CLOB API 客户端(可选,如果提供则查询订单详情获取实际价格和数量)
|
||||
* @param apiKey API Key(可选,用于查询订单详情)
|
||||
* @param apiSecret API Secret(可选,用于查询订单详情)
|
||||
* @param apiPassphrase API Passphrase(可选,用于查询订单详情)
|
||||
* @param walletAddressForApi 钱包地址(可选,用于查询订单详情)
|
||||
* @param locale 语言设置(可选,如果提供则使用,否则使用 LocaleContextHolder 获取)
|
||||
*/
|
||||
suspend fun sendOrderSuccessNotification(
|
||||
orderId: String?,
|
||||
marketTitle: String,
|
||||
marketId: String? = null,
|
||||
marketSlug: String? = null,
|
||||
side: String,
|
||||
accountName: String? = null,
|
||||
walletAddress: String? = null,
|
||||
clobApi: com.wrbug.polymarketbot.api.PolymarketClobApi? = null,
|
||||
apiKey: String? = null,
|
||||
apiSecret: String? = null,
|
||||
apiPassphrase: String? = null,
|
||||
walletAddressForApi: String? = null,
|
||||
locale: java.util.Locale? = null
|
||||
) {
|
||||
// 获取语言设置(优先使用传入的 locale,否则从 LocaleContextHolder 获取)
|
||||
val currentLocale = locale ?: try {
|
||||
LocaleContextHolder.getLocale()
|
||||
} catch (e: Exception) {
|
||||
logger.warn("获取语言设置失败,使用默认语言: ${e.message}", e)
|
||||
java.util.Locale("zh", "CN") // 默认简体中文
|
||||
}
|
||||
|
||||
// 尝试从订单详情获取实际价格和数量
|
||||
var actualPrice: String? = null
|
||||
var actualSize: String? = null
|
||||
var actualSide: String = side
|
||||
var actualOutcome: String? = null // 市场方向(outcome)
|
||||
|
||||
if (orderId != null && clobApi != null && apiKey != null && apiSecret != null && apiPassphrase != null && walletAddressForApi != null) {
|
||||
try {
|
||||
val orderResponse = clobApi.getOrder(orderId)
|
||||
if (orderResponse.isSuccessful && orderResponse.body() != null) {
|
||||
val order = orderResponse.body()!!
|
||||
actualPrice = order.price
|
||||
actualSize = order.originalSize // 使用 originalSize 作为订单数量
|
||||
actualSide = order.side // 使用订单详情中的 side
|
||||
actualOutcome = order.outcome // 使用订单详情中的 outcome(市场方向)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logger.warn("查询订单详情失败,使用默认值: ${e.message}", e)
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没有获取到实际值,使用默认值(这种情况不应该发生,但为了兼容性保留)
|
||||
val price = actualPrice ?: "0"
|
||||
val size = actualSize ?: "0"
|
||||
|
||||
// 计算订单金额 = price × size(USDC)
|
||||
val amount = try {
|
||||
val priceDecimal = price.toSafeBigDecimal()
|
||||
val sizeDecimal = size.toSafeBigDecimal()
|
||||
priceDecimal.multiply(sizeDecimal).toString()
|
||||
} catch (e: Exception) {
|
||||
logger.warn("计算订单金额失败: ${e.message}", e)
|
||||
null
|
||||
}
|
||||
|
||||
val message = buildOrderSuccessMessage(
|
||||
orderId = orderId,
|
||||
marketTitle = marketTitle,
|
||||
marketId = marketId,
|
||||
marketSlug = marketSlug,
|
||||
side = actualSide,
|
||||
outcome = actualOutcome,
|
||||
price = price,
|
||||
size = size,
|
||||
amount = amount,
|
||||
accountName = accountName,
|
||||
walletAddress = walletAddress,
|
||||
locale = currentLocale
|
||||
)
|
||||
sendMessage(message)
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送订单失败通知
|
||||
* @param locale 语言设置(可选,如果提供则使用,否则使用 LocaleContextHolder 获取)
|
||||
*/
|
||||
suspend fun sendOrderFailureNotification(
|
||||
marketTitle: String,
|
||||
marketId: String? = null, // 市场ID(conditionId),用于生成链接
|
||||
marketSlug: String? = null, // 市场slug,用于生成链接
|
||||
side: String,
|
||||
outcome: String? = null, // 市场方向(outcome,如 "YES", "NO" 等)
|
||||
price: String,
|
||||
size: String,
|
||||
errorMessage: String, // 只传递后端返回的 msg,不传递完整堆栈
|
||||
accountName: String? = null,
|
||||
walletAddress: String? = null,
|
||||
locale: java.util.Locale? = null
|
||||
) {
|
||||
// 获取语言设置(优先使用传入的 locale,否则从 LocaleContextHolder 获取)
|
||||
val currentLocale = locale ?: try {
|
||||
LocaleContextHolder.getLocale()
|
||||
} catch (e: Exception) {
|
||||
logger.warn("获取语言设置失败,使用默认语言: ${e.message}", e)
|
||||
java.util.Locale("zh", "CN") // 默认简体中文
|
||||
}
|
||||
|
||||
// 计算订单金额 = price × size(USDC)
|
||||
val amount = try {
|
||||
val priceDecimal = price.toSafeBigDecimal()
|
||||
val sizeDecimal = size.toSafeBigDecimal()
|
||||
priceDecimal.multiply(sizeDecimal).toString()
|
||||
} catch (e: Exception) {
|
||||
logger.warn("计算订单金额失败: ${e.message}", e)
|
||||
null
|
||||
}
|
||||
|
||||
val message = buildOrderFailureMessage(
|
||||
marketTitle = marketTitle,
|
||||
marketId = marketId,
|
||||
marketSlug = marketSlug,
|
||||
side = side,
|
||||
outcome = outcome,
|
||||
price = price,
|
||||
size = size,
|
||||
amount = amount,
|
||||
errorMessage = errorMessage,
|
||||
accountName = accountName,
|
||||
walletAddress = walletAddress,
|
||||
locale = currentLocale
|
||||
)
|
||||
sendMessage(message)
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送测试消息
|
||||
*/
|
||||
suspend fun sendTestMessage(message: String = "这是一条测试消息"): Boolean {
|
||||
return try {
|
||||
val configs = notificationConfigService.getEnabledConfigsByType("telegram")
|
||||
if (configs.isEmpty()) {
|
||||
logger.warn("没有启用的 Telegram 配置")
|
||||
return false
|
||||
}
|
||||
|
||||
return coroutineScope {
|
||||
val results = configs.map { config ->
|
||||
async(Dispatchers.IO) {
|
||||
when (val configData = config.config) {
|
||||
is NotificationConfigData.Telegram -> {
|
||||
sendTelegramMessage(configData.data, message)
|
||||
}
|
||||
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
}.awaitAll()
|
||||
|
||||
results.any { it }
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logger.error("发送测试消息失败: ${e.message}", e)
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送消息(发送给所有启用的 Telegram 配置)
|
||||
*/
|
||||
private suspend fun sendMessage(message: String) {
|
||||
try {
|
||||
val configs = notificationConfigService.getEnabledConfigsByType("telegram")
|
||||
if (configs.isEmpty()) {
|
||||
logger.debug("没有启用的 Telegram 配置,跳过发送消息")
|
||||
return
|
||||
}
|
||||
|
||||
// 异步发送给所有配置
|
||||
configs.forEach { config ->
|
||||
scope.launch {
|
||||
try {
|
||||
when (val configData = config.config) {
|
||||
is NotificationConfigData.Telegram -> {
|
||||
sendTelegramMessage(configData.data, message)
|
||||
}
|
||||
|
||||
else -> {
|
||||
logger.warn("不支持的配置类型: ${config.type}")
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logger.error("发送 Telegram 消息失败 (configId=${config.id}): ${e.message}", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logger.error("发送通知消息失败: ${e.message}", e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送 Telegram 消息
|
||||
*/
|
||||
private suspend fun sendTelegramMessage(config: TelegramConfigData, message: String): Boolean {
|
||||
return withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val results = config.chatIds.map { chatId ->
|
||||
async {
|
||||
sendToSingleChat(config.botToken, chatId, message)
|
||||
}
|
||||
}.awaitAll()
|
||||
|
||||
results.any { it }
|
||||
} catch (e: Exception) {
|
||||
logger.error("发送 Telegram 消息失败: ${e.message}", e)
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 Chat IDs(通过 getUpdates API)
|
||||
* 需要用户先向机器人发送消息
|
||||
*/
|
||||
suspend fun getChatIds(botToken: String): Result<List<String>> {
|
||||
return withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val url = "$apiBaseUrl$botToken/getUpdates"
|
||||
|
||||
val request = Request.Builder()
|
||||
.url(url)
|
||||
.get()
|
||||
.build()
|
||||
|
||||
val response = okHttpClient.newCall(request).execute()
|
||||
|
||||
if (!response.isSuccessful) {
|
||||
val errorBody = response.body?.string()
|
||||
response.close()
|
||||
return@withContext Result.failure(
|
||||
Exception("获取 Chat IDs 失败: code=${response.code}, body=$errorBody")
|
||||
)
|
||||
}
|
||||
|
||||
val responseBody = response.body?.string() ?: ""
|
||||
response.close()
|
||||
|
||||
// 解析 JSON 响应
|
||||
val jsonNode = objectMapper.readTree(responseBody)
|
||||
|
||||
if (jsonNode.get("ok")?.asBoolean()?.not() ?: false) {
|
||||
val description = jsonNode.get("description")?.asText() ?: "未知错误"
|
||||
return@withContext Result.failure(Exception("Telegram API 错误: $description"))
|
||||
}
|
||||
|
||||
val result = jsonNode.get("result")
|
||||
if (result == null || !result.isArray) {
|
||||
return@withContext Result.failure(Exception("未找到消息记录,请先向机器人发送一条消息(如 /start)"))
|
||||
}
|
||||
|
||||
// 提取所有唯一的 chat.id
|
||||
val chatIds = mutableSetOf<String>()
|
||||
result.forEach { update ->
|
||||
val message = update.get("message")
|
||||
if (message != null) {
|
||||
val chat = message.get("chat")
|
||||
if (chat != null) {
|
||||
val chatId = chat.get("id")?.asText()
|
||||
if (chatId != null) {
|
||||
chatIds.add(chatId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (chatIds.isEmpty()) {
|
||||
return@withContext Result.failure(
|
||||
Exception("未找到 Chat ID,请先向机器人发送一条消息(如 /start),然后重试")
|
||||
)
|
||||
}
|
||||
|
||||
Result.success(chatIds.toList())
|
||||
} catch (e: Exception) {
|
||||
logger.error("获取 Chat IDs 异常: ${e.message}", e)
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送到单个 Chat
|
||||
*/
|
||||
private suspend fun sendToSingleChat(botToken: String, chatId: String, message: String): Boolean {
|
||||
return try {
|
||||
val url = "$apiBaseUrl$botToken/sendMessage"
|
||||
|
||||
val requestBody = objectMapper.writeValueAsString(
|
||||
mapOf(
|
||||
"chat_id" to chatId,
|
||||
"text" to message,
|
||||
"parse_mode" to "HTML", // 支持 HTML 格式
|
||||
"disable_web_page_preview" to false // 允许显示链接预览
|
||||
)
|
||||
)
|
||||
|
||||
val request = Request.Builder()
|
||||
.url(url)
|
||||
.post(requestBody.toRequestBody("application/json".toMediaType()))
|
||||
.build()
|
||||
|
||||
val response = okHttpClient.newCall(request).execute()
|
||||
val isSuccess = response.isSuccessful
|
||||
|
||||
if (!isSuccess) {
|
||||
val errorBody = response.body?.string()
|
||||
logger.error("Telegram API 调用失败: code=${response.code}, body=$errorBody")
|
||||
}
|
||||
|
||||
response.close()
|
||||
isSuccess
|
||||
} catch (e: Exception) {
|
||||
logger.error("发送 Telegram 消息异常: ${e.message}", e)
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建订单成功消息
|
||||
*/
|
||||
private fun buildOrderSuccessMessage(
|
||||
orderId: String?,
|
||||
marketTitle: String,
|
||||
marketId: String?,
|
||||
marketSlug: String?,
|
||||
side: String,
|
||||
outcome: String?,
|
||||
price: String,
|
||||
size: String,
|
||||
amount: String?,
|
||||
accountName: String?,
|
||||
walletAddress: String?,
|
||||
locale: java.util.Locale
|
||||
): String {
|
||||
|
||||
// 获取多语言文本
|
||||
val orderCreatedSuccess = messageSource.getMessage("notification.order.created.success", null, "订单创建成功", locale)
|
||||
val orderInfo = messageSource.getMessage("notification.order.info", null, "订单信息", locale)
|
||||
val orderIdLabel = messageSource.getMessage("notification.order.id", null, "订单ID", locale)
|
||||
val marketLabel = messageSource.getMessage("notification.order.market", null, "市场", locale)
|
||||
val sideLabel = messageSource.getMessage("notification.order.side", null, "方向", locale)
|
||||
val outcomeLabel = messageSource.getMessage("notification.order.outcome", null, "市场方向", locale)
|
||||
val priceLabel = messageSource.getMessage("notification.order.price", null, "价格", locale)
|
||||
val quantityLabel = messageSource.getMessage("notification.order.quantity", null, "数量", locale)
|
||||
val amountLabel = messageSource.getMessage("notification.order.amount", null, "金额", locale)
|
||||
val accountLabel = messageSource.getMessage("notification.order.account", null, "账户", locale)
|
||||
val timeLabel = messageSource.getMessage("notification.order.time", null, "时间", locale)
|
||||
val unknown = messageSource.getMessage("common.unknown", null, "未知", locale)
|
||||
val unknownAccount: String = messageSource.getMessage("notification.order.unknown_account", null, "未知账户", locale) ?: "未知账户"
|
||||
val calculateFailed = messageSource.getMessage("notification.order.calculate_failed", null, "计算失败", locale)
|
||||
|
||||
// 获取方向的多语言文本
|
||||
val sideDisplay = when (side.uppercase()) {
|
||||
"BUY" -> messageSource.getMessage("notification.order.side.buy", null, "买入", locale)
|
||||
"SELL" -> messageSource.getMessage("notification.order.side.sell", null, "卖出", locale)
|
||||
else -> side
|
||||
}
|
||||
|
||||
// 优先使用账户名称,如果没有账户名称才显示钱包地址
|
||||
val accountInfo: String = when {
|
||||
!accountName.isNullOrBlank() -> {
|
||||
accountName!!
|
||||
}
|
||||
!walletAddress.isNullOrBlank() -> {
|
||||
maskAddress(walletAddress!!)
|
||||
}
|
||||
else -> {
|
||||
unknownAccount
|
||||
}
|
||||
}
|
||||
|
||||
val time = java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(java.util.Date())
|
||||
|
||||
// 转义 HTML 特殊字符
|
||||
val escapedMarketTitle = marketTitle.replace("<", "<").replace(">", ">")
|
||||
val escapedAccountInfo = accountInfo.replace("<", "<").replace(">", ">")
|
||||
|
||||
// 格式化金额显示
|
||||
val amountDisplay = if (amount != null) {
|
||||
try {
|
||||
// 保留最多4位小数,去除尾随零
|
||||
val amountDecimal = amount.toSafeBigDecimal()
|
||||
val formatted = if (amountDecimal.scale() > 4) {
|
||||
amountDecimal.setScale(4, java.math.RoundingMode.DOWN).stripTrailingZeros()
|
||||
} else {
|
||||
amountDecimal.stripTrailingZeros()
|
||||
}
|
||||
formatted.toPlainString()
|
||||
} catch (e: Exception) {
|
||||
amount
|
||||
}
|
||||
} else {
|
||||
calculateFailed
|
||||
}
|
||||
|
||||
// 生成市场链接
|
||||
val marketLink = when {
|
||||
!marketSlug.isNullOrBlank() -> {
|
||||
"https://polymarket.com/event/$marketSlug"
|
||||
}
|
||||
!marketId.isNullOrBlank() && marketId.startsWith("0x") -> {
|
||||
"https://polymarket.com/condition/$marketId"
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
|
||||
val marketDisplay = if (marketLink != null) {
|
||||
"<a href=\"$marketLink\">$escapedMarketTitle</a>"
|
||||
} else {
|
||||
escapedMarketTitle
|
||||
}
|
||||
|
||||
// 显示市场方向(outcome)
|
||||
val outcomeDisplay = if (!outcome.isNullOrBlank()) {
|
||||
val escapedOutcome = outcome.replace("<", "<").replace(">", ">")
|
||||
"\n• $outcomeLabel: <b>$escapedOutcome</b>"
|
||||
} else {
|
||||
""
|
||||
}
|
||||
|
||||
return """
|
||||
✅ <b>$orderCreatedSuccess</b>
|
||||
|
||||
📊 <b>$orderInfo:</b>
|
||||
• $orderIdLabel: <code>${orderId ?: unknown}</code>
|
||||
• $marketLabel: $marketDisplay$outcomeDisplay
|
||||
• $sideLabel: <b>$sideDisplay</b>
|
||||
• $priceLabel: <code>$price</code>
|
||||
• $quantityLabel: <code>$size</code> shares
|
||||
• $amountLabel: <code>$amountDisplay</code> USDC
|
||||
• $accountLabel: $escapedAccountInfo
|
||||
|
||||
⏰ $timeLabel: <code>$time</code>
|
||||
""".trimIndent()
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建订单失败消息
|
||||
*/
|
||||
private fun buildOrderFailureMessage(
|
||||
marketTitle: String,
|
||||
marketId: String?,
|
||||
marketSlug: String?,
|
||||
side: String,
|
||||
outcome: String?,
|
||||
price: String,
|
||||
size: String,
|
||||
amount: String?,
|
||||
errorMessage: String,
|
||||
accountName: String?,
|
||||
walletAddress: String?,
|
||||
locale: java.util.Locale
|
||||
): String {
|
||||
|
||||
// 获取多语言文本
|
||||
val orderCreatedFailed = messageSource.getMessage("notification.order.created.failed", null, "订单创建失败", locale)
|
||||
val orderInfo = messageSource.getMessage("notification.order.info", null, "订单信息", locale)
|
||||
val marketLabel = messageSource.getMessage("notification.order.market", null, "市场", locale)
|
||||
val sideLabel = messageSource.getMessage("notification.order.side", null, "方向", locale)
|
||||
val outcomeLabel = messageSource.getMessage("notification.order.outcome", null, "市场方向", locale)
|
||||
val priceLabel = messageSource.getMessage("notification.order.price", null, "价格", locale)
|
||||
val quantityLabel = messageSource.getMessage("notification.order.quantity", null, "数量", locale)
|
||||
val amountLabel = messageSource.getMessage("notification.order.amount", null, "金额", locale)
|
||||
val accountLabel = messageSource.getMessage("notification.order.account", null, "账户", locale)
|
||||
val errorInfo = messageSource.getMessage("notification.order.error_info", null, "错误信息", locale)
|
||||
val timeLabel = messageSource.getMessage("notification.order.time", null, "时间", locale)
|
||||
val unknownAccount: String = messageSource.getMessage("notification.order.unknown_account", null, "未知账户", locale) ?: "未知账户"
|
||||
val calculateFailed = messageSource.getMessage("notification.order.calculate_failed", null, "计算失败", locale)
|
||||
|
||||
// 获取方向的多语言文本
|
||||
val sideDisplay = when (side.uppercase()) {
|
||||
"BUY" -> messageSource.getMessage("notification.order.side.buy", null, "买入", locale)
|
||||
"SELL" -> messageSource.getMessage("notification.order.side.sell", null, "卖出", locale)
|
||||
else -> side
|
||||
}
|
||||
|
||||
// 优先使用账户名称,如果没有账户名称才显示钱包地址
|
||||
val accountInfo: String = when {
|
||||
!accountName.isNullOrBlank() -> {
|
||||
accountName!!
|
||||
}
|
||||
!walletAddress.isNullOrBlank() -> {
|
||||
maskAddress(walletAddress!!)
|
||||
}
|
||||
else -> {
|
||||
unknownAccount
|
||||
}
|
||||
}
|
||||
|
||||
val time = java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(java.util.Date())
|
||||
|
||||
// 错误信息已经是后端返回的 msg,不需要截断(但为了安全,限制长度)
|
||||
val shortErrorMessage = if (errorMessage.length > 500) {
|
||||
errorMessage.substring(0, 500) + "..."
|
||||
} else {
|
||||
errorMessage
|
||||
}
|
||||
|
||||
// 转义 HTML 特殊字符
|
||||
val escapedMarketTitle = marketTitle.replace("<", "<").replace(">", ">")
|
||||
val escapedAccountInfo = accountInfo.replace("<", "<").replace(">", ">")
|
||||
val escapedErrorMessage = shortErrorMessage.replace("<", "<").replace(">", ">")
|
||||
|
||||
// 格式化金额显示
|
||||
val amountDisplay = if (amount != null) {
|
||||
try {
|
||||
// 保留最多4位小数,去除尾随零
|
||||
val amountDecimal = amount.toSafeBigDecimal()
|
||||
val formatted = if (amountDecimal.scale() > 4) {
|
||||
amountDecimal.setScale(4, java.math.RoundingMode.DOWN).stripTrailingZeros()
|
||||
} else {
|
||||
amountDecimal.stripTrailingZeros()
|
||||
}
|
||||
formatted.toPlainString()
|
||||
} catch (e: Exception) {
|
||||
amount
|
||||
}
|
||||
} else {
|
||||
calculateFailed
|
||||
}
|
||||
|
||||
// 生成市场链接
|
||||
val marketLink = when {
|
||||
!marketSlug.isNullOrBlank() -> {
|
||||
"https://polymarket.com/event/$marketSlug"
|
||||
}
|
||||
!marketId.isNullOrBlank() && marketId.startsWith("0x") -> {
|
||||
"https://polymarket.com/condition/$marketId"
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
|
||||
val marketDisplay = if (marketLink != null) {
|
||||
"<a href=\"$marketLink\">$escapedMarketTitle</a>"
|
||||
} else {
|
||||
escapedMarketTitle
|
||||
}
|
||||
|
||||
// 显示市场方向(outcome)
|
||||
val outcomeDisplay = if (!outcome.isNullOrBlank()) {
|
||||
val escapedOutcome = outcome.replace("<", "<").replace(">", ">")
|
||||
"\n• $outcomeLabel: <b>$escapedOutcome</b>"
|
||||
} else {
|
||||
""
|
||||
}
|
||||
|
||||
return """
|
||||
❌ <b>$orderCreatedFailed</b>
|
||||
|
||||
📊 <b>$orderInfo:</b>
|
||||
• $marketLabel: $marketDisplay$outcomeDisplay
|
||||
• $sideLabel: <b>$sideDisplay</b>
|
||||
• $priceLabel: <code>$price</code>
|
||||
• $quantityLabel: <code>$size</code> shares
|
||||
• $amountLabel: <code>$amountDisplay</code> USDC
|
||||
• $accountLabel: $escapedAccountInfo
|
||||
|
||||
⚠️ <b>$errorInfo:</b>
|
||||
<code>$escapedErrorMessage</code>
|
||||
|
||||
⏰ $timeLabel: <code>$time</code>
|
||||
""".trimIndent()
|
||||
}
|
||||
|
||||
/**
|
||||
* 脱敏显示地址(只显示前6位和后4位)
|
||||
*/
|
||||
private fun maskAddress(address: String): String {
|
||||
if (address.length <= 10) {
|
||||
return address
|
||||
}
|
||||
return "${address.substring(0, 6)}...${address.substring(address.length - 4)}"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user