## Context

项目于 2024 年初启动，经过一年多的功能迭代，业务模块逐步完善（支付、轮询、分佣、多租户等），但代码库中积累了分散的废弃代码、硬编码常量、规范缺陷和设计遗留问题。这些问题目前不影响功能，但随着新功能增加，维护成本快速上升：

1. **API 文档覆盖率仅 18.75%**：docs.go/gendocs 只注册了 9 个 Handler，实际路由 48 个，导致 39 个接口无法在 OpenAPI 文档中查阅
2. **支付配置硬编码**：order/service 和 recharge/service 中 3 处 TODO，直接使用全局 `s.wechatPayment`，多商户场景下无法正确验签
3. **硬编码魔法字符串**：轮询系统中 10+ 处状态字符串未提取常量，无法被 IDE 重构工具识别，变更风险高
4. **废弃代码散布各处**：15 个常量别名、3 个 DTO、2 个方法标注废弃但仍保留，新开发者易误用
5. **Model 数据冗余**：IotCard 和 Device 模型中 4 个字段已被替代但仍保留在数据库，造成数据不一致风险
6. **DTO 规范缺口**：122 个 Response DTO 缺少 `_name` 文字字段，违反项目规范
7. **迁移文件堆积，无法支撑生产部署**：开发阶段逐步积累 114 个迁移（000000~000113），包含大量中间过渡状态（重命名表、删除旧表、修复字段类型等），问题具体为：
   - `000104_polling_config_data` 仅有 `.up.sql`，缺少 `.down.sql`
   - `backfill_order_purchase_role.sql` 游离在外，不符合 golang-migrate 命名规范，不会被自动执行
   - 新环境部署需顺序执行 114 个迁移，任何一步失败即卡住

## Goals / Non-Goals

**Goals:**

1. **文档完整性**：补全 API 文档生成器，达到 100% 覆盖率，确保前端/对接方能通过 OpenAPI 文档查阅所有接口
2. **系统可靠性**：实现支付配置动态加载，支持多商户场景，消除验签失败隐患
3. **代码规范化**：清理所有硬编码常量、废弃代码、重复 DTO，统一提取到 `pkg/constants/`，降低维护成本和新手上手难度
4. **数据库一致性**：删除废弃 Model 字段和对应数据库列，防止新/旧逻辑产生的数据不一致
5. **规范落实**：补全 DTO `_name` 字段和 Service 层赋值逻辑，确保规范 100% 贯彻
6. **生产就绪的迁移基线**：将 114 个开发期迁移合并为 2 个生产基线文件，新环境一条命令完成全量建库，彻底消除中间过渡状态

**Non-Goals:**

- 重构现有业务逻辑（支付、轮询、分佣等），仅清理代码结构
- 修改 API 契约或响应格式（除了补充 `_name` 字段）
- 优化性能（无性能变更）
- 新增功能

## Decisions

### 决策 1：API 文档生成器补全

**选择**：修改 `cmd/api/docs.go` 和 `cmd/gendocs/main.go`，在 `bootstrap.Handlers` 结构体中注册缺失的 39 个 Handler（Account、AdminOrder、Asset、Authorization 等）

**理由**：
- 这两个文件是唯一的 API 文档入口，所有接口都需在此注册才能出现在 OpenAPI 文档中
- 注册逻辑简单且低风险（仅字段赋值，无业务逻辑变更）
- 满足项目规范："新增 Handler 时必须同步更新文档生成器"

**替代方案考虑**：
- ❌ 自动扫描所有 Handler：增加框架复杂性，不符合 Go 惯用模式（显式优于隐式）
- ❌ 分阶段补全：低优先级接口仍可能被遗漏，最终还是需要一次性全部补全

---

### 决策 2：支付配置动态加载架构

**选择**：在 Service 层新增 `PaymentConfigLoader` 接口和实现，`order/service.go` 和 `recharge/service.go` 中调用此接口从 `payment_config_id` 动态获取对应的支付配置实例

```go
// pkg/payment/loader.go（新增）
type PaymentConfigLoader interface {
    LoadConfig(ctx context.Context, configID uint) (Payment, error)
}

// internal/service/order/service.go（修改）
func (s *OrderService) PayOrder(ctx context.Context, orderID uint) error {
    order := s.store.GetOrder(orderID)
    cfg, err := s.paymentLoader.LoadConfig(ctx, order.PaymentConfigID)  // 动态加载
    return cfg.Verify(order.PaymentData)
}
```

**理由**：
- 解耦支付配置与订单逻辑，支持多商户多配置场景
- 缓存在 Redis 中（key: `payment:config:{configID}`），减少数据库查询
- 若配置不存在或无权限，返回 `errors.New(errors.CodePaymentConfigNotFound)`

**替代方案考虑**：
- ❌ 在 Store 层加载：支付验签是业务逻辑，应在 Service 层处理
- ❌ 继续使用全局单例：无法支持多商户，前期配置可行但长期不可扩展

---

### 决策 3：轮询状态常量提取

**选择**：在 `pkg/constants/polling.go` 中新增轮询日志状态常量，替换 `polling_manual_trigger.go`、`manual_trigger_service.go`、`polling_manual_trigger_store.go`、`alert_service.go` 中的硬编码字符串

```go
// pkg/constants/polling.go（新增）
const (
    // 轮询手动触发日志状态
    PollingManualTriggerStatusPending   = "pending"      // 待处理
    PollingManualTriggerStatusProcessing = "processing"  // 处理中
    PollingManualTriggerStatusCompleted = "completed"    // 已完成
    PollingManualTriggerStatusCancelled = "cancelled"    // 已取消
)
```

**理由**：
- 集中管理状态值，便于全局修改和 IDE 重构
- 保持与轮询系统其他常量（`PollingStatusEnabled` 等）的命名一致性
- 消除硬编码减少代码行数 ~10 行

**替代方案考虑**：
- ❌ 定义 enum 类型：Go 中无原生 enum，定义 type+const 更符合惯用模式
- ❌ 分散定义在各模块：违反"集中在 pkg/constants/"规范

**已知遗留问题（不在本次范围内）**：
轮询手动触发日志的状态字段（`status`）在数据库和代码中均为 `string` 类型，而项目规范要求"状态类（生命周期）用 `int`"。本次清理**仅提取常量，不修改字段类型**，原因：
1. 修改字段类型需要数据库迁移，会产生 API 破坏性变更（现有 JSON 字段值改变）
2. 当前没有并行业务需求推动这个改动
本次提案将此作为已知技术债务记录，待后续版本统一规划。

---

### 决策 4：废弃代码清理

**选择**：
1. **同步模板代码**（`internal/task/sync.go`）：删除整个文件，轮询系统已实现真正的 SIM 卡状态/实名状态/流量同步逻辑
2. **废弃常量别名**（`pkg/constants/wallet.go`）：全局搜索并替换 15 个 `Deprecated` 别名为新常量，然后删除别名定义
3. **废弃 DTO 类型**（3 个）：确认无引用后删除 `DeviceBundle`、`DeviceBundleCard`、`AllocatedDevice`
4. **废弃方法**（2 个）：确认无引用后删除 `CreateLegacy()` 和 `CheckAndStopCard()`

**理由**：
- 废弃代码继续存在会增加代码认知负担，新开发者易误用
- 同步任务虽然是模板，但从未被入队使用，保留无意义
- 这些清理是一次性工作，越早做越好（后续修改越多，冲突风险越大）

**替代方案考虑**：
- ❌ 标注但保留：会持续占用代码审查注意力，推迟问题不是解决方案
- ❌ 分批清理：逐个清理会产生多个 PR，合并前后的重构冲突难以管理

---

### 决策 5：Model 废弃字段清理

**选择**：
1. 从 `iot_card.go` 和 `device.go` 中删除 `FirstCommissionPaid` 和 `AccumulatedRecharge` 字段定义
2. 创建数据库迁移脚本（`000XXX_remove_legacy_commission_fields.up.sql`），删除对应列

**理由**：
- 这两个字段已被 `AccumulatedRechargeBySeriesJSON` 和 `FirstRechargeTriggeredBySeriesJSON` 替代，新逻辑不再维护它们
- 保留在数据库中造成数据冗余和不一致风险（新逻辑更新 BySeriesJSON，旧字段不更新）
- GORM 对比迁移后，Model 与数据库结构保持一致

**替代方案考虑**：
- ❌ 仅删除 Model 定义不删除数据库列：GORM 迁移时会警告字段缺失，容易引发混淆
- ❌ 只删除数据库列不删除 Model 定义：运行时可能触发 scanning 错误

---

### 决策 6：DTO `_name` 字段补全

**选择**：
1. 遍历 `internal/model/dto/` 下所有 DTO 文件，找出所有 int 类型状态字段
2. 为每个状态字段补充 `_name` 或 `_text` 文字字段（命名规则：字段名+`_name`）
3. 在 Service 层使用中间件或钩子函数，自动赋值这些文字字段

示例：
```go
// internal/model/dto/account_dto.go（修改）
type AccountResponse struct {
    ID          uint    `json:"id"`
    Status      int     `json:"status" description:"状态 (0:禁用, 1:启用)"`
    StatusName  string  `json:"status_name" description:"状态名称"`  // 新增
    // ...
}

// internal/service/account/service.go（修改）
func (s *Service) GetAccount(ctx context.Context, id uint) (*dto.AccountResponse, error) {
    account, err := s.store.Get(ctx, id)
    resp := s.toAccountResponse(account)
    resp.StatusName = constants.GetAccountStatusName(resp.Status)  // 赋值
    return resp, nil
}
```

**理由**：
- 规范要求：项目 AGENTS.md 明确规定 "Response DTO 的 int 状态字段必须有 `_name` 字段"
- 无需前端维护枚举映射表，直接使用后端返回的中文文本显示
- 规范 100% 贯彻，新接口强制遵守，存量接口逐步补全

**替代方案考虑**：
- ❌ 统一使用 string 类型状态字段：破坏现有 API 契约，多个客户端需升级
- ❌ 前端维护枚举映射表：维护成本高，易不同步

---

### 决策 7：未使用常量清理

**选择**：
1. 删除 `pkg/errors/codes.go` 中的 `CodeExceedLimit` 错误码及其消息映射
2. 为 `pkg/constants/iot.go` 中 ~22 个未引用的预留常量（Replacement/Merchant/Approval 系列）添加注释说明预留用途，暂不删除

**理由**：
- `CodeExceedLimit` 完全未使用，删除无损失
- iot.go 中的常量可能是为未来功能预留（如设备更换流程、商家管理等），贸然删除可能影响后续规划
- 保留预留常量并标注其用途，便于未来实现相应功能时快速找到

**替代方案考虑**：
- ❌ 全部删除：若未来需要这些常量，重新定义会遇到 Git 历史问题
- ❌ 全部保留不标注：占用代码空间，不清楚预留目的

---

## Risks / Trade-offs

### 风险 1：支付配置动态加载的缓存策略

**风险**：支付配置改变后，Redis 缓存不会立即更新，旧订单可能使用过期配置验签

**缓解**：
- 缓存 TTL 设置为 1 小时（`JUNHONG_PAYMENT_CONFIG_CACHE_TTL=3600`）
- 支付配置变更时主动清除 Redis 缓存（`DELETE payment:config:{configID}`）
- 监控日志记录每次加载的配置 ID 和来源（缓存/DB）

---

### 风险 2：废弃代码全量删除的合并冲突

**风险**：若有并行开发的分支引用被删除的废弃 DTO 或方法，合并时会产生编译错误

**缓解**：
- 清理前通知团队成员，检查是否有进行中的 Feature 分支
- 先提交清理 PR，所有进行中的 Feature 分支在合并前需 rebase main
- 提前进行代码搜索确认零引用，避免删除有隐式依赖的代码

---

### 风险 3：Model 废弃字段删除的数据丢失

**风险**：若有代码仍在写入这些字段（虽已确认无引用），删除数据库列后无法回滚

**缓解**：
- 迁移前导出这些列的数据备份（`SELECT id, first_commission_paid, accumulated_recharge FROM tb_iot_card`）
- 迁移脚本提供回滚版本（.down.sql）
- 在测试环境验证迁移逻辑和回滚步骤

---

### 权衡：API 文档生成器的手动注册 vs 自动扫描

**当前选择**：手动注册（在 docs.go 中列出所有 Handler）

**权衡**：
- ✅ 显式、可控、符合 Go 惯用模式
- ✅ 可灵活控制哪些 Handler 出现在文档中（如隐藏内部接口）
- ❌ 新增 Handler 时需同步更新，易遗漏

**为什么不自动扫描**：
- Go 反射在编译期无法获知（需运行时），不符合项目"显式优于隐式"原则
- 增加框架复杂性，维护成本高

---

### 决策 8：数据库迁移文件合并策略

**选择**：将 000000~000113 归档，生成新的生产基线迁移（000114、000115），编号接续现有序列

**执行步骤**：

```
Step 1: pg_dump --schema-only 生成当前 schema
        → migrations/000114_squash_baseline.up.sql（建表 DDL）
        → migrations/000114_squash_baseline.down.sql（DROP 所有表）

Step 2: 整合数据初始化
        → migrations/000115_init_data.up.sql
          内容：轮询系统初始配置（原 000104 内容）+
                历史订单 purchase_role 回填（原 backfill 脚本，已幂等）
        → migrations/000115_init_data.down.sql
          内容：DELETE 插入的轮询配置行（purchase_role 回填不需要 rollback）

Step 3: Model 废弃字段删除迁移
        → migrations/000116_remove_legacy_commission_fields.up.sql
          DROP COLUMN first_commission_paid, accumulated_recharge（tb_iot_card, tb_device）
        → migrations/000116_remove_legacy_commission_fields.down.sql

Step 4: 归档旧迁移
        → 将 000000~000113 全部移入 migrations/archive/

Step 5: 提供重置脚本
        → scripts/reset_db.sh（DROP DATABASE → CREATE DATABASE → migrate up）
```

**生产部署流程**（首次）：
```bash
migrate -path migrations -database "$DB_DSN" up
# 执行 000114 → 000115 → 000116，完成建库 + 数据初始化
```

**测试环境重置**：
```bash
./scripts/reset_db.sh  # 清空后重建，等价于全新生产部署
```

**理由**：
- 无生产历史数据，squash 没有历史包袱
- 000114 + 000115 完全等价于顺序执行 000000~000113 的最终结果
- 消除 114 步执行链的失败风险，部署过程更可控
- `backfill_order_purchase_role.sql` 已确认执行过（测试环境 5 条记录有 purchase_role），合并进 000115 后幂等安全

**替代方案考虑**：
- ❌ 保留全部 114 个迁移：生产首次部署需顺序执行 114 步，中途失败难以排查
- ❌ 从编号 000001 重新开始：测试环境会产生混淆（历史 PR 记录编号冲突）

---

## Migration Plan

### 第一阶段：准备（半天）

1. 通知团队成员，清理方案确认
2. 备份测试环境数据库（快照或 pg_dump）
3. 确认所有功能分支代码已 merge 或暂存

### 第二阶段：实现（4-5 天）

1. **第 1 天**：迁移文件合并（000114 + 000115 + 000116）+ 测试环境重置验证
2. **第 2 天**：API 文档补全 + 支付配置动态加载
3. **第 3 天**：轮询状态常量提取 + 废弃代码清理（别名、DTO、方法、空文件）
4. **第 4 天**：Model 废弃字段清理 + DTO `_name` 字段批量补全
5. **第 5 天**：未使用常量清理 + 全量编译测试 + 代码审查

### 第三阶段：上线准备（1 天）

1. 在纯净环境验证 `migrate up` 从 0 跑到底（000114 → 000115 → 000116）
2. 验证所有接口正常（特别是支付、轮询、账号管理）
3. 部署到生产环境

### 回滚策略

- **代码**：git revert（无 API 破坏性变更）
- **数据库 000114**：执行 `.down.sql` 删除所有表（仅在新环境适用，旧数据无法恢复）
- **数据库 000116**：执行 `.down.sql` 恢复废弃列（列结构恢复，历史数据值已备份）

---

## Open Questions

1. **支付配置 Redis 缓存 TTL**：现拟设置为 1 小时，是否有其他考虑？
2. **DTO `_name` 字段赋值时机**：是否需要在 Service 层统一处理，还是允许各模块自行处理？
3. **轮询日志状态是否需要 API 端点查询**：当前只在日志中记录，是否需要开放查询接口？
4. **实名认证检查的业务意图**（`client_order/service.go:141`）：注释掉的原因是什么，是暂时关闭还是永久删除？
