Files
junhong_cmp_fiber/openspec/changes/tech-debt-cleanup/design.md
huang 42c5ec912f
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m13s
feat: 技术债务清理(支付配置动态化、API文档补全、轮询常量提取、废弃代码清理)
2026-04-14 11:11:15 +08:00

341 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
## 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`):注释掉的原因是什么,是暂时关闭还是永久删除?