8.2 KiB
幽灵 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 | 顾客可选已售座位,可能超卖 |
后端关键发现:
-
P1 根因(Critical):
AdminGoodsSaveHandle.php:88-89中continue不删除 config 块,导致含无效template_id的脏配置被写回 DB(第 148-150 行)。无效模板的 config 块在每次保存后持续累积。 -
spec_base_id_map不是幽灵 spec 来源:该字段存储在vr_seat_templates表,模板硬删除后自然消失,不会在 goods 表的vr_goods_config中残留。 -
spec_base_id_map数据流:存储在模板表 →GetGoodsViewData读取解码(SeatSkuService.php:404-409)→ 前端 JS 接收。删除后前端 fallback 到sessionSpecId。 -
多模板模式 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):
spec_base_id_map不可控:不在表单提交范围内,不在vr_goods_config中template_snapshot保存时由后端重建,前端传入值被覆盖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 |
六、后续行动
- BackendArchitect 实施 P1 Fix(AdminGoodsSaveHandle 无效 config 块移除)
- FrontendDev 实施 P2-中修复(loadSoldSeats 实现 + 前端提示)
- 优先处理 P1(无效 config 块移除)和 P2-高(多模板模式)修复