Files
junhong_cmp_fiber/openspec/changes/archive/2026-01-30-code-cleanup-docs-update/specs/error-handling-updates.md
huang 409a68d60b
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 5m45s
feat: OpenAPI 契约对齐与框架优化
主要变更:
1. OpenAPI 文档契约对齐
   - 统一错误响应字段名为 msg(非 message)
   - 规范 envelope 响应结构(code, msg, data, timestamp)
   - 个人客户路由纳入文档体系(使用 Register 机制)
   - 新增 BuildDocHandlers() 统一管理 handler 构造
   - 确保文档生成的幂等性

2. Service 层错误处理统一
   - 全面替换 fmt.Errorf 为 errors.New/Wrap
   - 统一错误码使用规范
   - Handler 层参数校验不泄露底层细节
   - 新增错误码验证集成测试

3. 代码质量提升
   - 删除未使用的 Task handler 和路由
   - 新增代码规范检查脚本(check-service-errors.sh)
   - 新增注释路径一致性检查(check-comment-paths.sh)
   - 更新 API 文档生成指南

4. OpenSpec 归档
   - 归档 openapi-contract-alignment 变更(63 tasks)
   - 归档 service-error-unify-core 变更
   - 归档 service-error-unify-support 变更
   - 归档 code-cleanup-docs-update 变更
   - 归档 handler-validation-security 变更
   - 同步 delta specs 到主规范文件

影响范围:
- pkg/openapi: 新增 handlers.go,优化 generator.go
- internal/service/*: 48 个 service 文件错误处理统一
- internal/handler/admin: 优化参数校验错误提示
- internal/routes: 个人客户路由改造,删除 task 路由
- scripts: 新增 3 个代码检查脚本
- docs: 更新 OpenAPI 文档(15750+ 行)
- openspec/specs: 同步 3 个主规范文件

破坏性变更:无
向后兼容:是
2026-01-30 11:40:36 +08:00

8.5 KiB
Raw Blame History

错误处理规范更新

概述

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

更新的规范文件

1. openspec/specs/error-handling/spec.md

新增内容

Purpose 章节

补充规范的目的说明:

  • 错误码一致性和可追踪性
  • 客户端能准确识别错误类型
  • 日志记录完整便于排查
  • 避免泄露内部实现细节

错误报错规范章节

新增"错误报错规范(必须遵守)"章节,详细说明:

Handler 层规范

  • 禁止直接返回/拼接底层错误信息给客户端
  • 参数校验失败统一返回 errors.New(CodeInvalidParam)
  • 详细校验错误写日志,对外返回通用消息

Service 层规范

  • 禁止对外返回 fmt.Errorf(...)
  • 业务错误使用 errors.New(code[, msg])
  • 系统错误使用 errors.Wrap(code, err[, msg])

代码示例

// ❌ 错误示例 - 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资源不存在

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状态不允许

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数据库错误

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 层参数校验案例:

参数解析错误

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
}

参数验证错误

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 层测试

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 层测试

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)
    })
}

检查清单

在实施这些更新后,需要验证:

  • openspec/specs/error-handling/spec.md 包含 Purpose 章节
  • openspec/specs/error-handling/spec.md 包含"错误报错规范"章节
  • AGENTS.md 包含错误处理摘要
  • AGENTS.md 包含 Code Review 检查清单
  • docs/003-error-handling/使用指南.md 包含 Service 层实际案例
  • docs/003-error-handling/使用指南.md 包含 Handler 层实际案例
  • docs/003-error-handling/使用指南.md 包含单元测试示例

影响范围

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

后续维护

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