feat: add engineering conventions and CI gates documentation
- Introduced a new document outlining SDK version locking, static analysis, formatting, generated artifacts management, branching and commit conventions, and CI gate checks. - Updated README to include the new conventions document. - Modified API design to use numeric error codes instead of strings, with a dedicated ErrorCode object for better maintainability. - Adjusted global exception handling to return numeric error codes. - Updated tests to reflect changes in error code handling.
This commit is contained in:
@@ -23,20 +23,36 @@ domains/xxx/api/
|
||||
```kotlin
|
||||
// platform-web/.../ApiResult.kt
|
||||
data class ApiResult<T>(
|
||||
val code: String,
|
||||
val code: Int, // 0 = 成功;非 0 见下面的错误码分段
|
||||
val message: String,
|
||||
val data: T?,
|
||||
val traceId: String,
|
||||
) {
|
||||
companion object {
|
||||
fun <T> ok(data: T): ApiResult<T> =
|
||||
ApiResult("OK", "success", data, TraceIdHolder.current())
|
||||
ApiResult(ErrorCode.OK, "success", data, TraceIdHolder.current())
|
||||
|
||||
fun error(code: String, message: String): ApiResult<Nothing> =
|
||||
fun error(code: Int, message: String): ApiResult<Nothing> =
|
||||
ApiResult(code, message, null, TraceIdHolder.current())
|
||||
}
|
||||
}
|
||||
|
||||
// platform-web/.../ErrorCode.kt
|
||||
object ErrorCode {
|
||||
const val OK = 0
|
||||
|
||||
// 10xxx 平台通用
|
||||
const val INVALID_PARAM = 10001
|
||||
const val UNAUTHORIZED = 10401
|
||||
const val FORBIDDEN = 10403
|
||||
const val INTERNAL_ERROR = 10500
|
||||
|
||||
// 11xxx 认证与门店
|
||||
const val STORE_NOT_ACCESSIBLE = 11001
|
||||
const val NO_STORE_PERMISSION = 11002
|
||||
// 20xxx 采购 / 21xxx 库存 / 3xxxx F6·Mini 透传类,各 domain 在自己的段内分配
|
||||
}
|
||||
|
||||
// platform-web/.../GlobalExceptionHandler.kt
|
||||
@RestControllerAdvice
|
||||
class GlobalExceptionHandler {
|
||||
@@ -44,7 +60,7 @@ class GlobalExceptionHandler {
|
||||
@ExceptionHandler(MethodArgumentNotValidException::class)
|
||||
fun handleValidation(ex: MethodArgumentNotValidException): ResponseEntity<ApiResult<Nothing>> {
|
||||
val message = ex.bindingResult.fieldErrors.joinToString("; ") { "${it.field}: ${it.defaultMessage}" }
|
||||
return ResponseEntity.badRequest().body(ApiResult.error("INVALID_PARAM", message))
|
||||
return ResponseEntity.badRequest().body(ApiResult.error(ErrorCode.INVALID_PARAM, message))
|
||||
}
|
||||
|
||||
@ExceptionHandler(BusinessException::class)
|
||||
@@ -54,7 +70,7 @@ class GlobalExceptionHandler {
|
||||
@ExceptionHandler(Exception::class)
|
||||
fun handleUnexpected(ex: Exception): ResponseEntity<ApiResult<Nothing>> {
|
||||
// 未预期异常统一兜底,避免堆栈信息泄漏给前端,详细堆栈走日志(见 08-observability.md)
|
||||
return ResponseEntity.internalServerError().body(ApiResult.error("INTERNAL_ERROR", "系统繁忙,请稍后重试"))
|
||||
return ResponseEntity.internalServerError().body(ApiResult.error(ErrorCode.INTERNAL_ERROR, "系统繁忙,请稍后重试"))
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -133,12 +149,15 @@ interface StoreMapper {
|
||||
- 路径版本化:`/api/v1/...`,未来 breaking change 走 `/api/v2/...`,不在原路径上做不兼容修改。
|
||||
- 用 [springdoc-openapi](https://springdoc.org/) 自动生成接口文档,Controller 上写清楚的 `@Operation` 描述;`build.gradle` 加 `implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.6.0'` 即可在 `/swagger-ui.html` 看到文档。
|
||||
- `traceId` 贯穿请求全链路(对应架构图 `Observability` 的要求),从入口 filter 生成,写入 `ApiResult` 和日志,详见 [08-observability.md](./08-observability.md)。
|
||||
- **错误码是数字,`0` 表示成功**,按 domain 分段(`10xxx` 平台通用 / `11xxx` 认证与门店 / `20xxx` 采购 / `21xxx` 库存 / `3xxxx` F6·Mini 透传类)。分段的价值是看到前两位就知道该找哪个域;全局连续编号在多域并行开发时必然撞号。
|
||||
- **不允许在业务代码里写裸数字**,一律走 `ErrorCode` 常量。数字码在监控里聚合方便(可以直接 `group by code`),代价是不自解释——所以 `message` 必须始终是给人看的,日志里 `code` 和 `message` 一起打。
|
||||
- 客户端侧的对应契约见 [../12-error-and-api-contract.md](../12-error-and-api-contract.md),两边的分段方案必须保持一致。
|
||||
|
||||
## 附录:为什么要统一响应包装,而不是直接返回业务对象
|
||||
|
||||
不统一包装的话,前端(APP)拿到的成功响应是 `{ id, name }`,失败响应是 Spring 默认的 `{ timestamp, status, error, path }`——两种结构完全不一样,前端每个接口都要单独判断"这次失败长什么样"。统一成 `{ code, message, data, traceId }` 之后:
|
||||
|
||||
- 前端只需要判断 `code == "OK"` 就知道成功与否,不用对着 HTTP status code 猜。
|
||||
- 前端只需要判断 `code == 0` 就知道成功与否,不用对着 HTTP status code 猜。
|
||||
- `traceId` 无论成功失败都会带上,用户反馈问题时报个 `traceId`,就能在日志里定位到具体这一次请求(见 [08-observability.md](./08-observability.md)),不需要靠时间戳模糊查找。
|
||||
- 新增一种失败场景时,只需要新增一个 `code`,不需要前端为每种 HTTP status code 单独写处理分支。
|
||||
|
||||
@@ -147,7 +166,7 @@ interface StoreMapper {
|
||||
## 待补充
|
||||
|
||||
- 分页/排序参数的统一约定。
|
||||
- 错误码表(按 domain 分段还是全局统一编码)。
|
||||
- **完整错误码表**:分段方案已定(见上),但各 domain 段内的具体码值还没分配,需要各 domain 负责人一起填,并与客户端的 `ApiCode`([../12-error-and-api-contract.md](../12-error-and-api-contract.md))保持同步。
|
||||
|
||||
## 参考链接
|
||||
|
||||
|
||||
@@ -101,7 +101,7 @@ class StoreControllerTest {
|
||||
content = """{}"""
|
||||
}.andExpect {
|
||||
status { isBadRequest() }
|
||||
jsonPath("$.code") { value("INVALID_PARAM") } // 对应 06-api-design.md 的 ApiResult 结构
|
||||
jsonPath("$.code") { value(ErrorCode.INVALID_PARAM) } // 对应 06-api-design.md 的 ApiResult 结构
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user