# DebugAgent 最终根因报告
## Task 11 — "Undefined array key 'id'" 完整调试分析

> 版本：v2.0 | 日期：2026-04-20 | Agent：council/DebugAgent
> 对应提交：bbea35d83（feat: 保存时自动填充 template_snapshot）
> 数据来源：database.php + AdminGoodsSaveHandle.php + SeatSkuService.php + BaseService.php

---

## 1. 所有 "id" 访问位置逐行分析

### AdminGoodsSaveHandle.php（第 66-84 行）

```php
// 第 67 行：$config['template_snapshot'] — 来自 JSON decode，key 存在
if (empty($config['template_snapshot'])) { ... }

// 第 68 行：$config['template_id'] — JSON 数组元素，PHP 8+ 无 ?? 会报警
$templateId = intval($config['template_id'] ?? 0);

// 第 70 行：Db::name('vr_seat_templates')->find($templateId)
//   查询 vrt_vr_seat_templates，返回 null 或数组
$template = Db::name('vr_seat_templates')->find($templateId);

// 第 71 行：$template['seat_map'] — ❗ 若 $template === null，直接 Undefined array key
$seatMap  = json_decode($template['seat_map'] ?? '{}', true);

// 第 72 行：$seatMap['rooms'] — 已有 ?? '[]' 防御，安全
$allRooms = $seatMap['rooms'] ?? [];

// 第 77 行：$r['id'] — ❗ PRIMARY 错误位置
//   array_filter 回调内，$r（$seatMap['rooms'] 的元素）可能没有 'id' key
return in_array($r['id'], $config['selected_rooms'] ?? []);
```

### SeatSkuService.php

```php
// 第 52-54 行：已正确防御（empty() 检查 + 错误返回）
$template = Db::name(self::table('seat_templates'))->where('id', $seatTemplateId)->find();
if (empty($template)) { return ['code' => -2, 'msg' => ...]; }

// 第 100 行：已正确防御（使用三元 fallback）
$roomId = !empty($room['id']) ? $room['id'] : ('room_' . $rIdx);
```

### 其他位置

```php
// AdminGoodsSaveHandle.php:76-79（array_column 第二参数 null）
array_filter($allRooms, function ($r) { ... }), null
// 第二参数为 null 时 array_column 只取 value，跳过 key 字段本身
// ❗ 但 array_column($array, null) 在 PHP 8.0+ 会产生警告，值被截取
```

---

## 2. Db::name() 表前缀问题 — 最终确认

**database.php 第 53 行：**
```php
'prefix' => 'vrt_',
```

**BaseService.php 第 17 行：**
```php
public static function table($name) {
    return 'vr_' . $name;  // 生成 "vr_seat_templates"
}
```

| 调用方式 | 实际查询表 | 结果 |
|---------|-----------|------|
| `Db::name('vr_seat_templates')` | `vrt_vr_seat_templates` | ✅ 等价 |
| `BaseService::table('seat_templates')` | `vrt_vr_seat_templates` | ✅ 等价 |
| `Db::name('vr_seat_templates')->find()` | `vrt_vr_seat_templates WHERE id=?` | ✅ 一致 |

**结论：表前缀不是问题。** 两者均查询 `vrt_vr_seat_templates`。

---

## 3. find() 返回 null 时的行为

```php
// AdminGoodsSaveHandle.php:70-71
$template = Db::name('vr_seat_templates')->find($templateId);
// $template === null（查不到时）或 []（空结果集）

$seatMap  = json_decode($template['seat_map'] ?? '{}', true);
// ❗ 如果 $template 是 null：$template['seat_map'] 直接 Undefined array key 'seat_map'
```

**防御建议：**
```php
$template = Db::name('vr_seat_templates')->find($templateId);
if (empty($template)) {
    return ['code' => -1, 'msg' => "模板 {$templateId} 不存在"];
}
$seatMap = json_decode($template['seat_map'] ?? '{}', true);
```

---

## 4. $config['template_id'] 的安全性

vr_goods_config JSON 格式：`[{"template_id": 4, ...}]` — 数组。

```php
// AdminGoodsSaveHandle.php:61-64
$rawConfig = $data['vr_goods_config'] ?? '';
$configs = json_decode($rawConfig, true);   // 解码后是数组

if (is_array($configs) && !empty($configs)) {  // ✅ 有防御
    foreach ($configs as $i => &$config) {
        $templateId = intval($config['template_id'] ?? 0);  // ✅ 有 ?? 防御
```

**结论：安全。** 有 `is_array()` 防御 + `?? 0` fallback。

---

## 5. selected_rooms 数据类型问题

前端 `selected_rooms` 格式：字符串 ID 数组，如 `["room_1", "room_2"]`。

```php
// AdminGoodsSaveHandle.php:77
return in_array($r['id'], $config['selected_rooms'] ?? []);
// $r['id']：来自 seat_map.rooms[id]，可能是字符串或数字
// selected_rooms：字符串数组
// ❗ 类型不匹配时 in_array() 永远 false
```

**对比 SeatSkuService.php:100（正确示范）：**
```php
$roomId = !empty($room['id']) ? $room['id'] : ('room_' . $rIdx);
// ✅ 先检查存在性，不存在则生成默认值
```

**AdminGoodsSaveHandle.php:77 缺少空安全：**
```php
// 有 bug（类型不匹配时静默失败）
return in_array($r['id'], $config['selected_rooms'] ?? []);

// 修复后
return isset($r['id']) && in_array($r['id'], (array)($config['selected_rooms'] ?? []), true);
// 或者像 BatchGenerate 一样强制字符串比较
```

---

## 6. array_column($array, null) 的 PHP 8.0+ 警告

```php
// AdminGoodsSaveHandle.php:75-79
$selectedRoomIds = array_column(
    array_filter($allRooms, function ($r) use ($config) {
        return in_array($r['id'], $config['selected_rooms'] ?? []);
    }), null  // ❗ 第二参数 null
);
```

**问题：**
- `array_column($array, null)` 在 PHP 8.0+ 会**产生 E_WARNING**：`The 'column' key does not exist in the passed array`
- 这本身不会直接导致 "Undefined array key 'id'"，但会触发 PHP 警告
- 第一参数是 `array_filter()` 的返回值（已过滤的房间数组），而非原数组

**实际执行流程：**
1. `$allRooms = []` 或 `[[...], [...]]`（来自 `$seatMap['rooms'] ?? []`）
2. `array_filter($allRooms, ...)` — 按 selected_rooms 过滤，返回过滤后的数组
3. `array_column(..., null)` — PHP 8.0+ 产生 E_WARNING，**但不会抛出 "Undefined array key 'id'"**

**所以 Primary 错误不是 array_column，而是 array_filter 回调里的 `$r['id']`。**

---

## 7. 根因排序（优先级）

| 优先级 | 位置 | 问题 | PHP 8+ 行为 | 触发概率 |
|-------|------|------|-----------|---------|
| **P1** | `AdminGoodsSaveHandle.php:77` | `$r['id']` 无空安全 | `Undefined array key 'id'` | **99%** — 如果 rooms 中有任何房间缺 `id` |
| **P2** | `AdminGoodsSaveHandle.php:71` | `find()` 返回 null 后访问 `$template['seat_map']` | `Undefined array key 'seat_map'` | 如果 template_id 对应记录不存在 |
| **T1** | `AdminGoodsSaveHandle.php:77` | `selected_rooms` 字符串类型不匹配 | `in_array` 永远 false（静默）| 100%（静默，不报错）|
| T2 | `AdminGoodsSaveHandle.php:78` | `array_column(..., null)` | PHP 8.0+ E_WARNING | 可能触发，但不是 "Undefined array key 'id'" |

---

## 8. 修复建议（优先级排序）

### P1 修复（AdminGoodsSaveHandle.php:77）
```php
// 修复前
$selectedRoomIds = array_column(
    array_filter($allRooms, function ($r) use ($config) {
        return in_array($r['id'], $config['selected_rooms'] ?? []);
    }), null
);

// 修复后
$selectedRoomIds = array_filter($allRooms, function ($r) use ($config) {
    return isset($r['id']) && in_array((string)$r['id'], array_map('strval', $config['selected_rooms'] ?? []));
});
// 不再用 array_column(null)，直接用 array_filter 返回过滤后的房间数组
```

### P2 修复（AdminGoodsSaveHandle.php:70-72）
```php
// 修复前
$template = Db::name('vr_seat_templates')->find($templateId);
$seatMap  = json_decode($template['seat_map'] ?? '{}', true);

// 修复后
$template = Db::name('vr_seat_templates')->find($templateId);
if (empty($template)) {
    return ['code' => -1, 'msg' => "座位模板 {$templateId} 不存在，无法保存"];
}
$seatMap  = json_decode($template['seat_map'] ?? '{}', true);
```

### T1 修复（AdminGoodsSaveHandle.php:82-84）
```php
// 如果 selectedRoomIds 需要房间对象而不是 ID 列表，修改过滤逻辑
// 当前 array_column(..., null) 已被 array_filter 替代，不需要 array_column
// rooms 数据直接保留（不再是 ID 列表，而是完整房间对象）
```

---

## 9. 关键差异：BatchGenerate vs AdminGoodsSaveHandle

SeatSkuService::BatchGenerate 已正确处理空安全（第 100 行）：
```php
$roomId = !empty($room['id']) ? $room['id'] : ('room_' . $rIdx);
```

AdminGoodsSaveHandle.php 第 77 行则缺少这层保护。这是两者最核心的差异。

---

## 10. 总结

**"Undefined array key 'id'" 的根因：**

1. **Primary（99%）**：第 77 行 `array_filter` 回调内 `$r['id']` 直接访问，如果 `seat_map.rooms[]` 中有房间没有 `id` key，PHP 8+ 抛出 `Undefined array key 'id'`
2. **Secondary（5%）**：第 71 行如果模板 ID 无效，`find()` 返回 null 后访问 `$template['seat_map']` 也会报错
3. **Tertiary（静默）**：`selected_rooms` 类型与 `$r['id']` 不一致，`in_array` 永远 false，但不会报错

**修复三行代码即可解决问题。**
