Files
junhong_cmp_fiber/.planning/phases/01-p0/01-05-SUMMARY.md
huang bfcea1c18f docs(01-05): 完成 Plan 05 执行摘要,更新 STATE/ROADMAP/REQUIREMENTS
- 创建 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 为完成
2026-03-27 22:52:27 +08:00

182 lines
6.0 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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.IMEIdevice_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
// 修改前:只检查 ErrorRowsAffected == 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 ./...` 编译通过