From bfcea1c18ffe8fe115eabe08fdeb8983462a2164 Mon Sep 17 00:00:00 2001 From: huang Date: Fri, 27 Mar 2026 22:52:27 +0800 Subject: [PATCH] =?UTF-8?q?docs(01-05):=20=E5=AE=8C=E6=88=90=20Plan=2005?= =?UTF-8?q?=20=E6=89=A7=E8=A1=8C=E6=91=98=E8=A6=81=EF=BC=8C=E6=9B=B4?= =?UTF-8?q?=E6=96=B0=20STATE/ROADMAP/REQUIREMENTS?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 创建 01-05-SUMMARY.md(CRITICAL-07 + CRITICAL-08 修复记录) - STATE.md 进度推进至 plan 2/5(20%) - ROADMAP.md 更新 Phase 1 进度(1/5 SUMMARY 已完成) - REQUIREMENTS.md 标记 CRITICAL-07、CRITICAL-08 为完成 --- .planning/REQUIREMENTS.md | 8 +- .planning/ROADMAP.md | 4 +- .planning/STATE.md | 22 ++- .planning/phases/01-p0/01-05-SUMMARY.md | 181 ++++++++++++++++++++++++ 4 files changed, 208 insertions(+), 7 deletions(-) create mode 100644 .planning/phases/01-p0/01-05-SUMMARY.md diff --git a/.planning/REQUIREMENTS.md b/.planning/REQUIREMENTS.md index cb15e1c..21f727f 100644 --- a/.planning/REQUIREMENTS.md +++ b/.planning/REQUIREMENTS.md @@ -16,8 +16,8 @@ - [ ] **CRITICAL-04**: 充值回调后自动购包触发 — `recharge/service.go` 在 HandlePaymentCallback 成功后入队 `TaskTypeAutoPurchaseAfterRecharge`;注册 `AutoPurchaseHandler` - [ ] **CRITICAL-05**: 自动购包佣金链补全 — `AutoPurchaseHandler.ProcessTask()` 事务提交成功后入队 `TaskTypeCommission` - [ ] **CRITICAL-06**: 代购订单金额修正 — 场景 5(代理代购下级)`sellerCostPrice` 全部 6 处逐一审查,确保使用 `operatorCostPrice`(非 `buyerCostPrice`) -- [ ] **CRITICAL-07**: 设备导入补充 IMEI 字段 — `DeviceRow` 新增 IMEI,`ParseDeviceExcel` 列名映射新增 IMEI,`device_import.go` 导入时填充 -- [ ] **CRITICAL-08**: 提现冻结并发校验 — `my_commission/service.go` 冻结余额后检查 `RowsAffected == 0`,余额不足并发场景返回错误而非静默继续 +- [x] **CRITICAL-07**: 设备导入补充 IMEI 字段 — `DeviceRow` 新增 IMEI,`ParseDeviceExcel` 列名映射新增 IMEI,`device_import.go` 导入时填充 +- [x] **CRITICAL-08**: 提现冻结并发校验 — `my_commission/service.go` 冻结余额后检查 `RowsAffected == 0`,余额不足并发场景返回错误而非静默继续 ### PERM — 企业端权限完善(方案 B) @@ -126,8 +126,8 @@ | CRITICAL-04 | Phase 1 | Pending | | CRITICAL-05 | Phase 1 | Pending | | CRITICAL-06 | Phase 1 | Pending | -| CRITICAL-07 | Phase 1 | Pending | -| CRITICAL-08 | Phase 1 | Pending | +| CRITICAL-07 | Phase 1 | Complete | +| CRITICAL-08 | Phase 1 | Complete | | PERM-01 | Phase 2 | Pending | | PERM-02 | Phase 2 | Pending | | FIN-01 | Phase 2 | Pending | diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index df3a242..18c4b88 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -38,7 +38,7 @@ Plans: - [ ] 01-02-PLAN.md — CRITICAL-03:实名激活任务断链(PollingHandler 注入 asynqClient)(Wave 2) - [ ] 01-03-PLAN.md — CRITICAL-04/05:充值回调触发自动购包 + 佣金链补全(Wave 2,parallel) - [ ] 01-04-PLAN.md — CRITICAL-06:代购订单 sellerCostPrice 6 处语义审查修正(Wave 1,parallel) -- [ ] 01-05-PLAN.md — CRITICAL-07/08:设备导入 IMEI + 提现冻结并发校验(Wave 1,parallel) +- [x] 01-05-PLAN.md — CRITICAL-07/08:设备导入 IMEI + 提现冻结并发校验(Wave 1,parallel) --- @@ -118,7 +118,7 @@ Plans: | Phase | Plans Complete | Status | Completed | |-------|----------------|--------|-----------| -| 1. P0 紧急修复 | 0/5 | Planning complete | - | +| 1. P0 紧急修复 | 1/5 | In Progress| | | 2. 权限/财务/实名/轮询修复 | 0/1 | Not started | - | | 3. 设备体系完善 | 0/1 | Not started | - | | 4. 退款 + 支付 + 运营修复 | 0/1 | Not started | - | diff --git a/.planning/STATE.md b/.planning/STATE.md index 840ed69..886fc4a 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -1,3 +1,16 @@ +--- +gsd_state_version: 1.0 +milestone: v1.0 +milestone_name: milestone +status: unknown +last_updated: "2026-03-27T14:51:42.219Z" +progress: + total_phases: 6 + completed_phases: 0 + total_plans: 5 + completed_plans: 1 +--- + # Project State — 君鸿卡管系统 **Project:** 君鸿卡管系统 v1.0 @@ -8,6 +21,8 @@ ## Current Position +Phase: 01 (p0) — EXECUTING +Plan: 2 of 5 | Field | Value | |-------|-------| | **Current Phase** | 1 — P0 紧急修复 | @@ -27,7 +42,7 @@ Progress: [ 1 ]──[ 2 ]──[ 3 ]──[ 4 ]──[ 5 ]──[ 6 ] **Core Value:** 业务链路完整可用——代理能采购分配卡/设备,C端客户能充值购包激活套餐,佣金能正确计算并可提现。这条主链路必须端到端跑通。 -**Current Focus:** Phase 1 — 修复 8 个阻塞主链路的 P0 Bug(实名常量、自动购包、佣金链、设备导入、提现并发) +**Current Focus:** Phase 01 — p0 --- @@ -56,10 +71,12 @@ Progress: [ 1 ]──[ 2 ]──[ 3 ]──[ 4 ]──[ 5 ]──[ 6 ] | Phases complete | 0/6 | --- +| Phase 01-p0 P05 | 8 | 2 tasks | 3 files | ## Accumulated Context ### Key Decisions + - TRAFFIC(Phase 6)必须独立部署,低峰期(凌晨 2~5 点),涉及 DB 迁移 + Redis 架构变更 - REALNAME-01~05 是原子重构,必须在一个 plan 内完整执行,不可拆分 - REFUND 退款佣金回扣(REFUND-03)必须使用整数算术,禁用 float64 @@ -68,12 +85,14 @@ Progress: [ 1 ]──[ 2 ]──[ 3 ]──[ 4 ]──[ 5 ]──[ 6 ] - REFUND-02/03 依赖 REFUND-01(数据模型先行) ### Architecture Notes + - 双进程(API + Worker),依赖集中在 bootstrap 装配 - PollingHandler 缺 asynqClient(CRITICAL-03 修复),AutoPurchaseHandler 未注册(CRITICAL-04 修复) - StopResumeCallback / ResumeCallback 未注入(CLEAN-06 修复) - 流量体系当前用直接覆盖写,Phase 6 改为增量累加 + 日粒度 DB ### Known Risks + - sellerCostPrice 错误赋值可能不止场景 5(CRITICAL-06 需逐一检查 6 处) - 分佣树形遍历无循环检测(不在当前 scope,v2 考量) - 店铺软删除无级联检查(v2 考量) @@ -85,6 +104,7 @@ Progress: [ 1 ]──[ 2 ]──[ 3 ]──[ 4 ]──[ 5 ]──[ 6 ] **Next Action:** Run `/gsd-plan-phase 1` to create detailed plan for Phase 1 (P0 紧急修复) **Context Files:** + - `.planning/ROADMAP.md` — 完整路线图和成功标准 - `.planning/REQUIREMENTS.md` — 46 个 v1 需求(含 Traceability 表) - `.planning/PROJECT.md` — 项目背景和约束 diff --git a/.planning/phases/01-p0/01-05-SUMMARY.md b/.planning/phases/01-p0/01-05-SUMMARY.md new file mode 100644 index 0000000..2e3c5be --- /dev/null +++ b/.planning/phases/01-p0/01-05-SUMMARY.md @@ -0,0 +1,181 @@ +--- +phase: 01-p0 +plan: 05 +subsystem: device-import, my-commission +tags: [bug-fix, critical, imei, concurrent-safety] +dependency_graph: + requires: [] + provides: [CRITICAL-07-fixed, CRITICAL-08-fixed] + affects: [pkg/utils/excel.go, internal/task/device_import.go, internal/service/my_commission/service.go] +tech_stack: + added: [] + patterns: [conditional-update, rows-affected-check] +key_files: + created: [] + modified: + - pkg/utils/excel.go + - internal/task/device_import.go + - internal/service/my_commission/service.go +decisions: + - "CRITICAL-07: DeviceRow 新增 IMEI 字段,buildDeviceColumnIndex 以 switch-case 支持多列名别名(imei/设备imei/imei号),ParseDeviceExcel 数据行提取 row.IMEI,device_import.go 创建 Device 时填充 IMEI: row.IMEI" + - "CRITICAL-08: 冻结余额改为 result := tx...Updates() 分离结果对象,先检 result.Error,再检 result.RowsAffected == 0 返回 CodeInsufficientBalance,防止并发时 WHERE balance >= ? 条件不满足仍创建提现单" +metrics: + duration: "~8min" + completed_date: "2026-03-27" + tasks_completed: 2 + files_modified: 3 +requirements: + - CRITICAL-07 + - CRITICAL-08 +--- + +# Phase 01 Plan 05: 设备导入 IMEI 字段 + 提现冻结并发校验 Summary + +**一句话总结:** 修复设备导入时 IMEI 字段始终为空(CRITICAL-07)及并发提现场景下余额冻结缺少 RowsAffected 校验(CRITICAL-08)两个独立 Bug。 + +--- + +## 完成的任务 + +### Task 1: 设备导入补充 IMEI 字段(CRITICAL-07) + +**修改文件:** `pkg/utils/excel.go`、`internal/task/device_import.go` +**提交:** `809cb2b` + +#### pkg/utils/excel.go 改动 + +**DeviceRow struct 新增 IMEI 字段:** + +```go +// 修改前 +type DeviceRow struct { + Line int + VirtualNo string + // IMEI 字段缺失 + DeviceName string + ... +} + +// 修改后 +type DeviceRow struct { + Line int + VirtualNo string + IMEI string // 设备IMEI,对应 Excel IMEI 列,用于 Gateway API 调用 + DeviceName string + ... +} +``` + +**buildDeviceColumnIndex 新增 IMEI 列名支持:** + +- 在 `index` map 中初始化 `"imei": -1` +- 表头遍历时用 switch-case 特殊处理 IMEI 的多种列名别名:`imei`、`设备imei`、`imei号`(不区分大小写,因为 `strings.ToLower` 已预处理) + +**ParseDeviceExcel 数据行提取 row.IMEI:** + +```go +if idx := colIndex["imei"]; idx >= 0 && idx < len(record) { + row.IMEI = strings.TrimSpace(record[idx]) +} +``` + +#### internal/task/device_import.go 改动 + +在 `processBatch` 函数创建 `model.Device` 时新增 `IMEI: row.IMEI`: + +```go +device := &model.Device{ + VirtualNo: row.VirtualNo, + IMEI: row.IMEI, // 新增 + DeviceName: row.DeviceName, + ... +} +``` + +--- + +### Task 2: 提现冻结并发校验(CRITICAL-08) + +**修改文件:** `internal/service/my_commission/service.go` +**提交:** `db16680` + +**改动前后对比(CreateWithdrawalRequest 第 162-170 行):** + +```go +// 修改前:只检查 Error,RowsAffected == 0 时无感知 +if err := tx.WithContext(ctx).Model(&model.AgentWallet{}). + Where("id = ? AND balance >= ?", wallet.ID, req.Amount). + Updates(map[string]interface{}{ + "balance": gorm.Expr("balance - ?", req.Amount), + "frozen_balance": gorm.Expr("frozen_balance + ?", req.Amount), + }).Error; err != nil { + return errors.Wrap(errors.CodeInternalError, err, "冻结余额失败") +} +// ← 缺少 RowsAffected == 0 的检查,并发场景下仍会继续创建提现单 + +// 修改后:分离 result 对象,同时检查 Error 和 RowsAffected +result := tx.WithContext(ctx).Model(&model.AgentWallet{}). + Where("id = ? AND balance >= ?", wallet.ID, req.Amount). + Updates(map[string]interface{}{ + "balance": gorm.Expr("balance - ?", req.Amount), + "frozen_balance": gorm.Expr("frozen_balance + ?", req.Amount), + }) +if result.Error != nil { + return errors.Wrap(errors.CodeInternalError, result.Error, "冻结余额失败") +} +// RowsAffected == 0 说明余额已不足(并发场景下被其他请求先行扣减) +if result.RowsAffected == 0 { + return errors.New(errors.CodeInsufficientBalance, "余额不足或并发冲突,请稍后重试") +} +``` + +**修复原理:** GORM 条件更新 `WHERE balance >= ?` 在余额不足时不更新任何行(返回 0 行),但不会返回 Error。原代码只检查 Error 故无法感知。修复后检查 RowsAffected == 0,正确返回 `CodeInsufficientBalance` 错误,阻断后续提现单创建。 + +--- + +## 编译验证 + +``` +go build ./... → 通过(无错误) +``` + +--- + +## CRITICAL-07 完成状态 + +- [x] DeviceRow struct 含 IMEI string 字段 +- [x] ParseDeviceExcel 支持 imei/IMEI/设备IMEI/IMEI号 列名(不区分大小写) +- [x] buildDeviceColumnIndex 已初始化 imei 键并处理多别名 +- [x] device_import.go processBatch 填充 IMEI: row.IMEI +- [x] go build ./... 编译通过 + +## CRITICAL-08 完成状态 + +- [x] my_commission/service.go 冻结余额使用 result := tx...Updates() +- [x] 检查 result.Error,处理数据库错误 +- [x] 检查 result.RowsAffected == 0,返回 errors.New(CodeInsufficientBalance, ...) +- [x] go build ./... 编译通过 + +--- + +## 人工验收要求(生产环境) + +- **CRITICAL-07:** 上传含 IMEI 列的设备导入 Excel 后,DBHub 查询 `SELECT id, virtual_no, imei FROM tb_device ORDER BY id DESC LIMIT 20;` 确认 IMEI 字段已填充 +- **CRITICAL-08:** 并发提现场景下,DBHub 确认提现单数量与余额冻结成功次数一致(不存在重复提现单) + +--- + +## Deviations from Plan + +None - 计划按原样执行。两个修复完全独立,互无依赖。 + +--- + +## Self-Check: PASSED + +- [x] `pkg/utils/excel.go` 存在且包含 IMEI 字段 +- [x] `internal/task/device_import.go` 存在且包含 `IMEI: row.IMEI` +- [x] `internal/service/my_commission/service.go` 存在且包含 `result.RowsAffected == 0` +- [x] commit `809cb2b` 存在(CRITICAL-07) +- [x] commit `db16680` 存在(CRITICAL-08) +- [x] `go build ./...` 编译通过