192 lines
8.7 KiB
Markdown
192 lines
8.7 KiB
Markdown
|
|
# 幽灵 Spec 问题 — Council 调研汇总报告
|
|||
|
|
|
|||
|
|
> 日期:2026-04-20 | Agent:FrontendDev + BackendArchitect + SecurityEngineer
|
|||
|
|
> 版本:v2.1 | 基于 main 分支 `11fdf0309`
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 一、问题定义
|
|||
|
|
|
|||
|
|
**「场馆删除后编辑商品出现规格重复错误」**的技术描述:
|
|||
|
|
|
|||
|
|
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/council-ghost-spec-BackendArchitect.md`)
|
|||
|
|
|
|||
|
|
#### 关键发现(逐行验证)
|
|||
|
|
|
|||
|
|
**根因 1(Critical):无效 config 块未被移除,脏数据写回 DB**
|
|||
|
|
|
|||
|
|
`AdminGoodsSaveHandle.php:83-90` — `continue` 跳过 snapshot 重建但不删除 config 块,第 148-150 行将脏 config 无条件写回 goods 表。
|
|||
|
|
|
|||
|
|
**根因 2(High):GetGoodsViewData 仅处理单模板模式,多模板时无效块不清理**
|
|||
|
|
|
|||
|
|
`SeatSkuService.php:368` — 只取 `$vrGoodsConfig[0]`,多模板场景下其余配置块被完全忽略;第 386-388 行写回 DB 时只写 `[$config]` 单元素。
|
|||
|
|
|
|||
|
|
**根因 3(Medium):BatchGenerate 对无效 template_id 返回 code=-2,阻断保存**
|
|||
|
|
|
|||
|
|
`AdminGoodsSaveHandle.php:164-170` — 无效 config 块的 `templateId` 仍为原值,BatchGenerate 内部检测到模板不存在后返回错误码,阻断整个保存流程。
|
|||
|
|
|
|||
|
|
**根因 4(Medium):前端过滤无法防御 DB 层污染**
|
|||
|
|
|
|||
|
|
`AdminGoodsSave.php:196-229` — 前端 JS 通过 `validTemplateIds.has(c.template_id)` 过滤无效块,但无法保证 DB 层 config 块被正确清理。
|
|||
|
|
|
|||
|
|
#### 后端根因
|
|||
|
|
|
|||
|
|
幽灵 spec 在 `AdminGoodsSaveHandle.php:88` 的 `continue` 处产生:当模板不存在时,`continue` 跳过 snapshot 重建,但 **config 块本身未被移除**,残存在 `vr_goods_config` 中。
|
|||
|
|
|
|||
|
|
#### 后端修复建议(已合并)
|
|||
|
|
|
|||
|
|
1. `AdminGoodsSaveHandle.php:88` — `continue` 改为 `unset($configs[$i])`,第 145 行后加 `$configs = array_values($configs);`
|
|||
|
|
2. `AdminGoodsSaveHandle.php:148-150` — 写回前加 `if (!empty($configs))`
|
|||
|
|
3. `SeatSkuService.php:368` — 遍历所有配置块而非只处理第一个
|
|||
|
|
4. `SeatSkuService.php:386-388` — 写回 validConfigs 而非 `[$config]`
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### 2.3 SecurityEngineer — 安全审计(`reviews/SecurityEngineer-AUDIT.md`)
|
|||
|
|
|
|||
|
|
#### 审计报告来源
|
|||
|
|
|
|||
|
|
- `reviews/SecurityEngineer-AUDIT.md` — `AdminGoodsSaveHandle.php` 根因分析 + 修复建议
|
|||
|
|
- `reviews/council-ghost-spec-BackendArchitect.md` — "幽灵 spec" 全链路根因分析(4 个根因)
|
|||
|
|
|
|||
|
|
#### 审计结论(来源: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` 数组,最终被写回 DB(AdminGoodsSaveHandle.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/council-ghost-spec-BackendArchitect.md` |
|
|||
|
|
| SecurityEngineer 安全审计 | `reviews/SecurityEngineer-AUDIT.md` |
|
|||
|
|
| BackendArchitect 幽灵 spec 调研 | `reviews/council-ghost-spec-BackendArchitect.md` |
|
|||
|
|
| 本汇总报告 | `reviews/council-ghost-spec-summary.md` |
|