feat: 改进下单错误处理和市场价格接口
- 修复市场价格接口:移除side参数,使用outcomeIndex判断方向 - 添加规范:禁止使用YES/NO字符串判断side,必须使用outcomeIndex - 改进所有下单错误处理:所有错误都打印详细日志并存入数据库 - AccountService.sellPosition: 添加完整错误日志 - CopyOrderTrackingService.createOrderWithRetry: 所有失败都记录详细日志 - recordFailedTrade: 改进错误信息存储,包含堆栈信息 - PolymarketClobService.createSignedOrder: 添加完整错误日志 - 前端:更新市场价格接口调用,使用outcomeIndex替代side参数 - 清理:删除过时的i18n文档
This commit is contained in:
@@ -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 = 第一个 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 进行判断
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|||||||
@@ -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")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|||||||
@@ -13,25 +13,16 @@ class WebMvcConfig(
|
|||||||
private val jwtAuthenticationInterceptor: JwtAuthenticationInterceptor,
|
private val jwtAuthenticationInterceptor: JwtAuthenticationInterceptor,
|
||||||
private val localeInterceptor: LocaleInterceptor
|
private val localeInterceptor: LocaleInterceptor
|
||||||
) : WebMvcConfigurer {
|
) : WebMvcConfigurer {
|
||||||
|
|
||||||
override fun addInterceptors(registry: InterceptorRegistry) {
|
override fun addInterceptors(registry: InterceptorRegistry) {
|
||||||
// 先注册语言拦截器(优先级更高)
|
// 先注册语言拦截器(优先级更高)
|
||||||
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 ID(16进制字符串,如 "0x...")
|
* @param conditionId condition ID(16进制字符串,如 "0x...")
|
||||||
* @param side YES 或 NO
|
* @param side YES 或 NO(不推荐使用,应使用 outcomeIndex)
|
||||||
* @return tokenId(BigInteger 的字符串表示)
|
* @return tokenId(BigInteger 的字符串表示)
|
||||||
*/
|
*/
|
||||||
|
@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)
|
||||||
}
|
}
|
||||||
|
|||||||
+96
-59
@@ -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中提取side(YES/NO)
|
* 从trade中提取side(结果名称)
|
||||||
*
|
*
|
||||||
* 说明:
|
* 说明:
|
||||||
* - 根据设计文档,系统只支持sports和crypto分类,这些通常是二元市场(YES/NO)
|
* - 根据设计文档,系统只支持sports和crypto分类,这些通常是二元市场(YES/NO)
|
||||||
* - TradeResponse中的side是BUY/SELL(订单方向),不是YES/NO(outcome)
|
* - TradeResponse中的side是BUY/SELL(订单方向),不是YES/NO(outcome)
|
||||||
* - 在二元市场中:
|
* - 在二元市场中:
|
||||||
* - 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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -48,58 +48,4 @@ class MessageUtils(
|
|||||||
defaultMessage
|
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -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. **测试**:更新后测试不同语言下的错误消息显示
|
|
||||||
|
|
||||||
@@ -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. (可选)添加语言切换组件
|
|
||||||
|
|
||||||
@@ -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 的使用
|
|
||||||
|
|
||||||
@@ -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. **语言切换动画**(可选):
|
|
||||||
- 添加平滑的切换动画,提升用户体验
|
|
||||||
|
|
||||||
@@ -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
|
|
||||||
// 旧代码
|
|
||||||
<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)
|
|
||||||
|
|
||||||
// 模式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
|
|
||||||
// 旧代码
|
|
||||||
<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
|
|
||||||
- [ ] 所有硬编码文本都已替换
|
|
||||||
- [ ] 语言包包含所有需要的翻译键
|
|
||||||
- [ ] 测试不同语言下的页面显示
|
|
||||||
- [ ] 测试语言切换功能
|
|
||||||
|
|
||||||
@@ -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)
|
||||||
// 默认使用最优买价作为限价
|
// 默认使用最优买价作为限价
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user