All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 5m45s
主要变更: 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 个主规范文件 破坏性变更:无 向后兼容:是
8.5 KiB
8.5 KiB
错误处理规范更新
概述
本变更更新了错误处理规范文档,补充了缺失的内容和实际案例。
更新的规范文件
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包含单元测试示例
影响范围
这些文档更新不影响现有代码逻辑,仅完善规范说明和最佳实践指引。
后续维护
- 新增错误码时,同步更新使用指南中的案例
- 发现新的错误处理模式时,补充到文档中
- 定期检查文档案例与代码实际实现的一致性