docs(phase-01): complete phase execution — verification passed, all CRITICAL-01~08 satisfied

This commit is contained in:
2026-03-28 09:47:54 +08:00
parent dbeed92ed9
commit fb14c51fc2
2 changed files with 237 additions and 3 deletions

View File

@@ -3,7 +3,7 @@ gsd_state_version: 1.0
milestone: v1.0
milestone_name: milestone
status: unknown
last_updated: "2026-03-27T15:33:12.355Z"
last_updated: "2026-03-28T01:47:47.024Z"
progress:
total_phases: 6
completed_phases: 1
@@ -21,8 +21,8 @@ progress:
## Current Position
Phase: 01 (p0) — EXECUTING
Plan: 4 of 5
Phase: 2
Plan: Not started
| Field | Value |
|-------|-------|
| **Current Phase** | 1 — P0 紧急修复 |

View File

@@ -0,0 +1,234 @@
---
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)_