# 错误处理规范更新

## 概述

本变更更新了错误处理规范文档，补充了缺失的内容和实际案例。

## 更新的规范文件

### 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` 包含单元测试示例

## 影响范围

这些文档更新不影响现有代码逻辑，仅完善规范说明和最佳实践指引。

## 后续维护

- 新增错误码时，同步更新使用指南中的案例
- 发现新的错误处理模式时，补充到文档中
- 定期检查文档案例与代码实际实现的一致性
