refactor: 移除 polygon.rpc.url 配置,使用 RpcNodeService 统一管理 RPC 节点

- 删除 application.properties 中的 polygon.rpc.url 配置
- 更新 ApiHealthCheckService 直接使用 RpcNodeService.getHttpUrl()
- 删除所有 Docker Compose 配置中的 POLYGON_RPC_URL 环境变量
- 删除所有部署脚本中的 POLYGON_RPC_URL 环境变量
- 更新所有文档,移除 POLYGON_RPC_URL 相关说明
- 删除 application.properties 中无用的 position.push 配置项
- 修正日志配置中的包名(polyhermes -> polymarketbot)

现在系统通过 RpcNodeService 从数据库读取 RPC 节点配置,用户可以通过系统设置页面管理 RPC 节点,不再需要环境变量配置。
This commit is contained in:
WrBug
2025-12-28 04:51:07 +08:00
parent 9f0b22fab5
commit 89fb980da7
15 changed files with 75 additions and 135 deletions
-2
View File
@@ -270,7 +270,6 @@ DB_USERNAME=root
DB_PASSWORD=your_password_here DB_PASSWORD=your_password_here
SPRING_PROFILES_ACTIVE=prod SPRING_PROFILES_ACTIVE=prod
SERVER_PORT=80 SERVER_PORT=80
POLYGON_RPC_URL=https://polygon-rpc.com
JWT_SECRET=your-jwt-secret-key-change-in-production JWT_SECRET=your-jwt-secret-key-change-in-production
ADMIN_RESET_PASSWORD_KEY=your-admin-reset-key-change-in-production ADMIN_RESET_PASSWORD_KEY=your-admin-reset-key-change-in-production
EOF EOF
@@ -337,7 +336,6 @@ cd frontend
| `DB_USERNAME` | 数据库用户名 | `root` | | `DB_USERNAME` | 数据库用户名 | `root` |
| `DB_PASSWORD` | 数据库密码 | - | | `DB_PASSWORD` | 数据库密码 | - |
| `SERVER_PORT` | 后端服务端口 | `8000` | | `SERVER_PORT` | 后端服务端口 | `8000` |
| `POLYGON_RPC_URL` | Polygon RPC 地址 | `https://polygon-rpc.com` |
| `JWT_SECRET` | JWT 密钥 | - | | `JWT_SECRET` | JWT 密钥 | - |
| `ADMIN_RESET_PASSWORD_KEY` | 管理员密码重置密钥 | - | | `ADMIN_RESET_PASSWORD_KEY` | 管理员密码重置密钥 | - |
| `CRYPTO_SECRET_KEY` | 加密密钥(用于加密存储私钥和 API Key) | - | | `CRYPTO_SECRET_KEY` | 加密密钥(用于加密存储私钥和 API Key) | - |
-2
View File
@@ -270,7 +270,6 @@ DB_USERNAME=root
DB_PASSWORD=your_password_here DB_PASSWORD=your_password_here
SPRING_PROFILES_ACTIVE=prod SPRING_PROFILES_ACTIVE=prod
SERVER_PORT=80 SERVER_PORT=80
POLYGON_RPC_URL=https://polygon-rpc.com
JWT_SECRET=your-jwt-secret-key-change-in-production JWT_SECRET=your-jwt-secret-key-change-in-production
ADMIN_RESET_PASSWORD_KEY=your-admin-reset-key-change-in-production ADMIN_RESET_PASSWORD_KEY=your-admin-reset-key-change-in-production
EOF EOF
@@ -337,7 +336,6 @@ cd frontend
| `DB_USERNAME` | Database username | `root` | | `DB_USERNAME` | Database username | `root` |
| `DB_PASSWORD` | Database password | - | | `DB_PASSWORD` | Database password | - |
| `SERVER_PORT` | Backend service port | `8000` | | `SERVER_PORT` | Backend service port | `8000` |
| `POLYGON_RPC_URL` | Polygon RPC address | `https://polygon-rpc.com` |
| `JWT_SECRET` | JWT secret key | - | | `JWT_SECRET` | JWT secret key | - |
| `ADMIN_RESET_PASSWORD_KEY` | Admin password reset key | - | | `ADMIN_RESET_PASSWORD_KEY` | Admin password reset key | - |
| `CRYPTO_SECRET_KEY` | Encryption key (for encrypting stored private keys and API Keys) | - | | `CRYPTO_SECRET_KEY` | Encryption key (for encrypting stored private keys and API Keys) | - |
-3
View File
@@ -154,9 +154,6 @@ SPRING_PROFILES_ACTIVE=prod
# 服务器端口 # 服务器端口
SERVER_PORT=8000 SERVER_PORT=8000
# Polygon RPC
POLYGON_RPC_URL=https://polygon-rpc.com
# JWT 密钥(已自动生成随机值,生产环境建议修改) # JWT 密钥(已自动生成随机值,生产环境建议修改)
JWT_SECRET=${JWT_SECRET} JWT_SECRET=${JWT_SECRET}
-1
View File
@@ -14,7 +14,6 @@ services:
- DB_USERNAME=${DB_USERNAME:-root} - DB_USERNAME=${DB_USERNAME:-root}
- DB_PASSWORD=${DB_PASSWORD:-} - DB_PASSWORD=${DB_PASSWORD:-}
- SERVER_PORT=8000 - SERVER_PORT=8000
- POLYGON_RPC_URL=${POLYGON_RPC_URL:-https://polygon-rpc.com}
- JWT_SECRET=${JWT_SECRET:-change-me-in-production} - JWT_SECRET=${JWT_SECRET:-change-me-in-production}
- ADMIN_RESET_PASSWORD_KEY=${ADMIN_RESET_PASSWORD_KEY:-change-me-in-production} - ADMIN_RESET_PASSWORD_KEY=${ADMIN_RESET_PASSWORD_KEY:-change-me-in-production}
depends_on: depends_on:
@@ -125,88 +125,88 @@ open class CopyOrderTrackingService(
return mutex.withLock { return mutex.withLock {
try { try {
// 1. 检查是否已处理(去重,包括失败状态) // 1. 检查是否已处理(去重,包括失败状态)
val existingProcessed = processedTradeRepository.findByLeaderIdAndLeaderTradeId(leaderId, trade.id) val existingProcessed = processedTradeRepository.findByLeaderIdAndLeaderTradeId(leaderId, trade.id)
if (existingProcessed != null) { if (existingProcessed != null) {
if (existingProcessed.status == "FAILED") { if (existingProcessed.status == "FAILED") {
return@withLock Result.success(Unit) return@withLock Result.success(Unit)
} }
return@withLock Result.success(Unit) return@withLock Result.success(Unit)
} }
// 检查是否已记录为失败交易 // 检查是否已记录为失败交易
val failedTrade = failedTradeRepository.findByLeaderIdAndLeaderTradeId(leaderId, trade.id) val failedTrade = failedTradeRepository.findByLeaderIdAndLeaderTradeId(leaderId, trade.id)
if (failedTrade != null) { if (failedTrade != null) {
return@withLock Result.success(Unit) return@withLock Result.success(Unit)
} }
// 2. 处理交易逻辑 // 2. 处理交易逻辑
val result = when (trade.side.uppercase()) { val result = when (trade.side.uppercase()) {
"BUY" -> processBuyTrade(leaderId, trade) "BUY" -> processBuyTrade(leaderId, trade)
"SELL" -> processSellTrade(leaderId, trade) "SELL" -> processSellTrade(leaderId, trade)
else -> { else -> {
logger.warn("未知的交易方向: ${trade.side}") logger.warn("未知的交易方向: ${trade.side}")
Result.failure(IllegalArgumentException("未知的交易方向: ${trade.side}")) Result.failure(IllegalArgumentException("未知的交易方向: ${trade.side}"))
}
} }
}
if (result.isFailure) { if (result.isFailure) {
logger.error( logger.error(
"处理交易失败: leaderId=$leaderId, tradeId=${trade.id}, side=${trade.side}", "处理交易失败: leaderId=$leaderId, tradeId=${trade.id}, side=${trade.side}",
result.exceptionOrNull() result.exceptionOrNull()
) )
return@withLock result return@withLock result
} }
// 3. 标记为已处理(成功状态) // 3. 标记为已处理(成功状态)
// 由于使用了 Mutex,这里理论上不会出现并发冲突,但保留异常处理作为兜底 // 由于使用了 Mutex,这里理论上不会出现并发冲突,但保留异常处理作为兜底
try { try {
val processed = ProcessedTrade( val processed = ProcessedTrade(
leaderId = leaderId, leaderId = leaderId,
leaderTradeId = trade.id, leaderTradeId = trade.id,
tradeType = trade.side.uppercase(), tradeType = trade.side.uppercase(),
source = source, source = source,
status = "SUCCESS", status = "SUCCESS",
processedAt = System.currentTimeMillis() processedAt = System.currentTimeMillis()
) )
processedTradeRepository.save(processed) processedTradeRepository.save(processed)
} catch (e: Exception) {
// 检查是否是唯一键冲突异常(理论上不会发生,但保留作为兜底)
if (isUniqueConstraintViolation(e)) {
val existing = processedTradeRepository.findByLeaderIdAndLeaderTradeId(leaderId, trade.id)
if (existing != null) {
if (existing.status == "FAILED") {
logger.debug("交易已标记为失败,跳过处理: leaderId=$leaderId, tradeId=${trade.id}")
return@withLock Result.success(Unit)
}
logger.debug("交易已处理(并发检测): leaderId=$leaderId, tradeId=${trade.id}, status=${existing.status}")
return@withLock Result.success(Unit)
} else {
// 如果检查不到,可能是事务隔离级别问题,等待一下再查询
delay(100)
val existingAfterDelay =
processedTradeRepository.findByLeaderIdAndLeaderTradeId(leaderId, trade.id)
if (existingAfterDelay != null) {
logger.debug("延迟查询到记录(并发检测): leaderId=$leaderId, tradeId=${trade.id}, status=${existingAfterDelay.status}")
return@withLock Result.success(Unit)
}
logger.warn(
"保存ProcessedTrade时发生唯一约束冲突,但查询不到记录: leaderId=$leaderId, tradeId=${trade.id}",
e
)
return@withLock Result.success(Unit)
}
} else {
// 其他类型的异常,重新抛出
throw e
}
}
Result.success(Unit)
} catch (e: Exception) { } catch (e: Exception) {
logger.error("处理交易异常: leaderId=$leaderId, tradeId=${trade.id}", e) // 检查是否是唯一键冲突异常(理论上不会发生,但保留作为兜底)
Result.failure(e) if (isUniqueConstraintViolation(e)) {
val existing = processedTradeRepository.findByLeaderIdAndLeaderTradeId(leaderId, trade.id)
if (existing != null) {
if (existing.status == "FAILED") {
logger.debug("交易已标记为失败,跳过处理: leaderId=$leaderId, tradeId=${trade.id}")
return@withLock Result.success(Unit)
}
logger.debug("交易已处理(并发检测): leaderId=$leaderId, tradeId=${trade.id}, status=${existing.status}")
return@withLock Result.success(Unit)
} else {
// 如果检查不到,可能是事务隔离级别问题,等待一下再查询
delay(100)
val existingAfterDelay =
processedTradeRepository.findByLeaderIdAndLeaderTradeId(leaderId, trade.id)
if (existingAfterDelay != null) {
logger.debug("延迟查询到记录(并发检测): leaderId=$leaderId, tradeId=${trade.id}, status=${existingAfterDelay.status}")
return@withLock Result.success(Unit)
}
logger.warn(
"保存ProcessedTrade时发生唯一约束冲突,但查询不到记录: leaderId=$leaderId, tradeId=${trade.id}",
e
)
return@withLock Result.success(Unit)
}
} else {
// 其他类型的异常,重新抛出
throw e
}
}
Result.success(Unit)
} catch (e: Exception) {
logger.error("处理交易异常: leaderId=$leaderId, tradeId=${trade.id}", e)
Result.failure(e)
} }
} }
} }
@@ -28,8 +28,6 @@ class ApiHealthCheckService(
private val dataApiBaseUrl: String, private val dataApiBaseUrl: String,
@Value("\${polymarket.gamma.base-url}") @Value("\${polymarket.gamma.base-url}")
private val gammaBaseUrl: String, private val gammaBaseUrl: String,
@Value("\${polygon.rpc.url:}")
private val polygonRpcUrl: String,
@Value("\${polymarket.rtds.ws-url}") @Value("\${polymarket.rtds.ws-url}")
private val polymarketWsUrl: String, private val polymarketWsUrl: String,
@Value("\${polymarket.builder.relayer-url:}") @Value("\${polymarket.builder.relayer-url:}")
@@ -187,27 +185,11 @@ class ApiHealthCheckService(
/** /**
* 检查 Polygon RPC * 检查 Polygon RPC
* 使用动态获取的可用节点而不是固定的配置 * 使用动态获取的可用节点RpcNodeService 总是返回一个有效的 URL包括默认节点
*/ */
private suspend fun checkPolygonRpc(): ApiHealthCheckDto = withContext(Dispatchers.IO) { private suspend fun checkPolygonRpc(): ApiHealthCheckDto = withContext(Dispatchers.IO) {
// 优先使用动态获取可用节点 // 使用 RpcNodeService 获取可用节点(总是返回有效值,包括默认节点)
val rpcUrl = try { val rpcUrl = rpcNodeService.getHttpUrl()
rpcNodeService.getHttpUrl()
} catch (e: Exception) {
logger.debug("获取可用 RPC 节点失败,使用配置的默认值: ${e.message}")
// 如果获取失败,使用配置的默认值作为兜底
if (polygonRpcUrl.isNotBlank()) {
polygonRpcUrl
} else {
return@withContext ApiHealthCheckDto(
name = "Polygon RPC",
url = "未配置",
status = "skipped",
message = "未配置 Polygon RPC URL 且没有可用的节点"
)
}
}
checkJsonRpcApi("Polygon RPC", rpcUrl) checkJsonRpcApi("Polygon RPC", rpcUrl)
} }
@@ -40,23 +40,12 @@ polymarket.rtds.ws-url=wss://ws-subscriptions-clob.polymarket.com
polymarket.data-api.base-url=https://data-api.polymarket.com polymarket.data-api.base-url=https://data-api.polymarket.com
polymarket.gamma.base-url=https://gamma-api.polymarket.com polymarket.gamma.base-url=https://gamma-api.polymarket.com
# Polygon RPC 配置(用于查询链上余额)
# 可选:如果未配置,将无acc法查询 USDC 余额,但仍可通过 Subgraph API 查询持仓
# 示例:https://polygon-rpc.com 或 https://polygon-mainnet.infura.io/v3/YOUR_PROJECT_ID
polygon.rpc.url=${POLYGON_RPC_URL:https://polygon-rpc.com}
# Builder Relayer 配置(用于 Gasless 交易) # Builder Relayer 配置(用于 Gasless 交易)
# 从 polymarket.com/settings?tab=builder 获取 Builder API 凭证 # 从 polymarket.com/settings?tab=builder 获取 Builder API 凭证
# Builder API Key、Secret、Passphrase 现在通过系统设置页面配置,存储在数据库中 # Builder API Key、Secret、Passphrase 现在通过系统设置页面配置,存储在数据库中
# 如果未配置,将使用手动发送交易的方式(需要用户支付 gas) # 如果未配置,将使用手动发送交易的方式(需要用户支付 gas)
polymarket.builder.relayer-url=${POLYMARKET_BUILDER_RELAYER_URL:https://relayer-v2.polymarket.com/} polymarket.builder.relayer-url=${POLYMARKET_BUILDER_RELAYER_URL:https://relayer-v2.polymarket.com/}
# 仓位推送配置
# 轮询间隔(毫秒),默认3秒
position.push.polling-interval=${POSITION_PUSH_POLLING_INTERVAL:3000}
# 心跳超时时间(毫秒),默认60秒,超过此时间未收到心跳则清理连接
position.push.heartbeat-timeout=${POSITION_PUSH_HEARTBEAT_TIMEOUT:60000}
# 跟单轮询配置 # 跟单轮询配置
# 轮询间隔(毫秒),默认2秒 # 轮询间隔(毫秒),默认2秒
copy.trading.polling.interval=${COPY_TRADING_POLLING_INTERVAL:2000} copy.trading.polling.interval=${COPY_TRADING_POLLING_INTERVAL:2000}
-3
View File
@@ -71,9 +71,6 @@ SERVER_PORT=80
# MySQL 端口(可选,用于外部连接,默认 3307 避免与本地 MySQL 冲突) # MySQL 端口(可选,用于外部连接,默认 3307 避免与本地 MySQL 冲突)
MYSQL_PORT=3307 MYSQL_PORT=3307
# Polygon RPC
POLYGON_RPC_URL=https://polygon-rpc.com
# JWT 密钥(已自动生成随机值,生产环境建议修改) # JWT 密钥(已自动生成随机值,生产环境建议修改)
JWT_SECRET=${JWT_SECRET} JWT_SECRET=${JWT_SECRET}
-6
View File
@@ -38,12 +38,6 @@ SERVER_PORT=80
# Optional, for external connections, default 3307 to avoid conflicts with local MySQL # Optional, for external connections, default 3307 to avoid conflicts with local MySQL
MYSQL_PORT=3307 MYSQL_PORT=3307
# ============================================
# Polygon RPC
# ============================================
# Polygon 网络 RPC 地址 / Polygon network RPC address
POLYGON_RPC_URL=https://polygon-rpc.com
# ============================================ # ============================================
# 安全配置 / Security Configuration # 安全配置 / Security Configuration
# ============================================ # ============================================
-1
View File
@@ -24,7 +24,6 @@ services:
- DB_USERNAME=${DB_USERNAME:-root} - DB_USERNAME=${DB_USERNAME:-root}
- DB_PASSWORD=${DB_PASSWORD:-} - DB_PASSWORD=${DB_PASSWORD:-}
- SERVER_PORT=8000 - SERVER_PORT=8000
- POLYGON_RPC_URL=${POLYGON_RPC_URL:-https://polygon-rpc.com}
# ⚠️ 安全警告:以下两个环境变量不能使用默认值,否则容器启动会失败 # ⚠️ 安全警告:以下两个环境变量不能使用默认值,否则容器启动会失败
# 请在 .env 文件中设置,或通过环境变量传入 # 请在 .env 文件中设置,或通过环境变量传入
# 生成随机密钥:openssl rand -hex 32 (ADMIN_RESET_PASSWORD_KEY) 或 openssl rand -hex 64 (JWT_SECRET) # 生成随机密钥:openssl rand -hex 32 (ADMIN_RESET_PASSWORD_KEY) 或 openssl rand -hex 64 (JWT_SECRET)
-1
View File
@@ -22,7 +22,6 @@ services:
- DB_USERNAME=${DB_USERNAME:-root} - DB_USERNAME=${DB_USERNAME:-root}
- DB_PASSWORD=${DB_PASSWORD:-} - DB_PASSWORD=${DB_PASSWORD:-}
- SERVER_PORT=8000 - SERVER_PORT=8000
- POLYGON_RPC_URL=${POLYGON_RPC_URL:-https://polygon-rpc.com}
# ⚠️ 安全警告:以下两个环境变量不能使用默认值,否则容器启动会失败 # ⚠️ 安全警告:以下两个环境变量不能使用默认值,否则容器启动会失败
# 请在 .env 文件中设置,或通过环境变量传入 # 请在 .env 文件中设置,或通过环境变量传入
# 生成随机密钥:openssl rand -hex 32 (ADMIN_RESET_PASSWORD_KEY) 或 openssl rand -hex 64 (JWT_SECRET) # 生成随机密钥:openssl rand -hex 32 (ADMIN_RESET_PASSWORD_KEY) 或 openssl rand -hex 64 (JWT_SECRET)
-3
View File
@@ -163,7 +163,6 @@ DB_USERNAME=root
DB_PASSWORD=your_password_here DB_PASSWORD=your_password_here
SPRING_PROFILES_ACTIVE=prod SPRING_PROFILES_ACTIVE=prod
SERVER_PORT=80 SERVER_PORT=80
POLYGON_RPC_URL=https://polygon-rpc.com
JWT_SECRET=your-jwt-secret-key-change-in-production JWT_SECRET=your-jwt-secret-key-change-in-production
ADMIN_RESET_PASSWORD_KEY=your-admin-reset-key-change-in-production ADMIN_RESET_PASSWORD_KEY=your-admin-reset-key-change-in-production
EOF EOF
@@ -393,7 +392,6 @@ DB_USERNAME=root
DB_PASSWORD=your_password_here DB_PASSWORD=your_password_here
SPRING_PROFILES_ACTIVE=prod SPRING_PROFILES_ACTIVE=prod
SERVER_PORT=8000 SERVER_PORT=8000
POLYGON_RPC_URL=https://polygon-rpc.com
JWT_SECRET=your-jwt-secret-key-change-in-production JWT_SECRET=your-jwt-secret-key-change-in-production
ADMIN_RESET_PASSWORD_KEY=your-admin-reset-key-change-in-production ADMIN_RESET_PASSWORD_KEY=your-admin-reset-key-change-in-production
EOF EOF
@@ -554,7 +552,6 @@ serve -s dist -l 3000
| `DB_USERNAME` | Database username | `root` | Yes (Production) | | `DB_USERNAME` | Database username | `root` | Yes (Production) |
| `DB_PASSWORD` | Database password | - | Yes (Production) | | `DB_PASSWORD` | Database password | - | Yes (Production) |
| `SERVER_PORT` | Server port | `8000` | No | | `SERVER_PORT` | Server port | `8000` | No |
| `POLYGON_RPC_URL` | Polygon RPC address | `https://polygon-rpc.com` | No |
| `JWT_SECRET` | JWT secret key | - | Yes (Production) | | `JWT_SECRET` | JWT secret key | - | Yes (Production) |
| `ADMIN_RESET_PASSWORD_KEY` | Admin password reset key | - | Yes (Production) | | `ADMIN_RESET_PASSWORD_KEY` | Admin password reset key | - | Yes (Production) |
-3
View File
@@ -100,9 +100,6 @@ spring.datasource.password=${DB_PASSWORD:password}
# Server port # Server port
server.port=${SERVER_PORT:8000} server.port=${SERVER_PORT:8000}
# Polygon RPC
polygon.rpc.url=${POLYGON_RPC_URL:https://polygon-rpc.com}
# JWT secret # JWT secret
jwt.secret=${JWT_SECRET:change-me-in-production} jwt.secret=${JWT_SECRET:change-me-in-production}
-3
View File
@@ -165,7 +165,6 @@ DB_USERNAME=root
DB_PASSWORD=your_password_here DB_PASSWORD=your_password_here
SPRING_PROFILES_ACTIVE=prod SPRING_PROFILES_ACTIVE=prod
SERVER_PORT=80 SERVER_PORT=80
POLYGON_RPC_URL=https://polygon-rpc.com
JWT_SECRET=your-jwt-secret-key-change-in-production JWT_SECRET=your-jwt-secret-key-change-in-production
ADMIN_RESET_PASSWORD_KEY=your-admin-reset-key-change-in-production ADMIN_RESET_PASSWORD_KEY=your-admin-reset-key-change-in-production
EOF EOF
@@ -397,7 +396,6 @@ DB_USERNAME=root
DB_PASSWORD=your_password_here DB_PASSWORD=your_password_here
SPRING_PROFILES_ACTIVE=prod SPRING_PROFILES_ACTIVE=prod
SERVER_PORT=8000 SERVER_PORT=8000
POLYGON_RPC_URL=https://polygon-rpc.com
JWT_SECRET=your-jwt-secret-key-change-in-production JWT_SECRET=your-jwt-secret-key-change-in-production
ADMIN_RESET_PASSWORD_KEY=your-admin-reset-key-change-in-production ADMIN_RESET_PASSWORD_KEY=your-admin-reset-key-change-in-production
EOF EOF
@@ -558,7 +556,6 @@ serve -s dist -l 3000
| `DB_USERNAME` | 数据库用户名 | `root` | 是(生产) | | `DB_USERNAME` | 数据库用户名 | `root` | 是(生产) |
| `DB_PASSWORD` | 数据库密码 | - | 是(生产) | | `DB_PASSWORD` | 数据库密码 | - | 是(生产) |
| `SERVER_PORT` | 服务器端口 | `8000` | 否 | | `SERVER_PORT` | 服务器端口 | `8000` | 否 |
| `POLYGON_RPC_URL` | Polygon RPC 地址 | `https://polygon-rpc.com` | 否 |
| `JWT_SECRET` | JWT 密钥 | - | 是(生产) | | `JWT_SECRET` | JWT 密钥 | - | 是(生产) |
| `ADMIN_RESET_PASSWORD_KEY` | 管理员密码重置密钥 | - | 是(生产) | | `ADMIN_RESET_PASSWORD_KEY` | 管理员密码重置密钥 | - | 是(生产) |
-3
View File
@@ -93,9 +93,6 @@ spring.datasource.password=${DB_PASSWORD:password}
# 服务器端口 # 服务器端口
server.port=${SERVER_PORT:8000} server.port=${SERVER_PORT:8000}
# Polygon RPC
polygon.rpc.url=${POLYGON_RPC_URL:https://polygon-rpc.com}
# JWT 密钥 # JWT 密钥
jwt.secret=${JWT_SECRET:change-me-in-production} jwt.secret=${JWT_SECRET:change-me-in-production}