# 幽灵 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-高(多模板模式)修复