feat: 实现统一错误处理系统 (003-error-handling)
- 新增统一错误码定义和管理 (pkg/errors/codes.go) - 新增全局错误处理器和中间件 (pkg/errors/handler.go, internal/middleware/error_handler.go) - 新增错误上下文管理 (pkg/errors/context.go) - 增强 Panic 恢复中间件 (internal/middleware/recover.go) - 新增完整的单元测试和集成测试 - 新增功能文档 (docs/003-error-handling/) - 新增功能规范 (specs/003-error-handling/) - 更新 CLAUDE.md 和 README.md
This commit is contained in:
@@ -1297,6 +1297,203 @@ db.Save(&user)
|
||||
|
||||
---
|
||||
|
||||
### X. Error Handling Standards (错误处理规范)
|
||||
|
||||
**规则 (RULES):**
|
||||
|
||||
- **所有** API 错误响应 **MUST** 使用统一的 JSON 格式(通过 `pkg/errors/` 全局 ErrorHandler)
|
||||
- **所有** Handler 层错误 **MUST** 通过返回 `error` 传递给全局 ErrorHandler,**MUST NOT** 手动构造错误响应
|
||||
- **所有** 业务错误 **MUST** 使用 `pkg/errors.New()` 或 `pkg/errors.Wrap()` 创建 `AppError`,并指定错误码
|
||||
- **所有** 错误码 **MUST** 在 `pkg/errors/codes.go` 中统一定义和管理
|
||||
- **所有** Panic **MUST** 被 Recover 中间件自动捕获,转换为 500 错误响应
|
||||
- **所有** 错误日志 **MUST** 包含完整的请求上下文(Request ID、路径、方法、参数等)
|
||||
- 5xx 服务端错误 **MUST** 自动脱敏,只返回通用错误消息,原始错误仅记录到日志
|
||||
- 4xx 客户端错误 **MAY** 返回具体业务错误消息(如"用户名已存在")
|
||||
- **MUST NOT** 在业务代码中主动 `panic`(除非遇到不可恢复的编程错误)
|
||||
- **MUST NOT** 在 Handler 中直接使用 `c.Status().JSON()` 返回错误响应
|
||||
|
||||
**统一错误响应格式:**
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 1001,
|
||||
"data": null,
|
||||
"msg": "参数验证失败",
|
||||
"timestamp": "2025-11-15T10:00:00+08:00"
|
||||
}
|
||||
```
|
||||
|
||||
HTTP Header 包含:
|
||||
```
|
||||
X-Request-ID: 550e8400-e29b-41d4-a716-446655440000
|
||||
```
|
||||
|
||||
**错误码分类系统:**
|
||||
|
||||
- `0`: 成功
|
||||
- `1000-1999`: 客户端错误(4xx HTTP 状态码,日志级别 Warn)
|
||||
- `1001`: 参数验证失败 → 400
|
||||
- `1002`: 缺少认证令牌 → 401
|
||||
- `1003`: 无效认证令牌 → 401
|
||||
- `1004`: 认证凭证无效 → 401
|
||||
- `1005`: 禁止访问 → 403
|
||||
- `1006`: 资源未找到 → 404
|
||||
- `1007`: 资源冲突 → 409
|
||||
- `1008`: 请求过多 → 429
|
||||
- `1009`: 请求体过大 → 413
|
||||
- `2000-2999`: 服务端错误(5xx HTTP 状态码,日志级别 Error)
|
||||
- `2001`: 内部服务器错误 → 500
|
||||
- `2002`: 数据库错误 → 500
|
||||
- `2003`: 缓存服务错误 → 500
|
||||
- `2004`: 服务不可用 → 503
|
||||
- `2005`: 请求超时 → 504
|
||||
- `2006`: 任务队列错误 → 500
|
||||
|
||||
**正确的错误处理模式:**
|
||||
|
||||
```go
|
||||
// ✅ Handler 层 - 直接返回错误
|
||||
func (h *UserHandler) CreateUser(c *fiber.Ctx) error {
|
||||
var req CreateUserRequest
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return errors.New(errors.CodeInvalidParam, "请求参数格式错误")
|
||||
}
|
||||
|
||||
// 业务验证
|
||||
if len(req.Username) < 3 {
|
||||
return errors.New(errors.CodeInvalidParam, "用户名长度必须在 3-20 个字符之间")
|
||||
}
|
||||
|
||||
// 调用 Service 层
|
||||
user, err := h.service.Create(c.Context(), &req)
|
||||
if err != nil {
|
||||
// 包装底层错误,保留错误链
|
||||
return errors.Wrap(errors.CodeDatabaseError, "创建用户失败", err)
|
||||
}
|
||||
|
||||
return response.Success(c, user)
|
||||
}
|
||||
|
||||
// ✅ Service 层 - 返回标准 error
|
||||
func (s *Service) Create(ctx context.Context, req *CreateUserRequest) (*User, error) {
|
||||
// 检查用户名是否已存在
|
||||
exists, err := s.store.ExistsByUsername(ctx, req.Username)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("检查用户名失败: %w", err)
|
||||
}
|
||||
|
||||
if exists {
|
||||
return nil, errors.New(errors.CodeConflict, "用户名已被使用")
|
||||
}
|
||||
|
||||
// 创建用户
|
||||
user := &User{Username: req.Username, Email: req.Email}
|
||||
if err := s.store.Create(ctx, user); err != nil {
|
||||
return nil, fmt.Errorf("创建用户失败: %w", err)
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
```
|
||||
|
||||
**错误的错误处理模式(禁止):**
|
||||
|
||||
```go
|
||||
// ❌ 在 Handler 中手动构造错误响应
|
||||
func (h *UserHandler) CreateUser(c *fiber.Ctx) error {
|
||||
var req CreateUserRequest
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
// ❌ 不要手动构造 JSON 响应
|
||||
return c.Status(400).JSON(fiber.Map{
|
||||
"error": "invalid request",
|
||||
})
|
||||
}
|
||||
// ...
|
||||
}
|
||||
|
||||
// ❌ 不使用错误码系统
|
||||
func (h *UserHandler) CreateUser(c *fiber.Ctx) error {
|
||||
// ❌ 直接返回 Fiber 错误,没有业务错误码
|
||||
return fiber.NewError(400, "invalid request")
|
||||
}
|
||||
|
||||
// ❌ 在业务代码中主动 panic
|
||||
func (s *Service) Create(ctx context.Context, req *CreateUserRequest) (*User, error) {
|
||||
if req.Username == "" {
|
||||
panic("username is empty") // ❌ 应该返回错误
|
||||
}
|
||||
// ...
|
||||
}
|
||||
|
||||
// ❌ 丢失错误链
|
||||
func (s *Service) Create(ctx context.Context, req *CreateUserRequest) (*User, error) {
|
||||
user, err := s.store.Create(ctx, user)
|
||||
if err != nil {
|
||||
// ❌ 应该使用 errors.Wrap 保留错误链
|
||||
return nil, errors.New(errors.CodeDatabaseError, "创建用户失败")
|
||||
}
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
**Panic 恢复机制:**
|
||||
|
||||
- Recover 中间件 **MUST** 在中间件链的最前面注册(第一层防护)
|
||||
- Panic 被捕获后 **MUST** 记录完整的堆栈跟踪到日志
|
||||
- Panic **MUST** 转换为 `AppError(Code2001)` 并传递给 ErrorHandler
|
||||
- 单个请求的 panic **MUST NOT** 影响其他请求的处理
|
||||
|
||||
```go
|
||||
// ✅ Recover 中间件自动处理
|
||||
app.Use(middleware.Recover(logger)) // 第一个注册
|
||||
app.Use(requestid.New())
|
||||
app.Use(logger.Middleware())
|
||||
// ... 其他中间件
|
||||
|
||||
// 当业务代码发生 panic 时:
|
||||
// 1. Recover 捕获 panic
|
||||
// 2. 记录堆栈跟踪到日志
|
||||
// 3. 返回 500 错误响应
|
||||
// 4. 服务继续运行
|
||||
```
|
||||
|
||||
**错误上下文和追踪:**
|
||||
|
||||
- 每个错误日志 **MUST** 包含 `request_id`(由 RequestID 中间件生成)
|
||||
- 每个错误日志 **MUST** 包含请求路径、方法、IP、User-Agent
|
||||
- 每个错误日志 **MUST** 包含错误码和错误消息
|
||||
- 5xx 错误日志 **MUST** 包含完整的堆栈跟踪(如果是 panic)
|
||||
- 错误响应 Header **MUST** 包含 `X-Request-ID`,便于客户端追踪
|
||||
|
||||
**敏感信息保护:**
|
||||
|
||||
- 数据库错误 **MUST NOT** 暴露 SQL 语句或数据库结构
|
||||
- 文件系统错误 **MUST NOT** 暴露服务器路径
|
||||
- 外部 API 错误 **MUST NOT** 暴露 API 密钥或认证信息
|
||||
- 所有 5xx 错误 **MUST** 返回通用消息,原始错误仅记录到日志
|
||||
|
||||
**错误码使用规范:**
|
||||
|
||||
- 新增错误码 **MUST** 在 `pkg/errors/codes.go` 中定义常量
|
||||
- 新增错误码 **MUST** 在 `errorMessages` 中添加中文消息
|
||||
- 错误码 **MUST** 遵循分段规则(1xxx=客户端,2xxx=服务端)
|
||||
- 相同类型的错误 **SHOULD** 复用已有错误码,避免错误码膨胀
|
||||
|
||||
**理由 (RATIONALE):**
|
||||
|
||||
统一的错误处理系统确保:
|
||||
|
||||
1. **一致性**:所有 API 错误响应格式统一,客户端集成成本低
|
||||
2. **稳定性**:100% 捕获 panic,防止服务崩溃,保障系统稳定性
|
||||
3. **安全性**:5xx 错误自动脱敏,防止敏感信息泄露
|
||||
4. **可追踪性**:Request ID 贯穿整个请求生命周期,快速定位问题
|
||||
5. **可维护性**:错误码集中管理,错误分类清晰,日志级别明确
|
||||
6. **开发效率**:Handler 层只需返回错误,全局 ErrorHandler 自动处理格式化和日志记录
|
||||
|
||||
通过全局 ErrorHandler + Recover 中间件的双层防护,确保系统在任何错误情况下都能优雅降级,不会崩溃,同时保留完整的调试信息。
|
||||
|
||||
---
|
||||
|
||||
## Development Workflow (开发工作流程)
|
||||
|
||||
### 分支管理
|
||||
|
||||
@@ -117,6 +117,22 @@
|
||||
- [ ] Body truncation indicates "... (truncated)" when over 50KB limit
|
||||
- [ ] Access log includes all required fields: method, path, query, status, duration_ms, request_id, ip, user_agent, user_id, request_body, response_body
|
||||
|
||||
**Error Handling Standards** (Constitution Principle X):
|
||||
- [ ] All API error responses use unified JSON format (via pkg/errors/ global ErrorHandler)
|
||||
- [ ] Handler layer errors return error (not manual JSON responses)
|
||||
- [ ] Business errors use pkg/errors.New() or pkg/errors.Wrap() with error codes
|
||||
- [ ] All error codes defined in pkg/errors/codes.go
|
||||
- [ ] All panics caught by Recover middleware and converted to 500 responses
|
||||
- [ ] Error logs include complete request context (Request ID, path, method, params)
|
||||
- [ ] 5xx server errors auto-sanitized (generic message to client, full error in logs)
|
||||
- [ ] 4xx client errors may return specific business messages
|
||||
- [ ] No panic in business code (except unrecoverable programming errors)
|
||||
- [ ] No manual error response construction in Handler (c.Status().JSON())
|
||||
- [ ] Error codes follow classification: 0=success, 1xxx=client (4xx), 2xxx=server (5xx)
|
||||
- [ ] Recover middleware registered first in middleware chain
|
||||
- [ ] Panic recovery logs complete stack trace
|
||||
- [ ] Single request panic does not affect other requests
|
||||
|
||||
## Project Structure
|
||||
|
||||
### Documentation (this feature)
|
||||
|
||||
@@ -146,6 +146,21 @@
|
||||
- [ ] Non-realtime operations delegated to async tasks
|
||||
- [ ] Uses `context.Context` for timeouts and cancellation
|
||||
|
||||
**Error Handling Requirements** (Constitution Principle X):
|
||||
- [ ] All API errors use unified JSON format (via `pkg/errors/` global ErrorHandler)
|
||||
- [ ] Handler layer returns errors (no manual `c.Status().JSON()` for errors)
|
||||
- [ ] Business errors use `pkg/errors.New()` or `pkg/errors.Wrap()` with error codes
|
||||
- [ ] All error codes defined in `pkg/errors/codes.go`
|
||||
- [ ] All panics caught by Recover middleware, converted to 500 responses
|
||||
- [ ] Error logs include complete request context (Request ID, path, method, params)
|
||||
- [ ] 5xx server errors auto-sanitized (generic message to client, full error in logs)
|
||||
- [ ] 4xx client errors may return specific business messages
|
||||
- [ ] No panic in business code (except unrecoverable programming errors)
|
||||
- [ ] Error codes follow classification: 0=success, 1xxx=client (4xx), 2xxx=server (5xx)
|
||||
- [ ] Recover middleware registered first in middleware chain
|
||||
- [ ] Panic recovery logs complete stack trace
|
||||
- [ ] Single request panic does not affect other requests
|
||||
|
||||
**Testing Requirements**:
|
||||
- [ ] Unit tests for all Service layer business logic
|
||||
- [ ] Integration tests for all API endpoints
|
||||
|
||||
@@ -207,6 +207,18 @@ Foundational tasks for 君鸿卡管系统 tech stack:
|
||||
- [ ] TXXX Quality Gate: Verify logging via centralized Logger middleware (pkg/logger/Middleware())
|
||||
- [ ] TXXX Quality Gate: Verify no middleware bypasses logging (test auth failures, rate limits, etc.)
|
||||
- [ ] TXXX Quality Gate: Verify access log has all required fields (method, path, query, status, duration_ms, request_id, ip, user_agent, user_id, request_body, response_body)
|
||||
- [ ] TXXX Quality Gate: Verify all API errors use unified JSON format (pkg/errors/ ErrorHandler)
|
||||
- [ ] TXXX Quality Gate: Verify Handler layer returns errors (no manual c.Status().JSON() for errors)
|
||||
- [ ] TXXX Quality Gate: Verify business errors use pkg/errors.New() or pkg/errors.Wrap()
|
||||
- [ ] TXXX Quality Gate: Verify all error codes defined in pkg/errors/codes.go
|
||||
- [ ] TXXX Quality Gate: Verify Recover middleware catches all panics
|
||||
- [ ] TXXX Quality Gate: Verify error logs include request context (Request ID, path, method)
|
||||
- [ ] TXXX Quality Gate: Verify 5xx errors auto-sanitized (no sensitive info exposed)
|
||||
- [ ] TXXX Quality Gate: Verify no panic in business code (search for panic() calls)
|
||||
- [ ] TXXX Quality Gate: Verify error codes follow classification (0=success, 1xxx=4xx, 2xxx=5xx)
|
||||
- [ ] TXXX Quality Gate: Verify Recover middleware registered first in chain
|
||||
- [ ] TXXX Quality Gate: Test panic recovery logs complete stack trace
|
||||
- [ ] TXXX Quality Gate: Test single request panic doesn't affect other requests
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user