Files
junhong_cmp_fiber/docs/003-error-handling/使用指南.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

25 KiB
Raw Blame History

使用指南Fiber 错误处理集成

功能编号: 003-error-handling
版本: 1.0.0
更新日期: 2025-11-15

目录

  1. 快速开始
  2. 错误码参考
  3. Handler 中使用错误
  4. 客户端错误处理
  5. 错误日志查询
  6. 最佳实践
  7. 常见问题

快速开始

1. 在 Handler 中返回错误

package handler

import (
    "github.com/break/junhong_cmp_fiber/pkg/errors"
    "github.com/break/junhong_cmp_fiber/pkg/response"
    "github.com/gofiber/fiber/v2"
)

func (h *UserHandler) GetUser(c *fiber.Ctx) error {
    userID := c.Params("id")
    
    // 参数验证失败
    if userID == "" {
        return errors.New(errors.CodeInvalidParam, "用户 ID 不能为空")
    }
    
    // 调用服务层
    user, err := h.service.GetByID(c.Context(), userID)
    if err != nil {
        // 包装底层错误
        return errors.Wrap(errors.CodeDatabaseError, "查询用户失败", err)
    }
    
    // 资源未找到
    if user == nil {
        return errors.New(errors.CodeNotFound, "用户不存在")
    }
    
    return response.Success(c, user)
}

2. 错误响应格式

所有错误自动转换为统一格式:

{
  "code": 1001,
  "data": null,
  "msg": "参数验证失败",
  "timestamp": "2025-11-15T10:00:00+08:00"
}

HTTP Header 中包含 Request ID

X-Request-ID: 550e8400-e29b-41d4-a716-446655440000

错误码参考

成功

错误码 名称 HTTP 状态 消息
0 CodeSuccess 200 操作成功

客户端错误 (1000-1999)

错误码 名称 HTTP 状态 消息 使用场景
1001 CodeInvalidParam 400 参数验证失败 请求参数格式错误、必填字段缺失
1002 CodeMissingToken 401 缺少认证令牌 未提供 Token
1003 CodeInvalidToken 401 无效的认证令牌 Token 格式错误
1004 CodeInvalidCredentials 401 认证凭证无效 Token 过期、验证失败
1005 CodeForbidden 403 禁止访问 无权限访问资源
1006 CodeNotFound 404 资源未找到 用户、订单等资源不存在
1007 CodeConflict 409 资源冲突 唯一性约束冲突
1008 CodeTooManyRequests 429 请求过多 触发限流
1009 CodeRequestEntityTooLarge 413 请求体过大 文件上传超限

财务相关错误 (1050-1069)

错误码 名称 HTTP 状态 消息 使用场景
1050 CodeInvalidStatus 400 状态不允许此操作 资源状态不允许执行当前操作
1051 CodeInsufficientBalance 400 余额不足 钱包余额不足以完成操作
1052 CodeWithdrawalNotFound 404 提现申请不存在 提现记录未找到
1053 CodeWalletNotFound 404 钱包不存在 钱包记录未找到
1054 CodeInsufficientQuota 400 额度不足 套餐分配额度不足
1055 CodeExceedLimit 400 超过限制 超过系统限制(如设备绑定卡数)

服务端错误 (2000-2999)

错误码 名称 HTTP 状态 消息 使用场景
2001 CodeInternalError 500 内部服务器错误 未分类的内部错误
2002 CodeDatabaseError 500 数据库错误 数据库连接失败、查询错误
2003 CodeCacheError 500 缓存服务错误 Redis 连接失败
2004 CodeServiceUnavailable 503 服务暂时不可用 外部服务不可用
2005 CodeTimeout 504 请求超时 上游服务超时
2006 CodeQueueError 500 任务队列错误 Asynq 任务投递失败

Handler 中使用错误

1. 参数验证错误

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 || len(req.Username) > 20 {
        return errors.New(errors.CodeInvalidParam, "用户名长度必须在 3-20 个字符之间")
    }
    
    if !isValidEmail(req.Email) {
        return errors.New(errors.CodeInvalidParam, "邮箱格式不正确")
    }
    
    // 继续处理...
}

2. 认证/授权错误

func (h *OrderHandler) GetOrder(c *fiber.Ctx) error {
    // 检查用户是否登录
    userID := c.Locals("user_id")
    if userID == nil {
        return errors.New(errors.CodeMissingToken, "请先登录")
    }
    
    order, err := h.service.GetByID(c.Params("id"))
    if err != nil {
        return errors.Wrap(errors.CodeDatabaseError, "查询订单失败", err)
    }
    
    // 检查权限
    if order.UserID != userID.(string) {
        return errors.New(errors.CodeForbidden, "无权访问此订单")
    }
    
    return response.Success(c, order)
}

3. 资源未找到

func (h *UserHandler) GetUser(c *fiber.Ctx) error {
    user, err := h.service.GetByID(c.Params("id"))
    if err != nil {
        return errors.Wrap(errors.CodeDatabaseError, "查询用户失败", err)
    }
    
    if user == nil {
        return errors.New(errors.CodeNotFound, fmt.Sprintf("用户 ID %s 不存在", c.Params("id")))
    }
    
    return response.Success(c, user)
}

4. 资源冲突

func (h *UserHandler) CreateUser(c *fiber.Ctx) error {
    var req CreateUserRequest
    if err := c.BodyParser(&req); err != nil {
        return errors.New(errors.CodeInvalidParam, "请求参数错误")
    }
    
    // 检查用户名是否已存在
    exists, err := h.service.ExistsByUsername(req.Username)
    if err != nil {
        return errors.Wrap(errors.CodeDatabaseError, "检查用户名失败", err)
    }
    
    if exists {
        return errors.New(errors.CodeConflict, "用户名已被使用")
    }
    
    // 创建用户...
}

5. 外部服务错误

func (h *NotificationHandler) SendEmail(c *fiber.Ctx) error {
    var req SendEmailRequest
    if err := c.BodyParser(&req); err != nil {
        return errors.New(errors.CodeInvalidParam, "请求参数错误")
    }
    
    // 调用外部邮件服务
    err := h.emailService.Send(req.To, req.Subject, req.Body)
    if err != nil {
        // 包装外部服务错误
        return errors.Wrap(errors.CodeServiceUnavailable, "邮件发送失败", err)
    }
    
    return response.Success(c, nil)
}

6. 自定义 HTTP 状态码(高级用法)

func (h *Handler) SpecialCase(c *fiber.Ctx) error {
    // 默认 CodeInvalidParam 映射为 400
    // 但某些场景需要返回 422
    appErr := errors.New(errors.CodeInvalidParam, "数据验证失败")
    appErr = appErr.WithHTTPStatus(422)
    return appErr
}

Handler 层参数校验安全实践

错误示例:泄露内部细节

func (h *ShopHandler) Create(c *fiber.Ctx) error {
    var req dto.CreateShopRequest
    
    // ❌ 错误:直接暴露解析错误
    if err := c.BodyParser(&req); err != nil {
        return errors.New(errors.CodeInvalidParam, "参数解析失败: "+err.Error())
        // 可能泄露json: cannot unmarshal number into Go struct field CreateShopRequest.ShopCode of type string
    }
    
    // ❌ 错误:直接暴露 validator 错误
    if err := h.validator.Struct(&req); err != nil {
        return errors.New(errors.CodeInvalidParam, "参数验证失败: "+err.Error())
        // 可能泄露Key: 'CreateShopRequest.ShopName' Error:Field validation for 'ShopName' failed on the 'required' tag
    }
    
    // ...
}

安全风险

  • 泄露内部字段名ShopCode、ShopName
  • 泄露数据类型string、number
  • 泄露验证规则required、min、max 等)
  • 攻击者可根据错误消息推断 API 内部结构

正确示例:安全的参数校验

func (h *ShopHandler) Create(c *fiber.Ctx) error {
    var req dto.CreateShopRequest
    
    // ✅ 正确:通用错误消息 + 结构化日志WARN 级别)
    if err := c.BodyParser(&req); err != nil {
        logger.GetAppLogger().Warn("参数解析失败", 
            zap.String("path", c.Path()),
            zap.String("method", c.Method()),
            zap.Error(err),
        )
        return errors.New(errors.CodeInvalidParam, "请求参数格式错误")
    }
    
    // ✅ 正确:使用默认消息 + 结构化日志WARN 级别)
    if err := h.validator.Struct(&req); err != nil {
        logger.GetAppLogger().Warn("参数验证失败",
            zap.String("path", c.Path()),
            zap.String("method", c.Method()),
            zap.Error(err),
        )
        return errors.New(errors.CodeInvalidParam)  // 使用默认消息
    }
    
    // 业务逻辑...
    shop, err := h.service.Create(c.UserContext(), &req)
    if err != nil {
        return err
    }
    
    return response.Success(c, shop)
}

安全优势

  • 对外:统一返回通用消息("参数验证失败"
  • 日志:记录详细错误信息用于排查
  • 包含 request_id便于日志关联和问题追踪

单元测试示例

func TestShopHandler_Create_ParamValidation(t *testing.T) {
    // 准备测试环境
    app := fiber.New()
    handler := NewShopHandler(mockService, mockValidator, logger)
    app.Post("/shops", handler.Create)
    
    tests := []struct {
        name           string
        requestBody    string
        expectedCode   int
        expectedMsg    string
    }{
        {
            name:        "参数解析失败",
            requestBody: `{"shop_code": 123}`, // 类型错误
            expectedCode: errors.CodeInvalidParam,
            expectedMsg:  "请求参数格式错误",
        },
        {
            name:        "必填字段缺失",
            requestBody: `{"shop_code": ""}`, // ShopName 缺失
            expectedCode: errors.CodeInvalidParam,
            expectedMsg:  "参数验证失败",
        },
        {
            name:        "正常请求",
            requestBody: `{"shop_code": "SH001", "shop_name": "测试店铺"}`,
            expectedCode: errors.CodeSuccess,
            expectedMsg:  "操作成功",
        },
    }
    
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            req := httptest.NewRequest("POST", "/shops", strings.NewReader(tt.requestBody))
            req.Header.Set("Content-Type", "application/json")
            
            resp, _ := app.Test(req)
            defer resp.Body.Close()
            
            var result map[string]interface{}
            json.NewDecoder(resp.Body).Decode(&result)
            
            assert.Equal(t, tt.expectedCode, int(result["code"].(float64)))
            assert.Equal(t, tt.expectedMsg, result["msg"])
            
            // ✅ 验证:错误消息不泄露内部细节
            assert.NotContains(t, result["msg"], "ShopCode")
            assert.NotContains(t, result["msg"], "ShopName")
            assert.NotContains(t, result["msg"], "required")
        })
    }
}

客户端错误处理

JavaScript/TypeScript

async function fetchUser(userId: string) {
    try {
        const response = await fetch(`/api/v1/users/${userId}`);
        const data = await response.json();
        
        // 检查业务错误码
        if (data.code !== 0) {
            const requestId = response.headers.get('X-Request-ID');
            
            switch (data.code) {
                // 认证错误 - 跳转登录
                case 1002:
                case 1003:
                case 1004:
                    redirectToLogin();
                    break;
                
                // 权限错误 - 显示无权限提示
                case 1005:
                    showError('您没有权限访问此资源');
                    break;
                
                // 资源未找到 - 显示 404 页面
                case 1006:
                    showNotFoundPage();
                    break;
                
                // 服务端错误 - 显示错误并提供 Request ID
                case 2001:
                case 2002:
                case 2003:
                case 2004:
                    showError(`服务器错误请联系管理员。Request ID: ${requestId}`);
                    break;
                
                // 限流错误 - 提示稍后重试
                case 1008:
                    showError('请求过于频繁,请稍后再试');
                    break;
                
                // 其他错误 - 显示错误消息
                default:
                    showError(data.msg);
            }
            
            return null;
        }
        
        return data.data;
    } catch (err) {
        // 网络错误
        showError('网络连接失败,请检查您的网络');
        return null;
    }
}

Axios 拦截器

import axios from 'axios';

const api = axios.create({
    baseURL: '/api/v1',
});

// 响应拦截器
api.interceptors.response.use(
    (response) => {
        const { code, data, msg } = response.data;
        
        if (code !== 0) {
            const requestId = response.headers['x-request-id'];
            
            // 根据错误码处理
            if ([1002, 1003, 1004].includes(code)) {
                // 认证失败,跳转登录
                redirectToLogin();
                return Promise.reject(new Error(msg));
            }
            
            if (code === 1005) {
                // 权限不足
                showError('您没有权限执行此操作');
                return Promise.reject(new Error(msg));
            }
            
            if (code >= 2000) {
                // 服务端错误
                console.error(`Server error: ${msg}, Request ID: ${requestId}`);
                showError(`服务器错误Request ID: ${requestId}`);
                return Promise.reject(new Error(msg));
            }
            
            // 其他业务错误
            showError(msg);
            return Promise.reject(new Error(msg));
        }
        
        return data;
    },
    (error) => {
        // 网络错误
        showError('网络连接失败');
        return Promise.reject(error);
    }
);

错误日志查询

1. 通过 Request ID 查询

# 查询特定请求的所有日志
grep "550e8400-e29b-41d4-a716-446655440000" logs/app.log

# 使用 jq 格式化 JSON 日志
grep "550e8400-e29b-41d4-a716-446655440000" logs/app.log | jq .

2. 查询特定错误码

# 查询所有参数验证失败的错误
grep '"error_code":1001' logs/app.log | jq .

# 查询所有数据库错误
grep '"error_code":2002' logs/app.log | jq .

3. 查询 Panic 堆栈

# 查询所有 panic 日志
grep "panic recovered" logs/app.log

# 查询包含堆栈的完整 panic 日志
grep -A 20 "panic recovered" logs/app.log

4. 按时间范围查询

# 查询最近 1 小时的错误日志
grep '"level":"error"' logs/app.log | grep "$(date -u -d '1 hour ago' '+%Y-%m-%dT%H')"

最佳实践

1. 错误码选择

正确示例

// 参数验证失败
return errors.New(errors.CodeInvalidParam, "用户名不能为空")

// 资源未找到
return errors.New(errors.CodeNotFound, "订单不存在")

// 数据库错误
return errors.Wrap(errors.CodeDatabaseError, "查询失败", err)

错误示例

// 不要使用错误的错误码
return errors.New(errors.CodeDatabaseError, "用户名不能为空") // 应该用 CodeInvalidParam

// 不要返回空消息
return errors.New(errors.CodeNotFound, "") // 应该提供具体消息

2. 参数校验安全加固(重要)

正确示例

// 参数解析失败
if err := c.BodyParser(&req); err != nil {
    logger.GetAppLogger().Warn("参数解析失败",
        zap.String("path", c.Path()),
        zap.String("method", c.Method()),
        zap.Error(err),
    )
    return errors.New(errors.CodeInvalidParam, "请求参数格式错误")
}

// 参数验证失败
if err := h.validator.Struct(&req); err != nil {
    logger.GetAppLogger().Warn("参数验证失败",
        zap.String("path", c.Path()),
        zap.String("method", c.Method()),
        zap.Error(err),
    )
    return errors.New(errors.CodeInvalidParam)  // 使用默认消息
}

错误示例 - 泄露内部细节

// ❌ 危险:泄露 validator 规则和字段名
if err := h.validator.Struct(&req); err != nil {
    return errors.New(errors.CodeInvalidParam, "参数验证失败: "+err.Error())
}
// 可能返回:"Field validation for 'Username' failed on the 'required' tag"

// ❌ 危险:泄露类型信息
if err := c.BodyParser(&req); err != nil {
    return errors.New(errors.CodeInvalidParam, "参数解析失败: "+err.Error())
}
// 可能返回:"Unmarshal type error: expected=uint got=string field=shop_id"

安全原则

  • 对外统一返回通用消息("参数验证失败"
  • 详细错误信息仅记录到日志
  • 使用 WARN 级别(客户端错误)
  • 必须包含请求上下文path、method

3. 错误消息编写

正确示例

// 清晰、具体的错误消息(不泄露内部细节)
errors.New(errors.CodeInvalidParam, "用户名长度必须在 3-20 个字符之间")
errors.New(errors.CodeNotFound, "用户不存在")
errors.New(errors.CodeConflict, "邮箱已被注册")

错误示例

// 不要使用模糊的消息
errors.New(errors.CodeInvalidParam, "错误")
errors.New(errors.CodeNotFound, "not found")

// 不要暴露敏感信息和内部细节
errors.New(errors.CodeDatabaseError, "SQL error: SELECT * FROM users WHERE password = '...'")
errors.New(errors.CodeInvalidParam, "Field 'Username' validation failed") // 泄露字段名

3. 错误包装

正确示例

// 包装底层错误,保留错误链
user, err := h.repo.GetByID(id)
if err != nil {
    return errors.Wrap(errors.CodeDatabaseError, "查询用户失败", err)
}

错误示例

// 丢失原始错误信息
user, err := h.repo.GetByID(id)
if err != nil {
    return errors.New(errors.CodeDatabaseError, "查询用户失败") // 应该用 Wrap
}

4. 不要过度处理错误

正确示例

func (h *Handler) GetUser(c *fiber.Ctx) error {
    user, err := h.service.GetByID(c.Params("id"))
    if err != nil {
        // 直接返回错误,让 ErrorHandler 统一处理
        return err
    }
    return response.Success(c, user)
}

错误示例

func (h *Handler) GetUser(c *fiber.Ctx) error {
    user, err := h.service.GetByID(c.Params("id"))
    if err != nil {
        // 不要在 Handler 中手动构造错误响应
        return c.Status(500).JSON(fiber.Map{"error": err.Error()})
    }
    return response.Success(c, user)
}

5. Panic 使用建议

正确做法

// 让代码正常返回错误,不要主动 panic
func (s *Service) Process() error {
    if invalidState {
        return errors.New(errors.CodeInternalError, "无效状态")
    }
    return nil
}

避免使用

// 避免在业务代码中主动 panic
func (s *Service) Process() {
    if invalidState {
        panic("invalid state") // 不推荐
    }
}

注意:即使代码中有 panicRecover 中间件也会自动捕获并转换为错误响应,确保服务不崩溃。


常见问题

Q1: 如何自定义错误消息?

A: 使用 errors.New() 的第二个参数:

return errors.New(errors.CodeInvalidParam, "自定义错误消息")

Q2: 如何查看底层错误详情?

A: 底层错误会记录在日志中,通过 Request ID 查询:

grep "<request-id>" logs/app.log | jq .

Q3: 客户端如何获取 Request ID

A: 从响应 Header 中获取:

const requestId = response.headers.get('X-Request-ID');

Q4: 错误码冲突怎么办?

A: 参考 pkg/errors/codes.go 中的定义,避免使用已定义的错误码。如需新增错误码,请在对应范围内添加。

Q5: 如何测试错误处理?

A: 参考 tests/integration/error_handler_test.go 中的示例:

resp, _ := app.Test(httptest.NewRequest("GET", "/api/v1/users/invalid", nil))
assert.Equal(t, 400, resp.StatusCode)

Q6: 如何关闭堆栈跟踪?

A: 堆栈跟踪仅在 panic 时记录,无法关闭。如需调整,修改 internal/middleware/recover.go


更多信息


Service 层错误处理实战案例

案例 1套餐服务 - 资源查询

场景:获取套餐详情,需处理不存在和数据库错误

// internal/service/package/service.go
func (s *Service) Get(ctx context.Context, id uint) (*dto.PackageResponse, error) {
    pkg, err := s.packageStore.GetByID(ctx, id)
    if err != nil {
        // ✅ 业务错误:资源不存在
        if err == gorm.ErrRecordNotFound {
            return nil, errors.New(errors.CodeNotFound, "套餐不存在")
        }
        // ✅ 系统错误:数据库查询失败
        return nil, errors.Wrap(errors.CodeInternalError, err, "获取套餐失败")
    }
    
    return s.toResponse(ctx, pkg), nil
}

错误返回示例

  • 套餐不存在404
    {"code": 1006, "msg": "套餐不存在", "data": null}
    
  • 数据库错误500
    {"code": 2001, "msg": "内部服务器错误", "data": null}
    
    日志中记录详细错误:获取套餐失败: connection refused

案例 2分佣提现 - 复杂业务校验

场景:提现审核,需验证余额、状态等

// internal/service/commission_withdrawal/service.go
func (s *Service) Approve(ctx context.Context, id uint, req *dto.ApproveWithdrawalReq) (*dto.WithdrawalApprovalResp, error) {
    // ✅ 业务错误:资源不存在
    withdrawal, err := s.commissionWithdrawalReqStore.GetByID(ctx, id)
    if err != nil {
        return nil, errors.New(errors.CodeNotFound, "提现申请不存在")
    }
    
    // ✅ 业务错误:状态不允许
    if withdrawal.Status != constants.WithdrawalStatusPending {
        return nil, errors.New(errors.CodeInvalidStatus, "申请状态不允许此操作")
    }
    
    // ✅ 业务错误:余额不足
    wallet, err := s.walletStore.GetShopCommissionWallet(ctx, withdrawal.ShopID)
    if err != nil {
        return nil, errors.New(errors.CodeNotFound, "店铺佣金钱包不存在")
    }
    if wallet.FrozenBalance < amount {
        return nil, errors.New(errors.CodeInsufficientBalance, "钱包冻结余额不足")
    }
    
    // ✅ 系统错误:事务执行失败
    err = s.db.Transaction(func(tx *gorm.DB) error {
        if err := s.walletStore.DeductFrozenBalanceWithTx(ctx, tx, wallet.ID, amount); err != nil {
            return errors.Wrap(errors.CodeInternalError, err, "扣除冻结余额失败")
        }
        // ...其他事务操作
        return nil
    })
    
    if err != nil {
        return nil, err
    }
    
    return &dto.WithdrawalApprovalResp{...}, nil
}

案例 3店铺管理 - 重复性检查

场景:创建店铺,需检查代码重复和层级限制

// internal/service/shop/service.go
func (s *Service) Create(ctx context.Context, req *dto.CreateShopRequest) (*dto.ShopResponse, error) {
    // ✅ 业务错误:重复检查
    existing, _ := s.shopStore.GetByCode(ctx, req.ShopCode)
    if existing != nil {
        return nil, errors.New(errors.CodeDuplicate, "店铺代码已存在")
    }
    
    // ✅ 业务错误:层级限制
    level := 1
    if req.ParentID != nil {
        parent, err := s.shopStore.GetByID(ctx, *req.ParentID)
        if err != nil {
            return nil, errors.New(errors.CodeNotFound, "上级店铺不存在")
        }
        level = parent.Level + 1
        if level > 7 {
            return nil, errors.New(errors.CodeInvalidParam, "店铺层级超过限制")
        }
    }
    
    // ✅ 系统错误:数据库操作
    shop := &model.Shop{...}
    if err := s.shopStore.Create(ctx, shop); err != nil {
        return nil, errors.Wrap(errors.CodeInternalError, err, "创建店铺失败")
    }
    
    return s.toResponse(shop), nil
}

错误处理原则总结

场景类型 使用方式 HTTP 状态码 示例
资源不存在 errors.New(CodeNotFound) 404 套餐、店铺、用户不存在
状态不允许 errors.New(CodeInvalidStatus) 400 订单已取消、提现已审核
参数错误 errors.New(CodeInvalidParam) 400 层级超限、金额无效
重复操作 errors.New(CodeDuplicate) 409 代码重复、用户名已存在
余额不足 errors.New(CodeInsufficientBalance) 400 钱包余额不足
数据库错误 errors.Wrap(CodeInternalError, err) 500 查询失败、创建失败
队列错误 errors.Wrap(CodeInternalError, err) 500 任务提交失败

核心原则

  1. 业务错误4xx使用 errors.New(Code4xx, msg)
  2. 系统错误5xx使用 errors.Wrap(Code5xx, err, msg)
  3. 错误消息保持中文,便于日志排查
  4. 禁止 fmt.Errorf 直接对外返回,避免泄露内部细节

版本历史:

  • v1.1.0 (2026-01-29): 补充 Service 层错误处理实战案例
  • v1.0.0 (2025-11-15): 初始版本