# 错误处理规范更新 ## 概述 本变更更新了错误处理规范文档,补充了缺失的内容和实际案例。 ## 更新的规范文件 ### 1. openspec/specs/error-handling/spec.md **新增内容**: #### Purpose 章节 补充规范的目的说明: - 错误码一致性和可追踪性 - 客户端能准确识别错误类型 - 日志记录完整便于排查 - 避免泄露内部实现细节 #### 错误报错规范章节 新增"错误报错规范(必须遵守)"章节,详细说明: **Handler 层规范**: - ❌ 禁止直接返回/拼接底层错误信息给客户端 - ✅ 参数校验失败统一返回 `errors.New(CodeInvalidParam)` - ✅ 详细校验错误写日志,对外返回通用消息 **Service 层规范**: - ❌ 禁止对外返回 `fmt.Errorf(...)` - ✅ 业务错误使用 `errors.New(code[, msg])` - ✅ 系统错误使用 `errors.Wrap(code, err[, msg])` **代码示例**: ```go // ❌ 错误示例 - Handler 层 if err := c.BodyParser(&req); err != nil { return response.Error(c, 400, errors.CodeInvalidParam, "参数验证失败: "+err.Error()) } // ✅ 正确示例 - Handler 层 if err := c.BodyParser(&req); err != nil { logger.Error("参数解析失败", zap.Error(err)) return errors.New(errors.CodeInvalidParam) } // ❌ 错误示例 - Service 层 if user == nil { return fmt.Errorf("用户不存在: %w", err) } // ✅ 正确示例 - Service 层 if user == nil { return errors.New(errors.CodeUserNotFound, "用户不存在") } if err := db.Save(&user).Error; err != nil { return errors.Wrap(errors.CodeInternalError, err, "保存用户失败") } ``` ### 2. AGENTS.md **新增内容**: #### 错误处理摘要 在"错误处理"章节补充"错误报错规范(必须遵守)"摘要: - Handler 层禁止直接返回/拼接底层错误信息(例如 `"参数验证失败: "+err.Error()`) - 参数校验失败:对外统一返回 `errors.New(CodeInvalidParam)`(详细错误写日志) - Service 层禁止对外返回 `fmt.Errorf(...)`,必须返回 `errors.New(...)` 或 `errors.Wrap(...)` #### Code Review 检查清单 新增完整的 Code Review 检查清单: **错误处理**: - [ ] Service 层无 `fmt.Errorf` 对外返回 - [ ] Handler 层参数校验不泄露细节 - [ ] 错误码使用正确(4xx vs 5xx) - [ ] 错误日志完整(包含上下文) **代码质量**: - [ ] 遵循 Handler → Service → Store → Model 分层 - [ ] 函数长度 ≤ 100 行(核心逻辑 ≤ 50 行) - [ ] 常量定义在 `pkg/constants/` - [ ] 使用 Go 惯用法(非 Java 风格) **测试覆盖**: - [ ] 核心业务逻辑测试覆盖率 ≥ 90% - [ ] 所有 API 端点有集成测试 - [ ] 测试验证真实功能(不绕过核心逻辑) **文档和注释**: - [ ] 所有注释使用中文 - [ ] 导出函数/类型有文档注释 - [ ] API 路径注释与真实路由一致 ### 3. docs/003-error-handling/使用指南.md **新增内容**: #### Service 层错误处理 补充 Service 层错误处理实际案例: **示例 1:资源不存在** ```go func (s *ShopService) GetShop(ctx context.Context, shopID uint) (*model.Shop, error) { shop, err := s.store.Shop.GetByID(ctx, shopID) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, errors.New(errors.CodeShopNotFound, "店铺不存在") } return nil, errors.Wrap(errors.CodeInternalError, err, "查询店铺失败") } return shop, nil } ``` **示例 2:状态不允许** ```go func (s *SIMService) Activate(ctx context.Context, iccid string) error { sim, err := s.store.SIM.GetByICCID(ctx, iccid) if err != nil { return errors.Wrap(errors.CodeInternalError, err, "查询SIM卡失败") } if sim.Status != constants.SIMStatusInactive { return errors.New(errors.CodeInvalidOperation, "只有未激活的SIM卡才能激活") } // 执行激活逻辑... return nil } ``` **示例 3:数据库错误** ```go func (s *AccountService) CreateAccount(ctx context.Context, req *dto.CreateAccountRequest) error { account := &model.Account{ Username: req.Username, Phone: req.Phone, // ... } if err := s.store.Account.Create(ctx, account); err != nil { return errors.Wrap(errors.CodeInternalError, err, "创建账号失败") } return nil } ``` #### Handler 层参数校验 补充 Handler 层参数校验案例: **参数解析错误** ```go func (h *AccountHandler) CreateAccount(c *fiber.Ctx) error { var req dto.CreateAccountRequest if err := c.BodyParser(&req); err != nil { h.logger.Error("参数解析失败", zap.Error(err)) return errors.New(errors.CodeInvalidParam) } if err := h.validator.Struct(&req); err != nil { h.logger.Error("参数验证失败", zap.Error(err)) return errors.New(errors.CodeInvalidParam) } // 调用 Service... return nil } ``` **参数验证错误** ```go func (h *ShopHandler) UpdateShop(c *fiber.Ctx) error { shopID, err := strconv.ParseUint(c.Params("id"), 10, 32) if err != nil { h.logger.Error("店铺ID格式错误", zap.Error(err)) return errors.New(errors.CodeInvalidParam) } var req dto.UpdateShopRequest if err := c.BodyParser(&req); err != nil { h.logger.Error("参数解析失败", zap.Error(err)) return errors.New(errors.CodeInvalidParam) } // 调用 Service... return nil } ``` #### 错误场景单元测试 补充测试代码示例: **Service 层测试** ```go func TestShopService_GetShop_NotFound(t *testing.T) { tx := testutils.NewTestTransaction(t) rdb := testutils.GetTestRedis(t) testutils.CleanTestRedisKeys(t, rdb) store := postgres.NewShopStore(tx, rdb) service := service.NewShopService(store, logger) // 测试不存在的店铺 _, err := service.GetShop(context.Background(), 99999) assert.Error(t, err) assert.True(t, errors.Is(err, errors.CodeShopNotFound)) } func TestSIMService_Activate_InvalidStatus(t *testing.T) { tx := testutils.NewTestTransaction(t) rdb := testutils.GetTestRedis(t) testutils.CleanTestRedisKeys(t, rdb) store := postgres.NewSIMStore(tx, rdb) service := service.NewSIMService(store, logger) // 创建已激活的 SIM 卡 sim := &model.SIM{ ICCID: "898600123456789", Status: constants.SIMStatusActive, } store.Create(context.Background(), sim) // 尝试再次激活 err := service.Activate(context.Background(), sim.ICCID) assert.Error(t, err) assert.True(t, errors.Is(err, errors.CodeInvalidOperation)) } ``` **Handler 层测试** ```go func TestAccountHandler_CreateAccount_InvalidParam(t *testing.T) { env := testutils.NewIntegrationTestEnv(t) t.Run("缺少必填字段", func(t *testing.T) { reqBody := map[string]interface{}{ "username": "test", // 缺少 phone 字段 } resp, err := env.AsSuperAdmin().Request("POST", "/api/admin/accounts", reqBody) require.NoError(t, err) assert.Equal(t, 400, resp.StatusCode) var result map[string]interface{} json.Unmarshal(resp.Body, &result) assert.Equal(t, float64(errors.CodeInvalidParam), result["code"]) }) t.Run("手机号格式错误", func(t *testing.T) { reqBody := map[string]interface{}{ "username": "test", "phone": "invalid", } resp, err := env.AsSuperAdmin().Request("POST", "/api/admin/accounts", reqBody) require.NoError(t, err) assert.Equal(t, 400, resp.StatusCode) }) } ``` ## 检查清单 在实施这些更新后,需要验证: - [x] `openspec/specs/error-handling/spec.md` 包含 Purpose 章节 - [x] `openspec/specs/error-handling/spec.md` 包含"错误报错规范"章节 - [x] `AGENTS.md` 包含错误处理摘要 - [x] `AGENTS.md` 包含 Code Review 检查清单 - [x] `docs/003-error-handling/使用指南.md` 包含 Service 层实际案例 - [x] `docs/003-error-handling/使用指南.md` 包含 Handler 层实际案例 - [x] `docs/003-error-handling/使用指南.md` 包含单元测试示例 ## 影响范围 这些文档更新不影响现有代码逻辑,仅完善规范说明和最佳实践指引。 ## 后续维护 - 新增错误码时,同步更新使用指南中的案例 - 发现新的错误处理模式时,补充到文档中 - 定期检查文档案例与代码实际实现的一致性