--- 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 ./...` 编译通过