8.7 KiB
幽灵 Spec 问题 — Council 调研汇总报告
日期:2026-04-20 | Agent:FrontendDev + BackendArchitect + SecurityEngineer 版本:v2.1 | 基于 main 分支
11fdf0309
一、问题定义
**「场馆删除后编辑商品出现规格重复错误」**的技术描述:
- 商品关联场馆模板 A,
vr_goods_config中存储template_id、template_snapshot、spec_base_id_map - 场馆 A 被硬删除,
vr_seat_templates表中无记录 - 编辑商品时前端检测到模板不存在,自动置空场馆选择
- 但旧的幽灵 spec(来自已删除场馆的配置)仍混入表单
- 提交时触发「规格不允许重复」
二、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):
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 中。
后端修复建议(已合并)
AdminGoodsSaveHandle.php:88—continue改为unset($configs[$i]),第 145 行后加$configs = array_values($configs);AdminGoodsSaveHandle.php:148-150— 写回前加if (!empty($configs))SeatSkuService.php:368— 遍历所有配置块而非只处理第一个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 |