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:
+166
@@ -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()
|
||||
)
|
||||
+11
@@ -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
|
||||
}
|
||||
+430
@@ -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
|
||||
)
|
||||
}
|
||||
+320
-17
@@ -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("<", "<").replace(">", ">"),
|
||||
"market_link" to marketLink,
|
||||
"side" to sideDisplay,
|
||||
"outcome" to (outcome?.replace("<", "<")?.replace(">", ">") ?: ""),
|
||||
"price" to formatPrice(price),
|
||||
"quantity" to formatQuantity(size),
|
||||
"amount" to amountDisplay,
|
||||
"account_name" to accountInfo,
|
||||
"error_message" to shortError.replace("<", "<").replace(">", ">"),
|
||||
"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("<", "<").replace(">", ">"),
|
||||
"market_link" to marketLink,
|
||||
"side" to sideDisplay,
|
||||
"outcome" to (outcome?.replace("<", "<")?.replace(">", ">") ?: ""),
|
||||
"price" to formatPrice(price),
|
||||
"quantity" to formatQuantity(size),
|
||||
"amount" to amountDisplay,
|
||||
"account_name" to accountInfo,
|
||||
"filter_type" to filterTypeDisplay,
|
||||
"filter_reason" to filterReason.replace("<", "<").replace(">", ">"),
|
||||
"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("<", "<").replace(">", ">"),
|
||||
"market_link" to marketLink,
|
||||
"side" to sideDisplay,
|
||||
"outcome" to (outcome?.replace("<", "<")?.replace(">", ">") ?: ""),
|
||||
"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("<", "<").replace(">", ">")
|
||||
val escapedOutcome = outcome?.replace("<", "<")?.replace(">", ">") ?: ""
|
||||
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("<", "<").replace(">", ">"),
|
||||
"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("<", "<").replace(">", ">"),
|
||||
"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=倉位贖回成功
|
||||
|
||||
@@ -29,6 +29,7 @@ import CopyTradingSellOrders from './pages/CopyTradingSellOrders'
|
||||
import CopyTradingMatchedOrders from './pages/CopyTradingMatchedOrders'
|
||||
import FilteredOrdersList from './pages/FilteredOrdersList'
|
||||
import SystemSettings from './pages/SystemSettings'
|
||||
import NotificationSettingsPage from './pages/NotificationSettingsPage'
|
||||
import ApiHealthStatus from './pages/ApiHealthStatus'
|
||||
import RpcNodeSettings from './pages/RpcNodeSettings'
|
||||
import Announcements from './pages/Announcements'
|
||||
@@ -268,6 +269,7 @@ function App() {
|
||||
<Route path="/users" element={<ProtectedRoute><UserList /></ProtectedRoute>} />
|
||||
<Route path="/announcements" element={<ProtectedRoute><Announcements /></ProtectedRoute>} />
|
||||
<Route path="/system-settings" element={<ProtectedRoute><SystemSettings /></ProtectedRoute>} />
|
||||
<Route path="/system-settings/notification" element={<ProtectedRoute><NotificationSettingsPage /></ProtectedRoute>} />
|
||||
<Route path="/system-settings/rpc-nodes" element={<ProtectedRoute><RpcNodeSettings /></ProtectedRoute>} /> <Route path="/system-settings/api-health" element={<ProtectedRoute><ApiHealthStatus /></ProtectedRoute>} />
|
||||
|
||||
{/* 默认重定向到登录页 */}
|
||||
|
||||
@@ -216,6 +216,11 @@ const Layout: React.FC<LayoutProps> = ({ children }) => {
|
||||
key: '/system-settings/api-health',
|
||||
icon: <CheckCircleOutlined />,
|
||||
label: t('menu.apiHealth') || 'API健康'
|
||||
},
|
||||
{
|
||||
key: '/system-settings/notification',
|
||||
icon: <NotificationOutlined />,
|
||||
label: t('menu.notifications') || '消息推送设置'
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -1167,7 +1167,42 @@
|
||||
"getChatIdsFailed": "Failed to get Chat IDs",
|
||||
"getChatIdsNoToken": "Please enter Bot Token first",
|
||||
"getChatIdsNoMessage": "Chat ID not found, please send a message to the bot first (e.g., /start), then retry",
|
||||
"getChatIdsButton": "Get Chat ID"
|
||||
"getChatIdsButton": "Get Chat ID",
|
||||
"botConfig": "Bot Configuration",
|
||||
"templateConfig": "Template Configuration",
|
||||
"templates": {
|
||||
"title": "Message Template Configuration",
|
||||
"templateType": "Template Type",
|
||||
"templateContent": "Template Content",
|
||||
"isDefault": "Default Template",
|
||||
"isCustom": "Custom Template",
|
||||
"resetToDefault": "Reset to Default",
|
||||
"resetConfirm": "Are you sure you want to reset to default? Your custom content will be lost.",
|
||||
"resetSuccess": "Reset successfully",
|
||||
"resetFailed": "Reset failed",
|
||||
"saveSuccess": "Saved successfully",
|
||||
"saveFailed": "Save failed",
|
||||
"testSuccess": "Test message sent successfully, please check Telegram",
|
||||
"testFailed": "Failed to send test message",
|
||||
"variables": "Available Variables",
|
||||
"clickToCopy": "Click to copy",
|
||||
"copied": "Copied",
|
||||
"commonVariables": "Common Variables",
|
||||
"orderVariables": "Order Variables",
|
||||
"copyTradingVariables": "Copy Trading Variables",
|
||||
"redeemVariables": "Redeem Variables",
|
||||
"errorVariables": "Error Variables",
|
||||
"filterVariables": "Filter Variables",
|
||||
"strategyVariables": "Strategy Variables"
|
||||
},
|
||||
"templateTypes": {
|
||||
"ORDER_SUCCESS": "Order Success",
|
||||
"ORDER_FAILED": "Order Failed",
|
||||
"ORDER_FILTERED": "Order Filtered",
|
||||
"CRYPTO_TAIL_SUCCESS": "Crypto Spread Strategy Success",
|
||||
"REDEEM_SUCCESS": "Position Redeem Success",
|
||||
"REDEEM_NO_RETURN": "Position Settled (No Return)"
|
||||
}
|
||||
},
|
||||
"telegramConfig": {
|
||||
"title": "Telegram Configuration Guide",
|
||||
|
||||
@@ -1167,7 +1167,42 @@
|
||||
"getChatIdsFailed": "获取 Chat IDs 失败",
|
||||
"getChatIdsNoToken": "请先填写 Bot Token",
|
||||
"getChatIdsNoMessage": "未找到 Chat ID,请先向机器人发送一条消息(如 /start),然后重试",
|
||||
"getChatIdsButton": "获取 Chat ID"
|
||||
"getChatIdsButton": "获取 Chat ID",
|
||||
"botConfig": "机器人配置",
|
||||
"templateConfig": "模板配置",
|
||||
"templates": {
|
||||
"title": "消息模板配置",
|
||||
"templateType": "模板类型",
|
||||
"templateContent": "模板内容",
|
||||
"isDefault": "默认模板",
|
||||
"isCustom": "自定义模板",
|
||||
"resetToDefault": "重置为默认",
|
||||
"resetConfirm": "确定要重置为默认模板吗?您的自定义内容将丢失。",
|
||||
"resetSuccess": "重置成功",
|
||||
"resetFailed": "重置失败",
|
||||
"saveSuccess": "保存成功",
|
||||
"saveFailed": "保存失败",
|
||||
"testSuccess": "测试消息发送成功,请检查 Telegram",
|
||||
"testFailed": "测试消息发送失败",
|
||||
"variables": "可用变量",
|
||||
"clickToCopy": "点击复制",
|
||||
"copied": "已复制",
|
||||
"commonVariables": "通用变量",
|
||||
"orderVariables": "订单变量",
|
||||
"copyTradingVariables": "跟单变量",
|
||||
"redeemVariables": "赎回变量",
|
||||
"errorVariables": "错误变量",
|
||||
"filterVariables": "过滤变量",
|
||||
"strategyVariables": "策略变量"
|
||||
},
|
||||
"templateTypes": {
|
||||
"ORDER_SUCCESS": "订单成功通知",
|
||||
"ORDER_FAILED": "订单失败通知",
|
||||
"ORDER_FILTERED": "订单过滤通知",
|
||||
"CRYPTO_TAIL_SUCCESS": "加密价差策略成功通知",
|
||||
"REDEEM_SUCCESS": "仓位赎回成功通知",
|
||||
"REDEEM_NO_RETURN": "仓位结算(无收益)通知"
|
||||
}
|
||||
},
|
||||
"telegramConfig": {
|
||||
"title": "Telegram 配置说明",
|
||||
|
||||
@@ -1167,7 +1167,42 @@
|
||||
"getChatIdsFailed": "獲取 Chat IDs 失敗",
|
||||
"getChatIdsNoToken": "請先填寫 Bot Token",
|
||||
"getChatIdsNoMessage": "未找到 Chat ID,請先向機器人發送一條消息(如 /start),然後重試",
|
||||
"getChatIdsButton": "獲取 Chat ID"
|
||||
"getChatIdsButton": "獲取 Chat ID",
|
||||
"botConfig": "機器人配置",
|
||||
"templateConfig": "模板配置",
|
||||
"templates": {
|
||||
"title": "消息模板配置",
|
||||
"templateType": "模板類型",
|
||||
"templateContent": "模板內容",
|
||||
"isDefault": "默認模板",
|
||||
"isCustom": "自定義模板",
|
||||
"resetToDefault": "重置為默認",
|
||||
"resetConfirm": "確定要重置為默認模板嗎?您的自定義內容將丟失。",
|
||||
"resetSuccess": "重置成功",
|
||||
"resetFailed": "重置失敗",
|
||||
"saveSuccess": "保存成功",
|
||||
"saveFailed": "保存失敗",
|
||||
"testSuccess": "測試消息發送成功,請檢查 Telegram",
|
||||
"testFailed": "測試消息發送失敗",
|
||||
"variables": "可用變量",
|
||||
"clickToCopy": "點擊複製",
|
||||
"copied": "已複製",
|
||||
"commonVariables": "通用變量",
|
||||
"orderVariables": "訂單變量",
|
||||
"copyTradingVariables": "跟單變量",
|
||||
"redeemVariables": "贖回變量",
|
||||
"errorVariables": "錯誤變量",
|
||||
"filterVariables": "過濾變量",
|
||||
"strategyVariables": "策略變量"
|
||||
},
|
||||
"templateTypes": {
|
||||
"ORDER_SUCCESS": "訂單成功通知",
|
||||
"ORDER_FAILED": "訂單失敗通知",
|
||||
"ORDER_FILTERED": "訂單過濾通知",
|
||||
"CRYPTO_TAIL_SUCCESS": "加密價差策略成功通知",
|
||||
"REDEEM_SUCCESS": "倉位贖回成功通知",
|
||||
"REDEEM_NO_RETURN": "倉位結算(無收益)通知"
|
||||
}
|
||||
},
|
||||
"telegramConfig": {
|
||||
"title": "Telegram 配置說明",
|
||||
|
||||
@@ -0,0 +1,708 @@
|
||||
import React, { useEffect, useState, useCallback } from 'react'
|
||||
import { Card, Table, Button, Space, Tag, Popconfirm, message, Typography, Modal, Form, Input, Switch, Tooltip, Row, Col, Menu } from 'antd'
|
||||
import { PlusOutlined, EditOutlined, DeleteOutlined, SendOutlined, CopyOutlined, ReloadOutlined, CheckOutlined, RobotOutlined, FormOutlined } from '@ant-design/icons'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { apiService } from '../services/api'
|
||||
import type { NotificationConfig, NotificationConfigRequest, NotificationConfigUpdateRequest, NotificationTemplate, TemplateTypeInfo, TemplateVariablesResponse, TemplateVariable } from '../types'
|
||||
import { useMediaQuery } from 'react-responsive'
|
||||
import { TelegramConfigForm } from '../components/notifications'
|
||||
import TextArea from 'antd/es/input/TextArea'
|
||||
|
||||
const { Title, Text, Paragraph } = Typography
|
||||
|
||||
const templateTypeMenuStyle: React.CSSProperties = {
|
||||
border: 'none',
|
||||
background: 'transparent',
|
||||
}
|
||||
|
||||
const variableChipStyle: React.CSSProperties = {
|
||||
display: 'inline-block',
|
||||
cursor: 'pointer',
|
||||
marginBottom: 8,
|
||||
marginRight: 8,
|
||||
borderRadius: 16,
|
||||
padding: '6px 12px',
|
||||
fontSize: 13,
|
||||
transition: 'all 0.2s',
|
||||
border: '1px solid #d9d9d9',
|
||||
background: '#fafafa',
|
||||
}
|
||||
|
||||
const variableChipHoverStyle: React.CSSProperties = {
|
||||
borderColor: '#1890ff',
|
||||
background: '#e6f7ff',
|
||||
color: '#1890ff',
|
||||
}
|
||||
|
||||
/**
|
||||
* 变量分类标签映射
|
||||
*/
|
||||
const CATEGORY_LABELS: Record<string, string> = {
|
||||
common: 'notificationSettings.templates.commonVariables',
|
||||
order: 'notificationSettings.templates.orderVariables',
|
||||
copy_trading: 'notificationSettings.templates.copyTradingVariables',
|
||||
redeem: 'notificationSettings.templates.redeemVariables',
|
||||
error: 'notificationSettings.templates.errorVariables',
|
||||
filter: 'notificationSettings.templates.filterVariables',
|
||||
strategy: 'notificationSettings.templates.strategyVariables'
|
||||
}
|
||||
|
||||
const NotificationSettingsPage: React.FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const isMobile = useMediaQuery({ maxWidth: 768 })
|
||||
|
||||
// 机器人配置相关状态
|
||||
const [configs, setConfigs] = useState<NotificationConfig[]>([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [modalVisible, setModalVisible] = useState(false)
|
||||
const [editingConfig, setEditingConfig] = useState<NotificationConfig | null>(null)
|
||||
const [form] = Form.useForm()
|
||||
const [testLoading, setTestLoading] = useState(false)
|
||||
|
||||
// 模板配置相关状态
|
||||
const [templateTypes, setTemplateTypes] = useState<TemplateTypeInfo[]>([])
|
||||
const [templates, setTemplates] = useState<NotificationTemplate[]>([])
|
||||
const [selectedTemplateType, setSelectedTemplateType] = useState<string>('ORDER_SUCCESS')
|
||||
const [currentTemplate, setCurrentTemplate] = useState<NotificationTemplate | null>(null)
|
||||
const [templateVariables, setTemplateVariables] = useState<TemplateVariablesResponse | null>(null)
|
||||
const [templateContent, setTemplateContent] = useState('')
|
||||
const [templateLoading, setTemplateLoading] = useState(false)
|
||||
const [testTemplateLoading, setTestTemplateLoading] = useState(false)
|
||||
|
||||
// 加载机器人配置
|
||||
useEffect(() => {
|
||||
fetchConfigs()
|
||||
}, [])
|
||||
|
||||
// 加载模板类型
|
||||
useEffect(() => {
|
||||
fetchTemplateTypes()
|
||||
}, [])
|
||||
|
||||
// 加载模板数据
|
||||
useEffect(() => {
|
||||
fetchTemplates()
|
||||
}, [])
|
||||
|
||||
// 当选中的模板类型改变时,加载模板详情和变量
|
||||
useEffect(() => {
|
||||
if (selectedTemplateType) {
|
||||
fetchTemplateDetail(selectedTemplateType)
|
||||
fetchTemplateVariables(selectedTemplateType)
|
||||
}
|
||||
}, [selectedTemplateType])
|
||||
|
||||
const fetchConfigs = async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const response = await apiService.notifications.list({ type: 'telegram' })
|
||||
if (response.data.code === 0 && response.data.data) {
|
||||
setConfigs(response.data.data)
|
||||
} else {
|
||||
message.error(response.data.msg || t('notificationSettings.fetchFailed'))
|
||||
}
|
||||
} catch (error: any) {
|
||||
message.error(error.message || t('notificationSettings.fetchFailed'))
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const fetchTemplateTypes = async () => {
|
||||
try {
|
||||
const response = await apiService.notifications.getTemplateTypes()
|
||||
if (response.data.code === 0 && response.data.data) {
|
||||
setTemplateTypes(response.data.data)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取模板类型失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const fetchTemplates = async () => {
|
||||
setTemplateLoading(true)
|
||||
try {
|
||||
const response = await apiService.notifications.getTemplates()
|
||||
if (response.data.code === 0 && response.data.data) {
|
||||
setTemplates(response.data.data)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取模板列表失败:', error)
|
||||
} finally {
|
||||
setTemplateLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const fetchTemplateDetail = async (templateType: string) => {
|
||||
try {
|
||||
const response = await apiService.notifications.getTemplateDetail({ templateType })
|
||||
if (response.data.code === 0 && response.data.data) {
|
||||
setCurrentTemplate(response.data.data)
|
||||
setTemplateContent(response.data.data.templateContent)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取模板详情失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const fetchTemplateVariables = async (templateType: string) => {
|
||||
try {
|
||||
const response = await apiService.notifications.getTemplateVariables({ templateType })
|
||||
if (response.data.code === 0 && response.data.data) {
|
||||
setTemplateVariables(response.data.data)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取模板变量失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 机器人配置相关方法
|
||||
const handleCreate = () => {
|
||||
setEditingConfig(null)
|
||||
form.resetFields()
|
||||
form.setFieldsValue({
|
||||
type: 'telegram',
|
||||
enabled: true,
|
||||
config: {
|
||||
botToken: '',
|
||||
chatIds: []
|
||||
}
|
||||
})
|
||||
setModalVisible(true)
|
||||
}
|
||||
|
||||
const handleEdit = (config: NotificationConfig) => {
|
||||
setEditingConfig(config)
|
||||
let botToken = ''
|
||||
let chatIds = ''
|
||||
|
||||
if (config.config) {
|
||||
if ('data' in config.config && config.config.data) {
|
||||
const data = config.config.data as any
|
||||
botToken = data.botToken || ''
|
||||
if (data.chatIds) {
|
||||
if (Array.isArray(data.chatIds)) {
|
||||
chatIds = data.chatIds.join(',')
|
||||
} else if (typeof data.chatIds === 'string') {
|
||||
chatIds = data.chatIds
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if ('botToken' in config.config) {
|
||||
botToken = (config.config as any).botToken || ''
|
||||
}
|
||||
if ('chatIds' in config.config) {
|
||||
const ids = (config.config as any).chatIds
|
||||
if (Array.isArray(ids)) {
|
||||
chatIds = ids.join(',')
|
||||
} else if (typeof ids === 'string') {
|
||||
chatIds = ids
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
form.setFieldsValue({
|
||||
type: config.type,
|
||||
name: config.name,
|
||||
enabled: config.enabled,
|
||||
config: {
|
||||
botToken: botToken,
|
||||
chatIds: chatIds
|
||||
}
|
||||
})
|
||||
setModalVisible(true)
|
||||
}
|
||||
|
||||
const handleDelete = async (id: number) => {
|
||||
try {
|
||||
const response = await apiService.notifications.delete({ id })
|
||||
if (response.data.code === 0) {
|
||||
message.success(t('notificationSettings.deleteSuccess'))
|
||||
fetchConfigs()
|
||||
} else {
|
||||
message.error(response.data.msg || t('notificationSettings.deleteFailed'))
|
||||
}
|
||||
} catch (error: any) {
|
||||
message.error(error.message || t('notificationSettings.deleteFailed'))
|
||||
}
|
||||
}
|
||||
|
||||
const handleUpdateEnabled = async (id: number, enabled: boolean) => {
|
||||
try {
|
||||
const response = await apiService.notifications.updateEnabled({ id, enabled })
|
||||
if (response.data.code === 0) {
|
||||
message.success(enabled ? t('notificationSettings.enableSuccess') : t('notificationSettings.disableSuccess'))
|
||||
fetchConfigs()
|
||||
} else {
|
||||
message.error(response.data.msg || t('notificationSettings.updateStatusFailed'))
|
||||
}
|
||||
} catch (error: any) {
|
||||
message.error(error.message || t('notificationSettings.updateStatusFailed'))
|
||||
}
|
||||
}
|
||||
|
||||
const handleTest = async () => {
|
||||
setTestLoading(true)
|
||||
try {
|
||||
const response = await apiService.notifications.test({ message: '这是一条测试消息' })
|
||||
if (response.data.code === 0 && response.data.data) {
|
||||
message.success(t('notificationSettings.testSuccess'))
|
||||
} else {
|
||||
message.error(response.data.msg || t('notificationSettings.testFailed'))
|
||||
}
|
||||
} catch (error: any) {
|
||||
message.error(error.message || t('notificationSettings.testFailed'))
|
||||
} finally {
|
||||
setTestLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
const values = await form.validateFields()
|
||||
const chatIds = typeof values.config.chatIds === 'string'
|
||||
? values.config.chatIds.split(',').map((id: string) => id.trim()).filter((id: string) => id)
|
||||
: values.config.chatIds || []
|
||||
|
||||
const configData: NotificationConfigRequest | NotificationConfigUpdateRequest = {
|
||||
type: values.type,
|
||||
name: values.name,
|
||||
enabled: values.enabled,
|
||||
config: {
|
||||
botToken: values.config.botToken,
|
||||
chatIds: chatIds
|
||||
}
|
||||
}
|
||||
|
||||
if (editingConfig?.id) {
|
||||
const updateData = {
|
||||
...configData,
|
||||
id: editingConfig.id
|
||||
} as NotificationConfigUpdateRequest
|
||||
const response = await apiService.notifications.update(updateData)
|
||||
if (response.data.code === 0) {
|
||||
message.success(t('notificationSettings.updateSuccess'))
|
||||
setModalVisible(false)
|
||||
fetchConfigs()
|
||||
} else {
|
||||
message.error(response.data.msg || t('notificationSettings.updateFailed'))
|
||||
}
|
||||
} else {
|
||||
const response = await apiService.notifications.create(configData)
|
||||
if (response.data.code === 0) {
|
||||
message.success(t('notificationSettings.createSuccess'))
|
||||
setModalVisible(false)
|
||||
fetchConfigs()
|
||||
} else {
|
||||
message.error(response.data.msg || t('notificationSettings.createFailed'))
|
||||
}
|
||||
}
|
||||
} catch (error: any) {
|
||||
if (error.errorFields) {
|
||||
return
|
||||
}
|
||||
message.error(error.message || t('message.error'))
|
||||
}
|
||||
}
|
||||
|
||||
const getConfigFormComponent = (type: string) => {
|
||||
switch (type?.toLowerCase()) {
|
||||
case 'telegram':
|
||||
return <TelegramConfigForm form={form} />
|
||||
default:
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
// 模板配置相关方法
|
||||
const handleTemplateTypeChange = (type: string) => {
|
||||
setSelectedTemplateType(type)
|
||||
}
|
||||
|
||||
const handleTemplateContentChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
setTemplateContent(e.target.value)
|
||||
}
|
||||
|
||||
const handleSaveTemplate = async () => {
|
||||
try {
|
||||
const response = await apiService.notifications.updateTemplate({
|
||||
templateType: selectedTemplateType,
|
||||
templateContent: templateContent
|
||||
})
|
||||
if (response.data.code === 0) {
|
||||
message.success(t('notificationSettings.templates.saveSuccess'))
|
||||
fetchTemplates()
|
||||
fetchTemplateDetail(selectedTemplateType)
|
||||
} else {
|
||||
message.error(response.data.msg || t('notificationSettings.templates.saveFailed'))
|
||||
}
|
||||
} catch (error: any) {
|
||||
message.error(error.message || t('notificationSettings.templates.saveFailed'))
|
||||
}
|
||||
}
|
||||
|
||||
const handleResetTemplate = async () => {
|
||||
try {
|
||||
const response = await apiService.notifications.resetTemplate({
|
||||
templateType: selectedTemplateType
|
||||
})
|
||||
if (response.data.code === 0) {
|
||||
message.success(t('notificationSettings.templates.resetSuccess'))
|
||||
fetchTemplates()
|
||||
fetchTemplateDetail(selectedTemplateType)
|
||||
} else {
|
||||
message.error(response.data.msg || t('notificationSettings.templates.resetFailed'))
|
||||
}
|
||||
} catch (error: any) {
|
||||
message.error(error.message || t('notificationSettings.templates.resetFailed'))
|
||||
}
|
||||
}
|
||||
|
||||
const handleTestTemplate = async () => {
|
||||
setTestTemplateLoading(true)
|
||||
try {
|
||||
const response = await apiService.notifications.testTemplate({
|
||||
templateType: selectedTemplateType,
|
||||
templateContent: templateContent
|
||||
})
|
||||
if (response.data.code === 0 && response.data.data) {
|
||||
message.success(t('notificationSettings.templates.testSuccess'))
|
||||
} else {
|
||||
message.error(response.data.msg || t('notificationSettings.templates.testFailed'))
|
||||
}
|
||||
} catch (error: any) {
|
||||
message.error(error.message || t('notificationSettings.templates.testFailed'))
|
||||
} finally {
|
||||
setTestTemplateLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleCopyVariable = useCallback((variable: string) => {
|
||||
navigator.clipboard.writeText(`{{${variable}}}`)
|
||||
message.success(t('notificationSettings.templates.copied'))
|
||||
}, [t])
|
||||
|
||||
const [variableHoverKey, setVariableHoverKey] = useState<string | null>(null)
|
||||
|
||||
const renderVariableItem = (variable: TemplateVariable) => {
|
||||
const isHover = variableHoverKey === variable.key
|
||||
return (
|
||||
<Tooltip key={variable.key} title={variable.description || `{{${variable.key}}}`}>
|
||||
<span
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
style={{ ...variableChipStyle, ...(isHover ? variableChipHoverStyle : {}) }}
|
||||
onClick={() => handleCopyVariable(variable.key)}
|
||||
onMouseEnter={() => setVariableHoverKey(variable.key)}
|
||||
onMouseLeave={() => setVariableHoverKey(null)}
|
||||
onKeyDown={(e) => e.key === 'Enter' && handleCopyVariable(variable.key)}
|
||||
>
|
||||
<CopyOutlined style={{ marginRight: 6, fontSize: 12 }} />
|
||||
{variable.label}
|
||||
</span>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
const renderVariablesPanel = () => {
|
||||
if (!templateVariables) return null
|
||||
|
||||
return (
|
||||
<Card
|
||||
size="small"
|
||||
title={
|
||||
<span style={{ fontSize: 14, fontWeight: 600 }}>
|
||||
{t('notificationSettings.templates.variables')}
|
||||
</span>
|
||||
}
|
||||
style={{ height: '100%', borderRadius: 8 }}
|
||||
bodyStyle={{ paddingTop: 12 }}
|
||||
>
|
||||
{templateVariables.categories.map(category => {
|
||||
const categoryVariables = templateVariables.variables.filter(v => v.category === category.key)
|
||||
if (categoryVariables.length === 0) return null
|
||||
return (
|
||||
<div key={category.key} style={{ marginBottom: 20 }}>
|
||||
<Text strong style={{ marginBottom: 10, display: 'block', fontSize: 13, color: 'rgba(0,0,0,0.65)' }}>
|
||||
{t(CATEGORY_LABELS[category.key] || category.label)}
|
||||
</Text>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap' }}>
|
||||
{categoryVariables.sort((a, b) => a.sortOrder - b.sortOrder).map(renderVariableItem)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
<Paragraph type="secondary" style={{ marginTop: 16, marginBottom: 0, fontSize: 12 }}>
|
||||
{t('notificationSettings.templates.clickToCopy')}
|
||||
</Paragraph>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
// 机器人配置表格列
|
||||
const configColumns = [
|
||||
{
|
||||
title: t('notificationSettings.configName'),
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
},
|
||||
{
|
||||
title: t('notificationSettings.type'),
|
||||
dataIndex: 'type',
|
||||
key: 'type',
|
||||
render: (type: string) => <Tag color="blue">{type.toUpperCase()}</Tag>
|
||||
},
|
||||
{
|
||||
title: t('notificationSettings.status'),
|
||||
dataIndex: 'enabled',
|
||||
key: 'enabled',
|
||||
render: (enabled: boolean) => (
|
||||
<Tag color={enabled ? 'green' : 'default'}>
|
||||
{enabled ? t('notificationSettings.enabledStatus') : t('notificationSettings.disabledStatus')}
|
||||
</Tag>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: t('notificationSettings.chatIds'),
|
||||
key: 'chatIds',
|
||||
render: (_: any, record: NotificationConfig) => {
|
||||
let chatIds: string[] = []
|
||||
if (record.config) {
|
||||
if ('data' in record.config && record.config.data) {
|
||||
const data = (record.config as any).data
|
||||
if (data.chatIds) {
|
||||
if (Array.isArray(data.chatIds)) {
|
||||
chatIds = data.chatIds.filter((id: any) => id && String(id).trim())
|
||||
} else if (typeof data.chatIds === 'string') {
|
||||
chatIds = data.chatIds.split(',').map((id: string) => id.trim()).filter((id: string) => id)
|
||||
}
|
||||
}
|
||||
} else if ('chatIds' in record.config) {
|
||||
const ids = (record.config as any).chatIds
|
||||
if (Array.isArray(ids)) {
|
||||
chatIds = ids.filter((id: any) => id && String(id).trim())
|
||||
} else if (typeof ids === 'string') {
|
||||
chatIds = (ids as string).split(',').map((id: string) => id.trim()).filter((id: string) => id)
|
||||
}
|
||||
}
|
||||
}
|
||||
return chatIds.length > 0 ? (
|
||||
<Text type="secondary" style={{ fontSize: '12px' }}>
|
||||
{chatIds.join(', ')}
|
||||
</Text>
|
||||
) : (
|
||||
<Text type="danger" style={{ fontSize: '12px' }}>{t('notificationSettings.chatIdsNotConfigured')}</Text>
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
title: t('common.actions'),
|
||||
key: 'action',
|
||||
width: isMobile ? 120 : 200,
|
||||
render: (_: any, record: NotificationConfig) => (
|
||||
<Space size="small" wrap>
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
icon={<EditOutlined />}
|
||||
onClick={() => handleEdit(record)}
|
||||
>
|
||||
{t('notificationSettings.edit')}
|
||||
</Button>
|
||||
<Switch
|
||||
checked={record.enabled}
|
||||
size="small"
|
||||
onChange={(checked) => handleUpdateEnabled(record.id!, checked)}
|
||||
/>
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
icon={<SendOutlined />}
|
||||
loading={testLoading}
|
||||
onClick={handleTest}
|
||||
>
|
||||
{t('notificationSettings.test')}
|
||||
</Button>
|
||||
<Popconfirm
|
||||
title={t('notificationSettings.deleteConfirm')}
|
||||
onConfirm={() => handleDelete(record.id!)}
|
||||
okText={t('common.confirm')}
|
||||
cancelText={t('common.cancel')}
|
||||
>
|
||||
<Button
|
||||
type="link"
|
||||
danger
|
||||
size="small"
|
||||
icon={<DeleteOutlined />}
|
||||
>
|
||||
{t('notificationSettings.delete')}
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
)
|
||||
}
|
||||
]
|
||||
|
||||
const templateTypeMenuItems = templateTypes.map(type => ({
|
||||
key: type.type,
|
||||
icon: <FormOutlined />,
|
||||
label: (
|
||||
<div>
|
||||
<div style={{ fontWeight: 500 }}>{t(`notificationSettings.templateTypes.${type.type}`)}</div>
|
||||
<div style={{ fontSize: 12, color: 'rgba(0,0,0,0.45)', marginTop: 2 }}>{type.description}</div>
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div style={{ marginBottom: '16px' }}>
|
||||
<Title level={2} style={{ margin: 0 }}>{t('notificationSettings.title')}</Title>
|
||||
</div>
|
||||
|
||||
{/* 机器人配置 */}
|
||||
<Card
|
||||
title={
|
||||
<Space>
|
||||
<RobotOutlined />
|
||||
<span>{t('notificationSettings.botConfig')}</span>
|
||||
</Space>
|
||||
}
|
||||
style={{ marginBottom: '16px' }}
|
||||
extra={
|
||||
<Button type="primary" icon={<PlusOutlined />} onClick={handleCreate}>
|
||||
{t('notificationSettings.addConfig')}
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<Table
|
||||
columns={configColumns}
|
||||
dataSource={configs}
|
||||
loading={loading}
|
||||
rowKey="id"
|
||||
pagination={false}
|
||||
scroll={{ x: isMobile ? 600 : 'auto' }}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
{/* 模板配置 */}
|
||||
<Card
|
||||
title={
|
||||
<Space>
|
||||
<FormOutlined />
|
||||
<span>{t('notificationSettings.templateConfig')}</span>
|
||||
</Space>
|
||||
}
|
||||
loading={templateLoading}
|
||||
style={{ marginBottom: '16px' }}
|
||||
>
|
||||
<Row gutter={[20, 20]}>
|
||||
<Col xs={24} sm={24} md={6}>
|
||||
<div style={{ marginBottom: 8 }}>
|
||||
<Text strong style={{ display: 'block', marginBottom: 12, fontSize: 14 }}>
|
||||
{t('notificationSettings.templates.templateType')}
|
||||
</Text>
|
||||
<Menu
|
||||
mode="inline"
|
||||
selectedKeys={[selectedTemplateType]}
|
||||
style={{ ...templateTypeMenuStyle, minHeight: 320 }}
|
||||
items={templateTypeMenuItems}
|
||||
onClick={({ key }) => handleTemplateTypeChange(key)}
|
||||
/>
|
||||
</div>
|
||||
</Col>
|
||||
<Col xs={24} sm={24} md={10}>
|
||||
<Card size="small" bordered style={{ marginBottom: 12 }}>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 8, alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<Space wrap size="small">
|
||||
<Text strong style={{ fontSize: 14 }}>{t('notificationSettings.templates.templateContent')}</Text>
|
||||
{currentTemplate && (
|
||||
<Tag color={currentTemplate.isDefault ? 'green' : 'blue'}>
|
||||
{currentTemplate.isDefault ? t('notificationSettings.templates.isDefault') : t('notificationSettings.templates.isCustom')}
|
||||
</Tag>
|
||||
)}
|
||||
</Space>
|
||||
<Space wrap size="small">
|
||||
<Popconfirm
|
||||
title={t('notificationSettings.templates.resetConfirm')}
|
||||
onConfirm={handleResetTemplate}
|
||||
okText={t('common.confirm')}
|
||||
cancelText={t('common.cancel')}
|
||||
>
|
||||
<Button size={isMobile ? 'small' : 'middle'} icon={<ReloadOutlined />}>
|
||||
{t('notificationSettings.templates.resetToDefault')}
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
<Button size={isMobile ? 'small' : 'middle'} type="primary" icon={<CheckOutlined />} onClick={handleSaveTemplate}>
|
||||
{t('common.save')}
|
||||
</Button>
|
||||
<Button size={isMobile ? 'small' : 'middle'} icon={<SendOutlined />} loading={testTemplateLoading} onClick={handleTestTemplate}>
|
||||
{t('notificationSettings.test')}
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
</Card>
|
||||
<TextArea
|
||||
value={templateContent}
|
||||
onChange={handleTemplateContentChange}
|
||||
rows={14}
|
||||
style={{ fontFamily: 'monospace', fontSize: 13 }}
|
||||
/>
|
||||
</Col>
|
||||
<Col xs={24} sm={24} md={8}>
|
||||
{renderVariablesPanel()}
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
|
||||
<Modal
|
||||
title={editingConfig ? t('notificationSettings.editConfig') : t('notificationSettings.addConfig')}
|
||||
open={modalVisible}
|
||||
onOk={handleSubmit}
|
||||
onCancel={() => setModalVisible(false)}
|
||||
width={isMobile ? '90%' : 600}
|
||||
okText={t('common.confirm')}
|
||||
cancelText={t('common.cancel')}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
>
|
||||
<Form.Item
|
||||
name="type"
|
||||
label={t('notificationSettings.type')}
|
||||
rules={[{ required: true, message: t('notificationSettings.typeRequired') }]}
|
||||
>
|
||||
<Input disabled value="telegram" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="name"
|
||||
label={t('notificationSettings.configName')}
|
||||
rules={[{ required: true, message: t('notificationSettings.configNameRequired') }]}
|
||||
>
|
||||
<Input placeholder={t('notificationSettings.configNamePlaceholder')} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="enabled"
|
||||
label={t('notificationSettings.enabled')}
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item shouldUpdate={(prevValues, currentValues) => {
|
||||
return prevValues.type !== currentValues.type ||
|
||||
prevValues.config !== currentValues.config
|
||||
}}>
|
||||
{() => {
|
||||
const currentType = form.getFieldValue('type') || 'telegram'
|
||||
return getConfigFormComponent(currentType)
|
||||
}}
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default NotificationSettingsPage
|
||||
@@ -1,11 +1,11 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { Card, Form, Button, Switch, Input, InputNumber, message, Typography, Space, Alert, Select, Table, Tag, Popconfirm, Modal } from 'antd'
|
||||
import { SaveOutlined, CheckCircleOutlined, ReloadOutlined, GlobalOutlined, NotificationOutlined, KeyOutlined, LinkOutlined, PlusOutlined, EditOutlined, DeleteOutlined, SendOutlined } from '@ant-design/icons'
|
||||
import { SaveOutlined, CheckCircleOutlined, ReloadOutlined, GlobalOutlined, NotificationOutlined, KeyOutlined, LinkOutlined, PlusOutlined, EditOutlined, DeleteOutlined, SendOutlined, RightOutlined } from '@ant-design/icons'
|
||||
import { apiService } from '../services/api'
|
||||
import { useMediaQuery } from 'react-responsive'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import type { SystemConfig, BuilderApiKeyUpdateRequest, NotificationConfig, NotificationConfigRequest, NotificationConfigUpdateRequest } from '../types'
|
||||
import { TelegramConfigForm } from '../components/notifications'
|
||||
import type { SystemConfig, BuilderApiKeyUpdateRequest } from '../types'
|
||||
import SystemUpdate from './SystemUpdate'
|
||||
|
||||
const { Title, Text, Paragraph } = Typography
|
||||
@@ -33,20 +33,13 @@ interface ProxyCheckResponse {
|
||||
const SystemSettings: React.FC = () => {
|
||||
const { t, i18n: i18nInstance } = useTranslation()
|
||||
const isMobile = useMediaQuery({ maxWidth: 768 })
|
||||
const navigate = useNavigate()
|
||||
|
||||
// 第一部分:多语言
|
||||
const [languageForm] = Form.useForm()
|
||||
const [currentLang, setCurrentLang] = useState<string>('auto')
|
||||
|
||||
// 第二部分:消息推送设置
|
||||
const [notificationConfigs, setNotificationConfigs] = useState<NotificationConfig[]>([])
|
||||
const [notificationLoading, setNotificationLoading] = useState(false)
|
||||
const [notificationModalVisible, setNotificationModalVisible] = useState(false)
|
||||
const [editingNotificationConfig, setEditingNotificationConfig] = useState<NotificationConfig | null>(null)
|
||||
const [notificationForm] = Form.useForm()
|
||||
const [testLoading, setTestLoading] = useState(false)
|
||||
|
||||
// 第三部分:Relayer配置
|
||||
// 第二部分:Relayer配置
|
||||
const [relayerForm] = Form.useForm()
|
||||
const [autoRedeemForm] = Form.useForm()
|
||||
const [systemConfig, setSystemConfig] = useState<SystemConfig | null>(null)
|
||||
@@ -67,7 +60,6 @@ const SystemSettings: React.FC = () => {
|
||||
languageForm.setFieldsValue({ language: savedLanguage })
|
||||
|
||||
// 加载其他配置
|
||||
fetchNotificationConfigs()
|
||||
fetchSystemConfig()
|
||||
fetchProxyConfig()
|
||||
}, [])
|
||||
@@ -103,246 +95,7 @@ const SystemSettings: React.FC = () => {
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 第二部分:消息推送设置 ====================
|
||||
const fetchNotificationConfigs = async () => {
|
||||
setNotificationLoading(true)
|
||||
try {
|
||||
const response = await apiService.notifications.list({ type: 'telegram' })
|
||||
if (response.data.code === 0 && response.data.data) {
|
||||
setNotificationConfigs(response.data.data)
|
||||
} else {
|
||||
message.error(response.data.msg || t('notificationSettings.fetchFailed'))
|
||||
}
|
||||
} catch (error: any) {
|
||||
message.error(error.message || t('notificationSettings.fetchFailed'))
|
||||
} finally {
|
||||
setNotificationLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleNotificationCreate = () => {
|
||||
setEditingNotificationConfig(null)
|
||||
notificationForm.resetFields()
|
||||
notificationForm.setFieldsValue({
|
||||
type: 'telegram',
|
||||
enabled: true,
|
||||
config: {
|
||||
botToken: '',
|
||||
chatIds: []
|
||||
}
|
||||
})
|
||||
setNotificationModalVisible(true)
|
||||
}
|
||||
|
||||
const handleNotificationEdit = (config: NotificationConfig) => {
|
||||
setEditingNotificationConfig(config)
|
||||
|
||||
let botToken = ''
|
||||
let chatIds = ''
|
||||
|
||||
if (config.config) {
|
||||
if ('data' in config.config && config.config.data) {
|
||||
const data = config.config.data as any
|
||||
botToken = data.botToken || ''
|
||||
if (data.chatIds) {
|
||||
if (Array.isArray(data.chatIds)) {
|
||||
chatIds = data.chatIds.join(',')
|
||||
} else if (typeof data.chatIds === 'string') {
|
||||
chatIds = data.chatIds
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if ('botToken' in config.config) {
|
||||
botToken = (config.config as any).botToken || ''
|
||||
}
|
||||
if ('chatIds' in config.config) {
|
||||
const ids = (config.config as any).chatIds
|
||||
if (Array.isArray(ids)) {
|
||||
chatIds = ids.join(',')
|
||||
} else if (typeof ids === 'string') {
|
||||
chatIds = ids
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
notificationForm.setFieldsValue({
|
||||
type: config.type,
|
||||
name: config.name,
|
||||
enabled: config.enabled,
|
||||
config: {
|
||||
botToken: botToken,
|
||||
chatIds: chatIds
|
||||
}
|
||||
})
|
||||
setNotificationModalVisible(true)
|
||||
}
|
||||
|
||||
const handleNotificationDelete = async (id: number) => {
|
||||
try {
|
||||
const response = await apiService.notifications.delete({ id })
|
||||
if (response.data.code === 0) {
|
||||
message.success(t('notificationSettings.deleteSuccess'))
|
||||
fetchNotificationConfigs()
|
||||
} else {
|
||||
message.error(response.data.msg || t('notificationSettings.deleteFailed'))
|
||||
}
|
||||
} catch (error: any) {
|
||||
message.error(error.message || t('notificationSettings.deleteFailed'))
|
||||
}
|
||||
}
|
||||
|
||||
const handleNotificationUpdateEnabled = async (id: number, enabled: boolean) => {
|
||||
try {
|
||||
const response = await apiService.notifications.updateEnabled({ id, enabled })
|
||||
if (response.data.code === 0) {
|
||||
message.success(enabled ? t('notificationSettings.enableSuccess') : t('notificationSettings.disableSuccess'))
|
||||
fetchNotificationConfigs()
|
||||
} else {
|
||||
message.error(response.data.msg || t('notificationSettings.updateStatusFailed'))
|
||||
}
|
||||
} catch (error: any) {
|
||||
message.error(error.message || t('notificationSettings.updateStatusFailed'))
|
||||
}
|
||||
}
|
||||
|
||||
const handleNotificationTest = async () => {
|
||||
setTestLoading(true)
|
||||
try {
|
||||
const response = await apiService.notifications.test({ message: '这是一条测试消息' })
|
||||
if (response.data.code === 0 && response.data.data) {
|
||||
message.success(t('notificationSettings.testSuccess'))
|
||||
} else {
|
||||
message.error(response.data.msg || t('notificationSettings.testFailed'))
|
||||
}
|
||||
} catch (error: any) {
|
||||
message.error(error.message || t('notificationSettings.testFailed'))
|
||||
} finally {
|
||||
setTestLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleNotificationSubmit = async () => {
|
||||
try {
|
||||
const values = await notificationForm.validateFields()
|
||||
|
||||
const chatIds = typeof values.config.chatIds === 'string'
|
||||
? values.config.chatIds.split(',').map((id: string) => id.trim()).filter((id: string) => id)
|
||||
: values.config.chatIds || []
|
||||
|
||||
const configData: NotificationConfigRequest | NotificationConfigUpdateRequest = {
|
||||
type: values.type,
|
||||
name: values.name,
|
||||
enabled: values.enabled,
|
||||
config: {
|
||||
botToken: values.config.botToken,
|
||||
chatIds: chatIds
|
||||
}
|
||||
}
|
||||
|
||||
if (editingNotificationConfig?.id) {
|
||||
const updateData = {
|
||||
...configData,
|
||||
id: editingNotificationConfig.id
|
||||
} as NotificationConfigUpdateRequest
|
||||
|
||||
const response = await apiService.notifications.update(updateData)
|
||||
if (response.data.code === 0) {
|
||||
message.success(t('notificationSettings.updateSuccess'))
|
||||
setNotificationModalVisible(false)
|
||||
fetchNotificationConfigs()
|
||||
} else {
|
||||
message.error(response.data.msg || t('notificationSettings.updateFailed'))
|
||||
}
|
||||
} else {
|
||||
const response = await apiService.notifications.create(configData)
|
||||
if (response.data.code === 0) {
|
||||
message.success(t('notificationSettings.createSuccess'))
|
||||
setNotificationModalVisible(false)
|
||||
fetchNotificationConfigs()
|
||||
} else {
|
||||
message.error(response.data.msg || t('notificationSettings.createFailed'))
|
||||
}
|
||||
}
|
||||
} catch (error: any) {
|
||||
if (error.errorFields) {
|
||||
return
|
||||
}
|
||||
message.error(error.message || t('message.error'))
|
||||
}
|
||||
}
|
||||
|
||||
const notificationColumns = [
|
||||
{
|
||||
title: t('notificationSettings.configName'),
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
},
|
||||
{
|
||||
title: t('notificationSettings.type'),
|
||||
dataIndex: 'type',
|
||||
key: 'type',
|
||||
render: (type: string) => <Tag color="blue">{type.toUpperCase()}</Tag>
|
||||
},
|
||||
{
|
||||
title: t('notificationSettings.status'),
|
||||
dataIndex: 'enabled',
|
||||
key: 'enabled',
|
||||
render: (enabled: boolean) => (
|
||||
<Tag color={enabled ? 'green' : 'default'}>
|
||||
{enabled ? t('notificationSettings.enabledStatus') : t('notificationSettings.disabledStatus')}
|
||||
</Tag>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: t('common.actions'),
|
||||
key: 'action',
|
||||
width: isMobile ? 120 : 200,
|
||||
render: (_: any, record: NotificationConfig) => (
|
||||
<Space size="small" wrap>
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
icon={<EditOutlined />}
|
||||
onClick={() => handleNotificationEdit(record)}
|
||||
>
|
||||
{t('notificationSettings.edit')}
|
||||
</Button>
|
||||
<Switch
|
||||
checked={record.enabled}
|
||||
size="small"
|
||||
onChange={(checked) => handleNotificationUpdateEnabled(record.id!, checked)}
|
||||
/>
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
icon={<SendOutlined />}
|
||||
loading={testLoading}
|
||||
onClick={handleNotificationTest}
|
||||
>
|
||||
{t('notificationSettings.test')}
|
||||
</Button>
|
||||
<Popconfirm
|
||||
title={t('notificationSettings.deleteConfirm')}
|
||||
onConfirm={() => handleNotificationDelete(record.id!)}
|
||||
okText={t('common.confirm')}
|
||||
cancelText={t('common.cancel')}
|
||||
>
|
||||
<Button
|
||||
type="link"
|
||||
danger
|
||||
size="small"
|
||||
icon={<DeleteOutlined />}
|
||||
>
|
||||
{t('notificationSettings.delete')}
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
)
|
||||
}
|
||||
]
|
||||
|
||||
// ==================== 第三部分:Relayer配置 ====================
|
||||
// ==================== 第二部分:Relayer配置 ====================
|
||||
const fetchSystemConfig = async () => {
|
||||
try {
|
||||
const response = await apiService.systemConfig.get()
|
||||
@@ -551,7 +304,7 @@ const SystemSettings: React.FC = () => {
|
||||
</Form>
|
||||
</Card>
|
||||
|
||||
{/* 第二部分:消息推送设置 */}
|
||||
{/* 第二部分:消息推送设置(独立页面入口) */}
|
||||
<Card
|
||||
title={
|
||||
<Space>
|
||||
@@ -563,73 +316,24 @@ const SystemSettings: React.FC = () => {
|
||||
extra={
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={handleNotificationCreate}
|
||||
icon={<RightOutlined />}
|
||||
onClick={() => navigate('/system-settings/notification')}
|
||||
>
|
||||
{t('notificationSettings.addConfig')}
|
||||
{t('notificationSettings.title')}
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<Table
|
||||
columns={notificationColumns}
|
||||
dataSource={notificationConfigs}
|
||||
loading={notificationLoading}
|
||||
rowKey="id"
|
||||
pagination={false}
|
||||
scroll={{ x: isMobile ? 600 : 'auto' }}
|
||||
/>
|
||||
|
||||
<Modal
|
||||
title={editingNotificationConfig ? t('notificationSettings.editConfig') : t('notificationSettings.addConfig')}
|
||||
open={notificationModalVisible}
|
||||
onOk={handleNotificationSubmit}
|
||||
onCancel={() => setNotificationModalVisible(false)}
|
||||
width={isMobile ? '90%' : 600}
|
||||
okText={t('common.confirm')}
|
||||
cancelText={t('common.cancel')}
|
||||
<Paragraph type="secondary" style={{ marginBottom: 16 }}>
|
||||
{t('notificationSettings.botConfig')}、{t('notificationSettings.templateConfig')}等请在独立页面中配置。
|
||||
</Paragraph>
|
||||
<Button
|
||||
type="link"
|
||||
icon={<RightOutlined />}
|
||||
onClick={() => navigate('/system-settings/notification')}
|
||||
style={{ padding: 0 }}
|
||||
>
|
||||
<Form
|
||||
form={notificationForm}
|
||||
layout="vertical"
|
||||
>
|
||||
<Form.Item
|
||||
name="type"
|
||||
label={t('notificationSettings.type')}
|
||||
rules={[{ required: true, message: t('notificationSettings.typeRequired') }]}
|
||||
>
|
||||
<Input disabled value="telegram" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="name"
|
||||
label={t('notificationSettings.configName')}
|
||||
rules={[{ required: true, message: t('notificationSettings.configNameRequired') }]}
|
||||
>
|
||||
<Input placeholder={t('notificationSettings.configNamePlaceholder')} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="enabled"
|
||||
label={t('notificationSettings.enabled')}
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item shouldUpdate={(prevValues, currentValues) => {
|
||||
return prevValues.type !== currentValues.type ||
|
||||
prevValues.config !== currentValues.config
|
||||
}}>
|
||||
{() => {
|
||||
const currentType = notificationForm.getFieldValue('type') || 'telegram'
|
||||
if (currentType === 'telegram') {
|
||||
return <TelegramConfigForm form={notificationForm} />
|
||||
}
|
||||
return null
|
||||
}}
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
{t('notificationSettings.title')} →
|
||||
</Button>
|
||||
</Card>
|
||||
|
||||
{/* 第三部分:Relayer配置 */}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import axios, { AxiosInstance, AxiosError } from 'axios'
|
||||
import type { ApiResponse, NotificationConfig, NotificationConfigRequest, NotificationConfigUpdateRequest } from '../types'
|
||||
import type { ApiResponse, NotificationConfig, NotificationConfigRequest, NotificationConfigUpdateRequest, NotificationTemplate, TemplateTypeInfo, TemplateVariablesResponse } from '../types'
|
||||
import { getToken, setToken, removeToken } from '../utils'
|
||||
import { wsManager } from './websocket'
|
||||
import i18n from '../i18n/config'
|
||||
@@ -686,7 +686,51 @@ export const apiService = {
|
||||
* 获取 Telegram Chat IDs
|
||||
*/
|
||||
getTelegramChatIds: (data: { botToken: string }) =>
|
||||
apiClient.post<ApiResponse<string[]>>('/system/notifications/telegram/get-chat-ids', data)
|
||||
apiClient.post<ApiResponse<string[]>>('/system/notifications/telegram/get-chat-ids', data),
|
||||
|
||||
// ==================== 模板相关 API ====================
|
||||
|
||||
/**
|
||||
* 获取所有模板类型
|
||||
*/
|
||||
getTemplateTypes: () =>
|
||||
apiClient.post<ApiResponse<TemplateTypeInfo[]>>('/system/notifications/templates/types', {}),
|
||||
|
||||
/**
|
||||
* 获取所有模板列表
|
||||
*/
|
||||
getTemplates: () =>
|
||||
apiClient.post<ApiResponse<NotificationTemplate[]>>('/system/notifications/templates/list', {}),
|
||||
|
||||
/**
|
||||
* 获取单个模板详情
|
||||
*/
|
||||
getTemplateDetail: (data: { templateType: string }) =>
|
||||
apiClient.post<ApiResponse<NotificationTemplate>>('/system/notifications/templates/detail', data),
|
||||
|
||||
/**
|
||||
* 获取模板可用变量
|
||||
*/
|
||||
getTemplateVariables: (data: { templateType: string }) =>
|
||||
apiClient.post<ApiResponse<TemplateVariablesResponse>>('/system/notifications/templates/variables', data),
|
||||
|
||||
/**
|
||||
* 更新模板
|
||||
*/
|
||||
updateTemplate: (data: { templateType: string; templateContent: string }) =>
|
||||
apiClient.post<ApiResponse<NotificationTemplate>>('/system/notifications/templates/update', data),
|
||||
|
||||
/**
|
||||
* 重置模板为默认
|
||||
*/
|
||||
resetTemplate: (data: { templateType: string }) =>
|
||||
apiClient.post<ApiResponse<NotificationTemplate>>('/system/notifications/templates/reset', data),
|
||||
|
||||
/**
|
||||
* 发送模板测试消息
|
||||
*/
|
||||
testTemplate: (data: { templateType: string; templateContent?: string }) =>
|
||||
apiClient.post<ApiResponse<boolean>>('/system/notifications/templates/test', data)
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
@@ -1258,3 +1258,56 @@ export interface ManualOrderDetails {
|
||||
/** 总金额 */
|
||||
totalAmount: string
|
||||
}
|
||||
|
||||
// ==================== 消息模板相关类型 ====================
|
||||
|
||||
/**
|
||||
* 消息模板
|
||||
*/
|
||||
export interface NotificationTemplate {
|
||||
id?: number
|
||||
templateType: string // 模板类型
|
||||
templateContent: string // 模板内容
|
||||
isDefault: boolean // 是否使用默认模板
|
||||
createdAt?: number
|
||||
updatedAt?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* 模板类型信息
|
||||
*/
|
||||
export interface TemplateTypeInfo {
|
||||
type: string // 模板类型
|
||||
name: string // 类型名称
|
||||
description: string // 类型描述
|
||||
}
|
||||
|
||||
/**
|
||||
* 模板变量
|
||||
*/
|
||||
export interface TemplateVariable {
|
||||
key: string // 变量名
|
||||
label: string // 显示名称
|
||||
description: string // 变量说明
|
||||
category: string // 分类
|
||||
sortOrder: number // 排序顺序
|
||||
}
|
||||
|
||||
/**
|
||||
* 模板变量分类
|
||||
*/
|
||||
export interface TemplateVariableCategory {
|
||||
key: string // 分类 key
|
||||
label: string // 分类名称
|
||||
sortOrder: number // 排序顺序
|
||||
}
|
||||
|
||||
/**
|
||||
* 模板变量列表响应
|
||||
*/
|
||||
export interface TemplateVariablesResponse {
|
||||
templateType: string // 模板类型
|
||||
templateTypeName: string // 模板类型名称
|
||||
categories: TemplateVariableCategory[] // 分类列表
|
||||
variables: TemplateVariable[] // 变量列表
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user