Files
junhong_cmp_fiber/.planning/phases/01-p0/01-VERIFICATION.md

235 lines
17 KiB
Markdown
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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.
---
phase: 01-p0
verified: 2026-03-28T00:00:00+08:00
status: passed
score: 5/5 must-haves verified
re_verification: false
---
# Phase 1: P0 紧急修复 Verification Report
**Phase Goal:** 修复所有已知的主链路断点,使"充值→自动购包→佣金计算→实名激活"的完整业务链路可端到端跑通,消除数据写入混乱(实名状态常量)和并发安全漏洞(提现重复创建)
**Verified:** 2026-03-28
**Status:** ✅ PASSED
**Re-verification:** No — initial verification
---
## Goal Achievement
### Observable Truths (来自 ROADMAP Success Criteria)
| # | Truth | Status | Evidence |
|---|-------|--------|----------|
| 1 | C 端用户完成实名后,实名状态正确写入 DB值为常量 1不再是 2实名激活任务正确触发 | ✓ VERIFIED | `parseRealnameStatus` 返回 `constants.RealNameStatusVerified(=1)``isFirstRealname` 使用常量比较;`triggerFirstRealnameActivation` 使用 `asynqClient.EnqueueContext` 正确入队 |
| 2 | C 端用户钱包充值成功后,自动购包任务入队并被消费,佣金计算任务被正确触发 | ✓ VERIFIED | `HandlePaymentCallback``LinkedPackageIDs` 非空时调用 `queueClient.EnqueueTask(TaskTypeAutoPurchaseAfterRecharge)``AutoPurchaseHandler` 已注册;`ProcessTask` 事务外触发 `TaskTypeCommission` 入队 |
| 3 | 代理代购下级场景(场景 5的订单金额正确使用 operatorCostPrice佣金链计算正常 | ✓ VERIFIED | `sellerCostPrice = operatorCostPrice` 出现 3 次(普通/后台/H5 三个下单函数,各一处),三处均已修正 |
| 4 | 批量导入设备 Excel 后,设备记录中含 IMEI 字段Gateway 调用不再因 IMEI 缺失而失败 | ✓ VERIFIED | `DeviceRow.IMEI` 字段存在;`buildDeviceColumnIndex` 支持 `imei/设备imei/imei号` 多别名;`device_import.go` 填充 `IMEI: row.IMEI` |
| 5 | 并发提交提现申请时RowsAffected 检查生效,不会创建重复提现单 | ✓ VERIFIED | `result.RowsAffected == 0` 检查存在,返回 `errors.New(errors.CodeInsufficientBalance, ...)` |
**Score:** 5/5 truths verified
---
## Required Artifacts
### Plan 01 (CRITICAL-01/02)
| Artifact | Expected | Status | Details |
|----------|----------|--------|---------|
| `internal/task/polling_handler.go` | `parseRealnameStatus` 返回常量值,`isFirstRealname` 使用常量 | ✓ VERIFIED | 行 702: `return constants.RealNameStatusVerified`;行 704: `return constants.RealNameStatusNotVerified`;行 173-174: `isFirstRealname` 使用常量 |
| `internal/model/iot_card.go` | Model 字段注释统一 `0-未实名 1-已实名` | ✓ VERIFIED | 行 30: `comment:实名状态 0-未实名 1-已实名` |
| `internal/service/client_order/service.go` | 实名校验改为常量比较 | ✓ VERIFIED | 行 115: `assetInfo.RealNameStatus != constants.RealNameStatusVerified` |
**额外修复(超出 PLAN 范围SUMMARY 已记录):**
| Artifact | Status | Details |
|----------|--------|---------|
| `internal/model/dto/asset_dto.go` | ✓ VERIFIED | DTO description 更新为 `0未实名 1已实名`(旧三态 `1实名中 2已实名` 已消除) |
| `internal/polling/scheduler.go` | ✓ VERIFIED | `getCardCondition` 改为 `!= constants.RealNameStatusVerified`(旧硬编码 0/1/2 三态) |
| `internal/service/iot_card/service.go` | ✓ VERIFIED | `parseGatewayRealnameStatus` 返回 `constants.RealNameStatusVerified`(旧 `return 2` 已消除) |
| `internal/handler/app/client_realname.go` | ✓ VERIFIED | 行 128: `== constants.RealNameStatusVerified`(旧 `== 1` 已消除) |
### Plan 02 (CRITICAL-03)
| Artifact | Expected | Status | Details |
|----------|----------|--------|---------|
| `internal/task/polling_handler.go` | `PollingHandler` 注入 `asynq.Client``triggerFirstRealnameActivation` 正确入队 | ✓ VERIFIED | 行 33: `asynqClient *asynq.Client` 字段;行 1103: `h.asynqClient.EnqueueContext(ctx, task)` |
| `pkg/queue/handler.go` | `registerPollingHandlers` 传入 `h.asynqClient` | ✓ VERIFIED | 行 155: `h.asynqClient` 作为第三参数传入 `NewPollingHandler` |
### Plan 03 (CRITICAL-04/05)
| Artifact | Expected | Status | Details |
|----------|----------|--------|---------|
| `internal/service/recharge/service.go` | `queueClient` 注入,`HandlePaymentCallback` 触发自动购包 | ✓ VERIFIED | 行 52: `queueClient *queue.Client` 字段;行 394-404: `EnqueueTask` 触发自动购包 |
| `pkg/queue/handler.go` | `registerAutoPurchaseHandler` 方法和注册调用 | ✓ VERIFIED | 行 74: `h.registerAutoPurchaseHandler()`;行 208-221: `HandleFunc` 注册 |
| `internal/task/auto_purchase.go` | `asynqClient` 注入,`ProcessTask` 触发佣金 | ✓ VERIFIED | 行 34: `asynqClient *asynq.Client` 字段;行 204-218: 事务外 `EnqueueContext(TaskTypeCommission)` |
| `internal/bootstrap/services.go` | `recharge.New()` 传入 `queueClient` | ✓ VERIFIED | 行 173: `rechargeSvc.New(..., deps.QueueClient)` |
### Plan 04 (CRITICAL-06)
| Artifact | Expected | Status | Details |
|----------|----------|--------|---------|
| `internal/service/order/service.go` | 场景 5 `sellerCostPrice` 修正 | ✓ VERIFIED | 行 264, 551, 813: 三处 `sellerCostPrice = operatorCostPrice`(三个下单函数各一处) |
### Plan 05 (CRITICAL-07/08)
| Artifact | Expected | Status | Details |
|----------|----------|--------|---------|
| `pkg/utils/excel.go` | `DeviceRow``IMEI` 字段,`ParseDeviceExcel` 列名映射新增 IMEI | ✓ VERIFIED | 行 38: `IMEI string` 字段;行 324-350: `buildDeviceColumnIndex` 支持 `imei/设备imei/imei号` |
| `internal/task/device_import.go` | 导入时填充 `IMEI` 字段 | ✓ VERIFIED | 行 267: `IMEI: row.IMEI` |
| `internal/service/my_commission/service.go` | `RowsAffected` 校验,并发冻结失败返回错误 | ✓ VERIFIED | 行 162-174: `result := tx...Updates()``result.Error``result.RowsAffected == 0` 双重检查 |
---
## Key Link Verification
| From | To | Via | Status | Details |
|------|-----|-----|--------|---------|
| `internal/task/polling_handler.go` | `pkg/constants/iot.go` | `constants.RealNameStatusVerified` | ✓ WIRED | 行 702, 704, 173-174, 986 均使用常量 |
| `internal/service/client_order/service.go` | `pkg/constants/iot.go` | `constants.RealNameStatusVerified` | ✓ WIRED | 行 115 使用常量 |
| `pkg/queue/handler.go` | `internal/task/polling_handler.go` | `NewPollingHandler(h.db, h.redis, h.asynqClient, ...)` | ✓ WIRED | 行 152-157 传入 `h.asynqClient` |
| `internal/task/polling_handler.go` | `asynq.Client` | `h.asynqClient.EnqueueContext(ctx, task)` | ✓ WIRED | 行 1103 |
| `internal/service/recharge/service.go` | `pkg/queue/handler.go` | `queueClient.EnqueueTask(TaskTypeAutoPurchaseAfterRecharge)` | ✓ WIRED | 行 394-404 入队;`pkg/queue/handler.go` 行 74/208-221 注册消费 |
| `internal/task/auto_purchase.go` | `asynq.Client` | `h.asynqClient.EnqueueContext(ctx, commissionTask)` | ✓ WIRED | 行 215事务外触发 |
| `internal/bootstrap/services.go` | `internal/service/recharge/service.go` | `rechargeSvc.New(..., deps.QueueClient)` | ✓ WIRED | 行 173 |
| `pkg/utils/excel.go` | `internal/task/device_import.go` | `DeviceRow.IMEI → model.Device.IMEI` | ✓ WIRED | `device_import.go` 行 267 填充 |
| `internal/service/my_commission/service.go` | `model.AgentWallet` | `result.RowsAffected == 0 → 返回余额不足错误` | ✓ WIRED | 行 162-174 |
---
## Data-Flow Trace (Level 4)
所有 Phase 1 修复均为业务逻辑链路修复非前端渲染组件Level 4 关注任务链路数据流的完整性。
| 链路段 | 数据变量 | 数据源 | 产生真实数据 | Status |
|--------|----------|--------|------------|--------|
| 实名状态写入 | `newRealnameStatus` | Gateway 返回 `RealStatus bool``parseRealnameStatus()` | ✓ 非静态(依赖真实 Gateway 响应) | ✓ FLOWING |
| 实名激活任务入队 | `task payload (cardID, packageUsageIDs)` | `triggerFirstRealnameActivation` 从 DB 查询真实套餐 | ✓ 非空(前置条件:待激活套餐存在) | ✓ FLOWING |
| 自动购包入队 | `AutoPurchasePayload{RechargeRecordID}` | `recharge.LinkedPackageIDs` 来自 DB 充值记录 | ✓ 非静态 | ✓ FLOWING |
| 自动购包消费 | `createdOrderID` | 事务内 `tx.Create(order)` 生成真实订单 | ✓ DB 写入 | ✓ FLOWING |
| 佣金任务入队 | `{order_id: createdOrderID}` | 事务提交后 `createdOrderID > 0` 才触发 | ✓ 有效订单 ID | ✓ FLOWING |
| sellerCostPrice | `operatorCostPrice` | `getCostPrice(ctx, operatorShopID, ...)` 查 DB | ✓ 非静态(真实成本价) | ✓ FLOWING |
| IMEI 写入 | `DeviceRow.IMEI` | Excel 文件列解析 | ✓ 非静态(依赖 Excel 数据) | ✓ FLOWING |
| 提现冻结 | `result.RowsAffected` | GORM 条件更新返回影响行数 | ✓ 数据库真实返回 | ✓ FLOWING |
---
## Behavioral Spot-Checks
| Behavior | Command | Result | Status |
|----------|---------|--------|--------|
| 编译通过(无错误) | `go build ./...` | 无输出,无错误 | ✓ PASS |
| `parseRealnameStatus(true)` 不再返回 2 | `grep "return 2" internal/task/polling_handler.go` | 无匹配 | ✓ PASS |
| 无遗留实名硬编码 2 | `grep -rn "RealNameStatus == 2\|newRealnameStatus == 2\|return 2" internal/` | `client_order/service.go``return 2``PaymentStatusCancelled`(与实名无关,见 `orderStatusToClientStatus` 函数) | ✓ PASS |
| `_ = task` 已删除 | `grep "_ = task" internal/task/polling_handler.go` | 无匹配 | ✓ PASS |
| `registerAutoPurchaseHandler` 已注册 | `grep "registerAutoPurchaseHandler" pkg/queue/handler.go` | 行 74: 在 `RegisterHandlers` 中调用;行 208: 方法定义 | ✓ PASS |
| `sellerCostPrice = operatorCostPrice` 出现 3 次 | `grep -n "sellerCostPrice = operatorCostPrice" internal/service/order/service.go` | 行 264, 551, 813 — 共 3 处 | ✓ PASS |
| `result.RowsAffected == 0` 校验存在 | `grep "RowsAffected" internal/service/my_commission/service.go` | 行 173: `if result.RowsAffected == 0` | ✓ PASS |
| `IMEI: row.IMEI` 存在 | `grep "IMEI: row.IMEI" internal/task/device_import.go` | 行 267 | ✓ PASS |
| 所有 commits 存在 | `git log --oneline 1fd0072 da50a35 20b4b6d 9a4d87a 030425b f74b7da 809cb2b db16680` | 全部 8 个 commit 存在 | ✓ PASS |
---
## Requirements Coverage
| Requirement | Source Plan | Description | Status | Evidence |
|-------------|------------|-------------|--------|----------|
| CRITICAL-01 | 01-01-PLAN.md | 实名状态常量统一(`parseRealnameStatus` 写入 1Model 注释统一) | ✓ SATISFIED | `polling_handler.go` 行 702-704 使用常量;`iot_card.go` 行 30 注释正确 |
| CRITICAL-02 | 01-01-PLAN.md | C 端实名校验修复(`client_order/service.go` 改用常量) | ✓ SATISFIED | `client_order/service.go` 行 115 使用 `constants.RealNameStatusVerified` |
| CRITICAL-03 | 01-02-PLAN.md | 实名激活任务断链修复(`PollingHandler` 注入 `asynq.Client`,删除 RPush 降级) | ✓ SATISFIED | `PollingHandler``asynqClient` 字段;`EnqueueContext` 调用存在RPush 降级和 `_ = task` 已消除 |
| CRITICAL-04 | 01-03-PLAN.md | 充值回调后自动购包触发(`recharge/service.go` 入队 `TaskTypeAutoPurchaseAfterRecharge`;注册 `AutoPurchaseHandler` | ✓ SATISFIED | `recharge/service.go` 行 394-404 入队;`pkg/queue/handler.go` 行 208-221 注册 |
| CRITICAL-05 | 01-03-PLAN.md | 自动购包佣金链补全(`AutoPurchaseHandler.ProcessTask` 事务提交成功后入队 `TaskTypeCommission` | ✓ SATISFIED | `auto_purchase.go` 行 204-218 事务外入队佣金任务 |
| CRITICAL-06 | 01-04-PLAN.md | 代购订单金额修正(场景 5 `sellerCostPrice` 使用 `operatorCostPrice` | ✓ SATISFIED | `order/service.go` 行 264, 551, 813 三处均已修正 |
| CRITICAL-07 | 01-05-PLAN.md | 设备导入补充 IMEI 字段 | ✓ SATISFIED | `excel.go` 行 38 字段定义,行 342-349 列名映射;`device_import.go` 行 267 填充 |
| CRITICAL-08 | 01-05-PLAN.md | 提现冻结并发校验(`RowsAffected == 0` 检查) | ✓ SATISFIED | `my_commission/service.go` 行 162-174 双重校验 |
**所有 8 个 CRITICAL 需求均已满足REQUIREMENTS.md 中已标记为 `[x]` 完成状态。**
**孤立需求检查:** REQUIREMENTS.md 中 Phase 1 映射的需求仅为 CRITICAL-01 ~ CRITICAL-08与 PLANs 声明完全一致,无孤立需求。
---
## Anti-Patterns Found
| File | Line | Pattern | Severity | Impact |
|------|------|---------|----------|--------|
| `internal/service/recharge/service.go` | 277 | `TODO: 按 payment_config_id 加载配置验签(当前留桩,验签由外层处理)` | Info | 预存 TODO属于 PAY 系列Phase 4不影响 Phase 1 目标 |
| `internal/service/order/service.go` | 2360 | `TODO: 实现富友支付发起逻辑` | Info | 预存 TODO属于 PAY-01Phase 4不影响 Phase 1 目标 |
| `internal/service/order/service.go` | 2366 | `TODO: 实现富友小程序支付发起逻辑` | Info | 预存 TODO属于 PAY-02Phase 4不影响 Phase 1 目标 |
> **注意:** `internal/service/client_order/service.go` 行 659/672 的 `return 2` 及 `internal/handler/app/client_order.go` 行 383 的 `return 2` 均属于 `orderStatusToClientStatus` 函数中 `PaymentStatusCancelled` 的映射,与实名状态常量无关,**不是 CRITICAL-01 遗留问题**。
**无阻塞性 Anti-Pattern。**
---
## Human Verification Required
### 1. 实名激活端到端验证
**Test:** 模拟 IoT 卡实名成功回调触发轮询后Gateway 返回 `RealStatus=true`
**Expected:** `tb_iot_card.real_name_status = 1`(不是 2Asynq 队列中出现 `task:package:first_activation` 任务;待激活套餐(`pending_realname_activation=true`)被处理
**Why human:** 需要真实 Gateway 连接或 Mock 环境才能端到端验证
### 2. 自动购包完整链路验证
**Test:** C 端用户充值,支付回调携带含 `LinkedPackageIDs` 的充值记录
**Expected:** `tb_asset_recharge_record.auto_purchase_status=success``tb_order` 生成订单 → `tb_package_usage` 激活套餐 → `tb_commission_record` 生成佣金记录
**Why human:** 需要完整 Worker 运行环境和真实支付回调数据
### 3. 代购场景佣金差额验证
**Test:** 代理代购下级代理客户套餐
**Expected:** `tb_order.seller_cost_price` = 操作代理自己的成本价(非下级成本价),佣金差额 > 0
**Why human:** 需要特定账号体系(代理+下级代理)的测试数据
### 4. 设备导入 IMEI 字段验证
**Test:** 上传含 IMEI 列的设备导入 Excel
**Expected:** `SELECT id, virtual_no, imei FROM tb_device ORDER BY id DESC LIMIT 20;``imei` 字段非空
**Why human:** 需要数据库直接查询验证PostgreSQL MCP 工具)
### 5. 并发提现防重复验证
**Test:** 并发发起多笔提现申请(余额仅够一笔)
**Expected:** 只有一笔提现单创建成功,其余返回 `CodeInsufficientBalance` 错误,无重复提现单
**Why human:** 需要并发压测工具触发竞争条件
---
## Gaps Summary
**无 Gap。** 所有 5 个 Success Criteria、所有 8 个 CRITICAL 需求均通过代码层面验证。
Phase 1 目标("充值→自动购包→佣金计算→实名激活" 主链路端到端可跑通)在代码层面全部完成:
1. **实名状态一致性**全链路轮询写入、轮询激活判断、C 端校验、DTO、scheduler、iot_card service、handler统一使用 `constants.RealNameStatusVerified(=1)`,旧错误值 `2` 彻底消除。
2. **实名激活任务链路**`PollingHandler` 正确注入 `asynq.Client``triggerFirstRealnameActivation` 从 RPush 降级方案改为 `EnqueueContext`,任务可被 Worker 消费。
3. **充值→购包→佣金链路**`recharge.HandlePaymentCallback``AutoPurchaseHandler.ProcessTask`(注册并消费)→ 事务外 `EnqueueContext(TaskTypeCommission)`,三段链路均已打通。
4. **代购佣金差额**:三个下单函数(普通/后台/H5的场景 5 均使用 `operatorCostPrice` 作为 `sellerCostPrice`,佣金差额计算基准已修正。
5. **并发安全**:提现冻结余额 `RowsAffected == 0` 校验防止并发重复创建提现单。
6. **IMEI 字段**设备导入链路Excel 解析 → device 创建IMEI 字段完整传递。
---
## Commit Traceability
| Plan | Commit | Description | Files Changed |
|------|--------|-------------|---------------|
| 01-01 (CRITICAL-01) | `1fd0072` | 统一实名状态常量 | polling_handler, iot_card, asset_dto, scheduler, iot_card/service |
| 01-01 (CRITICAL-02) | `da50a35` | C 端实名校验改用常量 | client_order/service, client_realname |
| 01-02 (CRITICAL-03) | `20b4b6d` | PollingHandler 注入 asynq.Client | polling_handler, queue/handler |
| 01-03 (CRITICAL-04) | `9a4d87a` | 充值回调触发自动购包入队 | recharge/service, bootstrap/services |
| 01-03 (CRITICAL-04/05) | `030425b` | AutoPurchaseHandler 注入 asynqClient + 佣金触发 | auto_purchase, queue/handler |
| 01-04 (CRITICAL-06) | `f74b7da` | sellerCostPrice 三处修正 | order/service |
| 01-05 (CRITICAL-07) | `809cb2b` | 设备导入 IMEI 字段 | excel, device_import |
| 01-05 (CRITICAL-08) | `db16680` | 提现冻结 RowsAffected 校验 | my_commission/service |
所有 8 个 commit 已通过 `git log` 验证存在。
---
_Verified: 2026-03-28_
_Verifier: the agent (gsd-verifier)_