docs(phase-01): complete phase execution — verification passed, all CRITICAL-01~08 satisfied
This commit is contained in:
@@ -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 紧急修复 |
|
||||
|
||||
234
.planning/phases/01-p0/01-VERIFICATION.md
Normal file
234
.planning/phases/01-p0/01-VERIFICATION.md
Normal 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` 写入 1,Model 注释统一) | ✓ 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-01(Phase 4),不影响 Phase 1 目标 |
|
||||
| `internal/service/order/service.go` | 2366 | `TODO: 实现富友小程序支付发起逻辑` | ℹ️ Info | 预存 TODO,属于 PAY-02(Phase 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`(不是 2);Asynq 队列中出现 `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)_
|
||||
Reference in New Issue
Block a user