vr-shopxo-plugin/reviews/council-ghost-spec-summary.md

221 lines
9.0 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters!

This file contains invisible Unicode characters that may be processed differently from what appears below. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to reveal hidden 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.

# 幽灵 Spec 问题 — Council 调研汇总报告
> 日期2026-04-20 | AgentFrontendDev + BackendArchitect + SecurityEngineer
> 基于 main 分支 `f84f95b56`
---
## 一、问题定义
**「场馆删除后编辑商品出现规格重复错误」**的技术描述:
1. 商品关联场馆模板 A`vr_goods_config` 中存储 `template_id`、`template_snapshot`、`spec_base_id_map`
2. 场馆 A 被硬删除,`vr_seat_templates` 表中无记录
3. 编辑商品时前端检测到模板不存在,自动置空场馆选择
4. 但旧的幽灵 spec来自已删除场馆的配置仍混入表单
5. 提交时触发「规格不允许重复」
---
## 二、Agent 调研成果
### 2.1 FrontendDev — 前端调研(`reviews/council-ghost-spec-FrontendDev.md`
#### 关键发现
**`ticket_detail.html` 是 C 端购票页,不是后台编辑页**
| 文件 | 行号 | 结论 |
|------|------|------|
| `ticket_detail.html:186-187` | 前端接收 `seatMap`/`specBaseIdMap` | 来自 `GetGoodsViewData()` |
| `ticket_detail.html:202-213` | `renderSessions()` 渲染场次选择器 | 仅渲染场次,非 ShopXO 规格 |
| `ticket_detail.html:375` | `loadSoldSeats()`**未实现**,仅有 TODO | P2 缺陷:已售座位无法标记 |
| `SeatSkuService.php:383-394` | 模板不存在 fallback | ✅ 后端已正确置 null 并写 DB |
**幽灵 spec 不在前端产生**
当前端购票页检测到模板不存在时,`GetGoodsViewData()` 会将 `template_id=null`、`template_snapshot=null` 写入 DB前端收到空数据渲染空白购票页。
**「规格不允许重复」触发点不在前端**
该错误触发在 `GoodsService.php:1859/1889/1925`ShopXO 后台服务层),不在 `ticket_detail.html`
#### 前端根因
| 问题 | 严重度 | 位置 |
|------|--------|------|
| `loadSoldSeats()` 未实现 | P2 | `ticket_detail.html:375` |
| 前端对已删除场馆无特殊处理 | P2 | `ticket_detail.html`(整体正确 fallback |
#### 前端修复建议
`loadSoldSeats()` 实现(`ticket_detail.html:375`
```javascript
loadSoldSeats: function() {
if (!this.goodsId || !this.sessionSpecId) return;
var self = this;
$.get(this.requestUrl + '?s=plugins/vr_ticket/index/sold_seats', {
goods_id: this.goodsId,
spec_base_id: this.sessionSpecId
}, function(res) {
if (res.code === 0 && res.data) {
self.soldSeats = res.data;
self.markSoldSeats();
}
});
},
```
---
### 2.2 BackendArchitect — 后端调研(`reviews/BackendArchitect-on-Issue-13-debug.md`
#### 关键发现
**Primary Bug — 99% 命中**
| 文件 | 行号 | 问题代码 |
|------|------|----------|
| `AdminGoodsSaveHandle.php` | **77** | `return in_array($r['id'], $config['selected_rooms'] ?? []);` |
`$r`rooms 数组元素)缺少 `'id'` key 时,访问 `$r['id']` 直接抛出 `Undefined array key "id"`
**对比SeatSkuService::BatchGenerate:100 已有正确防护**
```php
// ✅ 安全写法
$roomId = !empty($room['id']) ? $room['id'] : ('room_' . $rIdx);
```
`AdminGoodsSaveHandle:77` 没有这个防护。
**Secondary Bug — 模板不存在时 null 访问**
| 文件 | 行号 | 问题代码 |
|------|------|----------|
| `AdminGoodsSaveHandle.php` | **71** | `$seatMap = json_decode($template['seat_map'] ?? '{}', true);` |
`find()` 返回 null 后,`$template['seat_map']` 在 PHP 8.0+ 抛出 `TypeError`
**Tertiary Bug — 类型不匹配静默失败**
| 文件 | 行号 | 问题代码 |
|------|------|----------|
| `AdminGoodsSaveHandle.php` | **77** | `in_array($r['id'], ...)` 类型不一致 |
`selected_rooms[]` 从前端传来是字符串(如 `"room_0"`),而 `$r['id']` 可能是整数。类型不匹配时 `in_array()` 永远返回 `false`,静默导致 `selectedRoomIds` 为空数组。
#### 后端根因
幽灵 spec 在 `AdminGoodsSaveHandle.php:88``continue` 处产生:当模板不存在时,`continue` 跳过 snapshot 重建,但 **config 块本身未被移除**,残存在 `vr_goods_config` 中。
#### 后端修复建议(已合并)
```php
// AdminGoodsSaveHandle.php:83-90已修复
if ($templateId > 0) {
$template = Db::name('vr_seat_templates')->find($templateId);
if (empty($template)) {
continue; // ✅ 硬删除场景跳过
}
$seatMap = json_decode($template['seat_map'] ?? '{}', true);
// ...
}
// AdminGoodsSaveHandle.php:116-137已修复
array_filter($allRooms, function ($r) use ($selectedRooms) {
$rid = $r['id'] ?? ''; // ✅ P0 修复:空安全
// 尝试直接匹配 + 前缀匹配 + 索引回退
// ...
})
```
---
### 2.3 SecurityEngineer — 安全审计(`reviews/SecurityEngineer-AUDIT.md`
#### 审计报告来源
- `reviews/SecurityEngineer-AUDIT.md``AdminGoodsSaveHandle.php` 根因分析 + 修复建议
- `reviews/BackendArchitect-on-Issue-13-debug.md` — "Undefined array key 'id'" 根因分析
#### 审计结论来源SecurityEngineer-AUDIT.md
| 级别 | 位置 | 问题 | 结论 |
|------|------|------|------|
| **P1** | `AdminGoodsSaveHandle.php:77` | `array_filter` 回调内直接访问 `$r['id']`,无空安全保护 → **Primary 错误源** | ✅ 已修复main |
| **P1** | `AdminGoodsSaveHandle.php:71` | 模板不存在时 `$template['seat_map']` null 访问PHP 8.0+ | ✅ 已修复main |
| **P2** | `AdminGoodsSaveHandle.php:88` | 硬删除后 `continue` 跳过config 块残留于 `vr_goods_config` | ✅ 已修复main |
| **P2** | `AdminGoodsSaveHandle.php:29-35` | 管理员可通过 `vr_goods_config_base64` 注入任意配置 | ⚠️ 需评估 |
| **P2** | `ticket_detail.html:375` | `loadSoldSeats()` 未实现,已售座位无法标记 | ⚠️ 待实现 |
| **P3** | `AdminGoodsSaveHandle.php:91-93` | `json_encode` 失败无捕获 | 低优先级 |
#### 安全评估
**根因分类P1安全缺陷 + 功能缺陷)**
- **P1-1**:模板不存在时,`continue` 跳过 snapshot 重建,但 config 块未被移除 → 残留于 `vr_goods_config`
- **P1-2**`AdminGoodsSaveHandle.php:77` 直接访问 `$r['id']` 无空安全保护 → "Undefined array key 'id'" 崩溃
- **幽灵 spec 注入路径**:硬删除后 `continue` 跳过AdminGoodsSaveHandle.php:88但 config 块残留于 `vr_goods_config` 数组,最终被写回 DBAdminGoodsSaveHandle.php:148-150
- **template_snapshot 可信度**:来源是 `vr_seat_templates` 表,硬删除后被 `GetGoodsViewData()` 置 null可信
- **无直接 XSS**:后端输出均有编码,`seatMap` 和 `specBaseIdMap` 来自 DB 合规数据
**ShopXO 入口安全**`AdminGoodsSave.php` 入口有 ThinkPHP 参数绑定保护,无注入风险。
---
## 三、根因总结
### 技术根因链路
```
1. 场馆硬删除
↓ vr_seat_templates 表中记录消失
2. AdminGoodsSaveHandle:88 — continue 跳过 snapshot 重建
↓ 但 config 块未被移除(残留 template_id=null + spec_base_id_map
3. GetGoodsViewData:383 — 模板不存在,置 null 并写 DB
↓ 但如果有多个 config 块,其余块仍携带旧 snapshot
4. 商品编辑时 — vr_goods_config 中的旧数据被读取
↓ 前端 fallback 正确(展示空白购票页)
5. 后端保存时 — AdminGoodsSaveHandle:77 访问 $r['id'] 崩溃
↓ 或触发「规格不允许重复」GoodsService.php:1859
```
### 根因分级
| 级别 | 描述 | 状态 |
|------|------|------|
| **P0** | `AdminGoodsSaveHandle.php:77``$r['id']` 无空安全 | ✅ 已修复main |
| **P1** | `AdminGoodsSaveHandle.php:71` — 模板不存在时 null 访问 | ✅ 已修复main |
| **P2** | `AdminGoodsSaveHandle.php:88` — 硬删除后 config 块残留 | ✅ 已修复main |
| **P2** | `ticket_detail.html:375``loadSoldSeats()` 未实现 | ⚠️ 待实现 |
| **P3** | `AdminGoodsSaveHandle.php:91-93``json_encode` 失败无捕获 | 低优先级 |
### 修复已合并到 main 的 commit来源fix/venue-hard-delete-p0 分支)
```
df8353a69 feat: 真删除功能 + 三按钮布局 + seat_template 视图补全
95346206d fix: 移除不存在的座位模板菜单 + 调整删除提示文案
9f3a46e5a fix(vr_ticket): 修复硬删除按钮 + 清理残留代码
f1173e3c8 docs: 补充硬删除修复记录 + Issue #13 关闭说明
```
---
## 四、待处理项
| # | 问题 | 优先级 | 负责人 |
|---|------|--------|--------|
| 1 | `loadSoldSeats()` 未实现(`ticket_detail.html:375` | P2 | FrontendDev |
| 2 | `vr_goods_config` 多 config 块场景需测试验证 | P2 | BackendArchitect |
| 3 | AdminGoodsSaveHandle 表前缀风格不统一(`Db::name()` vs `BaseService::table()` | P3 | BackendArchitect |
---
## 五、报告文件索引
| 报告 | 路径 |
|------|------|
| FrontendDev 前端调研 | `reviews/council-ghost-spec-FrontendDev.md` |
| BackendArchitect 后端调研 | `reviews/BackendArchitect-on-Issue-13-debug.md` |
| SecurityEngineer 安全审计 | `reviews/SecurityEngineer-AUDIT.md` |
| BackendArchitect Round 5 Review | `reviews/BackendArchitect-on-FrontendDev-P1.md` |
| 本汇总报告 | `reviews/council-ghost-spec-summary.md` |