From fb14c51fc2380f1299d30610440d9e9095de0e87 Mon Sep 17 00:00:00 2001 From: huang Date: Sat, 28 Mar 2026 09:47:54 +0800 Subject: [PATCH] =?UTF-8?q?docs(phase-01):=20complete=20phase=20execution?= =?UTF-8?q?=20=E2=80=94=20verification=20passed,=20all=20CRITICAL-01~08=20?= =?UTF-8?q?satisfied?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .planning/STATE.md | 6 +- .planning/phases/01-p0/01-VERIFICATION.md | 234 ++++++++++++++++++++++ 2 files changed, 237 insertions(+), 3 deletions(-) create mode 100644 .planning/phases/01-p0/01-VERIFICATION.md diff --git a/.planning/STATE.md b/.planning/STATE.md index b85a594..3dfd949 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -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 紧急修复 | diff --git a/.planning/phases/01-p0/01-VERIFICATION.md b/.planning/phases/01-p0/01-VERIFICATION.md new file mode 100644 index 0000000..99b1b65 --- /dev/null +++ b/.planning/phases/01-p0/01-VERIFICATION.md @@ -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)_