vr-shopxo-plugin/reports/DebugAgent-ROOT_CAUSE.md

195 lines
6.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# DebugAgent 根因分析最终报告
> Agentcouncil/DebugAgent | 日期2026-04-20
> 代码版本bbea35d83feat: 保存时自动填充 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 顺序修复。