feat: 改进下单错误处理和市场价格接口

- 修复市场价格接口:移除side参数,使用outcomeIndex判断方向
- 添加规范:禁止使用YES/NO字符串判断side,必须使用outcomeIndex
- 改进所有下单错误处理:所有错误都打印详细日志并存入数据库
  - AccountService.sellPosition: 添加完整错误日志
  - CopyOrderTrackingService.createOrderWithRetry: 所有失败都记录详细日志
  - recordFailedTrade: 改进错误信息存储,包含堆栈信息
  - PolymarketClobService.createSignedOrder: 添加完整错误日志
- 前端:更新市场价格接口调用,使用outcomeIndex替代side参数
- 清理:删除过时的i18n文档
This commit is contained in:
WrBug
2025-12-05 00:23:44 +08:00
parent 1a9407c544
commit 41596887c9
18 changed files with 215 additions and 1841 deletions
+31
View File
@@ -504,3 +504,34 @@ data class ApiResponse<T>(
- ❌ 禁止使用GET、PUT、DELETE等方法(统一使用POST - ❌ 禁止使用GET、PUT、DELETE等方法(统一使用POST
- ❌ 禁止返回不符合统一格式的响应 - ❌ 禁止返回不符合统一格式的响应
- ❌ 禁止在响应中直接返回Map类型(使用data class - ❌ 禁止在响应中直接返回Map类型(使用data class
### Side 判断规范
- ❌ **禁止使用 "YES" 或 "NO" 字符串去判断 side**
- ✅ **必须使用 `outcomeIndex` 来判断方向**0 = 第一个 outcome1 = 第二个 outcome,以此类推)
- ✅ 如果必须使用 side 字符串,应该从市场的 outcomes 数组中获取,而不是硬编码 "YES"/"NO"
- ✅ 对于二元市场的价格转换,应该通过 `outcomeIndex` 判断是否为第二个 outcomeindex = 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 进行判断
}
```
@@ -64,71 +64,3 @@ class LocaleInterceptor : HandlerInterceptor {
return Locale("en") 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")
}
}
@@ -18,20 +18,11 @@ class WebMvcConfig(
// 先注册语言拦截器(优先级更高) // 先注册语言拦截器(优先级更高)
registry.addInterceptor(localeInterceptor) registry.addInterceptor(localeInterceptor)
.addPathPatterns("/api/**") .addPathPatterns("/api/**")
// 再注册JWT认证拦截器 // 再注册JWT认证拦截器
registry.addInterceptor(jwtAuthenticationInterceptor) registry.addInterceptor(jwtAuthenticationInterceptor)
.addPathPatterns("/api/**") .addPathPatterns("/api/**")
}
}
registry.addInterceptor(jwtAuthenticationInterceptor) registry.addInterceptor(jwtAuthenticationInterceptor)
.addPathPatterns("/api/**") .addPathPatterns("/api/**")
}
}
registry.addInterceptor(jwtAuthenticationInterceptor) registry.addInterceptor(jwtAuthenticationInterceptor)
.addPathPatterns("/api/**") .addPathPatterns("/api/**")
} }
@@ -36,7 +36,7 @@ class MarketController(
return ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_MARKET_ID_EMPTY, messageSource = messageSource)) 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( result.fold(
onSuccess = { response -> onSuccess = { response ->
ResponseEntity.ok(ApiResponse.success(response)) ResponseEntity.ok(ApiResponse.success(response))
@@ -160,7 +160,8 @@ data class PositionSellResponse(
* 市场价格请求 * 市场价格请求
*/ */
data class MarketPriceRequest( data class MarketPriceRequest(
val marketId: String // 市场ID val marketId: String, // 市场ID
val outcomeIndex: Int? = null // 结果索引(可选):0, 1, 2...,用于确定需要查询哪个 outcome 的价格。如果提供了 outcomeIndex,会转换价格(1 - 第一个outcome的价格)
) )
/** /**
@@ -7,6 +7,7 @@ import com.wrbug.polymarketbot.repository.AccountRepository
import com.wrbug.polymarketbot.util.RetrofitFactory import com.wrbug.polymarketbot.util.RetrofitFactory
import com.wrbug.polymarketbot.util.toSafeBigDecimal import com.wrbug.polymarketbot.util.toSafeBigDecimal
import com.wrbug.polymarketbot.util.eq import com.wrbug.polymarketbot.util.eq
import com.wrbug.polymarketbot.util.JsonUtils
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import org.springframework.stereotype.Service import org.springframework.stereotype.Service
@@ -739,19 +740,12 @@ class AccountService(
// 6. 获取 tokenId(从 conditionId 和 outcomeIndex 计算) // 6. 获取 tokenId(从 conditionId 和 outcomeIndex 计算)
// 需要先获取 tokenId,以便后续通过 CLOB API 获取三元及以上市场的价格 // 需要先获取 tokenId,以便后续通过 CLOB API 获取三元及以上市场的价格
// 优先使用 outcomeIndex,如果没有则尝试从 side 推断(仅支持 YES/NO // 优先使用 outcomeIndex,如果没有则返回错误(不再通过 side 字符串推断
val tokenIdResult = if (request.outcomeIndex != null) { val tokenIdResult = if (request.outcomeIndex != null) {
blockchainService.getTokenId(request.marketId, request.outcomeIndex) blockchainService.getTokenId(request.marketId, request.outcomeIndex)
} else { } else {
// 向后兼容:尝试从 side 推断(仅支持 YES/NO logger.warn("缺少 outcomeIndex 参数,无法计算 tokenId: marketId=${request.marketId}, side=${request.side}")
when (request.side.uppercase()) { Result.failure<String>(IllegalArgumentException("缺少 outcomeIndex 参数,无法计算 tokenId。请提供 outcomeIndex 参数"))
"YES" -> blockchainService.getTokenId(request.marketId, 0)
"NO" -> blockchainService.getTokenId(request.marketId, 1)
else -> {
logger.warn("无法从 side 推断 outcomeIndex,需要提供 outcomeIndex: side=${request.side}")
Result.failure<String>(IllegalArgumentException("无法从 side '${request.side}' 推断 outcomeIndex,请提供 outcomeIndex 参数"))
}
}
} }
val tokenId = tokenIdResult.getOrNull() val tokenId = tokenIdResult.getOrNull()
@@ -879,8 +873,9 @@ class AccountService(
) )
} else { } else {
val errorMsg = response.errorMsg ?: "未知错误" val errorMsg = response.errorMsg ?: "未知错误"
logger.error("创建订单失败: $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"
Result.failure(Exception("创建订单失败: $errorMsg")) logger.error(fullErrorMsg)
Result.failure(Exception(fullErrorMsg))
} }
} else { } else {
val errorBody = try { val errorBody = try {
@@ -888,12 +883,14 @@ class AccountService(
} catch (e: Exception) { } catch (e: Exception) {
null null
} }
logger.error("创建订单失败: code=${orderResponse.code()}, message=${orderResponse.message()}, errorBody=$errorBody") 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 ""}"
Result.failure(Exception("创建订单失败: ${orderResponse.code()} ${orderResponse.message()}${if (errorBody != null) " - $errorBody" else ""}")) logger.error(fullErrorMsg)
Result.failure(Exception(fullErrorMsg))
} }
} catch (e: Exception) { } catch (e: Exception) {
logger.error("卖出仓位异常: ${e.message}", e) val fullErrorMsg = "卖出仓位异常: accountId=${request.accountId}, marketId=${request.marketId}, side=${request.side}, orderType=${request.orderType}, error=${e.message}"
Result.failure(e) logger.error(fullErrorMsg, e)
Result.failure(Exception(fullErrorMsg))
} }
} }
@@ -919,8 +916,10 @@ class AccountService(
/** /**
* 获取市场价格 * 获取市场价格
* 使用 Gamma API 获取价格信息,因为 Gamma API 支持 condition_ids 参数 * 使用 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<MarketPriceResponse> { suspend fun getMarketPrice(marketId: String, outcomeIndex: Int? = null): Result<MarketPriceResponse> {
return try { return try {
// 使用 Gamma API 获取市场信息(支持 condition_ids 参数) // 使用 Gamma API 获取市场信息(支持 condition_ids 参数)
val gammaApi = retrofitFactory.createGammaApi() val gammaApi = retrofitFactory.createGammaApi()
@@ -931,10 +930,36 @@ class AccountService(
val market = markets.firstOrNull() val market = markets.firstOrNull()
if (market != null) { if (market != null) {
// 从 Gamma API 响应中提取价格信息 // 从 Gamma API 响应中提取价格信息(这些价格通常是针对第一个 outcome,index = 0
val bestBid = market.bestBid?.toString() var bestBid = market.bestBid?.toString()
val bestAsk = market.bestAsk?.toString() var bestAsk = market.bestAsk?.toString()
val lastPrice = market.lastTradePrice?.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 // 计算中间价 = (bestBid + bestAsk) / 2
val midpoint = if (bestBid != null && bestAsk != null) { val midpoint = if (bestBid != null && bestAsk != null) {
@@ -348,18 +348,23 @@ class BlockchainService(
} }
/** /**
* 从 condition ID 和 side (YES/NO) 计算 tokenId向后兼容方法 * 从 condition ID 和 side (YES/NO) 计算 tokenId已废弃,不推荐使用
* 仅支持二元市场(YES/NO) * 仅支持二元市场(YES/NO)
* *
* @deprecated 禁止使用 "YES"/"NO" 字符串判断 side,请使用 getTokenId(conditionId, outcomeIndex) 方法
* @param conditionId condition ID16进制字符串,如 "0x..." * @param conditionId condition ID16进制字符串,如 "0x..."
* @param side YES 或 NO * @param side YES 或 NO(不推荐使用,应使用 outcomeIndex
* @return tokenIdBigInteger 的字符串表示) * @return tokenIdBigInteger 的字符串表示)
*/ */
@Deprecated("禁止使用 YES/NO 字符串判断 side,请使用 getTokenId(conditionId, outcomeIndex) 方法", ReplaceWith("getTokenId(conditionId, outcomeIndex)"))
suspend fun getTokenIdBySide(conditionId: String, side: String): Result<String> { suspend fun getTokenIdBySide(conditionId: String, side: String): Result<String> {
// 注意:此方法违反了规范,禁止使用 "YES"/"NO" 字符串判断
// 为了向后兼容,暂时保留,但应该尽快迁移到使用 outcomeIndex 的方法
logger.warn("使用已废弃的方法 getTokenIdBySide,建议使用 getTokenId(conditionId, outcomeIndex): conditionId=$conditionId, side=$side")
val outcomeIndex = when (side.uppercase()) { val outcomeIndex = when (side.uppercase()) {
"YES" -> 0 "YES" -> 0
"NO" -> 1 "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) return getTokenId(conditionId, outcomeIndex)
} }
@@ -288,7 +288,8 @@ class CopyOrderTrackingService(
if (createOrderResult.isFailure) { if (createOrderResult.isFailure) {
// 创建订单失败,记录到失败表 // 创建订单失败,记录到失败表
val errorMsg = createOrderResult.exceptionOrNull()?.message ?: "未知错误" val exception = createOrderResult.exceptionOrNull()
val errorMsg = buildFullErrorMessage(exception, "BUY", buyPrice.toString(), finalBuyQuantity.toString(), trade.id)
recordFailedTrade( recordFailedTrade(
leaderId = leaderId, leaderId = leaderId,
trade = trade, trade = trade,
@@ -555,7 +556,8 @@ class CopyOrderTrackingService(
if (createOrderResult.isFailure) { if (createOrderResult.isFailure) {
// 创建订单失败,记录到失败表 // 创建订单失败,记录到失败表
val errorMsg = createOrderResult.exceptionOrNull()?.message ?: "未知错误" val exception = createOrderResult.exceptionOrNull()
val errorMsg = buildFullErrorMessage(exception, "SELL", sellPrice.toString(), totalMatched.toString(), leaderSellTrade.id)
recordFailedTrade( recordFailedTrade(
leaderId = copyTrading.leaderId, leaderId = copyTrading.leaderId,
trade = leaderSellTrade, trade = leaderSellTrade,
@@ -659,46 +661,82 @@ class CopyOrderTrackingService(
val orderResponse = clobApi.createOrder(orderRequest) val orderResponse = clobApi.createOrder(orderRequest)
if (!orderResponse.isSuccessful || orderResponse.body() == null) { 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) { if (attempt < 2) {
// 第一次失败不记录日志,静默重试
delay(1000) // 重试前等待1秒 delay(1000) // 重试前等待1秒
continue continue
} }
// 重试后仍然失败,记录日志
logger.warn("创建订单失败(重试后仍失败): copyTradingId=$copyTradingId, tradeId=$tradeId, attempt=$attempt, code=${orderResponse.code()}, message=${orderResponse.message()}")
return Result.failure(lastError!!) return Result.failure(lastError!!)
} }
val response = orderResponse.body()!! val response = orderResponse.body()!!
if (!response.success || response.orderId == null) { 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) { if (attempt < 2) {
// 第一次失败不记录日志,静默重试
delay(1000) // 重试前等待1秒 delay(1000) // 重试前等待1秒
continue continue
} }
// 重试后仍然失败,记录日志
logger.warn("创建订单失败(重试后仍失败): copyTradingId=$copyTradingId, tradeId=$tradeId, attempt=$attempt, errorMsg=${response.errorMsg}")
return Result.failure(lastError!!) return Result.failure(lastError!!)
} }
// 成功 // 成功
return Result.success(response.orderId) return Result.success(response.orderId)
} catch (e: Exception) { } 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) { if (attempt < 2) {
// 第一次失败不记录日志,静默重试
delay(1000) // 重试前等待1秒 delay(1000) // 重试前等待1秒
continue continue
} }
// 重试后仍然失败,记录日志 return Result.failure(lastError!!)
logger.warn("调用创建订单API异常(重试后仍失败): copyTradingId=$copyTradingId, tradeId=$tradeId, attempt=$attempt", e)
return Result.failure(e)
} }
} }
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 retryCount: Int
) { ) {
try { try {
// 确保错误信息不超过数据库字段限制(TEXT类型通常支持65535字符)
val maxErrorMessageLength = 50000 // 保留一些余量
val finalErrorMessage = if (errorMessage.length > maxErrorMessageLength) {
errorMessage.substring(0, maxErrorMessageLength) + "... (截断)"
} else {
errorMessage
}
val failedTrade = FailedTrade( val failedTrade = FailedTrade(
leaderId = leaderId, leaderId = leaderId,
leaderTradeId = trade.id, leaderTradeId = trade.id,
@@ -727,12 +773,15 @@ class CopyOrderTrackingService(
side = side, side = side,
price = price, price = price,
size = size, size = size,
errorMessage = errorMessage, errorMessage = finalErrorMessage,
retryCount = retryCount, retryCount = retryCount,
failedAt = System.currentTimeMillis() failedAt = System.currentTimeMillis()
) )
failedTradeRepository.save(failedTrade) failedTradeRepository.save(failedTrade)
// 记录日志,确认已保存到数据库
logger.info("失败交易已保存到数据库: leaderId=$leaderId, tradeId=${trade.id}, errorMessageLength=${finalErrorMessage.length}")
// 标记为已处理(失败状态),避免重复处理 // 标记为已处理(失败状态),避免重复处理
// 注意:并发情况下可能多个请求同时处理同一笔交易,需要处理唯一约束冲突 // 注意:并发情况下可能多个请求同时处理同一笔交易,需要处理唯一约束冲突
try { try {
@@ -844,22 +893,20 @@ class CopyOrderTrackingService(
} }
/** /**
* 从trade中提取sideYES/NO * 从trade中提取side结果名称
* *
* 说明: * 说明:
* - 根据设计文档,系统只支持sports和crypto分类,这些通常是二元市场(YES/NO) * - 根据设计文档,系统只支持sports和crypto分类,这些通常是二元市场(YES/NO)
* - TradeResponse中的side是BUY/SELL(订单方向),不是YES/NOoutcome * - TradeResponse中的side是BUY/SELL(订单方向),不是YES/NOoutcome
* - 在二元市场中: * - 在二元市场中:
* - outcomeIndex 0 = YES token * - outcomeIndex 0 = 第一个 outcome(通常是 YES
* - outcomeIndex 1 = NO token * - outcomeIndex 1 = 第二个 outcome(通常是 NO
* - 如果Leader买入outcomeIndex=1的结果(如"Down"),应该买入NO token
* - 如果Leader买入outcomeIndex=0的结果(如"Up"),应该买入YES token
* *
* 判断逻辑: * 判断逻辑(禁止使用 "YES"/"NO" 字符串判断)
* 1. 如果tradeSide已经是YES/NO,直接返回 * 1. 优先使用 outcomeIndex:根据 outcomeIndex 返回对应的结果名称
* 2. 如果有outcomeIndex,根据outcomeIndex判断:0=YES, 1=NO * 2. 如果有 outcome 名称,直接返回 outcome 名称
* 3. 如果有outcome名称,尝试从名称判断(Up/Yes=YES, Down/No=NO * 3. 如果 tradeSide 已经是结果名称(不是 BUY/SELL),直接返回
* 4. 否则,默认返回YES(兼容旧逻辑 * 4. 否则,返回默认值(兼容旧逻辑,但不使用 YES/NO 字符串判断
*/ */
private fun extractSide( private fun extractSide(
marketId: String, marketId: String,
@@ -867,49 +914,39 @@ class CopyOrderTrackingService(
outcomeIndex: Int? = null, outcomeIndex: Int? = null,
outcome: String? = null outcome: String? = null
): String { ): String {
// 1. 如果tradeSide已经是YES/NO,直接返回 // 1. 优先使用 outcomeIndex(最准确,不依赖字符串判断)
when (tradeSide.uppercase()) {
"YES" -> return "YES"
"NO" -> return "NO"
}
// 2. 根据outcomeIndex判断(最准确)
if (outcomeIndex != null) { 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) { return when (outcomeIndex) {
0 -> "YES" // outcomeIndex 0 = YES token 0 -> "YES" // outcomeIndex 0 = 第一个 outcome
1 -> "NO" // outcomeIndex 1 = NO token 1 -> "NO" // outcomeIndex 1 = 第二个 outcome
else -> { else -> {
logger.warn("未知的outcomeIndex,默认返回YES: outcomeIndex=$outcomeIndex, marketId=$marketId") logger.warn("未知的outcomeIndex,默认返回第一个outcome: outcomeIndex=$outcomeIndex, marketId=$marketId")
"YES" "YES" // 默认返回第一个 outcome
} }
} }
} }
// 3. 根据outcome名称判断(备用方案) // 2. 如果有 outcome 名称,直接返回
if (outcome != null) { if (outcome != null) {
val outcomeUpper = outcome.uppercase() return outcome
when {
outcomeUpper.contains("UP") || outcomeUpper.contains("YES") -> return "YES"
outcomeUpper.contains("DOWN") || outcomeUpper.contains("NO") -> return "NO"
}
} }
// 4. 根据tradeSide判断(兼容旧逻辑) // 3. 如果 tradeSide 不是 BUY/SELL,可能是结果名称,直接返回
return when (tradeSide.uppercase()) { if (tradeSide.uppercase() !in listOf("BUY", "SELL")) {
"BUY" -> { return tradeSide
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
}
} }
// 4. 无法确定,返回默认值(兼容旧逻辑)
logger.warn("无法确定 side,默认返回第一个outcome: marketId=$marketId, tradeSide=$tradeSide, outcomeIndex=$outcomeIndex, outcome=$outcome")
return "YES" // 默认返回第一个 outcome
} }
} }
@@ -242,13 +242,33 @@ class PolymarketClobService(
return try { return try {
val response = clobApi.createOrder(request) val response = clobApi.createOrder(request)
if (response.isSuccessful && response.body() != null) { 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 { } 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) { } catch (e: Exception) {
logger.error("创建订单异常: ${e.message}", e) val errorMsg = "创建订单异常: orderType=${request.orderType}, owner=${request.owner}, error=${e.message}"
Result.failure(e) logger.error(errorMsg, e)
Result.failure(Exception(errorMsg, e))
} }
} }
@@ -49,57 +49,3 @@ class MessageUtils(
} }
} }
} }
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
}
}
}
-266
View File
@@ -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)
```
**模式 2ErrorCode + 自定义消息**
```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)
```
**模式 2ErrorCode + 自定义消息**
```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. **测试**:更新后测试不同语言下的错误消息显示
View File
-246
View File
@@ -1,246 +0,0 @@
# 多语言支持实现状态
## ✅ 已完成的工作
### 后端部分(100% 完成)
#### 1. 核心框架 ✅
- ✅ 创建语言资源文件(messages_zh_CN.properties, messages_zh_TW.properties, messages_en.properties
- ✅ 配置 MessageSource BeanMessageSourceConfig.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 中添加语言 HeaderX-Language
- ✅ 在 main.tsx 中初始化 i18n
#### 2. 页面更新 ✅(2个页面已完成)
- ✅ AccountDetail.tsx - 完全使用 i18n
- ✅ App.tsx - 订单推送通知已使用 i18nAnt 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 BeanMessageSourceConfig.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 中添加语言 HeaderX-Language
- ✅ 在 main.tsx 中初始化 i18n
#### 2. 页面更新 ✅(2个页面已完成)
- ✅ AccountDetail.tsx - 完全使用 i18n
- ✅ App.tsx - 订单推送通知已使用 i18nAnt 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. (可选)添加语言切换组件
-444
View File
@@ -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<ApiResponse<Unit>> {
// 使用扩展函数,自动国际化
return ResponseEntity.ok(
ApiResponse.error(ErrorCode.PARAM_ERROR, messageSource = messageSource)
)
}
}
// 方式2:使用 MessageUtils
@RestController
class ExampleController(
private val messageUtils: MessageUtils
) {
@PostMapping("/example")
fun example(): ResponseEntity<ApiResponse<Unit>> {
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 (
<div>
<Button>{t('common.save')}</Button>
<div>{t('account.accountName')}</div>
</div>
)
}
```
需要更新的页面:
- `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<ApiResponse<Unit>> {
// 使用扩展函数,自动国际化
return ResponseEntity.ok(
ApiResponse.error(ErrorCode.PARAM_ERROR, messageSource = messageSource)
)
}
}
// 方式2:使用 MessageUtils
@RestController
class ExampleController(
private val messageUtils: MessageUtils
) {
@PostMapping("/example")
fun example(): ResponseEntity<ApiResponse<Unit>> {
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 (
<div>
<Button>{t('common.save')}</Button>
<div>{t('account.accountName')}</div>
</div>
)
}
```
需要更新的页面:
- `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 的使用
-89
View File
@@ -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. **语言切换动画**(可选):
- 添加平滑的切换动画,提升用户体验
-572
View File
@@ -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)
// 模式2ErrorCode + 消息
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
// 旧代码
<Button></Button>
message.success('操作成功')
// 新代码
<Button>{t('common.save')}</Button>
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)
// 模式2ErrorCode + 消息
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
// 旧代码
<Button></Button>
message.success('操作成功')
// 新代码
<Button>{t('common.save')}</Button>
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
- [ ] 所有硬编码文本都已替换
- [ ] 语言包包含所有需要的翻译键
- [ ] 测试不同语言下的页面显示
- [ ] 测试语言切换功能
+4 -1
View File
@@ -367,7 +367,10 @@ const PositionList: React.FC = () => {
// 加载市场价格 // 加载市场价格
try { 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) { if (response.data.code === 0 && response.data.data) {
setMarketPrice(response.data.data) setMarketPrice(response.data.data)
// 默认使用最优买价作为限价 // 默认使用最优买价作为限价
+1 -1
View File
@@ -239,7 +239,7 @@ export const apiService = {
/** /**
* Gamma API * Gamma API
*/ */
getMarketPrice: (data: { marketId: string }) => getMarketPrice: (data: { marketId: string; outcomeIndex?: number }) =>
apiClient.post<ApiResponse<any>>('/copy-trading/markets/price', data), apiClient.post<ApiResponse<any>>('/copy-trading/markets/price', data),
/** /**