# tech-debt-cleanup 学习记录

## [2026-04-14] 初始化

### 项目规范关键点
- 所有注释必须使用中文
- Redis Key 必须通过函数生成（格式：`Redis{Module}{Purpose}Key(params...)`）
- 错误码必须定义在 `pkg/errors/codes.go`
- 常量定义在 `pkg/constants/` 
- 架构：Handler → Service → Store → Model
- 禁止使用外键约束
- 禁止 Go 测试文件（*_test.go）

### 关键文件路径
- 支付服务: `internal/service/order/service.go`, `internal/service/recharge/service.go`
- 轮询服务: `internal/service/polling/manual_trigger_service.go`, `alert_service.go`
- 轮询 Handler: `internal/handler/admin/polling_manual_trigger.go`
- 轮询 Store: `internal/store/postgres/polling_manual_trigger_store.go`
- 文档生成: `cmd/api/docs.go`, `cmd/gendocs/main.go`
- Redis Key: `pkg/constants/redis.go`
- 支付包: `pkg/payment/`
- 废弃 DTO: `internal/model/dto/enterprise_card_authorization_dto.go`
- 废弃任务: `internal/task/sync.go`
- 空文件: `internal/routes/recharge.go`
- Model: `internal/model/iot_card.go`, `internal/model/device.go`

## [2026-04-14] Task 3: 轮询状态常量提取

### 发现和注意点

1. **alert_service.go 的 "pending" 只有 1 处**：`NotificationStatus: "pending"` 在 `triggerAlert()` 函数中，其他字符串如 `"sent"`、`"partial"`、`"failed"` 是通知发送状态（非轮询触发日志状态），不在替换范围内。

2. **Store 层 SQL 字符串处理**：`polling_manual_trigger_store.go` 中 `GetRunning` 函数原本使用 SQL 字面量 `status IN ('pending', 'processing')`，改为 GORM 参数化查询 `status IN (?, ?)` 并传入常量，既消除硬编码又提升安全性。

3. **Store 层需要新增 import**：`polling_manual_trigger_store.go` 原本未导入 `constants` 包，添加时注意 Go import 分组规范（stdlib / 第三方 / 内部包）。

4. **constants 包在 handler 文件已存在**：`polling_manual_trigger.go` 已导入 `pkg/constants`，无需额外修改 import。

5. **manual_trigger_service.go 共 6 处替换**：3 处 `"processing"`（triggerLog 创建）、2 处 `"completed"`（UpdateStatus 调用）、1 处同时含 `"pending"` + `"processing"` + `"cancelled"` 的 CancelTrigger 状态检查。

## [2026-04-14] Task 4: 废弃代码清理

### 废弃别名实际引用情况
扫描 `internal/` 和 `pkg/` 后，真正使用废弃别名的只有 3 处（其他文件早已用新常量）：
- `shop_commission/service.go:645` → `TransactionTypeWithdrawal` 替换为 `AgentTransactionTypeWithdrawal`
- `recharge/service.go:91,94,417,418` → `RechargeMinAmount/RechargeMaxAmount` 替换为 `AssetRechargeMinAmount/AssetRechargeMaxAmount`（资产钱包充值场景，最小1元/最大100000元）

### CheckAndStopCard 无法删除
`stop_resume_service.go` 中的 `CheckAndStopCard` 在接口 `StopResumeServiceInterface`（`usage_service.go:20`）中声明，并在 `package_activation_handler.go`（327、347行）和 `usage_service.go`（230、255行）中活跃调用。任务描述要求"确认无调用后再删除"，此方法有调用，故跳过删除。

### grep 验证注意事项
- `SyncHandler` 的 grep 命中了 `CommissionStatsSyncHandler`（合法），不是废弃的 DataSync handler。
- `CardWallet` 出现在 `data_scope.go:82` 的注释字符串里（非代码引用），不需要修改。

### 删除的代码统计
- `sync.go` 整个文件（166行）
- `handler.go` 3行（syncHandler var + HandleFunc + logger.Info）
- `constants.go` 1行（TaskTypeDataSync）
- `wallet.go` 86行（149-234行废弃别名块）
- `enterprise_card_authorization_dto.go` 22行（DeviceBundle、DeviceBundleCard、AllocatedDevice）
- `order/service.go` 约243行（CreateLegacy 方法）

## [2026-04-14] Task 7+8: 常量清理和空文件删除

### 执行内容

**Task 7.1：删除 CodeExceedLimit 错误码**
- 删除常量定义：`CodeExceedLimit = 1055` (line 68)
- 删除 allErrorCodes 列表条目 (line 227)
- 删除 errorMessages 映射条目 (line 359)
- 验证：grep 确认代码库中无任何引用

**Task 7.2：为预留常量添加 [预留] 注释**
- LadderType 系列（激活量、提货量、充值量）：添加 `[预留] 用于分佣阶梯功能，待产品规划`
- ApprovalType/ApprovalStatus 系列（审批类型/状态）：添加 `[预留] 用于审批流程功能，待产品规划`
- MerchantType 系列（支付宝、微信、银行卡）：添加 `[预留] 用于未来商户管理功能，待产品规划`
- ReplacementStatus 系列（换卡申请状态）：添加 `[预留] 用于换卡申请功能，待产品规划`
- ReplacementReason 系列（换货原因）：添加 `[预留] 用于换卡原因管理功能，待产品规划`

**Task 8.1：删除空文件**
- 删除 `internal/routes/recharge.go`（仅包含 `package routes`）

**Task 8.2：删除 postgres.go 的 AutoMigrate 注释块**
- 删除 lines 90-95 的注释掉的 AutoMigrate 代码块

**Task 8.3：更新 client_order/service.go 注释**
- 在 line 140 的实名认证检查注释前添加：`// [待确认] 业务是否需要实名认证检查？参见 tech-debt-cleanup 提案`

### 验证结果
- ✅ `go build ./cmd/api ./cmd/worker` 通过
- ✅ `go vet ./pkg/errors/ ./pkg/constants/ ./pkg/database/` 无报告
- ✅ `grep -r "CodeExceedLimit"` 无结果（完全删除）
- ✅ 所有预留常量已添加 [预留] 标记和产品规划说明

## [2026-04-14] Task 2: API 文档生成器补全

### 发现

- `BuildDocHandlers()` 缺少 3 个 Handler：`ClientAuth`、`AdminAuth`、`AssetLifecycle`，它们在 `bootstrap/types.go` 中有定义，但从未加入文档生成器
- `AdminAuth` 在 `docs.go` 和 `gendocs/main.go` 中**完全未被设置**（连手动覆写都没有），这意味着 Admin Auth 相关路由一直缺失于 OpenAPI 文档
- `docs.go` 和 `gendocs/main.go` 的手动覆写列表（9 行 `handlers.Xxx = ...`）中，大多数 handler 其实已经在 `BuildDocHandlers()` 中了，属于纯冗余

### 操作

1. `pkg/openapi/handlers.go`：按 types.go 字段顺序插入 `ClientAuth`（PersonalCustomer 后）、`AdminAuth`（ShopRole 后）、`AssetLifecycle`（Asset 后）
2. `cmd/api/docs.go`：删除所有手动覆写行（9 行）+ 移除 `admin` 和 `apphandler` import
3. `cmd/gendocs/main.go`：同上

### 注意点

- `admin.NewAuthHandler` 需要 2 个 nil 参数（authService + validator）
- `app.NewClientAuthHandler` 需要 2 个 nil 参数（service + logger）
- `admin.NewAssetLifecycleHandler` 只需 1 个 nil 参数（service 接口）
- 清理完 import 后 `go build` + `go vet` 均通过，无报错

## [2026-04-14] Task 5: Model 废弃字段清理

### 引用情况（超出预期的额外文件）

计划中列出的文件之外，还发现了额外引用：
- `internal/service/device/service.go` line 576-577：`DeviceResponse` 构建中有引用（计划中未提及）
- `internal/service/recharge/service.go` line 31：`ForceRechargeRequirement` struct 有 `FirstCommissionPaid` 字段
- `internal/service/recharge/service.go` line 458：`result.FirstCommissionPaid = firstCommissionPaid` 赋值

### 关键判断：recharge/service.go 的 firstCommissionPaid 变量

`checkForceRechargeRequirement` 中的 `firstCommissionPaid` 变量**不是废弃字段**：
- 它读自 `card.IsFirstRechargeTriggeredBySeries(*seriesID)` 和 `device.IsFirstRechargeTriggeredBySeries(*seriesID)`（均是新 BySeries 方法）
- 该变量用于 `if firstCommissionPaid {` 的业务判断（已发放则跳过强充检查）
- 正确做法：只删 `ForceRechargeRequirement.FirstCommissionPaid` 字段 + `result.FirstCommissionPaid = firstCommissionPaid` 赋值，保留变量声明和 if 判断

### 迁移文件
- 新建 `migrations/000116_remove_legacy_commission_fields.up.sql`
- 新建 `migrations/000116_remove_legacy_commission_fields.down.sql`

### 验证结果
- ✅ `go build ./cmd/api ./cmd/worker` 通过，无任何错误

## [2026-04-14] Task 1: 支付配置动态加载

### 实现架构决策

#### 1. PaymentConfigLoader 位置
- 新建 `pkg/payment/loader.go`（包名 `payment`）
- `pkg/` 下可以 import `internal/` 包（项目中已有先例：`pkg/wechat/config.go` import `internal/model`）

#### 2. 缓存策略
- 不缓存 Payment 实例（含连接/状态，不可序列化）
- 缓存 **WechatConfig JSON 数据**（key: `payment:config:{id}`，TTL: 1 小时）
- 每次从缓存数据构建 Payment 实例（轻量操作，无 I/O）

#### 3. v2 适配器模式
- `PaymentV2Service` 未实现完整 `PaymentServiceInterface`（缺少 H5/查单/关单/HandlePaymentNotify）
- 通过 `v2PaymentAdapter` 包装，补全缺失方法（返回"不支持"错误）
- **不修改** `payment_v2.go` 原文件

#### 4. `appID` 来源
- `NewPaymentAppFromConfig` / `NewPaymentV2ServiceFromConfig` 均需要 `appID` 参数
- 使用 `wechatConfig.OaAppID`（公众号 AppID）

#### 5. wechatCache 共享
- loader 在构造时通过 `wechat.NewRedisCache(rdb)` 创建一次，所有 LoadConfig 调用复用
- v3 Payment 实例构建需要此 cache（用于 PowerWeChat SDK 内部 token 缓存）

#### 6. worker_services.go 也需更新
- `internal/bootstrap/worker_services.go` 里有另一处 `orderSvc.New` 调用（超时取消专用）
- 需额外传 `nil` 占位

### 关键文件变更
| 文件 | 变更内容 |
|------|---------|
| `pkg/constants/redis.go` | +`RedisPaymentConfigKey` |
| `pkg/payment/loader.go` | 新建：接口 + 实现 + v2 适配器 |
| `internal/service/order/service.go` | +`paymentLoader` 字段/参数，替换 2 处 TODO |
| `internal/service/recharge/service.go` | +`paymentLoader` 字段/参数，更新 1 处 TODO 注释 |
| `internal/bootstrap/services.go` | 创建 loader 实例，传入 Order/Recharge 构造函数 |
| `internal/bootstrap/worker_services.go` | `orderSvc.New` 补传 `nil` |

### 验证结果
- ✅ `go build ./cmd/api ./cmd/worker` 通过，无任何错误
- ✅ `go vet` 无报告

## [2026-04-14] Task 6: DTO _name 字段补全

### 概述
为所有 Response DTO 中的 `int` 类型状态字段补全对应 `string` 类型 `_name` 字段，并在 Service 层赋值。

### 常量映射函数（新增）

**`pkg/constants/constants.go`：**
- `GetStatusName(status int) string` — 通用 0=禁用/1=启用
- `GetShelfStatusName(status int) string` — 1=上架/2=下架
- `GetExchangeStatusName(status int) string` — 换货单状态

**`pkg/constants/iot.go`：**
- `GetIotCardStatusName` — 1=在库, 2=已分销, 3=已激活, 4=已停用
- `GetActivationStatusName` — 0=未激活, 1=已激活
- `GetRealNameStatusName` — 0=未实名, 1=已实名
- `GetNetworkStatusName` — 0=停机, 1=开机
- `GetCommissionRecordStatusName` — 1=已冻结/2=解冻中/3=已发放/4=已失效/99=待人工修正
- `GetOrderPaymentStatusName` — 1=待支付/2=已支付/3=已取消/4=已退款
- `GetOrderCommissionStatusName` — 1=待计算/2=已计算
- `GetRefundStatusName` — 1=待审批/2=已通过/3=已拒绝/4=已退回

### 模块处理情况

| 模块 | DTO 改动 | Service 赋值位置 | 备注 |
|------|---------|-----------------|------|
| Account | `AccountResponse` + `status_name` | `toAccountResponse()` | constants 已导入 |
| Shop | `ShopResponse` + `status_name` | Create/Update/ListShopResponses（3处）| constants 已导入 |
| IotCard | `StandaloneIotCardResponse` + 4个 `_name` 字段 | `toStandaloneResponse()` | status/activation/realname/network |
| Order | `OrderResponse` + `commission_status_name` | `buildOrderResponse()` | payment_status 已有 `_text` 字段 |
| Package | `PackageResponse` + `status_name`/`shelf_status_name` | `toResponse()` 末尾（agent 覆盖后赋值） | my_package.go 也同步更新（不涉及 service）|
| Refund | `RefundResponse` + `status_name` | `buildRefundResponse()` | constants 已导入 |
| Device | `DeviceCardBindingResponse` + `status_name` | `device/binding.go` | 新增 constants import |
| Asset | `AssetResolveResponse` + `status_name` | `buildDeviceResolveResponse` + `buildCardResolveResponse` | constants 已导入 |
| Commission | `CommissionRecordResponse` + `status_name` | 未找到 service 调用（DTO 未被使用，ShopCommissionRecordItem 已有 status_name）| 仅 DTO 更新；同时修正了 description 错误 |
| Client Asset | `AssetInfoResponse` + 4个 `_name` 字段 | handler `client_asset.go`（直接赋值）| constants 已导入 |
| Client Order | `ClientOrderInfo/ListItem/DetailResponse` + `payment_status_name`; `ClientRechargeInfo` + `status_name` | `client_order/service.go` | 增加 `clientRechargeStatusName` 辅助函数 |
| MyPackage | `MyPackageResponse/DetailResponse` + `status_name`/`shelf_status_name`; `MySeriesAllocationResponse` + `status_name` | 未找到 service 调用，仅 DTO 更新 | |

### 关键发现
1. `CommissionRecordResponse.status` description 原先错误（写的 "1:已入账" 但常量是 "1:已冻结"），已修正为正确值
2. `ClientRechargeInfo.Status` 使用的是 `rechargeStatusToClientStatus()` 转换后的值（0/1/2），与 RechargeStatus* 常量不同
3. 部分 DTO（MyPackage、CommissionRecord）未被 service 层引用，仅更新 DTO 定义
4. `DeviceResponse` 和 `ImportTaskResponse` 已经有 `status_name` 和 `status_text` 字段，无需修改

### 验证结果
- ✅ `go build ./cmd/api ./cmd/worker` 通过，无任何错误
