refactor: 统一使用 MarketService 并改用 LRU 缓存

主要改进:
1. 将 MarketService 缓存从 ConcurrentHashMap 改为 Caffeine LRU 缓存
   - 最多缓存 200 条市场记录
   - 自动淘汰最近最少使用的记录
   - 提高缓存命中率和性能

2. 添加 Caffeine 依赖 (com.github.ben-manes.caffeine:caffeine:3.1.8)

3. 重构以下服务,统一使用 MarketService 获取市场信息:
   - OrderPushService: 删除 fetchMarketInfo() 方法,直接使用 MarketService
   - CopyOrderTrackingService: 移除直接调用 Gamma API 的代码
   - AccountService: 订单通知中使用 MarketService
   - OrderStatusUpdateService: 买入/卖出订单通知使用 MarketService

优势:
- 减少重复的 API 调用,利用三级缓存(内存 → 数据库 → API)
- 提高代码一致性和可维护性
- 统一市场数据获取逻辑,便于后续扩展
This commit is contained in:
WrBug
2026-01-08 18:33:49 +08:00
parent 9ed5190bfe
commit b13aef944c
6 changed files with 71 additions and 193 deletions
@@ -11,6 +11,7 @@ import com.wrbug.polymarketbot.util.JsonUtils
import com.wrbug.polymarketbot.util.getEventSlug
import com.wrbug.polymarketbot.service.common.PolymarketClobService
import com.wrbug.polymarketbot.service.common.BlockchainService
import com.wrbug.polymarketbot.service.common.MarketService
import com.wrbug.polymarketbot.service.common.PolymarketApiKeyService
import com.wrbug.polymarketbot.service.copytrading.orders.OrderPushService
import com.wrbug.polymarketbot.service.copytrading.orders.OrderSigningService
@@ -37,6 +38,7 @@ class AccountService(
private val orderPushService: OrderPushService,
private val orderSigningService: OrderSigningService,
private val cryptoUtils: CryptoUtils,
private val marketService: MarketService, // 市场信息服务
private val telegramNotificationService: TelegramNotificationService? = null, // 可选,避免循环依赖
private val relayClientService: RelayClientService,
private val jsonUtils: JsonUtils
@@ -935,35 +937,22 @@ class AccountService(
notificationScope.launch {
try {
// 获取市场信息(标题和slug
val marketInfo = withContext(Dispatchers.IO) {
try {
val gammaApi = retrofitFactory.createGammaApi()
val marketResponse = gammaApi.listMarkets(conditionIds = listOf(request.marketId))
if (marketResponse.isSuccessful && marketResponse.body() != null) {
marketResponse.body()!!.firstOrNull()
} else {
null
}
} catch (e: Exception) {
logger.warn("获取市场信息失败: ${e.message}", e)
null
}
}
val marketTitle = marketInfo?.question ?: request.marketId
val market = marketService.getMarket(request.marketId)
val marketTitle = market?.title ?: request.marketId
val marketSlug = market?.eventSlug // 跳转用的 slug
// 获取当前语言设置(从 LocaleContextHolder
val locale = try {
org.springframework.context.i18n.LocaleContextHolder.getLocale()
} catch (e: Exception) {
java.util.Locale("zh", "CN") // 默认简体中文
}
telegramNotificationService?.sendOrderSuccessNotification(
orderId = orderId,
marketTitle = marketTitle,
marketId = request.marketId,
marketSlug = marketInfo.getEventSlug(), // 跳转用的 slug
marketSlug = marketSlug,
side = request.side,
price = sellPrice, // 直接传递卖出价格
size = sellQuantity.toPlainString(), // 直接传递卖出数量
@@ -1002,34 +991,21 @@ class AccountService(
notificationScope.launch {
try {
// 获取市场信息(标题和slug
val marketInfo = withContext(Dispatchers.IO) {
try {
val gammaApi = retrofitFactory.createGammaApi()
val marketResponse = gammaApi.listMarkets(conditionIds = listOf(request.marketId))
if (marketResponse.isSuccessful && marketResponse.body() != null) {
marketResponse.body()!!.firstOrNull()
} else {
null
}
} catch (e: Exception) {
logger.warn("获取市场信息失败: ${e.message}", e)
null
}
}
val marketTitle = marketInfo?.question ?: request.marketId
val market = marketService.getMarket(request.marketId)
val marketTitle = market?.title ?: request.marketId
val marketSlug = market?.eventSlug // 跳转用的 slug
// 获取当前语言设置(从 LocaleContextHolder
val locale = try {
org.springframework.context.i18n.LocaleContextHolder.getLocale()
} catch (e: Exception) {
java.util.Locale("zh", "CN") // 默认简体中文
}
telegramNotificationService?.sendOrderFailureNotification(
marketTitle = marketTitle,
marketId = request.marketId,
marketSlug = marketInfo.getEventSlug(), // 跳转用的 slug
marketSlug = marketSlug,
side = request.side,
outcome = null, // 失败时可能没有 outcome
price = if (request.orderType == "LIMIT") sellPrice.toString() else "MARKET",
@@ -1059,37 +1035,24 @@ class AccountService(
notificationScope.launch {
try {
// 获取市场信息(标题和slug
val marketInfo = withContext(Dispatchers.IO) {
try {
val gammaApi = retrofitFactory.createGammaApi()
val marketResponse = gammaApi.listMarkets(conditionIds = listOf(request.marketId))
if (marketResponse.isSuccessful && marketResponse.body() != null) {
marketResponse.body()!!.firstOrNull()
} else {
null
}
} catch (e: Exception) {
logger.warn("获取市场信息失败: ${e.message}", e)
null
}
}
val marketTitle = marketInfo?.question ?: request.marketId
val market = marketService.getMarket(request.marketId)
val marketTitle = market?.title ?: request.marketId
val marketSlug = market?.eventSlug // 跳转用的 slug
// 获取当前语言设置(从 LocaleContextHolder
val locale = try {
org.springframework.context.i18n.LocaleContextHolder.getLocale()
} catch (e: Exception) {
java.util.Locale("zh", "CN") // 默认简体中文
}
// 只传递后端返回的 msg,不传递完整堆栈
val errorMsg = orderResponse.body()?.errorMsg ?: "创建订单失败"
telegramNotificationService?.sendOrderFailureNotification(
marketTitle = marketTitle,
marketId = request.marketId,
marketSlug = marketInfo.getEventSlug(), // 跳转用的 slug
marketSlug = marketSlug,
side = request.side,
outcome = null, // 失败时可能没有 outcome
price = if (request.orderType == "LIMIT") sellPrice.toString() else "MARKET",
@@ -1,5 +1,7 @@
package com.wrbug.polymarketbot.service.common
import com.github.benmanes.caffeine.cache.Cache
import com.github.benmanes.caffeine.cache.Caffeine
import com.wrbug.polymarketbot.api.MarketResponse
import com.wrbug.polymarketbot.api.PolymarketGammaApi
import com.wrbug.polymarketbot.entity.Market
@@ -9,7 +11,6 @@ import com.wrbug.polymarketbot.util.getEventSlug
import kotlinx.coroutines.runBlocking
import org.slf4j.LoggerFactory
import org.springframework.stereotype.Service
import java.util.concurrent.ConcurrentHashMap
/**
* 市场信息服务
@@ -20,11 +21,13 @@ class MarketService(
val marketRepository: MarketRepository, // 改为 public,供 MarketPollingService 使用
private val retrofitFactory: RetrofitFactory
) {
private val logger = LoggerFactory.getLogger(MarketService::class.java)
// 内存缓存(避免频繁查询数据库)
private val marketCache = ConcurrentHashMap<String, Market>()
// LRU 缓存(避免频繁查询数据库),最多缓存 200 条记录
private val marketCache: Cache<String, Market> = Caffeine.newBuilder()
.maximumSize(200) // 最多缓存 200 条记录
.build()
/**
* 根据市场ID获取市场信息
@@ -32,15 +35,15 @@ class MarketService(
*/
fun getMarket(marketId: String): Market? {
// 1. 从缓存获取
marketCache[marketId]?.let { return it }
marketCache.getIfPresent(marketId)?.let { return it }
// 2. 从数据库查询
val market = marketRepository.findByMarketId(marketId)
if (market != null) {
marketCache[marketId] = market
marketCache.put(marketId, market)
return market
}
// 3. 从API获取(异步,不阻塞)
runBlocking {
try {
@@ -49,10 +52,10 @@ class MarketService(
logger.warn("获取市场信息失败: marketId=$marketId, error=${e.message}")
}
}
// 再次从数据库查询(API可能已经保存)
return marketRepository.findByMarketId(marketId)?.also {
marketCache[marketId] = it
marketCache.put(marketId, it)
}
}
@@ -87,7 +90,7 @@ class MarketService(
val savedMarkets = marketRepository.findByMarketIdIn(missingIds)
for (market in savedMarkets) {
result[market.marketId] = market
marketCache[market.marketId] = market
marketCache.put(market.marketId, market)
}
}
@@ -192,7 +195,7 @@ class MarketService(
}
val savedMarket = marketRepository.save(market)
marketCache[marketId] = savedMarket
marketCache.put(marketId, savedMarket)
savedMarket
} catch (e: Exception) {
logger.error("保存市场信息失败: marketId=$marketId, error=${e.message}", e)
@@ -204,7 +207,7 @@ class MarketService(
* 清除缓存(用于测试或手动刷新)
*/
fun clearCache() {
marketCache.clear()
marketCache.invalidateAll()
}
}
@@ -1,9 +1,7 @@
package com.wrbug.polymarketbot.service.copytrading.orders
import com.fasterxml.jackson.databind.ObjectMapper
import com.wrbug.polymarketbot.api.MarketResponse
import com.wrbug.polymarketbot.dto.OrderDetailDto
import com.wrbug.polymarketbot.util.getEventSlug
import com.wrbug.polymarketbot.dto.OrderMessageDto
import com.wrbug.polymarketbot.dto.OrderPushMessage
import com.wrbug.polymarketbot.entity.Account
@@ -20,6 +18,7 @@ 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 com.wrbug.polymarketbot.service.common.MarketService
import org.springframework.stereotype.Service
import java.util.concurrent.ConcurrentHashMap
@@ -36,7 +35,8 @@ class OrderPushService(
private val cryptoUtils: CryptoUtils,
private val copyOrderTrackingRepository: CopyOrderTrackingRepository? = null, // 可选,避免循环依赖
private val copyTradingRepository: CopyTradingRepository? = null, // 可选,避免循环依赖
private val leaderRepository: LeaderRepository? = null // 可选,避免循环依赖
private val leaderRepository: LeaderRepository? = null, // 可选,避免循环依赖
private val marketService: MarketService // 市场信息服务
) {
private val logger = LoggerFactory.getLogger(OrderPushService::class.java)
@@ -423,8 +423,8 @@ class OrderPushService(
result.fold(
onSuccess = { openOrder ->
// 获取市场信息(通过 Gamma API
val marketInfo = fetchMarketInfo(conditionId ?: openOrder.market)
// 获取市场信息(使用 MarketService,优先从数据库/缓存获取
val market = marketService.getMarket(conditionId ?: openOrder.market)
// 转换为 DTO
// 注意:createdAt 是 unix timestamp (Long),需要转换为字符串
@@ -437,9 +437,9 @@ class OrderPushService(
filled = openOrder.sizeMatched, // 使用 size_matched
status = openOrder.status,
createdAt = openOrder.createdAt.toString(), // unix timestamp 转换为字符串
marketName = marketInfo?.question,
marketSlug = marketInfo?.slug, // 显示用的 slug
marketIcon = marketInfo?.icon
marketName = market?.title,
marketSlug = market?.slug, // 显示用的 slug
marketIcon = market?.icon
)
},
onFailure = { e ->
@@ -453,40 +453,6 @@ class OrderPushService(
}
}
/**
* 获取市场信息(通过 Gamma API)
* 文档: https://docs.polymarket.com/api-reference/markets/list-markets
*
* 使用 /markets 接口,通过 condition_ids 查询参数获取市场信息
* 订单返回的 market 字段是 16 进制的 condition ID(如 "0x..."
*/
private suspend fun fetchMarketInfo(conditionId: String): MarketResponse? {
return try {
// 创建 Gamma API 客户端(公开 API,不需要认证)
val gammaApi = retrofitFactory.createGammaApi()
// 调用 Gamma API 获取市场信息
// 使用 /markets 接口,通过 condition_ids 查询参数
val response = gammaApi.listMarkets(
conditionIds = listOf(conditionId),
includeTag = null
)
if (response.isSuccessful && response.body() != null) {
val markets = response.body()!!
if (markets.isNotEmpty()) {
val market = markets.first()
return market
} else {
return null
}
} else {
null
}
} catch (e: Exception) {
null
}
}
/**
* 订阅账户的订单推送(保留用于向后兼容)
*/
@@ -7,7 +7,6 @@ import com.wrbug.polymarketbot.entity.*
import com.wrbug.polymarketbot.repository.*
import com.wrbug.polymarketbot.util.RetrofitFactory
import com.wrbug.polymarketbot.util.*
import com.wrbug.polymarketbot.util.getEventSlug
import kotlinx.coroutines.*
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
@@ -20,6 +19,7 @@ import com.wrbug.polymarketbot.service.copytrading.configs.CopyTradingFilterServ
import com.wrbug.polymarketbot.service.copytrading.configs.FilterStatus
import com.wrbug.polymarketbot.service.copytrading.orders.OrderSigningService
import com.wrbug.polymarketbot.service.common.BlockchainService
import com.wrbug.polymarketbot.service.common.MarketService
import com.wrbug.polymarketbot.service.common.PolymarketClobService
import com.wrbug.polymarketbot.service.system.TelegramNotificationService
import com.wrbug.polymarketbot.util.CryptoUtils
@@ -49,6 +49,7 @@ open class CopyOrderTrackingService(
private val clobService: PolymarketClobService,
private val retrofitFactory: RetrofitFactory,
private val cryptoUtils: CryptoUtils,
private val marketService: MarketService, // 市场信息服务
private val telegramNotificationService: TelegramNotificationService? = null // 可选,避免循环依赖
) {
@@ -269,11 +270,8 @@ open class CopyOrderTrackingService(
var marketTitle: String? = null
if (copyTrading.keywordFilterMode != null && copyTrading.keywordFilterMode != "DISABLED") {
try {
val gammaApi = retrofitFactory.createGammaApi()
val marketResponse = gammaApi.listMarkets(conditionIds = listOf(trade.market))
if (marketResponse.isSuccessful && marketResponse.body() != null) {
marketTitle = marketResponse.body()!!.firstOrNull()?.question
}
val market = marketService.getMarket(trade.market)
marketTitle = market?.title
} catch (e: Exception) {
logger.warn("获取市场标题失败(关键字过滤需要): ${e.message}", e)
}
@@ -300,23 +298,9 @@ open class CopyOrderTrackingService(
notificationScope.launch {
try {
// 获取市场信息(标题和slug
val marketInfo = withContext(Dispatchers.IO) {
try {
val gammaApi = retrofitFactory.createGammaApi()
val marketResponse = gammaApi.listMarkets(conditionIds = listOf(trade.market))
if (marketResponse.isSuccessful && marketResponse.body() != null) {
marketResponse.body()!!.firstOrNull()
} else {
null
}
} catch (e: Exception) {
logger.warn("获取市场信息失败: ${e.message}", e)
null
}
}
val marketTitle = marketInfo?.question ?: trade.market
val marketSlug = marketInfo?.slug // 显示用的 slug
val market = marketService.getMarket(trade.market)
val marketTitle = market?.title ?: trade.market
val marketSlug = market?.slug // 显示用的 slug
// 从过滤结果中提取 filterType
val filterType = extractFilterType(filterResult.status, filterResult.reason)
@@ -365,7 +349,7 @@ open class CopyOrderTrackingService(
telegramNotificationService?.sendOrderFilteredNotification(
marketTitle = marketTitle,
marketId = trade.market,
marketSlug = marketInfo.getEventSlug(), // 跳转用的 slug
marketSlug = marketSlug,
side = "BUY",
outcome = trade.outcome,
price = trade.price,
@@ -571,23 +555,9 @@ open class CopyOrderTrackingService(
notificationScope.launch {
try {
// 获取市场信息(标题和slug
val marketInfo = withContext(Dispatchers.IO) {
try {
val gammaApi = retrofitFactory.createGammaApi()
val marketResponse =
gammaApi.listMarkets(conditionIds = listOf(trade.market))
if (marketResponse.isSuccessful && marketResponse.body() != null) {
marketResponse.body()!!.firstOrNull()
} else {
null
}
} catch (e: Exception) {
logger.warn("获取市场信息失败: ${e.message}", e)
null
}
}
val marketTitle = marketInfo?.question ?: trade.market
val market = marketService.getMarket(trade.market)
val marketTitle = market?.title ?: trade.market
val marketSlug = market?.eventSlug // 跳转用的 slug
// 获取当前语言设置(从 LocaleContextHolder
val locale = try {
@@ -599,7 +569,7 @@ open class CopyOrderTrackingService(
telegramNotificationService?.sendOrderFailureNotification(
marketTitle = marketTitle,
marketId = trade.market,
marketSlug = marketInfo.getEventSlug(), // 跳转用的 slug
marketSlug = marketSlug,
side = "BUY",
outcome = null, // 失败时可能没有 outcome
price = buyPrice.toString(),
@@ -3,12 +3,12 @@ package com.wrbug.polymarketbot.service.copytrading.statistics
import com.wrbug.polymarketbot.api.PolymarketClobApi
import com.wrbug.polymarketbot.entity.*
import com.wrbug.polymarketbot.repository.*
import com.wrbug.polymarketbot.service.common.MarketService
import com.wrbug.polymarketbot.service.system.TelegramNotificationService
import com.wrbug.polymarketbot.util.RetrofitFactory
import com.wrbug.polymarketbot.util.CryptoUtils
import com.wrbug.polymarketbot.util.toSafeBigDecimal
import com.wrbug.polymarketbot.util.multi
import com.wrbug.polymarketbot.util.getEventSlug
import kotlinx.coroutines.*
import org.slf4j.LoggerFactory
import org.springframework.boot.context.event.ApplicationReadyEvent
@@ -34,6 +34,7 @@ class OrderStatusUpdateService(
private val retrofitFactory: RetrofitFactory,
private val cryptoUtils: CryptoUtils,
private val trackingService: CopyOrderTrackingService,
private val marketService: MarketService, // 市场信息服务
private val telegramNotificationService: TelegramNotificationService?
) {
@@ -713,24 +714,10 @@ class OrderStatusUpdateService(
logger.warn("账户不存在,跳过发送通知: accountId=${order.accountId}")
return
}
// 获取市场信息
val marketInfo = withContext(Dispatchers.IO) {
try {
val gammaApi = retrofitFactory.createGammaApi()
val marketResponse = gammaApi.listMarkets(conditionIds = listOf(order.marketId))
if (marketResponse.isSuccessful && marketResponse.body() != null) {
marketResponse.body()!!.firstOrNull()
} else {
null
}
} catch (e: Exception) {
logger.warn("获取市场信息失败: ${e.message}", e)
null
}
}
val marketTitle = marketInfo?.question ?: order.marketId
val market = marketService.getMarket(order.marketId)
val marketTitle = market?.title ?: order.marketId
// 获取 Leader 和跟单配置信息
val leader = leaderRepository.findById(order.leaderId).orElse(null)
@@ -761,7 +748,7 @@ class OrderStatusUpdateService(
orderId = order.buyOrderId,
marketTitle = marketTitle,
marketId = order.marketId,
marketSlug = marketInfo.getEventSlug(), // 跳转用的 slug
marketSlug = market?.eventSlug, // 跳转用的 slug
side = "BUY",
price = actualPrice ?: order.price.toString(), // 使用实际价格或临时价格
size = actualSize ?: order.quantity.toString(), // 使用实际数量或临时数量
@@ -817,24 +804,10 @@ class OrderStatusUpdateService(
logger.warn("账户不存在,跳过发送通知: accountId=${finalCopyTrading.accountId}")
return
}
// 获取市场信息
val marketInfo = withContext(Dispatchers.IO) {
try {
val gammaApi = retrofitFactory.createGammaApi()
val marketResponse = gammaApi.listMarkets(conditionIds = listOf(record.marketId))
if (marketResponse.isSuccessful && marketResponse.body() != null) {
marketResponse.body()!!.firstOrNull()
} else {
null
}
} catch (e: Exception) {
logger.warn("获取市场信息失败: ${e.message}", e)
null
}
}
val marketTitle = marketInfo?.question ?: record.marketId
val market = marketService.getMarket(record.marketId)
val marketTitle = market?.title ?: record.marketId
// 获取 Leader 和跟单配置信息
val leader = leaderRepository.findById(finalCopyTrading.leaderId).orElse(null)
@@ -865,7 +838,7 @@ class OrderStatusUpdateService(
orderId = record.sellOrderId,
marketTitle = marketTitle,
marketId = record.marketId,
marketSlug = marketInfo.getEventSlug(), // 跳转用的 slug
marketSlug = market?.eventSlug, // 跳转用的 slug
side = "SELL",
price = actualPrice ?: record.sellPrice.toString(), // 使用实际价格或临时价格
size = actualSize ?: record.totalMatchedQuantity.toString(), // 使用实际数量或临时数量