feat: 消息推送自定义模板与独立设置页

后端:
- 新增通知模板表与实体、DTO、Repository、NotificationTemplateService
- 支持 {{variable}} 模板语法,提供模板类型与变量接口
- TelegramNotificationService 改为通过模板渲染发送(ORDER_SUCCESS/ORDER_FAILED/ORDER_FILTERED/CRYPTO_TAIL_SUCCESS/REDEEM_SUCCESS/REDEEM_NO_RETURN)
- 模板 CRUD、重置默认、测试发送接口
- 补全订单过滤相关 i18n key(zh/en/zh-TW)

前端:
- 消息推送设置抽离为独立页 /system-settings/notification
- 系统设置概览改为入口卡片,侧栏增加「消息推送设置」菜单
- NotificationSettingsPage: 机器人配置 + 模板配置双卡片布局(与概览一致)
- 模板配置支持选择类型、编辑内容、变量面板(点击复制、悬停说明)、保存/重置/测试
- 多语言 key 补全(notificationSettings.templates.*、templateTypes.*)

Made-with: Cursor
This commit is contained in:
WrBug
2026-03-02 21:31:45 +08:00
parent 83bc209489
commit e7af4d4821
19 changed files with 2087 additions and 338 deletions
@@ -3,6 +3,7 @@ package com.wrbug.polymarketbot.controller.system
import com.wrbug.polymarketbot.dto.*
import com.wrbug.polymarketbot.enums.ErrorCode
import com.wrbug.polymarketbot.service.system.NotificationConfigService
import com.wrbug.polymarketbot.service.system.NotificationTemplateService
import com.wrbug.polymarketbot.service.system.TelegramNotificationService
import kotlinx.coroutines.runBlocking
import org.slf4j.LoggerFactory
@@ -18,6 +19,7 @@ import org.springframework.web.bind.annotation.*
class NotificationController(
private val notificationConfigService: NotificationConfigService,
private val telegramNotificationService: TelegramNotificationService,
private val notificationTemplateService: NotificationTemplateService,
private val messageSource: MessageSource
) {
@@ -335,6 +337,155 @@ class NotificationController(
))
}
}
// ==================== 模板相关 API ====================
/**
* 获取所有模板类型
*/
@PostMapping("/templates/types")
fun getTemplateTypes(): ResponseEntity<ApiResponse<List<TemplateTypeInfoDto>>> {
return try {
val types = notificationTemplateService.getTemplateTypes()
ResponseEntity.ok(ApiResponse.success(types))
} catch (e: Exception) {
logger.error("获取模板类型失败: ${e.message}", e)
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_ERROR, messageSource = messageSource))
}
}
/**
* 获取所有模板
*/
@PostMapping("/templates/list")
fun getTemplates(): ResponseEntity<ApiResponse<List<NotificationTemplateDto>>> {
return try {
val templates = notificationTemplateService.getAllTemplates()
ResponseEntity.ok(ApiResponse.success(templates))
} catch (e: Exception) {
logger.error("获取模板列表失败: ${e.message}", e)
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_ERROR, messageSource = messageSource))
}
}
/**
* 获取单个模板
*/
@PostMapping("/templates/detail")
fun getTemplateDetail(@RequestBody request: TemplateDetailRequest): ResponseEntity<ApiResponse<NotificationTemplateDto>> {
return try {
if (request.templateType.isBlank()) {
return ResponseEntity.ok(ApiResponse.paramError("模板类型不能为空"))
}
val template = notificationTemplateService.getTemplate(request.templateType)
if (template == null) {
ResponseEntity.ok(ApiResponse.error(ErrorCode.NOT_FOUND, messageSource = messageSource))
} else {
ResponseEntity.ok(ApiResponse.success(template))
}
} catch (e: Exception) {
logger.error("获取模板详情失败: ${e.message}", e)
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_ERROR, messageSource = messageSource))
}
}
/**
* 获取模板可用变量
*/
@PostMapping("/templates/variables")
fun getTemplateVariables(@RequestBody request: TemplateDetailRequest): ResponseEntity<ApiResponse<TemplateVariablesResponse>> {
return try {
if (request.templateType.isBlank()) {
return ResponseEntity.ok(ApiResponse.paramError("模板类型不能为空"))
}
val variables = notificationTemplateService.getTemplateVariables(request.templateType)
if (variables == null) {
ResponseEntity.ok(ApiResponse.error(ErrorCode.NOT_FOUND, messageSource = messageSource))
} else {
ResponseEntity.ok(ApiResponse.success(variables))
}
} catch (e: Exception) {
logger.error("获取模板变量失败: ${e.message}", e)
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_ERROR, messageSource = messageSource))
}
}
/**
* 更新模板
*/
@PostMapping("/templates/update")
fun updateTemplate(@RequestBody request: UpdateTemplateRequestWithId): ResponseEntity<ApiResponse<NotificationTemplateDto>> {
return try {
if (request.templateType.isBlank()) {
return ResponseEntity.ok(ApiResponse.paramError("模板类型不能为空"))
}
if (request.templateContent.isBlank()) {
return ResponseEntity.ok(ApiResponse.paramError("模板内容不能为空"))
}
val template = notificationTemplateService.updateTemplate(request.templateType, request.templateContent)
ResponseEntity.ok(ApiResponse.success(template))
} catch (e: Exception) {
logger.error("更新模板失败: ${e.message}", e)
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_ERROR, messageSource = messageSource))
}
}
/**
* 重置模板为默认
*/
@PostMapping("/templates/reset")
fun resetTemplate(@RequestBody request: TemplateDetailRequest): ResponseEntity<ApiResponse<NotificationTemplateDto>> {
return try {
if (request.templateType.isBlank()) {
return ResponseEntity.ok(ApiResponse.paramError("模板类型不能为空"))
}
val template = notificationTemplateService.resetTemplate(request.templateType)
if (template == null) {
ResponseEntity.ok(ApiResponse.error(ErrorCode.NOT_FOUND, messageSource = messageSource))
} else {
ResponseEntity.ok(ApiResponse.success(template))
}
} catch (e: Exception) {
logger.error("重置模板失败: ${e.message}", e)
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_ERROR, messageSource = messageSource))
}
}
/**
* 发送模板测试消息
*/
@PostMapping("/templates/test")
fun testTemplate(@RequestBody request: TestTemplateRequest): ResponseEntity<ApiResponse<Boolean>> {
return try {
if (request.templateType.isBlank()) {
return ResponseEntity.ok(ApiResponse.paramError("模板类型不能为空"))
}
val success = runBlocking {
notificationTemplateService.sendTestMessage(request.templateType, request.templateContent)
}
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
))
}
}
}
/**
@@ -384,3 +535,18 @@ data class NotificationConfigDeleteRequest(
val id: Long
)
/**
* 模板详情请求
*/
data class TemplateDetailRequest(
val templateType: String
)
/**
* 更新模板请求(带类型)
*/
data class UpdateTemplateRequestWithId(
val templateType: String,
val templateContent: String
)
@@ -0,0 +1,67 @@
package com.wrbug.polymarketbot.dto
/**
* 消息模板 DTO
*/
data class NotificationTemplateDto(
val id: Long? = null,
val templateType: String, // 模板类型
val templateContent: String, // 模板内容
val isDefault: Boolean = false, // 是否使用默认模板
val createdAt: Long? = null,
val updatedAt: Long? = null
)
/**
* 模板变量 DTO
*/
data class TemplateVariableDto(
val key: String, // 变量名,如 account_name
val label: String, // 显示名称,如 账户名称
val description: String, // 变量说明
val category: String, // 分类:common, order, copy_trading, redeem, error
val sortOrder: Int = 0 // 排序顺序
)
/**
* 模板变量分类 DTO
*/
data class TemplateVariableCategoryDto(
val key: String, // 分类 key
val label: String, // 分类名称
val sortOrder: Int = 0 // 排序顺序
)
/**
* 模板变量列表响应
*/
data class TemplateVariablesResponse(
val templateType: String, // 模板类型
val templateTypeName: String, // 模板类型名称
val categories: List<TemplateVariableCategoryDto>, // 分类列表
val variables: List<TemplateVariableDto> // 变量列表
)
/**
* 更新模板请求
*/
data class UpdateTemplateRequest(
val templateContent: String // 模板内容
)
/**
* 测试模板请求
*/
data class TestTemplateRequest(
val templateType: String, // 模板类型
val templateContent: String? = null // 可选,如果不提供则使用已保存的模板
)
/**
* 模板类型信息
*/
data class TemplateTypeInfoDto(
val type: String, // 模板类型
val name: String, // 类型名称
val description: String // 类型描述
)
@@ -0,0 +1,30 @@
package com.wrbug.polymarketbot.entity
import jakarta.persistence.*
/**
* 消息推送模板实体
* 用于存储用户自定义的消息模板
*/
@Entity
@Table(name = "notification_templates")
data class NotificationTemplate(
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long? = null,
@Column(name = "template_type", unique = true, nullable = false, length = 50)
val templateType: String, // ORDER_SUCCESS, ORDER_FAILED, ORDER_FILTERED, CRYPTO_TAIL_SUCCESS, REDEEM_SUCCESS, REDEEM_NO_RETURN
@Column(name = "template_content", nullable = false, columnDefinition = "TEXT")
var templateContent: String, // 模板内容,支持 {{variable}} 变量
@Column(name = "is_default", nullable = false)
var isDefault: Boolean = false, // 是否使用默认模板
@Column(name = "created_at", nullable = false)
val createdAt: Long = System.currentTimeMillis(),
@Column(name = "updated_at", nullable = false)
var updatedAt: Long = System.currentTimeMillis()
)
@@ -0,0 +1,11 @@
package com.wrbug.polymarketbot.repository
import com.wrbug.polymarketbot.entity.NotificationTemplate
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.stereotype.Repository
@Repository
interface NotificationTemplateRepository : JpaRepository<NotificationTemplate, Long> {
fun findByTemplateType(templateType: String): NotificationTemplate?
fun existsByTemplateType(templateType: String): Boolean
}
@@ -0,0 +1,430 @@
package com.wrbug.polymarketbot.service.system
import com.wrbug.polymarketbot.dto.*
import com.wrbug.polymarketbot.entity.NotificationTemplate
import com.wrbug.polymarketbot.repository.NotificationTemplateRepository
import org.slf4j.LoggerFactory
import org.springframework.context.annotation.Lazy
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
/**
* 消息模板服务
* 负责管理消息模板、渲染模板、提供变量信息
*/
@Service
class NotificationTemplateService(
private val templateRepository: NotificationTemplateRepository,
@Lazy private val telegramNotificationService: TelegramNotificationService
) {
private val logger = LoggerFactory.getLogger(NotificationTemplateService::class.java)
companion object {
// 模板类型定义
val TEMPLATE_TYPES = mapOf(
"ORDER_SUCCESS" to TemplateTypeInfoDto(
type = "ORDER_SUCCESS",
name = "订单成功通知",
description = "订单创建成功时发送的通知"
),
"ORDER_FAILED" to TemplateTypeInfoDto(
type = "ORDER_FAILED",
name = "订单失败通知",
description = "订单创建失败时发送的通知"
),
"ORDER_FILTERED" to TemplateTypeInfoDto(
type = "ORDER_FILTERED",
name = "订单过滤通知",
description = "订单被风控过滤时发送的通知"
),
"CRYPTO_TAIL_SUCCESS" to TemplateTypeInfoDto(
type = "CRYPTO_TAIL_SUCCESS",
name = "加密价差策略成功通知",
description = "加密价差策略下单成功时发送的通知"
),
"REDEEM_SUCCESS" to TemplateTypeInfoDto(
type = "REDEEM_SUCCESS",
name = "仓位赎回成功通知",
description = "仓位赎回成功时发送的通知"
),
"REDEEM_NO_RETURN" to TemplateTypeInfoDto(
type = "REDEEM_NO_RETURN",
name = "仓位结算(无收益)通知",
description = "仓位结算但无收益时发送的通知"
)
)
// 变量分类
val VARIABLE_CATEGORIES = listOf(
TemplateVariableCategoryDto("common", "通用变量", 0),
TemplateVariableCategoryDto("order", "订单变量", 10),
TemplateVariableCategoryDto("copy_trading", "跟单变量", 20),
TemplateVariableCategoryDto("redeem", "赎回变量", 30),
TemplateVariableCategoryDto("error", "错误变量", 40),
TemplateVariableCategoryDto("filter", "过滤变量", 50),
TemplateVariableCategoryDto("strategy", "策略变量", 60)
)
// 各模板类型可用的变量
val TEMPLATE_VARIABLES = mapOf(
"ORDER_SUCCESS" to listOf(
// 通用变量
TemplateVariableDto("account_name", "账户名称", "执行订单的账户名称", "common", 1),
TemplateVariableDto("wallet_address", "钱包地址", "钱包地址(已脱敏)", "common", 2),
TemplateVariableDto("time", "时间", "通知发送时间", "common", 3),
// 订单变量
TemplateVariableDto("order_id", "订单ID", "订单唯一标识", "order", 10),
TemplateVariableDto("market_title", "市场标题", "市场/事件名称", "order", 11),
TemplateVariableDto("market_link", "市场链接", "Polymarket 市场链接", "order", 12),
TemplateVariableDto("side", "方向", "订单方向(买入/卖出)", "order", 13),
TemplateVariableDto("outcome", "市场方向", "市场方向(YES/NO 等)", "order", 14),
TemplateVariableDto("price", "价格", "订单价格", "order", 15),
TemplateVariableDto("quantity", "数量", "订单数量(shares", "order", 16),
TemplateVariableDto("amount", "金额", "订单金额(USDC", "order", 17),
TemplateVariableDto("available_balance", "可用余额", "账户可用余额(USDC", "order", 18),
// 跟单变量
TemplateVariableDto("leader_name", "Leader 名称", "跟单的 Leader 名称/备注", "copy_trading", 21),
TemplateVariableDto("config_name", "跟单配置名", "跟单配置名称", "copy_trading", 22)
),
"ORDER_FAILED" to listOf(
// 通用变量
TemplateVariableDto("account_name", "账户名称", "执行订单的账户名称", "common", 1),
TemplateVariableDto("wallet_address", "钱包地址", "钱包地址(已脱敏)", "common", 2),
TemplateVariableDto("time", "时间", "通知发送时间", "common", 3),
// 订单变量
TemplateVariableDto("market_title", "市场标题", "市场/事件名称", "order", 10),
TemplateVariableDto("market_link", "市场链接", "Polymarket 市场链接", "order", 11),
TemplateVariableDto("side", "方向", "订单方向(买入/卖出)", "order", 12),
TemplateVariableDto("outcome", "市场方向", "市场方向(YES/NO 等)", "order", 13),
TemplateVariableDto("price", "价格", "订单价格", "order", 14),
TemplateVariableDto("quantity", "数量", "订单数量(shares", "order", 15),
TemplateVariableDto("amount", "金额", "订单金额(USDC", "order", 16),
// 错误变量
TemplateVariableDto("error_message", "错误信息", "订单失败原因", "error", 20)
),
"ORDER_FILTERED" to listOf(
// 通用变量
TemplateVariableDto("account_name", "账户名称", "执行订单的账户名称", "common", 1),
TemplateVariableDto("wallet_address", "钱包地址", "钱包地址(已脱敏)", "common", 2),
TemplateVariableDto("time", "时间", "通知发送时间", "common", 3),
// 订单变量
TemplateVariableDto("market_title", "市场标题", "市场/事件名称", "order", 10),
TemplateVariableDto("market_link", "市场链接", "Polymarket 市场链接", "order", 11),
TemplateVariableDto("side", "方向", "订单方向(买入/卖出)", "order", 12),
TemplateVariableDto("outcome", "市场方向", "市场方向(YES/NO 等)", "order", 13),
TemplateVariableDto("price", "价格", "订单价格", "order", 14),
TemplateVariableDto("quantity", "数量", "订单数量(shares", "order", 15),
TemplateVariableDto("amount", "金额", "订单金额(USDC", "order", 16),
// 过滤变量
TemplateVariableDto("filter_type", "过滤类型", "订单被过滤的类型", "filter", 20),
TemplateVariableDto("filter_reason", "过滤原因", "订单被过滤的详细原因", "filter", 21)
),
"CRYPTO_TAIL_SUCCESS" to listOf(
// 通用变量
TemplateVariableDto("account_name", "账户名称", "执行订单的账户名称", "common", 1),
TemplateVariableDto("wallet_address", "钱包地址", "钱包地址(已脱敏)", "common", 2),
TemplateVariableDto("time", "时间", "通知发送时间", "common", 3),
// 订单变量
TemplateVariableDto("order_id", "订单ID", "订单唯一标识", "order", 10),
TemplateVariableDto("market_title", "市场标题", "市场/事件名称", "order", 11),
TemplateVariableDto("market_link", "市场链接", "Polymarket 市场链接", "order", 12),
TemplateVariableDto("side", "方向", "订单方向(买入/卖出)", "order", 13),
TemplateVariableDto("outcome", "市场方向", "市场方向(YES/NO 等)", "order", 14),
TemplateVariableDto("price", "价格", "订单价格", "order", 15),
TemplateVariableDto("quantity", "数量", "订单数量(shares", "order", 16),
TemplateVariableDto("amount", "金额", "订单金额(USDC", "order", 17),
// 策略变量
TemplateVariableDto("strategy_name", "策略名称", "加密价差策略名称", "strategy", 20)
),
"REDEEM_SUCCESS" to listOf(
// 通用变量
TemplateVariableDto("account_name", "账户名称", "执行赎回的账户名称", "common", 1),
TemplateVariableDto("wallet_address", "钱包地址", "钱包地址(已脱敏)", "common", 2),
TemplateVariableDto("time", "时间", "通知发送时间", "common", 3),
// 赎回变量
TemplateVariableDto("transaction_hash", "交易哈希", "赎回交易的哈希值", "redeem", 10),
TemplateVariableDto("total_value", "赎回总价值", "赎回的总价值(USDC", "redeem", 11),
TemplateVariableDto("available_balance", "可用余额", "账户可用余额(USDC", "redeem", 12)
),
"REDEEM_NO_RETURN" to listOf(
// 通用变量
TemplateVariableDto("account_name", "账户名称", "执行赎回的账户名称", "common", 1),
TemplateVariableDto("wallet_address", "钱包地址", "钱包地址(已脱敏)", "common", 2),
TemplateVariableDto("time", "时间", "通知发送时间", "common", 3),
// 赎回变量
TemplateVariableDto("transaction_hash", "交易哈希", "赎回交易的哈希值", "redeem", 10),
TemplateVariableDto("available_balance", "可用余额", "账户可用余额(USDC", "redeem", 11)
)
)
// 默认模板
val DEFAULT_TEMPLATES = mapOf(
"ORDER_SUCCESS" to """
🚀 <b>订单创建成功</b>
📊 <b>订单信息:</b>
• 订单ID: <code>{{order_id}}</code>
• 市场: <a href="{{market_link}}">{{market_title}}</a>
• 市场方向: <b>{{outcome}}</b>
• 方向: <b>{{side}}</b>
• 价格: <code>{{price}}</code>
• 数量: <code>{{quantity}}</code> shares
• 金额: <code>{{amount}}</code> USDC
• 账户: {{account_name}}
• 可用余额: <code>{{available_balance}}</code> USDC
⏰ 时间: <code>{{time}}</code>
""".trimIndent(),
"ORDER_FAILED" to """
❌ <b>订单创建失败</b>
📊 <b>订单信息:</b>
• 市场: <a href="{{market_link}}">{{market_title}}</a>
• 市场方向: <b>{{outcome}}</b>
• 方向: <b>{{side}}</b>
• 价格: <code>{{price}}</code>
• 数量: <code>{{quantity}}</code> shares
• 金额: <code>{{amount}}</code> USDC
• 账户: {{account_name}}
⚠️ <b>错误信息:</b>
<code>{{error_message}}</code>
⏰ 时间: <code>{{time}}</code>
""".trimIndent(),
"ORDER_FILTERED" to """
🚫 <b>订单被过滤</b>
📊 <b>订单信息:</b>
• 市场: <a href="{{market_link}}">{{market_title}}</a>
• 市场方向: <b>{{outcome}}</b>
• 方向: <b>{{side}}</b>
• 价格: <code>{{price}}</code>
• 数量: <code>{{quantity}}</code> shares
• 金额: <code>{{amount}}</code> USDC
• 账户: {{account_name}}
⚠️ <b>过滤类型:</b> <code>{{filter_type}}</code>
📝 <b>过滤原因:</b>
<code>{{filter_reason}}</code>
⏰ 时间: <code>{{time}}</code>
""".trimIndent(),
"CRYPTO_TAIL_SUCCESS" to """
🚀 <b>加密价差策略下单成功</b>
📊 <b>订单信息:</b>
• 订单ID: <code>{{order_id}}</code>
• 策略: {{strategy_name}}
• 市场: <a href="{{market_link}}">{{market_title}}</a>
• 市场方向: <b>{{outcome}}</b>
• 方向: <b>{{side}}</b>
• 价格: <code>{{price}}</code>
• 数量: <code>{{quantity}}</code> shares
• 金额: <code>{{amount}}</code> USDC
• 账户: {{account_name}}
⏰ 时间: <code>{{time}}</code>
""".trimIndent(),
"REDEEM_SUCCESS" to """
💸 <b>仓位赎回成功</b>
📊 <b>赎回信息:</b>
• 账户: {{account_name}}
• 交易哈希: <code>{{transaction_hash}}</code>
• 赎回总价值: <code>{{total_value}}</code> USDC
• 可用余额: <code>{{available_balance}}</code> USDC
⏰ 时间: <code>{{time}}</code>
""".trimIndent(),
"REDEEM_NO_RETURN" to """
📋 <b>仓位已结算(无收益)</b>
📊 <b>结算信息:</b>
<i>市场已结算,您的预测未命中,赎回价值为 0。</i>
• 账户: {{account_name}}
• 交易哈希: <code>{{transaction_hash}}</code>
• 可用余额: <code>{{available_balance}}</code> USDC
⏰ 时间: <code>{{time}}</code>
""".trimIndent()
)
}
/**
* 获取所有模板类型
*/
fun getTemplateTypes(): List<TemplateTypeInfoDto> {
return TEMPLATE_TYPES.values.toList()
}
/**
* 获取所有模板列表
*/
fun getAllTemplates(): List<NotificationTemplateDto> {
return templateRepository.findAll().map { it.toDto() }
}
/**
* 获取单个模板
*/
fun getTemplate(templateType: String): NotificationTemplateDto? {
return templateRepository.findByTemplateType(templateType)?.toDto()
?: DEFAULT_TEMPLATES[templateType]?.let {
NotificationTemplateDto(
templateType = templateType,
templateContent = it,
isDefault = true
)
}
}
/**
* 获取模板可用变量
*/
fun getTemplateVariables(templateType: String): TemplateVariablesResponse? {
val typeInfo = TEMPLATE_TYPES[templateType] ?: return null
val variables = TEMPLATE_VARIABLES[templateType] ?: emptyList()
// 获取使用的分类
val usedCategories = variables.map { it.category }.toSet()
val categories = VARIABLE_CATEGORIES.filter { usedCategories.contains(it.key) }
return TemplateVariablesResponse(
templateType = templateType,
templateTypeName = typeInfo.name,
categories = categories,
variables = variables
)
}
/**
* 更新模板
*/
@Transactional
fun updateTemplate(templateType: String, content: String): NotificationTemplateDto {
val template = templateRepository.findByTemplateType(templateType)
val now = System.currentTimeMillis()
return if (template != null) {
template.templateContent = content
template.isDefault = false
template.updatedAt = now
templateRepository.save(template).toDto()
} else {
val newTemplate = NotificationTemplate(
templateType = templateType,
templateContent = content,
isDefault = false,
createdAt = now,
updatedAt = now
)
templateRepository.save(newTemplate).toDto()
}
}
/**
* 重置模板为默认
*/
@Transactional
fun resetTemplate(templateType: String): NotificationTemplateDto? {
val defaultContent = DEFAULT_TEMPLATES[templateType] ?: return null
val template = templateRepository.findByTemplateType(templateType)
val now = System.currentTimeMillis()
return if (template != null) {
template.templateContent = defaultContent
template.isDefault = true
template.updatedAt = now
templateRepository.save(template).toDto()
} else {
val newTemplate = NotificationTemplate(
templateType = templateType,
templateContent = defaultContent,
isDefault = true,
createdAt = now,
updatedAt = now
)
templateRepository.save(newTemplate).toDto()
}
}
/**
* 渲染模板(按类型取模板内容后替换变量)
*/
fun renderTemplate(templateType: String, variables: Map<String, String>): String {
val template = getTemplate(templateType)
val content = template?.templateContent ?: DEFAULT_TEMPLATES[templateType] ?: ""
return renderTemplateContent(content, variables)
}
/**
* 对给定模板内容做变量替换(不查库)
*/
fun renderTemplateContent(content: String, variables: Map<String, String>): String {
var result = content
variables.forEach { (key, value) ->
result = result.replace("{{$key}}", value)
}
result = result.replace(Regex("\\{\\{[^}]+}}"), "-")
return result
}
/**
* 发送测试消息
*/
suspend fun sendTestMessage(templateType: String, content: String? = null): Boolean {
val templateContent = content ?: getTemplate(templateType)?.templateContent ?: return false
val testVariables = generateTestVariables(templateType)
val message = renderTemplateContent(templateContent, testVariables)
return try {
telegramNotificationService.sendMessage(message)
true
} catch (e: Exception) {
logger.error("发送测试消息失败: ${e.message}", e)
false
}
}
/**
* 生成测试变量数据
*/
private fun generateTestVariables(templateType: String): Map<String, String> {
return mapOf(
"account_name" to "测试账户",
"wallet_address" to "0x1234...5678",
"time" to "2024-01-15 12:30:00",
"order_id" to "12345678",
"market_title" to "测试市场标题",
"market_link" to "https://polymarket.com/event/test",
"side" to "买入",
"outcome" to "YES",
"price" to "0.55",
"quantity" to "100",
"amount" to "55.00",
"available_balance" to "1000.00",
"leader_name" to "测试Leader",
"config_name" to "测试配置",
"error_message" to "余额不足",
"filter_type" to "价差过大",
"filter_reason" to "当前市场价差为 5%,超过设定的 3% 限制",
"strategy_name" to "BTC价差策略",
"transaction_hash" to "0xabcd...efgh",
"total_value" to "100.00"
)
}
/**
* Entity 转 DTO
*/
private fun NotificationTemplate.toDto() = NotificationTemplateDto(
id = id,
templateType = templateType,
templateContent = templateContent,
isDefault = isDefault,
createdAt = createdAt,
updatedAt = updatedAt
)
}
@@ -26,6 +26,7 @@ import java.util.concurrent.TimeUnit
@Service
class TelegramNotificationService(
private val notificationConfigService: NotificationConfigService,
private val notificationTemplateService: NotificationTemplateService,
private val objectMapper: ObjectMapper,
private val messageSource: MessageSource
) {
@@ -178,7 +179,9 @@ class TelegramNotificationService(
null
}
val message = buildOrderSuccessMessage(
val unknownAccount = messageSource.getMessage("notification.order.unknown_account", null, "未知账户", currentLocale).orEmpty().ifEmpty { "未知账户" }
val calculateFailed = messageSource.getMessage("notification.order.calculate_failed", null, "计算失败", currentLocale).orEmpty().ifEmpty { "计算失败" }
val vars = buildOrderSuccessVariables(
orderId = orderId,
marketTitle = marketTitle,
marketId = marketId,
@@ -194,8 +197,11 @@ class TelegramNotificationService(
leaderName = leaderName,
configName = configName,
orderTime = orderTime,
availableBalance = availableBalance
availableBalance = availableBalance,
unknownAccount = unknownAccount,
calculateFailed = calculateFailed
)
val message = notificationTemplateService.renderTemplate("ORDER_SUCCESS", vars)
sendMessage(message)
}
@@ -234,7 +240,9 @@ class TelegramNotificationService(
null
}
val message = buildOrderFailureMessage(
val unknownAccount = messageSource.getMessage("notification.order.unknown_account", null, "未知账户", currentLocale).orEmpty().ifEmpty { "未知账户" }
val calculateFailed = messageSource.getMessage("notification.order.calculate_failed", null, "计算失败", currentLocale).orEmpty().ifEmpty { "计算失败" }
val vars = buildOrderFailureVariables(
marketTitle = marketTitle,
marketId = marketId,
marketSlug = marketSlug,
@@ -246,11 +254,65 @@ class TelegramNotificationService(
errorMessage = errorMessage,
accountName = accountName,
walletAddress = walletAddress,
locale = currentLocale
locale = currentLocale,
unknownAccount = unknownAccount,
calculateFailed = calculateFailed
)
val message = notificationTemplateService.renderTemplate("ORDER_FAILED", vars)
sendMessage(message)
}
/**
* 构建订单失败通知的变量 Map
*/
private fun buildOrderFailureVariables(
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,
unknownAccount: String,
calculateFailed: String
): Map<String, String> {
val sideDisplay = when (side.uppercase()) {
"BUY" -> messageSource.getMessage("notification.order.side.buy", null, "买入", locale).orEmpty().ifEmpty { "买入" }
"SELL" -> messageSource.getMessage("notification.order.side.sell", null, "卖出", locale).orEmpty().ifEmpty { "卖出" }
else -> side
}
val accountInfo = buildAccountInfo(accountName, walletAddress, unknownAccount)
val marketLink = when {
!marketSlug.isNullOrBlank() -> "https://polymarket.com/event/$marketSlug"
!marketId.isNullOrBlank() && marketId.startsWith("0x") -> "https://polymarket.com/condition/$marketId"
else -> ""
}
val amountDisplay = amount?.let { am ->
try {
val amountDecimal = am.toSafeBigDecimal()
(if (amountDecimal.scale() > 4) amountDecimal.setScale(4, java.math.RoundingMode.DOWN).stripTrailingZeros() else amountDecimal.stripTrailingZeros()).toPlainString()
} catch (e: Exception) { am }
} ?: calculateFailed
val shortError = if (errorMessage.length > 500) errorMessage.substring(0, 500) + "..." else errorMessage
return mapOf(
"market_title" to marketTitle.replace("<", "&lt;").replace(">", "&gt;"),
"market_link" to marketLink,
"side" to sideDisplay,
"outcome" to (outcome?.replace("<", "&lt;")?.replace(">", "&gt;") ?: ""),
"price" to formatPrice(price),
"quantity" to formatQuantity(size),
"amount" to amountDisplay,
"account_name" to accountInfo,
"error_message" to shortError.replace("<", "&lt;").replace(">", "&gt;"),
"time" to DateUtils.formatDateTime()
)
}
/**
* 发送订单被过滤通知
* @param locale 语言设置(可选,如果提供则使用,否则使用 LocaleContextHolder 获取)
@@ -287,7 +349,9 @@ class TelegramNotificationService(
null
}
val message = buildOrderFilteredMessage(
val unknownAccount = messageSource.getMessage("notification.order.unknown_account", null, "未知账户", currentLocale).orEmpty().ifEmpty { "未知账户" }
val calculateFailed = messageSource.getMessage("notification.order.calculate_failed", null, "计算失败", currentLocale).orEmpty().ifEmpty { "计算失败" }
val vars = buildOrderFilteredVariables(
marketTitle = marketTitle,
marketId = marketId,
marketSlug = marketSlug,
@@ -300,11 +364,70 @@ class TelegramNotificationService(
filterType = filterType,
accountName = accountName,
walletAddress = walletAddress,
locale = currentLocale
locale = currentLocale,
unknownAccount = unknownAccount,
calculateFailed = calculateFailed
)
val message = notificationTemplateService.renderTemplate("ORDER_FILTERED", vars)
sendMessage(message)
}
private fun buildOrderFilteredVariables(
marketTitle: String,
marketId: String?,
marketSlug: String?,
side: String,
outcome: String?,
price: String,
size: String,
amount: String?,
filterReason: String,
filterType: String,
accountName: String?,
walletAddress: String?,
locale: java.util.Locale,
unknownAccount: String,
calculateFailed: String
): Map<String, String> {
val sideDisplay = when (side.uppercase()) {
"BUY" -> messageSource.getMessage("notification.order.side.buy", null, "买入", locale).orEmpty().ifEmpty { "买入" }
"SELL" -> messageSource.getMessage("notification.order.side.sell", null, "卖出", locale).orEmpty().ifEmpty { "卖出" }
else -> side
}
val filterTypeDisplay = when (filterType.uppercase()) {
"ORDER_DEPTH" -> messageSource.getMessage("notification.filter.type.order_depth", null, "订单深度不足", locale).orEmpty().ifEmpty { "订单深度不足" }
"SPREAD" -> messageSource.getMessage("notification.filter.type.spread", null, "价差过大", locale).orEmpty().ifEmpty { "价差过大" }
"ORDERBOOK_DEPTH" -> messageSource.getMessage("notification.filter.type.orderbook_depth", null, "订单簿深度不足", locale).orEmpty().ifEmpty { "订单簿深度不足" }
"PRICE_VALIDITY" -> messageSource.getMessage("notification.filter.type.price_validity", null, "价格不合理", locale).orEmpty().ifEmpty { "价格不合理" }
"MARKET_STATUS" -> messageSource.getMessage("notification.filter.type.market_status", null, "市场状态不可交易", locale).orEmpty().ifEmpty { "市场状态不可交易" }
else -> filterType
}
val accountInfo = buildAccountInfo(accountName, walletAddress, unknownAccount)
val marketLink = when {
!marketSlug.isNullOrBlank() -> "https://polymarket.com/event/$marketSlug"
!marketId.isNullOrBlank() && marketId.startsWith("0x") -> "https://polymarket.com/condition/$marketId"
else -> ""
}
val amountDisplay = amount?.let { am ->
try {
(am.toSafeBigDecimal().let { if (it.scale() > 4) it.setScale(4, java.math.RoundingMode.DOWN).stripTrailingZeros() else it.stripTrailingZeros() }.toPlainString())
} catch (e: Exception) { am }
} ?: calculateFailed
return mapOf(
"market_title" to marketTitle.replace("<", "&lt;").replace(">", "&gt;"),
"market_link" to marketLink,
"side" to sideDisplay,
"outcome" to (outcome?.replace("<", "&lt;")?.replace(">", "&gt;") ?: ""),
"price" to formatPrice(price),
"quantity" to formatQuantity(size),
"amount" to amountDisplay,
"account_name" to accountInfo,
"filter_type" to filterTypeDisplay,
"filter_reason" to filterReason.replace("<", "&lt;").replace(">", "&gt;"),
"time" to DateUtils.formatDateTime()
)
}
/**
* 发送加密价差策略下单成功通知(与跟单一致:在收到 WS 订单推送时匹配价差策略订单后调用)
*/
@@ -349,7 +472,10 @@ class TelegramNotificationService(
logger.warn("计算订单金额失败: ${e.message}", e)
null
}
val message = buildCryptoTailOrderSuccessMessage(
val unknown = messageSource.getMessage("common.unknown", null, "未知", currentLocale).orEmpty().ifEmpty { "未知" }
val unknownAccount = messageSource.getMessage("notification.order.unknown_account", null, "未知账户", currentLocale).orEmpty().ifEmpty { "未知账户" }
val calculateFailed = messageSource.getMessage("notification.order.calculate_failed", null, "计算失败", currentLocale).orEmpty().ifEmpty { "计算失败" }
val vars = buildCryptoTailOrderSuccessVariables(
orderId = orderId,
marketTitle = marketTitle,
marketId = marketId,
@@ -362,12 +488,67 @@ class TelegramNotificationService(
strategyName = strategyName,
accountName = accountName,
walletAddress = walletAddress,
locale = currentLocale,
orderTime = orderTime
orderTime = orderTime,
unknown = unknown,
unknownAccount = unknownAccount,
calculateFailed = calculateFailed,
locale = currentLocale
)
val message = notificationTemplateService.renderTemplate("CRYPTO_TAIL_SUCCESS", vars)
sendMessage(message)
}
private fun buildCryptoTailOrderSuccessVariables(
orderId: String?,
marketTitle: String,
marketId: String?,
marketSlug: String?,
side: String,
outcome: String?,
price: String,
size: String,
amount: String?,
strategyName: String?,
accountName: String?,
walletAddress: String?,
orderTime: Long?,
unknown: String,
unknownAccount: String,
calculateFailed: String,
locale: java.util.Locale
): Map<String, String> {
val sideDisplay = when (side.uppercase()) {
"BUY" -> messageSource.getMessage("notification.order.side.buy", null, "买入", locale).orEmpty().ifEmpty { "买入" }
"SELL" -> messageSource.getMessage("notification.order.side.sell", null, "卖出", locale).orEmpty().ifEmpty { "卖出" }
else -> side
}
val accountInfo = buildAccountInfo(accountName, walletAddress, unknownAccount)
val time = if (orderTime != null) DateUtils.formatDateTime(orderTime) else DateUtils.formatDateTime()
val marketLink = when {
!marketSlug.isNullOrBlank() -> "https://polymarket.com/event/$marketSlug"
!marketId.isNullOrBlank() && marketId.startsWith("0x") -> "https://polymarket.com/condition/$marketId"
else -> ""
}
val amountDisplay = amount?.let { am ->
try {
(am.toSafeBigDecimal().let { if (it.scale() > 4) it.setScale(4, java.math.RoundingMode.DOWN).stripTrailingZeros() else it.stripTrailingZeros() }.toPlainString())
} catch (e: Exception) { am }
} ?: calculateFailed
return mapOf(
"order_id" to (orderId ?: unknown),
"market_title" to marketTitle.replace("<", "&lt;").replace(">", "&gt;"),
"market_link" to marketLink,
"side" to sideDisplay,
"outcome" to (outcome?.replace("<", "&lt;")?.replace(">", "&gt;") ?: ""),
"price" to formatPrice(price),
"quantity" to formatQuantity(size),
"amount" to amountDisplay,
"account_name" to accountInfo,
"strategy_name" to (strategyName?.takeIf { it.isNotBlank() } ?: unknown),
"time" to time
)
}
/**
* 构建订单被过滤消息
*/
@@ -750,6 +931,76 @@ class TelegramNotificationService(
}
}
/**
* 构建订单成功通知的变量 Map(供模板渲染)
*/
private fun buildOrderSuccessVariables(
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,
leaderName: String?,
configName: String?,
orderTime: Long?,
availableBalance: String?,
unknownAccount: String,
calculateFailed: String
): Map<String, String> {
val sideDisplay = when (side.uppercase()) {
"BUY" -> messageSource.getMessage("notification.order.side.buy", null, "买入", locale).orEmpty().ifEmpty { "买入" }
"SELL" -> messageSource.getMessage("notification.order.side.sell", null, "卖出", locale).orEmpty().ifEmpty { "卖出" }
else -> side
}
val unknown = messageSource.getMessage("common.unknown", null, "未知", locale).orEmpty().ifEmpty { "未知" }
val accountInfo = buildAccountInfo(accountName, walletAddress, unknownAccount)
val time = if (orderTime != null) DateUtils.formatDateTime(orderTime) else DateUtils.formatDateTime()
val marketLink = when {
!marketSlug.isNullOrBlank() -> "https://polymarket.com/event/$marketSlug"
!marketId.isNullOrBlank() && marketId.startsWith("0x") -> "https://polymarket.com/condition/$marketId"
else -> ""
}
val amountDisplay = when {
amount != null -> try {
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 ?: calculateFailed }
else -> calculateFailed
}
val availableBalanceDisplay = if (!availableBalance.isNullOrBlank()) {
try {
val balanceDecimal = availableBalance.toSafeBigDecimal()
val formatted = if (balanceDecimal.scale() > 4) balanceDecimal.setScale(4, java.math.RoundingMode.DOWN).stripTrailingZeros() else balanceDecimal.stripTrailingZeros()
formatted.toPlainString()
} catch (e: Exception) { availableBalance ?: "" }
} else { "" }
val escapedMarketTitle = marketTitle.replace("<", "&lt;").replace(">", "&gt;")
val escapedOutcome = outcome?.replace("<", "&lt;")?.replace(">", "&gt;") ?: ""
return mapOf(
"order_id" to (orderId ?: unknown),
"market_title" to escapedMarketTitle,
"market_link" to marketLink,
"side" to sideDisplay,
"outcome" to escapedOutcome,
"price" to formatPrice(price),
"quantity" to formatQuantity(size),
"amount" to amountDisplay,
"account_name" to accountInfo,
"available_balance" to availableBalanceDisplay,
"leader_name" to (leaderName ?: ""),
"config_name" to (configName ?: ""),
"time" to time
)
}
/**
* 构建订单成功消息
*/
@@ -1135,17 +1386,46 @@ class TelegramNotificationService(
java.util.Locale("zh", "CN") // 默认简体中文
}
val message = buildRedeemMessage(
val unknownAccount = messageSource.getMessage("notification.order.unknown_account", null, "未知账户", currentLocale) ?: "未知账户"
val vars = buildRedeemSuccessVariables(
accountName = accountName,
walletAddress = walletAddress,
transactionHash = transactionHash,
totalRedeemedValue = totalRedeemedValue,
positions = positions,
locale = currentLocale,
availableBalance = availableBalance
availableBalance = availableBalance,
unknownAccount = unknownAccount
)
val message = notificationTemplateService.renderTemplate("REDEEM_SUCCESS", vars)
sendMessage(message)
}
private fun buildRedeemSuccessVariables(
accountName: String?,
walletAddress: String?,
transactionHash: String,
totalRedeemedValue: String,
availableBalance: String?,
unknownAccount: String
): Map<String, String> {
val accountInfo = buildAccountInfo(accountName, walletAddress, unknownAccount)
val totalValueDisplay = try {
val d = totalRedeemedValue.toSafeBigDecimal()
(if (d.scale() > 4) d.setScale(4, java.math.RoundingMode.DOWN).stripTrailingZeros() else d.stripTrailingZeros()).toPlainString()
} catch (e: Exception) { totalRedeemedValue }
val availableBalanceDisplay = availableBalance?.let { ab ->
try {
val d = ab.toSafeBigDecimal()
(if (d.scale() > 4) d.setScale(4, java.math.RoundingMode.DOWN).stripTrailingZeros() else d.stripTrailingZeros()).toPlainString()
} catch (e: Exception) { ab }
} ?: ""
return mapOf(
"account_name" to accountInfo,
"transaction_hash" to transactionHash.replace("<", "&lt;").replace(">", "&gt;"),
"total_value" to totalValueDisplay,
"available_balance" to availableBalanceDisplay,
"time" to DateUtils.formatDateTime()
)
}
/**
* 构建仓位赎回消息
@@ -1261,17 +1541,40 @@ $positionsText
java.util.Locale("zh", "CN")
}
val message = buildRedeemNoReturnMessage(
val unknownAccount = messageSource.getMessage("notification.order.unknown_account", null, "未知账户", currentLocale) ?: "未知账户"
val vars = buildRedeemNoReturnVariables(
accountName = accountName,
walletAddress = walletAddress,
transactionHash = transactionHash,
positions = positions,
locale = currentLocale,
availableBalance = availableBalance
availableBalance = availableBalance,
unknownAccount = unknownAccount
)
val message = notificationTemplateService.renderTemplate("REDEEM_NO_RETURN", vars)
sendMessage(message)
}
private fun buildRedeemNoReturnVariables(
accountName: String?,
walletAddress: String?,
transactionHash: String,
availableBalance: String?,
unknownAccount: String
): Map<String, String> {
val accountInfo = buildAccountInfo(accountName, walletAddress, unknownAccount)
val availableBalanceDisplay = availableBalance?.let { ab ->
try {
val d = ab.toSafeBigDecimal()
(if (d.scale() > 4) d.setScale(4, java.math.RoundingMode.DOWN).stripTrailingZeros() else d.stripTrailingZeros()).toPlainString()
} catch (e: Exception) { ab }
} ?: ""
return mapOf(
"account_name" to accountInfo,
"transaction_hash" to transactionHash.replace("<", "&lt;").replace(">", "&gt;"),
"available_balance" to availableBalanceDisplay,
"time" to DateUtils.formatDateTime()
)
}
/**
* 构建仓位已结算(无收益)消息
*/
@@ -0,0 +1,97 @@
-- 消息模板表
CREATE TABLE notification_templates (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
template_type VARCHAR(50) NOT NULL COMMENT '模板类型',
template_content TEXT NOT NULL COMMENT '模板内容,支持 {{variable}} 变量',
is_default TINYINT(1) DEFAULT 0 COMMENT '是否使用默认模板(0=自定义,1=默认)',
created_at BIGINT NOT NULL,
updated_at BIGINT NOT NULL,
UNIQUE KEY uk_template_type (template_type)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='消息推送模板';
-- 插入默认模板
INSERT INTO notification_templates (template_type, template_content, is_default, created_at, updated_at) VALUES
('ORDER_SUCCESS', '🚀 <b>订单创建成功</b>
📊 <b>订单信息:</b>
• 订单ID: <code>{{order_id}}</code>
• 市场: <a href="{{market_link}}">{{market_title}}</a>
• 市场方向: <b>{{outcome}}</b>
• 方向: <b>{{side}}</b>
• 价格: <code>{{price}}</code>
• 数量: <code>{{quantity}}</code> shares
• 金额: <code>{{amount}}</code> USDC
• 账户: {{account_name}}
• 可用余额: <code>{{available_balance}}</code> USDC
⏰ 时间: <code>{{time}}</code>', 1, UNIX_TIMESTAMP() * 1000, UNIX_TIMESTAMP() * 1000),
('ORDER_FAILED', '❌ <b>订单创建失败</b>
📊 <b>订单信息:</b>
• 市场: <a href="{{market_link}}">{{market_title}}</a>
• 市场方向: <b>{{outcome}}</b>
• 方向: <b>{{side}}</b>
• 价格: <code>{{price}}</code>
• 数量: <code>{{quantity}}</code> shares
• 金额: <code>{{amount}}</code> USDC
• 账户: {{account_name}}
⚠️ <b>错误信息:</b>
<code>{{error_message}}</code>
⏰ 时间: <code>{{time}}</code>', 1, UNIX_TIMESTAMP() * 1000, UNIX_TIMESTAMP() * 1000),
('ORDER_FILTERED', '🚫 <b>订单被过滤</b>
📊 <b>订单信息:</b>
• 市场: <a href="{{market_link}}">{{market_title}}</a>
• 市场方向: <b>{{outcome}}</b>
• 方向: <b>{{side}}</b>
• 价格: <code>{{price}}</code>
• 数量: <code>{{quantity}}</code> shares
• 金额: <code>{{amount}}</code> USDC
• 账户: {{account_name}}
⚠️ <b>过滤类型:</b> <code>{{filter_type}}</code>
📝 <b>过滤原因:</b>
<code>{{filter_reason}}</code>
⏰ 时间: <code>{{time}}</code>', 1, UNIX_TIMESTAMP() * 1000, UNIX_TIMESTAMP() * 1000),
('CRYPTO_TAIL_SUCCESS', '🚀 <b>加密价差策略下单成功</b>
📊 <b>订单信息:</b>
• 订单ID: <code>{{order_id}}</code>
• 策略: {{strategy_name}}
• 市场: <a href="{{market_link}}">{{market_title}}</a>
• 市场方向: <b>{{outcome}}</b>
• 方向: <b>{{side}}</b>
• 价格: <code>{{price}}</code>
• 数量: <code>{{quantity}}</code> shares
• 金额: <code>{{amount}}</code> USDC
• 账户: {{account_name}}
⏰ 时间: <code>{{time}}</code>', 1, UNIX_TIMESTAMP() * 1000, UNIX_TIMESTAMP() * 1000),
('REDEEM_SUCCESS', '💸 <b>仓位赎回成功</b>
📊 <b>赎回信息:</b>
• 账户: {{account_name}}
• 交易哈希: <code>{{transaction_hash}}</code>
• 赎回总价值: <code>{{total_value}}</code> USDC
• 可用余额: <code>{{available_balance}}</code> USDC
⏰ 时间: <code>{{time}}</code>', 1, UNIX_TIMESTAMP() * 1000, UNIX_TIMESTAMP() * 1000),
('REDEEM_NO_RETURN', '📋 <b>仓位已结算(无收益)</b>
📊 <b>结算信息:</b>
<i>市场已结算,您的预测未命中,赎回价值为 0。</i>
• 账户: {{account_name}}
• 交易哈希: <code>{{transaction_hash}}</code>
• 可用余额: <code>{{available_balance}}</code> USDC
⏰ 时间: <code>{{time}}</code>', 1, UNIX_TIMESTAMP() * 1000, UNIX_TIMESTAMP() * 1000);
@@ -17,6 +17,14 @@ notification.order.available_balance=Available Balance
notification.order.error_info=Error Information
notification.order.unknown_account=Unknown Account
notification.order.calculate_failed=Calculation Failed
notification.order.filtered=Order Filtered
notification.order.filter_reason=Filter Reason
notification.order.filter_type=Filter Type
notification.filter.type.order_depth=Insufficient Order Depth
notification.filter.type.spread=Spread Too Large
notification.filter.type.orderbook_depth=Insufficient Orderbook Depth
notification.filter.type.price_validity=Invalid Price
notification.filter.type.market_status=Market Not Tradable
notification.tail.order.success=Crypto spread strategy order success
notification.tail.strategy=Strategy
notification.redeem.success=Position Redeemed Successfully
@@ -17,6 +17,14 @@ notification.order.available_balance=可用余额
notification.order.error_info=错误信息
notification.order.unknown_account=未知账户
notification.order.calculate_failed=计算失败
notification.order.filtered=订单被过滤
notification.order.filter_reason=过滤原因
notification.order.filter_type=过滤类型
notification.filter.type.order_depth=订单深度不足
notification.filter.type.spread=价差过大
notification.filter.type.orderbook_depth=订单簿深度不足
notification.filter.type.price_validity=价格不合理
notification.filter.type.market_status=市场状态不可交易
notification.tail.order.success=加密价差策略下单成功
notification.tail.strategy=策略
notification.redeem.success=仓位赎回成功
@@ -17,6 +17,14 @@ notification.order.available_balance=可用餘額
notification.order.error_info=錯誤信息
notification.order.unknown_account=未知賬戶
notification.order.calculate_failed=計算失敗
notification.order.filtered=訂單被過濾
notification.order.filter_reason=過濾原因
notification.order.filter_type=過濾類型
notification.filter.type.order_depth=訂單深度不足
notification.filter.type.spread=價差過大
notification.filter.type.orderbook_depth=訂單簿深度不足
notification.filter.type.price_validity=價格不合理
notification.filter.type.market_status=市場狀態不可交易
notification.tail.order.success=加密價差策略下單成功
notification.tail.strategy=策略
notification.redeem.success=倉位贖回成功