--- 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 { // 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 { 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> { val data = runBlocking { exampleService.getData() } return ResponseEntity.ok(ApiResponse.success(data)) } } // ❌ 错误 @RestController class ExampleController { @PostMapping("/example") suspend fun getExample(): ResponseEntity> { // 禁止使用suspend // ... } } ``` ### Service规范 - Service 层可以使用 `suspend` 方法 - 使用 `@Transactional` 管理事务 - 使用构造函数注入依赖 ```kotlin @Service class ExampleService( private val exampleRepository: ExampleRepository ) { suspend fun getAllData(): List { return exampleRepository.findAll() } @Transactional fun saveData(entity: ExampleEntity): ExampleEntity { return exampleRepository.save(entity) } } ``` ### Repository规范 - Repository 接口继承 `JpaRepository` - 使用 Spring Data JPA 方法命名规范 ```kotlin @Repository interface ExampleRepository : JpaRepository { fun findByCode(code: String): ExampleEntity? fun findByCategoryAndStatus(category: String, status: String): List fun findByCategory(category: String): List // 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 @GET("/orders/active") suspend fun getActiveOrders( @Query("market") market: String?, @Query("limit") limit: Int?, @Query("offset") offset: Int? ): Response> @DELETE("/orders/{orderId}") suspend fun cancelOrder(@Path("orderId") orderId: String): Response @GET("/trades") suspend fun getTrades( @Query("market") market: String?, @Query("user") user: String?, @Query("limit") limit: Int?, @Query("offset") offset: Int? ): Response> } // 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> { 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( val code: Int, val data: T?, val msg: String ) { companion object { fun success(data: T): ApiResponse = ApiResponse(0, data, "") fun paramError(msg: String): ApiResponse = ApiResponse(1001, null, msg) fun serverError(msg: String): ApiResponse = 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 = 第一个 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 进行判断 } ``` ## 多语言使用规范 ### 错误消息和响应文本 - **禁止**在代码中硬编码中文或英文错误消息 - **必须**使用 `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>> { 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> { if (request.id == null) { // ✅ 正确:使用 ErrorCode return ResponseEntity.ok(ApiResponse.error( ErrorCode.PARAM_EMPTY, messageSource = messageSource )) } // ... 业务逻辑 } } ```