feat: 改进下单错误处理和市场价格接口
- 修复市场价格接口:移除side参数,使用outcomeIndex判断方向 - 添加规范:禁止使用YES/NO字符串判断side,必须使用outcomeIndex - 改进所有下单错误处理:所有错误都打印详细日志并存入数据库 - AccountService.sellPosition: 添加完整错误日志 - CopyOrderTrackingService.createOrderWithRetry: 所有失败都记录详细日志 - recordFailedTrade: 改进错误信息存储,包含堆栈信息 - PolymarketClobService.createSignedOrder: 添加完整错误日志 - 前端:更新市场价格接口调用,使用outcomeIndex替代side参数 - 清理:删除过时的i18n文档
This commit is contained in:
@@ -64,71 +64,3 @@ class LocaleInterceptor : HandlerInterceptor {
|
||||
return Locale("en")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest
|
||||
import jakarta.servlet.http.HttpServletResponse
|
||||
import org.springframework.context.i18n.LocaleContextHolder
|
||||
import org.springframework.stereotype.Component
|
||||
import org.springframework.web.servlet.HandlerInterceptor
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* 语言拦截器
|
||||
* 从 HTTP Header 读取语言设置,并设置到 LocaleContextHolder
|
||||
*
|
||||
* 支持的 Header:
|
||||
* - Accept-Language: 标准 HTTP Header(如 zh-CN, zh-TW, en)
|
||||
* - X-Language: 自定义 Header(如 zh-CN, zh-TW, en)
|
||||
*
|
||||
* 语言映射规则:
|
||||
* - zh-CN, zh -> zh-CN (简体中文)
|
||||
* - zh-TW, zh-HK -> zh-TW (繁体中文)
|
||||
* - 其他 -> en (英文,默认)
|
||||
*/
|
||||
@Component
|
||||
class LocaleInterceptor : HandlerInterceptor {
|
||||
|
||||
override fun preHandle(
|
||||
request: HttpServletRequest,
|
||||
response: HttpServletResponse,
|
||||
handler: Any
|
||||
): Boolean {
|
||||
// 优先从 X-Language Header 读取
|
||||
val language = request.getHeader("X-Language")
|
||||
?: request.getHeader("Accept-Language")
|
||||
?: "en"
|
||||
|
||||
// 解析语言
|
||||
val locale = parseLocale(language)
|
||||
|
||||
// 设置到 LocaleContextHolder,供 MessageSource 使用
|
||||
LocaleContextHolder.setLocale(locale)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析语言字符串为 Locale
|
||||
* 支持格式:zh-CN, zh_TW, zh, en, en-US 等
|
||||
*/
|
||||
private fun parseLocale(language: String): Locale {
|
||||
// 移除空格并转为小写
|
||||
val lang = language.trim().lowercase()
|
||||
|
||||
// 处理 zh-CN, zh_CN 等格式
|
||||
if (lang.startsWith("zh")) {
|
||||
// 检查是否是繁体中文
|
||||
if (lang.contains("tw") || lang.contains("hk") || lang.contains("mo")) {
|
||||
return Locale("zh", "TW")
|
||||
}
|
||||
// 默认简体中文
|
||||
return Locale("zh", "CN")
|
||||
}
|
||||
|
||||
// 英文(默认)
|
||||
return Locale("en")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,25 +13,16 @@ class WebMvcConfig(
|
||||
private val jwtAuthenticationInterceptor: JwtAuthenticationInterceptor,
|
||||
private val localeInterceptor: LocaleInterceptor
|
||||
) : WebMvcConfigurer {
|
||||
|
||||
|
||||
override fun addInterceptors(registry: InterceptorRegistry) {
|
||||
// 先注册语言拦截器(优先级更高)
|
||||
registry.addInterceptor(localeInterceptor)
|
||||
.addPathPatterns("/api/**")
|
||||
|
||||
// 再注册JWT认证拦截器
|
||||
registry.addInterceptor(jwtAuthenticationInterceptor)
|
||||
.addPathPatterns("/api/**")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
registry.addInterceptor(jwtAuthenticationInterceptor)
|
||||
.addPathPatterns("/api/**")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
registry.addInterceptor(jwtAuthenticationInterceptor)
|
||||
.addPathPatterns("/api/**")
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ class MarketController(
|
||||
return ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_MARKET_ID_EMPTY, messageSource = messageSource))
|
||||
}
|
||||
|
||||
val result = runBlocking { accountService.getMarketPrice(request.marketId) }
|
||||
val result = runBlocking { accountService.getMarketPrice(request.marketId, request.outcomeIndex) }
|
||||
result.fold(
|
||||
onSuccess = { response ->
|
||||
ResponseEntity.ok(ApiResponse.success(response))
|
||||
|
||||
@@ -160,7 +160,8 @@ data class PositionSellResponse(
|
||||
* 市场价格请求
|
||||
*/
|
||||
data class MarketPriceRequest(
|
||||
val marketId: String // 市场ID
|
||||
val marketId: String, // 市场ID
|
||||
val outcomeIndex: Int? = null // 结果索引(可选):0, 1, 2...,用于确定需要查询哪个 outcome 的价格。如果提供了 outcomeIndex,会转换价格(1 - 第一个outcome的价格)
|
||||
)
|
||||
|
||||
/**
|
||||
|
||||
@@ -7,6 +7,7 @@ import com.wrbug.polymarketbot.repository.AccountRepository
|
||||
import com.wrbug.polymarketbot.util.RetrofitFactory
|
||||
import com.wrbug.polymarketbot.util.toSafeBigDecimal
|
||||
import com.wrbug.polymarketbot.util.eq
|
||||
import com.wrbug.polymarketbot.util.JsonUtils
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.stereotype.Service
|
||||
@@ -739,19 +740,12 @@ class AccountService(
|
||||
|
||||
// 6. 获取 tokenId(从 conditionId 和 outcomeIndex 计算)
|
||||
// 需要先获取 tokenId,以便后续通过 CLOB API 获取三元及以上市场的价格
|
||||
// 优先使用 outcomeIndex,如果没有则尝试从 side 推断(仅支持 YES/NO)
|
||||
// 优先使用 outcomeIndex,如果没有则返回错误(不再通过 side 字符串推断)
|
||||
val tokenIdResult = if (request.outcomeIndex != null) {
|
||||
blockchainService.getTokenId(request.marketId, request.outcomeIndex)
|
||||
} else {
|
||||
// 向后兼容:尝试从 side 推断(仅支持 YES/NO)
|
||||
when (request.side.uppercase()) {
|
||||
"YES" -> blockchainService.getTokenId(request.marketId, 0)
|
||||
"NO" -> blockchainService.getTokenId(request.marketId, 1)
|
||||
else -> {
|
||||
logger.warn("无法从 side 推断 outcomeIndex,需要提供 outcomeIndex: side=${request.side}")
|
||||
Result.failure<String>(IllegalArgumentException("无法从 side '${request.side}' 推断 outcomeIndex,请提供 outcomeIndex 参数"))
|
||||
}
|
||||
}
|
||||
logger.warn("缺少 outcomeIndex 参数,无法计算 tokenId: marketId=${request.marketId}, side=${request.side}")
|
||||
Result.failure<String>(IllegalArgumentException("缺少 outcomeIndex 参数,无法计算 tokenId。请提供 outcomeIndex 参数"))
|
||||
}
|
||||
val tokenId = tokenIdResult.getOrNull()
|
||||
|
||||
@@ -879,8 +873,9 @@ class AccountService(
|
||||
)
|
||||
} else {
|
||||
val errorMsg = response.errorMsg ?: "未知错误"
|
||||
logger.error("创建订单失败: $errorMsg")
|
||||
Result.failure(Exception("创建订单失败: $errorMsg"))
|
||||
val fullErrorMsg = "创建订单失败: accountId=${account.id}, marketId=${request.marketId}, side=${request.side}, orderType=${request.orderType}, price=${if (request.orderType == "LIMIT") sellPrice else "MARKET"}, quantity=${sellQuantity.toPlainString()}, errorMsg=$errorMsg"
|
||||
logger.error(fullErrorMsg)
|
||||
Result.failure(Exception(fullErrorMsg))
|
||||
}
|
||||
} else {
|
||||
val errorBody = try {
|
||||
@@ -888,12 +883,14 @@ class AccountService(
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
logger.error("创建订单失败: code=${orderResponse.code()}, message=${orderResponse.message()}, errorBody=$errorBody")
|
||||
Result.failure(Exception("创建订单失败: ${orderResponse.code()} ${orderResponse.message()}${if (errorBody != null) " - $errorBody" else ""}"))
|
||||
val fullErrorMsg = "创建订单失败: accountId=${account.id}, marketId=${request.marketId}, side=${request.side}, orderType=${request.orderType}, price=${if (request.orderType == "LIMIT") sellPrice else "MARKET"}, quantity=${sellQuantity.toPlainString()}, code=${orderResponse.code()}, message=${orderResponse.message()}${if (errorBody != null) ", errorBody=$errorBody" else ""}"
|
||||
logger.error(fullErrorMsg)
|
||||
Result.failure(Exception(fullErrorMsg))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logger.error("卖出仓位异常: ${e.message}", e)
|
||||
Result.failure(e)
|
||||
val fullErrorMsg = "卖出仓位异常: accountId=${request.accountId}, marketId=${request.marketId}, side=${request.side}, orderType=${request.orderType}, error=${e.message}"
|
||||
logger.error(fullErrorMsg, e)
|
||||
Result.failure(Exception(fullErrorMsg))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -919,8 +916,10 @@ class AccountService(
|
||||
/**
|
||||
* 获取市场价格
|
||||
* 使用 Gamma API 获取价格信息,因为 Gamma API 支持 condition_ids 参数
|
||||
* @param marketId 市场ID
|
||||
* @param outcomeIndex 结果索引(可选):0, 1, 2...,用于确定需要查询哪个 outcome 的价格。如果提供了 outcomeIndex 且 > 0,会转换价格(1 - 第一个outcome的价格)
|
||||
*/
|
||||
suspend fun getMarketPrice(marketId: String): Result<MarketPriceResponse> {
|
||||
suspend fun getMarketPrice(marketId: String, outcomeIndex: Int? = null): Result<MarketPriceResponse> {
|
||||
return try {
|
||||
// 使用 Gamma API 获取市场信息(支持 condition_ids 参数)
|
||||
val gammaApi = retrofitFactory.createGammaApi()
|
||||
@@ -931,10 +930,36 @@ class AccountService(
|
||||
val market = markets.firstOrNull()
|
||||
|
||||
if (market != null) {
|
||||
// 从 Gamma API 响应中提取价格信息
|
||||
val bestBid = market.bestBid?.toString()
|
||||
val bestAsk = market.bestAsk?.toString()
|
||||
val lastPrice = market.lastTradePrice?.toString()
|
||||
// 从 Gamma API 响应中提取价格信息(这些价格通常是针对第一个 outcome,index = 0)
|
||||
var bestBid = market.bestBid?.toString()
|
||||
var bestAsk = market.bestAsk?.toString()
|
||||
var lastPrice = market.lastTradePrice?.toString()
|
||||
|
||||
// 如果目标 outcome 不是第一个(index != 0),需要转换价格
|
||||
// 对于二元市场:第二个 outcome 的价格 = 1 - 第一个 outcome 的价格
|
||||
if (outcomeIndex != null && outcomeIndex > 0) {
|
||||
val outcomes = JsonUtils.parseStringArray(market.outcomes)
|
||||
// 只对二元市场进行价格转换
|
||||
if (outcomes.size == 2) {
|
||||
// 保存原始第一个 outcome 的价格
|
||||
val firstOutcomeBestBid = bestBid
|
||||
val firstOutcomeBestAsk = bestAsk
|
||||
|
||||
// 转换价格:第二个 outcome 的 bestBid = 1 - 第一个 outcome 的 bestAsk
|
||||
// 第二个 outcome 的 bestAsk = 1 - 第一个 outcome 的 bestBid
|
||||
bestBid = firstOutcomeBestAsk?.let {
|
||||
BigDecimal.ONE.subtract(it.toSafeBigDecimal()).toString()
|
||||
}
|
||||
bestAsk = firstOutcomeBestBid?.let {
|
||||
BigDecimal.ONE.subtract(it.toSafeBigDecimal()).toString()
|
||||
}
|
||||
|
||||
// 转换最后成交价:第二个 outcome 的 lastPrice = 1 - 第一个 outcome 的 lastPrice
|
||||
lastPrice = lastPrice?.let {
|
||||
BigDecimal.ONE.subtract(it.toSafeBigDecimal()).toString()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 计算中间价 = (bestBid + bestAsk) / 2
|
||||
val midpoint = if (bestBid != null && bestAsk != null) {
|
||||
|
||||
@@ -348,18 +348,23 @@ class BlockchainService(
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 condition ID 和 side (YES/NO) 计算 tokenId(向后兼容方法)
|
||||
* 从 condition ID 和 side (YES/NO) 计算 tokenId(已废弃,不推荐使用)
|
||||
* 仅支持二元市场(YES/NO)
|
||||
*
|
||||
* @deprecated 禁止使用 "YES"/"NO" 字符串判断 side,请使用 getTokenId(conditionId, outcomeIndex) 方法
|
||||
* @param conditionId condition ID(16进制字符串,如 "0x...")
|
||||
* @param side YES 或 NO
|
||||
* @param side YES 或 NO(不推荐使用,应使用 outcomeIndex)
|
||||
* @return tokenId(BigInteger 的字符串表示)
|
||||
*/
|
||||
@Deprecated("禁止使用 YES/NO 字符串判断 side,请使用 getTokenId(conditionId, outcomeIndex) 方法", ReplaceWith("getTokenId(conditionId, outcomeIndex)"))
|
||||
suspend fun getTokenIdBySide(conditionId: String, side: String): Result<String> {
|
||||
// 注意:此方法违反了规范,禁止使用 "YES"/"NO" 字符串判断
|
||||
// 为了向后兼容,暂时保留,但应该尽快迁移到使用 outcomeIndex 的方法
|
||||
logger.warn("使用已废弃的方法 getTokenIdBySide,建议使用 getTokenId(conditionId, outcomeIndex): conditionId=$conditionId, side=$side")
|
||||
val outcomeIndex = when (side.uppercase()) {
|
||||
"YES" -> 0
|
||||
"NO" -> 1
|
||||
else -> return Result.failure(IllegalArgumentException("side 必须是 YES 或 NO(仅支持二元市场)"))
|
||||
else -> return Result.failure(IllegalArgumentException("side 必须是 YES 或 NO(仅支持二元市场)。建议使用 getTokenId(conditionId, outcomeIndex) 方法"))
|
||||
}
|
||||
return getTokenId(conditionId, outcomeIndex)
|
||||
}
|
||||
|
||||
+96
-59
@@ -288,7 +288,8 @@ class CopyOrderTrackingService(
|
||||
|
||||
if (createOrderResult.isFailure) {
|
||||
// 创建订单失败,记录到失败表
|
||||
val errorMsg = createOrderResult.exceptionOrNull()?.message ?: "未知错误"
|
||||
val exception = createOrderResult.exceptionOrNull()
|
||||
val errorMsg = buildFullErrorMessage(exception, "BUY", buyPrice.toString(), finalBuyQuantity.toString(), trade.id)
|
||||
recordFailedTrade(
|
||||
leaderId = leaderId,
|
||||
trade = trade,
|
||||
@@ -555,7 +556,8 @@ class CopyOrderTrackingService(
|
||||
|
||||
if (createOrderResult.isFailure) {
|
||||
// 创建订单失败,记录到失败表
|
||||
val errorMsg = createOrderResult.exceptionOrNull()?.message ?: "未知错误"
|
||||
val exception = createOrderResult.exceptionOrNull()
|
||||
val errorMsg = buildFullErrorMessage(exception, "SELL", sellPrice.toString(), totalMatched.toString(), leaderSellTrade.id)
|
||||
recordFailedTrade(
|
||||
leaderId = copyTrading.leaderId,
|
||||
trade = leaderSellTrade,
|
||||
@@ -659,46 +661,82 @@ class CopyOrderTrackingService(
|
||||
val orderResponse = clobApi.createOrder(orderRequest)
|
||||
|
||||
if (!orderResponse.isSuccessful || orderResponse.body() == null) {
|
||||
lastError = Exception("创建订单失败: code=${orderResponse.code()}, message=${orderResponse.message()}")
|
||||
val errorBody = try {
|
||||
orderResponse.errorBody()?.string()
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
val errorMsg = "创建订单失败: copyTradingId=$copyTradingId, tradeId=$tradeId, attempt=$attempt, side=$side, price=$price, size=$size, tokenId=$tokenId, code=${orderResponse.code()}, message=${orderResponse.message()}${if (errorBody != null) ", errorBody=$errorBody" else ""}"
|
||||
lastError = Exception(errorMsg)
|
||||
// 所有失败都记录详细日志
|
||||
logger.error(errorMsg)
|
||||
if (attempt < 2) {
|
||||
// 第一次失败不记录日志,静默重试
|
||||
delay(1000) // 重试前等待1秒
|
||||
continue
|
||||
}
|
||||
// 重试后仍然失败,记录日志
|
||||
logger.warn("创建订单失败(重试后仍失败): copyTradingId=$copyTradingId, tradeId=$tradeId, attempt=$attempt, code=${orderResponse.code()}, message=${orderResponse.message()}")
|
||||
return Result.failure(lastError!!)
|
||||
}
|
||||
|
||||
val response = orderResponse.body()!!
|
||||
if (!response.success || response.orderId == null) {
|
||||
lastError = Exception("创建订单失败: errorMsg=${response.errorMsg}")
|
||||
val errorMsg = "创建订单失败: copyTradingId=$copyTradingId, tradeId=$tradeId, attempt=$attempt, side=$side, price=$price, size=$size, tokenId=$tokenId, errorMsg=${response.errorMsg}"
|
||||
lastError = Exception(errorMsg)
|
||||
// 所有失败都记录详细日志
|
||||
logger.error(errorMsg)
|
||||
if (attempt < 2) {
|
||||
// 第一次失败不记录日志,静默重试
|
||||
delay(1000) // 重试前等待1秒
|
||||
continue
|
||||
}
|
||||
// 重试后仍然失败,记录日志
|
||||
logger.warn("创建订单失败(重试后仍失败): copyTradingId=$copyTradingId, tradeId=$tradeId, attempt=$attempt, errorMsg=${response.errorMsg}")
|
||||
return Result.failure(lastError!!)
|
||||
}
|
||||
|
||||
// 成功
|
||||
return Result.success(response.orderId)
|
||||
} catch (e: Exception) {
|
||||
lastError = e
|
||||
val errorMsg = "调用创建订单API异常: copyTradingId=$copyTradingId, tradeId=$tradeId, attempt=$attempt, side=$side, price=$price, size=$size, tokenId=$tokenId, error=${e.message}"
|
||||
lastError = Exception(errorMsg, e)
|
||||
// 所有失败都记录详细日志(包括堆栈)
|
||||
logger.error(errorMsg, e)
|
||||
if (attempt < 2) {
|
||||
// 第一次失败不记录日志,静默重试
|
||||
delay(1000) // 重试前等待1秒
|
||||
continue
|
||||
}
|
||||
// 重试后仍然失败,记录日志
|
||||
logger.warn("调用创建订单API异常(重试后仍失败): copyTradingId=$copyTradingId, tradeId=$tradeId, attempt=$attempt", e)
|
||||
return Result.failure(e)
|
||||
return Result.failure(lastError!!)
|
||||
}
|
||||
}
|
||||
|
||||
return Result.failure(lastError ?: Exception("创建订单失败:未知错误"))
|
||||
val finalError = lastError ?: Exception("创建订单失败:未知错误")
|
||||
logger.error("创建订单失败(所有重试都失败): copyTradingId=$copyTradingId, tradeId=$tradeId, side=$side, price=$price, size=$size, tokenId=$tokenId", finalError)
|
||||
return Result.failure(finalError)
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建完整的错误信息(包括堆栈)
|
||||
*/
|
||||
private fun buildFullErrorMessage(exception: Throwable?, side: String, price: String, size: String, tradeId: String): String {
|
||||
if (exception == null) {
|
||||
return "创建订单失败: side=$side, price=$price, size=$size, tradeId=$tradeId, 未知错误"
|
||||
}
|
||||
|
||||
val errorMsg = StringBuilder()
|
||||
errorMsg.append("创建订单失败: side=$side, price=$price, size=$size, tradeId=$tradeId")
|
||||
errorMsg.append(", error=${exception.message}")
|
||||
|
||||
// 添加堆栈信息(限制长度,避免过长)
|
||||
val stackTrace = exception.stackTraceToString()
|
||||
val maxLength = 2000 // 限制错误信息最大长度为2000字符
|
||||
if (stackTrace.length > maxLength) {
|
||||
errorMsg.append(", stackTrace=${stackTrace.substring(0, maxLength)}...")
|
||||
} else {
|
||||
errorMsg.append(", stackTrace=$stackTrace")
|
||||
}
|
||||
|
||||
// 如果有 cause,也添加
|
||||
exception.cause?.let { cause ->
|
||||
errorMsg.append(", cause=${cause.message}")
|
||||
}
|
||||
|
||||
return errorMsg.toString()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -717,6 +755,14 @@ class CopyOrderTrackingService(
|
||||
retryCount: Int
|
||||
) {
|
||||
try {
|
||||
// 确保错误信息不超过数据库字段限制(TEXT类型通常支持65535字符)
|
||||
val maxErrorMessageLength = 50000 // 保留一些余量
|
||||
val finalErrorMessage = if (errorMessage.length > maxErrorMessageLength) {
|
||||
errorMessage.substring(0, maxErrorMessageLength) + "... (截断)"
|
||||
} else {
|
||||
errorMessage
|
||||
}
|
||||
|
||||
val failedTrade = FailedTrade(
|
||||
leaderId = leaderId,
|
||||
leaderTradeId = trade.id,
|
||||
@@ -727,12 +773,15 @@ class CopyOrderTrackingService(
|
||||
side = side,
|
||||
price = price,
|
||||
size = size,
|
||||
errorMessage = errorMessage,
|
||||
errorMessage = finalErrorMessage,
|
||||
retryCount = retryCount,
|
||||
failedAt = System.currentTimeMillis()
|
||||
)
|
||||
failedTradeRepository.save(failedTrade)
|
||||
|
||||
// 记录日志,确认已保存到数据库
|
||||
logger.info("失败交易已保存到数据库: leaderId=$leaderId, tradeId=${trade.id}, errorMessageLength=${finalErrorMessage.length}")
|
||||
|
||||
// 标记为已处理(失败状态),避免重复处理
|
||||
// 注意:并发情况下可能多个请求同时处理同一笔交易,需要处理唯一约束冲突
|
||||
try {
|
||||
@@ -844,22 +893,20 @@ class CopyOrderTrackingService(
|
||||
}
|
||||
|
||||
/**
|
||||
* 从trade中提取side(YES/NO)
|
||||
* 从trade中提取side(结果名称)
|
||||
*
|
||||
* 说明:
|
||||
* - 根据设计文档,系统只支持sports和crypto分类,这些通常是二元市场(YES/NO)
|
||||
* - TradeResponse中的side是BUY/SELL(订单方向),不是YES/NO(outcome)
|
||||
* - 在二元市场中:
|
||||
* - outcomeIndex 0 = YES token
|
||||
* - outcomeIndex 1 = NO token
|
||||
* - 如果Leader买入outcomeIndex=1的结果(如"Down"),应该买入NO token
|
||||
* - 如果Leader买入outcomeIndex=0的结果(如"Up"),应该买入YES token
|
||||
* - outcomeIndex 0 = 第一个 outcome(通常是 YES)
|
||||
* - outcomeIndex 1 = 第二个 outcome(通常是 NO)
|
||||
*
|
||||
* 判断逻辑:
|
||||
* 1. 如果tradeSide已经是YES/NO,直接返回
|
||||
* 2. 如果有outcomeIndex,根据outcomeIndex判断:0=YES, 1=NO
|
||||
* 3. 如果有outcome名称,尝试从名称判断(Up/Yes=YES, Down/No=NO)
|
||||
* 4. 否则,默认返回YES(兼容旧逻辑)
|
||||
* 判断逻辑(禁止使用 "YES"/"NO" 字符串判断):
|
||||
* 1. 优先使用 outcomeIndex:根据 outcomeIndex 返回对应的结果名称
|
||||
* 2. 如果有 outcome 名称,直接返回 outcome 名称
|
||||
* 3. 如果 tradeSide 已经是结果名称(不是 BUY/SELL),直接返回
|
||||
* 4. 否则,返回默认值(兼容旧逻辑,但不使用 YES/NO 字符串判断)
|
||||
*/
|
||||
private fun extractSide(
|
||||
marketId: String,
|
||||
@@ -867,49 +914,39 @@ class CopyOrderTrackingService(
|
||||
outcomeIndex: Int? = null,
|
||||
outcome: String? = null
|
||||
): String {
|
||||
// 1. 如果tradeSide已经是YES/NO,直接返回
|
||||
when (tradeSide.uppercase()) {
|
||||
"YES" -> return "YES"
|
||||
"NO" -> return "NO"
|
||||
}
|
||||
|
||||
// 2. 根据outcomeIndex判断(最准确)
|
||||
// 1. 优先使用 outcomeIndex(最准确,不依赖字符串判断)
|
||||
if (outcomeIndex != null) {
|
||||
// 如果有 outcome 名称,优先使用 outcome 名称
|
||||
if (outcome != null) {
|
||||
return outcome
|
||||
}
|
||||
// 如果没有 outcome 名称,根据 outcomeIndex 返回(仅用于向后兼容)
|
||||
// 注意:这里不应该硬编码 "YES"/"NO",但为了向后兼容,暂时保留
|
||||
// 理想情况下,应该从市场数据中获取 outcome 名称
|
||||
logger.warn("使用 outcomeIndex 推断 side,建议提供 outcome 名称: outcomeIndex=$outcomeIndex, marketId=$marketId")
|
||||
return when (outcomeIndex) {
|
||||
0 -> "YES" // outcomeIndex 0 = YES token
|
||||
1 -> "NO" // outcomeIndex 1 = NO token
|
||||
0 -> "YES" // outcomeIndex 0 = 第一个 outcome
|
||||
1 -> "NO" // outcomeIndex 1 = 第二个 outcome
|
||||
else -> {
|
||||
logger.warn("未知的outcomeIndex,默认返回YES: outcomeIndex=$outcomeIndex, marketId=$marketId")
|
||||
"YES"
|
||||
logger.warn("未知的outcomeIndex,默认返回第一个outcome: outcomeIndex=$outcomeIndex, marketId=$marketId")
|
||||
"YES" // 默认返回第一个 outcome
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 根据outcome名称判断(备用方案)
|
||||
// 2. 如果有 outcome 名称,直接返回
|
||||
if (outcome != null) {
|
||||
val outcomeUpper = outcome.uppercase()
|
||||
when {
|
||||
outcomeUpper.contains("UP") || outcomeUpper.contains("YES") -> return "YES"
|
||||
outcomeUpper.contains("DOWN") || outcomeUpper.contains("NO") -> return "NO"
|
||||
}
|
||||
return outcome
|
||||
}
|
||||
|
||||
// 4. 根据tradeSide判断(兼容旧逻辑)
|
||||
return when (tradeSide.uppercase()) {
|
||||
"BUY" -> {
|
||||
logger.warn("无法确定BUY的方向,默认返回YES: marketId=$marketId, outcomeIndex=$outcomeIndex, outcome=$outcome")
|
||||
"YES" // 默认假设买入YES token
|
||||
}
|
||||
"SELL" -> {
|
||||
// 卖出时,需要匹配之前买入的订单,所以也返回YES(表示卖出YES,即买入NO)
|
||||
logger.warn("无法确定SELL的方向,默认返回YES: marketId=$marketId, outcomeIndex=$outcomeIndex, outcome=$outcome")
|
||||
"YES"
|
||||
}
|
||||
else -> {
|
||||
logger.warn("未知的交易方向,默认返回YES: tradeSide=$tradeSide, marketId=$marketId")
|
||||
"YES" // 默认返回YES
|
||||
}
|
||||
// 3. 如果 tradeSide 不是 BUY/SELL,可能是结果名称,直接返回
|
||||
if (tradeSide.uppercase() !in listOf("BUY", "SELL")) {
|
||||
return tradeSide
|
||||
}
|
||||
|
||||
// 4. 无法确定,返回默认值(兼容旧逻辑)
|
||||
logger.warn("无法确定 side,默认返回第一个outcome: marketId=$marketId, tradeSide=$tradeSide, outcomeIndex=$outcomeIndex, outcome=$outcome")
|
||||
return "YES" // 默认返回第一个 outcome
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -242,13 +242,33 @@ class PolymarketClobService(
|
||||
return try {
|
||||
val response = clobApi.createOrder(request)
|
||||
if (response.isSuccessful && response.body() != null) {
|
||||
Result.success(response.body()!!)
|
||||
val responseBody = response.body()!!
|
||||
if (responseBody.success) {
|
||||
Result.success(responseBody)
|
||||
} else {
|
||||
val errorBody = try {
|
||||
response.errorBody()?.string()
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
val errorMsg = "创建订单失败: orderType=${request.orderType}, owner=${request.owner}, errorMsg=${responseBody.errorMsg}${if (errorBody != null) ", errorBody=$errorBody" else ""}"
|
||||
logger.error(errorMsg)
|
||||
Result.failure(Exception(errorMsg))
|
||||
}
|
||||
} else {
|
||||
Result.failure(Exception("创建订单失败: ${response.code()} ${response.message()}"))
|
||||
val errorBody = try {
|
||||
response.errorBody()?.string()
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
val errorMsg = "创建订单失败: orderType=${request.orderType}, owner=${request.owner}, code=${response.code()}, message=${response.message()}${if (errorBody != null) ", errorBody=$errorBody" else ""}"
|
||||
logger.error(errorMsg)
|
||||
Result.failure(Exception(errorMsg))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logger.error("创建订单异常: ${e.message}", e)
|
||||
Result.failure(e)
|
||||
val errorMsg = "创建订单异常: orderType=${request.orderType}, owner=${request.owner}, error=${e.message}"
|
||||
logger.error(errorMsg, e)
|
||||
Result.failure(Exception(errorMsg, e))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -48,58 +48,4 @@ class MessageUtils(
|
||||
defaultMessage
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
import com.wrbug.polymarketbot.enums.ErrorCode
|
||||
import org.springframework.context.MessageSource
|
||||
import org.springframework.context.i18n.LocaleContextHolder
|
||||
import org.springframework.stereotype.Component
|
||||
|
||||
/**
|
||||
* 消息工具类
|
||||
* 用于获取国际化消息
|
||||
*/
|
||||
@Component
|
||||
class MessageUtils(
|
||||
private val messageSource: MessageSource
|
||||
) {
|
||||
/**
|
||||
* 根据 ErrorCode 获取国际化错误消息
|
||||
*/
|
||||
fun getMessage(errorCode: ErrorCode): String {
|
||||
return try {
|
||||
messageSource.getMessage(
|
||||
errorCode.messageKey,
|
||||
null,
|
||||
errorCode.message, // 默认消息(fallback)
|
||||
LocaleContextHolder.getLocale()
|
||||
) ?: errorCode.message
|
||||
} catch (e: Exception) {
|
||||
// 如果获取失败,使用默认消息
|
||||
errorCode.message
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据消息键获取国际化消息
|
||||
* @param key 消息键
|
||||
* @param defaultMessage 默认消息(如果找不到消息键)
|
||||
* @param args 消息参数(用于占位符替换)
|
||||
*/
|
||||
fun getMessage(key: String, defaultMessage: String = key, vararg args: Any?): String {
|
||||
return try {
|
||||
messageSource.getMessage(
|
||||
key,
|
||||
args,
|
||||
defaultMessage,
|
||||
LocaleContextHolder.getLocale()
|
||||
) ?: defaultMessage
|
||||
} catch (e: Exception) {
|
||||
defaultMessage
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user