diff --git a/backend/src/main/kotlin/com/wrbug/polymarketbot/config/GsonConfig.kt b/backend/src/main/kotlin/com/wrbug/polymarketbot/config/GsonConfig.kt new file mode 100644 index 0000000..e8ab03c --- /dev/null +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/config/GsonConfig.kt @@ -0,0 +1,26 @@ +package com.wrbug.polymarketbot.config + +import com.google.gson.Gson +import com.google.gson.GsonBuilder +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration + +/** + * Gson 配置类 + * 统一配置 Gson 实例,使用 lenient 模式允许解析格式不严格的 JSON + */ +@Configuration +class GsonConfig { + + /** + * 创建 Gson Bean + * 使用 lenient 模式,允许解析格式不严格的 JSON + */ + @Bean + fun gson(): Gson { + return GsonBuilder() + .setLenient() + .create() + } +} + diff --git a/backend/src/main/kotlin/com/wrbug/polymarketbot/config/RetrofitConfig.kt b/backend/src/main/kotlin/com/wrbug/polymarketbot/config/RetrofitConfig.kt index a4e23be..514964d 100644 --- a/backend/src/main/kotlin/com/wrbug/polymarketbot/config/RetrofitConfig.kt +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/config/RetrofitConfig.kt @@ -1,5 +1,6 @@ package com.wrbug.polymarketbot.config +import com.google.gson.Gson import com.wrbug.polymarketbot.api.PolymarketClobApi import com.wrbug.polymarketbot.util.createClient import org.springframework.beans.factory.annotation.Value @@ -18,7 +19,9 @@ import retrofit2.converter.gson.GsonConverterFactory * - 账户 API Key 在调用时动态设置,不在此处配置 */ @Configuration -class RetrofitConfig { +class RetrofitConfig( + private val gson: Gson +) { @Value("\${polymarket.clob.base-url}") private lateinit var clobBaseUrl: String @@ -37,7 +40,7 @@ class RetrofitConfig { return Retrofit.Builder() .baseUrl(clobBaseUrl) .client(okHttpClient) - .addConverterFactory(GsonConverterFactory.create()) + .addConverterFactory(GsonConverterFactory.create(gson)) .build() .create(PolymarketClobApi::class.java) } diff --git a/backend/src/main/kotlin/com/wrbug/polymarketbot/service/accounts/AccountService.kt b/backend/src/main/kotlin/com/wrbug/polymarketbot/service/accounts/AccountService.kt index 2675d1d..d82607c 100644 --- a/backend/src/main/kotlin/com/wrbug/polymarketbot/service/accounts/AccountService.kt +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/service/accounts/AccountService.kt @@ -37,7 +37,8 @@ class AccountService( private val orderSigningService: OrderSigningService, private val cryptoUtils: CryptoUtils, private val telegramNotificationService: TelegramNotificationService? = null, // 可选,避免循环依赖 - private val relayClientService: RelayClientService + private val relayClientService: RelayClientService, + private val jsonUtils: JsonUtils ) { private val logger = LoggerFactory.getLogger(AccountService::class.java) @@ -1121,7 +1122,7 @@ class AccountService( // 如果目标 outcome 不是第一个(index != 0),需要转换价格 // 对于二元市场:第二个 outcome 的价格 = 1 - 第一个 outcome 的价格 if (outcomeIndex != null && outcomeIndex > 0) { - val outcomes = JsonUtils.parseStringArray(market.outcomes) + val outcomes = jsonUtils.parseStringArray(market.outcomes) // 只对二元市场进行价格转换 if (outcomes.size == 2) { // 保存原始第一个 outcome 的价格 diff --git a/backend/src/main/kotlin/com/wrbug/polymarketbot/service/accounts/PositionCheckService.kt b/backend/src/main/kotlin/com/wrbug/polymarketbot/service/accounts/PositionCheckService.kt index da03f0e..faee608 100644 --- a/backend/src/main/kotlin/com/wrbug/polymarketbot/service/accounts/PositionCheckService.kt +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/service/accounts/PositionCheckService.kt @@ -48,7 +48,8 @@ class PositionCheckService( private val accountRepository: AccountRepository, private val messageSource: MessageSource, private val retrofitFactory: RetrofitFactory, - private val blockchainService: BlockchainService + private val blockchainService: BlockchainService, + private val jsonUtils: JsonUtils ) { private val logger = LoggerFactory.getLogger(PositionCheckService::class.java) @@ -582,7 +583,7 @@ class PositionCheckService( // 优先使用 outcomePrices(结算价格数组) val outcomePrices = market.outcomePrices if (outcomePrices != null && outcomePrices.isNotBlank()) { - val prices = JsonUtils.parseStringArray(outcomePrices) + val prices = jsonUtils.parseStringArray(outcomePrices) if (outcomeIndex < prices.size) { val price = prices[outcomeIndex].toSafeBigDecimal() // 如果价格 >= 0.99,认为赢了 diff --git a/backend/src/main/kotlin/com/wrbug/polymarketbot/service/common/BlockchainService.kt b/backend/src/main/kotlin/com/wrbug/polymarketbot/service/common/BlockchainService.kt index e400398..cda8d2b 100644 --- a/backend/src/main/kotlin/com/wrbug/polymarketbot/service/common/BlockchainService.kt +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/service/common/BlockchainService.kt @@ -1,5 +1,6 @@ package com.wrbug.polymarketbot.service.common +import com.google.gson.Gson import com.wrbug.polymarketbot.api.EthereumRpcApi import com.wrbug.polymarketbot.api.JsonRpcRequest import com.wrbug.polymarketbot.api.JsonRpcResponse @@ -29,7 +30,8 @@ class BlockchainService( private val dataApiBaseUrl: String, private val retrofitFactory: RetrofitFactory, private val relayClientService: RelayClientService, - private val rpcNodeService: RpcNodeService + private val rpcNodeService: RpcNodeService, + private val gson: Gson ) { private val logger = LoggerFactory.getLogger(BlockchainService::class.java) @@ -64,7 +66,7 @@ class BlockchainService( Retrofit.Builder() .baseUrl("$baseUrl/") .client(okHttpClient) - .addConverterFactory(GsonConverterFactory.create()) + .addConverterFactory(GsonConverterFactory.create(gson)) .build() .create(PolymarketDataApi::class.java) } diff --git a/backend/src/main/kotlin/com/wrbug/polymarketbot/service/common/PolymarketApiKeyService.kt b/backend/src/main/kotlin/com/wrbug/polymarketbot/service/common/PolymarketApiKeyService.kt index 339634c..d8f0e95 100644 --- a/backend/src/main/kotlin/com/wrbug/polymarketbot/service/common/PolymarketApiKeyService.kt +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/service/common/PolymarketApiKeyService.kt @@ -1,5 +1,6 @@ package com.wrbug.polymarketbot.service.common +import com.google.gson.Gson import com.wrbug.polymarketbot.api.ApiKeyResponse import com.wrbug.polymarketbot.api.PolymarketClobApi import com.wrbug.polymarketbot.util.PolymarketL1AuthInterceptor @@ -19,7 +20,8 @@ import retrofit2.converter.gson.GsonConverterFactory @Service class PolymarketApiKeyService( @Value("\${polymarket.clob.base-url}") - private val clobBaseUrl: String + private val clobBaseUrl: String, + private val gson: Gson ) { private val logger = LoggerFactory.getLogger(PolymarketApiKeyService::class.java) @@ -222,7 +224,7 @@ class PolymarketApiKeyService( return Retrofit.Builder() .baseUrl(clobBaseUrl) .client(okHttpClient) - .addConverterFactory(GsonConverterFactory.create()) + .addConverterFactory(GsonConverterFactory.create(gson)) .build() .create(PolymarketClobApi::class.java) } @@ -236,7 +238,7 @@ class PolymarketApiKeyService( return Retrofit.Builder() .baseUrl(clobBaseUrl) .client(okHttpClient) - .addConverterFactory(GsonConverterFactory.create()) + .addConverterFactory(GsonConverterFactory.create(gson)) .build() .create(PolymarketClobApi::class.java) } diff --git a/backend/src/main/kotlin/com/wrbug/polymarketbot/service/copytrading/monitor/CopyTradingWebSocketService.kt b/backend/src/main/kotlin/com/wrbug/polymarketbot/service/copytrading/monitor/CopyTradingWebSocketService.kt index f3ff981..825be9f 100644 --- a/backend/src/main/kotlin/com/wrbug/polymarketbot/service/copytrading/monitor/CopyTradingWebSocketService.kt +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/service/copytrading/monitor/CopyTradingWebSocketService.kt @@ -22,15 +22,14 @@ import java.util.concurrent.ConcurrentHashMap @Service class CopyTradingWebSocketService( private val copyOrderTrackingService: CopyOrderTrackingService, - private val templateRepository: CopyTradingTemplateRepository + private val templateRepository: CopyTradingTemplateRepository, + private val gson: Gson ) { private val logger = LoggerFactory.getLogger(CopyTradingWebSocketService::class.java) @Value("\${polymarket.websocket.url:wss://ws-live-data.polymarket.com}") private var websocketUrl: String = "wss://ws-live-data.polymarket.com" - - private val gson = Gson() private val scope = CoroutineScope(Dispatchers.Default + SupervisorJob()) // 存储每个Leader的WebSocket客户端:leaderId -> WebSocketClient diff --git a/backend/src/main/kotlin/com/wrbug/polymarketbot/service/copytrading/monitor/OnChainWsUtils.kt b/backend/src/main/kotlin/com/wrbug/polymarketbot/service/copytrading/monitor/OnChainWsUtils.kt index a45221b..7ba35c6 100644 --- a/backend/src/main/kotlin/com/wrbug/polymarketbot/service/copytrading/monitor/OnChainWsUtils.kt +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/service/copytrading/monitor/OnChainWsUtils.kt @@ -1,5 +1,7 @@ package com.wrbug.polymarketbot.service.copytrading.monitor +import com.google.gson.Gson +import com.google.gson.GsonBuilder import com.google.gson.JsonArray import com.google.gson.reflect.TypeToken import com.wrbug.polymarketbot.api.* @@ -19,6 +21,29 @@ object OnChainWsUtils { private val logger = LoggerFactory.getLogger(OnChainWsUtils::class.java) + // 创建 Gson 实例(与 GsonConfig 中的配置一致,使用 lenient 模式) + private val gson: Gson = GsonBuilder() + .setLenient() + .create() + + /** + * 解析 JSON 字符串数组 + * @param jsonString JSON 字符串,如 "[\"Yes\", \"No\"]" + * @return 字符串列表,如果解析失败返回空列表 + */ + private fun parseStringArray(jsonString: String?): List { + if (jsonString.isNullOrBlank()) { + return emptyList() + } + + return try { + val listType = object : TypeToken>() {}.type + gson.fromJson>(jsonString, listType) ?: emptyList() + } catch (e: Exception) { + emptyList() + } + } + // 合约地址 const val USDC_CONTRACT = "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174" const val ERC1155_CONTRACT = "0x4d97dcd97ec945f40cf65f87097ace5ea0476045" @@ -240,24 +265,17 @@ object OnChainWsUtils { val clobTokenIds = when { clobTokenIdsRaw == null -> null else -> { - try { - // 尝试解析 JSON 字符串 - val gson = com.google.gson.Gson() - val listType = object : com.google.gson.reflect.TypeToken>() {}.type - gson.fromJson>(clobTokenIdsRaw, listType) - } catch (e: Exception) { - // 如果不是 JSON 字符串,可能是其他格式,返回 null - null - } + // 解析 JSON 字符串 + parseStringArray(clobTokenIdsRaw) } } // 解析 outcomes(可能是 JSON 字符串或数组) - val outcomes = com.wrbug.polymarketbot.util.JsonUtils.parseStringArray(market.outcomes) + val outcomes = parseStringArray(market.outcomes) // 查找 tokenId 在 clobTokenIds 中的索引 - val outcomeIndex = clobTokenIds?.indexOfFirst { - it.equals(tokenId, ignoreCase = true) + val outcomeIndex = clobTokenIds?.indexOfFirst { token -> + token.equals(tokenId, ignoreCase = true) }?.takeIf { it >= 0 } // 获取 outcome 名称 diff --git a/backend/src/main/kotlin/com/wrbug/polymarketbot/service/copytrading/monitor/UnifiedOnChainWsService.kt b/backend/src/main/kotlin/com/wrbug/polymarketbot/service/copytrading/monitor/UnifiedOnChainWsService.kt index c9f91ce..22d02ab 100644 --- a/backend/src/main/kotlin/com/wrbug/polymarketbot/service/copytrading/monitor/UnifiedOnChainWsService.kt +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/service/copytrading/monitor/UnifiedOnChainWsService.kt @@ -27,14 +27,12 @@ import java.util.concurrent.ConcurrentHashMap @Service class UnifiedOnChainWsService( private val rpcNodeService: RpcNodeService, - private val retrofitFactory: RetrofitFactory + private val retrofitFactory: RetrofitFactory, + private val gson: Gson ) { private val logger = LoggerFactory.getLogger(UnifiedOnChainWsService::class.java) - // Gson 实例,用于解析 JSON - private val gson = Gson() - @Value("\${copy.trading.onchain.ws.reconnect.delay:3000}") private var reconnectDelay: Long = 3000 // 重连延迟(毫秒),默认3秒 diff --git a/backend/src/main/kotlin/com/wrbug/polymarketbot/util/JsonUtils.kt b/backend/src/main/kotlin/com/wrbug/polymarketbot/util/JsonUtils.kt index 3994a9e..d7e1306 100644 --- a/backend/src/main/kotlin/com/wrbug/polymarketbot/util/JsonUtils.kt +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/util/JsonUtils.kt @@ -2,14 +2,16 @@ package com.wrbug.polymarketbot.util import com.google.gson.Gson import com.google.gson.reflect.TypeToken +import org.springframework.stereotype.Component /** * JSON 工具类 * 用于解析 JSON 字符串 */ -object JsonUtils { - - private val gson = Gson() +@Component +class JsonUtils( + private val gson: Gson +) { /** * 解析 JSON 字符串数组 diff --git a/backend/src/main/kotlin/com/wrbug/polymarketbot/util/RetrofitFactory.kt b/backend/src/main/kotlin/com/wrbug/polymarketbot/util/RetrofitFactory.kt index 14f574b..16879dd 100644 --- a/backend/src/main/kotlin/com/wrbug/polymarketbot/util/RetrofitFactory.kt +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/util/RetrofitFactory.kt @@ -1,7 +1,6 @@ package com.wrbug.polymarketbot.util import com.google.gson.Gson -import com.google.gson.GsonBuilder import com.wrbug.polymarketbot.api.BuilderRelayerApi import com.wrbug.polymarketbot.api.EthereumRpcApi import com.wrbug.polymarketbot.api.GitHubApi @@ -34,7 +33,8 @@ class RetrofitFactory( @Value("\${polymarket.clob.base-url}") private val clobBaseUrl: String, @Value("\${polymarket.gamma.base-url}") - private val gammaBaseUrl: String + private val gammaBaseUrl: String, + private val gson: Gson ) { /** @@ -61,11 +61,6 @@ class RetrofitFactory( .addInterceptor(responseLoggingInterceptor) .build() - // 创建 lenient 模式的 Gson,允许解析格式不严格的 JSON - val gson = GsonBuilder() - .setLenient() - .create() - return Retrofit.Builder() .baseUrl(clobBaseUrl) .client(okHttpClient) @@ -87,11 +82,6 @@ class RetrofitFactory( .addInterceptor(responseLoggingInterceptor) .build() - // 创建 lenient 模式的 Gson,允许解析格式不严格的 JSON - val gson = GsonBuilder() - .setLenient() - .create() - return Retrofit.Builder() .baseUrl(clobBaseUrl) .client(okHttpClient) @@ -129,11 +119,6 @@ class RetrofitFactory( .addInterceptor(urlReplaceInterceptor) .build() - // 创建 lenient 模式的 Gson - val gson = GsonBuilder() - .setLenient() - .create() - return Retrofit.Builder() .baseUrl(fixedBaseUrl) .client(okHttpClient) @@ -228,11 +213,6 @@ class RetrofitFactory( } val okHttpClient = createClient().build() - // 创建 lenient 模式的 Gson - val gson = GsonBuilder() - .setLenient() - .create() - return Retrofit.Builder() .baseUrl("$baseUrl/") .client(okHttpClient) @@ -253,11 +233,6 @@ class RetrofitFactory( .followSslRedirects(true) .build() - // 创建 lenient 模式的 Gson - val gson = GsonBuilder() - .setLenient() - .create() - return Retrofit.Builder() .baseUrl("$baseUrl/") .client(okHttpClient) @@ -292,10 +267,6 @@ class RetrofitFactory( .addInterceptor(builderAuthInterceptor) .build() - val gson = GsonBuilder() - .setLenient() - .create() - return Retrofit.Builder() .baseUrl("$baseUrl/") .client(okHttpClient) @@ -327,10 +298,6 @@ class RetrofitFactory( .addInterceptor(githubInterceptor) .build() - val gson = GsonBuilder() - .setLenient() - .create() - return Retrofit.Builder() .baseUrl("$baseUrl/") .client(okHttpClient) @@ -393,18 +360,32 @@ class ResponseLoggingInterceptor : Interceptor { val responseBody = response.peekBody(2048) val responseBodyString = responseBody.string() - // 检查是否是有效的 JSON - val isJson = responseBodyString.trim().startsWith("{") || - responseBodyString.trim().startsWith("[") + // 检查响应体是否为空 + val isEmpty = responseBodyString.isBlank() - if (!isJson || !response.isSuccessful) { + // 检查是否是有效的 JSON + val trimmedBody = responseBodyString.trim() + val isJson = !isEmpty && ( + trimmedBody.startsWith("{") || + trimmedBody.startsWith("[") + ) + + // 如果响应体为空或不是 JSON,记录警告 + if (isEmpty || !isJson) { + val bodyPreview = if (isEmpty) { + "(空响应体)" + } else { + trimmedBody.take(500) + } logger.warn( "API 响应异常: method=${request.method}, url=${request.url}, " + - "code=${response.code}, isJson=$isJson, " + - "responseBody=${responseBodyString.take(500)}" + "code=${response.code}, isJson=$isJson, isEmpty=$isEmpty, " + + "responseBody=$bodyPreview" ) } } catch (e: Exception) { + // 如果读取响应体失败,记录异常但不影响响应 + logger.debug("读取响应体失败: ${e.message}") } }