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

16 KiB
Raw Blame History

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.gocmd/gendocs/main.go,在 bootstrap.Handlers 结构体中注册缺失的 39 个 HandlerAccount、AdminOrder、Asset、Authorization 等)

理由

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

替代方案考虑

  • 自动扫描所有 Handler增加框架复杂性不符合 Go 惯用模式(显式优于隐式)
  • 分阶段补全:低优先级接口仍可能被遗漏,最终还是需要一次性全部补全

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

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

// 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.gomanual_trigger_service.gopolling_manual_trigger_store.goalert_service.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 个):确认无引用后删除 DeviceBundleDeviceBundleCardAllocatedDevice
  4. 废弃方法2 个):确认无引用后删除 CreateLegacy()CheckAndStopCard()

理由

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

替代方案考虑

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

决策 5Model 废弃字段清理

选择

  1. iot_card.godevice.go 中删除 FirstCommissionPaidAccumulatedRecharge 字段定义
  2. 创建数据库迁移脚本(000XXX_remove_legacy_commission_fields.up.sql),删除对应列

理由

  • 这两个字段已被 AccumulatedRechargeBySeriesJSONFirstRechargeTriggeredBySeriesJSON 替代,新逻辑不再维护它们
  • 保留在数据库中造成数据冗余和不一致风险(新逻辑更新 BySeriesJSON旧字段不更新
  • GORM 对比迁移后Model 与数据库结构保持一致

替代方案考虑

  • 仅删除 Model 定义不删除数据库列GORM 迁移时会警告字段缺失,容易引发混淆
  • 只删除数据库列不删除 Model 定义:运行时可能触发 scanning 错误

决策 6DTO _name 字段补全

选择

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

示例:

// 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

生产部署流程(首次):

migrate -path migrations -database "$DB_DSN" up
# 执行 000114 → 000115 → 000116完成建库 + 数据初始化

测试环境重置

./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):注释掉的原因是什么,是暂时关闭还是永久删除?