# DebugAgent 根因分析最终报告

> Agent：council/DebugAgent | 日期：2026-04-20
> 代码版本：bbea35d83（feat: 保存时自动填充 template_snapshot）

---

## 执行摘要

通过静态代码分析 + 配置文件验证，确认 **"Undefined array key 'id'" 错误的根因**位于 `AdminGoodsSaveHandle.php` 第 77 行。表前缀问题已排除。

---

## 一、核心根因：第 77 行 `$r['id']` 无空安全

**文件**：`shopxo/app/plugins/vr_ticket/hook/AdminGoodsSaveHandle.php`
**行号**：第 77 行（`array_filter` 回调内）
**代码**：
```php
$selectedRoomIds = array_column(
    array_filter($allRooms, function ($r) use ($config) {
        return in_array($r['id'], $config['selected_rooms'] ?? []);  // ← 崩溃点
    }), null
);
```

**触发条件**：当 `$allRooms`（来自 `$seatMap['rooms']`）中存在缺少 `'id'` key 的房间对象时，访问 `$r['id']` 直接抛出 `Undefined array key "id"`。

**对比 Safe 版本**：在 `SeatSkuService::BatchGenerate` 第 100 行有正确的空安全写法：
```php
$roomId = !empty($room['id']) ? $room['id'] : ('room_' . $rIdx);  // ✅ 安全
```

**根本原因**：AdminGoodsSaveHandle 的 `array_filter` 回调中，`$r` 直接访问 `'id'` 键，没有做存在性检查。

---

## 二、表前缀验证：已排除

### 验证方法

1. **install.sql 第 2 行**：
   ```sql
   CREATE TABLE IF NOT EXISTS `{{prefix}}vr_seat_templates` (...)
   ```
   前缀变量为 `{{prefix}}`。

2. **Admin.php 第 66-67 行**（`checkAndInstallTables()` 方法）：
   ```php
   $prefix = \think\facade\Config::get('database.connections.mysql.prefix', 'vrt_');
   $tableName = $prefix . 'vr_seat_templates';  // → vrt_vr_seat_templates
   ```
   默认前缀为 `vrt_`。

3. **BaseService::table()** 第 15-18 行：
   ```php
   public static function table($name) {
       return 'vr_' . $name;  // 'vr_seat_templates'
   }
   ```
   ThinkPHP 会对 `vr_seat_templates` 应用 `vrt_` 前缀 → `vrt_vr_seat_templates`。

### 结论

| 方法 | 展开 | 实际表名 |
|------|------|---------|
| `Db::name('vr_seat_templates')` | `vrt_` + `vr_seat_templates` | `vrt_vr_seat_templates` ✅ |
| `BaseService::table('seat_templates')` | `'vr_'` + `'seat_templates'` → ThinkPHP prefix | `vrt_vr_seat_templates` ✅ |

**两者完全等价，表前缀不是错误来源。**

---

## 三、`find()` 返回 null 的次级风险（第 71 行）

```php
$template = Db::name('vr_seat_templates')->find($templateId);
$seatMap  = json_decode($template['seat_map'] ?? '{}', true);
```

**风险**：若 `vr_seat_templates` 表中不存在该记录，`find()` 返回 `null`，访问 `$template['seat_map']` 抛出 `Undefined array key "seat_map"`。

**注意**：报错不是 `"id"` 而是 `"seat_map"`，所以这不是 Primary 根因。

**PHP 8+ `??` 行为关键点**：`??` 只防御 `$template === null`，**不防御** `$template = []`（空数组）：
```php
$template = [];  // find() 查不到记录时，理论上也可能返回空数组（取决于 ThinkPHP 版本）
$template['seat_map'] ?? '{}';  // PHP 8+: Undefined array key "seat_map"
```

---

## 四、`selected_rooms` 类型不匹配（静默错误，第 77 行）

```php
return in_array($r['id'], $config['selected_rooms'] ?? []);
```

**风险**：前端传来的 `selected_rooms` 元素是字符串（如 `"room_id_xxx"`），而 `$r['id']` 可能是字符串或整数（取决于模板创建时的数据格式）。PHP 的 `in_array()` 默认使用松散比较（`==`），所以 `1 == '1'` 为 `true`，但 `1 === '1'` 为 `false`。这种不匹配会导致过滤逻辑静默失效，不会触发 PHP 错误，但用户选择的房间可能全部丢失。

**修复建议**：
```php
// 方案 1：严格类型比较
in_array($r['id'], $config['selected_rooms'] ?? [], true)

// 方案 2：统一字符串化
in_array((string)($r['id'] ?? ''), array_map('strval', $config['selected_rooms'] ?? []))
```

---

## 五、`$data['item_type']` 访问安全性

```php
if ($goodsId > 0 && ($data['item_type'] ?? '') === 'ticket') {
```

**结论**：安全。`?? ''` 提供默认值，`'' === 'ticket'` 为 `false`，不会误入票务分支。

---

## 六、`SeatSkuService::BatchGenerate` 审计结论

BackendArchitect 报告已确认：
- 第 100 行：`$roomId = !empty($room['id']) ? $room['id'] : ('room_' . $rIdx)` ✅ 有空安全
- 第 55-57 行：`if (empty($template)) { return ...; }` ✅ 有空安全

**结论**：SeatSkuService 无 "Undefined array key 'id'" 风险。

---

## 七、根因概率汇总

| # | 位置 | 错误信息 | 概率 | 结论 |
|---|------|---------|------|------|
| **P1** | AdminGoodsSaveHandle.php:77 `$r['id']` | "Undefined array key 'id'" | **99%** | Primary |
| **P2** | AdminGoodsSaveHandle.php:71 `$template['seat_map']` | "Undefined array key 'seat_map'" | **5%**（不是 "id"） | Secondary |
| **P3** | AdminGoodsSaveHandle.php:77 `in_array` 类型 | 静默失效 | **高** | Tertiary |

**表前缀问题：已排除 ✅**

---

## 八、修复方案

### P1 必须修复（对应 BackendArchitect P1）

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

// 修改后（参考 BatchGenerate 第 100 行写法）
$selectedRoomIds = array_column(
    array_filter($allRooms, function ($r, $idx) use ($config) {
        $roomId = !empty($r['id']) ? $r['id'] : ('room_' . $idx);
        return in_array($roomId, array_map('strval', $config['selected_rooms'] ?? []));
    }), null
);
```

### P2 必须修复（对应 BackendArchitect P2）

```php
// 修改前（AdminGoodsSaveHandle.php:70-72）
$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)) {
    continue;  // 或 return ['code' => -1, 'msg' => '座位模板不存在'];
}
$seatMap = json_decode($template['seat_map'] ?? '{}', true);
```

### P3 建议修复（对应 BackendArchitect P3）

已在 P1 的修复方案中一并解决（`array_map('strval', ...)` 统一字符串化）。

---

## 九、报告结论

**根因已确认**：`AdminGoodsSaveHandle.php:77` 的 `$r['id']` 无空安全，当 room 数据缺少 `id` 字段时触发 "Undefined array key 'id'"。

**表前缀已排除**：两者均查询 `vrt_vr_seat_templates`，等价。

**优先级**：P1 > P2 > P3，与 BackendArchitect 报告一致。

**[APPROVE]** — 与 BackendArchitect 报告结论一致，建议按 P1→P2→P3 顺序修复。
