+28
@@ -3,6 +3,7 @@ package com.wrbug.polymarketbot.controller.system
|
||||
import com.wrbug.polymarketbot.dto.*
|
||||
import com.wrbug.polymarketbot.enums.ErrorCode
|
||||
import com.wrbug.polymarketbot.service.system.ApiHealthCheckService
|
||||
import com.wrbug.polymarketbot.service.system.GeoblockService
|
||||
import com.wrbug.polymarketbot.service.system.ProxyConfigService
|
||||
import jakarta.servlet.http.HttpServletRequest
|
||||
import kotlinx.coroutines.runBlocking
|
||||
@@ -19,6 +20,7 @@ import org.springframework.web.bind.annotation.*
|
||||
class ProxyConfigController(
|
||||
private val proxyConfigService: ProxyConfigService,
|
||||
private val apiHealthCheckService: ApiHealthCheckService,
|
||||
private val geoblockService: GeoblockService,
|
||||
private val messageSource: MessageSource
|
||||
) {
|
||||
|
||||
@@ -123,6 +125,32 @@ class ProxyConfigController(
|
||||
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_ERROR, "API 健康检查失败:${e.message}", messageSource))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查交易服务器出口 IP 的地域限制(Polymarket Geoblock)
|
||||
*/
|
||||
@PostMapping("/geoblock-check")
|
||||
fun checkGeoblock(): ResponseEntity<ApiResponse<GeoblockCheckDto>> {
|
||||
return try {
|
||||
val result = runBlocking { geoblockService.checkGeoblock() }
|
||||
if (result.isSuccess) {
|
||||
ResponseEntity.ok(ApiResponse.success(result.getOrNull()))
|
||||
} else {
|
||||
val error = result.exceptionOrNull()
|
||||
logger.error("Geoblock 检查失败", error)
|
||||
ResponseEntity.ok(
|
||||
ApiResponse.error(
|
||||
ErrorCode.SERVER_ERROR,
|
||||
error?.message ?: "Geoblock 检查失败",
|
||||
messageSource
|
||||
)
|
||||
)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logger.error("Geoblock 检查异常", e)
|
||||
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_ERROR, "Geoblock 检查失败:${e.message}", messageSource))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.wrbug.polymarketbot.dto
|
||||
|
||||
/**
|
||||
* Polymarket Geoblock API 原始响应
|
||||
*/
|
||||
data class PolymarketGeoblockApiResponse(
|
||||
val blocked: Boolean = false,
|
||||
val ip: String = "",
|
||||
val country: String = "",
|
||||
val region: String = ""
|
||||
)
|
||||
|
||||
/**
|
||||
* 地域限制检查结果(返回给前端)
|
||||
*/
|
||||
data class GeoblockCheckDto(
|
||||
val blocked: Boolean,
|
||||
val ip: String,
|
||||
val country: String,
|
||||
val region: String,
|
||||
val checkedAt: Long,
|
||||
val source: String = "server"
|
||||
)
|
||||
@@ -36,6 +36,18 @@ data class SubscriptionProxyConfigRequest(
|
||||
val type: String // CLASH 或 SS
|
||||
)
|
||||
|
||||
/**
|
||||
* 代理检查时的 Polymarket 地域限制检测结果
|
||||
*/
|
||||
data class ProxyCheckGeoblockResult(
|
||||
val checked: Boolean,
|
||||
val blocked: Boolean? = null,
|
||||
val ip: String? = null,
|
||||
val country: String? = null,
|
||||
val region: String? = null,
|
||||
val message: String? = null
|
||||
)
|
||||
|
||||
/**
|
||||
* 代理检查响应
|
||||
*/
|
||||
@@ -43,15 +55,22 @@ data class ProxyCheckResponse(
|
||||
val success: Boolean,
|
||||
val message: String,
|
||||
val responseTime: Long? = null, // 响应时间(毫秒)
|
||||
val latency: Long? = null // 延迟(毫秒),与 responseTime 相同,用于前端显示
|
||||
val latency: Long? = null, // 延迟(毫秒),与 responseTime 相同,用于前端显示
|
||||
val geoblock: ProxyCheckGeoblockResult? = null
|
||||
) {
|
||||
companion object {
|
||||
fun create(success: Boolean, message: String, responseTime: Long? = null): ProxyCheckResponse {
|
||||
fun create(
|
||||
success: Boolean,
|
||||
message: String,
|
||||
responseTime: Long? = null,
|
||||
geoblock: ProxyCheckGeoblockResult? = null
|
||||
): ProxyCheckResponse {
|
||||
return ProxyCheckResponse(
|
||||
success = success,
|
||||
message = message,
|
||||
responseTime = responseTime,
|
||||
latency = responseTime // latency 和 responseTime 相同
|
||||
latency = responseTime,
|
||||
geoblock = geoblock
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
package com.wrbug.polymarketbot.service.system
|
||||
|
||||
import com.google.gson.Gson
|
||||
import com.wrbug.polymarketbot.dto.GeoblockCheckDto
|
||||
import com.wrbug.polymarketbot.dto.PolymarketGeoblockApiResponse
|
||||
import com.wrbug.polymarketbot.util.createClient
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.stereotype.Service
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
@Service
|
||||
class GeoblockService(
|
||||
private val gson: Gson
|
||||
) {
|
||||
|
||||
private val logger = LoggerFactory.getLogger(GeoblockService::class.java)
|
||||
|
||||
companion object {
|
||||
private const val GEOBLOCK_URL = "https://polymarket.com/api/geoblock"
|
||||
}
|
||||
|
||||
suspend fun checkGeoblock(): Result<GeoblockCheckDto> = withContext(Dispatchers.IO) {
|
||||
val client = createClient()
|
||||
.connectTimeout(10, TimeUnit.SECONDS)
|
||||
.readTimeout(10, TimeUnit.SECONDS)
|
||||
.writeTimeout(10, TimeUnit.SECONDS)
|
||||
.build()
|
||||
checkGeoblockWithClient(client)
|
||||
}
|
||||
|
||||
fun checkGeoblockWithClient(client: OkHttpClient): Result<GeoblockCheckDto> {
|
||||
return try {
|
||||
val request = Request.Builder()
|
||||
.url(GEOBLOCK_URL)
|
||||
.get()
|
||||
.header("Accept", "application/json")
|
||||
.build()
|
||||
|
||||
client.newCall(request).execute().use { response ->
|
||||
if (!response.isSuccessful) {
|
||||
return Result.failure(
|
||||
IllegalStateException("HTTP ${response.code}: ${response.message}")
|
||||
)
|
||||
}
|
||||
val body = response.body?.string()
|
||||
?: return Result.failure(IllegalStateException("Empty response body"))
|
||||
val apiResponse = gson.fromJson(body, PolymarketGeoblockApiResponse::class.java)
|
||||
Result.success(
|
||||
GeoblockCheckDto(
|
||||
blocked = apiResponse.blocked,
|
||||
ip = apiResponse.ip,
|
||||
country = apiResponse.country,
|
||||
region = apiResponse.region,
|
||||
checkedAt = System.currentTimeMillis()
|
||||
)
|
||||
)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logger.warn("Geoblock 检查失败", e)
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
+102
-48
@@ -24,7 +24,8 @@ import java.util.concurrent.TimeUnit
|
||||
*/
|
||||
@Service
|
||||
class ProxyConfigService(
|
||||
private val proxyConfigRepository: ProxyConfigRepository
|
||||
private val proxyConfigRepository: ProxyConfigRepository,
|
||||
private val geoblockService: GeoblockService
|
||||
) : ApplicationContextAware {
|
||||
|
||||
private var applicationContext: ApplicationContext? = null
|
||||
@@ -148,100 +149,153 @@ class ProxyConfigService(
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查代理是否可用
|
||||
* 使用配置的代理请求 Polymarket 健康检查接口
|
||||
* 检查代理是否可用,并检测经该代理访问 Polymarket 时的地域限制
|
||||
*/
|
||||
fun checkProxy(): ProxyCheckResponse {
|
||||
return try {
|
||||
val config = proxyConfigRepository.findByEnabledTrue()
|
||||
?: return ProxyCheckResponse.create(
|
||||
if (config == null) {
|
||||
return ProxyCheckResponse.create(
|
||||
success = false,
|
||||
message = "未配置代理或代理未启用"
|
||||
message = "未配置代理或代理未启用",
|
||||
geoblock = checkGeoblockForProxy(null)
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
if (config.type != "HTTP") {
|
||||
return ProxyCheckResponse.create(
|
||||
success = false,
|
||||
message = "当前仅支持检查 HTTP 代理(订阅代理检查功能待实现)"
|
||||
message = "当前仅支持检查 HTTP 代理(订阅代理检查功能待实现)",
|
||||
geoblock = checkGeoblockForProxy(null)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
if (config.host == null || config.port == null) {
|
||||
return ProxyCheckResponse.create(
|
||||
success = false,
|
||||
message = "代理配置不完整:缺少主机或端口"
|
||||
message = "代理配置不完整:缺少主机或端口",
|
||||
geoblock = checkGeoblockForProxy(null)
|
||||
)
|
||||
}
|
||||
|
||||
// 创建代理
|
||||
val proxy = Proxy(Proxy.Type.HTTP, InetSocketAddress(config.host, config.port))
|
||||
|
||||
// 创建 OkHttpClient
|
||||
val clientBuilder = OkHttpClient.Builder()
|
||||
.proxy(proxy)
|
||||
.connectTimeout(10, TimeUnit.SECONDS)
|
||||
.readTimeout(10, TimeUnit.SECONDS)
|
||||
.writeTimeout(10, TimeUnit.SECONDS)
|
||||
|
||||
// 配置 SSL:信任所有证书(用于代理连接)
|
||||
clientBuilder.createSSLSocketFactory()
|
||||
clientBuilder.hostnameVerifier(TrustAllHostnameVerifier())
|
||||
|
||||
// 如果配置了用户名和密码,添加代理认证
|
||||
if (config.username != null && config.password != null) {
|
||||
clientBuilder.proxyAuthenticator { _, response ->
|
||||
val credential = okhttp3.Credentials.basic(config.username, config.password)
|
||||
response.request.newBuilder()
|
||||
.header("Proxy-Authorization", credential)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
||||
val client = clientBuilder.build()
|
||||
|
||||
// 请求 Polymarket 健康检查接口
|
||||
|
||||
val client = buildProxyHttpClient(config)
|
||||
val geoblock = checkGeoblockForProxy(client)
|
||||
|
||||
val request = Request.Builder()
|
||||
.url("https://data-api.polymarket.com/")
|
||||
.get()
|
||||
.build()
|
||||
|
||||
|
||||
val startTime = System.currentTimeMillis()
|
||||
val response = client.newCall(request).execute()
|
||||
val responseTime = System.currentTimeMillis() - startTime
|
||||
|
||||
|
||||
val responseBody = response.body?.string()
|
||||
|
||||
|
||||
if (response.isSuccessful && responseBody != null) {
|
||||
// 检查响应内容是否为 {"data": "OK"}
|
||||
if (responseBody.contains("\"data\"") && responseBody.contains("OK")) {
|
||||
logger.info("代理检查成功:host=${config.host}, port=${config.port}, responseTime=${responseTime}ms")
|
||||
val message = buildProxyCheckMessage("代理连接成功", geoblock)
|
||||
ProxyCheckResponse.create(
|
||||
success = true,
|
||||
message = "代理连接成功",
|
||||
responseTime = responseTime
|
||||
message = message,
|
||||
responseTime = responseTime,
|
||||
geoblock = geoblock
|
||||
)
|
||||
} else {
|
||||
ProxyCheckResponse.create(
|
||||
success = false,
|
||||
message = "代理连接成功,但响应格式不正确:$responseBody",
|
||||
responseTime = responseTime
|
||||
message = buildProxyCheckMessage(
|
||||
"代理连接成功,但响应格式不正确:$responseBody",
|
||||
geoblock
|
||||
),
|
||||
responseTime = responseTime,
|
||||
geoblock = geoblock
|
||||
)
|
||||
}
|
||||
} else {
|
||||
ProxyCheckResponse.create(
|
||||
success = false,
|
||||
message = "代理连接失败:HTTP ${response.code} ${response.message}",
|
||||
responseTime = responseTime
|
||||
message = buildProxyCheckMessage(
|
||||
"代理连接失败:HTTP ${response.code} ${response.message}",
|
||||
geoblock
|
||||
),
|
||||
responseTime = responseTime,
|
||||
geoblock = geoblock
|
||||
)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logger.error("代理检查异常", e)
|
||||
ProxyCheckResponse.create(
|
||||
success = false,
|
||||
message = "代理检查失败:${e.message}"
|
||||
message = "代理检查失败:${e.message}",
|
||||
geoblock = checkGeoblockForProxy(null)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildProxyHttpClient(config: ProxyConfig): OkHttpClient {
|
||||
val host = requireNotNull(config.host) { "代理主机不能为空" }
|
||||
val port = requireNotNull(config.port) { "代理端口不能为空" }
|
||||
val proxy = Proxy(Proxy.Type.HTTP, InetSocketAddress(host, port))
|
||||
val clientBuilder = OkHttpClient.Builder()
|
||||
.proxy(proxy)
|
||||
.connectTimeout(10, TimeUnit.SECONDS)
|
||||
.readTimeout(10, TimeUnit.SECONDS)
|
||||
.writeTimeout(10, TimeUnit.SECONDS)
|
||||
|
||||
clientBuilder.createSSLSocketFactory()
|
||||
clientBuilder.hostnameVerifier(TrustAllHostnameVerifier())
|
||||
|
||||
if (config.username != null && config.password != null) {
|
||||
clientBuilder.proxyAuthenticator { _, response ->
|
||||
val credential = Credentials.basic(config.username, config.password)
|
||||
response.request.newBuilder()
|
||||
.header("Proxy-Authorization", credential)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
||||
return clientBuilder.build()
|
||||
}
|
||||
|
||||
private fun checkGeoblockForProxy(client: OkHttpClient?): ProxyCheckGeoblockResult {
|
||||
val httpClient = client ?: com.wrbug.polymarketbot.util.createClient()
|
||||
.connectTimeout(10, TimeUnit.SECONDS)
|
||||
.readTimeout(10, TimeUnit.SECONDS)
|
||||
.writeTimeout(10, TimeUnit.SECONDS)
|
||||
.build()
|
||||
|
||||
return geoblockService.checkGeoblockWithClient(httpClient).fold(
|
||||
onSuccess = { dto ->
|
||||
ProxyCheckGeoblockResult(
|
||||
checked = true,
|
||||
blocked = dto.blocked,
|
||||
ip = dto.ip,
|
||||
country = dto.country,
|
||||
region = dto.region,
|
||||
message = if (dto.blocked) {
|
||||
"当前出口 IP(${dto.country}/${dto.region})在 Polymarket 受限,无法下单"
|
||||
} else {
|
||||
"当前出口 IP(${dto.country}/${dto.region})可向 Polymarket 下单"
|
||||
}
|
||||
)
|
||||
},
|
||||
onFailure = { e ->
|
||||
ProxyCheckGeoblockResult(
|
||||
checked = true,
|
||||
message = "地域限制检测失败:${e.message}"
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun buildProxyCheckMessage(baseMessage: String, geoblock: ProxyCheckGeoblockResult): String {
|
||||
if (!geoblock.checked || geoblock.message.isNullOrBlank()) {
|
||||
return baseMessage
|
||||
}
|
||||
return "$baseMessage;${geoblock.message}"
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除代理配置
|
||||
|
||||
Reference in New Issue
Block a user