---
phase: 01-p0
plan: 05
type: execute
wave: 1
depends_on: []
files_modified:
  - pkg/utils/excel.go
  - internal/task/device_import.go
  - internal/service/my_commission/service.go
autonomous: true
requirements:
  - CRITICAL-07
  - CRITICAL-08
must_haves:
  truths:
    - "DeviceRow struct 含 IMEI string 字段"
    - "ParseDeviceExcel 中表头映射支持 IMEI 列名（imei/IMEI/设备IMEI/IMEI号）"
    - "device_import.go 创建 model.Device 时赋值 IMEI: row.IMEI"
    - "my_commission/service.go 冻结余额后检查 RowsAffected == 0，不满足时返回余额不足错误"
  artifacts:
    - path: pkg/utils/excel.go
      provides: "DeviceRow 含 IMEI 字段，ParseDeviceExcel 列名映射新增 IMEI"
      contains: "IMEI"
    - path: internal/task/device_import.go
      provides: "导入时填充 IMEI 字段"
      contains: "IMEI: row.IMEI"
    - path: internal/service/my_commission/service.go
      provides: "RowsAffected 校验，并发冻结失败返回错误"
      contains: "RowsAffected == 0"
  key_links:
    - from: pkg/utils/excel.go
      to: internal/task/device_import.go
      via: "DeviceRow.IMEI → model.Device.IMEI"
      pattern: "DeviceRow"
    - from: internal/service/my_commission/service.go
      to: "tx.Model(&AgentWallet{}).Updates()"
      via: "result.RowsAffected == 0 → 返回余额不足错误"
      pattern: "RowsAffected"
---

<objective>
修复设备导入缺失 IMEI 字段（CRITICAL-07）和提现冻结并发校验缺失（CRITICAL-08）。这两个 Bug 互相独立，也与前序 Plan 无依赖，可以与 Plan 01/04 并行执行。

Purpose:
- CRITICAL-07: 设备导入后 IMEI 为空，导致 Gateway 调用失败（IMEI 是调 Gateway 必填字段）
- CRITICAL-08: 并发提现请求中，余额冻结失败（RowsAffected=0）未检测到，导致余额不足时仍创建重复提现单

Output:
- excel.go: DeviceRow 新增 IMEI 字段，ParseDeviceExcel 支持 IMEI 列名
- device_import.go: 创建 Device 时填充 IMEI
- my_commission/service.go: RowsAffected 校验，并发失败返回错误
</objective>

<execution_context>
@$HOME/.config/opencode/get-shit-done/workflows/execute-plan.md
@$HOME/.config/opencode/get-shit-done/templates/summary.md
</execution_context>

<context>
@.planning/phases/01-p0/01-CONTEXT.md

# 修复规格书（方案 A-6 第 621-670 行，方案 A-7 第 673-705 行）
@.sisyphus/plans/修正业务-完整方案.md

<interfaces>
<!-- DeviceRow 当前结构（pkg/utils/excel.go:35-43）-->
```go
type DeviceRow struct {
    Line         int
    VirtualNo    string
    // IMEI 字段缺失 ← 需新增
    DeviceName   string
    DeviceModel  string
    DeviceType   string
    MaxSimSlots  int
    Manufacturer string
    ICCIDs       []string
}
```

<!-- my_commission/service.go 当前错误代码（第 162-170 行）-->
```go
// 当前：只检查 Error，不检查 RowsAffected
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 的检查
```
</interfaces>
</context>

<tasks>

<task type="auto">
  <name>Task 1: 设备导入补充 IMEI 字段（CRITICAL-07）</name>
  <files>pkg/utils/excel.go, internal/task/device_import.go</files>
  <action>
**修改 `pkg/utils/excel.go`：**

① 在 DeviceRow struct（第 35-43 行）新增 IMEI 字段：
```go
type DeviceRow struct {
    Line         int
    VirtualNo    string
    IMEI         string   // 新增，对应 Excel IMEI 列
    DeviceName   string
    DeviceModel  string
    DeviceType   string
    MaxSimSlots  int
    Manufacturer string
    ICCIDs       []string
}
```

② 在 `ParseDeviceExcel` 函数的表头列名映射 switch-case（约第 290-320 行）中，新增 IMEI 的 case：
```go
case "imei", "IMEI", "设备IMEI", "IMEI号":
    row.IMEI = cellValue
```

找到现有 case 的位置（如 `case "虚拟号", "virtual_no"`），在同一 switch 结构内添加 IMEI 的 case。

**修改 `internal/task/device_import.go`：**

在 `processBatch` 函数（约第 178 行）创建 `model.Device{}` 的位置，新增 `IMEI: row.IMEI`：
```go
device := &model.Device{
    // ... 已有字段 ...
    IMEI: row.IMEI,  // 新增
}
```

执行 `go build ./...`，编译通过后提交独立 commit（per D-05）。
  </action>
  <verify>
    <automated>go build ./... && rg "IMEI" pkg/utils/excel.go internal/task/device_import.go | grep -c "IMEI" | grep -v "^0$" && echo "PASS: IMEI 字段已添加" || echo "FAIL"</automated>
  </verify>
  <done>
    - DeviceRow struct 含 IMEI string 字段
    - ParseDeviceExcel 支持 imei/IMEI/设备IMEI/IMEI号 列名
    - device_import.go processBatch 填充 IMEI: row.IMEI
    - go build ./... 编译通过
  </done>
</task>

<task type="auto">
  <name>Task 2: 提现冻结并发校验（CRITICAL-08）</name>
  <files>internal/service/my_commission/service.go</files>
  <action>
**修改 `internal/service/my_commission/service.go`（第 162-170 行）：**

将只检查 Error 的写法改为同时检查 RowsAffected：

```go
// 修改前（只检查 Error）
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 校验，per CRITICAL-08 规格）
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, "冻结余额失败")
}
if result.RowsAffected == 0 {
    return errors.New(errors.CodeInsufficientBalance, "余额不足或并发冲突，请稍后重试")
}
```

执行 `go build ./...`，编译通过后提交独立 commit（per D-05）。
  </action>
  <verify>
    <automated>go build ./... && rg "RowsAffected" internal/service/my_commission/service.go && echo "PASS: RowsAffected 校验已添加" || echo "FAIL: RowsAffected 校验缺失"</automated>
  </verify>
  <done>
    - my_commission/service.go 冻结余额使用 result := tx...Updates()，检查 result.Error 和 result.RowsAffected == 0
    - RowsAffected == 0 时返回 errors.New(errors.CodeInsufficientBalance, ...)
    - go build ./... 编译通过
  </done>
</task>

</tasks>

<verification>
整体验收：

```bash
# 1. 编译验证
go build ./...

# 2. CRITICAL-07 验证
rg "IMEI" pkg/utils/excel.go     # 应有 struct 字段和 case 映射
rg "IMEI" internal/task/device_import.go  # 应有 IMEI: row.IMEI

# 3. CRITICAL-08 验证
rg "RowsAffected" internal/service/my_commission/service.go  # 应有 == 0 检查
```

人工验收（参见修正业务-完整方案.md A-6、A-7 验收清单）：
- CRITICAL-07: 设备导入后 DBHub 查询 `SELECT id, virtual_no, imei FROM tb_device ORDER BY id DESC LIMIT 20;` 确认 IMEI 已填充
- CRITICAL-08: 并发提现场景下，DBHub 确认提现单数量与余额冻结成功次数一致
</verification>

<success_criteria>
1. `go build ./...` 编译通过
2. DeviceRow struct 含 IMEI 字段，ParseDeviceExcel 支持 IMEI 列名，device_import.go 填充 IMEI
3. my_commission/service.go 冻结余额后检查 RowsAffected，为 0 时返回余额不足错误
4. 两个独立 commit 已提交（CRITICAL-07 一个，CRITICAL-08 一个）
</success_criteria>

<output>
完成后创建 `.planning/phases/01-p0/01-05-SUMMARY.md`，记录：
- CRITICAL-07: DeviceRow 改动说明，ParseDeviceExcel 改动说明，device_import.go 改动说明
- CRITICAL-08: my_commission/service.go 改动前后对比
- 编译验证结果
- CRITICAL-07 和 CRITICAL-08 完成状态
</output>
