diff --git a/README.md b/README.md
index 93c70c5..f3e120d 100644
--- a/README.md
+++ b/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)
```
### 请求流程示例
diff --git a/cmd/api/main.go b/cmd/api/main.go
index aa181b9..dc8a2fe 100644
--- a/cmd/api/main.go
+++ b/cmd/api/main.go
@@ -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)
diff --git a/configs/config.dev.yaml b/configs/config.dev.yaml
index 77c42d3..889ac61 100644
--- a/configs/config.dev.yaml
+++ b/configs/config.dev.yaml
@@ -53,7 +53,6 @@ logging:
compress: false
middleware:
- enable_auth: true # 开发环境可选禁用认证
enable_rate_limiter: true
rate_limiter:
max: 1000
diff --git a/configs/config.prod.yaml b/configs/config.prod.yaml
index 9b3de76..3468f7a 100644
--- a/configs/config.prod.yaml
+++ b/configs/config.prod.yaml
@@ -52,9 +52,6 @@ logging:
compress: true
middleware:
- # 生产环境必须启用认证
- enable_auth: true
-
# 生产环境启用限流,保护服务免受滥用
enable_rate_limiter: true
diff --git a/configs/config.staging.yaml b/configs/config.staging.yaml
index 5e6a6e5..9591920 100644
--- a/configs/config.staging.yaml
+++ b/configs/config.staging.yaml
@@ -52,9 +52,6 @@ logging:
compress: true
middleware:
- # 预发布环境启用认证
- enable_auth: true
-
# 预发布环境启用限流,测试生产配置
enable_rate_limiter: true
diff --git a/configs/config.yaml b/configs/config.yaml
index 0675ab9..2b14bb8 100644
--- a/configs/config.yaml
+++ b/configs/config.yaml
@@ -53,7 +53,6 @@ logging:
compress: false
middleware:
- enable_auth: true # 开发环境可选禁用认证
enable_rate_limiter: true
rate_limiter:
max: 1000
diff --git a/docs/PROJECT-COMPLETION-SUMMARY.md b/docs/PROJECT-COMPLETION-SUMMARY.md
index 65ec94e..8a64c51 100644
--- a/docs/PROJECT-COMPLETION-SUMMARY.md
+++ b/docs/PROJECT-COMPLETION-SUMMARY.md
@@ -481,7 +481,6 @@ redis:
pool_size: 50 # 连接池大小
middleware:
- enable_auth: true # 启用认证
enable_rate_limiter: true # 启用限流
rate_limiter:
max: 5000 # 每分钟最大请求数
diff --git a/docs/deployment/deployment-guide.md b/docs/deployment/deployment-guide.md
index 274768e..1c3e0b3 100644
--- a/docs/deployment/deployment-guide.md
+++ b/docs/deployment/deployment-guide.md
@@ -152,7 +152,6 @@ logging:
compress: true
middleware:
- enable_auth: true
enable_rate_limiter: false
EOF
```
diff --git a/docs/rate-limiting.md b/docs/rate-limiting.md
index 68fc42f..57b582d 100644
--- a/docs/rate-limiting.md
+++ b/docs/rate-limiting.md
@@ -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:
diff --git a/internal/handler/app/personal_customer.go b/internal/handler/app/personal_customer.go
index 465c7bc..9c7233a 100644
--- a/internal/handler/app/personal_customer.go
+++ b/internal/handler/app/personal_customer.go
@@ -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, "获取个人资料失败")
}
// 构造响应
diff --git a/internal/middleware/recover.go b/internal/middleware/recover.go
index 77d0ec0..0125c3f 100644
--- a/internal/middleware/recover.go
+++ b/internal/middleware/recover.go
@@ -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(通过返回错误)
diff --git a/openspec/changes/archive/2026-01-22-unify-error-message-source/.openspec.yaml b/openspec/changes/archive/2026-01-22-unify-error-message-source/.openspec.yaml
new file mode 100644
index 0000000..ec9a990
--- /dev/null
+++ b/openspec/changes/archive/2026-01-22-unify-error-message-source/.openspec.yaml
@@ -0,0 +1,2 @@
+schema: spec-driven
+created: 2026-01-22
diff --git a/openspec/changes/archive/2026-01-22-unify-error-message-source/design.md b/openspec/changes/archive/2026-01-22-unify-error-message-source/design.md
new file mode 100644
index 0000000..3ebaf7b
--- /dev/null
+++ b/openspec/changes/archive/2026-01-22-unify-error-message-source/design.md
@@ -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)` | ✅ 强制单一数据源
❌ 丢失业务上下文 |
+| B. message 改为可选 | `New(code int, msg ...string)` | ✅ 默认用映射表
✅ 允许覆盖
✅ 向后兼容 |
+| C. 新增 NewWithMsg | `New(code)` + `NewWithMsg(code, msg)` | ✅ 清晰区分
❌ 需改所有调用点 |
+
+**决策**:选项 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 | 程序启动 | ✅ 立即发现
❌ 启动失败 |
+| B. init() 日志警告 | 程序启动 | ✅ 不阻塞启动
❌ 可能被忽略 |
+| C. 仅测试校验 | CI 运行 | ✅ 不影响生产
❌ 本地开发可能遗漏 |
+
+**决策**:选项 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` 扫描生成 | ✅ 自动化
❌ 增加构建复杂度 |
+| D. 基于映射表反向校验 | 遍历 errorMessages 的 key | ✅ 简单
❌ 无法检测缺失 |
+| E. 定义错误码注册表 | 显式注册每个错误码 | ✅ 清晰
✅ 编译时检查 |
+
+**决策**:选项 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 处 | ✅ 彻底
❌ 风险高,改动大 |
+| B. 仅清理冗余 | 消息与映射表一致的 | ✅ 安全
✅ 保留业务上下文 |
+| C. 不清理 | 保持现状 | ✅ 零风险
❌ 技术债未还 |
+
+**决策**:选项 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 与映射表重复的情况
+ - 暂不实施,后续根据需要添加
diff --git a/openspec/changes/archive/2026-01-22-unify-error-message-source/proposal.md b/openspec/changes/archive/2026-01-22-unify-error-message-source/proposal.md
new file mode 100644
index 0000000..64191c7
--- /dev/null
+++ b/openspec/changes/archive/2026-01-22-unify-error-message-source/proposal.md
@@ -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 | 中 | 分阶段实施:先加校验,再清理代码 |
diff --git a/openspec/changes/archive/2026-01-22-unify-error-message-source/specs/error-code-validation/spec.md b/openspec/changes/archive/2026-01-22-unify-error-message-source/specs/error-code-validation/spec.md
new file mode 100644
index 0000000..c7aec26
--- /dev/null
+++ b/openspec/changes/archive/2026-01-22-unify-error-message-source/specs/error-code-validation/spec.md
@@ -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** 测试失败,输出孤立的错误码列表(可选警告)
diff --git a/openspec/changes/archive/2026-01-22-unify-error-message-source/tasks.md b/openspec/changes/archive/2026-01-22-unify-error-message-source/tasks.md
new file mode 100644
index 0000000..b94f620
--- /dev/null
+++ b/openspec/changes/archive/2026-01-22-unify-error-message-source/tasks.md
@@ -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 ./...` - 成功
diff --git a/openspec/specs/error-code-validation/spec.md b/openspec/specs/error-code-validation/spec.md
new file mode 100644
index 0000000..6aac43c
--- /dev/null
+++ b/openspec/specs/error-code-validation/spec.md
@@ -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** 测试失败,输出孤立的错误码列表(可选警告)
+
diff --git a/pkg/config/config.go b/pkg/config/config.go
index 1e6c82c..19f613c 100644
--- a/pkg/config/config.go
+++ b/pkg/config/config.go
@@ -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"` // 限流器配置
}
diff --git a/pkg/config/config_bench_test.go b/pkg/config/config_bench_test.go
index 739ee60..7750357 100644
--- a/pkg/config/config_bench_test.go
+++ b/pkg/config/config_bench_test.go
@@ -54,7 +54,7 @@ func BenchmarkGet(b *testing.B) {
_ = cfg.Server.Address
_ = cfg.Redis.Address
_ = cfg.Logging.Level
- _ = cfg.Middleware.EnableAuth
+ _ = cfg.Middleware.EnableRateLimiter
}
})
}
diff --git a/pkg/config/loader_test.go b/pkg/config/loader_test.go
index 8609135..4dc7271 100644
--- a/pkg/config/loader_test.go
+++ b/pkg/config/loader_test.go
@@ -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)
}
diff --git a/pkg/errors/codes.go b/pkg/errors/codes.go
index 8772ae6..db4407e 100644
--- a/pkg/errors/codes.go
+++ b/pkg/errors/codes.go
@@ -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: "成功",
diff --git a/pkg/errors/codes_test.go b/pkg/errors/codes_test.go
index 3e4ca99..6b571d4 100644
--- a/pkg/errors/codes_test.go
+++ b/pkg/errors/codes_test.go
@@ -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{
diff --git a/pkg/errors/errors.go b/pkg/errors/errors.go
index 7df2c6f..35d9156 100644
--- a/pkg/errors/errors.go
+++ b/pkg/errors/errors.go
@@ -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,
diff --git a/pkg/errors/handler_test.go b/pkg/errors/handler_test.go
index a1da393..fbc996f 100644
--- a/pkg/errors/handler_test.go
+++ b/pkg/errors/handler_test.go
@@ -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)
diff --git a/pkg/middleware/auth.go b/pkg/middleware/auth.go
index bde72c6..cc0f9ed 100644
--- a/pkg/middleware/auth.go
+++ b/pkg/middleware/auth.go
@@ -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
diff --git a/pkg/middleware/permission.go b/pkg/middleware/permission.go
index 279dc1d..b79fdba 100644
--- a/pkg/middleware/permission.go
+++ b/pkg/middleware/permission.go
@@ -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, "权限检查失败")
}
// 如果缺少任意一个权限,则拒绝访问
diff --git a/specs/001-fiber-middleware-integration/data-model.md b/specs/001-fiber-middleware-integration/data-model.md
index 3925909..396da2e 100644
--- a/specs/001-fiber-middleware-integration/data-model.md
+++ b/specs/001-fiber-middleware-integration/data-model.md
@@ -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
diff --git a/specs/001-fiber-middleware-integration/quickstart.md b/specs/001-fiber-middleware-integration/quickstart.md
index 531ac8f..2b305a8 100644
--- a/specs/001-fiber-middleware-integration/quickstart.md
+++ b/specs/001-fiber-middleware-integration/quickstart.md
@@ -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**: