refactor: 重构代码结构,按业务分类组织 Controller 和 Service

- 重构 API 路径为 /api/业务/xxx 格式
- 将 Controller 和 Service 按业务分类组织:
  * copytrading: 跟单相关(configs, leaders, statistics, templates)
  * accounts: 账户管理(公共模块)
  * markets: 市场数据(公共模块)
  * system: 系统管理(users, notifications, proxy, health)
  * auth: 认证
  * announcement: 公告
- 将所有完整类名引用改为使用 import 导入
- 修复 calculatePositionValue 方法实现
- 更新前端 API 调用路径
This commit is contained in:
WrBug
2025-12-08 23:04:40 +08:00
parent ac4e1dde2b
commit 7173d24fe1
48 changed files with 310 additions and 234 deletions
+7
View File
@@ -94,6 +94,13 @@ coverage/
test-results/ test-results/
*.test.log *.test.log
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
# Misc # Misc
*.bak *.bak
*.backup *.backup
@@ -227,7 +227,7 @@ data class CreateOrderRequest(
@Deprecated("使用 NewOrderRequest 代替") @Deprecated("使用 NewOrderRequest 代替")
data class CreateOrdersBatchRequest( data class CreateOrdersBatchRequest(
val orders: List<CreateOrderRequest> val orders: List<NewOrderRequest>
) )
data class CancelOrdersBatchRequest( data class CancelOrdersBatchRequest(
@@ -1,6 +1,6 @@
package com.wrbug.polymarketbot.config package com.wrbug.polymarketbot.config
import com.wrbug.polymarketbot.service.ProxyConfigService import com.wrbug.polymarketbot.service.system.ProxyConfigService
import jakarta.annotation.PostConstruct import jakarta.annotation.PostConstruct
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import org.springframework.stereotype.Component import org.springframework.stereotype.Component
@@ -1,8 +1,8 @@
package com.wrbug.polymarketbot.controller package com.wrbug.polymarketbot.controller.accounts
import com.wrbug.polymarketbot.dto.* import com.wrbug.polymarketbot.dto.*
import com.wrbug.polymarketbot.enums.ErrorCode import com.wrbug.polymarketbot.enums.ErrorCode
import com.wrbug.polymarketbot.service.AccountService import com.wrbug.polymarketbot.service.accounts.AccountService
import com.wrbug.polymarketbot.util.toSafeBigDecimal import com.wrbug.polymarketbot.util.toSafeBigDecimal
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
@@ -15,7 +15,7 @@ import java.math.BigDecimal
* 账户管理控制器 * 账户管理控制器
*/ */
@RestController @RestController
@RequestMapping("/api/copy-trading/accounts") @RequestMapping("/api/accounts")
class AccountController( class AccountController(
private val accountService: AccountService, private val accountService: AccountService,
private val messageSource: MessageSource private val messageSource: MessageSource
@@ -1,8 +1,8 @@
package com.wrbug.polymarketbot.controller package com.wrbug.polymarketbot.controller.announcement
import com.wrbug.polymarketbot.dto.* import com.wrbug.polymarketbot.dto.*
import com.wrbug.polymarketbot.enums.ErrorCode import com.wrbug.polymarketbot.enums.ErrorCode
import com.wrbug.polymarketbot.service.AnnouncementService import com.wrbug.polymarketbot.service.announcement.AnnouncementService
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import org.springframework.context.MessageSource import org.springframework.context.MessageSource
@@ -1,8 +1,8 @@
package com.wrbug.polymarketbot.controller package com.wrbug.polymarketbot.controller.auth
import com.wrbug.polymarketbot.dto.* import com.wrbug.polymarketbot.dto.*
import com.wrbug.polymarketbot.enums.ErrorCode import com.wrbug.polymarketbot.enums.ErrorCode
import com.wrbug.polymarketbot.service.AuthService import com.wrbug.polymarketbot.service.auth.AuthService
import jakarta.servlet.http.HttpServletRequest import jakarta.servlet.http.HttpServletRequest
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import org.springframework.context.MessageSource import org.springframework.context.MessageSource
@@ -1,9 +1,9 @@
package com.wrbug.polymarketbot.controller package com.wrbug.polymarketbot.controller.copytrading.configs
import com.wrbug.polymarketbot.dto.* import com.wrbug.polymarketbot.dto.*
import com.wrbug.polymarketbot.enums.ErrorCode import com.wrbug.polymarketbot.enums.ErrorCode
import com.wrbug.polymarketbot.service.CopyTradingService import com.wrbug.polymarketbot.service.copytrading.configs.CopyTradingService
import com.wrbug.polymarketbot.service.FilteredOrderService import com.wrbug.polymarketbot.service.copytrading.configs.FilteredOrderService
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import org.springframework.context.MessageSource import org.springframework.context.MessageSource
import org.springframework.http.ResponseEntity import org.springframework.http.ResponseEntity
@@ -13,7 +13,7 @@ import org.springframework.web.bind.annotation.*
* 跟单配置管理控制器钱包-模板关联 * 跟单配置管理控制器钱包-模板关联
*/ */
@RestController @RestController
@RequestMapping("/api/copy-trading") @RequestMapping("/api/copy-trading/configs")
class CopyTradingController( class CopyTradingController(
private val copyTradingService: CopyTradingService, private val copyTradingService: CopyTradingService,
private val filteredOrderService: FilteredOrderService, private val filteredOrderService: FilteredOrderService,
@@ -1,8 +1,8 @@
package com.wrbug.polymarketbot.controller package com.wrbug.polymarketbot.controller.copytrading.leaders
import com.wrbug.polymarketbot.dto.* import com.wrbug.polymarketbot.dto.*
import com.wrbug.polymarketbot.enums.ErrorCode import com.wrbug.polymarketbot.enums.ErrorCode
import com.wrbug.polymarketbot.service.LeaderService import com.wrbug.polymarketbot.service.copytrading.leaders.LeaderService
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import org.springframework.context.MessageSource import org.springframework.context.MessageSource
import org.springframework.http.ResponseEntity import org.springframework.http.ResponseEntity
@@ -1,8 +1,8 @@
package com.wrbug.polymarketbot.controller package com.wrbug.polymarketbot.controller.copytrading.statistics
import com.wrbug.polymarketbot.dto.* import com.wrbug.polymarketbot.dto.*
import com.wrbug.polymarketbot.enums.ErrorCode import com.wrbug.polymarketbot.enums.ErrorCode
import com.wrbug.polymarketbot.service.CopyTradingStatisticsService import com.wrbug.polymarketbot.service.copytrading.statistics.CopyTradingStatisticsService
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import org.springframework.context.MessageSource import org.springframework.context.MessageSource
@@ -1,8 +1,8 @@
package com.wrbug.polymarketbot.controller package com.wrbug.polymarketbot.controller.copytrading.templates
import com.wrbug.polymarketbot.dto.* import com.wrbug.polymarketbot.dto.*
import com.wrbug.polymarketbot.enums.ErrorCode import com.wrbug.polymarketbot.enums.ErrorCode
import com.wrbug.polymarketbot.service.CopyTradingTemplateService import com.wrbug.polymarketbot.service.copytrading.templates.CopyTradingTemplateService
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import org.springframework.context.MessageSource import org.springframework.context.MessageSource
import org.springframework.http.ResponseEntity import org.springframework.http.ResponseEntity
@@ -1,10 +1,10 @@
package com.wrbug.polymarketbot.controller package com.wrbug.polymarketbot.controller.markets
import com.wrbug.polymarketbot.api.LatestPriceResponse import com.wrbug.polymarketbot.api.LatestPriceResponse
import com.wrbug.polymarketbot.dto.* import com.wrbug.polymarketbot.dto.*
import com.wrbug.polymarketbot.enums.ErrorCode import com.wrbug.polymarketbot.enums.ErrorCode
import com.wrbug.polymarketbot.service.AccountService import com.wrbug.polymarketbot.service.accounts.AccountService
import com.wrbug.polymarketbot.service.PolymarketClobService import com.wrbug.polymarketbot.service.common.PolymarketClobService
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import org.springframework.context.MessageSource import org.springframework.context.MessageSource
@@ -16,7 +16,7 @@ import org.springframework.web.bind.annotation.*
* 提供市场相关的数据查询接口价格订单簿等 * 提供市场相关的数据查询接口价格订单簿等
*/ */
@RestController @RestController
@RequestMapping("/api/copy-trading/markets") @RequestMapping("/api/markets")
class MarketController( class MarketController(
private val accountService: AccountService, private val accountService: AccountService,
private val clobService: PolymarketClobService, private val clobService: PolymarketClobService,
@@ -1,4 +1,4 @@
package com.wrbug.polymarketbot.controller package com.wrbug.polymarketbot.controller.system
import org.springframework.http.ResponseEntity import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.GetMapping
@@ -11,7 +11,7 @@ import java.util.*
* 用于 Docker 健康检查和监控 * 用于 Docker 健康检查和监控
*/ */
@RestController @RestController
@RequestMapping("/api/health") @RequestMapping("/api/system/health")
class HealthController { class HealthController {
@GetMapping @GetMapping
@@ -1,9 +1,9 @@
package com.wrbug.polymarketbot.controller package com.wrbug.polymarketbot.controller.system
import com.wrbug.polymarketbot.dto.* import com.wrbug.polymarketbot.dto.*
import com.wrbug.polymarketbot.enums.ErrorCode import com.wrbug.polymarketbot.enums.ErrorCode
import com.wrbug.polymarketbot.service.NotificationConfigService import com.wrbug.polymarketbot.service.system.NotificationConfigService
import com.wrbug.polymarketbot.service.TelegramNotificationService import com.wrbug.polymarketbot.service.system.TelegramNotificationService
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import org.springframework.context.MessageSource import org.springframework.context.MessageSource
@@ -14,7 +14,7 @@ import org.springframework.web.bind.annotation.*
* 消息推送配置控制器 * 消息推送配置控制器
*/ */
@RestController @RestController
@RequestMapping("/api/notifications") @RequestMapping("/api/system/notifications")
class NotificationController( class NotificationController(
private val notificationConfigService: NotificationConfigService, private val notificationConfigService: NotificationConfigService,
private val telegramNotificationService: TelegramNotificationService, private val telegramNotificationService: TelegramNotificationService,
@@ -1,9 +1,9 @@
package com.wrbug.polymarketbot.controller package com.wrbug.polymarketbot.controller.system
import com.wrbug.polymarketbot.dto.* import com.wrbug.polymarketbot.dto.*
import com.wrbug.polymarketbot.enums.ErrorCode import com.wrbug.polymarketbot.enums.ErrorCode
import com.wrbug.polymarketbot.service.ApiHealthCheckService import com.wrbug.polymarketbot.service.system.ApiHealthCheckService
import com.wrbug.polymarketbot.service.ProxyConfigService import com.wrbug.polymarketbot.service.system.ProxyConfigService
import jakarta.servlet.http.HttpServletRequest import jakarta.servlet.http.HttpServletRequest
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
@@ -15,7 +15,7 @@ import org.springframework.web.bind.annotation.*
* 代理配置控制器 * 代理配置控制器
*/ */
@RestController @RestController
@RequestMapping("/api/proxy-config") @RequestMapping("/api/system/proxy")
class ProxyConfigController( class ProxyConfigController(
private val proxyConfigService: ProxyConfigService, private val proxyConfigService: ProxyConfigService,
private val apiHealthCheckService: ApiHealthCheckService, private val apiHealthCheckService: ApiHealthCheckService,
@@ -1,9 +1,9 @@
package com.wrbug.polymarketbot.controller package com.wrbug.polymarketbot.controller.system
import com.wrbug.polymarketbot.dto.* import com.wrbug.polymarketbot.dto.*
import com.wrbug.polymarketbot.enums.ErrorCode import com.wrbug.polymarketbot.enums.ErrorCode
import com.wrbug.polymarketbot.service.SystemConfigService import com.wrbug.polymarketbot.service.system.SystemConfigService
import com.wrbug.polymarketbot.service.RelayClientService import com.wrbug.polymarketbot.service.system.RelayClientService
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import org.springframework.context.MessageSource import org.springframework.context.MessageSource
@@ -1,8 +1,8 @@
package com.wrbug.polymarketbot.controller package com.wrbug.polymarketbot.controller.system
import com.wrbug.polymarketbot.dto.* import com.wrbug.polymarketbot.dto.*
import com.wrbug.polymarketbot.enums.ErrorCode import com.wrbug.polymarketbot.enums.ErrorCode
import com.wrbug.polymarketbot.service.UserService import com.wrbug.polymarketbot.service.system.UserService
import jakarta.servlet.http.HttpServletRequest import jakarta.servlet.http.HttpServletRequest
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import org.springframework.context.MessageSource import org.springframework.context.MessageSource
@@ -13,7 +13,7 @@ import org.springframework.web.bind.annotation.*
* 用户管理控制器 * 用户管理控制器
*/ */
@RestController @RestController
@RequestMapping("/api/users") @RequestMapping("/api/system/users")
class UserController( class UserController(
private val userService: UserService, private val userService: UserService,
private val messageSource: MessageSource private val messageSource: MessageSource
@@ -1,4 +1,4 @@
package com.wrbug.polymarketbot.service package com.wrbug.polymarketbot.service.accounts
import com.wrbug.polymarketbot.api.TradeResponse import com.wrbug.polymarketbot.api.TradeResponse
import com.wrbug.polymarketbot.dto.* import com.wrbug.polymarketbot.dto.*
@@ -8,6 +8,14 @@ import com.wrbug.polymarketbot.util.RetrofitFactory
import com.wrbug.polymarketbot.util.toSafeBigDecimal import com.wrbug.polymarketbot.util.toSafeBigDecimal
import com.wrbug.polymarketbot.util.eq import com.wrbug.polymarketbot.util.eq
import com.wrbug.polymarketbot.util.JsonUtils import com.wrbug.polymarketbot.util.JsonUtils
import com.wrbug.polymarketbot.service.common.PolymarketClobService
import com.wrbug.polymarketbot.service.common.BlockchainService
import com.wrbug.polymarketbot.service.common.PolymarketApiKeyService
import com.wrbug.polymarketbot.service.copytrading.orders.OrderPushService
import com.wrbug.polymarketbot.service.copytrading.orders.OrderSigningService
import com.wrbug.polymarketbot.service.system.TelegramNotificationService
import com.wrbug.polymarketbot.service.system.RelayClientService
import com.wrbug.polymarketbot.util.CryptoUtils
import kotlinx.coroutines.* import kotlinx.coroutines.*
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
@@ -27,7 +35,7 @@ class AccountService(
private val apiKeyService: PolymarketApiKeyService, private val apiKeyService: PolymarketApiKeyService,
private val orderPushService: OrderPushService, private val orderPushService: OrderPushService,
private val orderSigningService: OrderSigningService, private val orderSigningService: OrderSigningService,
private val cryptoUtils: com.wrbug.polymarketbot.util.CryptoUtils, private val cryptoUtils: CryptoUtils,
private val telegramNotificationService: TelegramNotificationService? = null, // 可选,避免循环依赖 private val telegramNotificationService: TelegramNotificationService? = null, // 可选,避免循环依赖
private val relayClientService: RelayClientService private val relayClientService: RelayClientService
) { ) {
@@ -1070,7 +1078,7 @@ class AccountService(
/** /**
* 从订单表获取最优价用于市价单 * 从订单表获取最优价用于市价单
* 支持多元市场二元三元及以上 * 支持多元市场二元三元及以上
* 委托给 PolymarketClobService.getOptimalPrice 方法 * 委托给 com.wrbug.polymarketbot.service.common.PolymarketClobService.getOptimalPrice 方法
* *
* @param tokenId token ID通过 marketId outcomeIndex 计算得出 * @param tokenId token ID通过 marketId outcomeIndex 计算得出
* @param isSellOrder 是否为卖出订单true: 卖单需要 bestBidfalse: 买单需要 bestAsk * @param isSellOrder 是否为卖出订单true: 卖单需要 bestBidfalse: 买单需要 bestAsk
@@ -1167,7 +1175,7 @@ class AccountService(
/** /**
* 获取可赎回仓位统计 * 获取可赎回仓位统计
*/ */
suspend fun getRedeemablePositionsSummary(accountId: Long? = null): Result<com.wrbug.polymarketbot.dto.RedeemablePositionsSummary> { suspend fun getRedeemablePositionsSummary(accountId: Long? = null): Result<RedeemablePositionsSummary> {
return try { return try {
val positionsResult = getAllPositions() val positionsResult = getAllPositions()
positionsResult.fold( positionsResult.fold(
@@ -1223,7 +1231,7 @@ class AccountService(
* 赎回仓位 * 赎回仓位
* 支持多账户多仓位赎回自动按账户和市场分组 * 支持多账户多仓位赎回自动按账户和市场分组
*/ */
suspend fun redeemPositions(request: com.wrbug.polymarketbot.dto.PositionRedeemRequest): Result<com.wrbug.polymarketbot.dto.PositionRedeemResponse> { suspend fun redeemPositions(request: PositionRedeemRequest): Result<PositionRedeemResponse> {
return try { return try {
// 检查 Builder API Key 是否已配置 // 检查 Builder API Key 是否已配置
if (!relayClientService.isBuilderApiKeyConfigured()) { if (!relayClientService.isBuilderApiKeyConfigured()) {
@@ -1,4 +1,4 @@
package com.wrbug.polymarketbot.service package com.wrbug.polymarketbot.service.accounts
import com.wrbug.polymarketbot.dto.AccountPositionDto import com.wrbug.polymarketbot.dto.AccountPositionDto
import com.wrbug.polymarketbot.entity.CopyOrderTracking import com.wrbug.polymarketbot.entity.CopyOrderTracking
@@ -18,6 +18,9 @@ import jakarta.annotation.PostConstruct
import jakarta.annotation.PreDestroy import jakarta.annotation.PreDestroy
import org.springframework.context.MessageSource import org.springframework.context.MessageSource
import org.springframework.context.i18n.LocaleContextHolder import org.springframework.context.i18n.LocaleContextHolder
import com.wrbug.polymarketbot.service.system.SystemConfigService
import com.wrbug.polymarketbot.service.system.RelayClientService
import com.wrbug.polymarketbot.service.system.TelegramNotificationService
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
import java.math.BigDecimal import java.math.BigDecimal
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
@@ -368,15 +371,15 @@ class PositionCheckService(
if (position == null) { if (position == null) {
// 仓位不存在,更新所有订单状态为已卖出 // 仓位不存在,更新所有订单状态为已卖出
val currentPrice = getCurrentMarketPrice(marketId, outcomeIndex) val currentPrice = getCurrentMarketPrice(marketId, outcomeIndex)
updateOrdersAsSold(orders, currentPrice, copyTrading.id!!, marketId, outcomeIndex) updateOrdersAsSold(orders, currentPrice, copyTrading.id, marketId, outcomeIndex)
} else { } else {
// 有仓位,按订单下单顺序(FIFO)更新状态 // 有仓位,按订单下单顺序(FIFO)更新状态
// 如果仓位数量 >= 订单数量总和,所有订单完全成交 // 如果仓位数量 >= 订单数量总和,所有订单完全成交
// 如果仓位数量 < 订单数量总和,按FIFO顺序部分成交 // 如果仓位数量 < 订单数量总和,按FIFO顺序部分成交
val totalUnmatchedQuantity = orders.sumOf { it.remainingQuantity.toSafeBigDecimal() }
val positionQuantity = position.quantity.toSafeBigDecimal() val positionQuantity = position.quantity.toSafeBigDecimal()
val currentPrice = getCurrentMarketPrice(marketId, outcomeIndex) val currentPrice = getCurrentMarketPrice(marketId, outcomeIndex)
updateOrdersAsSoldByFIFO(orders, positionQuantity, currentPrice, copyTrading.id!!, marketId, outcomeIndex) updateOrdersAsSoldByFIFO(orders, positionQuantity, currentPrice,
copyTrading.id, marketId, outcomeIndex)
} }
} }
} }
@@ -1,4 +1,4 @@
package com.wrbug.polymarketbot.service package com.wrbug.polymarketbot.service.accounts
import com.wrbug.polymarketbot.dto.PositionListResponse import com.wrbug.polymarketbot.dto.PositionListResponse
import jakarta.annotation.PostConstruct import jakarta.annotation.PostConstruct
@@ -1,6 +1,7 @@
package com.wrbug.polymarketbot.service package com.wrbug.polymarketbot.service.accounts
import com.wrbug.polymarketbot.dto.AccountPositionDto import com.wrbug.polymarketbot.dto.AccountPositionDto
import com.wrbug.polymarketbot.dto.PositionListResponse
import com.wrbug.polymarketbot.dto.PositionPushMessage import com.wrbug.polymarketbot.dto.PositionPushMessage
import com.wrbug.polymarketbot.dto.PositionPushMessageType import com.wrbug.polymarketbot.dto.PositionPushMessageType
import com.wrbug.polymarketbot.dto.getPositionKey import com.wrbug.polymarketbot.dto.getPositionKey
@@ -155,7 +156,7 @@ class PositionPushService(
* 处理仓位更新事件 * 处理仓位更新事件
* 根据文档要求每次轮训完成后向订阅者发送全量数据 * 根据文档要求每次轮训完成后向订阅者发送全量数据
*/ */
private fun handlePositionUpdate(positions: com.wrbug.polymarketbot.dto.PositionListResponse) { private fun handlePositionUpdate(positions: PositionListResponse) {
// 更新快照 // 更新快照
lastCurrentPositions = positions.currentPositions.associateBy { it.getPositionKey() } lastCurrentPositions = positions.currentPositions.associateBy { it.getPositionKey() }
lastHistoryPositions = positions.historyPositions.associateBy { it.getPositionKey() } lastHistoryPositions = positions.historyPositions.associateBy { it.getPositionKey() }
@@ -1,6 +1,7 @@
package com.wrbug.polymarketbot.service package com.wrbug.polymarketbot.service.announcement
import com.wrbug.polymarketbot.api.GitHubApi import com.wrbug.polymarketbot.api.GitHubApi
import com.wrbug.polymarketbot.api.GitHubCommentResponse
import com.wrbug.polymarketbot.dto.AnnouncementDto import com.wrbug.polymarketbot.dto.AnnouncementDto
import com.wrbug.polymarketbot.dto.AnnouncementListResponse import com.wrbug.polymarketbot.dto.AnnouncementListResponse
import com.wrbug.polymarketbot.util.RetrofitFactory import com.wrbug.polymarketbot.util.RetrofitFactory
@@ -25,30 +26,30 @@ class AnnouncementService(
@Value("\${github.announcement.issue.number:1}") @Value("\${github.announcement.issue.number:1}")
private val issueNumber: Int private val issueNumber: Int
) { ) {
private val logger = LoggerFactory.getLogger(AnnouncementService::class.java) private val logger = LoggerFactory.getLogger(AnnouncementService::class.java)
// GitHub API 客户端(懒加载) // GitHub API 客户端(懒加载)
private val githubApi: GitHubApi by lazy { private val githubApi: GitHubApi by lazy {
retrofitFactory.createGitHubApi() retrofitFactory.createGitHubApi()
} }
// 需要排除的 Issue ID(从 issue_url 中提取) // 需要排除的 Issue ID(从 issue_url 中提取)
private val excludedIssueId = "3703128976" private val excludedIssueId = "3703128976"
// 缓存数据(1分钟有效期) // 缓存数据(1分钟有效期)
private data class CachedData<T>( private data class CachedData<T>(
val data: T, val data: T,
val timestamp: Long val timestamp: Long
) )
private var cachedList: CachedData<AnnouncementListResponse>? = null private var cachedList: CachedData<AnnouncementListResponse>? = null
private var cachedAssignees: CachedData<List<String>>? = null private var cachedAssignees: CachedData<List<String>>? = null
private var cachedComments: CachedData<List<com.wrbug.polymarketbot.api.GitHubCommentResponse>>? = null private var cachedComments: CachedData<List<com.wrbug.polymarketbot.api.GitHubCommentResponse>>? = null
// 缓存有效期:10分钟(毫秒) // 缓存有效期:10分钟(毫秒)
private val cacheExpiryTime = 10 * 60 * 1000L private val cacheExpiryTime = 10 * 60 * 1000L
/** /**
* 检查缓存是否有效 * 检查缓存是否有效
*/ */
@@ -57,7 +58,7 @@ class AnnouncementService(
val now = System.currentTimeMillis() val now = System.currentTimeMillis()
return (now - cached.timestamp) < cacheExpiryTime return (now - cached.timestamp) < cacheExpiryTime
} }
/** /**
* 检查是否被限流 * 检查是否被限流
*/ */
@@ -70,7 +71,7 @@ class AnnouncementService(
val remaining = response.headers()["X-RateLimit-Remaining"] val remaining = response.headers()["X-RateLimit-Remaining"]
return remaining == "0" return remaining == "0"
} }
/** /**
* 获取 Issue assignees 列表通过 API 获取带缓存 * 获取 Issue assignees 列表通过 API 获取带缓存
* @return Pair<assignees列表, 是否使用了缓存> * @return Pair<assignees列表, 是否使用了缓存>
@@ -81,14 +82,14 @@ class AnnouncementService(
logger.debug("使用缓存的 assignees") logger.debug("使用缓存的 assignees")
return Pair(cachedAssignees!!.data, true) return Pair(cachedAssignees!!.data, true)
} }
return try { return try {
val response = githubApi.getIssue( val response = githubApi.getIssue(
owner = repoOwner, owner = repoOwner,
repo = repoName, repo = repoName,
issueNumber = issueNumber issueNumber = issueNumber
) )
// 如果被限流,使用缓存数据,不更新缓存 // 如果被限流,使用缓存数据,不更新缓存
if (isRateLimited(response)) { if (isRateLimited(response)) {
logger.warn("GitHub API 被限流,使用缓存的 assignees(不更新缓存)") logger.warn("GitHub API 被限流,使用缓存的 assignees(不更新缓存)")
@@ -98,14 +99,14 @@ class AnnouncementService(
// 如果没有缓存,使用默认值 // 如果没有缓存,使用默认值
return Pair(listOf("WrBug"), false) return Pair(listOf("WrBug"), false)
} }
val assignees = if (response.isSuccessful && response.body() != null) { val assignees = if (response.isSuccessful && response.body() != null) {
response.body()!!.assignees.map { it.login } response.body()!!.assignees.map { it.login }
} else { } else {
logger.warn("获取 Issue assignees 失败,使用默认值: code=${response.code()}") logger.warn("获取 Issue assignees 失败,使用默认值: code=${response.code()}")
listOf("WrBug") // 默认值 listOf("WrBug") // 默认值
} }
// 更新缓存 // 更新缓存
cachedAssignees = CachedData(assignees, System.currentTimeMillis()) cachedAssignees = CachedData(assignees, System.currentTimeMillis())
Pair(assignees, false) // 返回新数据,标记为未使用缓存 Pair(assignees, false) // 返回新数据,标记为未使用缓存
@@ -119,24 +120,24 @@ class AnnouncementService(
Pair(listOf("WrBug"), false) // 默认值 Pair(listOf("WrBug"), false) // 默认值
} }
} }
/** /**
* 获取 Issue 评论列表带缓存 * 获取 Issue 评论列表带缓存
* @return Pair<评论列表, 是否使用了缓存> * @return Pair<评论列表, 是否使用了缓存>
*/ */
private suspend fun getIssueComments(forceRefresh: Boolean = false): Pair<List<com.wrbug.polymarketbot.api.GitHubCommentResponse>, Boolean> { private suspend fun getIssueComments(forceRefresh: Boolean = false): Pair<List<GitHubCommentResponse>, Boolean> {
// 检查缓存 // 检查缓存
if (!forceRefresh && isCacheValid(cachedComments)) { if (!forceRefresh && isCacheValid(cachedComments)) {
logger.debug("使用缓存的评论列表") logger.debug("使用缓存的评论列表")
return Pair(cachedComments!!.data, true) return Pair(cachedComments!!.data, true)
} }
val response = githubApi.getIssueComments( val response = githubApi.getIssueComments(
owner = repoOwner, owner = repoOwner,
repo = repoName, repo = repoName,
issueNumber = issueNumber issueNumber = issueNumber
) )
// 如果被限流,使用缓存数据,不更新缓存 // 如果被限流,使用缓存数据,不更新缓存
if (isRateLimited(response)) { if (isRateLimited(response)) {
logger.warn("GitHub API 被限流,使用缓存的评论列表(不更新缓存)") logger.warn("GitHub API 被限流,使用缓存的评论列表(不更新缓存)")
@@ -146,7 +147,7 @@ class AnnouncementService(
// 如果没有缓存,抛出异常 // 如果没有缓存,抛出异常
throw Exception("获取公告列表失败: GitHub API 被限流,且无缓存数据") throw Exception("获取公告列表失败: GitHub API 被限流,且无缓存数据")
} }
if (!response.isSuccessful || response.body() == null) { if (!response.isSuccessful || response.body() == null) {
logger.error("获取 GitHub Issue 评论失败: code=${response.code()}, message=${response.message()}") logger.error("获取 GitHub Issue 评论失败: code=${response.code()}, message=${response.message()}")
// 如果缓存存在,使用缓存 // 如果缓存存在,使用缓存
@@ -156,14 +157,14 @@ class AnnouncementService(
} }
throw Exception("获取公告列表失败: HTTP ${response.code()}") throw Exception("获取公告列表失败: HTTP ${response.code()}")
} }
val comments = response.body()!! val comments = response.body()!!
// 更新缓存 // 更新缓存
cachedComments = CachedData(comments, System.currentTimeMillis()) cachedComments = CachedData(comments, System.currentTimeMillis())
return Pair(comments, false) // 返回新数据,标记为未使用缓存 return Pair(comments, false) // 返回新数据,标记为未使用缓存
} }
/** /**
* 获取公告列表最近10条 * 获取公告列表最近10条
* @param forceRefresh 是否强制刷新缓存 * @param forceRefresh 是否强制刷新缓存
@@ -174,12 +175,12 @@ class AnnouncementService(
logger.debug("使用缓存的公告列表") logger.debug("使用缓存的公告列表")
return Result.success(cachedList!!.data) return Result.success(cachedList!!.data)
} }
return try { return try {
// 强制刷新时,先尝试获取新数据 // 强制刷新时,先尝试获取新数据
val (assigneeList, assigneesFromCache) = getAssignees(forceRefresh) val (assigneeList, assigneesFromCache) = getAssignees(forceRefresh)
val (comments, commentsFromCache) = getIssueComments(forceRefresh) val (comments, commentsFromCache) = getIssueComments(forceRefresh)
// 如果强制刷新时使用了缓存(被限流),直接返回缓存数据,不更新缓存 // 如果强制刷新时使用了缓存(被限流),直接返回缓存数据,不更新缓存
if (forceRefresh && (assigneesFromCache || commentsFromCache)) { if (forceRefresh && (assigneesFromCache || commentsFromCache)) {
logger.warn("强制刷新时被限流,返回缓存的公告列表(不更新缓存)") logger.warn("强制刷新时被限流,返回缓存的公告列表(不更新缓存)")
@@ -187,48 +188,47 @@ class AnnouncementService(
return Result.success(cachedList!!.data) return Result.success(cachedList!!.data)
} }
} }
// 筛选条件: // 筛选条件:
// 1. assignees 发布的评论 // 1. assignees 发布的评论
// 2. 排除 issueNumber 为 3703128976 的评论(从 issue_url 中提取) // 2. 排除 issueNumber 为 3703128976 的评论(从 issue_url 中提取)
val filteredComments = comments val filteredComments = comments.filter { comment ->
.filter { comment -> // 检查是否为 assignee
// 检查是否为 assignee val isAssignee = assigneeList.contains(comment.user.login)
val isAssignee = assigneeList.contains(comment.user.login)
// 检查是否应该排除(从 issue_url 中提取 issue ID
// 检查是否应该排除(从 issue_url 中提取 issue ID val shouldExclude = comment.issue_url?.let { issueUrl ->
val shouldExclude = comment.issue_url?.let { issueUrl -> // issue_url 格式:https://api.github.com/repos/owner/repo/issues/3703128976
// issue_url 格式:https://api.github.com/repos/owner/repo/issues/3703128976 // 提取最后的数字
// 提取最后的数字 val issueId = issueUrl.split("/").lastOrNull()
val issueId = issueUrl.split("/").lastOrNull() issueId == excludedIssueId
issueId == excludedIssueId } ?: false
} ?: false
isAssignee && !shouldExclude
isAssignee && !shouldExclude }
}
.sortedByDescending { comment -> .sortedByDescending { comment ->
parseGitHubTime(comment.created_at) parseGitHubTime(comment.created_at)
} }
val total = filteredComments.size val total = filteredComments.size
val hasMore = total > 10 val hasMore = total > 10
// 取前10条 // 取前10条
val latest10 = filteredComments.take(10).map { comment -> val latest10 = filteredComments.take(10).map { comment ->
toAnnouncementDto(comment) toAnnouncementDto(comment)
} }
val result = AnnouncementListResponse( val result = AnnouncementListResponse(
list = latest10, list = latest10,
hasMore = hasMore, hasMore = hasMore,
total = total total = total
) )
// 只有在数据正常返回时才更新缓存(不是从缓存获取的) // 只有在数据正常返回时才更新缓存(不是从缓存获取的)
if (!assigneesFromCache && !commentsFromCache) { if (!assigneesFromCache && !commentsFromCache) {
cachedList = CachedData(result, System.currentTimeMillis()) cachedList = CachedData(result, System.currentTimeMillis())
} }
Result.success(result) Result.success(result)
} catch (e: Exception) { } catch (e: Exception) {
logger.error("获取公告列表异常: ${e.message}", e) logger.error("获取公告列表异常: ${e.message}", e)
@@ -240,7 +240,7 @@ class AnnouncementService(
Result.failure(e) Result.failure(e)
} }
} }
/** /**
* 获取公告详情 * 获取公告详情
* @param id 评论ID如果为 null 则返回最新一条 * @param id 评论ID如果为 null 则返回最新一条
@@ -249,19 +249,18 @@ class AnnouncementService(
suspend fun getAnnouncementDetail(id: Long?, forceRefresh: Boolean = false): Result<AnnouncementDto> { suspend fun getAnnouncementDetail(id: Long?, forceRefresh: Boolean = false): Result<AnnouncementDto> {
return try { return try {
// 获取 assignees // 获取 assignees
val (assigneeList, assigneesFromCache) = getAssignees(forceRefresh) val (assigneeList, _) = getAssignees(forceRefresh)
// 获取评论列表 // 获取评论列表
val (comments, commentsFromCache) = getIssueComments(forceRefresh) val (comments, _) = getIssueComments(forceRefresh)
// 筛选条件: // 筛选条件:
// 1. assignees 发布的评论 // 1. assignees 发布的评论
// 2. 排除 issueNumber 为 3703128976 的评论(从 issue_url 中提取) // 2. 排除 issueNumber 为 3703128976 的评论(从 issue_url 中提取)
val filteredComments = comments val filteredComments = comments.filter { comment ->
.filter { comment ->
// 检查是否为 assignee // 检查是否为 assignee
val isAssignee = assigneeList.contains(comment.user.login) val isAssignee = assigneeList.contains(comment.user.login)
// 检查是否应该排除(从 issue_url 中提取 issue ID // 检查是否应该排除(从 issue_url 中提取 issue ID
val shouldExclude = comment.issue_url?.let { issueUrl -> val shouldExclude = comment.issue_url?.let { issueUrl ->
// issue_url 格式:https://api.github.com/repos/owner/repo/issues/3703128976 // issue_url 格式:https://api.github.com/repos/owner/repo/issues/3703128976
@@ -269,37 +268,37 @@ class AnnouncementService(
val issueId = issueUrl.split("/").lastOrNull() val issueId = issueUrl.split("/").lastOrNull()
issueId == excludedIssueId issueId == excludedIssueId
} ?: false } ?: false
isAssignee && !shouldExclude isAssignee && !shouldExclude
} }
.sortedByDescending { comment -> .sortedByDescending { comment ->
parseGitHubTime(comment.created_at) parseGitHubTime(comment.created_at)
} }
val targetComment = if (id != null) { val targetComment = if (id != null) {
filteredComments.find { it.id == id } filteredComments.find { it.id == id }
} else { } else {
filteredComments.firstOrNull() filteredComments.firstOrNull()
} }
if (targetComment == null) { if (targetComment == null) {
return Result.failure(IllegalArgumentException("公告不存在")) return Result.failure(IllegalArgumentException("公告不存在"))
} }
Result.success(toAnnouncementDto(targetComment)) Result.success(toAnnouncementDto(targetComment))
} catch (e: Exception) { } catch (e: Exception) {
logger.error("获取公告详情异常: ${e.message}", e) logger.error("获取公告详情异常: ${e.message}", e)
Result.failure(e) Result.failure(e)
} }
} }
/** /**
* GitHub 评论转换为 AnnouncementDto * GitHub 评论转换为 AnnouncementDto
*/ */
private fun toAnnouncementDto(comment: com.wrbug.polymarketbot.api.GitHubCommentResponse): AnnouncementDto { private fun toAnnouncementDto(comment: GitHubCommentResponse): AnnouncementDto {
// 提取标题(第一行,移除 Markdown 格式) // 提取标题(第一行,移除 Markdown 格式)
val title = extractTitle(comment.body) val title = extractTitle(comment.body)
// 转换 reactions 数据 // 转换 reactions 数据
val reactions = comment.reactions?.let { r -> val reactions = comment.reactions?.let { r ->
com.wrbug.polymarketbot.dto.ReactionsDto( com.wrbug.polymarketbot.dto.ReactionsDto(
@@ -314,7 +313,7 @@ class AnnouncementService(
total = r.total_count total = r.total_count
) )
} }
return AnnouncementDto( return AnnouncementDto(
id = comment.id, id = comment.id,
title = title, title = title,
@@ -326,7 +325,7 @@ class AnnouncementService(
reactions = reactions reactions = reactions
) )
} }
/** /**
* 从评论内容中提取标题第一行移除 Markdown 格式 * 从评论内容中提取标题第一行移除 Markdown 格式
* 支持的 Markdown 格式 * 支持的 Markdown 格式
@@ -342,49 +341,49 @@ class AnnouncementService(
if (body.isBlank()) { if (body.isBlank()) {
return "" return ""
} }
// 获取第一行 // 获取第一行
val firstLine = body.lines().firstOrNull()?.trim() ?: "" val firstLine = body.lines().firstOrNull()?.trim() ?: ""
if (firstLine.isBlank()) { if (firstLine.isBlank()) {
return "" return ""
} }
// 移除 Markdown 格式 // 移除 Markdown 格式
var title = firstLine var title = firstLine
// 移除标题标记(# ## ### 等) // 移除标题标记(# ## ### 等)
title = title.replace(Regex("^#{1,6}\\s+"), "") title = title.replace(Regex("^#{1,6}\\s+"), "")
// 移除粗体标记(**text** 或 __text__ // 移除粗体标记(**text** 或 __text__
title = title.replace(Regex("\\*\\*([^*]+)\\*\\*"), "$1") title = title.replace(Regex("\\*\\*([^*]+)\\*\\*"), "$1")
title = title.replace(Regex("__([^_]+)__"), "$1") title = title.replace(Regex("__([^_]+)__"), "$1")
// 移除斜体标记(*text* 或 _text_ // 移除斜体标记(*text* 或 _text_
title = title.replace(Regex("(?<!\\*)\\*([^*]+)\\*(?!\\*)"), "$1") title = title.replace(Regex("(?<!\\*)\\*([^*]+)\\*(?!\\*)"), "$1")
title = title.replace(Regex("(?<!_)_([^_]+)_(?!_)"), "$1") title = title.replace(Regex("(?<!_)_([^_]+)_(?!_)"), "$1")
// 移除代码标记(`code` // 移除代码标记(`code`
title = title.replace(Regex("`([^`]+)`"), "$1") title = title.replace(Regex("`([^`]+)`"), "$1")
// 移除链接标记([text](url) // 移除链接标记([text](url)
title = title.replace(Regex("\\[([^\\]]+)\\]\\([^\\)]+\\)"), "$1") title = title.replace(Regex("\\[([^\\]]+)\\]\\([^\\)]+\\)"), "$1")
// 移除图片标记(![alt](url) // 移除图片标记(![alt](url)
title = title.replace(Regex("!\\[([^\\]]*)\\]\\([^\\)]+\\)"), "$1") title = title.replace(Regex("!\\[([^\\]]*)\\]\\([^\\)]+\\)"), "$1")
// 移除删除线标记(~~text~~ // 移除删除线标记(~~text~~
title = title.replace(Regex("~~([^~]+)~~"), "$1") title = title.replace(Regex("~~([^~]+)~~"), "$1")
// 移除引用标记(> text // 移除引用标记(> text
title = title.replace(Regex("^>\\s+"), "") title = title.replace(Regex("^>\\s+"), "")
// 移除列表标记(- * + 1. 等) // 移除列表标记(- * + 1. 等)
title = title.replace(Regex("^[-*+]\\s+"), "") title = title.replace(Regex("^[-*+]\\s+"), "")
title = title.replace(Regex("^\\d+\\.\\s+"), "") title = title.replace(Regex("^\\d+\\.\\s+"), "")
return title.trim() return title.trim()
} }
/** /**
* 解析 GitHub 时间格式ISO 8601为时间戳毫秒 * 解析 GitHub 时间格式ISO 8601为时间戳毫秒
* GitHub API 返回的时间格式2025-12-07T14:30:00Z * GitHub API 返回的时间格式2025-12-07T14:30:00Z
@@ -1,4 +1,4 @@
package com.wrbug.polymarketbot.service package com.wrbug.polymarketbot.service.auth
import com.wrbug.polymarketbot.dto.CheckFirstUseResponse import com.wrbug.polymarketbot.dto.CheckFirstUseResponse
import com.wrbug.polymarketbot.dto.LoginResponse import com.wrbug.polymarketbot.dto.LoginResponse
@@ -10,6 +10,7 @@ import jakarta.servlet.http.HttpServletRequest
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Value import org.springframework.beans.factory.annotation.Value
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
import com.wrbug.polymarketbot.service.common.RateLimitService
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional import org.springframework.transaction.annotation.Transactional
@@ -1,4 +1,4 @@
package com.wrbug.polymarketbot.service package com.wrbug.polymarketbot.service.common
import com.wrbug.polymarketbot.api.EthereumRpcApi import com.wrbug.polymarketbot.api.EthereumRpcApi
import com.wrbug.polymarketbot.api.JsonRpcRequest import com.wrbug.polymarketbot.api.JsonRpcRequest
@@ -11,6 +11,7 @@ import com.wrbug.polymarketbot.util.RetrofitFactory
import com.wrbug.polymarketbot.util.createClient import com.wrbug.polymarketbot.util.createClient
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Value import org.springframework.beans.factory.annotation.Value
import com.wrbug.polymarketbot.service.system.RelayClientService
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
import retrofit2.Retrofit import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory import retrofit2.converter.gson.GsonConverterFactory
@@ -1,4 +1,4 @@
package com.wrbug.polymarketbot.service package com.wrbug.polymarketbot.service.common
import com.wrbug.polymarketbot.api.ApiKeyResponse import com.wrbug.polymarketbot.api.ApiKeyResponse
import com.wrbug.polymarketbot.api.PolymarketClobApi import com.wrbug.polymarketbot.api.PolymarketClobApi
@@ -1,4 +1,4 @@
package com.wrbug.polymarketbot.service package com.wrbug.polymarketbot.service.common
import com.wrbug.polymarketbot.api.* import com.wrbug.polymarketbot.api.*
import com.wrbug.polymarketbot.util.RetrofitFactory import com.wrbug.polymarketbot.util.RetrofitFactory
@@ -1,4 +1,4 @@
package com.wrbug.polymarketbot.service package com.wrbug.polymarketbot.service.common
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Value import org.springframework.beans.factory.annotation.Value
@@ -1,9 +1,11 @@
package com.wrbug.polymarketbot.service package com.wrbug.polymarketbot.service.common
import com.wrbug.polymarketbot.dto.OrderPushMessage import com.wrbug.polymarketbot.dto.OrderPushMessage
import com.wrbug.polymarketbot.dto.PositionPushMessage import com.wrbug.polymarketbot.dto.PositionPushMessage
import com.wrbug.polymarketbot.dto.WebSocketMessage as WsMessage import com.wrbug.polymarketbot.dto.WebSocketMessage as WsMessage
import com.wrbug.polymarketbot.dto.WebSocketMessageType import com.wrbug.polymarketbot.dto.WebSocketMessageType
import com.wrbug.polymarketbot.service.accounts.PositionPushService
import com.wrbug.polymarketbot.service.copytrading.orders.OrderPushService
import kotlinx.coroutines.* import kotlinx.coroutines.*
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
@@ -1,4 +1,4 @@
package com.wrbug.polymarketbot.service package com.wrbug.polymarketbot.service.copytrading.configs
import com.wrbug.polymarketbot.api.OrderbookResponse import com.wrbug.polymarketbot.api.OrderbookResponse
import com.wrbug.polymarketbot.entity.CopyTrading import com.wrbug.polymarketbot.entity.CopyTrading
@@ -7,6 +7,7 @@ import com.wrbug.polymarketbot.util.lt
import com.wrbug.polymarketbot.util.multi import com.wrbug.polymarketbot.util.multi
import com.wrbug.polymarketbot.util.toSafeBigDecimal import com.wrbug.polymarketbot.util.toSafeBigDecimal
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import com.wrbug.polymarketbot.service.common.PolymarketClobService
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
import java.math.BigDecimal import java.math.BigDecimal
@@ -1,11 +1,14 @@
package com.wrbug.polymarketbot.service package com.wrbug.polymarketbot.service.copytrading.configs
import com.wrbug.polymarketbot.dto.* import com.wrbug.polymarketbot.dto.*
import com.wrbug.polymarketbot.entity.Account
import com.wrbug.polymarketbot.entity.CopyTrading import com.wrbug.polymarketbot.entity.CopyTrading
import com.wrbug.polymarketbot.entity.Leader
import com.wrbug.polymarketbot.repository.AccountRepository import com.wrbug.polymarketbot.repository.AccountRepository
import com.wrbug.polymarketbot.repository.CopyTradingRepository import com.wrbug.polymarketbot.repository.CopyTradingRepository
import com.wrbug.polymarketbot.repository.CopyTradingTemplateRepository import com.wrbug.polymarketbot.repository.CopyTradingTemplateRepository
import com.wrbug.polymarketbot.repository.LeaderRepository import com.wrbug.polymarketbot.repository.LeaderRepository
import com.wrbug.polymarketbot.service.copytrading.monitor.CopyTradingMonitorService
import com.wrbug.polymarketbot.util.toSafeBigDecimal import com.wrbug.polymarketbot.util.toSafeBigDecimal
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
@@ -339,7 +342,7 @@ class CopyTradingService(
fun getAccountTemplates(accountId: Long): Result<AccountTemplatesResponse> { fun getAccountTemplates(accountId: Long): Result<AccountTemplatesResponse> {
return try { return try {
// 验证账户是否存在 // 验证账户是否存在
val account = accountRepository.findById(accountId).orElse(null) accountRepository.findById(accountId).orElse(null)
?: return Result.failure(IllegalArgumentException("账户不存在")) ?: return Result.failure(IllegalArgumentException("账户不存在"))
val copyTradings = copyTradingRepository.findByAccountId(accountId) val copyTradings = copyTradingRepository.findByAccountId(accountId)
@@ -380,8 +383,8 @@ class CopyTradingService(
*/ */
private fun toDto( private fun toDto(
copyTrading: CopyTrading, copyTrading: CopyTrading,
account: com.wrbug.polymarketbot.entity.Account, account: Account,
leader: com.wrbug.polymarketbot.entity.Leader leader: Leader
): CopyTradingDto { ): CopyTradingDto {
return CopyTradingDto( return CopyTradingDto(
id = copyTrading.id!!, id = copyTrading.id!!,
@@ -1,4 +1,4 @@
package com.wrbug.polymarketbot.service package com.wrbug.polymarketbot.service.copytrading.configs
import com.wrbug.polymarketbot.dto.FilteredOrderDto import com.wrbug.polymarketbot.dto.FilteredOrderDto
import com.wrbug.polymarketbot.dto.FilteredOrderListRequest import com.wrbug.polymarketbot.dto.FilteredOrderListRequest
@@ -1,4 +1,4 @@
package com.wrbug.polymarketbot.service package com.wrbug.polymarketbot.service.copytrading.leaders
import com.wrbug.polymarketbot.dto.* import com.wrbug.polymarketbot.dto.*
import com.wrbug.polymarketbot.entity.Leader import com.wrbug.polymarketbot.entity.Leader
@@ -1,4 +1,4 @@
package com.wrbug.polymarketbot.service package com.wrbug.polymarketbot.service.copytrading.monitor
import com.wrbug.polymarketbot.entity.CopyTrading import com.wrbug.polymarketbot.entity.CopyTrading
import com.wrbug.polymarketbot.entity.Leader import com.wrbug.polymarketbot.entity.Leader
@@ -1,4 +1,4 @@
package com.wrbug.polymarketbot.service package com.wrbug.polymarketbot.service.copytrading.monitor
import com.wrbug.polymarketbot.api.TradeResponse import com.wrbug.polymarketbot.api.TradeResponse
import com.wrbug.polymarketbot.api.UserActivityResponse import com.wrbug.polymarketbot.api.UserActivityResponse
@@ -9,6 +9,7 @@ import jakarta.annotation.PreDestroy
import kotlinx.coroutines.* import kotlinx.coroutines.*
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Value import org.springframework.beans.factory.annotation.Value
import com.wrbug.polymarketbot.service.copytrading.statistics.CopyOrderTrackingService
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
import retrofit2.Response import retrofit2.Response
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
@@ -72,7 +73,7 @@ class CopyTradingPollingService(
return return
} }
val leaderId = leader.id!! val leaderId = leader.id
monitoredLeaders[leaderId] = leader monitoredLeaders[leaderId] = leader
// 初始化缓存的交易ID集合 // 初始化缓存的交易ID集合
cachedTradeIds[leaderId] = mutableSetOf() cachedTradeIds[leaderId] = mutableSetOf()
@@ -178,7 +179,7 @@ class CopyTradingPollingService(
return return
} }
val leaderId = leader.id!! val leaderId = leader.id
val leaderAddress = leader.leaderAddress val leaderAddress = leader.leaderAddress
try { try {
@@ -1,4 +1,4 @@
package com.wrbug.polymarketbot.service package com.wrbug.polymarketbot.service.copytrading.monitor
import com.google.gson.Gson import com.google.gson.Gson
import com.google.gson.JsonObject import com.google.gson.JsonObject
@@ -11,6 +11,7 @@ import jakarta.annotation.PreDestroy
import kotlinx.coroutines.* import kotlinx.coroutines.*
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Value import org.springframework.beans.factory.annotation.Value
import com.wrbug.polymarketbot.service.copytrading.statistics.CopyOrderTrackingService
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
@@ -65,7 +66,7 @@ class CopyTradingWebSocketService(
return return
} }
val leaderId = leader.id!! val leaderId = leader.id
val leaderAddress = leader.leaderAddress.lowercase() val leaderAddress = leader.leaderAddress.lowercase()
leaderAddresses[leaderId] = leaderAddress leaderAddresses[leaderId] = leaderAddress
@@ -1,6 +1,7 @@
package com.wrbug.polymarketbot.service package com.wrbug.polymarketbot.service.copytrading.orders
import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.databind.ObjectMapper
import com.wrbug.polymarketbot.api.MarketResponse
import com.wrbug.polymarketbot.dto.OrderDetailDto import com.wrbug.polymarketbot.dto.OrderDetailDto
import com.wrbug.polymarketbot.dto.OrderMessageDto import com.wrbug.polymarketbot.dto.OrderMessageDto
import com.wrbug.polymarketbot.dto.OrderPushMessage import com.wrbug.polymarketbot.dto.OrderPushMessage
@@ -13,6 +14,11 @@ import jakarta.annotation.PreDestroy
import kotlinx.coroutines.* import kotlinx.coroutines.*
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Value import org.springframework.beans.factory.annotation.Value
import com.wrbug.polymarketbot.service.common.PolymarketClobService
import com.wrbug.polymarketbot.util.CryptoUtils
import com.wrbug.polymarketbot.repository.CopyOrderTrackingRepository
import com.wrbug.polymarketbot.repository.CopyTradingRepository
import com.wrbug.polymarketbot.repository.LeaderRepository
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
@@ -26,10 +32,10 @@ class OrderPushService(
private val objectMapper: ObjectMapper, private val objectMapper: ObjectMapper,
private val clobService: PolymarketClobService, private val clobService: PolymarketClobService,
private val retrofitFactory: RetrofitFactory, // 用于创建 Gamma API 客户端(不需要认证) private val retrofitFactory: RetrofitFactory, // 用于创建 Gamma API 客户端(不需要认证)
private val cryptoUtils: com.wrbug.polymarketbot.util.CryptoUtils, private val cryptoUtils: CryptoUtils,
private val copyOrderTrackingRepository: com.wrbug.polymarketbot.repository.CopyOrderTrackingRepository? = null, // 可选,避免循环依赖 private val copyOrderTrackingRepository: CopyOrderTrackingRepository? = null, // 可选,避免循环依赖
private val copyTradingRepository: com.wrbug.polymarketbot.repository.CopyTradingRepository? = null, // 可选,避免循环依赖 private val copyTradingRepository: CopyTradingRepository? = null, // 可选,避免循环依赖
private val leaderRepository: com.wrbug.polymarketbot.repository.LeaderRepository? = null // 可选,避免循环依赖 private val leaderRepository: LeaderRepository? = null // 可选,避免循环依赖
) { ) {
private val logger = LoggerFactory.getLogger(OrderPushService::class.java) private val logger = LoggerFactory.getLogger(OrderPushService::class.java)
@@ -152,9 +158,9 @@ class OrderPushService(
return account.apiKey != null && return account.apiKey != null &&
account.apiSecret != null && account.apiSecret != null &&
account.apiPassphrase != null && account.apiPassphrase != null &&
account.apiKey!!.isNotBlank() && account.apiKey.isNotBlank() &&
account.apiSecret!!.isNotBlank() && account.apiSecret.isNotBlank() &&
account.apiPassphrase!!.isNotBlank() account.apiPassphrase.isNotBlank()
} }
/** /**
@@ -378,7 +384,7 @@ class OrderPushService(
/** /**
* 获取订单详情 * 获取订单详情
* 通过 PolymarketClobService 获取订单详情 * 通过 com.wrbug.polymarketbot.service.common.PolymarketClobService 获取订单详情
*/ */
private suspend fun fetchOrderDetail( private suspend fun fetchOrderDetail(
account: Account, account: Account,
@@ -391,7 +397,7 @@ class OrderPushService(
return null return null
} }
// 通过 PolymarketClobService 获取订单详情(需要 L2 认证) // 通过 com.wrbug.polymarketbot.service.common.PolymarketClobService 获取订单详情(需要 L2 认证)
// 解密 API 凭证 // 解密 API 凭证
val apiSecret = try { val apiSecret = try {
decryptApiSecret(account) decryptApiSecret(account)
@@ -453,7 +459,7 @@ class OrderPushService(
* 使用 /markets 接口通过 condition_ids 查询参数获取市场信息 * 使用 /markets 接口通过 condition_ids 查询参数获取市场信息
* 订单返回的 market 字段是 16 进制的 condition ID "0x..." * 订单返回的 market 字段是 16 进制的 condition ID "0x..."
*/ */
private suspend fun fetchMarketInfo(conditionId: String): com.wrbug.polymarketbot.api.MarketResponse? { private suspend fun fetchMarketInfo(conditionId: String): MarketResponse? {
return try { return try {
// 创建 Gamma API 客户端(公开 API,不需要认证) // 创建 Gamma API 客户端(公开 API,不需要认证)
val gammaApi = retrofitFactory.createGammaApi() val gammaApi = retrofitFactory.createGammaApi()
@@ -1,4 +1,4 @@
package com.wrbug.polymarketbot.service package com.wrbug.polymarketbot.service.copytrading.orders
import com.wrbug.polymarketbot.api.SignedOrderObject import com.wrbug.polymarketbot.api.SignedOrderObject
import com.wrbug.polymarketbot.util.toSafeBigDecimal import com.wrbug.polymarketbot.util.toSafeBigDecimal
@@ -77,10 +77,6 @@ class OrderSigningService {
): OrderAmounts { ): OrderAmounts {
val sizeDecimal = size.toSafeBigDecimal() val sizeDecimal = size.toSafeBigDecimal()
val priceDecimal = price.toSafeBigDecimal() val priceDecimal = price.toSafeBigDecimal()
// 舍入价格
val roundedPrice = roundNormal(priceDecimal, roundConfig.price)
if (side.uppercase() == "BUY") { if (side.uppercase() == "BUY") {
// BUY: makerAmount = price * size (USDC), takerAmount = size (shares) // BUY: makerAmount = price * size (USDC), takerAmount = size (shares)
// makerAmount 是 USDC 金额,最多 2 位小数 // makerAmount 是 USDC 金额,最多 2 位小数
@@ -1,6 +1,7 @@
package com.wrbug.polymarketbot.service package com.wrbug.polymarketbot.service.copytrading.statistics
import com.wrbug.polymarketbot.api.NewOrderRequest import com.wrbug.polymarketbot.api.NewOrderRequest
import com.wrbug.polymarketbot.api.PolymarketClobApi
import com.wrbug.polymarketbot.api.TradeResponse import com.wrbug.polymarketbot.api.TradeResponse
import com.wrbug.polymarketbot.entity.* import com.wrbug.polymarketbot.entity.*
import com.wrbug.polymarketbot.repository.* import com.wrbug.polymarketbot.repository.*
@@ -9,6 +10,11 @@ import com.wrbug.polymarketbot.util.*
import kotlinx.coroutines.* import kotlinx.coroutines.*
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import org.springframework.dao.DataIntegrityViolationException import org.springframework.dao.DataIntegrityViolationException
import com.wrbug.polymarketbot.service.copytrading.configs.CopyTradingFilterService
import com.wrbug.polymarketbot.service.copytrading.orders.OrderSigningService
import com.wrbug.polymarketbot.service.common.BlockchainService
import com.wrbug.polymarketbot.service.system.TelegramNotificationService
import com.wrbug.polymarketbot.util.CryptoUtils
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional import org.springframework.transaction.annotation.Transactional
import java.math.BigDecimal import java.math.BigDecimal
@@ -33,7 +39,7 @@ class CopyOrderTrackingService(
private val orderSigningService: OrderSigningService, private val orderSigningService: OrderSigningService,
private val blockchainService: BlockchainService, private val blockchainService: BlockchainService,
private val retrofitFactory: RetrofitFactory, private val retrofitFactory: RetrofitFactory,
private val cryptoUtils: com.wrbug.polymarketbot.util.CryptoUtils, private val cryptoUtils: CryptoUtils,
private val telegramNotificationService: TelegramNotificationService? = null // 可选,避免循环依赖 private val telegramNotificationService: TelegramNotificationService? = null // 可选,避免循环依赖
) { ) {
@@ -381,7 +387,7 @@ class CopyOrderTrackingService(
side = "BUY", side = "BUY",
price = buyPrice.toString(), price = buyPrice.toString(),
size = finalBuyQuantity.toString(), size = finalBuyQuantity.toString(),
owner = account.apiKey!!, owner = account.apiKey,
copyTradingId = copyTrading.id!!, copyTradingId = copyTrading.id!!,
tradeId = trade.id tradeId = trade.id
) )
@@ -759,7 +765,7 @@ class CopyOrderTrackingService(
// 这样可以快速响应 Leader 的交易,避免订单长期挂单导致价格不匹配 // 这样可以快速响应 Leader 的交易,避免订单长期挂单导致价格不匹配
val orderRequest = NewOrderRequest( val orderRequest = NewOrderRequest(
order = signedOrder, order = signedOrder,
owner = account.apiKey!!, owner = account.apiKey,
orderType = "FAK", // Fill-And-Kill orderType = "FAK", // Fill-And-Kill
deferExec = false deferExec = false
) )
@@ -782,8 +788,8 @@ class CopyOrderTrackingService(
side = "SELL", side = "SELL",
price = sellPrice.toString(), price = sellPrice.toString(),
size = totalMatched.toString(), size = totalMatched.toString(),
owner = account.apiKey!!, owner = account.apiKey,
copyTradingId = copyTrading.id!!, copyTradingId = copyTrading.id,
tradeId = leaderSellTrade.id tradeId = leaderSellTrade.id
) )
@@ -830,11 +836,11 @@ class CopyOrderTrackingService(
val totalRealizedPnl = matchDetails.sumOf { it.realizedPnl.toSafeBigDecimal() } val totalRealizedPnl = matchDetails.sumOf { it.realizedPnl.toSafeBigDecimal() }
val matchRecord = SellMatchRecord( val matchRecord = SellMatchRecord(
copyTradingId = copyTrading.id!!, copyTradingId = copyTrading.id,
sellOrderId = realSellOrderId, // 使用真实订单ID sellOrderId = realSellOrderId, // 使用真实订单ID
leaderSellTradeId = leaderSellTrade.id, leaderSellTradeId = leaderSellTrade.id,
marketId = leaderSellTrade.market, marketId = leaderSellTrade.market,
side = leaderSellTrade.outcomeIndex?.toString() ?: "0", // 使用outcomeIndex作为side(兼容旧数据) side = leaderSellTrade.outcomeIndex.toString(), // 使用outcomeIndex作为side(兼容旧数据)
outcomeIndex = leaderSellTrade.outcomeIndex, // 新增字段 outcomeIndex = leaderSellTrade.outcomeIndex, // 新增字段
totalMatchedQuantity = totalMatched, totalMatchedQuantity = totalMatched,
sellPrice = sellPrice, sellPrice = sellPrice,
@@ -857,7 +863,7 @@ class CopyOrderTrackingService(
* 注意重试时会重新生成salt并重新签名确保每次重试都是新的订单 * 注意重试时会重新生成salt并重新签名确保每次重试都是新的订单
*/ */
private suspend fun createOrderWithRetry( private suspend fun createOrderWithRetry(
clobApi: com.wrbug.polymarketbot.api.PolymarketClobApi, clobApi: PolymarketClobApi,
privateKey: String, privateKey: String,
makerAddress: String, makerAddress: String,
tokenId: String, tokenId: String,
@@ -914,7 +920,7 @@ class CopyOrderTrackingService(
delay(1000) // 重试前等待1秒 delay(1000) // 重试前等待1秒
continue continue
} }
return Result.failure(lastError!!) return Result.failure(lastError)
} }
val response = orderResponse.body()!! val response = orderResponse.body()!!
@@ -928,7 +934,7 @@ class CopyOrderTrackingService(
delay(1000) // 重试前等待1秒 delay(1000) // 重试前等待1秒
continue continue
} }
return Result.failure(lastError!!) return Result.failure(lastError)
} }
// 成功 // 成功
@@ -943,7 +949,7 @@ class CopyOrderTrackingService(
delay(1000) // 重试前等待1秒 delay(1000) // 重试前等待1秒
continue continue
} }
return Result.failure(lastError!!) return Result.failure(lastError)
} }
} }
@@ -1105,7 +1111,7 @@ class CopyOrderTrackingService(
} }
// 2. 检查每日亏损限制(需要计算今日已实现盈亏) // 2. 检查每日亏损限制(需要计算今日已实现盈亏)
val todaySellRecords = sellMatchRecordRepository.findByCopyTradingId(copyTrading.id!!) val todaySellRecords = sellMatchRecordRepository.findByCopyTradingId(copyTrading.id)
.filter { it.createdAt >= todayStart } .filter { it.createdAt >= todayStart }
val todayRealizedPnl = todaySellRecords.sumOf { it.totalRealizedPnl.toSafeBigDecimal() } val todayRealizedPnl = todaySellRecords.sumOf { it.totalRealizedPnl.toSafeBigDecimal() }
@@ -1,4 +1,4 @@
package com.wrbug.polymarketbot.service package com.wrbug.polymarketbot.service.copytrading.statistics
import com.wrbug.polymarketbot.dto.* import com.wrbug.polymarketbot.dto.*
import com.wrbug.polymarketbot.entity.* import com.wrbug.polymarketbot.entity.*
@@ -13,6 +13,8 @@ import org.slf4j.LoggerFactory
import org.springframework.data.domain.PageRequest import org.springframework.data.domain.PageRequest
import org.springframework.data.domain.Pageable import org.springframework.data.domain.Pageable
import org.springframework.data.domain.Sort import org.springframework.data.domain.Sort
import com.wrbug.polymarketbot.service.accounts.AccountService
import com.wrbug.polymarketbot.service.common.BlockchainService
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
import java.math.BigDecimal import java.math.BigDecimal
import java.math.RoundingMode import java.math.RoundingMode
@@ -30,7 +32,7 @@ class CopyTradingStatisticsService(
private val accountRepository: AccountRepository, private val accountRepository: AccountRepository,
private val leaderRepository: LeaderRepository, private val leaderRepository: LeaderRepository,
private val accountService: AccountService, private val accountService: AccountService,
private val blockchainService: com.wrbug.polymarketbot.service.BlockchainService private val blockchainService: BlockchainService
) { ) {
private val logger = LoggerFactory.getLogger(CopyTradingStatisticsService::class.java) private val logger = LoggerFactory.getLogger(CopyTradingStatisticsService::class.java)
@@ -69,7 +71,10 @@ class CopyTradingStatisticsService(
// 9. 计算未实现盈亏(使用链上实际持仓,而不是 remainingQuantity // 9. 计算未实现盈亏(使用链上实际持仓,而不是 remainingQuantity
val unrealizedPnl = calculateUnrealizedPnl(buyOrders, currentPrice, actualPositions) val unrealizedPnl = calculateUnrealizedPnl(buyOrders, currentPrice, actualPositions)
// 9. 构建响应 // 10. 计算持仓价值(使用链上实际持仓和当前价格)
val positionValue = calculatePositionValue(buyOrders, currentPrice, actualPositions)
// 11. 构建响应
val response = CopyTradingStatisticsResponse( val response = CopyTradingStatisticsResponse(
copyTradingId = copyTradingId, copyTradingId = copyTradingId,
accountId = copyTrading.accountId, accountId = copyTrading.accountId,
@@ -85,7 +90,7 @@ class CopyTradingStatisticsService(
totalSellOrders = statistics.totalSellOrders, totalSellOrders = statistics.totalSellOrders,
totalSellAmount = statistics.totalSellAmount, totalSellAmount = statistics.totalSellAmount,
currentPositionQuantity = statistics.currentPositionQuantity, currentPositionQuantity = statistics.currentPositionQuantity,
currentPositionValue = calculatePositionValue(statistics.currentPositionQuantity, currentPrice), currentPositionValue = positionValue,
totalRealizedPnl = statistics.totalRealizedPnl, totalRealizedPnl = statistics.totalRealizedPnl,
totalUnrealizedPnl = unrealizedPnl, totalUnrealizedPnl = unrealizedPnl,
totalPnl = (statistics.totalRealizedPnl.toSafeBigDecimal().add(unrealizedPnl.toSafeBigDecimal())).toString(), totalPnl = (statistics.totalRealizedPnl.toSafeBigDecimal().add(unrealizedPnl.toSafeBigDecimal())).toString(),
@@ -105,7 +110,7 @@ class CopyTradingStatisticsService(
fun getOrderList(request: OrderTrackingRequest): Result<OrderListResponse> { fun getOrderList(request: OrderTrackingRequest): Result<OrderListResponse> {
return try { return try {
// 1. 验证跟单关系 // 1. 验证跟单关系
val copyTrading = copyTradingRepository.findById(request.copyTradingId).orElse(null) copyTradingRepository.findById(request.copyTradingId).orElse(null)
?: return Result.failure(IllegalArgumentException("跟单关系不存在: ${request.copyTradingId}")) ?: return Result.failure(IllegalArgumentException("跟单关系不存在: ${request.copyTradingId}"))
// 2. 根据类型查询 // 2. 根据类型查询
@@ -354,7 +359,7 @@ class CopyTradingStatisticsService(
* 获取链上实际持仓 * 获取链上实际持仓
* (marketId, outcomeIndex) 组合返回实际持仓数量 * (marketId, outcomeIndex) 组合返回实际持仓数量
*/ */
private suspend fun getActualPositions(account: com.wrbug.polymarketbot.entity.Account?): Map<String, BigDecimal> { private suspend fun getActualPositions(account: Account?): Map<String, BigDecimal> {
val positions = mutableMapOf<String, BigDecimal>() val positions = mutableMapOf<String, BigDecimal>()
if (account == null || account.proxyAddress.isBlank()) { if (account == null || account.proxyAddress.isBlank()) {
@@ -424,11 +429,41 @@ class CopyTradingStatisticsService(
/** /**
* 计算持仓价值 * 计算持仓价值
* 使用链上实际持仓数量和当前市场价格计算
*/ */
private fun calculatePositionValue(positionQuantity: String, currentPrices: Map<String, String>): String { private fun calculatePositionValue(
// 这里简化处理,实际应该根据每个市场的持仓分别计算 buyOrders: List<CopyOrderTracking>,
// 暂时返回0,因为需要知道每个市场的持仓数量 currentPrices: Map<String, String>,
return "0" actualPositions: Map<String, BigDecimal>
): String {
var totalPositionValue = BigDecimal.ZERO
for (order in buyOrders) {
// 如果没有 outcomeIndex,跳过(无法确定价格和持仓)
if (order.outcomeIndex == null) {
logger.warn("订单缺少 outcomeIndex,跳过持仓价值计算: orderId=${order.buyOrderId}, marketId=${order.marketId}")
continue
}
// 使用 "marketId:outcomeIndex" 作为 key
val key = "${order.marketId}:${order.outcomeIndex}"
// 获取链上实际持仓数量(如果存在),否则使用 remainingQuantity
val actualQty = actualPositions[key] ?: order.remainingQuantity.toSafeBigDecimal()
// 如果实际持仓 <= 0,说明已全部卖出(包括手动卖出),跳过持仓价值计算
if (actualQty.lte(BigDecimal.ZERO)) continue
// 获取当前市场价格
val currentPrice = currentPrices[key]?.toSafeBigDecimal()
?: continue // 如果没有当前价格,跳过
// 计算持仓价值:持仓数量 × 当前价格
val positionValue = actualQty.multi(currentPrice)
totalPositionValue = totalPositionValue.add(positionValue)
}
return totalPositionValue.toString()
} }
/** /**
@@ -1,4 +1,4 @@
package com.wrbug.polymarketbot.service package com.wrbug.polymarketbot.service.copytrading.templates
import com.wrbug.polymarketbot.dto.* import com.wrbug.polymarketbot.dto.*
import com.wrbug.polymarketbot.entity.CopyTradingTemplate import com.wrbug.polymarketbot.entity.CopyTradingTemplate
@@ -1,4 +1,4 @@
package com.wrbug.polymarketbot.service package com.wrbug.polymarketbot.service.system
import com.wrbug.polymarketbot.dto.ApiHealthCheckDto import com.wrbug.polymarketbot.dto.ApiHealthCheckDto
import com.wrbug.polymarketbot.dto.ApiHealthCheckResponse import com.wrbug.polymarketbot.dto.ApiHealthCheckResponse
@@ -12,6 +12,8 @@ import org.springframework.beans.BeansException
import org.springframework.context.ApplicationContext import org.springframework.context.ApplicationContext
import org.springframework.context.ApplicationContextAware import org.springframework.context.ApplicationContextAware
import org.springframework.beans.factory.annotation.Value import org.springframework.beans.factory.annotation.Value
import com.wrbug.polymarketbot.service.copytrading.orders.OrderPushService
import com.wrbug.polymarketbot.service.copytrading.monitor.CopyTradingWebSocketService
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
@@ -1,4 +1,4 @@
package com.wrbug.polymarketbot.service package com.wrbug.polymarketbot.service.system
import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.databind.ObjectMapper
import com.wrbug.polymarketbot.dto.* import com.wrbug.polymarketbot.dto.*
@@ -1,8 +1,10 @@
package com.wrbug.polymarketbot.service package com.wrbug.polymarketbot.service.system
import com.wrbug.polymarketbot.dto.* import com.wrbug.polymarketbot.dto.*
import com.wrbug.polymarketbot.entity.ProxyConfig import com.wrbug.polymarketbot.entity.ProxyConfig
import com.wrbug.polymarketbot.repository.ProxyConfigRepository import com.wrbug.polymarketbot.repository.ProxyConfigRepository
import com.wrbug.polymarketbot.service.copytrading.monitor.CopyTradingWebSocketService
import com.wrbug.polymarketbot.service.copytrading.orders.OrderPushService
import com.wrbug.polymarketbot.util.ProxyConfigProvider import com.wrbug.polymarketbot.util.ProxyConfigProvider
import com.wrbug.polymarketbot.util.TrustAllHostnameVerifier import com.wrbug.polymarketbot.util.TrustAllHostnameVerifier
import com.wrbug.polymarketbot.util.createSSLSocketFactory import com.wrbug.polymarketbot.util.createSSLSocketFactory
@@ -1,4 +1,4 @@
package com.wrbug.polymarketbot.service package com.wrbug.polymarketbot.service.system
import com.wrbug.polymarketbot.api.BuilderRelayerApi import com.wrbug.polymarketbot.api.BuilderRelayerApi
import com.wrbug.polymarketbot.api.EthereumRpcApi import com.wrbug.polymarketbot.api.EthereumRpcApi
@@ -802,10 +802,10 @@ class RelayClientService(
safeTxs: List<SafeTransaction> safeTxs: List<SafeTransaction>
): Result<String> { ): Result<String> {
// 批量执行:将多个交易合并为一个 execTransaction 调用 // 批量执行:将多个交易合并为一个 execTransaction 调用
// 当前实现:委托给 BlockchainService // 当前实现:委托给 com.wrbug.polymarketbot.service.common.BlockchainService
return Result.failure( return Result.failure(
UnsupportedOperationException( UnsupportedOperationException(
"批量 Gasless 执行暂未实现。请使用 BlockchainService.redeemPositions() 方法。" "批量 Gasless 执行暂未实现。请使用 com.wrbug.polymarketbot.service.common.BlockchainService.redeemPositions() 方法。"
) )
) )
} }
@@ -1,4 +1,4 @@
package com.wrbug.polymarketbot.service package com.wrbug.polymarketbot.service.system
import com.wrbug.polymarketbot.dto.SystemConfigDto import com.wrbug.polymarketbot.dto.SystemConfigDto
import com.wrbug.polymarketbot.dto.SystemConfigUpdateRequest import com.wrbug.polymarketbot.dto.SystemConfigUpdateRequest
@@ -1,7 +1,8 @@
package com.wrbug.polymarketbot.service package com.wrbug.polymarketbot.service.system
import com.fasterxml.jackson.databind.JsonNode import com.fasterxml.jackson.databind.JsonNode
import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.databind.ObjectMapper
import com.wrbug.polymarketbot.api.PolymarketClobApi
import com.wrbug.polymarketbot.dto.NotificationConfigData import com.wrbug.polymarketbot.dto.NotificationConfigData
import com.wrbug.polymarketbot.dto.TelegramConfigData import com.wrbug.polymarketbot.dto.TelegramConfigData
import com.wrbug.polymarketbot.util.createClient import com.wrbug.polymarketbot.util.createClient
@@ -65,7 +66,7 @@ class TelegramNotificationService(
side: String, side: String,
accountName: String? = null, accountName: String? = null,
walletAddress: String? = null, walletAddress: String? = null,
clobApi: com.wrbug.polymarketbot.api.PolymarketClobApi? = null, clobApi: PolymarketClobApi? = null,
apiKey: String? = null, apiKey: String? = null,
apiSecret: String? = null, apiSecret: String? = null,
apiPassphrase: String? = null, apiPassphrase: String? = null,
@@ -1,4 +1,4 @@
package com.wrbug.polymarketbot.service package com.wrbug.polymarketbot.service.system
import com.wrbug.polymarketbot.dto.UserCreateRequest import com.wrbug.polymarketbot.dto.UserCreateRequest
import com.wrbug.polymarketbot.dto.UserDto import com.wrbug.polymarketbot.dto.UserDto
@@ -3,7 +3,7 @@ package com.wrbug.polymarketbot.websocket
import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.databind.ObjectMapper
import com.wrbug.polymarketbot.dto.WebSocketMessage as WsMessage import com.wrbug.polymarketbot.dto.WebSocketMessage as WsMessage
import com.wrbug.polymarketbot.dto.WebSocketMessageType import com.wrbug.polymarketbot.dto.WebSocketMessageType
import com.wrbug.polymarketbot.service.WebSocketSubscriptionService import com.wrbug.polymarketbot.service.common.WebSocketSubscriptionService
import jakarta.annotation.PostConstruct import jakarta.annotation.PostConstruct
import jakarta.annotation.PreDestroy import jakarta.annotation.PreDestroy
import kotlinx.coroutines.* import kotlinx.coroutines.*
+38 -38
View File
@@ -145,31 +145,31 @@ export const apiService = {
* 获取用户列表 * 获取用户列表
*/ */
list: () => list: () =>
apiClient.post<ApiResponse<any[]>>('/users/list', {}), apiClient.post<ApiResponse<any[]>>('/system/users/list', {}),
/** /**
* 创建用户 * 创建用户
*/ */
create: (data: { username: string; password: string }) => create: (data: { username: string; password: string }) =>
apiClient.post<ApiResponse<any>>('/users/create', data), apiClient.post<ApiResponse<any>>('/system/users/create', data),
/** /**
* 更新用户密码 * 更新用户密码
*/ */
updatePassword: (data: { userId: number; newPassword: string }) => updatePassword: (data: { userId: number; newPassword: string }) =>
apiClient.post<ApiResponse<void>>('/users/update-password', data), apiClient.post<ApiResponse<void>>('/system/users/update-password', data),
/** /**
* 删除用户 * 删除用户
*/ */
delete: (data: { userId: number }) => delete: (data: { userId: number }) =>
apiClient.post<ApiResponse<void>>('/users/delete', data), apiClient.post<ApiResponse<void>>('/system/users/delete', data),
/** /**
* 用户修改自己的密码 * 用户修改自己的密码
*/ */
updateOwnPassword: (data: { newPassword: string }) => updateOwnPassword: (data: { newPassword: string }) =>
apiClient.post<ApiResponse<void>>('/users/update-own-password', data) apiClient.post<ApiResponse<void>>('/system/users/update-own-password', data)
}, },
/** /**
@@ -203,61 +203,61 @@ export const apiService = {
* 导入账户 * 导入账户
*/ */
import: (data: any) => import: (data: any) =>
apiClient.post<ApiResponse<any>>('/copy-trading/accounts/import', data), apiClient.post<ApiResponse<any>>('/accounts/import', data),
/** /**
* 更新账户 * 更新账户
*/ */
update: (data: any) => update: (data: any) =>
apiClient.post<ApiResponse<any>>('/copy-trading/accounts/update', data), apiClient.post<ApiResponse<any>>('/accounts/update', data),
/** /**
* 删除账户 * 删除账户
*/ */
delete: (data: { accountId: number }) => delete: (data: { accountId: number }) =>
apiClient.post<ApiResponse<void>>('/copy-trading/accounts/delete', data), apiClient.post<ApiResponse<void>>('/accounts/delete', data),
/** /**
* 查询账户列表 * 查询账户列表
*/ */
list: () => list: () =>
apiClient.post<ApiResponse<any>>('/copy-trading/accounts/list', {}), apiClient.post<ApiResponse<any>>('/accounts/list', {}),
/** /**
* 查询账户详情 * 查询账户详情
*/ */
detail: (data: { accountId?: number }) => detail: (data: { accountId?: number }) =>
apiClient.post<ApiResponse<any>>('/copy-trading/accounts/detail', data), apiClient.post<ApiResponse<any>>('/accounts/detail', data),
/** /**
* 查询账户余额 * 查询账户余额
*/ */
balance: (data: { accountId?: number }) => balance: (data: { accountId?: number }) =>
apiClient.post<ApiResponse<any>>('/copy-trading/accounts/balance', data), apiClient.post<ApiResponse<any>>('/accounts/balance', data),
/** /**
* 查询所有账户的仓位列表 * 查询所有账户的仓位列表
*/ */
positionsList: () => positionsList: () =>
apiClient.post<ApiResponse<any>>('/copy-trading/accounts/positions/list', {}), apiClient.post<ApiResponse<any>>('/accounts/positions/list', {}),
/** /**
* 卖出仓位 * 卖出仓位
*/ */
sellPosition: (data: any) => sellPosition: (data: any) =>
apiClient.post<ApiResponse<any>>('/copy-trading/accounts/positions/sell', data), apiClient.post<ApiResponse<any>>('/accounts/positions/sell', data),
/** /**
* 获取可赎回仓位统计 * 获取可赎回仓位统计
*/ */
getRedeemableSummary: (data: { accountId?: number }) => getRedeemableSummary: (data: { accountId?: number }) =>
apiClient.post<ApiResponse<any>>('/copy-trading/accounts/positions/redeemable-summary', data), apiClient.post<ApiResponse<any>>('/accounts/positions/redeemable-summary', data),
/** /**
* 赎回仓位 * 赎回仓位
*/ */
redeemPositions: (data: any) => redeemPositions: (data: any) =>
apiClient.post<ApiResponse<any>>('/copy-trading/accounts/positions/redeem', data), apiClient.post<ApiResponse<any>>('/accounts/positions/redeem', data),
}, },
@@ -269,13 +269,13 @@ export const apiService = {
* 获取市场价格(通过 Gamma API) * 获取市场价格(通过 Gamma API)
*/ */
getMarketPrice: (data: { marketId: string; outcomeIndex?: number }) => getMarketPrice: (data: { marketId: string; outcomeIndex?: number }) =>
apiClient.post<ApiResponse<any>>('/copy-trading/markets/price', data), apiClient.post<ApiResponse<any>>('/markets/price', data),
/** /**
* 获取最新价(从订单表获取,供前端下单时显示) * 获取最新价(从订单表获取,供前端下单时显示)
*/ */
getLatestPrice: (data: { tokenId: string }) => getLatestPrice: (data: { tokenId: string }) =>
apiClient.post<ApiResponse<any>>('/copy-trading/markets/latest-price', data) apiClient.post<ApiResponse<any>>('/markets/latest-price', data)
}, },
/** /**
@@ -365,37 +365,37 @@ export const apiService = {
* 2. 不提供 templateId:手动输入所有配置参数 * 2. 不提供 templateId:手动输入所有配置参数
*/ */
create: (data: any) => create: (data: any) =>
apiClient.post<ApiResponse<any>>('/copy-trading/create', data), apiClient.post<ApiResponse<any>>('/copy-trading/configs/create', data),
/** /**
* 更新跟单配置 * 更新跟单配置
*/ */
update: (data: any) => update: (data: any) =>
apiClient.post<ApiResponse<any>>('/copy-trading/update', data), apiClient.post<ApiResponse<any>>('/copy-trading/configs/update', data),
/** /**
* 查询跟单列表 * 查询跟单列表
*/ */
list: (data: { accountId?: number; leaderId?: number; enabled?: boolean } = {}) => list: (data: { accountId?: number; leaderId?: number; enabled?: boolean } = {}) =>
apiClient.post<ApiResponse<any>>('/copy-trading/list', data), apiClient.post<ApiResponse<any>>('/copy-trading/configs/list', data),
/** /**
* 更新跟单状态(兼容旧接口) * 更新跟单状态(兼容旧接口)
*/ */
updateStatus: (data: { copyTradingId: number; enabled: boolean }) => updateStatus: (data: { copyTradingId: number; enabled: boolean }) =>
apiClient.post<ApiResponse<any>>('/copy-trading/update-status', data), apiClient.post<ApiResponse<any>>('/copy-trading/configs/update-status', data),
/** /**
* 删除跟单 * 删除跟单
*/ */
delete: (data: { copyTradingId: number }) => delete: (data: { copyTradingId: number }) =>
apiClient.post<ApiResponse<void>>('/copy-trading/delete', data), apiClient.post<ApiResponse<void>>('/copy-trading/configs/delete', data),
/** /**
* 查询钱包绑定的跟单配置(兼容旧接口) * 查询钱包绑定的跟单配置(兼容旧接口)
*/ */
getAccountTemplates: (data: { accountId: number }) => getAccountTemplates: (data: { accountId: number }) =>
apiClient.post<ApiResponse<any>>('/copy-trading/account-templates', data), apiClient.post<ApiResponse<any>>('/copy-trading/configs/account-templates', data),
/** /**
* 查询被过滤订单列表 * 查询被过滤订单列表
@@ -408,7 +408,7 @@ export const apiService = {
startTime?: number startTime?: number
endTime?: number endTime?: number
}) => }) =>
apiClient.post<ApiResponse<any>>('/copy-trading/filtered-orders', data) apiClient.post<ApiResponse<any>>('/copy-trading/configs/filtered-orders', data)
}, },
/** /**
@@ -476,13 +476,13 @@ export const apiService = {
* 获取当前代理配置 * 获取当前代理配置
*/ */
get: () => get: () =>
apiClient.post<ApiResponse<any>>('/proxy-config/get', {}), apiClient.post<ApiResponse<any>>('/system/proxy/get', {}),
/** /**
* 获取所有代理配置 * 获取所有代理配置
*/ */
list: () => list: () =>
apiClient.post<ApiResponse<any[]>>('/proxy-config/list', {}), apiClient.post<ApiResponse<any[]>>('/system/proxy/list', {}),
/** /**
* 保存 HTTP 代理配置 * 保存 HTTP 代理配置
@@ -494,7 +494,7 @@ export const apiService = {
username?: string username?: string
password?: string password?: string
}) => }) =>
apiClient.post<ApiResponse<any>>('/proxy-config/http/save', data), apiClient.post<ApiResponse<any>>('/system/proxy/http/save', data),
/** /**
* 检查代理是否可用 * 检查代理是否可用
@@ -504,13 +504,13 @@ export const apiService = {
success: boolean success: boolean
message: string message: string
responseTime?: number responseTime?: number
}>>('/proxy-config/check', {}), }>>('/system/proxy/check', {}),
/** /**
* 删除代理配置 * 删除代理配置
*/ */
delete: (data: { id: number }) => delete: (data: { id: number }) =>
apiClient.post<ApiResponse<void>>('/proxy-config/delete', data), apiClient.post<ApiResponse<void>>('/system/proxy/delete', data),
/** /**
* 检查所有 API 的健康状态 * 检查所有 API 的健康状态
@@ -524,7 +524,7 @@ export const apiService = {
message: string message: string
responseTime?: number responseTime?: number
}> }>
}>>('/proxy-config/api-health-check', {}) }>>('/system/proxy/api-health-check', {})
}, },
/** /**
@@ -535,49 +535,49 @@ export const apiService = {
* 获取配置列表 * 获取配置列表
*/ */
list: (data?: { type?: string }) => list: (data?: { type?: string }) =>
apiClient.post<ApiResponse<NotificationConfig[]>>('/notifications/configs/list', data || {}), apiClient.post<ApiResponse<NotificationConfig[]>>('/system/notifications/configs/list', data || {}),
/** /**
* 获取配置详情 * 获取配置详情
*/ */
detail: (data: { id: number }) => detail: (data: { id: number }) =>
apiClient.post<ApiResponse<NotificationConfig>>('/notifications/configs/detail', data), apiClient.post<ApiResponse<NotificationConfig>>('/system/notifications/configs/detail', data),
/** /**
* 创建配置 * 创建配置
*/ */
create: (data: NotificationConfigRequest) => create: (data: NotificationConfigRequest) =>
apiClient.post<ApiResponse<NotificationConfig>>('/notifications/configs/create', data), apiClient.post<ApiResponse<NotificationConfig>>('/system/notifications/configs/create', data),
/** /**
* 更新配置 * 更新配置
*/ */
update: (data: NotificationConfigUpdateRequest) => update: (data: NotificationConfigUpdateRequest) =>
apiClient.post<ApiResponse<NotificationConfig>>('/notifications/configs/update', data), apiClient.post<ApiResponse<NotificationConfig>>('/system/notifications/configs/update', data),
/** /**
* 更新启用状态 * 更新启用状态
*/ */
updateEnabled: (data: { id: number; enabled: boolean }) => updateEnabled: (data: { id: number; enabled: boolean }) =>
apiClient.post<ApiResponse<NotificationConfig>>('/notifications/configs/update-enabled', data), apiClient.post<ApiResponse<NotificationConfig>>('/system/notifications/configs/update-enabled', data),
/** /**
* 删除配置 * 删除配置
*/ */
delete: (data: { id: number }) => delete: (data: { id: number }) =>
apiClient.post<ApiResponse<void>>('/notifications/configs/delete', data), apiClient.post<ApiResponse<void>>('/system/notifications/configs/delete', data),
/** /**
* 测试通知 * 测试通知
*/ */
test: (data?: { message?: string }) => test: (data?: { message?: string }) =>
apiClient.post<ApiResponse<boolean>>('/notifications/test', data || {}), apiClient.post<ApiResponse<boolean>>('/system/notifications/test', data || {}),
/** /**
* 获取 Telegram Chat IDs * 获取 Telegram Chat IDs
*/ */
getTelegramChatIds: (data: { botToken: string }) => getTelegramChatIds: (data: { botToken: string }) =>
apiClient.post<ApiResponse<string[]>>('/notifications/telegram/get-chat-ids', data) apiClient.post<ApiResponse<string[]>>('/system/notifications/telegram/get-chat-ids', data)
}, },
/** /**