## 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 与映射表重复的情况
   - 暂不实施，后续根据需要添加
