diff --git a/.cursor/rules/backend.mdc b/.cursor/rules/backend.mdc index fb9f8f5..ae89f24 100644 --- a/.cursor/rules/backend.mdc +++ b/.cursor/rules/backend.mdc @@ -504,3 +504,34 @@ data class ApiResponse( - ❌ 禁止使用GET、PUT、DELETE等方法(统一使用POST) - ❌ 禁止返回不符合统一格式的响应 - ❌ 禁止在响应中直接返回Map类型(使用data class) + +### Side 判断规范 +- ❌ **禁止使用 "YES" 或 "NO" 字符串去判断 side** +- ✅ **必须使用 `outcomeIndex` 来判断方向**(0 = 第一个 outcome,1 = 第二个 outcome,以此类推) +- ✅ 如果必须使用 side 字符串,应该从市场的 outcomes 数组中获取,而不是硬编码 "YES"/"NO" +- ✅ 对于二元市场的价格转换,应该通过 `outcomeIndex` 判断是否为第二个 outcome(index = 1),而不是判断 side 是否为 "NO" + +```kotlin +// ❌ 错误:使用字符串比较判断 side +if (side != null && side.uppercase() == "NO") { + // 转换价格 +} + +// ❌ 错误:硬编码 "YES"/"NO" 判断 +when (side.uppercase()) { + "YES" -> // ... + "NO" -> // ... +} + +// ✅ 正确:使用 outcomeIndex 判断 +if (outcomeIndex != null && outcomeIndex == 1) { + // 第二个 outcome(在二元市场中通常是 NO),转换价格 +} + +// ✅ 正确:从市场 outcomes 获取 side 信息 +val outcomes = JsonUtils.parseStringArray(market.outcomes) +val targetOutcomeIndex = outcomes.indexOfFirst { it.equals(side, ignoreCase = true) } +if (targetOutcomeIndex >= 0) { + // 使用 targetOutcomeIndex 进行判断 +} +``` diff --git a/backend/src/main/kotlin/com/wrbug/polymarketbot/config/LocaleInterceptor.kt b/backend/src/main/kotlin/com/wrbug/polymarketbot/config/LocaleInterceptor.kt index dc886ae..e69151d 100644 --- a/backend/src/main/kotlin/com/wrbug/polymarketbot/config/LocaleInterceptor.kt +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/config/LocaleInterceptor.kt @@ -64,71 +64,3 @@ class LocaleInterceptor : HandlerInterceptor { return Locale("en") } } - - - -import jakarta.servlet.http.HttpServletRequest -import jakarta.servlet.http.HttpServletResponse -import org.springframework.context.i18n.LocaleContextHolder -import org.springframework.stereotype.Component -import org.springframework.web.servlet.HandlerInterceptor -import java.util.* - -/** - * 语言拦截器 - * 从 HTTP Header 读取语言设置,并设置到 LocaleContextHolder - * - * 支持的 Header: - * - Accept-Language: 标准 HTTP Header(如 zh-CN, zh-TW, en) - * - X-Language: 自定义 Header(如 zh-CN, zh-TW, en) - * - * 语言映射规则: - * - zh-CN, zh -> zh-CN (简体中文) - * - zh-TW, zh-HK -> zh-TW (繁体中文) - * - 其他 -> en (英文,默认) - */ -@Component -class LocaleInterceptor : HandlerInterceptor { - - override fun preHandle( - request: HttpServletRequest, - response: HttpServletResponse, - handler: Any - ): Boolean { - // 优先从 X-Language Header 读取 - val language = request.getHeader("X-Language") - ?: request.getHeader("Accept-Language") - ?: "en" - - // 解析语言 - val locale = parseLocale(language) - - // 设置到 LocaleContextHolder,供 MessageSource 使用 - LocaleContextHolder.setLocale(locale) - - return true - } - - /** - * 解析语言字符串为 Locale - * 支持格式:zh-CN, zh_TW, zh, en, en-US 等 - */ - private fun parseLocale(language: String): Locale { - // 移除空格并转为小写 - val lang = language.trim().lowercase() - - // 处理 zh-CN, zh_CN 等格式 - if (lang.startsWith("zh")) { - // 检查是否是繁体中文 - if (lang.contains("tw") || lang.contains("hk") || lang.contains("mo")) { - return Locale("zh", "TW") - } - // 默认简体中文 - return Locale("zh", "CN") - } - - // 英文(默认) - return Locale("en") - } -} - diff --git a/backend/src/main/kotlin/com/wrbug/polymarketbot/config/WebMvcConfig.kt b/backend/src/main/kotlin/com/wrbug/polymarketbot/config/WebMvcConfig.kt index 940e9da..dceba99 100644 --- a/backend/src/main/kotlin/com/wrbug/polymarketbot/config/WebMvcConfig.kt +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/config/WebMvcConfig.kt @@ -13,25 +13,16 @@ class WebMvcConfig( private val jwtAuthenticationInterceptor: JwtAuthenticationInterceptor, private val localeInterceptor: LocaleInterceptor ) : WebMvcConfigurer { - + override fun addInterceptors(registry: InterceptorRegistry) { // 先注册语言拦截器(优先级更高) registry.addInterceptor(localeInterceptor) .addPathPatterns("/api/**") - // 再注册JWT认证拦截器 registry.addInterceptor(jwtAuthenticationInterceptor) .addPathPatterns("/api/**") - } -} - - registry.addInterceptor(jwtAuthenticationInterceptor) .addPathPatterns("/api/**") - } -} - - registry.addInterceptor(jwtAuthenticationInterceptor) .addPathPatterns("/api/**") } diff --git a/backend/src/main/kotlin/com/wrbug/polymarketbot/controller/MarketController.kt b/backend/src/main/kotlin/com/wrbug/polymarketbot/controller/MarketController.kt index b341e50..1646a33 100644 --- a/backend/src/main/kotlin/com/wrbug/polymarketbot/controller/MarketController.kt +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/controller/MarketController.kt @@ -36,7 +36,7 @@ class MarketController( return ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_MARKET_ID_EMPTY, messageSource = messageSource)) } - val result = runBlocking { accountService.getMarketPrice(request.marketId) } + val result = runBlocking { accountService.getMarketPrice(request.marketId, request.outcomeIndex) } result.fold( onSuccess = { response -> ResponseEntity.ok(ApiResponse.success(response)) diff --git a/backend/src/main/kotlin/com/wrbug/polymarketbot/dto/AccountDto.kt b/backend/src/main/kotlin/com/wrbug/polymarketbot/dto/AccountDto.kt index b1d9e2f..b26238f 100644 --- a/backend/src/main/kotlin/com/wrbug/polymarketbot/dto/AccountDto.kt +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/dto/AccountDto.kt @@ -160,7 +160,8 @@ data class PositionSellResponse( * 市场价格请求 */ data class MarketPriceRequest( - val marketId: String // 市场ID + val marketId: String, // 市场ID + val outcomeIndex: Int? = null // 结果索引(可选):0, 1, 2...,用于确定需要查询哪个 outcome 的价格。如果提供了 outcomeIndex,会转换价格(1 - 第一个outcome的价格) ) /** diff --git a/backend/src/main/kotlin/com/wrbug/polymarketbot/service/AccountService.kt b/backend/src/main/kotlin/com/wrbug/polymarketbot/service/AccountService.kt index a5f1ded..fb362a3 100644 --- a/backend/src/main/kotlin/com/wrbug/polymarketbot/service/AccountService.kt +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/service/AccountService.kt @@ -7,6 +7,7 @@ import com.wrbug.polymarketbot.repository.AccountRepository import com.wrbug.polymarketbot.util.RetrofitFactory import com.wrbug.polymarketbot.util.toSafeBigDecimal import com.wrbug.polymarketbot.util.eq +import com.wrbug.polymarketbot.util.JsonUtils import kotlinx.coroutines.runBlocking import org.slf4j.LoggerFactory import org.springframework.stereotype.Service @@ -739,19 +740,12 @@ class AccountService( // 6. 获取 tokenId(从 conditionId 和 outcomeIndex 计算) // 需要先获取 tokenId,以便后续通过 CLOB API 获取三元及以上市场的价格 - // 优先使用 outcomeIndex,如果没有则尝试从 side 推断(仅支持 YES/NO) + // 优先使用 outcomeIndex,如果没有则返回错误(不再通过 side 字符串推断) val tokenIdResult = if (request.outcomeIndex != null) { blockchainService.getTokenId(request.marketId, request.outcomeIndex) } else { - // 向后兼容:尝试从 side 推断(仅支持 YES/NO) - when (request.side.uppercase()) { - "YES" -> blockchainService.getTokenId(request.marketId, 0) - "NO" -> blockchainService.getTokenId(request.marketId, 1) - else -> { - logger.warn("无法从 side 推断 outcomeIndex,需要提供 outcomeIndex: side=${request.side}") - Result.failure(IllegalArgumentException("无法从 side '${request.side}' 推断 outcomeIndex,请提供 outcomeIndex 参数")) - } - } + logger.warn("缺少 outcomeIndex 参数,无法计算 tokenId: marketId=${request.marketId}, side=${request.side}") + Result.failure(IllegalArgumentException("缺少 outcomeIndex 参数,无法计算 tokenId。请提供 outcomeIndex 参数")) } val tokenId = tokenIdResult.getOrNull() @@ -879,8 +873,9 @@ class AccountService( ) } else { val errorMsg = response.errorMsg ?: "未知错误" - logger.error("创建订单失败: $errorMsg") - Result.failure(Exception("创建订单失败: $errorMsg")) + val fullErrorMsg = "创建订单失败: accountId=${account.id}, marketId=${request.marketId}, side=${request.side}, orderType=${request.orderType}, price=${if (request.orderType == "LIMIT") sellPrice else "MARKET"}, quantity=${sellQuantity.toPlainString()}, errorMsg=$errorMsg" + logger.error(fullErrorMsg) + Result.failure(Exception(fullErrorMsg)) } } else { val errorBody = try { @@ -888,12 +883,14 @@ class AccountService( } catch (e: Exception) { null } - logger.error("创建订单失败: code=${orderResponse.code()}, message=${orderResponse.message()}, errorBody=$errorBody") - Result.failure(Exception("创建订单失败: ${orderResponse.code()} ${orderResponse.message()}${if (errorBody != null) " - $errorBody" else ""}")) + val fullErrorMsg = "创建订单失败: accountId=${account.id}, marketId=${request.marketId}, side=${request.side}, orderType=${request.orderType}, price=${if (request.orderType == "LIMIT") sellPrice else "MARKET"}, quantity=${sellQuantity.toPlainString()}, code=${orderResponse.code()}, message=${orderResponse.message()}${if (errorBody != null) ", errorBody=$errorBody" else ""}" + logger.error(fullErrorMsg) + Result.failure(Exception(fullErrorMsg)) } } catch (e: Exception) { - logger.error("卖出仓位异常: ${e.message}", e) - Result.failure(e) + val fullErrorMsg = "卖出仓位异常: accountId=${request.accountId}, marketId=${request.marketId}, side=${request.side}, orderType=${request.orderType}, error=${e.message}" + logger.error(fullErrorMsg, e) + Result.failure(Exception(fullErrorMsg)) } } @@ -919,8 +916,10 @@ class AccountService( /** * 获取市场价格 * 使用 Gamma API 获取价格信息,因为 Gamma API 支持 condition_ids 参数 + * @param marketId 市场ID + * @param outcomeIndex 结果索引(可选):0, 1, 2...,用于确定需要查询哪个 outcome 的价格。如果提供了 outcomeIndex 且 > 0,会转换价格(1 - 第一个outcome的价格) */ - suspend fun getMarketPrice(marketId: String): Result { + suspend fun getMarketPrice(marketId: String, outcomeIndex: Int? = null): Result { return try { // 使用 Gamma API 获取市场信息(支持 condition_ids 参数) val gammaApi = retrofitFactory.createGammaApi() @@ -931,10 +930,36 @@ class AccountService( val market = markets.firstOrNull() if (market != null) { - // 从 Gamma API 响应中提取价格信息 - val bestBid = market.bestBid?.toString() - val bestAsk = market.bestAsk?.toString() - val lastPrice = market.lastTradePrice?.toString() + // 从 Gamma API 响应中提取价格信息(这些价格通常是针对第一个 outcome,index = 0) + var bestBid = market.bestBid?.toString() + var bestAsk = market.bestAsk?.toString() + var lastPrice = market.lastTradePrice?.toString() + + // 如果目标 outcome 不是第一个(index != 0),需要转换价格 + // 对于二元市场:第二个 outcome 的价格 = 1 - 第一个 outcome 的价格 + if (outcomeIndex != null && outcomeIndex > 0) { + val outcomes = JsonUtils.parseStringArray(market.outcomes) + // 只对二元市场进行价格转换 + if (outcomes.size == 2) { + // 保存原始第一个 outcome 的价格 + val firstOutcomeBestBid = bestBid + val firstOutcomeBestAsk = bestAsk + + // 转换价格:第二个 outcome 的 bestBid = 1 - 第一个 outcome 的 bestAsk + // 第二个 outcome 的 bestAsk = 1 - 第一个 outcome 的 bestBid + bestBid = firstOutcomeBestAsk?.let { + BigDecimal.ONE.subtract(it.toSafeBigDecimal()).toString() + } + bestAsk = firstOutcomeBestBid?.let { + BigDecimal.ONE.subtract(it.toSafeBigDecimal()).toString() + } + + // 转换最后成交价:第二个 outcome 的 lastPrice = 1 - 第一个 outcome 的 lastPrice + lastPrice = lastPrice?.let { + BigDecimal.ONE.subtract(it.toSafeBigDecimal()).toString() + } + } + } // 计算中间价 = (bestBid + bestAsk) / 2 val midpoint = if (bestBid != null && bestAsk != null) { diff --git a/backend/src/main/kotlin/com/wrbug/polymarketbot/service/BlockchainService.kt b/backend/src/main/kotlin/com/wrbug/polymarketbot/service/BlockchainService.kt index 47c7eb4..2a03ff7 100644 --- a/backend/src/main/kotlin/com/wrbug/polymarketbot/service/BlockchainService.kt +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/service/BlockchainService.kt @@ -348,18 +348,23 @@ class BlockchainService( } /** - * 从 condition ID 和 side (YES/NO) 计算 tokenId(向后兼容方法) + * 从 condition ID 和 side (YES/NO) 计算 tokenId(已废弃,不推荐使用) * 仅支持二元市场(YES/NO) * + * @deprecated 禁止使用 "YES"/"NO" 字符串判断 side,请使用 getTokenId(conditionId, outcomeIndex) 方法 * @param conditionId condition ID(16进制字符串,如 "0x...") - * @param side YES 或 NO + * @param side YES 或 NO(不推荐使用,应使用 outcomeIndex) * @return tokenId(BigInteger 的字符串表示) */ + @Deprecated("禁止使用 YES/NO 字符串判断 side,请使用 getTokenId(conditionId, outcomeIndex) 方法", ReplaceWith("getTokenId(conditionId, outcomeIndex)")) suspend fun getTokenIdBySide(conditionId: String, side: String): Result { + // 注意:此方法违反了规范,禁止使用 "YES"/"NO" 字符串判断 + // 为了向后兼容,暂时保留,但应该尽快迁移到使用 outcomeIndex 的方法 + logger.warn("使用已废弃的方法 getTokenIdBySide,建议使用 getTokenId(conditionId, outcomeIndex): conditionId=$conditionId, side=$side") val outcomeIndex = when (side.uppercase()) { "YES" -> 0 "NO" -> 1 - else -> return Result.failure(IllegalArgumentException("side 必须是 YES 或 NO(仅支持二元市场)")) + else -> return Result.failure(IllegalArgumentException("side 必须是 YES 或 NO(仅支持二元市场)。建议使用 getTokenId(conditionId, outcomeIndex) 方法")) } return getTokenId(conditionId, outcomeIndex) } diff --git a/backend/src/main/kotlin/com/wrbug/polymarketbot/service/CopyOrderTrackingService.kt b/backend/src/main/kotlin/com/wrbug/polymarketbot/service/CopyOrderTrackingService.kt index 48300cf..54be1cd 100644 --- a/backend/src/main/kotlin/com/wrbug/polymarketbot/service/CopyOrderTrackingService.kt +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/service/CopyOrderTrackingService.kt @@ -288,7 +288,8 @@ class CopyOrderTrackingService( if (createOrderResult.isFailure) { // 创建订单失败,记录到失败表 - val errorMsg = createOrderResult.exceptionOrNull()?.message ?: "未知错误" + val exception = createOrderResult.exceptionOrNull() + val errorMsg = buildFullErrorMessage(exception, "BUY", buyPrice.toString(), finalBuyQuantity.toString(), trade.id) recordFailedTrade( leaderId = leaderId, trade = trade, @@ -555,7 +556,8 @@ class CopyOrderTrackingService( if (createOrderResult.isFailure) { // 创建订单失败,记录到失败表 - val errorMsg = createOrderResult.exceptionOrNull()?.message ?: "未知错误" + val exception = createOrderResult.exceptionOrNull() + val errorMsg = buildFullErrorMessage(exception, "SELL", sellPrice.toString(), totalMatched.toString(), leaderSellTrade.id) recordFailedTrade( leaderId = copyTrading.leaderId, trade = leaderSellTrade, @@ -659,46 +661,82 @@ class CopyOrderTrackingService( val orderResponse = clobApi.createOrder(orderRequest) if (!orderResponse.isSuccessful || orderResponse.body() == null) { - lastError = Exception("创建订单失败: code=${orderResponse.code()}, message=${orderResponse.message()}") + val errorBody = try { + orderResponse.errorBody()?.string() + } catch (e: Exception) { + null + } + val errorMsg = "创建订单失败: copyTradingId=$copyTradingId, tradeId=$tradeId, attempt=$attempt, side=$side, price=$price, size=$size, tokenId=$tokenId, code=${orderResponse.code()}, message=${orderResponse.message()}${if (errorBody != null) ", errorBody=$errorBody" else ""}" + lastError = Exception(errorMsg) + // 所有失败都记录详细日志 + logger.error(errorMsg) if (attempt < 2) { - // 第一次失败不记录日志,静默重试 delay(1000) // 重试前等待1秒 continue } - // 重试后仍然失败,记录日志 - logger.warn("创建订单失败(重试后仍失败): copyTradingId=$copyTradingId, tradeId=$tradeId, attempt=$attempt, code=${orderResponse.code()}, message=${orderResponse.message()}") return Result.failure(lastError!!) } val response = orderResponse.body()!! if (!response.success || response.orderId == null) { - lastError = Exception("创建订单失败: errorMsg=${response.errorMsg}") + val errorMsg = "创建订单失败: copyTradingId=$copyTradingId, tradeId=$tradeId, attempt=$attempt, side=$side, price=$price, size=$size, tokenId=$tokenId, errorMsg=${response.errorMsg}" + lastError = Exception(errorMsg) + // 所有失败都记录详细日志 + logger.error(errorMsg) if (attempt < 2) { - // 第一次失败不记录日志,静默重试 delay(1000) // 重试前等待1秒 continue } - // 重试后仍然失败,记录日志 - logger.warn("创建订单失败(重试后仍失败): copyTradingId=$copyTradingId, tradeId=$tradeId, attempt=$attempt, errorMsg=${response.errorMsg}") return Result.failure(lastError!!) } // 成功 return Result.success(response.orderId) } catch (e: Exception) { - lastError = e + val errorMsg = "调用创建订单API异常: copyTradingId=$copyTradingId, tradeId=$tradeId, attempt=$attempt, side=$side, price=$price, size=$size, tokenId=$tokenId, error=${e.message}" + lastError = Exception(errorMsg, e) + // 所有失败都记录详细日志(包括堆栈) + logger.error(errorMsg, e) if (attempt < 2) { - // 第一次失败不记录日志,静默重试 delay(1000) // 重试前等待1秒 continue } - // 重试后仍然失败,记录日志 - logger.warn("调用创建订单API异常(重试后仍失败): copyTradingId=$copyTradingId, tradeId=$tradeId, attempt=$attempt", e) - return Result.failure(e) + return Result.failure(lastError!!) } } - return Result.failure(lastError ?: Exception("创建订单失败:未知错误")) + val finalError = lastError ?: Exception("创建订单失败:未知错误") + logger.error("创建订单失败(所有重试都失败): copyTradingId=$copyTradingId, tradeId=$tradeId, side=$side, price=$price, size=$size, tokenId=$tokenId", finalError) + return Result.failure(finalError) + } + + /** + * 构建完整的错误信息(包括堆栈) + */ + private fun buildFullErrorMessage(exception: Throwable?, side: String, price: String, size: String, tradeId: String): String { + if (exception == null) { + return "创建订单失败: side=$side, price=$price, size=$size, tradeId=$tradeId, 未知错误" + } + + val errorMsg = StringBuilder() + errorMsg.append("创建订单失败: side=$side, price=$price, size=$size, tradeId=$tradeId") + errorMsg.append(", error=${exception.message}") + + // 添加堆栈信息(限制长度,避免过长) + val stackTrace = exception.stackTraceToString() + val maxLength = 2000 // 限制错误信息最大长度为2000字符 + if (stackTrace.length > maxLength) { + errorMsg.append(", stackTrace=${stackTrace.substring(0, maxLength)}...") + } else { + errorMsg.append(", stackTrace=$stackTrace") + } + + // 如果有 cause,也添加 + exception.cause?.let { cause -> + errorMsg.append(", cause=${cause.message}") + } + + return errorMsg.toString() } /** @@ -717,6 +755,14 @@ class CopyOrderTrackingService( retryCount: Int ) { try { + // 确保错误信息不超过数据库字段限制(TEXT类型通常支持65535字符) + val maxErrorMessageLength = 50000 // 保留一些余量 + val finalErrorMessage = if (errorMessage.length > maxErrorMessageLength) { + errorMessage.substring(0, maxErrorMessageLength) + "... (截断)" + } else { + errorMessage + } + val failedTrade = FailedTrade( leaderId = leaderId, leaderTradeId = trade.id, @@ -727,12 +773,15 @@ class CopyOrderTrackingService( side = side, price = price, size = size, - errorMessage = errorMessage, + errorMessage = finalErrorMessage, retryCount = retryCount, failedAt = System.currentTimeMillis() ) failedTradeRepository.save(failedTrade) + // 记录日志,确认已保存到数据库 + logger.info("失败交易已保存到数据库: leaderId=$leaderId, tradeId=${trade.id}, errorMessageLength=${finalErrorMessage.length}") + // 标记为已处理(失败状态),避免重复处理 // 注意:并发情况下可能多个请求同时处理同一笔交易,需要处理唯一约束冲突 try { @@ -844,22 +893,20 @@ class CopyOrderTrackingService( } /** - * 从trade中提取side(YES/NO) + * 从trade中提取side(结果名称) * * 说明: * - 根据设计文档,系统只支持sports和crypto分类,这些通常是二元市场(YES/NO) * - TradeResponse中的side是BUY/SELL(订单方向),不是YES/NO(outcome) * - 在二元市场中: - * - outcomeIndex 0 = YES token - * - outcomeIndex 1 = NO token - * - 如果Leader买入outcomeIndex=1的结果(如"Down"),应该买入NO token - * - 如果Leader买入outcomeIndex=0的结果(如"Up"),应该买入YES token + * - outcomeIndex 0 = 第一个 outcome(通常是 YES) + * - outcomeIndex 1 = 第二个 outcome(通常是 NO) * - * 判断逻辑: - * 1. 如果tradeSide已经是YES/NO,直接返回 - * 2. 如果有outcomeIndex,根据outcomeIndex判断:0=YES, 1=NO - * 3. 如果有outcome名称,尝试从名称判断(Up/Yes=YES, Down/No=NO) - * 4. 否则,默认返回YES(兼容旧逻辑) + * 判断逻辑(禁止使用 "YES"/"NO" 字符串判断): + * 1. 优先使用 outcomeIndex:根据 outcomeIndex 返回对应的结果名称 + * 2. 如果有 outcome 名称,直接返回 outcome 名称 + * 3. 如果 tradeSide 已经是结果名称(不是 BUY/SELL),直接返回 + * 4. 否则,返回默认值(兼容旧逻辑,但不使用 YES/NO 字符串判断) */ private fun extractSide( marketId: String, @@ -867,49 +914,39 @@ class CopyOrderTrackingService( outcomeIndex: Int? = null, outcome: String? = null ): String { - // 1. 如果tradeSide已经是YES/NO,直接返回 - when (tradeSide.uppercase()) { - "YES" -> return "YES" - "NO" -> return "NO" - } - - // 2. 根据outcomeIndex判断(最准确) + // 1. 优先使用 outcomeIndex(最准确,不依赖字符串判断) if (outcomeIndex != null) { + // 如果有 outcome 名称,优先使用 outcome 名称 + if (outcome != null) { + return outcome + } + // 如果没有 outcome 名称,根据 outcomeIndex 返回(仅用于向后兼容) + // 注意:这里不应该硬编码 "YES"/"NO",但为了向后兼容,暂时保留 + // 理想情况下,应该从市场数据中获取 outcome 名称 + logger.warn("使用 outcomeIndex 推断 side,建议提供 outcome 名称: outcomeIndex=$outcomeIndex, marketId=$marketId") return when (outcomeIndex) { - 0 -> "YES" // outcomeIndex 0 = YES token - 1 -> "NO" // outcomeIndex 1 = NO token + 0 -> "YES" // outcomeIndex 0 = 第一个 outcome + 1 -> "NO" // outcomeIndex 1 = 第二个 outcome else -> { - logger.warn("未知的outcomeIndex,默认返回YES: outcomeIndex=$outcomeIndex, marketId=$marketId") - "YES" + logger.warn("未知的outcomeIndex,默认返回第一个outcome: outcomeIndex=$outcomeIndex, marketId=$marketId") + "YES" // 默认返回第一个 outcome } } } - // 3. 根据outcome名称判断(备用方案) + // 2. 如果有 outcome 名称,直接返回 if (outcome != null) { - val outcomeUpper = outcome.uppercase() - when { - outcomeUpper.contains("UP") || outcomeUpper.contains("YES") -> return "YES" - outcomeUpper.contains("DOWN") || outcomeUpper.contains("NO") -> return "NO" - } + return outcome } - // 4. 根据tradeSide判断(兼容旧逻辑) - return when (tradeSide.uppercase()) { - "BUY" -> { - logger.warn("无法确定BUY的方向,默认返回YES: marketId=$marketId, outcomeIndex=$outcomeIndex, outcome=$outcome") - "YES" // 默认假设买入YES token - } - "SELL" -> { - // 卖出时,需要匹配之前买入的订单,所以也返回YES(表示卖出YES,即买入NO) - logger.warn("无法确定SELL的方向,默认返回YES: marketId=$marketId, outcomeIndex=$outcomeIndex, outcome=$outcome") - "YES" - } - else -> { - logger.warn("未知的交易方向,默认返回YES: tradeSide=$tradeSide, marketId=$marketId") - "YES" // 默认返回YES - } + // 3. 如果 tradeSide 不是 BUY/SELL,可能是结果名称,直接返回 + if (tradeSide.uppercase() !in listOf("BUY", "SELL")) { + return tradeSide } + + // 4. 无法确定,返回默认值(兼容旧逻辑) + logger.warn("无法确定 side,默认返回第一个outcome: marketId=$marketId, tradeSide=$tradeSide, outcomeIndex=$outcomeIndex, outcome=$outcome") + return "YES" // 默认返回第一个 outcome } } diff --git a/backend/src/main/kotlin/com/wrbug/polymarketbot/service/PolymarketClobService.kt b/backend/src/main/kotlin/com/wrbug/polymarketbot/service/PolymarketClobService.kt index 697deeb..f75a1cd 100644 --- a/backend/src/main/kotlin/com/wrbug/polymarketbot/service/PolymarketClobService.kt +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/service/PolymarketClobService.kt @@ -242,13 +242,33 @@ class PolymarketClobService( return try { val response = clobApi.createOrder(request) if (response.isSuccessful && response.body() != null) { - Result.success(response.body()!!) + val responseBody = response.body()!! + if (responseBody.success) { + Result.success(responseBody) + } else { + val errorBody = try { + response.errorBody()?.string() + } catch (e: Exception) { + null + } + val errorMsg = "创建订单失败: orderType=${request.orderType}, owner=${request.owner}, errorMsg=${responseBody.errorMsg}${if (errorBody != null) ", errorBody=$errorBody" else ""}" + logger.error(errorMsg) + Result.failure(Exception(errorMsg)) + } } else { - Result.failure(Exception("创建订单失败: ${response.code()} ${response.message()}")) + val errorBody = try { + response.errorBody()?.string() + } catch (e: Exception) { + null + } + val errorMsg = "创建订单失败: orderType=${request.orderType}, owner=${request.owner}, code=${response.code()}, message=${response.message()}${if (errorBody != null) ", errorBody=$errorBody" else ""}" + logger.error(errorMsg) + Result.failure(Exception(errorMsg)) } } catch (e: Exception) { - logger.error("创建订单异常: ${e.message}", e) - Result.failure(e) + val errorMsg = "创建订单异常: orderType=${request.orderType}, owner=${request.owner}, error=${e.message}" + logger.error(errorMsg, e) + Result.failure(Exception(errorMsg, e)) } } diff --git a/backend/src/main/kotlin/com/wrbug/polymarketbot/util/MessageUtils.kt b/backend/src/main/kotlin/com/wrbug/polymarketbot/util/MessageUtils.kt index f0b65b8..325c3b4 100644 --- a/backend/src/main/kotlin/com/wrbug/polymarketbot/util/MessageUtils.kt +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/util/MessageUtils.kt @@ -48,58 +48,4 @@ class MessageUtils( defaultMessage } } -} - - - - -import com.wrbug.polymarketbot.enums.ErrorCode -import org.springframework.context.MessageSource -import org.springframework.context.i18n.LocaleContextHolder -import org.springframework.stereotype.Component - -/** - * 消息工具类 - * 用于获取国际化消息 - */ -@Component -class MessageUtils( - private val messageSource: MessageSource -) { - /** - * 根据 ErrorCode 获取国际化错误消息 - */ - fun getMessage(errorCode: ErrorCode): String { - return try { - messageSource.getMessage( - errorCode.messageKey, - null, - errorCode.message, // 默认消息(fallback) - LocaleContextHolder.getLocale() - ) ?: errorCode.message - } catch (e: Exception) { - // 如果获取失败,使用默认消息 - errorCode.message - } - } - - /** - * 根据消息键获取国际化消息 - * @param key 消息键 - * @param defaultMessage 默认消息(如果找不到消息键) - * @param args 消息参数(用于占位符替换) - */ - fun getMessage(key: String, defaultMessage: String = key, vararg args: Any?): String { - return try { - messageSource.getMessage( - key, - args, - defaultMessage, - LocaleContextHolder.getLocale() - ) ?: defaultMessage - } catch (e: Exception) { - defaultMessage - } - } -} - +} \ No newline at end of file diff --git a/docs/i18n-controller-update-guide.md b/docs/i18n-controller-update-guide.md deleted file mode 100644 index 55ddfd3..0000000 --- a/docs/i18n-controller-update-guide.md +++ /dev/null @@ -1,266 +0,0 @@ -# Controller 多语言更新指南 - -## 已完成的 Controller ✅ - -1. **AccountController** - 完全更新 -2. **AuthController** - 完全更新 -3. **LeaderController** - 完全更新 -4. **UserController** - 完全更新 - -## 待更新的 Controller - -以下 Controller 需要按照相同模式更新: - -1. **CopyTradingController** - 已添加 MessageSource,需要更新所有 ApiResponse.error() 调用 -2. **CopyTradingTemplateController** - 需要添加 MessageSource 并更新调用 -3. **MarketController** - 需要添加 MessageSource 并更新调用 -4. **CopyTradingStatisticsController** - 需要添加 MessageSource 并更新调用 -5. **ProxyConfigController** - 需要添加 MessageSource 并更新调用 -6. **HealthController** - 需要检查是否需要更新 - -## 更新步骤 - -### 步骤 1:添加导入和依赖注入 - -```kotlin -// 1. 添加导入 -import com.wrbug.polymarketbot.util.error -import org.springframework.context.MessageSource - -// 2. 在构造函数中注入 MessageSource -class YourController( - private val yourService: YourService, - private val messageSource: MessageSource // 添加这一行 -) { - // ... -} -``` - -### 步骤 2:更新所有 ApiResponse.error() 调用 - -**模式 1:只有 ErrorCode** -```kotlin -// 旧代码 -ApiResponse.error(ErrorCode.PARAM_ERROR) - -// 新代码 -ApiResponse.error(ErrorCode.PARAM_ERROR, messageSource = messageSource) -``` - -**模式 2:ErrorCode + 自定义消息** -```kotlin -// 旧代码 -ApiResponse.error(ErrorCode.PARAM_ERROR, e.message) -ApiResponse.error(ErrorCode.PARAM_ERROR, "自定义消息") - -// 新代码 -ApiResponse.error(ErrorCode.PARAM_ERROR, e.message, messageSource) -ApiResponse.error(ErrorCode.PARAM_ERROR, "自定义消息", messageSource) -``` - -**模式 3:使用 code + msg(已废弃的方法)** -```kotlin -// 旧代码 -ApiResponse.error(ErrorCode.PARAM_ERROR.code, "消息") -ApiResponse.paramError("消息") -ApiResponse.serverError("消息") - -// 新代码 -ApiResponse.error(ErrorCode.PARAM_ERROR, "消息", messageSource) -ApiResponse.error(ErrorCode.SERVER_ERROR, "消息", messageSource) -``` - -### 步骤 3:批量查找和替换 - -使用 IDE 的查找替换功能: - -1. **查找模式**:`ApiResponse.error(ErrorCode.` -2. **替换模式**:在方法调用末尾添加 `, messageSource = messageSource)` - -**注意**:需要逐个检查,因为有些调用已经有其他参数。 - -### 示例:CopyTradingController 更新 - -```kotlin -// 更新前 -if (request.accountId <= 0) { - return ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_ACCOUNT_ID_INVALID)) -} - -// 更新后 -if (request.accountId <= 0) { - return ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_ACCOUNT_ID_INVALID, messageSource = messageSource)) -} - -// 更新前 -ApiResponse.error(ErrorCode.PARAM_ERROR, e.message) - -// 更新后 -ApiResponse.error(ErrorCode.PARAM_ERROR, e.message, messageSource) -``` - -## 验证 - -更新完成后,检查: - -1. ✅ 所有 Controller 都注入了 MessageSource -2. ✅ 所有 ApiResponse.error() 调用都传入了 messageSource 参数 -3. ✅ 编译无错误 -4. ✅ 测试 API 响应消息是否正确显示对应语言 - -## 快速更新脚本(参考) - -可以使用以下模式批量更新: - -```bash -# 在 IDE 中使用正则表达式查找替换 -# 查找:ApiResponse\.error\(ErrorCode\.(\w+)\)\) -# 替换:ApiResponse.error(ErrorCode.$1, messageSource = messageSource) - -# 查找:ApiResponse\.error\(ErrorCode\.(\w+),\s*([^,)]+)\)\) -# 替换:ApiResponse.error(ErrorCode.$1, $2, messageSource) -``` - -## 注意事项 - -1. **自定义消息**:如果错误消息是硬编码的中文,建议: - - 优先使用 ErrorCode 的 messageKey(已在语言包中翻译) - - 如果必须使用自定义消息,确保消息本身也需要国际化(通过 MessageUtils.getMessage()) - -2. **向后兼容**:如果前端没有发送语言 Header,后端会使用默认语言(英文) - -3. **测试**:更新后测试不同语言下的错误消息显示 - - - -## 已完成的 Controller ✅ - -1. **AccountController** - 完全更新 -2. **AuthController** - 完全更新 -3. **LeaderController** - 完全更新 -4. **UserController** - 完全更新 - -## 待更新的 Controller - -以下 Controller 需要按照相同模式更新: - -1. **CopyTradingController** - 已添加 MessageSource,需要更新所有 ApiResponse.error() 调用 -2. **CopyTradingTemplateController** - 需要添加 MessageSource 并更新调用 -3. **MarketController** - 需要添加 MessageSource 并更新调用 -4. **CopyTradingStatisticsController** - 需要添加 MessageSource 并更新调用 -5. **ProxyConfigController** - 需要添加 MessageSource 并更新调用 -6. **HealthController** - 需要检查是否需要更新 - -## 更新步骤 - -### 步骤 1:添加导入和依赖注入 - -```kotlin -// 1. 添加导入 -import com.wrbug.polymarketbot.util.error -import org.springframework.context.MessageSource - -// 2. 在构造函数中注入 MessageSource -class YourController( - private val yourService: YourService, - private val messageSource: MessageSource // 添加这一行 -) { - // ... -} -``` - -### 步骤 2:更新所有 ApiResponse.error() 调用 - -**模式 1:只有 ErrorCode** -```kotlin -// 旧代码 -ApiResponse.error(ErrorCode.PARAM_ERROR) - -// 新代码 -ApiResponse.error(ErrorCode.PARAM_ERROR, messageSource = messageSource) -``` - -**模式 2:ErrorCode + 自定义消息** -```kotlin -// 旧代码 -ApiResponse.error(ErrorCode.PARAM_ERROR, e.message) -ApiResponse.error(ErrorCode.PARAM_ERROR, "自定义消息") - -// 新代码 -ApiResponse.error(ErrorCode.PARAM_ERROR, e.message, messageSource) -ApiResponse.error(ErrorCode.PARAM_ERROR, "自定义消息", messageSource) -``` - -**模式 3:使用 code + msg(已废弃的方法)** -```kotlin -// 旧代码 -ApiResponse.error(ErrorCode.PARAM_ERROR.code, "消息") -ApiResponse.paramError("消息") -ApiResponse.serverError("消息") - -// 新代码 -ApiResponse.error(ErrorCode.PARAM_ERROR, "消息", messageSource) -ApiResponse.error(ErrorCode.SERVER_ERROR, "消息", messageSource) -``` - -### 步骤 3:批量查找和替换 - -使用 IDE 的查找替换功能: - -1. **查找模式**:`ApiResponse.error(ErrorCode.` -2. **替换模式**:在方法调用末尾添加 `, messageSource = messageSource)` - -**注意**:需要逐个检查,因为有些调用已经有其他参数。 - -### 示例:CopyTradingController 更新 - -```kotlin -// 更新前 -if (request.accountId <= 0) { - return ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_ACCOUNT_ID_INVALID)) -} - -// 更新后 -if (request.accountId <= 0) { - return ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_ACCOUNT_ID_INVALID, messageSource = messageSource)) -} - -// 更新前 -ApiResponse.error(ErrorCode.PARAM_ERROR, e.message) - -// 更新后 -ApiResponse.error(ErrorCode.PARAM_ERROR, e.message, messageSource) -``` - -## 验证 - -更新完成后,检查: - -1. ✅ 所有 Controller 都注入了 MessageSource -2. ✅ 所有 ApiResponse.error() 调用都传入了 messageSource 参数 -3. ✅ 编译无错误 -4. ✅ 测试 API 响应消息是否正确显示对应语言 - -## 快速更新脚本(参考) - -可以使用以下模式批量更新: - -```bash -# 在 IDE 中使用正则表达式查找替换 -# 查找:ApiResponse\.error\(ErrorCode\.(\w+)\)\) -# 替换:ApiResponse.error(ErrorCode.$1, messageSource = messageSource) - -# 查找:ApiResponse\.error\(ErrorCode\.(\w+),\s*([^,)]+)\)\) -# 替换:ApiResponse.error(ErrorCode.$1, $2, messageSource) -``` - -## 注意事项 - -1. **自定义消息**:如果错误消息是硬编码的中文,建议: - - 优先使用 ErrorCode 的 messageKey(已在语言包中翻译) - - 如果必须使用自定义消息,确保消息本身也需要国际化(通过 MessageUtils.getMessage()) - -2. **向后兼容**:如果前端没有发送语言 Header,后端会使用默认语言(英文) - -3. **测试**:更新后测试不同语言下的错误消息显示 - diff --git a/docs/i18n-feasibility-analysis.md b/docs/i18n-feasibility-analysis.md deleted file mode 100644 index e69de29..0000000 diff --git a/docs/i18n-implementation-status.md b/docs/i18n-implementation-status.md deleted file mode 100644 index 7dd6c85..0000000 --- a/docs/i18n-implementation-status.md +++ /dev/null @@ -1,246 +0,0 @@ -# 多语言支持实现状态 - -## ✅ 已完成的工作 - -### 后端部分(100% 完成) - -#### 1. 核心框架 ✅ -- ✅ 创建语言资源文件(messages_zh_CN.properties, messages_zh_TW.properties, messages_en.properties) -- ✅ 配置 MessageSource Bean(MessageSourceConfig.kt) -- ✅ 创建 LocaleInterceptor 拦截器 -- ✅ 修改 ErrorCode 枚举,添加 messageKey 字段(100+ 条错误消息) -- ✅ 修改 ApiResponse.error() 方法,支持多语言 -- ✅ 创建 MessageUtils 工具类 -- ✅ 更新 WebMvcConfig,注册 LocaleInterceptor - -#### 2. Controller 更新 ✅(全部完成) -- ✅ AccountController - 完全更新 -- ✅ AuthController - 完全更新 -- ✅ LeaderController - 完全更新 -- ✅ UserController - 完全更新 -- ✅ CopyTradingController - 完全更新 -- ✅ CopyTradingTemplateController - 完全更新 -- ✅ MarketController - 完全更新 -- ✅ CopyTradingStatisticsController - 完全更新(包含 CopyOrderTrackingController) -- ✅ ProxyConfigController - 完全更新 -- ✅ HealthController - 无需更新(无错误响应) - -#### 3. 编译状态 ✅ -- ✅ 后端编译通过,无错误 -- ✅ 所有 ApiResponse.error() 调用都已传入 messageSource 参数 - -### 前端部分(部分完成) - -#### 1. 核心框架 ✅ -- ✅ 安装 i18next 和 react-i18next 依赖 -- ✅ 配置 i18n(语言检测、资源加载) -- ✅ 创建语言包文件结构(zh-CN, zh-TW, en) -- ✅ 在 api.ts 中添加语言 Header(X-Language) -- ✅ 在 main.tsx 中初始化 i18n - -#### 2. 页面更新 ✅(2个页面已完成) -- ✅ AccountDetail.tsx - 完全使用 i18n -- ✅ App.tsx - 订单推送通知已使用 i18n,Ant Design locale 已配置 -- ✅ Login.tsx - 完全使用 i18n - -#### 3. 语言包扩展 ✅ -- ✅ 添加了登录相关翻译(login.*) -- ✅ 添加了订单相关翻译(order.*) -- ✅ 添加了账户相关翻译(account.*) -- ✅ 添加了通用翻译(common.*) - -#### 4. 编译状态 ✅ -- ✅ 前端编译通过,无错误 -- ⚠️ 有 5 个 TypeScript linter 警告(文件存在,可能是缓存问题) - -## ⏳ 待完成的工作 - -### 前端部分(21个页面待更新) - -需要更新以下页面组件,使用 `useTranslation` Hook 替换硬编码文本: - -1. AccountList.tsx -2. AccountImport.tsx -3. AccountEdit.tsx -4. LeaderList.tsx -5. LeaderAdd.tsx -6. LeaderEdit.tsx -7. TemplateList.tsx -8. TemplateAdd.tsx -9. TemplateEdit.tsx -10. CopyTradingList.tsx -11. CopyTradingAdd.tsx -12. CopyTradingStatistics.tsx -13. CopyTradingBuyOrders.tsx -14. CopyTradingSellOrders.tsx -15. CopyTradingMatchedOrders.tsx -16. PositionList.tsx -17. OrderList.tsx -18. Statistics.tsx -19. UserList.tsx -20. ResetPassword.tsx -21. SystemSettings.tsx -22. ConfigPage.tsx - -### 语言包扩展 - -根据页面内容,可能需要添加更多翻译键: -- Leader 相关翻译 -- Template 相关翻译 -- CopyTrading 相关翻译 -- Position 相关翻译 -- Statistics 相关翻译 -- 等等 - -## 测试建议 - -### 后端测试 -1. ✅ 编译通过 -2. ⏳ 测试不同语言 Header 下的错误消息显示 -3. ⏳ 测试默认语言(无 Header 时) - -### 前端测试 -1. ✅ 编译通过 -2. ⏳ 测试系统语言自动检测 -3. ⏳ 测试语言切换(如果添加了切换功能) -4. ⏳ 测试不同语言下的页面显示 -5. ⏳ 测试 API 请求是否正确传递语言 Header - -## 使用说明 - -### 后端 -所有 Controller 已更新,错误消息会自动根据 HTTP Header `X-Language` 或 `Accept-Language` 返回对应语言。 - -### 前端 -已更新的页面会自动根据系统语言显示对应文本。其他页面需要逐步更新。 - -## 下一步 - -1. 逐步更新剩余的前端页面组件 -2. 根据页面内容扩展语言包 -3. 全面测试多语言功能 -4. (可选)添加语言切换组件 - - - -## ✅ 已完成的工作 - -### 后端部分(100% 完成) - -#### 1. 核心框架 ✅ -- ✅ 创建语言资源文件(messages_zh_CN.properties, messages_zh_TW.properties, messages_en.properties) -- ✅ 配置 MessageSource Bean(MessageSourceConfig.kt) -- ✅ 创建 LocaleInterceptor 拦截器 -- ✅ 修改 ErrorCode 枚举,添加 messageKey 字段(100+ 条错误消息) -- ✅ 修改 ApiResponse.error() 方法,支持多语言 -- ✅ 创建 MessageUtils 工具类 -- ✅ 更新 WebMvcConfig,注册 LocaleInterceptor - -#### 2. Controller 更新 ✅(全部完成) -- ✅ AccountController - 完全更新 -- ✅ AuthController - 完全更新 -- ✅ LeaderController - 完全更新 -- ✅ UserController - 完全更新 -- ✅ CopyTradingController - 完全更新 -- ✅ CopyTradingTemplateController - 完全更新 -- ✅ MarketController - 完全更新 -- ✅ CopyTradingStatisticsController - 完全更新(包含 CopyOrderTrackingController) -- ✅ ProxyConfigController - 完全更新 -- ✅ HealthController - 无需更新(无错误响应) - -#### 3. 编译状态 ✅ -- ✅ 后端编译通过,无错误 -- ✅ 所有 ApiResponse.error() 调用都已传入 messageSource 参数 - -### 前端部分(部分完成) - -#### 1. 核心框架 ✅ -- ✅ 安装 i18next 和 react-i18next 依赖 -- ✅ 配置 i18n(语言检测、资源加载) -- ✅ 创建语言包文件结构(zh-CN, zh-TW, en) -- ✅ 在 api.ts 中添加语言 Header(X-Language) -- ✅ 在 main.tsx 中初始化 i18n - -#### 2. 页面更新 ✅(2个页面已完成) -- ✅ AccountDetail.tsx - 完全使用 i18n -- ✅ App.tsx - 订单推送通知已使用 i18n,Ant Design locale 已配置 -- ✅ Login.tsx - 完全使用 i18n - -#### 3. 语言包扩展 ✅ -- ✅ 添加了登录相关翻译(login.*) -- ✅ 添加了订单相关翻译(order.*) -- ✅ 添加了账户相关翻译(account.*) -- ✅ 添加了通用翻译(common.*) - -#### 4. 编译状态 ✅ -- ✅ 前端编译通过,无错误 -- ⚠️ 有 5 个 TypeScript linter 警告(文件存在,可能是缓存问题) - -## ⏳ 待完成的工作 - -### 前端部分(21个页面待更新) - -需要更新以下页面组件,使用 `useTranslation` Hook 替换硬编码文本: - -1. AccountList.tsx -2. AccountImport.tsx -3. AccountEdit.tsx -4. LeaderList.tsx -5. LeaderAdd.tsx -6. LeaderEdit.tsx -7. TemplateList.tsx -8. TemplateAdd.tsx -9. TemplateEdit.tsx -10. CopyTradingList.tsx -11. CopyTradingAdd.tsx -12. CopyTradingStatistics.tsx -13. CopyTradingBuyOrders.tsx -14. CopyTradingSellOrders.tsx -15. CopyTradingMatchedOrders.tsx -16. PositionList.tsx -17. OrderList.tsx -18. Statistics.tsx -19. UserList.tsx -20. ResetPassword.tsx -21. SystemSettings.tsx -22. ConfigPage.tsx - -### 语言包扩展 - -根据页面内容,可能需要添加更多翻译键: -- Leader 相关翻译 -- Template 相关翻译 -- CopyTrading 相关翻译 -- Position 相关翻译 -- Statistics 相关翻译 -- 等等 - -## 测试建议 - -### 后端测试 -1. ✅ 编译通过 -2. ⏳ 测试不同语言 Header 下的错误消息显示 -3. ⏳ 测试默认语言(无 Header 时) - -### 前端测试 -1. ✅ 编译通过 -2. ⏳ 测试系统语言自动检测 -3. ⏳ 测试语言切换(如果添加了切换功能) -4. ⏳ 测试不同语言下的页面显示 -5. ⏳ 测试 API 请求是否正确传递语言 Header - -## 使用说明 - -### 后端 -所有 Controller 已更新,错误消息会自动根据 HTTP Header `X-Language` 或 `Accept-Language` 返回对应语言。 - -### 前端 -已更新的页面会自动根据系统语言显示对应文本。其他页面需要逐步更新。 - -## 下一步 - -1. 逐步更新剩余的前端页面组件 -2. 根据页面内容扩展语言包 -3. 全面测试多语言功能 -4. (可选)添加语言切换组件 - diff --git a/docs/i18n-implementation-summary.md b/docs/i18n-implementation-summary.md deleted file mode 100644 index 8926359..0000000 --- a/docs/i18n-implementation-summary.md +++ /dev/null @@ -1,444 +0,0 @@ -# 多语言支持实现总结 - -## 已完成的工作 - -### 后端部分 ✅ - -1. **语言资源文件** (`backend/src/main/resources/i18n/`) - - `messages_zh_CN.properties` - 简体中文 - - `messages_zh_TW.properties` - 繁体中文 - - `messages_en.properties` - 英文 - - 已翻译所有 ErrorCode 错误消息(100+ 条) - -2. **配置类** - - `MessageSourceConfig.kt` - 配置 MessageSource 和 LocaleResolver - - `LocaleInterceptor.kt` - 从 HTTP Header 读取语言设置 - -3. **核心修改** - - `ErrorCode.kt` - 添加 `messageKey` 字段,每个错误码都有对应的消息键 - - `ApiResponse.kt` - 支持多语言错误消息 - - `ApiResponseExt.kt` - 提供便捷的扩展函数 - - `MessageUtils.kt` - 消息工具类 - - `WebMvcConfig.kt` - 注册 LocaleInterceptor - -4. **示例更新** - - `AccountController.kt` - 已更新部分方法作为示例 - -### 前端部分 ✅ - -1. **依赖安装** - - `i18next` 和 `react-i18next` 已安装 - -2. **i18n 配置** (`frontend/src/i18n/config.ts`) - - 自动检测系统语言 - - 支持语言切换 - - 语言持久化到 localStorage - -3. **语言包** (`frontend/src/locales/`) - - `zh-CN/common.json` - 简体中文 - - `zh-TW/common.json` - 繁体中文 - - `en/common.json` - 英文 - - 已包含账户管理相关的基础翻译 - -4. **API 集成** - - `api.ts` - 自动在请求头添加 `X-Language` Header - -5. **示例页面** - - `AccountDetail.tsx` - 已完全使用 i18n - -## 待完成的工作 - -### 后端部分 - -需要更新所有 Controller,使用 `MessageUtils` 或扩展函数来获取国际化消息: - -```kotlin -// 方式1:使用扩展函数(推荐) -@RestController -class ExampleController( - private val messageSource: MessageSource -) { - @PostMapping("/example") - fun example(): ResponseEntity> { - // 使用扩展函数,自动国际化 - return ResponseEntity.ok( - ApiResponse.error(ErrorCode.PARAM_ERROR, messageSource = messageSource) - ) - } -} - -// 方式2:使用 MessageUtils -@RestController -class ExampleController( - private val messageUtils: MessageUtils -) { - @PostMapping("/example") - fun example(): ResponseEntity> { - val msg = messageUtils.getMessage(ErrorCode.PARAM_ERROR) - return ResponseEntity.ok( - ApiResponse.error(ErrorCode.PARAM_ERROR.code, msg) - ) - } -} -``` - -需要更新的 Controller: -- `AuthController.kt` -- `LeaderController.kt` -- `CopyTradingController.kt` -- `CopyTradingTemplateController.kt` -- `CopyTradingStatisticsController.kt` -- `MarketController.kt` -- `UserController.kt` -- `ProxyConfigController.kt` -- `AccountController.kt` (部分方法已更新,需要完成剩余部分) - -### 前端部分 - -需要更新所有页面组件,使用 `useTranslation` Hook: - -```typescript -import { useTranslation } from 'react-i18next' - -const MyComponent: React.FC = () => { - const { t } = useTranslation() - - return ( -
- -
{t('account.accountName')}
-
- ) -} -``` - -需要更新的页面: -- `AccountList.tsx` -- `AccountImport.tsx` -- `AccountEdit.tsx` -- `LeaderList.tsx` -- `LeaderAdd.tsx` -- `LeaderEdit.tsx` -- `TemplateList.tsx` -- `TemplateAdd.tsx` -- `TemplateEdit.tsx` -- `CopyTradingList.tsx` -- `CopyTradingAdd.tsx` -- `Login.tsx` -- `UserList.tsx` -- 以及其他所有页面 - -## 使用说明 - -### 后端使用 - -1. **在 Controller 中注入 MessageSource**: -```kotlin -@RestController -class MyController( - private val messageSource: MessageSource -) { - // ... -} -``` - -2. **使用扩展函数创建错误响应**: -```kotlin -ApiResponse.error(ErrorCode.PARAM_ERROR, messageSource = messageSource) -``` - -3. **自定义消息(如果需要)**: -```kotlin -ApiResponse.error(ErrorCode.PARAM_ERROR, "自定义消息", messageSource) -``` - -### 前端使用 - -1. **在组件中使用 useTranslation**: -```typescript -import { useTranslation } from 'react-i18next' - -const { t } = useTranslation() -``` - -2. **翻译文本**: -```typescript -t('common.save') // 返回当前语言的"保存" -t('account.accountName') // 返回当前语言的"账户名称" -``` - -3. **切换语言**(可选): -```typescript -import { changeLanguage } from '../i18n/config' - -changeLanguage('zh-CN') // 切换到简体中文 -changeLanguage('zh-TW') // 切换到繁体中文 -changeLanguage('en') // 切换到英文 -``` - -## 语言检测规则 - -### 前端 -1. 优先使用 localStorage 中保存的语言设置 -2. 如果没有,检测系统语言: - - `zh-CN`, `zh` → 简体中文 - - `zh-TW`, `zh-HK`, `zh-MO` → 繁体中文 - - 其他 → 英文(默认) - -### 后端 -1. 从 HTTP Header 读取: - - `X-Language` (优先) - - `Accept-Language` (备选) -2. 语言映射规则同前端 -3. 默认语言:英文 - -## 测试建议 - -1. **测试语言检测**: - - 修改浏览器语言设置,刷新页面,检查是否自动切换 - - 测试不同语言下的 API 响应消息 - -2. **测试语言切换**: - - 在前端添加语言切换组件(可选) - - 测试切换后 API 请求是否正确传递语言 Header - -3. **测试错误消息**: - - 触发各种错误,检查错误消息是否正确显示对应语言 - -## 注意事项 - -1. **向后兼容**:如果前端没有发送语言 Header,后端默认使用英文 -2. **消息键命名**:所有消息键使用 `error.` 前缀,便于管理 -3. **翻译质量**:建议由专业翻译人员审核翻译内容 -4. **性能**:语言包已配置缓存,不会影响性能 - -## 下一步 - -1. 完成所有 Controller 的更新 -2. 完成所有前端页面的翻译 -3. 添加语言切换组件(可选) -4. 全面测试多语言功能 -5. 更新 API 文档,说明语言 Header 的使用 - - - -## 已完成的工作 - -### 后端部分 ✅ - -1. **语言资源文件** (`backend/src/main/resources/i18n/`) - - `messages_zh_CN.properties` - 简体中文 - - `messages_zh_TW.properties` - 繁体中文 - - `messages_en.properties` - 英文 - - 已翻译所有 ErrorCode 错误消息(100+ 条) - -2. **配置类** - - `MessageSourceConfig.kt` - 配置 MessageSource 和 LocaleResolver - - `LocaleInterceptor.kt` - 从 HTTP Header 读取语言设置 - -3. **核心修改** - - `ErrorCode.kt` - 添加 `messageKey` 字段,每个错误码都有对应的消息键 - - `ApiResponse.kt` - 支持多语言错误消息 - - `ApiResponseExt.kt` - 提供便捷的扩展函数 - - `MessageUtils.kt` - 消息工具类 - - `WebMvcConfig.kt` - 注册 LocaleInterceptor - -4. **示例更新** - - `AccountController.kt` - 已更新部分方法作为示例 - -### 前端部分 ✅ - -1. **依赖安装** - - `i18next` 和 `react-i18next` 已安装 - -2. **i18n 配置** (`frontend/src/i18n/config.ts`) - - 自动检测系统语言 - - 支持语言切换 - - 语言持久化到 localStorage - -3. **语言包** (`frontend/src/locales/`) - - `zh-CN/common.json` - 简体中文 - - `zh-TW/common.json` - 繁体中文 - - `en/common.json` - 英文 - - 已包含账户管理相关的基础翻译 - -4. **API 集成** - - `api.ts` - 自动在请求头添加 `X-Language` Header - -5. **示例页面** - - `AccountDetail.tsx` - 已完全使用 i18n - -## 待完成的工作 - -### 后端部分 - -需要更新所有 Controller,使用 `MessageUtils` 或扩展函数来获取国际化消息: - -```kotlin -// 方式1:使用扩展函数(推荐) -@RestController -class ExampleController( - private val messageSource: MessageSource -) { - @PostMapping("/example") - fun example(): ResponseEntity> { - // 使用扩展函数,自动国际化 - return ResponseEntity.ok( - ApiResponse.error(ErrorCode.PARAM_ERROR, messageSource = messageSource) - ) - } -} - -// 方式2:使用 MessageUtils -@RestController -class ExampleController( - private val messageUtils: MessageUtils -) { - @PostMapping("/example") - fun example(): ResponseEntity> { - val msg = messageUtils.getMessage(ErrorCode.PARAM_ERROR) - return ResponseEntity.ok( - ApiResponse.error(ErrorCode.PARAM_ERROR.code, msg) - ) - } -} -``` - -需要更新的 Controller: -- `AuthController.kt` -- `LeaderController.kt` -- `CopyTradingController.kt` -- `CopyTradingTemplateController.kt` -- `CopyTradingStatisticsController.kt` -- `MarketController.kt` -- `UserController.kt` -- `ProxyConfigController.kt` -- `AccountController.kt` (部分方法已更新,需要完成剩余部分) - -### 前端部分 - -需要更新所有页面组件,使用 `useTranslation` Hook: - -```typescript -import { useTranslation } from 'react-i18next' - -const MyComponent: React.FC = () => { - const { t } = useTranslation() - - return ( -
- -
{t('account.accountName')}
-
- ) -} -``` - -需要更新的页面: -- `AccountList.tsx` -- `AccountImport.tsx` -- `AccountEdit.tsx` -- `LeaderList.tsx` -- `LeaderAdd.tsx` -- `LeaderEdit.tsx` -- `TemplateList.tsx` -- `TemplateAdd.tsx` -- `TemplateEdit.tsx` -- `CopyTradingList.tsx` -- `CopyTradingAdd.tsx` -- `Login.tsx` -- `UserList.tsx` -- 以及其他所有页面 - -## 使用说明 - -### 后端使用 - -1. **在 Controller 中注入 MessageSource**: -```kotlin -@RestController -class MyController( - private val messageSource: MessageSource -) { - // ... -} -``` - -2. **使用扩展函数创建错误响应**: -```kotlin -ApiResponse.error(ErrorCode.PARAM_ERROR, messageSource = messageSource) -``` - -3. **自定义消息(如果需要)**: -```kotlin -ApiResponse.error(ErrorCode.PARAM_ERROR, "自定义消息", messageSource) -``` - -### 前端使用 - -1. **在组件中使用 useTranslation**: -```typescript -import { useTranslation } from 'react-i18next' - -const { t } = useTranslation() -``` - -2. **翻译文本**: -```typescript -t('common.save') // 返回当前语言的"保存" -t('account.accountName') // 返回当前语言的"账户名称" -``` - -3. **切换语言**(可选): -```typescript -import { changeLanguage } from '../i18n/config' - -changeLanguage('zh-CN') // 切换到简体中文 -changeLanguage('zh-TW') // 切换到繁体中文 -changeLanguage('en') // 切换到英文 -``` - -## 语言检测规则 - -### 前端 -1. 优先使用 localStorage 中保存的语言设置 -2. 如果没有,检测系统语言: - - `zh-CN`, `zh` → 简体中文 - - `zh-TW`, `zh-HK`, `zh-MO` → 繁体中文 - - 其他 → 英文(默认) - -### 后端 -1. 从 HTTP Header 读取: - - `X-Language` (优先) - - `Accept-Language` (备选) -2. 语言映射规则同前端 -3. 默认语言:英文 - -## 测试建议 - -1. **测试语言检测**: - - 修改浏览器语言设置,刷新页面,检查是否自动切换 - - 测试不同语言下的 API 响应消息 - -2. **测试语言切换**: - - 在前端添加语言切换组件(可选) - - 测试切换后 API 请求是否正确传递语言 Header - -3. **测试错误消息**: - - 触发各种错误,检查错误消息是否正确显示对应语言 - -## 注意事项 - -1. **向后兼容**:如果前端没有发送语言 Header,后端默认使用英文 -2. **消息键命名**:所有消息键使用 `error.` 前缀,便于管理 -3. **翻译质量**:建议由专业翻译人员审核翻译内容 -4. **性能**:语言包已配置缓存,不会影响性能 - -## 下一步 - -1. 完成所有 Controller 的更新 -2. 完成所有前端页面的翻译 -3. 添加语言切换组件(可选) -4. 全面测试多语言功能 -5. 更新 API 文档,说明语言 Header 的使用 - diff --git a/docs/i18n-language-switcher-summary.md b/docs/i18n-language-switcher-summary.md deleted file mode 100644 index e45bf8e..0000000 --- a/docs/i18n-language-switcher-summary.md +++ /dev/null @@ -1,89 +0,0 @@ -# 前端语言切换功能实现总结 - -## ✅ 已完成的工作 - -### 1. 语言切换组件 ✅ -- **文件**: `frontend/src/components/LanguageSwitcher.tsx` -- **功能**: - - 显示当前语言(简体中文、繁體中文、English) - - 支持手动切换语言 - - 切换后自动刷新页面以应用 Ant Design 的 locale - - 支持移动端和桌面端响应式设计 - -### 2. Layout 组件集成 ✅ -- **文件**: `frontend/src/components/Layout.tsx` -- **更新内容**: - - 在移动端 Header 中添加了语言切换器 - - 在桌面端 Sider 中添加了语言切换器 - - 菜单项已使用 i18n 翻译(menu.*) - - 退出登录确认对话框已使用 i18n - -### 3. i18n 配置优化 ✅ -- **文件**: `frontend/src/i18n/config.ts` -- **功能**: - - 自动检测系统语言 - - 优先使用 localStorage 中保存的用户选择 - - 提供 `changeLanguage()` 函数用于手动切换 - - 提供 `getCurrentLanguage()` 函数获取当前语言 - -### 4. 语言包扩展 ✅ -- **新增翻译键**: `menu.*` - - `menu.accounts`: 账户管理 - - `menu.copyTrading`: 跟单交易 - - `menu.leaders`: Leader 管理 - - `menu.templates`: 跟单模板 - - `menu.copyTradingConfig`: 跟单配置 - - `menu.positions`: 仓位管理 - - `menu.statistics`: 统计信息 - - `menu.users`: 用户管理 - - `menu.systemSettings`: 系统管理 - - `menu.logout`: 退出登录 - - `menu.logoutConfirm`: 确认退出 - - `menu.logoutConfirmDesc`: 确定要退出登录吗? - - `menu.navigation`: 导航菜单 - -### 5. API 请求同步 ✅ -- **文件**: `frontend/src/services/api.ts` -- **功能**: 自动在请求头中添加 `X-Language`,与后端语言保持一致 - -## 使用方式 - -### 用户操作 -1. 在页面右上角(桌面端)或 Header(移动端)找到语言切换器 -2. 点击下拉菜单选择语言: - - 简体中文 - - 繁體中文 - - English -3. 选择后页面会自动刷新,应用新语言 - -### 技术实现 -- 语言选择保存在 `localStorage` 的 `i18n_language` 键中 -- 切换语言后自动刷新页面,确保: - - Ant Design 的 locale 正确应用 - - 所有组件重新渲染,使用新语言 - - API 请求自动携带新语言 Header - -## 编译状态 -- ✅ 前端编译通过,无错误 -- ✅ 所有组件正常工作 - -## 位置说明 - -### 桌面端 -- 语言切换器位于左侧导航栏顶部(PolyHermes 标题旁边) - -### 移动端 -- 语言切换器位于顶部 Header 右侧(GitHub 和 Twitter 图标之前) - -## 后续优化建议 - -1. **无需刷新页面切换**(可选): - - 当前实现需要刷新页面以确保 Ant Design locale 正确应用 - - 未来可以考虑动态更新 ConfigProvider 的 locale,避免刷新 - -2. **语言图标优化**(可选): - - 可以使用国旗图标或语言缩写(如 CN、TW、EN)代替文字 - -3. **语言切换动画**(可选): - - 添加平滑的切换动画,提升用户体验 - diff --git a/docs/i18n-missing-work-checklist.md b/docs/i18n-missing-work-checklist.md deleted file mode 100644 index 5733e2e..0000000 --- a/docs/i18n-missing-work-checklist.md +++ /dev/null @@ -1,572 +0,0 @@ -# 多语言支持遗漏工作清单 - -## 后端遗漏工作 - -### ❌ 需要更新的 Controller - -#### 1. CopyTradingController -- ✅ 已添加 MessageSource 依赖注入 -- ❌ **所有 ApiResponse.error() 调用都缺少 messageSource 参数** - - 第 31 行:`ApiResponse.error(ErrorCode.PARAM_ACCOUNT_ID_INVALID)` → 需要添加 `, messageSource = messageSource` - - 第 34 行:`ApiResponse.error(ErrorCode.PARAM_TEMPLATE_ID_INVALID)` → 需要添加 - - 第 37 行:`ApiResponse.error(ErrorCode.PARAM_LEADER_ID_INVALID)` → 需要添加 - - 第 48-49 行:`ApiResponse.error(ErrorCode.PARAM_ERROR, e.message)` → 需要添加 `, messageSource` - - 第 55 行:`ApiResponse.error(ErrorCode.SERVER_COPY_TRADING_CREATE_FAILED, e.message)` → 需要添加 - - 第 72, 77 行:查询列表的错误响应 → 需要添加 - - 第 88, 99-101, 107 行:更新状态的错误响应 → 需要添加 - - 第 118, 129-130, 136 行:删除的错误响应 → 需要添加 - - 第 147, 158-159, 165 行:查询模板的错误响应 → 需要添加 - -#### 2. CopyTradingTemplateController -- ❌ **未添加 MessageSource 依赖注入** -- ❌ **所有 ApiResponse.error() 调用都需要更新** - - 需要添加:`private val messageSource: MessageSource` - - 需要添加导入:`import com.wrbug.polymarketbot.util.error` - - 所有 `ApiResponse.error()` 调用都需要添加 `messageSource` 参数 - -#### 3. MarketController -- ❌ **未添加 MessageSource 依赖注入** -- ❌ **所有 ApiResponse.error() 调用都需要更新** - - 第 34 行:`ApiResponse.error(ErrorCode.PARAM_MARKET_ID_EMPTY)` - - 第 44, 49 行:`ApiResponse.error(ErrorCode.SERVER_MARKET_PRICE_FETCH_FAILED, e.message)` - - 第 62 行:`ApiResponse.error(ErrorCode.PARAM_TOKEN_ID_EMPTY)` - - 第 72, 77 行:`ApiResponse.error(ErrorCode.SERVER_MARKET_LATEST_PRICE_FETCH_FAILED, e.message)` - -#### 4. CopyTradingStatisticsController -- ❌ **未添加 MessageSource 依赖注入** -- ❌ **所有 ApiResponse.error() 调用都需要更新** - - 第 31 行:`ApiResponse.error(ErrorCode.PARAM_COPY_TRADING_ID_INVALID)` - - 第 42-43, 49 行:统计查询的错误响应 - - 需要检查整个文件的所有错误响应 - -#### 5. ProxyConfigController -- ❌ **未添加 MessageSource 依赖注入** -- ❌ **所有 ApiResponse.error() 调用都需要更新** - - 第 35 行:`ApiResponse.error(ErrorCode.SERVER_ERROR, "获取代理配置失败:${e.message}")` - - 第 49 行:`ApiResponse.error(ErrorCode.SERVER_ERROR, "获取代理配置列表失败:${e.message}")` - - 需要检查整个文件的所有错误响应 - -#### 6. HealthController -- ✅ **不需要更新**(没有错误响应,只有健康检查) - -### 更新模式 - -对于每个 Controller,需要: - -1. **添加导入**: -```kotlin -import com.wrbug.polymarketbot.util.error -import org.springframework.context.MessageSource -``` - -2. **添加依赖注入**: -```kotlin -class YourController( - private val yourService: YourService, - private val messageSource: MessageSource // 添加这一行 -) -``` - -3. **更新所有 ApiResponse.error() 调用**: -```kotlin -// 模式1:只有 ErrorCode -ApiResponse.error(ErrorCode.PARAM_ERROR) -→ ApiResponse.error(ErrorCode.PARAM_ERROR, messageSource = messageSource) - -// 模式2:ErrorCode + 消息 -ApiResponse.error(ErrorCode.PARAM_ERROR, e.message) -→ ApiResponse.error(ErrorCode.PARAM_ERROR, e.message, messageSource) -``` - -## 前端遗漏工作 - -### ❌ 需要更新的页面组件(23个页面,只有1个已更新) - -#### ✅ 已完成的页面 -1. **AccountDetail.tsx** - 已完全使用 i18n - -#### ❌ 待更新的页面(22个) - -1. **Login.tsx** - - 硬编码文本:`'登录成功'`, `'登录失败'`, `'用户名'`, `'密码'` 等 - - 需要添加:`import { useTranslation } from 'react-i18next'` - - 需要添加:`const { t } = useTranslation()` - - 需要替换所有硬编码文本 - -2. **AccountList.tsx** - - 需要检查并替换所有硬编码文本 - -3. **AccountImport.tsx** - - 硬编码文本:`'钱包地址与私钥不匹配'`, `'钱包地址格式不正确'`, `'钱包地址'` 等 - - 需要更新 - -4. **AccountEdit.tsx** - - 需要检查并替换所有硬编码文本 - -5. **LeaderList.tsx** - - 需要检查并替换所有硬编码文本 - -6. **LeaderAdd.tsx** - - 需要检查并替换所有硬编码文本 - -7. **LeaderEdit.tsx** - - 需要检查并替换所有硬编码文本 - -8. **TemplateList.tsx** - - 需要检查并替换所有硬编码文本 - -9. **TemplateAdd.tsx** - - 需要检查并替换所有硬编码文本 - -10. **TemplateEdit.tsx** - - 需要检查并替换所有硬编码文本 - -11. **CopyTradingList.tsx** - - 硬编码文本:`'删除跟单成功'`, `'删除跟单失败'`, `'钱包'`, `'模板'`, `'Leader'`, `'取消'`, `'订单'` 等 - - 需要更新 - -12. **CopyTradingAdd.tsx** - - 需要检查并替换所有硬编码文本 - -13. **CopyTradingStatistics.tsx** - - 需要检查并替换所有硬编码文本 - -14. **CopyTradingBuyOrders.tsx** - - 需要检查并替换所有硬编码文本 - -15. **CopyTradingSellOrders.tsx** - - 需要检查并替换所有硬编码文本 - -16. **CopyTradingMatchedOrders.tsx** - - 需要检查并替换所有硬编码文本 - -17. **PositionList.tsx** - - 硬编码文本:`'账户'`, `'市场'`, `'搜索账户、市场、方向...'`, `'确认卖出'`, `'取消'`, `'订单类型'`, `'确认赎回'` 等 - - 需要更新 - -18. **OrderList.tsx** - - 需要检查并替换所有硬编码文本 - -19. **Statistics.tsx** - - 需要检查并替换所有硬编码文本 - -20. **UserList.tsx** - - 需要检查并替换所有硬编码文本 - -21. **ResetPassword.tsx** - - 需要检查并替换所有硬编码文本 - -22. **SystemSettings.tsx** - - 需要检查并替换所有硬编码文本 - -23. **ConfigPage.tsx** - - 需要检查并替换所有硬编码文本 - -### App.tsx 中的硬编码文本 - -**App.tsx** 中有硬编码的中文文本: -- 第 61 行:`'订单创建'` -- 第 63 行:`'订单更新'` -- 第 65 行:`'订单取消'` -- 第 67 行:`'订单事件'` -- 第 79 行:`'买入'`, `'卖出'` -- 第 92 行:`'市场:'`, `'状态:'`, `'已成交:'`, `'剩余:'` - -需要: -1. 添加 `import { useTranslation } from 'react-i18next'` -2. 在组件中使用 `const { t } = useTranslation()` -3. 替换所有硬编码文本 - -### 前端更新模式 - -对于每个页面组件: - -1. **添加导入**: -```typescript -import { useTranslation } from 'react-i18next' -``` - -2. **在组件中使用**: -```typescript -const YourComponent: React.FC = () => { - const { t } = useTranslation() - // ... -} -``` - -3. **替换硬编码文本**: -```typescript -// 旧代码 - -message.success('操作成功') - -// 新代码 - -message.success(t('message.operationSuccess')) -``` - -4. **扩展语言包**: - - 在 `frontend/src/locales/*/common.json` 中添加缺失的翻译键 - - 确保三个语言包(zh-CN, zh-TW, en)都有对应的翻译 - -## 语言包扩展 - -### 需要添加的翻译键 - -根据检查,需要在语言包中添加以下键: - -```json -{ - "order": { - "create": "订单创建", - "update": "订单更新", - "cancel": "订单取消", - "event": "订单事件", - "buy": "买入", - "sell": "卖出", - "market": "市场", - "status": "状态", - "filled": "已成交", - "remaining": "剩余" - }, - "wallet": { - "address": "钱包地址", - "addressMismatch": "钱包地址与私钥不匹配", - "addressInvalid": "钱包地址格式不正确" - }, - "copyTrading": { - "deleteSuccess": "删除跟单成功", - "deleteFailed": "删除跟单失败", - "wallet": "钱包", - "template": "模板", - "leader": "Leader" - }, - "position": { - "account": "账户", - "market": "市场", - "searchPlaceholder": "搜索账户、市场、方向...", - "confirmSell": "确认卖出", - "confirmRedeem": "确认赎回", - "orderType": "订单类型" - } -} -``` - -## 优先级建议 - -### 高优先级(核心功能) -1. ✅ 后端:完成所有 Controller 的 MessageSource 更新 -2. ✅ 前端:更新 Login.tsx(登录页面) -3. ✅ 前端:更新 App.tsx(全局通知) - -### 中优先级(常用功能) -4. 前端:更新 AccountList.tsx, AccountImport.tsx -5. 前端:更新 LeaderList.tsx, LeaderAdd.tsx -6. 前端:更新 CopyTradingList.tsx - -### 低优先级(其他页面) -7. 前端:逐步更新其他页面组件 - -## 验证清单 - -完成后检查: - -### 后端 -- [ ] 所有 Controller 都注入了 MessageSource -- [ ] 所有 ApiResponse.error() 调用都传入了 messageSource -- [ ] 编译无错误 -- [ ] 测试不同语言下的错误消息 - -### 前端 -- [ ] 所有页面都使用了 useTranslation -- [ ] 所有硬编码文本都已替换 -- [ ] 语言包包含所有需要的翻译键 -- [ ] 测试不同语言下的页面显示 -- [ ] 测试语言切换功能 - - - -## 后端遗漏工作 - -### ❌ 需要更新的 Controller - -#### 1. CopyTradingController -- ✅ 已添加 MessageSource 依赖注入 -- ❌ **所有 ApiResponse.error() 调用都缺少 messageSource 参数** - - 第 31 行:`ApiResponse.error(ErrorCode.PARAM_ACCOUNT_ID_INVALID)` → 需要添加 `, messageSource = messageSource` - - 第 34 行:`ApiResponse.error(ErrorCode.PARAM_TEMPLATE_ID_INVALID)` → 需要添加 - - 第 37 行:`ApiResponse.error(ErrorCode.PARAM_LEADER_ID_INVALID)` → 需要添加 - - 第 48-49 行:`ApiResponse.error(ErrorCode.PARAM_ERROR, e.message)` → 需要添加 `, messageSource` - - 第 55 行:`ApiResponse.error(ErrorCode.SERVER_COPY_TRADING_CREATE_FAILED, e.message)` → 需要添加 - - 第 72, 77 行:查询列表的错误响应 → 需要添加 - - 第 88, 99-101, 107 行:更新状态的错误响应 → 需要添加 - - 第 118, 129-130, 136 行:删除的错误响应 → 需要添加 - - 第 147, 158-159, 165 行:查询模板的错误响应 → 需要添加 - -#### 2. CopyTradingTemplateController -- ❌ **未添加 MessageSource 依赖注入** -- ❌ **所有 ApiResponse.error() 调用都需要更新** - - 需要添加:`private val messageSource: MessageSource` - - 需要添加导入:`import com.wrbug.polymarketbot.util.error` - - 所有 `ApiResponse.error()` 调用都需要添加 `messageSource` 参数 - -#### 3. MarketController -- ❌ **未添加 MessageSource 依赖注入** -- ❌ **所有 ApiResponse.error() 调用都需要更新** - - 第 34 行:`ApiResponse.error(ErrorCode.PARAM_MARKET_ID_EMPTY)` - - 第 44, 49 行:`ApiResponse.error(ErrorCode.SERVER_MARKET_PRICE_FETCH_FAILED, e.message)` - - 第 62 行:`ApiResponse.error(ErrorCode.PARAM_TOKEN_ID_EMPTY)` - - 第 72, 77 行:`ApiResponse.error(ErrorCode.SERVER_MARKET_LATEST_PRICE_FETCH_FAILED, e.message)` - -#### 4. CopyTradingStatisticsController -- ❌ **未添加 MessageSource 依赖注入** -- ❌ **所有 ApiResponse.error() 调用都需要更新** - - 第 31 行:`ApiResponse.error(ErrorCode.PARAM_COPY_TRADING_ID_INVALID)` - - 第 42-43, 49 行:统计查询的错误响应 - - 需要检查整个文件的所有错误响应 - -#### 5. ProxyConfigController -- ❌ **未添加 MessageSource 依赖注入** -- ❌ **所有 ApiResponse.error() 调用都需要更新** - - 第 35 行:`ApiResponse.error(ErrorCode.SERVER_ERROR, "获取代理配置失败:${e.message}")` - - 第 49 行:`ApiResponse.error(ErrorCode.SERVER_ERROR, "获取代理配置列表失败:${e.message}")` - - 需要检查整个文件的所有错误响应 - -#### 6. HealthController -- ✅ **不需要更新**(没有错误响应,只有健康检查) - -### 更新模式 - -对于每个 Controller,需要: - -1. **添加导入**: -```kotlin -import com.wrbug.polymarketbot.util.error -import org.springframework.context.MessageSource -``` - -2. **添加依赖注入**: -```kotlin -class YourController( - private val yourService: YourService, - private val messageSource: MessageSource // 添加这一行 -) -``` - -3. **更新所有 ApiResponse.error() 调用**: -```kotlin -// 模式1:只有 ErrorCode -ApiResponse.error(ErrorCode.PARAM_ERROR) -→ ApiResponse.error(ErrorCode.PARAM_ERROR, messageSource = messageSource) - -// 模式2:ErrorCode + 消息 -ApiResponse.error(ErrorCode.PARAM_ERROR, e.message) -→ ApiResponse.error(ErrorCode.PARAM_ERROR, e.message, messageSource) -``` - -## 前端遗漏工作 - -### ❌ 需要更新的页面组件(23个页面,只有1个已更新) - -#### ✅ 已完成的页面 -1. **AccountDetail.tsx** - 已完全使用 i18n - -#### ❌ 待更新的页面(22个) - -1. **Login.tsx** - - 硬编码文本:`'登录成功'`, `'登录失败'`, `'用户名'`, `'密码'` 等 - - 需要添加:`import { useTranslation } from 'react-i18next'` - - 需要添加:`const { t } = useTranslation()` - - 需要替换所有硬编码文本 - -2. **AccountList.tsx** - - 需要检查并替换所有硬编码文本 - -3. **AccountImport.tsx** - - 硬编码文本:`'钱包地址与私钥不匹配'`, `'钱包地址格式不正确'`, `'钱包地址'` 等 - - 需要更新 - -4. **AccountEdit.tsx** - - 需要检查并替换所有硬编码文本 - -5. **LeaderList.tsx** - - 需要检查并替换所有硬编码文本 - -6. **LeaderAdd.tsx** - - 需要检查并替换所有硬编码文本 - -7. **LeaderEdit.tsx** - - 需要检查并替换所有硬编码文本 - -8. **TemplateList.tsx** - - 需要检查并替换所有硬编码文本 - -9. **TemplateAdd.tsx** - - 需要检查并替换所有硬编码文本 - -10. **TemplateEdit.tsx** - - 需要检查并替换所有硬编码文本 - -11. **CopyTradingList.tsx** - - 硬编码文本:`'删除跟单成功'`, `'删除跟单失败'`, `'钱包'`, `'模板'`, `'Leader'`, `'取消'`, `'订单'` 等 - - 需要更新 - -12. **CopyTradingAdd.tsx** - - 需要检查并替换所有硬编码文本 - -13. **CopyTradingStatistics.tsx** - - 需要检查并替换所有硬编码文本 - -14. **CopyTradingBuyOrders.tsx** - - 需要检查并替换所有硬编码文本 - -15. **CopyTradingSellOrders.tsx** - - 需要检查并替换所有硬编码文本 - -16. **CopyTradingMatchedOrders.tsx** - - 需要检查并替换所有硬编码文本 - -17. **PositionList.tsx** - - 硬编码文本:`'账户'`, `'市场'`, `'搜索账户、市场、方向...'`, `'确认卖出'`, `'取消'`, `'订单类型'`, `'确认赎回'` 等 - - 需要更新 - -18. **OrderList.tsx** - - 需要检查并替换所有硬编码文本 - -19. **Statistics.tsx** - - 需要检查并替换所有硬编码文本 - -20. **UserList.tsx** - - 需要检查并替换所有硬编码文本 - -21. **ResetPassword.tsx** - - 需要检查并替换所有硬编码文本 - -22. **SystemSettings.tsx** - - 需要检查并替换所有硬编码文本 - -23. **ConfigPage.tsx** - - 需要检查并替换所有硬编码文本 - -### App.tsx 中的硬编码文本 - -**App.tsx** 中有硬编码的中文文本: -- 第 61 行:`'订单创建'` -- 第 63 行:`'订单更新'` -- 第 65 行:`'订单取消'` -- 第 67 行:`'订单事件'` -- 第 79 行:`'买入'`, `'卖出'` -- 第 92 行:`'市场:'`, `'状态:'`, `'已成交:'`, `'剩余:'` - -需要: -1. 添加 `import { useTranslation } from 'react-i18next'` -2. 在组件中使用 `const { t } = useTranslation()` -3. 替换所有硬编码文本 - -### 前端更新模式 - -对于每个页面组件: - -1. **添加导入**: -```typescript -import { useTranslation } from 'react-i18next' -``` - -2. **在组件中使用**: -```typescript -const YourComponent: React.FC = () => { - const { t } = useTranslation() - // ... -} -``` - -3. **替换硬编码文本**: -```typescript -// 旧代码 - -message.success('操作成功') - -// 新代码 - -message.success(t('message.operationSuccess')) -``` - -4. **扩展语言包**: - - 在 `frontend/src/locales/*/common.json` 中添加缺失的翻译键 - - 确保三个语言包(zh-CN, zh-TW, en)都有对应的翻译 - -## 语言包扩展 - -### 需要添加的翻译键 - -根据检查,需要在语言包中添加以下键: - -```json -{ - "order": { - "create": "订单创建", - "update": "订单更新", - "cancel": "订单取消", - "event": "订单事件", - "buy": "买入", - "sell": "卖出", - "market": "市场", - "status": "状态", - "filled": "已成交", - "remaining": "剩余" - }, - "wallet": { - "address": "钱包地址", - "addressMismatch": "钱包地址与私钥不匹配", - "addressInvalid": "钱包地址格式不正确" - }, - "copyTrading": { - "deleteSuccess": "删除跟单成功", - "deleteFailed": "删除跟单失败", - "wallet": "钱包", - "template": "模板", - "leader": "Leader" - }, - "position": { - "account": "账户", - "market": "市场", - "searchPlaceholder": "搜索账户、市场、方向...", - "confirmSell": "确认卖出", - "confirmRedeem": "确认赎回", - "orderType": "订单类型" - } -} -``` - -## 优先级建议 - -### 高优先级(核心功能) -1. ✅ 后端:完成所有 Controller 的 MessageSource 更新 -2. ✅ 前端:更新 Login.tsx(登录页面) -3. ✅ 前端:更新 App.tsx(全局通知) - -### 中优先级(常用功能) -4. 前端:更新 AccountList.tsx, AccountImport.tsx -5. 前端:更新 LeaderList.tsx, LeaderAdd.tsx -6. 前端:更新 CopyTradingList.tsx - -### 低优先级(其他页面) -7. 前端:逐步更新其他页面组件 - -## 验证清单 - -完成后检查: - -### 后端 -- [ ] 所有 Controller 都注入了 MessageSource -- [ ] 所有 ApiResponse.error() 调用都传入了 messageSource -- [ ] 编译无错误 -- [ ] 测试不同语言下的错误消息 - -### 前端 -- [ ] 所有页面都使用了 useTranslation -- [ ] 所有硬编码文本都已替换 -- [ ] 语言包包含所有需要的翻译键 -- [ ] 测试不同语言下的页面显示 -- [ ] 测试语言切换功能 - diff --git a/frontend/src/pages/PositionList.tsx b/frontend/src/pages/PositionList.tsx index d1db94c..93a2bd8 100644 --- a/frontend/src/pages/PositionList.tsx +++ b/frontend/src/pages/PositionList.tsx @@ -367,7 +367,10 @@ const PositionList: React.FC = () => { // 加载市场价格 try { - const response = await apiService.markets.getMarketPrice({ marketId: position.marketId }) + const response = await apiService.markets.getMarketPrice({ + marketId: position.marketId, + outcomeIndex: position.outcomeIndex // 传递结果索引,用于确定需要查询哪个 outcome 的价格 + }) if (response.data.code === 0 && response.data.data) { setMarketPrice(response.data.data) // 默认使用最优买价作为限价 diff --git a/frontend/src/services/api.ts b/frontend/src/services/api.ts index e73d8d4..f80c8f1 100644 --- a/frontend/src/services/api.ts +++ b/frontend/src/services/api.ts @@ -239,7 +239,7 @@ export const apiService = { /** * 获取市场价格(通过 Gamma API) */ - getMarketPrice: (data: { marketId: string }) => + getMarketPrice: (data: { marketId: string; outcomeIndex?: number }) => apiClient.post>('/copy-trading/markets/price', data), /**