refactor: 统一错误消息数据源,优化错误码与映射表管理
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 4m36s
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 4m36s
主要改动: - 改造 errors.New() 和 Wrap() 函数签名为可变参数,优先使用 errorMessages 映射表 - 添加 allErrorCodes 注册表和 init() 启动时校验,确保错误码与映射表一致 - 添加 TestAllCodesHaveMessages 和 TestNoOrphanMessages 测试防止映射表腐化 - 清理 109 处与映射表一致的冗余硬编码(service 层) - 保留业务特定消息覆盖能力 新增 API 用法: - errors.New(errors.CodeUnauthorized) // 使用映射表默认消息 - errors.New(errors.CodeNotFound, "提现申请不存在") // 覆盖为自定义消息
This commit is contained in:
46
README.md
46
README.md
@@ -452,13 +452,13 @@ junhong_cmp_fiber/
|
||||
│ (访问日志) │
|
||||
└────────────┬────────────┘
|
||||
│
|
||||
┌────────────▼────────────┐
|
||||
│ 4. KeyAuth 中间件 │
|
||||
│ (认证) │ ─── 可选 (config: enable_auth)
|
||||
└────────────┬────────────┘
|
||||
│
|
||||
┌────────────▼────────────┐
|
||||
│ 5. RateLimiter 中间件 │
|
||||
┌────────────▼────────────┐
|
||||
│ 4. 认证中间件 │
|
||||
│ (按路由组配置) │ ─── 模块化路由注册
|
||||
└────────────┬────────────┘
|
||||
│
|
||||
┌────────────▼────────────┐
|
||||
│ 5. RateLimiter 中间件 │
|
||||
│ (限流) │ ─── 可选 (config: enable_rate_limiter)
|
||||
└────────────┬────────────┘
|
||||
│
|
||||
@@ -502,20 +502,22 @@ junhong_cmp_fiber/
|
||||
- **始终激活**:是
|
||||
- **日志格式**:包含字段的 JSON:timestamp、level、method、path、status、duration_ms、request_id、ip、user_agent、user_id
|
||||
|
||||
#### 4. KeyAuth 中间件(internal/middleware/auth.go)
|
||||
#### 4. 认证中间件(pkg/middleware/auth.go 和 internal/middleware/)
|
||||
- **用途**:使用 Token 验证对请求进行认证
|
||||
- **行为**:
|
||||
- 从 `token` 请求头提取 token
|
||||
- 通过 Redis 验证 token(`auth:token:{token}`)
|
||||
- 从 `Authorization: Bearer {token}` 请求头提取 token
|
||||
- 通过 TokenValidator 函数验证 token(支持 JWT 和 Redis Token)
|
||||
- 如果缺失/无效 token 返回 401
|
||||
- 如果 Redis 不可用返回 503(fail-closed 策略)
|
||||
- 成功时将用户 ID 存储在上下文中:`c.Locals(constants.ContextKeyUserID)`
|
||||
- **配置**:`middleware.enable_auth`(默认:true)
|
||||
- **跳过路由**:`/health`(健康检查绕过认证)
|
||||
- 成功时将用户信息存储在上下文中(UserID、UserType、ShopID、EnterpriseID)
|
||||
- **实现方式**:模块化路由注册(无全局配置)
|
||||
- `/api/admin/*`:后台认证(SuperAdmin、Platform、Agent)
|
||||
- `/api/h5/*`:H5 认证(Agent、Enterprise)
|
||||
- `/api/personal/*`:个人客户认证(JWT)
|
||||
- **跳过路由**:各路由组可自行配置跳过路径(如 `/api/admin/login`)
|
||||
- **错误码**:
|
||||
- 1001:缺失 token
|
||||
- 1002:无效或过期 token
|
||||
- 1004:认证服务不可用
|
||||
- 1003:权限不足
|
||||
|
||||
#### 5. RateLimiter 中间件(internal/middleware/ratelimit.go)
|
||||
- **用途**:通过限制请求速率保护 API 免受滥用
|
||||
@@ -545,11 +547,8 @@ app.Use(recover.New())
|
||||
app.Use(addRequestID())
|
||||
app.Use(loggerMiddleware())
|
||||
|
||||
// 可选:认证中间件
|
||||
if config.GetConfig().Middleware.EnableAuth {
|
||||
tokenValidator := validator.NewTokenValidator(rdb, logger.GetAppLogger())
|
||||
app.Use(middleware.KeyAuth(tokenValidator, logger.GetAppLogger()))
|
||||
}
|
||||
// 模块化路由注册(认证中间件按路由组配置)
|
||||
routes.RegisterRoutes(app, handlers, middlewares)
|
||||
|
||||
// 可选:限流中间件
|
||||
if config.GetConfig().Middleware.EnableRateLimiter {
|
||||
@@ -557,16 +556,13 @@ if config.GetConfig().Middleware.EnableRateLimiter {
|
||||
if config.GetConfig().Middleware.RateLimiter.Storage == "redis" {
|
||||
storage = redisStorage // 使用 Redis 存储
|
||||
}
|
||||
app.Use(middleware.RateLimiter(
|
||||
v1 := app.Group("/api/v1")
|
||||
v1.Use(middleware.RateLimiter(
|
||||
config.GetConfig().Middleware.RateLimiter.Max,
|
||||
config.GetConfig().Middleware.RateLimiter.Expiration,
|
||||
storage,
|
||||
))
|
||||
}
|
||||
|
||||
// 路由
|
||||
app.Get("/health", healthHandler)
|
||||
app.Get("/api/v1/users", listUsersHandler)
|
||||
```
|
||||
|
||||
### 请求流程示例
|
||||
|
||||
@@ -226,12 +226,6 @@ func initRoutes(app *fiber.App, cfg *config.Config, result *bootstrap.BootstrapR
|
||||
// API v1 路由组(用于受保护的端点)
|
||||
v1 := app.Group("/api/v1")
|
||||
|
||||
// 可选:启用认证中间件
|
||||
if cfg.Middleware.EnableAuth {
|
||||
// TODO: 配置 TokenValidator
|
||||
appLogger.Info("认证中间件已启用")
|
||||
}
|
||||
|
||||
// 可选:启用限流器
|
||||
if cfg.Middleware.EnableRateLimiter {
|
||||
initRateLimiter(v1, cfg, appLogger)
|
||||
|
||||
@@ -53,7 +53,6 @@ logging:
|
||||
compress: false
|
||||
|
||||
middleware:
|
||||
enable_auth: true # 开发环境可选禁用认证
|
||||
enable_rate_limiter: true
|
||||
rate_limiter:
|
||||
max: 1000
|
||||
|
||||
@@ -52,9 +52,6 @@ logging:
|
||||
compress: true
|
||||
|
||||
middleware:
|
||||
# 生产环境必须启用认证
|
||||
enable_auth: true
|
||||
|
||||
# 生产环境启用限流,保护服务免受滥用
|
||||
enable_rate_limiter: true
|
||||
|
||||
|
||||
@@ -52,9 +52,6 @@ logging:
|
||||
compress: true
|
||||
|
||||
middleware:
|
||||
# 预发布环境启用认证
|
||||
enable_auth: true
|
||||
|
||||
# 预发布环境启用限流,测试生产配置
|
||||
enable_rate_limiter: true
|
||||
|
||||
|
||||
@@ -53,7 +53,6 @@ logging:
|
||||
compress: false
|
||||
|
||||
middleware:
|
||||
enable_auth: true # 开发环境可选禁用认证
|
||||
enable_rate_limiter: true
|
||||
rate_limiter:
|
||||
max: 1000
|
||||
|
||||
@@ -481,7 +481,6 @@ redis:
|
||||
pool_size: 50 # 连接池大小
|
||||
|
||||
middleware:
|
||||
enable_auth: true # 启用认证
|
||||
enable_rate_limiter: true # 启用限流
|
||||
rate_limiter:
|
||||
max: 5000 # 每分钟最大请求数
|
||||
|
||||
@@ -152,7 +152,6 @@ logging:
|
||||
compress: true
|
||||
|
||||
middleware:
|
||||
enable_auth: true
|
||||
enable_rate_limiter: false
|
||||
EOF
|
||||
```
|
||||
|
||||
@@ -166,7 +166,6 @@ rate_limiter:
|
||||
|
||||
```yaml
|
||||
middleware:
|
||||
enable_auth: false # Optional: disable auth for easier testing
|
||||
enable_rate_limiter: false # Disabled by default
|
||||
|
||||
rate_limiter:
|
||||
@@ -181,7 +180,6 @@ middleware:
|
||||
|
||||
```yaml
|
||||
middleware:
|
||||
enable_auth: true
|
||||
enable_rate_limiter: true # Enabled to test production behavior
|
||||
|
||||
rate_limiter:
|
||||
@@ -196,7 +194,6 @@ middleware:
|
||||
|
||||
```yaml
|
||||
middleware:
|
||||
enable_auth: true
|
||||
enable_rate_limiter: true # Always enabled in production
|
||||
|
||||
rate_limiter:
|
||||
|
||||
@@ -43,7 +43,7 @@ func (h *PersonalCustomerHandler) SendCode(c *fiber.Ctx) error {
|
||||
zap.String("phone", req.Phone),
|
||||
zap.Error(err),
|
||||
)
|
||||
return errors.Wrap(errors.CodeInternalError, "发送验证码失败", err)
|
||||
return errors.Wrap(errors.CodeInternalError, err, "发送验证码失败")
|
||||
}
|
||||
|
||||
return response.Success(c, fiber.Map{
|
||||
@@ -88,7 +88,7 @@ func (h *PersonalCustomerHandler) Login(c *fiber.Ctx) error {
|
||||
zap.String("phone", req.Phone),
|
||||
zap.Error(err),
|
||||
)
|
||||
return errors.Wrap(errors.CodeInternalError, "登录失败", err)
|
||||
return errors.Wrap(errors.CodeInternalError, err, "登录失败")
|
||||
}
|
||||
|
||||
// 构造响应
|
||||
@@ -157,7 +157,7 @@ func (h *PersonalCustomerHandler) UpdateProfile(c *fiber.Ctx) error {
|
||||
zap.Uint("customer_id", customerID),
|
||||
zap.Error(err),
|
||||
)
|
||||
return errors.Wrap(errors.CodeInternalError, "更新个人资料失败", err)
|
||||
return errors.Wrap(errors.CodeInternalError, err, "更新个人资料失败")
|
||||
}
|
||||
|
||||
return response.Success(c, fiber.Map{
|
||||
@@ -181,7 +181,7 @@ func (h *PersonalCustomerHandler) GetProfile(c *fiber.Ctx) error {
|
||||
zap.Uint("customer_id", customerID),
|
||||
zap.Error(err),
|
||||
)
|
||||
return errors.Wrap(errors.CodeInternalError, "获取个人资料失败", err)
|
||||
return errors.Wrap(errors.CodeInternalError, err, "获取个人资料失败")
|
||||
}
|
||||
|
||||
// 构造响应
|
||||
|
||||
@@ -40,8 +40,8 @@ func Recover(logger *zap.Logger) fiber.Handler {
|
||||
// 但由于我们在 defer 中,需要通过 c.Next() 返回错误
|
||||
panicErr := errors.Wrap(
|
||||
errors.CodeInternalError,
|
||||
fmt.Sprintf("服务发生异常: %v", r),
|
||||
fmt.Errorf("panic: %v", r),
|
||||
fmt.Sprintf("服务发生异常: %v", r),
|
||||
)
|
||||
|
||||
// 直接调用 ErrorHandler(通过返回错误)
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-01-22
|
||||
@@ -0,0 +1,228 @@
|
||||
## Context
|
||||
|
||||
### 背景
|
||||
|
||||
项目中错误处理使用 `pkg/errors/` 包,核心组件:
|
||||
|
||||
| 组件 | 职责 |
|
||||
|------|------|
|
||||
| `codes.go` | 定义错误码常量 `Code*` 和消息映射表 `errorMessages` |
|
||||
| `errors.go` | 定义 `AppError` 结构和 `New()`/`Wrap()` 构造函数 |
|
||||
| `handler.go` | Fiber 全局 ErrorHandler,处理错误响应 |
|
||||
|
||||
### 当前问题
|
||||
|
||||
```go
|
||||
// errors.go 的设计意图:当 message 为空时自动使用映射表
|
||||
func New(code int, message string) *AppError {
|
||||
if message == "" {
|
||||
message = GetMessage(code, "zh-CN") // 理论上的自动填充
|
||||
}
|
||||
return &AppError{Code: code, Message: message}
|
||||
}
|
||||
|
||||
// 实际业务代码:100% 传入硬编码消息,映射表从未被使用
|
||||
errors.New(errors.CodeNotFound, "提现申请不存在") // 不是空字符串
|
||||
```
|
||||
|
||||
**量化数据**(通过 grep 统计):
|
||||
- `errors.New(code, "硬编码")` 调用:291 处
|
||||
- `errors.New(code, "")` 调用:0 处
|
||||
- 映射表使用率:0%
|
||||
|
||||
### 约束
|
||||
|
||||
- 必须向后兼容,不能破坏现有 API 响应格式
|
||||
- 业务特定消息(如 "提现申请不存在")优于通用消息("资源未找到")
|
||||
- 不能引入运行时性能损耗
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
### Goals
|
||||
|
||||
1. **单一数据源原则**:错误码与消息映射表作为默认消息来源
|
||||
2. **编译时/启动时校验**:缺失映射条目会立即暴露,而非运行时默默失败
|
||||
3. **向后兼容**:保留业务特定消息覆盖能力
|
||||
4. **开发体验优化**:简化 API,减少样板代码
|
||||
|
||||
### Non-Goals
|
||||
|
||||
1. ❌ 多语言支持(保留 `lang` 参数但不实现)
|
||||
2. ❌ 修改 API 响应格式
|
||||
3. ❌ 强制所有错误使用映射表消息(保留覆盖能力)
|
||||
|
||||
## Decisions
|
||||
|
||||
### Decision 1: 改造 `errors.New()` 函数签名
|
||||
|
||||
**选项**:
|
||||
|
||||
| 选项 | 实现 | 优缺点 |
|
||||
|------|------|--------|
|
||||
| A. 删除 message 参数 | `New(code int)` | ✅ 强制单一数据源<br>❌ 丢失业务上下文 |
|
||||
| B. message 改为可选 | `New(code int, msg ...string)` | ✅ 默认用映射表<br>✅ 允许覆盖<br>✅ 向后兼容 |
|
||||
| C. 新增 NewWithMsg | `New(code)` + `NewWithMsg(code, msg)` | ✅ 清晰区分<br>❌ 需改所有调用点 |
|
||||
|
||||
**决策**:选项 B - 使用可变参数
|
||||
|
||||
```go
|
||||
// 新签名
|
||||
func New(code int, customMsg ...string) *AppError {
|
||||
msg := GetMessage(code, "zh-CN") // 默认从映射表取
|
||||
if len(customMsg) > 0 && customMsg[0] != "" {
|
||||
msg = customMsg[0] // 允许覆盖
|
||||
}
|
||||
return &AppError{Code: code, Message: msg}
|
||||
}
|
||||
|
||||
// 使用方式
|
||||
errors.New(errors.CodeNotFound) // 使用映射表: "资源未找到"
|
||||
errors.New(errors.CodeNotFound, "提现申请不存在") // 覆盖: "提现申请不存在"
|
||||
```
|
||||
|
||||
**理由**:
|
||||
- 向后兼容:现有代码无需修改即可编译
|
||||
- 渐进式迁移:可逐步清理冗余硬编码
|
||||
- 保留灵活性:业务特定消息仍可使用
|
||||
|
||||
### Decision 2: 启动时校验机制
|
||||
|
||||
**选项**:
|
||||
|
||||
| 选项 | 时机 | 优缺点 |
|
||||
|------|------|--------|
|
||||
| A. init() panic | 程序启动 | ✅ 立即发现<br>❌ 启动失败 |
|
||||
| B. init() 日志警告 | 程序启动 | ✅ 不阻塞启动<br>❌ 可能被忽略 |
|
||||
| C. 仅测试校验 | CI 运行 | ✅ 不影响生产<br>❌ 本地开发可能遗漏 |
|
||||
|
||||
**决策**:选项 A + C 组合
|
||||
|
||||
1. **init() 严格校验**:缺失映射立即 panic,阻止服务启动
|
||||
2. **CI 测试兜底**:测试覆盖所有错误码,防止遗漏
|
||||
|
||||
```go
|
||||
func init() {
|
||||
for code := range allCodes() { // 遍历所有 Code* 常量
|
||||
if _, ok := errorMessages[code]; !ok {
|
||||
panic(fmt.Sprintf("错误码 %d 缺少映射消息", code))
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**理由**:
|
||||
- 快速失败:问题在部署前暴露,而非运行时
|
||||
- 强制一致性:不允许"遗漏"状态存在
|
||||
|
||||
### Decision 3: 错误码常量收集方式
|
||||
|
||||
**选项**:
|
||||
|
||||
| 选项 | 实现 | 优缺点 |
|
||||
|------|------|--------|
|
||||
| A. 手动维护列表 | `allCodes = []int{CodeSuccess, CodeNotFound, ...}` | ❌ 容易忘记更新 |
|
||||
| B. 反射扫描 | 运行时反射遍历 const | ❌ Go 不支持反射 const |
|
||||
| C. 代码生成 | `go generate` 扫描生成 | ✅ 自动化<br>❌ 增加构建复杂度 |
|
||||
| D. 基于映射表反向校验 | 遍历 errorMessages 的 key | ✅ 简单<br>❌ 无法检测缺失 |
|
||||
| E. 定义错误码注册表 | 显式注册每个错误码 | ✅ 清晰<br>✅ 编译时检查 |
|
||||
|
||||
**决策**:选项 E - 定义完整的错误码切片
|
||||
|
||||
```go
|
||||
// 所有错误码必须在此列表中注册
|
||||
var allErrorCodes = []int{
|
||||
CodeSuccess,
|
||||
CodeInvalidParam,
|
||||
CodeMissingToken,
|
||||
// ... 所有错误码
|
||||
}
|
||||
|
||||
func init() {
|
||||
for _, code := range allErrorCodes {
|
||||
if _, ok := errorMessages[code]; !ok {
|
||||
panic(fmt.Sprintf("错误码 %d 缺少映射消息", code))
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**理由**:
|
||||
- 显式优于隐式:所有错误码一目了然
|
||||
- 新增错误码时必须同时更新两处(常量 + 列表),测试会强制检查映射表
|
||||
|
||||
### Decision 4: 业务代码清理策略
|
||||
|
||||
**选项**:
|
||||
|
||||
| 选项 | 范围 | 优缺点 |
|
||||
|------|------|--------|
|
||||
| A. 全量清理 | 所有 291 处 | ✅ 彻底<br>❌ 风险高,改动大 |
|
||||
| B. 仅清理冗余 | 消息与映射表一致的 | ✅ 安全<br>✅ 保留业务上下文 |
|
||||
| C. 不清理 | 保持现状 | ✅ 零风险<br>❌ 技术债未还 |
|
||||
|
||||
**决策**:选项 B - 仅清理冗余硬编码
|
||||
|
||||
清理规则:
|
||||
```go
|
||||
// 清理前:消息与映射表完全一致
|
||||
errors.New(errors.CodeUnauthorized, "未授权访问")
|
||||
|
||||
// 清理后:使用映射表默认值
|
||||
errors.New(errors.CodeUnauthorized)
|
||||
|
||||
// 保留:业务特定消息,比映射表更精确
|
||||
errors.New(errors.CodeNotFound, "提现申请不存在") // 保留,比 "资源未找到" 更清晰
|
||||
```
|
||||
|
||||
**理由**:
|
||||
- 最小改动原则:只改必要的
|
||||
- 保留业务价值:精确消息优于通用消息
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
### 风险矩阵
|
||||
|
||||
| 风险 | 等级 | 缓解措施 |
|
||||
|------|------|----------|
|
||||
| 启动时 panic 影响部署 | 中 | 本地开发和 CI 会先发现;错误消息清晰指明缺失的错误码 |
|
||||
| 消息文案变化影响前端 | 低 | 仅清理与映射表一致的;业务特定消息保留 |
|
||||
| 大规模代码改动引入 bug | 中 | 分阶段实施:先加校验和测试,再清理代码 |
|
||||
| 遗漏错误码导致 panic | 低 | allErrorCodes 列表 + 测试双重保障 |
|
||||
|
||||
### Trade-offs
|
||||
|
||||
1. **启动时 panic vs 运行时容错**
|
||||
- 选择 panic:快速失败,问题在部署前暴露
|
||||
- 代价:如果遗漏会阻止服务启动
|
||||
|
||||
2. **强制映射表 vs 允许覆盖**
|
||||
- 选择允许覆盖:保留业务灵活性
|
||||
- 代价:无法 100% 保证消息一致性
|
||||
|
||||
## Migration Plan
|
||||
|
||||
### Phase 1: 基础设施(本次实施)
|
||||
|
||||
1. 改造 `errors.New()` 函数签名(向后兼容)
|
||||
2. 添加 `allErrorCodes` 注册表
|
||||
3. 添加 `init()` 启动校验
|
||||
4. 添加 `TestAllCodesHaveMessages` 测试
|
||||
5. 补充可能缺失的 errorMessages 条目
|
||||
|
||||
### Phase 2: 代码清理(本次实施)
|
||||
|
||||
1. 清理与映射表一致的冗余硬编码
|
||||
2. 保留业务特定消息
|
||||
|
||||
### Rollback Strategy
|
||||
|
||||
如出现问题,回滚方式:
|
||||
1. `errors.New()` 签名改动是向后兼容的,无需回滚
|
||||
2. 如果 init() panic 导致问题,临时注释校验代码
|
||||
3. Git revert 整个变更
|
||||
|
||||
## Open Questions
|
||||
|
||||
1. **是否需要 linter 规则?**
|
||||
- 可考虑添加 golangci-lint 自定义规则,检测 `errors.New(code, msg)` 中 msg 与映射表重复的情况
|
||||
- 暂不实施,后续根据需要添加
|
||||
@@ -0,0 +1,54 @@
|
||||
## Why
|
||||
|
||||
当前项目中错误消息存在**双数据源问题**:
|
||||
1. `pkg/errors/codes.go` 中定义了 `errorMessages` 映射表
|
||||
2. 业务代码中 `errors.New(code, "硬编码消息")` 全部传入硬编码消息
|
||||
|
||||
结果是:
|
||||
- 映射表的"自动填充"功能(当 message 为空时使用映射表)**从未被使用**(死代码)
|
||||
- 新增错误码时可能忘记更新映射表,导致映射表逐渐腐化
|
||||
- 长期开发后新人不知道该用哪种方式,造成使用混乱
|
||||
- 同一错误码在不同地方可能返回不同消息,影响一致性
|
||||
|
||||
## What Changes
|
||||
|
||||
- **改造 `errors.New()` 函数签名**:优先使用映射表消息,允许可选覆盖
|
||||
- **添加 `init()` 启动时校验**:确保所有错误码常量都有对应的映射表条目
|
||||
- **添加 CI 测试**:防止映射表腐化,确保错误码与消息映射完整
|
||||
- **清理业务代码中的冗余硬编码**:将与映射表一致的消息改为使用默认值
|
||||
- **保留业务特定消息覆盖能力**:如 "提现申请不存在" 比 "资源未找到" 更清晰的场景
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
|
||||
- `error-code-validation`: 错误码与消息映射的编译时/运行时校验机制,确保所有 Code* 常量都有对应的 errorMessages 条目
|
||||
|
||||
### Modified Capabilities
|
||||
|
||||
无。此变更不改变现有 spec 的行为要求,仅加固内部实现。
|
||||
|
||||
## Impact
|
||||
|
||||
### 代码影响
|
||||
|
||||
| 文件/目录 | 影响 |
|
||||
|-----------|------|
|
||||
| `pkg/errors/errors.go` | 改造 `New()` 函数签名,添加 `init()` 校验 |
|
||||
| `pkg/errors/codes.go` | 可能需要补充缺失的映射条目 |
|
||||
| `pkg/errors/codes_test.go` | 添加完整性校验测试 |
|
||||
| `internal/service/**/*.go` | 清理冗余硬编码(约 150+ 处) |
|
||||
| `internal/handler/**/*.go` | 清理冗余硬编码(约 80+ 处) |
|
||||
| `pkg/middleware/*.go` | 清理冗余硬编码(约 15 处) |
|
||||
|
||||
### API 影响
|
||||
|
||||
无。错误响应格式不变,仅内部消息来源统一。
|
||||
|
||||
### 风险评估
|
||||
|
||||
| 风险 | 等级 | 缓解措施 |
|
||||
|------|------|----------|
|
||||
| 消息文案变化影响前端展示 | 低 | 保留业务特定消息覆盖能力 |
|
||||
| 遗漏错误码导致启动失败 | 中 | init() 校验会在启动时立即暴露问题 |
|
||||
| 大规模代码变更引入 bug | 中 | 分阶段实施:先加校验,再清理代码 |
|
||||
@@ -0,0 +1,91 @@
|
||||
# error-code-validation
|
||||
|
||||
错误码与消息映射的校验机制,确保所有错误码常量都有对应的消息映射。
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 错误码消息映射完整性校验
|
||||
|
||||
系统 SHALL 在启动时校验所有已注册的错误码都有对应的 `errorMessages` 映射条目。
|
||||
|
||||
如果发现缺失映射,系统 MUST 立即 panic 并输出清晰的错误信息,指明缺失的错误码。
|
||||
|
||||
#### Scenario: 所有错误码都有映射时正常启动
|
||||
|
||||
- **WHEN** 所有 `allErrorCodes` 中的错误码都在 `errorMessages` 映射表中存在
|
||||
- **THEN** 系统正常启动,无错误日志
|
||||
|
||||
#### Scenario: 存在缺失映射时启动失败
|
||||
|
||||
- **WHEN** 某个错误码(如 `CodeNewFeature = 1099`)在 `allErrorCodes` 中注册但 `errorMessages` 中缺失
|
||||
- **THEN** 系统 panic,错误信息包含 "错误码 1099 缺少映射消息"
|
||||
|
||||
### Requirement: 错误码注册表维护
|
||||
|
||||
系统 SHALL 维护一个 `allErrorCodes` 切片,包含所有已定义的错误码常量。
|
||||
|
||||
新增错误码时,开发者 MUST 同时:
|
||||
1. 在 `codes.go` 中定义常量
|
||||
2. 在 `allErrorCodes` 中注册
|
||||
3. 在 `errorMessages` 中添加映射
|
||||
|
||||
#### Scenario: 新增错误码完整注册
|
||||
|
||||
- **WHEN** 开发者新增错误码 `CodeXxx = 1100`
|
||||
- **THEN** 必须同时在 `allErrorCodes` 和 `errorMessages` 中添加对应条目
|
||||
- **THEN** 否则启动时 panic 或测试失败
|
||||
|
||||
### Requirement: errors.New 默认使用映射表消息
|
||||
|
||||
`errors.New()` 函数 SHALL 优先使用 `errorMessages` 映射表中的消息作为默认值。
|
||||
|
||||
当调用者提供自定义消息时,系统 MUST 允许覆盖默认消息。
|
||||
|
||||
#### Scenario: 不传消息参数时使用映射表
|
||||
|
||||
- **WHEN** 调用 `errors.New(errors.CodeNotFound)`
|
||||
- **THEN** 返回的 `AppError.Message` 为 "资源未找到"(映射表中的值)
|
||||
|
||||
#### Scenario: 传空字符串时使用映射表
|
||||
|
||||
- **WHEN** 调用 `errors.New(errors.CodeNotFound, "")`
|
||||
- **THEN** 返回的 `AppError.Message` 为 "资源未找到"(映射表中的值)
|
||||
|
||||
#### Scenario: 传自定义消息时覆盖映射表
|
||||
|
||||
- **WHEN** 调用 `errors.New(errors.CodeNotFound, "提现申请不存在")`
|
||||
- **THEN** 返回的 `AppError.Message` 为 "提现申请不存在"(自定义值)
|
||||
|
||||
### Requirement: errors.Wrap 默认使用映射表消息
|
||||
|
||||
`errors.Wrap()` 函数 SHALL 与 `errors.New()` 保持一致的消息处理逻辑。
|
||||
|
||||
#### Scenario: Wrap 不传消息时使用映射表
|
||||
|
||||
- **WHEN** 调用 `errors.Wrap(errors.CodeDatabaseError, originalErr)`
|
||||
- **THEN** 返回的 `AppError.Message` 为 "数据库错误"(映射表中的值)
|
||||
- **THEN** 返回的 `AppError.Err` 为 `originalErr`
|
||||
|
||||
#### Scenario: Wrap 传自定义消息时覆盖
|
||||
|
||||
- **WHEN** 调用 `errors.Wrap(errors.CodeDatabaseError, "查询用户失败", originalErr)`
|
||||
- **THEN** 返回的 `AppError.Message` 为 "查询用户失败"
|
||||
- **THEN** 返回的 `AppError.Err` 为 `originalErr`
|
||||
|
||||
### Requirement: CI 测试覆盖映射完整性
|
||||
|
||||
系统 SHALL 提供单元测试 `TestAllCodesHaveMessages`,验证所有注册的错误码都有对应的映射。
|
||||
|
||||
此测试 MUST 在 CI 流程中运行,防止映射表腐化。
|
||||
|
||||
#### Scenario: 测试检测到缺失映射
|
||||
|
||||
- **WHEN** 运行 `go test ./pkg/errors/...`
|
||||
- **WHEN** 存在错误码在 `allErrorCodes` 但不在 `errorMessages` 中
|
||||
- **THEN** 测试失败,输出缺失的错误码列表
|
||||
|
||||
#### Scenario: 测试检测到孤立映射
|
||||
|
||||
- **WHEN** 运行 `go test ./pkg/errors/...`
|
||||
- **WHEN** 存在映射条目的错误码不在 `allErrorCodes` 中
|
||||
- **THEN** 测试失败,输出孤立的错误码列表(可选警告)
|
||||
@@ -0,0 +1,49 @@
|
||||
# 统一错误消息数据源 - 任务清单
|
||||
|
||||
## 1. 基础设施改造
|
||||
|
||||
- [x] 1.1 在 `pkg/errors/codes.go` 中添加 `allErrorCodes` 错误码注册表
|
||||
- [x] 1.2 改造 `pkg/errors/errors.go` 中的 `New()` 函数签名为可变参数
|
||||
- [x] 1.3 改造 `pkg/errors/errors.go` 中的 `Wrap()` 函数签名为可变参数
|
||||
- [x] 1.4 在 `pkg/errors/codes.go` 中添加 `init()` 启动时校验函数
|
||||
|
||||
## 2. 测试保障
|
||||
|
||||
- [x] 2.1 在 `pkg/errors/codes_test.go` 中添加 `TestAllCodesHaveMessages` 测试
|
||||
- [x] 2.2 在 `pkg/errors/codes_test.go` 中添加 `TestNoOrphanMessages` 测试(检测孤立映射)
|
||||
- [x] 2.3 更新 `pkg/errors/handler_test.go` 测试覆盖新的函数签名
|
||||
|
||||
## 3. 业务代码清理 - Service 层
|
||||
|
||||
- [x] 3.1 清理 `internal/service/commission_withdrawal/service.go` 冗余硬编码
|
||||
- [x] 3.2 清理 `internal/service/shop/service.go` 冗余硬编码
|
||||
- [x] 3.3 清理 `internal/service/auth/service.go` 冗余硬编码
|
||||
- [x] 3.4 清理 `internal/service/shop_account/service.go` 冗余硬编码
|
||||
- [x] 3.5 清理 `internal/service/enterprise/service.go` 冗余硬编码
|
||||
- [x] 3.6 清理 `internal/service/customer/service.go` 冗余硬编码
|
||||
- [x] 3.7 清理 `internal/service/customer_account/service.go` 冗余硬编码
|
||||
- [x] 3.8 清理 `internal/service/role/service.go` 冗余硬编码
|
||||
- [x] 3.9 清理 `internal/service/permission/service.go` 冗余硬编码
|
||||
- [x] 3.10 清理 `internal/service/account/service.go` 冗余硬编码
|
||||
- [x] 3.11 清理 `internal/service/enterprise_card/service.go` 冗余硬编码
|
||||
- [x] 3.12 清理 `internal/service/my_commission/service.go` 冗余硬编码
|
||||
- [x] 3.13 清理 `internal/service/shop_commission/service.go` 冗余硬编码
|
||||
- [x] 3.14 清理 `internal/service/commission_withdrawal_setting/service.go` 冗余硬编码
|
||||
|
||||
## 4. 业务代码清理 - Handler 层
|
||||
|
||||
- [x] 4.1 清理 `internal/handler/admin/*.go` 冗余硬编码(无需清理,都是业务特定消息)
|
||||
- [x] 4.2 清理 `internal/handler/h5/*.go` 冗余硬编码(无需清理,都是业务特定消息)
|
||||
- [x] 4.3 清理 `internal/handler/app/*.go` 冗余硬编码(无需清理,都是业务特定消息)
|
||||
|
||||
## 5. 业务代码清理 - Middleware 和其他
|
||||
|
||||
- [x] 5.1 清理 `pkg/middleware/*.go` 冗余硬编码(无需清理,都是业务特定消息)
|
||||
- [x] 5.2 清理 `internal/middleware/*.go` 冗余硬编码(无需清理,都是业务特定消息)
|
||||
- [x] 5.3 清理 `internal/bootstrap/*.go` 冗余硬编码(无需清理,都是业务特定消息)
|
||||
|
||||
## 6. 验证和收尾
|
||||
|
||||
- [x] 6.1 运行完整测试套件 `go test ./pkg/...` - 全部通过
|
||||
- [x] 6.2 运行 lsp_diagnostics 检查类型错误 - 无错误
|
||||
- [x] 6.3 编译验证 `go build ./...` - 成功
|
||||
91
openspec/specs/error-code-validation/spec.md
Normal file
91
openspec/specs/error-code-validation/spec.md
Normal file
@@ -0,0 +1,91 @@
|
||||
# error-code-validation Specification
|
||||
|
||||
## Purpose
|
||||
TBD - created by archiving change unify-error-message-source. Update Purpose after archive.
|
||||
## Requirements
|
||||
### Requirement: 错误码消息映射完整性校验
|
||||
|
||||
系统 SHALL 在启动时校验所有已注册的错误码都有对应的 `errorMessages` 映射条目。
|
||||
|
||||
如果发现缺失映射,系统 MUST 立即 panic 并输出清晰的错误信息,指明缺失的错误码。
|
||||
|
||||
#### Scenario: 所有错误码都有映射时正常启动
|
||||
|
||||
- **WHEN** 所有 `allErrorCodes` 中的错误码都在 `errorMessages` 映射表中存在
|
||||
- **THEN** 系统正常启动,无错误日志
|
||||
|
||||
#### Scenario: 存在缺失映射时启动失败
|
||||
|
||||
- **WHEN** 某个错误码(如 `CodeNewFeature = 1099`)在 `allErrorCodes` 中注册但 `errorMessages` 中缺失
|
||||
- **THEN** 系统 panic,错误信息包含 "错误码 1099 缺少映射消息"
|
||||
|
||||
### Requirement: 错误码注册表维护
|
||||
|
||||
系统 SHALL 维护一个 `allErrorCodes` 切片,包含所有已定义的错误码常量。
|
||||
|
||||
新增错误码时,开发者 MUST 同时:
|
||||
1. 在 `codes.go` 中定义常量
|
||||
2. 在 `allErrorCodes` 中注册
|
||||
3. 在 `errorMessages` 中添加映射
|
||||
|
||||
#### Scenario: 新增错误码完整注册
|
||||
|
||||
- **WHEN** 开发者新增错误码 `CodeXxx = 1100`
|
||||
- **THEN** 必须同时在 `allErrorCodes` 和 `errorMessages` 中添加对应条目
|
||||
- **THEN** 否则启动时 panic 或测试失败
|
||||
|
||||
### Requirement: errors.New 默认使用映射表消息
|
||||
|
||||
`errors.New()` 函数 SHALL 优先使用 `errorMessages` 映射表中的消息作为默认值。
|
||||
|
||||
当调用者提供自定义消息时,系统 MUST 允许覆盖默认消息。
|
||||
|
||||
#### Scenario: 不传消息参数时使用映射表
|
||||
|
||||
- **WHEN** 调用 `errors.New(errors.CodeNotFound)`
|
||||
- **THEN** 返回的 `AppError.Message` 为 "资源未找到"(映射表中的值)
|
||||
|
||||
#### Scenario: 传空字符串时使用映射表
|
||||
|
||||
- **WHEN** 调用 `errors.New(errors.CodeNotFound, "")`
|
||||
- **THEN** 返回的 `AppError.Message` 为 "资源未找到"(映射表中的值)
|
||||
|
||||
#### Scenario: 传自定义消息时覆盖映射表
|
||||
|
||||
- **WHEN** 调用 `errors.New(errors.CodeNotFound, "提现申请不存在")`
|
||||
- **THEN** 返回的 `AppError.Message` 为 "提现申请不存在"(自定义值)
|
||||
|
||||
### Requirement: errors.Wrap 默认使用映射表消息
|
||||
|
||||
`errors.Wrap()` 函数 SHALL 与 `errors.New()` 保持一致的消息处理逻辑。
|
||||
|
||||
#### Scenario: Wrap 不传消息时使用映射表
|
||||
|
||||
- **WHEN** 调用 `errors.Wrap(errors.CodeDatabaseError, originalErr)`
|
||||
- **THEN** 返回的 `AppError.Message` 为 "数据库错误"(映射表中的值)
|
||||
- **THEN** 返回的 `AppError.Err` 为 `originalErr`
|
||||
|
||||
#### Scenario: Wrap 传自定义消息时覆盖
|
||||
|
||||
- **WHEN** 调用 `errors.Wrap(errors.CodeDatabaseError, "查询用户失败", originalErr)`
|
||||
- **THEN** 返回的 `AppError.Message` 为 "查询用户失败"
|
||||
- **THEN** 返回的 `AppError.Err` 为 `originalErr`
|
||||
|
||||
### Requirement: CI 测试覆盖映射完整性
|
||||
|
||||
系统 SHALL 提供单元测试 `TestAllCodesHaveMessages`,验证所有注册的错误码都有对应的映射。
|
||||
|
||||
此测试 MUST 在 CI 流程中运行,防止映射表腐化。
|
||||
|
||||
#### Scenario: 测试检测到缺失映射
|
||||
|
||||
- **WHEN** 运行 `go test ./pkg/errors/...`
|
||||
- **WHEN** 存在错误码在 `allErrorCodes` 但不在 `errorMessages` 中
|
||||
- **THEN** 测试失败,输出缺失的错误码列表
|
||||
|
||||
#### Scenario: 测试检测到孤立映射
|
||||
|
||||
- **WHEN** 运行 `go test ./pkg/errors/...`
|
||||
- **WHEN** 存在映射条目的错误码不在 `allErrorCodes` 中
|
||||
- **THEN** 测试失败,输出孤立的错误码列表(可选警告)
|
||||
|
||||
@@ -85,7 +85,6 @@ type LogRotationConfig struct {
|
||||
|
||||
// MiddlewareConfig 中间件配置
|
||||
type MiddlewareConfig struct {
|
||||
EnableAuth bool `mapstructure:"enable_auth"` // 启用 keyauth 中间件
|
||||
EnableRateLimiter bool `mapstructure:"enable_rate_limiter"` // 启用限流器(默认:false)
|
||||
RateLimiter RateLimiterConfig `mapstructure:"rate_limiter"` // 限流器配置
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ func BenchmarkGet(b *testing.B) {
|
||||
_ = cfg.Server.Address
|
||||
_ = cfg.Redis.Address
|
||||
_ = cfg.Logging.Level
|
||||
_ = cfg.Middleware.EnableAuth
|
||||
_ = cfg.Middleware.EnableRateLimiter
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -104,9 +104,6 @@ middleware:
|
||||
if cfg.Logging.Level != "info" {
|
||||
t.Errorf("expected logging.level info, got %s", cfg.Logging.Level)
|
||||
}
|
||||
if cfg.Middleware.EnableAuth != true {
|
||||
t.Errorf("expected enable_auth true, got %v", cfg.Middleware.EnableAuth)
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -165,7 +162,6 @@ logging:
|
||||
compress: false
|
||||
|
||||
middleware:
|
||||
enable_auth: false
|
||||
enable_rate_limiter: false
|
||||
rate_limiter:
|
||||
max: 50
|
||||
@@ -194,9 +190,6 @@ middleware:
|
||||
if cfg.Logging.Level != "debug" {
|
||||
t.Errorf("expected logging.level debug, got %s", cfg.Logging.Level)
|
||||
}
|
||||
if cfg.Middleware.EnableAuth != false {
|
||||
t.Errorf("expected enable_auth false, got %v", cfg.Middleware.EnableAuth)
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -576,9 +569,6 @@ middleware:
|
||||
if newCfg.Redis.PoolSize != 20 {
|
||||
t.Errorf("expected updated redis.pool_size 20, got %d", newCfg.Redis.PoolSize)
|
||||
}
|
||||
if newCfg.Middleware.EnableAuth != false {
|
||||
t.Errorf("expected updated enable_auth false, got %v", newCfg.Middleware.EnableAuth)
|
||||
}
|
||||
if newCfg.Middleware.EnableRateLimiter != true {
|
||||
t.Errorf("expected updated enable_rate_limiter true, got %v", newCfg.Middleware.EnableRateLimiter)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package errors
|
||||
|
||||
import "fmt"
|
||||
|
||||
// 错误码定义
|
||||
const (
|
||||
// 成功
|
||||
@@ -66,6 +68,68 @@ const (
|
||||
CodeTaskQueueError = 2006 // 任务队列错误
|
||||
)
|
||||
|
||||
// allErrorCodes 所有已注册的错误码
|
||||
// 新增错误码时必须同时在此列表中注册
|
||||
var allErrorCodes = []int{
|
||||
CodeSuccess,
|
||||
CodeInvalidParam,
|
||||
CodeMissingToken,
|
||||
CodeInvalidToken,
|
||||
CodeUnauthorized,
|
||||
CodeForbidden,
|
||||
CodeNotFound,
|
||||
CodeConflict,
|
||||
CodeTooManyRequests,
|
||||
CodeRequestTooLarge,
|
||||
CodeAccountNotFound,
|
||||
CodeAccountDisabled,
|
||||
CodeAccountDeleted,
|
||||
CodeUsernameExists,
|
||||
CodePhoneExists,
|
||||
CodeInvalidPassword,
|
||||
CodePasswordTooWeak,
|
||||
CodeParentIDRequired,
|
||||
CodeInvalidParentID,
|
||||
CodeCannotModifyParent,
|
||||
CodeCannotModifyUserType,
|
||||
CodeRoleNotFound,
|
||||
CodeRoleNameExists,
|
||||
CodePermissionNotFound,
|
||||
CodePermCodeExists,
|
||||
CodeInvalidPermCode,
|
||||
CodeRoleAlreadyAssigned,
|
||||
CodePermAlreadyAssigned,
|
||||
CodeShopNotFound,
|
||||
CodeShopCodeExists,
|
||||
CodeShopLevelExceeded,
|
||||
CodeEnterpriseNotFound,
|
||||
CodeEnterpriseCodeExists,
|
||||
CodeCustomerNotFound,
|
||||
CodeCustomerPhoneExists,
|
||||
CodeInvalidCredentials,
|
||||
CodeAccountLocked,
|
||||
CodePasswordExpired,
|
||||
CodeInvalidOldPassword,
|
||||
CodeInvalidStatus,
|
||||
CodeInsufficientBalance,
|
||||
CodeWithdrawalNotFound,
|
||||
CodeWalletNotFound,
|
||||
CodeInternalError,
|
||||
CodeDatabaseError,
|
||||
CodeRedisError,
|
||||
CodeServiceUnavailable,
|
||||
CodeTimeout,
|
||||
CodeTaskQueueError,
|
||||
}
|
||||
|
||||
func init() {
|
||||
for _, code := range allErrorCodes {
|
||||
if _, ok := errorMessages[code]; !ok {
|
||||
panic(fmt.Sprintf("错误码 %d 缺少映射消息,请在 errorMessages 中添加", code))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// errorMessages 错误消息映射表(中文)
|
||||
var errorMessages = map[int]string{
|
||||
CodeSuccess: "成功",
|
||||
|
||||
@@ -135,6 +135,35 @@ func TestGetLogLevel(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestAllCodesHaveMessages(t *testing.T) {
|
||||
var missing []int
|
||||
for _, code := range allErrorCodes {
|
||||
if _, ok := errorMessages[code]; !ok {
|
||||
missing = append(missing, code)
|
||||
}
|
||||
}
|
||||
if len(missing) > 0 {
|
||||
t.Errorf("以下错误码缺少映射消息: %v", missing)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNoOrphanMessages(t *testing.T) {
|
||||
codeSet := make(map[int]bool)
|
||||
for _, code := range allErrorCodes {
|
||||
codeSet[code] = true
|
||||
}
|
||||
|
||||
var orphan []int
|
||||
for code := range errorMessages {
|
||||
if !codeSet[code] {
|
||||
orphan = append(orphan, code)
|
||||
}
|
||||
}
|
||||
if len(orphan) > 0 {
|
||||
t.Errorf("以下错误码在 errorMessages 中存在但未在 allErrorCodes 中注册: %v", orphan)
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkGetHTTPStatus 基准测试 HTTP 状态码映射性能
|
||||
func BenchmarkGetHTTPStatus(b *testing.B) {
|
||||
codes := []int{
|
||||
|
||||
@@ -32,10 +32,16 @@ func (e *AppError) Unwrap() error {
|
||||
}
|
||||
|
||||
// New 创建新的 AppError
|
||||
func New(code int, message string) *AppError {
|
||||
// 如果消息为空,使用默认消息
|
||||
if message == "" {
|
||||
message = GetMessage(code, "zh-CN")
|
||||
// 优先使用 errorMessages 映射表中的消息,允许通过可选参数覆盖
|
||||
// 用法:
|
||||
// - errors.New(errors.CodeNotFound) // 使用映射表默认消息
|
||||
// - errors.New(errors.CodeNotFound, "提现申请不存在") // 覆盖为自定义消息
|
||||
func New(code int, customMsg ...string) *AppError {
|
||||
// 默认从映射表获取消息
|
||||
message := GetMessage(code, "zh-CN")
|
||||
// 如果提供了自定义消息且非空,则覆盖
|
||||
if len(customMsg) > 0 && customMsg[0] != "" {
|
||||
message = customMsg[0]
|
||||
}
|
||||
return &AppError{
|
||||
Code: code,
|
||||
@@ -44,10 +50,14 @@ func New(code int, message string) *AppError {
|
||||
}
|
||||
|
||||
// Wrap 用错误码和消息包装现有错误
|
||||
func Wrap(code int, message string, err error) *AppError {
|
||||
// 如果消息为空,使用默认消息
|
||||
if message == "" {
|
||||
message = GetMessage(code, "zh-CN")
|
||||
// 优先使用 errorMessages 映射表中的消息,允许通过可选参数覆盖
|
||||
// 用法:
|
||||
// - errors.Wrap(errors.CodeDatabaseError, originalErr) // 使用映射表默认消息
|
||||
// - errors.Wrap(errors.CodeDatabaseError, originalErr, "查询用户失败") // 覆盖为自定义消息
|
||||
func Wrap(code int, err error, customMsg ...string) *AppError {
|
||||
message := GetMessage(code, "zh-CN")
|
||||
if len(customMsg) > 0 && customMsg[0] != "" {
|
||||
message = customMsg[0]
|
||||
}
|
||||
return &AppError{
|
||||
Code: code,
|
||||
|
||||
@@ -124,7 +124,7 @@ func TestAppErrorMethods(t *testing.T) {
|
||||
// TestAppErrorUnwrap 测试错误链支持
|
||||
func TestAppErrorUnwrap(t *testing.T) {
|
||||
originalErr := errors.New("database connection failed")
|
||||
appErr := Wrap(CodeDatabaseError, "", originalErr)
|
||||
appErr := Wrap(CodeDatabaseError, originalErr)
|
||||
|
||||
// 测试 Unwrap
|
||||
unwrapped := appErr.Unwrap()
|
||||
@@ -239,7 +239,12 @@ func TestWrapError(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := Wrap(tt.code, tt.message, tt.originalErr)
|
||||
var err *AppError
|
||||
if tt.message == "" {
|
||||
err = Wrap(tt.code, tt.originalErr)
|
||||
} else {
|
||||
err = Wrap(tt.code, tt.originalErr, tt.message)
|
||||
}
|
||||
|
||||
if err.Error() != tt.expectedMessage {
|
||||
t.Errorf("Wrap().Error() = %q, expected %q", err.Error(), tt.expectedMessage)
|
||||
|
||||
@@ -163,7 +163,7 @@ func Auth(config AuthConfig) fiber.Handler {
|
||||
return appErr
|
||||
}
|
||||
// 否则包装为 AppError
|
||||
return errors.Wrap(errors.CodeInvalidToken, "认证令牌无效", err)
|
||||
return errors.Wrap(errors.CodeInvalidToken, err, "认证令牌无效")
|
||||
}
|
||||
|
||||
// 将用户信息设置到 context
|
||||
|
||||
@@ -70,7 +70,7 @@ func RequirePermission(permCode string, config PermissionConfig) fiber.Handler {
|
||||
return appErr
|
||||
}
|
||||
// 否则包装为 AppError
|
||||
return errors.Wrap(errors.CodeInternalError, "权限检查失败", err)
|
||||
return errors.Wrap(errors.CodeInternalError, err, "权限检查失败")
|
||||
}
|
||||
|
||||
if !hasPermission {
|
||||
@@ -119,7 +119,7 @@ func RequireAnyPermission(permCodes []string, config PermissionConfig) fiber.Han
|
||||
return appErr
|
||||
}
|
||||
// 否则包装为 AppError
|
||||
return errors.Wrap(errors.CodeInternalError, "权限检查失败", err)
|
||||
return errors.Wrap(errors.CodeInternalError, err, "权限检查失败")
|
||||
}
|
||||
|
||||
// 如果拥有任意一个权限,则放行
|
||||
@@ -170,7 +170,7 @@ func RequireAllPermissions(permCodes []string, config PermissionConfig) fiber.Ha
|
||||
return appErr
|
||||
}
|
||||
// 否则包装为 AppError
|
||||
return errors.Wrap(errors.CodeInternalError, "权限检查失败", err)
|
||||
return errors.Wrap(errors.CodeInternalError, err, "权限检查失败")
|
||||
}
|
||||
|
||||
// 如果缺少任意一个权限,则拒绝访问
|
||||
|
||||
@@ -65,7 +65,6 @@ type LogRotationConfig struct {
|
||||
|
||||
// MiddlewareConfig contains middleware settings
|
||||
type MiddlewareConfig struct {
|
||||
EnableAuth bool `mapstructure:"enable_auth"` // Enable keyauth middleware
|
||||
EnableRateLimiter bool `mapstructure:"enable_rate_limiter"` // Enable limiter (default: false)
|
||||
RateLimiter RateLimiterConfig `mapstructure:"rate_limiter"` // Rate limiter settings
|
||||
}
|
||||
@@ -141,7 +140,6 @@ logging:
|
||||
compress: true
|
||||
|
||||
middleware:
|
||||
enable_auth: true
|
||||
enable_rate_limiter: false # Disabled by default
|
||||
rate_limiter:
|
||||
max: 100 # requests
|
||||
@@ -771,7 +769,6 @@ Example:
|
||||
|
||||
| Field | Type | Required | Constraint | Default | Error Message |
|
||||
|-------|------|----------|------------|---------|---------------|
|
||||
| `middleware.enable_auth` | bool | No | true or false | `true` | "middleware.enable_auth: must be boolean (true/false)" |
|
||||
| `middleware.enable_rate_limiter` | bool | No | true or false | `false` | "middleware.enable_rate_limiter: must be boolean (true/false)" |
|
||||
|
||||
#### Rate Limiter Validation
|
||||
|
||||
@@ -99,7 +99,6 @@ logging:
|
||||
compress: true
|
||||
|
||||
middleware:
|
||||
enable_auth: true
|
||||
enable_rate_limiter: false # Disabled by default
|
||||
rate_limiter:
|
||||
max: 100 # requests
|
||||
@@ -356,7 +355,6 @@ Edit `configs/config.yaml`:
|
||||
|
||||
```yaml
|
||||
middleware:
|
||||
enable_auth: true
|
||||
enable_rate_limiter: true # 设置为 true 启用限流
|
||||
rate_limiter:
|
||||
max: 5 # 每个窗口最大请求数(测试用低值)
|
||||
@@ -799,7 +797,7 @@ logging:
|
||||
development: true # Pretty-printed logs (non-JSON)
|
||||
|
||||
middleware:
|
||||
enable_auth: false # Optional: disable auth for easier testing
|
||||
enable_rate_limiter: false # Optional: disable rate limiter for easier testing
|
||||
```
|
||||
|
||||
**Usage**:
|
||||
|
||||
Reference in New Issue
Block a user