163 lines
8.2 KiB
Markdown
163 lines
8.2 KiB
Markdown
# 幽灵 Spec 问题 — 三方调研汇总报告(终版)
|
||
|
||
**版本**: v2.0
|
||
**日期**: 2026-04-20
|
||
**汇总人**: SecurityEngineer
|
||
**来源报告**: SecurityEngineer-GHOST_SPEC_SECURITY.md + council-ghost-spec-FrontendDev.md + council-ghost-spec-BackendArchitect.md
|
||
|
||
---
|
||
|
||
## 一、问题概述
|
||
|
||
当票务商品关联的场馆模板被硬删除后,编辑商品时出现「规格不允许重复」错误。
|
||
|
||
**注意**:`ticket_detail.html` 是 **C 端购票页面**(用于用户选座下单),不是后台商品编辑页面。「规格不允许重复」错误的真正触发点在 ShopXO 后台服务层 `GoodsService.php:1859/1889/1925`。
|
||
|
||
### 问题触发路径
|
||
|
||
```
|
||
1. 商品选择场馆 A → vr_goods_config 存储 template_id=A、template_snapshot
|
||
2. 场馆 A 被硬删除 → vr_seat_templates 表中无记录
|
||
3. 编辑商品 → GetGoodsViewData() 发现 template_id 无效
|
||
→ 将 template_id 置 null、template_snapshot 置 null
|
||
→ 写回 DB(自愈行为)
|
||
→ 前端收到 template_id=null,选单为空
|
||
4. 若 template_id 未被及时清理 → 保存时 BatchGenerate 返回 "座位模板 N 不存在"
|
||
5. 若 template_id 已清理 → 保存成功,但原规格数据丢失
|
||
```
|
||
|
||
---
|
||
|
||
## 二、各 Agent 调研结论
|
||
|
||
### 2.1 FrontendDev 调研结论(来源:`council-ghost-spec-FrontendDev.md`)
|
||
|
||
| 问题 | 文件:行号 | 严重度 |
|
||
|------|---------|--------|
|
||
| `loadSoldSeats()` 未实现(TODO 空函数) | ticket_detail.html:375-383 | P2 |
|
||
| 模板不存在时 fallback 行为正确 | SeatSkuService.php:383 | — |
|
||
| 「规格不允许重复」不在前端触发 | GoodsService.php:1859 | — |
|
||
| config 块残留(硬删除后未移除) | AdminGoodsSaveHandle.php | P2 |
|
||
| `spec_base_id_map` 不影响前端 | ticket_detail.html:417 | P3 |
|
||
|
||
**前端关键发现**:
|
||
- `ticket_detail.html` 本身不构建 ShopXO 规格表格,其规格项仅为场次选择器
|
||
- 模板不存在时前端展示空白购票页(符合业务预期)
|
||
- `loadSoldSeats()` 是 TODO 注释,未发送 HTTP 请求,已售座位无法灰显
|
||
|
||
**修复建议**:
|
||
- P2: 实现 `loadSoldSeats()` 从后端加载已售座位数据
|
||
- P2: AdminGoodsSaveHandle 硬删除后移除整个 config 块而非仅置 null
|
||
|
||
### 2.2 BackendArchitect 调研结论(来源:`council-ghost-spec-BackendArchitect.md`)
|
||
|
||
| 优先级 | 根因描述 | 文件:行号 | 影响 |
|
||
|--------|----------|-----------|------|
|
||
| **P1** | 无效 config 块未从数组移除,`continue` 后脏数据写回 DB | AdminGoodsSaveHandle.php:88-89 + 148-150 | 幽灵 config 累积,无效 template_id 持续残留 |
|
||
| **P2** | GetGoodsViewData 单模板模式处理,多模板场景会覆盖有效配置块 | SeatSkuService.php:368 + 386-388 | 多模板商品中一个模板删除后整体数据损坏 |
|
||
| P3 | BatchGenerate 对无效 template_id 返回 code=-2,阻断整个保存 | AdminGoodsSaveHandle.php:164-170 | 用户看到"座位模板不存在"错误 |
|
||
| P4 | 前端过滤后 configs 为空时,用户无声失去所有配置 | AdminGoodsSave.php:196-229 | 体验问题:用户不知道配置被过滤 |
|
||
| P5 | `loadSoldSeats()` 未实现 | ticket_detail.html:375-383 | 顾客可选已售座位,可能超卖 |
|
||
|
||
**后端关键发现**:
|
||
|
||
1. **P1 根因**(Critical):`AdminGoodsSaveHandle.php:88-89` 中 `continue` 不删除 config 块,导致含无效 `template_id` 的脏配置被写回 DB(第 148-150 行)。无效模板的 config 块在每次保存后持续累积。
|
||
|
||
2. **`spec_base_id_map` 不是幽灵 spec 来源**:该字段存储在 `vr_seat_templates` 表,模板硬删除后自然消失,不会在 goods 表的 `vr_goods_config` 中残留。
|
||
|
||
3. **`spec_base_id_map` 数据流**:存储在模板表 → `GetGoodsViewData` 读取解码(SeatSkuService.php:404-409)→ 前端 JS 接收。删除后前端 fallback 到 `sessionSpecId`。
|
||
|
||
4. **多模板模式 P2 缺陷**:GetGoodsViewData 第 368 行只取 `$vrGoodsConfig[0]`,多模板模式下第 2、3... 个配置块被完全忽略。第 386-388 行写回 DB 时只写 `[$config]`(单元素),会覆盖其他有效配置块。
|
||
|
||
**修复方案**:
|
||
|
||
- **P1 Fix**: `AdminGoodsSaveHandle.php:88-89` 将 `continue` 改为 `unset($configs[$i])`,第 145 行后加 `$configs = array_values($configs)` 重排索引,第 148-150 行前加判空。
|
||
- **P2 Fix**: `SeatSkuService.php:368-393` 改为遍历所有有效配置块,写回时使用 `$validConfigs` 而非单元素数组。
|
||
|
||
### 2.3 SecurityEngineer 安全审计结论(来源:`SecurityEngineer-GHOST_SPEC_SECURITY.md`)
|
||
|
||
| ID | 问题 | 严重性 |
|
||
|----|------|--------|
|
||
| S-1 | 场馆硬删除后保存失败,错误信息不友好 | P2 |
|
||
| S-2 | GetGoodsViewData 静默修改 DB | P2 |
|
||
| S-3 | `loadSoldSeats()` 空实现,前端无法标记已售座位 | P2 |
|
||
| S-4 | `template_snapshot` 无大小限制 | P3 |
|
||
|
||
**P1 安全漏洞发现:0 个**
|
||
|
||
| 维度 | 评估 |
|
||
|------|------|
|
||
| 脏数据注入 | **安全** — 无注入路径 |
|
||
| 规格覆盖 | **安全** — 先删后建,BatchGenerate 是唯一来源 |
|
||
| XSS 风险 | **安全** — 无渲染点 |
|
||
| 权限绕过 | **安全** — 依赖 ShopXO 内核 |
|
||
| DoS 风险 | **低** — 建议 DB 层加字段大小限制 |
|
||
|
||
**安全评估**:幽灵 spec 问题经审计后确认不是安全漏洞(无 P1):
|
||
1. `spec_base_id_map` 不可控:不在表单提交范围内,不在 `vr_goods_config` 中
|
||
2. `template_snapshot` 保存时由后端重建,前端传入值被覆盖
|
||
3. `BatchGenerate` 有保护:模板不存在时返回错误阻断保存
|
||
|
||
---
|
||
|
||
## 三、综合结论
|
||
|
||
### 问题定性
|
||
|
||
| 维度 | 结论 |
|
||
|------|------|
|
||
| **安全评级** | 无漏洞(0 P1 安全漏洞) |
|
||
| **功能评级** | **P1** — 无效 config 块未被移除,脏数据写回 DB |
|
||
| **其他功能缺陷** | P2 — 错误信息不友好、自愈行为副作用、超卖风险 |
|
||
|
||
**重要区分**:SecurityEngineer 的 P1 定义是「安全漏洞」,BackendArchitect 的 P1 定义是「功能性高优先级缺陷」。两者都正确:
|
||
- 从安全角度:无 P1 安全漏洞(0 个)
|
||
- 从功能角度:无效 config 块残留是 P1 优先级缺陷(需立即修复)
|
||
|
||
### 根因链
|
||
|
||
```
|
||
1. 场馆硬删除 → vr_seat_templates 表记录消失
|
||
2. 商品 vr_goods_config.template_id 仍为已删除场馆的 ID
|
||
3. AdminGoodsSaveHandle.php:88-89 执行 continue(不删除 config 块)
|
||
4. 第 148-150 行将含无效 template_id 的脏 config 写回 DB
|
||
5. 幽灵 config 块在 DB 中持续累积
|
||
6. 下次保存时 BatchGenerate 检测到无效模板 → 返回 code=-2 → 保存阻断
|
||
7. 用户看到不友好的错误信息「座位模板 N 不存在」
|
||
```
|
||
|
||
### 关键保护机制
|
||
|
||
- `BatchGenerate` 模板存在性检查(SeatSkuService.php:52-57)是最后防线:模板不存在时保存被阻断,无脏数据写入规格表
|
||
- 前端 `AdminGoodsSave.php:202` 过滤硬删除模板的 config 块(有效)
|
||
|
||
---
|
||
|
||
## 四、修复建议(优先级排序)
|
||
|
||
| 优先级 | 修复项 | 涉及文件 | Agent 归属 |
|
||
|--------|--------|---------|-----------|
|
||
| **P1** | 无效 config 块移除(`unset` + `array_values` + 判空) | AdminGoodsSaveHandle.php:88-145 + 148-150 | BackendArchitect |
|
||
| **P2-高** | GetGoodsViewData 多模板模式修复 | SeatSkuService.php:368-393 | BackendArchitect |
|
||
| **P2-中** | 改善 BatchGenerate 错误信息,引导用户重新选择场馆 | SeatSkuService.php:55-57 | BackendArchitect |
|
||
| **P2-中** | 改善前端过滤无效配置后的用户体验提示 | AdminGoodsSave.php:196-229 | FrontendDev |
|
||
| **P2-中** | 实现 `loadSoldSeats()` 标记已售座位 | ticket_detail.html:375-383 | FrontendDev |
|
||
| **P3-低** | `vr_goods_config` 字段加 TEXT 限制 | DB migration | BackendArchitect |
|
||
|
||
---
|
||
|
||
## 五、各 Agent 报告位置
|
||
|
||
| Agent | 报告文件 |
|
||
|-------|---------|
|
||
| SecurityEngineer | `reviews/SecurityEngineer-GHOST_SPEC_SECURITY.md` |
|
||
| FrontendDev | `.worktrees/FrontendDev/reviews/council-ghost-spec-FrontendDev.md` |
|
||
| BackendArchitect | `.worktrees/BackendArchitect/reviews/council-ghost-spec-BackendArchitect.md` |
|
||
|
||
---
|
||
|
||
## 六、后续行动
|
||
|
||
1. **BackendArchitect** 实施 P1 Fix(AdminGoodsSaveHandle 无效 config 块移除)
|
||
2. **FrontendDev** 实施 P2-中修复(loadSoldSeats 实现 + 前端提示)
|
||
3. 优先处理 P1(无效 config 块移除)和 P2-高(多模板模式)修复 |