From 3cc37dac55c23f315f26a6b4b49b84cf54c7e535 Mon Sep 17 00:00:00 2001 From: WrBug Date: Sun, 1 Mar 2026 05:41:04 +0800 Subject: [PATCH] =?UTF-8?q?feat(backend):=20=E4=BC=98=E5=8C=96=E9=80=9A?= =?UTF-8?q?=E7=9F=A5=E6=8E=A8=E9=80=81=EF=BC=8C=E5=8C=BA=E5=88=86=E8=BE=93?= =?UTF-8?q?=E8=B5=A2=E5=B9=B6=E6=98=BE=E7=A4=BA=E5=8F=AF=E7=94=A8=E4=BD=99?= =?UTF-8?q?=E9=A2=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 赎回通知区分输赢:赢的仓位显示"赎回成功",输的仓位显示"已结算(无收益)" - 买入/卖出/赎回通知消息中添加钱包可用余额显示 - 新增多语言文案支持 Made-with: Cursor --- .../service/accounts/AccountService.kt | 57 +++++-- .../statistics/OrderStatusUpdateService.kt | 25 ++- .../system/TelegramNotificationService.kt | 154 +++++++++++++++++- .../resources/i18n/messages_en.properties | 8 + .../resources/i18n/messages_zh_CN.properties | 8 + .../resources/i18n/messages_zh_TW.properties | 8 + 6 files changed, 237 insertions(+), 23 deletions(-) diff --git a/backend/src/main/kotlin/com/wrbug/polymarketbot/service/accounts/AccountService.kt b/backend/src/main/kotlin/com/wrbug/polymarketbot/service/accounts/AccountService.kt index d0c8cb5..49c1baf 100644 --- a/backend/src/main/kotlin/com/wrbug/polymarketbot/service/accounts/AccountService.kt +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/service/accounts/AccountService.kt @@ -1331,6 +1331,14 @@ class AccountService( // 使用当前时间作为订单创建时间 val orderTime = System.currentTimeMillis() + + // 查询可用余额 + val availableBalance = try { + blockchainService.getUsdcBalance(account.walletAddress, account.proxyAddress).getOrNull() + } catch (e: Exception) { + logger.warn("查询可用余额失败: accountId=${account.id}, ${e.message}") + null + } telegramNotificationService?.sendOrderSuccessNotification( orderId = orderId, @@ -1348,7 +1356,8 @@ class AccountService( apiPassphrase = try { cryptoUtils.decrypt(account.apiPassphrase!!) } catch (e: Exception) { null }, walletAddressForApi = account.walletAddress, locale = locale, - orderTime = orderTime // 使用订单创建时间 + orderTime = orderTime, // 使用订单创建时间 + availableBalance = availableBalance ) } catch (e: Exception) { logger.warn("发送订单成功通知失败: ${e.message}", e) @@ -1800,16 +1809,42 @@ class AccountService( for (transaction in accountTransactions) { val account = accounts[transaction.accountId] if (account != null) { - telegramNotificationService?.sendRedeemNotification( - accountName = account.accountName, - walletAddress = account.walletAddress, - transactionHash = transaction.transactionHash, - totalRedeemedValue = transaction.positions.fold(BigDecimal.ZERO) { sum, info -> - sum.add(info.value.toSafeBigDecimal()) - }.toPlainString(), - positions = transaction.positions, - locale = locale - ) + // 查询可用余额 + val availableBalance = try { + blockchainService.getUsdcBalance(account.walletAddress, account.proxyAddress).getOrNull() + } catch (e: Exception) { + logger.warn("查询可用余额失败: accountId=${account.id}, ${e.message}") + null + } + + // 计算该账户的赎回总价值 + val accountTotalValue = transaction.positions.fold(BigDecimal.ZERO) { sum, info -> + sum.add(info.value.toSafeBigDecimal()) + } + + // 根据赎回价值选择不同的通知类型 + if (accountTotalValue.gt(BigDecimal.ZERO)) { + // 有收益:发送赎回成功通知 + telegramNotificationService?.sendRedeemNotification( + accountName = account.accountName, + walletAddress = account.walletAddress, + transactionHash = transaction.transactionHash, + totalRedeemedValue = accountTotalValue.toPlainString(), + positions = transaction.positions, + locale = locale, + availableBalance = availableBalance + ) + } else { + // 无收益(输的仓位):发送已结算无收益通知 + telegramNotificationService?.sendRedeemNoReturnNotification( + accountName = account.accountName, + walletAddress = account.walletAddress, + transactionHash = transaction.transactionHash, + positions = transaction.positions, + locale = locale, + availableBalance = availableBalance + ) + } } } } catch (e: Exception) { diff --git a/backend/src/main/kotlin/com/wrbug/polymarketbot/service/copytrading/statistics/OrderStatusUpdateService.kt b/backend/src/main/kotlin/com/wrbug/polymarketbot/service/copytrading/statistics/OrderStatusUpdateService.kt index 2f31d63..97f58db 100644 --- a/backend/src/main/kotlin/com/wrbug/polymarketbot/service/copytrading/statistics/OrderStatusUpdateService.kt +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/service/copytrading/statistics/OrderStatusUpdateService.kt @@ -38,7 +38,8 @@ class OrderStatusUpdateService( private val cryptoUtils: CryptoUtils, private val trackingService: CopyOrderTrackingService, private val marketService: MarketService, // 市场信息服务 - private val telegramNotificationService: TelegramNotificationService? + private val telegramNotificationService: TelegramNotificationService?, + private val blockchainService: com.wrbug.polymarketbot.service.common.BlockchainService ) : ApplicationContextAware { private val logger = LoggerFactory.getLogger(OrderStatusUpdateService::class.java) @@ -930,6 +931,14 @@ class OrderStatusUpdateService( null } + // 查询可用余额 + val availableBalance = try { + blockchainService.getUsdcBalance(finalAccount.walletAddress, finalAccount.proxyAddress).getOrNull() + } catch (e: Exception) { + logger.warn("查询可用余额失败: accountId=${finalAccount.id}, ${e.message}") + null + } + // 发送通知 telegramNotificationService.sendOrderSuccessNotification( orderId = order.buyOrderId, @@ -950,7 +959,8 @@ class OrderStatusUpdateService( locale = locale, leaderName = leaderName, configName = configName, - orderTime = orderCreatedAt // 使用订单创建时间 + orderTime = orderCreatedAt, // 使用订单创建时间 + availableBalance = availableBalance ) logger.info("买入订单通知已发送: orderId=${order.buyOrderId}, copyTradingId=${order.copyTradingId}") @@ -1023,6 +1033,14 @@ class OrderStatusUpdateService( null } + // 查询可用余额 + val availableBalance = try { + blockchainService.getUsdcBalance(finalAccount.walletAddress, finalAccount.proxyAddress).getOrNull() + } catch (e: Exception) { + logger.warn("查询可用余额失败: accountId=${finalAccount.id}, ${e.message}") + null + } + // 发送通知 telegramNotificationService.sendOrderSuccessNotification( orderId = record.sellOrderId, @@ -1043,7 +1061,8 @@ class OrderStatusUpdateService( locale = locale, leaderName = leaderName, configName = configName, - orderTime = orderCreatedAt // 使用订单创建时间 + orderTime = orderCreatedAt, // 使用订单创建时间 + availableBalance = availableBalance ) logger.info("卖出订单通知已发送: orderId=${record.sellOrderId}, copyTradingId=${record.copyTradingId}") diff --git a/backend/src/main/kotlin/com/wrbug/polymarketbot/service/system/TelegramNotificationService.kt b/backend/src/main/kotlin/com/wrbug/polymarketbot/service/system/TelegramNotificationService.kt index 49dba41..c53256d 100644 --- a/backend/src/main/kotlin/com/wrbug/polymarketbot/service/system/TelegramNotificationService.kt +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/service/system/TelegramNotificationService.kt @@ -98,7 +98,8 @@ class TelegramNotificationService( locale: java.util.Locale? = null, leaderName: String? = null, // Leader 名称(备注) configName: String? = null, // 跟单配置名 - orderTime: Long? = null // 订单创建时间(毫秒时间戳),用于通知中的时间显示 + orderTime: Long? = null, // 订单创建时间(毫秒时间戳),用于通知中的时间显示 + availableBalance: String? = null // 可用余额(可选) ) { // 1. 如果提供了 orderId,检查是否已发送过通知(去重) if (orderId != null) { @@ -192,7 +193,8 @@ class TelegramNotificationService( locale = currentLocale, leaderName = leaderName, configName = configName, - orderTime = orderTime + orderTime = orderTime, + availableBalance = availableBalance ) sendMessage(message) } @@ -766,7 +768,8 @@ class TelegramNotificationService( locale: java.util.Locale, leaderName: String? = null, // Leader 名称(备注) configName: String? = null, // 跟单配置名 - orderTime: Long? = null // 订单创建时间(毫秒时间戳) + orderTime: Long? = null, // 订单创建时间(毫秒时间戳) + availableBalance: String? = null // 可用余额 ): String { // 获取多语言文本 @@ -781,6 +784,7 @@ class TelegramNotificationService( val amountLabel = messageSource.getMessage("notification.order.amount", null, "金额", locale) val accountLabel = messageSource.getMessage("notification.order.account", null, "账户", locale) val timeLabel = messageSource.getMessage("notification.order.time", null, "时间", locale) + val availableBalanceLabel = messageSource.getMessage("notification.order.available_balance", null, "可用余额", locale) val unknown = messageSource.getMessage("common.unknown", null, "未知", locale) val unknownAccount: String = messageSource.getMessage("notification.order.unknown_account", null, "未知账户", locale) ?: "未知账户" val calculateFailed = messageSource.getMessage("notification.order.calculate_failed", null, "计算失败", locale) @@ -879,6 +883,23 @@ class TelegramNotificationService( val priceDisplay = formatPrice(price) val sizeDisplay = formatQuantity(size) + // 格式化可用余额 + 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() + } + "\n• $availableBalanceLabel: ${formatted.toPlainString()} USDC" + } catch (e: Exception) { + "\n• $availableBalanceLabel: $availableBalance USDC" + } + } else { + "" + } + return """$icon $orderCreatedSuccess 📊 $orderInfo: @@ -888,7 +909,7 @@ class TelegramNotificationService( • $priceLabel: $priceDisplay • $quantityLabel: $sizeDisplay shares • $amountLabel: $amountDisplay USDC -• $accountLabel: $escapedAccountInfo$escapedCopyTradingInfo +• $accountLabel: $escapedAccountInfo$escapedCopyTradingInfo$availableBalanceDisplay ⏰ $timeLabel: $time""" } @@ -1095,6 +1116,7 @@ class TelegramNotificationService( /** * 发送仓位赎回通知 * @param locale 语言设置(可选,如果提供则使用,否则使用 LocaleContextHolder 获取) + * @param availableBalance 可用余额(可选) */ suspend fun sendRedeemNotification( accountName: String?, @@ -1102,7 +1124,8 @@ class TelegramNotificationService( transactionHash: String, totalRedeemedValue: String, positions: List, - locale: java.util.Locale? = null + locale: java.util.Locale? = null, + availableBalance: String? = null ) { // 获取语言设置(优先使用传入的 locale,否则从 LocaleContextHolder 获取) val currentLocale = locale ?: try { @@ -1118,7 +1141,8 @@ class TelegramNotificationService( transactionHash = transactionHash, totalRedeemedValue = totalRedeemedValue, positions = positions, - locale = currentLocale + locale = currentLocale, + availableBalance = availableBalance ) sendMessage(message) } @@ -1132,7 +1156,8 @@ class TelegramNotificationService( transactionHash: String, totalRedeemedValue: String, positions: List, - locale: java.util.Locale + locale: java.util.Locale, + availableBalance: String? = null ): String { // 获取多语言文本 val redeemSuccess = messageSource.getMessage("notification.redeem.success", null, "仓位赎回成功", locale) @@ -1145,6 +1170,7 @@ class TelegramNotificationService( val quantityLabel = messageSource.getMessage("notification.order.quantity", null, "数量", locale) val valueLabel = messageSource.getMessage("notification.order.amount", null, "金额", locale) val timeLabel = messageSource.getMessage("notification.order.time", null, "时间", locale) + val availableBalanceLabel = messageSource.getMessage("notification.redeem.available_balance", null, "可用余额", locale) val unknownAccount: String = messageSource.getMessage("notification.order.unknown_account", null, "未知账户", locale) ?: "未知账户" // 构建账户信息(格式:账户名(钱包地址)) @@ -1186,19 +1212,129 @@ class TelegramNotificationService( " • ${position.marketId.substring(0, 8)}... (${position.side}): $quantityDisplay shares = $valueDisplay USDC" } + // 格式化可用余额 + 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() + } + "\n• $availableBalanceLabel: ${formatted.toPlainString()} USDC" + } catch (e: Exception) { + "\n• $availableBalanceLabel: $availableBalance USDC" + } + } else { + "" + } + return """💸 $redeemSuccess 📊 $redeemInfo: • $accountLabel: $escapedAccountInfo • $transactionHashLabel: $escapedTxHash -• $totalValueLabel: $totalValueDisplay USDC +• $totalValueLabel: $totalValueDisplay USDC$availableBalanceDisplay 📦 $positionsLabel: $positionsText ⏰ $timeLabel: $time""" } - + + /** + * 发送仓位已结算(无收益)通知 + * 用于输的仓位,赎回价值为 0 的情况 + */ + suspend fun sendRedeemNoReturnNotification( + accountName: String?, + walletAddress: String?, + transactionHash: String, + positions: List, + locale: java.util.Locale? = null, + availableBalance: String? = null + ) { + val currentLocale = locale ?: try { + LocaleContextHolder.getLocale() + } catch (e: Exception) { + logger.warn("获取语言设置失败,使用默认语言: ${e.message}", e) + java.util.Locale("zh", "CN") + } + + val message = buildRedeemNoReturnMessage( + accountName = accountName, + walletAddress = walletAddress, + transactionHash = transactionHash, + positions = positions, + locale = currentLocale, + availableBalance = availableBalance + ) + sendMessage(message) + } + + /** + * 构建仓位已结算(无收益)消息 + */ + private fun buildRedeemNoReturnMessage( + accountName: String?, + walletAddress: String?, + transactionHash: String, + positions: List, + locale: java.util.Locale, + availableBalance: String? = null + ): String { + val noReturnTitle = messageSource.getMessage("notification.redeem.no_return.title", null, "仓位已结算(无收益)", locale) + val noReturnInfo = messageSource.getMessage("notification.redeem.no_return.info", null, "结算信息", locale) + val noReturnMessage = messageSource.getMessage("notification.redeem.no_return.message", null, "市场已结算,您的预测未命中,赎回价值为 0。", locale) + val accountLabel = messageSource.getMessage("notification.order.account", null, "账户", locale) + val transactionHashLabel = messageSource.getMessage("notification.redeem.transaction_hash", null, "交易哈希", locale) + val positionsLabel = messageSource.getMessage("notification.redeem.no_return.positions", null, "结算仓位", locale) + val timeLabel = messageSource.getMessage("notification.order.time", null, "时间", locale) + val availableBalanceLabel = messageSource.getMessage("notification.redeem.available_balance", null, "可用余额", locale) + val unknownAccount: String = messageSource.getMessage("notification.order.unknown_account", null, "未知账户", locale) ?: "未知账户" + + val accountInfo = buildAccountInfo(accountName, walletAddress, unknownAccount) + val time = DateUtils.formatDateTime() + + val escapedAccountInfo = accountInfo.replace("<", "<").replace(">", ">") + val escapedTxHash = transactionHash.replace("<", "<").replace(">", ">") + + val positionsText = positions.joinToString("\n") { position -> + val quantityDisplay = formatQuantity(position.quantity) + " • ${position.marketId.substring(0, 8)}... (${position.side}): $quantityDisplay shares" + } + + // 格式化可用余额 + 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() + } + "\n• $availableBalanceLabel: ${formatted.toPlainString()} USDC" + } catch (e: Exception) { + "\n• $availableBalanceLabel: $availableBalance USDC" + } + } else { + "" + } + + return """📋 $noReturnTitle + +📊 $noReturnInfo: +$noReturnMessage + +• $accountLabel: $escapedAccountInfo +• $transactionHashLabel: $escapedTxHash$availableBalanceDisplay + +📦 $positionsLabel: +$positionsText + +⏰ $timeLabel: $time""" + } + /** * 脱敏显示地址(只显示前6位和后4位) */ diff --git a/backend/src/main/resources/i18n/messages_en.properties b/backend/src/main/resources/i18n/messages_en.properties index 3cebc68..960115e 100644 --- a/backend/src/main/resources/i18n/messages_en.properties +++ b/backend/src/main/resources/i18n/messages_en.properties @@ -13,6 +13,7 @@ notification.order.quantity=Quantity notification.order.amount=Amount notification.order.account=Account notification.order.time=Time +notification.order.available_balance=Available Balance notification.order.error_info=Error Information notification.order.unknown_account=Unknown Account notification.order.calculate_failed=Calculation Failed @@ -26,6 +27,13 @@ notification.redeem.position_count=Position Count notification.redeem.positions=Redeemed Positions notification.redeem.account=Account notification.redeem.time=Time +notification.redeem.available_balance=Available Balance + +# Position Settled (No Return) +notification.redeem.no_return.title=Position Settled (No Return) +notification.redeem.no_return.info=Settlement Information +notification.redeem.no_return.message=Market settled. Your prediction was incorrect. Redemption value is 0. +notification.redeem.no_return.positions=Settled Positions # Auto Redeem related notifications notification.auto_redeem.disabled.title=Auto Redeem Disabled diff --git a/backend/src/main/resources/i18n/messages_zh_CN.properties b/backend/src/main/resources/i18n/messages_zh_CN.properties index 8d9d961..7fc04f6 100644 --- a/backend/src/main/resources/i18n/messages_zh_CN.properties +++ b/backend/src/main/resources/i18n/messages_zh_CN.properties @@ -13,6 +13,7 @@ notification.order.quantity=数量 notification.order.amount=金额 notification.order.account=账户 notification.order.time=时间 +notification.order.available_balance=可用余额 notification.order.error_info=错误信息 notification.order.unknown_account=未知账户 notification.order.calculate_failed=计算失败 @@ -26,6 +27,13 @@ notification.redeem.position_count=仓位数量 notification.redeem.positions=赎回仓位 notification.redeem.account=账户 notification.redeem.time=时间 +notification.redeem.available_balance=可用余额 + +# 仓位已结算(无收益) +notification.redeem.no_return.title=仓位已结算(无收益) +notification.redeem.no_return.info=结算信息 +notification.redeem.no_return.message=市场已结算,您的预测未命中,赎回价值为 0。 +notification.redeem.no_return.positions=结算仓位 # 自动赎回相关通知 notification.auto_redeem.disabled.title=自动赎回未开启 diff --git a/backend/src/main/resources/i18n/messages_zh_TW.properties b/backend/src/main/resources/i18n/messages_zh_TW.properties index 5d06e6c..a71d80c 100644 --- a/backend/src/main/resources/i18n/messages_zh_TW.properties +++ b/backend/src/main/resources/i18n/messages_zh_TW.properties @@ -13,6 +13,7 @@ notification.order.quantity=數量 notification.order.amount=金額 notification.order.account=賬戶 notification.order.time=時間 +notification.order.available_balance=可用餘額 notification.order.error_info=錯誤信息 notification.order.unknown_account=未知賬戶 notification.order.calculate_failed=計算失敗 @@ -26,6 +27,13 @@ notification.redeem.position_count=倉位數量 notification.redeem.positions=贖回倉位 notification.redeem.account=賬戶 notification.redeem.time=時間 +notification.redeem.available_balance=可用餘額 + +# 倉位已結算(無收益) +notification.redeem.no_return.title=倉位已結算(無收益) +notification.redeem.no_return.info=結算信息 +notification.redeem.no_return.message=市場已結算,您的預測未命中,贖回價值為 0。 +notification.redeem.no_return.positions=結算倉位 # 自動贖回相關通知 notification.auto_redeem.disabled.title=自動贖回未開啟