- 创建 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 为完成
6.0 KiB
6.0 KiB
phase, plan, subsystem, tags, dependency_graph, tech_stack, key_files, decisions, metrics, requirements
| phase | plan | subsystem | tags | dependency_graph | tech_stack | key_files | decisions | metrics | requirements | ||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 01-p0 | 05 | device-import, my-commission |
|
|
|
|
|
|
|
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 字段:
// 修改前
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 列名支持:
- 在
indexmap 中初始化"imei": -1 - 表头遍历时用 switch-case 特殊处理 IMEI 的多种列名别名:
imei、设备imei、imei号(不区分大小写,因为strings.ToLower已预处理)
ParseDeviceExcel 数据行提取 row.IMEI:
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:
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 行):
// 修改前:只检查 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 完成状态
- DeviceRow struct 含 IMEI string 字段
- ParseDeviceExcel 支持 imei/IMEI/设备IMEI/IMEI号 列名(不区分大小写)
- buildDeviceColumnIndex 已初始化 imei 键并处理多别名
- device_import.go processBatch 填充 IMEI: row.IMEI
- go build ./... 编译通过
CRITICAL-08 完成状态
- my_commission/service.go 冻结余额使用 result := tx...Updates()
- 检查 result.Error,处理数据库错误
- 检查 result.RowsAffected == 0,返回 errors.New(CodeInsufficientBalance, ...)
- 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
pkg/utils/excel.go存在且包含 IMEI 字段internal/task/device_import.go存在且包含IMEI: row.IMEIinternal/service/my_commission/service.go存在且包含result.RowsAffected == 0- commit
809cb2b存在(CRITICAL-07) - commit
db16680存在(CRITICAL-08) go build ./...编译通过