Files
PolyHermes/.cursor/rules/backend.mdc
T
WrBug 777710c2ed feat: 完善 Telegram 推送通知功能
- 推送模板优化:
  - 优先使用账户名称而不是钱包地址
  - 使用市场标题而不是16进制ID
  - 添加可点击的市场链接(支持 slug 和 conditionId)
  - 添加市场方向(outcome)显示
  - 数量和价格从订单详情API获取实际值
  - 失败通知只显示后端返回的msg,不显示完整堆栈

- 多语言支持:
  - 后端推送消息支持多语言(使用前端最后请求的语言)
  - 添加所有 ErrorCode 的多语言资源文件(中文简体、繁体、英文)
  - 通知消息文本全部使用多语言资源

- 功能改进:
  - 从订单详情获取实际的 side、price、size、outcome
  - 支持买入/卖出方向的多语言显示
  - 错误信息优化,只显示后端返回的错误消息
2025-12-05 02:20:46 +08:00

657 lines
21 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
alwaysApply: true
path: backend/**
---
# 后端开发规范
## 代码完成规范
### TODO 处理规则
- **禁止**在代码中添加 TODO 注释
- **必须**根据 TODO 的内容直接完成代码实现
- 如果遇到暂时无法完全实现的功能,应该:
1. 实现一个可用的基础版本
2. 添加清晰的注释说明当前实现的限制和后续改进方向
3. 确保代码可以正常编译和运行
- **禁止**使用 `// TODO: 实现XXX` 这样的注释
- **禁止**使用 `// FIXME:` 或 `// XXX:` 这样的注释
- 如果某个功能需要依赖外部资源(如 API、库等),应该:
1. 先实现一个占位或模拟实现
2. 在注释中说明依赖关系和实现方式
3. 确保代码逻辑完整,不会因为未实现的功能而崩溃
### API 调用实现规则
- **必须**查找相关的 API 文档或接口定义
- **必须**根据 API 文档完成代码实现
- **禁止**在 API 调用处添加 TODO 注释
- **禁止**直接返回 mock 数据或硬编码的假数据
- **必须**调用真实的 API 或查询真实的数据库
- 如果 API 文档不完整,应该:
1. 查找项目中已有的类似 API 调用作为参考
2. 查看 API 接口定义(如 `PolymarketClobApi.kt`
3. 查看 API 文档(如 `docs/polymarket-api-reference.md`
4. 实现一个可用的版本,包含错误处理
- 如果 API 调用失败,应该:
1. 返回明确的错误信息
2. 记录错误日志
3. 返回错误响应,而不是返回 mock 数据
### 代码完成示例
```kotlin
// ❌ 错误:添加 TODO 注释
fun getAccountBalance(accountId: Long?): Result<AccountBalanceResponse> {
// TODO: 调用 Polymarket API 查询余额
return Result.success(AccountBalanceResponse(usdcBalance = "0"))
}
// ✅ 正确:查找 API 文档并完成实现
// 1. 查找 API 接口定义:PolymarketClobApi.getActiveOrders()
// 2. 查找 API 文档:docs/polymarket-api-reference.md
// 3. 实现完整的 API 调用逻辑,调用真实的 API
fun getAccountBalance(accountId: Long?): Result<AccountBalanceResponse> {
return try {
val account = getAccount(accountId) ?: return Result.failure(IllegalArgumentException("账户不存在"))
// 如果账户没有配置 API 凭证,返回错误而不是 mock 数据
if (account.apiKey == null || account.apiSecret == null || account.apiPassphrase == null) {
return Result.failure(IllegalStateException("账户未配置 API 凭证,无法查询余额"))
}
// 解密 API 凭证并创建认证客户端
val apiKey = cryptoUtils.decrypt(account.apiKey!!)
val apiSecret = cryptoUtils.decrypt(account.apiSecret!!)
val apiPassphrase = cryptoUtils.decrypt(account.apiPassphrase!!)
val clobApi = retrofitFactory.createClobApi(apiKey, apiSecret, apiPassphrase)
// 调用真实的 API 查询余额
val response = clobApi.getActiveOrders(limit = 100)
if (response.isSuccessful && response.body() != null) {
// 根据 API 响应处理余额数据(从真实响应中解析)
val orders = response.body()!!
// 实际应该调用余额查询接口或从链上查询
// 这里只是示例,实际应该调用真实的余额查询 API
val balance = queryRealBalanceFromApi(clobApi, account.walletAddress)
Result.success(AccountBalanceResponse(usdcBalance = balance))
} else {
logger.error("查询余额失败: ${response.code()} ${response.message()}")
Result.failure(Exception("查询余额失败: ${response.code()} ${response.message()}"))
}
} catch (e: Exception) {
logger.error("查询账户余额失败", e)
Result.failure(e)
}
}
```
## 需求文档引用
### 跟单系统需求
- **需求文档**: `docs/copy-trading-requirements.md`
- **说明**: 所有跟单系统相关的功能实现必须严格按照需求文档执行
- **核心功能**:
- 账户管理(通过私钥导入,支持多账户)
- Leader 管理(被跟单者管理)
- 订单同步与执行(监控 Leader 交易并自动复制)
- 跟单配置管理(全局配置和单个 Leader 配置)
- 风险控制(每日亏损限制、订单数限制等)
- 跟单记录与统计
**重要提示**: 在实现跟单系统相关功能时,请先查阅 `docs/copy-trading-requirements.md` 了解详细需求,包括:
- 数据模型设计(Account、Leader、CopyOrder 等)
- API 接口设计(请求/响应格式)
- 业务规则和验证逻辑
- 安全要求(私钥加密存储、API Key 管理等)
## 项目范围
### 平台支持
- **仅支持**: Polymarket 平台
- **不支持**: 其他预测市场平台(如 Kalshi 等)
### 分类支持
- **仅支持**:
- `sports`: 体育相关市场
- `crypto`: 加密货币相关市场
- **不支持**: 其他分类(如 politics、entertainment 等)
### 分类验证
- 所有涉及分类的接口、实体、服务必须验证分类参数
- 分类参数只能是 `sports` 或 `crypto`
- 无效分类应返回明确的错误提示
## 包名规范
- **包名**: `com.wrbug.polymarketbot`
- 所有代码必须使用此包名
## 实体类规范
### ID字段规范
- **必须**使用 `Long? = null` 作为 `@GeneratedValue` 的 id 字段
- **禁止**使用 `Long = 0` 或其他默认值
```kotlin
// ✅ 正确
@Entity
@Table(name = "example_table")
data class ExampleEntity(
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long? = null,
// ...
)
// ❌ 错误
@Entity
data class ExampleEntity(
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long = 0, // 禁止使用
// ...
)
```
## 配置文件规范
### 配置文件格式
- **必须**使用 `application.properties` 格式
- **禁止**使用 `application.yml` 格式
- 配置文件位置: `src/main/resources/application.properties`
### 配置示例
```properties
# 应用配置
spring.application.name=polymarket-bot-backend
# 数据源配置
spring.datasource.url=jdbc:mysql://localhost:3306/polymarket_bot?useSSL=false&serverTimezone=UTC&characterEncoding=utf8mb4
spring.datasource.username=${DB_USERNAME:root}
spring.datasource.password=${DB_PASSWORD:password}
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
# HikariCP 连接池配置
spring.datasource.hikari.maximum-pool-size=10
spring.datasource.hikari.minimum-idle=2
spring.datasource.hikari.connection-timeout=30000
# JPA 配置
spring.jpa.hibernate.ddl-auto=validate
spring.jpa.show-sql=false
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL8Dialect
# Flyway 配置
spring.flyway.enabled=true
spring.flyway.locations=classpath:db/migration
spring.flyway.baseline-on-migrate=true
# 服务器配置
server.port=${SERVER_PORT:8000}
# 日志配置
logging.level.root=INFO
logging.level.com.wrbug.polymarketbot=DEBUG
logging.pattern.console=%d{yyyy-MM-dd HH:mm:ss} - %msg%n
```
### 环境变量引用
- 使用 `${ENV_VAR:default}` 格式引用环境变量
- 支持多环境配置: `application-dev.properties`, `application-prod.properties`
## 代码规范
### Controller规范
- Controller 方法**禁止**使用 `suspend`
- 如需调用 suspend 方法,使用 `runBlocking`(最小化使用)
- 只作用于 suspend 方法调用
```kotlin
// ✅ 正确
@RestController
class ExampleController(
private val exampleService: ExampleService
) {
@PostMapping("/example")
fun getExample(): ResponseEntity<ApiResponse<ExampleDto>> {
val data = runBlocking { exampleService.getData() }
return ResponseEntity.ok(ApiResponse.success(data))
}
}
// ❌ 错误
@RestController
class ExampleController {
@PostMapping("/example")
suspend fun getExample(): ResponseEntity<ApiResponse<ExampleDto>> { // 禁止使用suspend
// ...
}
}
```
### Service规范
- Service 层可以使用 `suspend` 方法
- 使用 `@Transactional` 管理事务
- 使用构造函数注入依赖
```kotlin
@Service
class ExampleService(
private val exampleRepository: ExampleRepository
) {
suspend fun getAllData(): List<ExampleEntity> {
return exampleRepository.findAll()
}
@Transactional
fun saveData(entity: ExampleEntity): ExampleEntity {
return exampleRepository.save(entity)
}
}
```
### Repository规范
- Repository 接口继承 `JpaRepository`
- 使用 Spring Data JPA 方法命名规范
```kotlin
@Repository
interface ExampleRepository : JpaRepository<ExampleEntity, Long> {
fun findByCode(code: String): ExampleEntity?
fun findByCategoryAndStatus(category: String, status: String): List<ExampleEntity>
fun findByCategory(category: String): List<ExampleEntity> // category: sports 或 crypto
}
```
### Entity规范
- 使用 `@Entity` 和 `@Table` 注解
- ID字段使用 `Long? = null`
- 时间字段使用 `Long` 时间戳(毫秒)
- 数值字段使用 `BigDecimal`,使用 `String` 存储
```kotlin
@Entity
@Table(name = "example_table")
data class ExampleEntity(
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long? = null,
@Column(name = "code", unique = true, nullable = false, length = 100)
val code: String = "",
@Column(name = "category", nullable = false, length = 20)
val category: String = "", // sports 或 crypto
@Column(name = "amount", nullable = false, precision = 20, scale = 8)
val amount: BigDecimal = BigDecimal.ZERO,
@Column(name = "status", nullable = false, length = 20)
val status: String = "", // active, inactive
@Column(name = "created_at", nullable = false)
val createdAt: Long = System.currentTimeMillis(),
@Column(name = "updated_at", nullable = false)
var updatedAt: Long = System.currentTimeMillis()
)
```
## 数值计算规范
### BigDecimal使用
- 所有数值计算必须使用 `BigDecimal`
- 使用 `String` 存储数值
- 使用扩展函数进行安全转换和比较
```kotlin
// 使用扩展函数
val amount = "0.5".toSafeBigDecimal()
val total = amount.add("0.4".toSafeBigDecimal())
// 比较
if (total.lt(BigDecimal.ONE)) {
// 业务逻辑
}
```
## 时间字段规范
### 时间戳使用
- 所有时间字段使用 `Long` 类型存储毫秒级时间戳
- **禁止**使用 `LocalDateTime` 或其他时间类型
```kotlin
// ✅ 正确
@Column(name = "created_at", nullable = false)
val createdAt: Long = System.currentTimeMillis()
// ❌ 错误
@Column(name = "created_at")
val createdAt: LocalDateTime = LocalDateTime.now() // 禁止使用
```
## HTTP客户端规范
### Retrofit使用
- 使用 Retrofit 定义 API 接口
- 使用 OkHttp 作为底层 HTTP 客户端
- 使用拦截器处理认证
```kotlin
// Polymarket CLOB API接口定义(跟单系统需要)
interface PolymarketClobApi {
@POST("/orders")
suspend fun createOrder(@Body order: CreateOrderRequest): Response<OrderResponse>
@GET("/orders/active")
suspend fun getActiveOrders(
@Query("market") market: String?,
@Query("limit") limit: Int?,
@Query("offset") offset: Int?
): Response<List<OrderResponse>>
@DELETE("/orders/{orderId}")
suspend fun cancelOrder(@Path("orderId") orderId: String): Response<CancelOrderResponse>
@GET("/trades")
suspend fun getTrades(
@Query("market") market: String?,
@Query("user") user: String?,
@Query("limit") limit: Int?,
@Query("offset") offset: Int?
): Response<List<TradeResponse>>
}
// Retrofit配置
@Configuration
class RetrofitConfig {
@Bean
fun polymarketClobApi(): PolymarketClobApi {
val okHttpClient = OkHttpClient.Builder()
.addInterceptor(AuthInterceptor())
.build()
return Retrofit.Builder()
.baseUrl("https://clob.polymarket.com")
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(PolymarketClobApi::class.java)
}
}
```
## API接口规范
### 请求规范
- **所有接口统一使用POST方法**,包括查询类接口
- 请求头: `Content-Type: application/json`
- 请求体: JSON格式
### 响应规范
- **统一响应格式**:
```json
{
"code": 0,
"data": {},
"msg": ""
}
```
- **响应字段说明**:
- `code`: 响应码,0表示成功,非0表示失败
- `data`: 响应数据,可以是任意类型(对象、数组、字符串、数字等)
- `msg`: 响应消息,成功时通常为空,失败时包含错误提示
### 响应示例
**成功响应**:
```json
{
"code": 0,
"data": {
"id": "123",
"name": "example"
},
"msg": ""
}
```
**失败响应**:
```json
{
"code": 1001,
"data": null,
"msg": "参数错误:参数不能为空"
}
```
### Controller实现示例
```kotlin
@RestController
@RequestMapping("/api/example")
class ExampleController(
private val exampleService: ExampleService
) {
private val logger = LoggerFactory.getLogger(ExampleController::class.java)
@PostMapping("/list")
fun getList(@RequestBody request: ExampleListRequest): ResponseEntity<ApiResponse<ExampleListResponse>> {
return try {
val data = runBlocking { exampleService.getList(request) }
val response = ExampleListResponse(
list = data,
total = data.size.toLong(),
page = request.page ?: 1,
limit = request.limit ?: 20
)
ResponseEntity.ok(ApiResponse.success(response))
} catch (e: Exception) {
logger.error("Failed to get list", e)
ResponseEntity.ok(ApiResponse.serverError("获取列表失败:${e.message}"))
}
}
}
// 统一响应格式
data class ApiResponse<T>(
val code: Int,
val data: T?,
val msg: String
) {
companion object {
fun <T> success(data: T): ApiResponse<T> = ApiResponse(0, data, "")
fun <T> paramError(msg: String): ApiResponse<T> = ApiResponse(1001, null, msg)
fun <T> serverError(msg: String): ApiResponse<T> = ApiResponse(5001, null, msg)
}
}
```
### 错误码规范
- `0`: 成功
- `1001-1999`: 参数错误
- `2001-2999`: 认证/权限错误
- `3001-3999`: 资源不存在
- `4001-4999`: 业务逻辑错误
- `5001-5999`: 服务器内部错误
详细错误码定义参见需求文档
## 禁止事项
### 代码质量
- ❌ 禁止使用 `!!` 除非有明确原因
- ❌ 禁止忽略异常
- ❌ 禁止硬编码配置值
- ❌ 禁止提交敏感信息到Git
- ❌ Controller 方法禁止使用 `suspend`
- ❌ 实体类ID禁止使用 `Long = 0`
- ❌ **禁止直接返回 mock 数据或硬编码的假数据**
- ❌ **禁止在 API 调用失败时返回 mock 数据作为默认值**
- ❌ **所有返回的数据必须来自真实的 API 调用或数据库查询**
### 配置
- ❌ 禁止使用 `application.yml`
- ❌ 禁止在代码中硬编码配置值
### 类型
- ❌ 禁止使用 `Double` 进行数值计算
- ❌ 禁止使用 `LocalDateTime` 存储时间
- ❌ 禁止实体类ID使用非空默认值
### API接口
- ❌ 禁止使用GET、PUT、DELETE等方法(统一使用POST
- ❌ 禁止返回不符合统一格式的响应
- ❌ 禁止在响应中直接返回Map类型(使用data class
### Side 判断规范
- ❌ **禁止使用 "YES" 或 "NO" 字符串去判断 side**
- ✅ **必须使用 `outcomeIndex` 来判断方向**0 = 第一个 outcome1 = 第二个 outcome,以此类推)
- ✅ 如果必须使用 side 字符串,应该从市场的 outcomes 数组中获取,而不是硬编码 "YES"/"NO"
- ✅ 对于二元市场的价格转换,应该通过 `outcomeIndex` 判断是否为第二个 outcomeindex = 1),而不是判断 side 是否为 "NO"
```kotlin
// ❌ 错误:使用字符串比较判断 side
if (side != null && side.uppercase() == "NO") {
// 转换价格
}
// ❌ 错误:硬编码 "YES"/"NO" 判断
when (side.uppercase()) {
"YES" -> // ...
"NO" -> // ...
}
// ✅ 正确:使用 outcomeIndex 判断
if (outcomeIndex != null && outcomeIndex == 1) {
// 第二个 outcome(在二元市场中通常是 NO),转换价格
}
// ✅ 正确:从市场 outcomes 获取 side 信息
val outcomes = JsonUtils.parseStringArray(market.outcomes)
val targetOutcomeIndex = outcomes.indexOfFirst { it.equals(side, ignoreCase = true) }
if (targetOutcomeIndex >= 0) {
// 使用 targetOutcomeIndex 进行判断
}
```
## 多语言使用规范
### 错误消息和响应文本
- **禁止**在代码中硬编码中文或英文错误消息
- **必须**使用 `ErrorCode` 枚举定义错误码和消息
- **必须**使用 `ApiResponse.error(ErrorCode, messageSource)` 或 `MessageUtils.getMessage()` 获取国际化消息
- **禁止**直接使用 `ApiResponse.paramError("硬编码消息")` 或 `ApiResponse.serverError("硬编码消息")`
- 错误消息的默认语言使用中文(在 `ErrorCode` 枚举中定义),通过 `MessageSource` 支持多语言
### 使用 ErrorCode 和 MessageSource
项目已经实现了国际化支持,必须使用以下方式:
```kotlin
// ❌ 错误:硬编码错误消息
return ResponseEntity.ok(ApiResponse.paramError("配置ID不能为空"))
return ResponseEntity.ok(ApiResponse.serverError("获取配置列表失败:${e.message}"))
// ✅ 正确:使用 ErrorCode 枚举
return ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_EMPTY, messageSource = messageSource))
// ✅ 正确:使用 ErrorCode 和自定义消息(如果需要动态消息)
return ResponseEntity.ok(ApiResponse.error(
ErrorCode.PARAM_ERROR,
customMsg = "配置ID不能为空",
messageSource = messageSource
))
// ✅ 正确:使用 MessageUtils
@Autowired
private lateinit var messageUtils: MessageUtils
return ResponseEntity.ok(ApiResponse.error(
ErrorCode.PARAM_EMPTY,
messageSource = messageSource
))
```
### 添加新的错误码
如果需要添加新的错误码,必须在 `ErrorCode` 枚举中定义:
```kotlin
enum class ErrorCode(
val code: Int,
val message: String, // 默认消息(中文)
val messageKey: String // 国际化消息键
) {
// 新错误码示例
NOTIFICATION_CONFIG_NOT_FOUND(3009, "通知配置不存在", "error.notification_config_not_found"),
NOTIFICATION_CONFIG_INVALID(4009, "通知配置无效", "error.notification_config_invalid"),
}
```
然后在语言资源文件中添加对应的翻译:
- `src/main/resources/messages_zh_CN.properties`
- `src/main/resources/messages_zh_TW.properties`
- `src/main/resources/messages_en.properties`
### 日志消息规范
- **日志消息可以使用中文或英文**,便于开发调试
- **禁止**在日志中硬编码用户可见的错误消息(应该使用 ErrorCode)
- 日志消息应该清晰、简洁,便于排查问题
- 日志中的业务数据(如账户名、订单ID等)可以使用原始值
### 代码注释规范
- **代码注释可以使用中文或英文**
- **业务逻辑注释**建议使用中文,便于团队理解
- **API 文档注释**(KDoc)建议使用中文,但也可以使用英文
- **类和方法注释**应该清晰说明功能和参数
### 数据库字段和配置
- **数据库字段名**使用英文(snake_case
- **配置项名称**使用英文(kebab-case 或 dot.notation
- **枚举值**使用英文(UPPER_SNAKE_CASE
- **常量定义**使用英文(UPPER_SNAKE_CASE
### 多语言支持策略
1. **API 响应消息**:使用 `ErrorCode` 枚举,通过 `MessageSource` 支持多语言
2. **错误码**:使用 `ErrorCode` 枚举,包含 `code`、`message`(默认中文)和 `messageKey`(国际化键)
3. **日志消息**:可以使用中文或英文,便于开发调试
4. **代码注释**:可以使用中文或英文,建议使用中文
5. **业务数据**:根据实际需求,可以包含多语言内容(如市场标题、描述等)
### Controller 中使用多语言示例
```kotlin
@RestController
class NotificationController(
private val notificationConfigService: NotificationConfigService,
private val messageSource: MessageSource // 注入 MessageSource
) {
@PostMapping("/configs/list")
fun list(@RequestBody request: NotificationConfigListRequest): ResponseEntity<ApiResponse<List<NotificationConfigDto>>> {
return try {
// ... 业务逻辑
ResponseEntity.ok(ApiResponse.success(configs))
} catch (e: Exception) {
logger.error("获取配置列表失败: ${e.message}", e)
// ✅ 正确:使用 ErrorCode 和 MessageSource
ResponseEntity.ok(ApiResponse.error(
ErrorCode.SERVER_ERROR,
messageSource = messageSource
))
}
}
@PostMapping("/configs/detail")
fun detail(@RequestBody request: NotificationConfigDetailRequest): ResponseEntity<ApiResponse<NotificationConfigDto>> {
if (request.id == null) {
// ✅ 正确:使用 ErrorCode
return ResponseEntity.ok(ApiResponse.error(
ErrorCode.PARAM_EMPTY,
messageSource = messageSource
))
}
// ... 业务逻辑
}
}
```