# 评审报告：Issue #13 "Undefined array key 'id'" 根因分析

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

---

## 一、"Undefined array key 'id'" 根因定位

### Primary Bug — 99% 是这行（第 77 行）

**文件**：`shopxo/app/plugins/vr_ticket/hook/AdminGoodsSaveHandle.php`
**行号**：第 77 行（`array_filter` 回调内）
**代码**：
```php
return in_array($r['id'], $config['selected_rooms'] ?? []);
```

**根因**：当 `$r`（rooms 数组元素）缺少 `'id'` key 时，访问 `$r['id']` 直接抛出 `Undefined array key "id"`。

**何时触发**：当 `vr_seat_templates.seat_map.rooms[]` 中存在任何一个没有 `id` 字段的房间对象时，在 `template_snapshot` 填充逻辑中崩溃。

**对比**：`SeatSkuService::BatchGenerate` 第 100 行做了正确防护：
```php
// ✅ 安全写法
$roomId = !empty($room['id']) ? $room['id'] : ('room_' . $rIdx);
```
而 `AdminGoodsSaveHandle` 第 77 行没有这个防护。

---

## 二、`Db::name('vr_seat_templates')` 表前缀问题

### 结论：两者等价，不存在前缀错误

**验证依据**（`admin/Admin.php` 第 66 行）：
```php
$prefix = \think\facade\Config::get('database.connections.mysql.prefix', 'vrt_');
$tableName = $prefix . 'vr_seat_templates';  // → vrt_vr_seat_templates
```

ShopXO 默认表前缀为 `vrt_`。因此：
- `Db::name('vr_seat_templates')` → `vrt_vr_seat_templates` ✅
- `BaseService::table('seat_templates')` → `vr_seat_templates` + ShopXO 前缀 → `vrt_vr_seat_templates` ✅

两者查询同一张表，**不是错误来源**。

> ⚠️ 但 `AdminGoodsSaveHandle` 使用裸 `Db::name()` 而非 `SeatSkuService` 使用的 `BaseService::table()`，风格不统一。建议统一。

---

## 三、`find()` 返回 null 的空安全问题

### Secondary Bug — 触发概率 5%（第 71 行）

**代码**：
```php
$template = Db::name('vr_seat_templates')->find($templateId);
$seatMap  = json_decode($template['seat_map'] ?? '{}', true);  // ❌ $template 可能是 null
```

**根因**：若 `vr_seat_templates` 表中不存在 `id = $templateId` 的记录，`find()` 返回 `null`，访问 `$template['seat_map']` 抛出 `Undefined array key "seat_map"`（虽然报错信息不是 "id"，但属于同类空安全问题）。

**对比**：`SeatSkuService::BatchGenerate` 第 55-57 行做了正确防护：
```php
if (empty($template)) {
    return ['code' => -2, 'msg' => "座位模板 {$seatTemplateId} 不存在"];
}
```
而 `AdminGoodsSaveHandle` 第 71 行没有等效检查。

---

## 四、`selected_rooms` 类型不匹配问题

### Tertiary Bug — 静默失败（第 77 行）

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

**根因**：`selected_rooms[]` 从前端传来是字符串（如 `"room_0"`），而 `$r['id']` 在 `vr_seat_templates.seat_map.rooms[]` 中可能是整数或字符串，取决于模板创建时的数据。

**影响**：类型不匹配时 `in_array()` 永远返回 `false`，导致 `selectedRoomIds` 永远为空数组，前端无法正确展示选中的房间。**但不会抛出 PHP 错误**，属于静默逻辑错误。

**修复建议**：
```php
// 使用严格模式 (bool) 第三个参数
in_array($r['id'], $config['selected_rooms'] ?? [], true)
// 或统一为字符串比较
in_array((string)($r['id'] ?? ''), array_map('strval', $config['selected_rooms'] ?? []))
```

---

## 五、SeatSkuService::BatchGenerate 审计结论

### ✅ 无 "id" 访问问题

| 位置 | 代码 | 结论 |
|------|------|------|
| 第 100 行 | `$roomId = !empty($room['id']) ? $room['id'] : ('room_' . $rIdx)` | ✅ 有 null-safe fallback |
| 第 103 行 | `in_array($roomId, $selectedRooms)` | ✅ 基于安全的 `$roomId` |
| 第 127-128 行 | `in_array($char, $selectedSections[$roomId])` | ✅ 先检查 `!empty()` |
| 第 278-280 行 | `json_decode($existingItems, true) ?: []` | ✅ 有 fallback |
| 第 283 行 | `array_column($existingItems, 'name')` | ⚠️ 若 `$existingItems` 不是数组，抛出 Warning |

---

## 六、`$data['item_type']` 访问安全分析

### ✅ 安全（第 59 行）

```php
if ($goodsId > 0 && ($data['item_type'] ?? '') === 'ticket') {
```
使用 `?? ''` 提供默认值，`'' === 'ticket'` 为 `false`，不会误入票务分支。

---

## 七、修复建议汇总

### 高优先级（必须修复）

| # | 位置 | 问题 | 修复方案 |
|---|------|------|----------|
| **P1** | AdminGoodsSaveHandle.php:77 | `$r['id']` 无空安全 | 参考 BatchGenerate 第 100 行：`(($r['id'] ?? null) ?: ('room_' . $rIdx))` |
| **P2** | AdminGoodsSaveHandle.php:71 | `$template` null 访问 | `find()` 后加 `if (empty($template)) { continue; }` |
| **P3** | AdminGoodsSaveHandle.php:77 | 类型不匹配静默失败 | 加严格类型比较或统一字符串化 |

### 建议优化（非必须）

| # | 位置 | 问题 | 建议 |
|---|------|------|------|
| S1 | AdminGoodsSaveHandle.php:70 | `Db::name()` 不统一 | 改用 `SeatSkuService` 或 `BaseService::table()` 风格一致 |
| S2 | AdminGoodsSaveHandle.php:91 | goods 表写回时机 | 确认 save_thing_end 时机 goods 已落表，可以直接 update |

---

## 八、最终根因结论

**"Undefined array key 'id'" 错误 99% 来自 AdminGoodsSaveHandle.php 第 77 行**：

```php
return in_array($r['id'], $config['selected_rooms'] ?? []);
//              ^^^^^^^^ 当 $r 无 'id' key 时崩溃
```

**触发条件**：`vr_seat_templates.seat_map.rooms[]` 中存在至少一个没有 `id` 字段的房间对象（这在前端手动构造 seat_map 或某些旧模板数据中很可能发生）。

**修复后代码建议**：
```php
$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
);
```

---

## 九、审查结论

| 审查项 | 结论 |
|--------|------|
| 错误根因 | ✅ 已定位：AdminGoodsSaveHandle.php:77 |
| 表前缀问题 | ✅ 确认无前缀错误，两者等价 |
| null 安全 | ❌ 存在两处 null 安全问题（P1/P2） |
| 类型匹配 | ⚠️ 存在静默类型不匹配（P3） |
| SeatSkuService | ✅ BatchGenerate 已正确处理 |
| 建议修复优先级 | P1 > P2 > P3 |

**[APPROVE] — 根因已确认，建议按 P1→P2→P3 顺序修复**
