feat: 技术债务清理(支付配置动态化、API文档补全、轮询常量提取、废弃代码清理)
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m13s

This commit is contained in:
2026-04-14 11:11:15 +08:00
parent c0b64c9e30
commit 42c5ec912f
63 changed files with 1979 additions and 775 deletions

View File

@@ -0,0 +1,340 @@
## 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
### 决策 1API 文档生成器补全
**选择**:修改 `cmd/api/docs.go``cmd/gendocs/main.go`,在 `bootstrap.Handlers` 结构体中注册缺失的 39 个 HandlerAccount、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合并前后的重构冲突难以管理
---
### 决策 5Model 废弃字段清理
**选择**
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 错误
---
### 决策 6DTO `_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
- 提前进行代码搜索确认零引用,避免删除有隐式依赖的代码
---
### 风险 3Model 废弃字段删除的数据丢失
**风险**:若有代码仍在写入这些字段(虽已确认无引用),删除数据库列后无法回滚
**缓解**
- 迁移前导出这些列的数据备份(`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.sqlDROP 所有表)
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_rechargetb_iot_card, tb_device
→ migrations/000116_remove_legacy_commission_fields.down.sql
Step 4: 归档旧迁移
→ 将 000000~000113 全部移入 migrations/archive/
Step 5: 提供重置脚本
→ scripts/reset_db.shDROP 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`):注释掉的原因是什么,是暂时关闭还是永久删除?