9.0 KiB
幽灵 Spec 问题 — Council 调研汇总报告
日期:2026-04-20 | Agent:FrontendDev + BackendArchitect + SecurityEngineer 基于 main 分支
f84f95b56
一、问题定义
**「场馆删除后编辑商品出现规格重复错误」**的技术描述:
- 商品关联场馆模板 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/BackendArchitect-on-Issue-13-debug.md)
关键发现
Primary Bug — 99% 命中
| 文件 | 行号 | 问题代码 |
|---|---|---|
AdminGoodsSaveHandle.php |
77 | return in_array($r['id'], $config['selected_rooms'] ?? []); |
当 $r(rooms 数组元素)缺少 'id' key 时,访问 $r['id'] 直接抛出 Undefined array key "id"。
对比:SeatSkuService::BatchGenerate:100 已有正确防护
// ✅ 安全写法
$roomId = !empty($room['id']) ? $room['id'] : ('room_' . $rIdx);
而 AdminGoodsSaveHandle:77 没有这个防护。
Secondary Bug — 模板不存在时 null 访问
| 文件 | 行号 | 问题代码 |
|---|---|---|
AdminGoodsSaveHandle.php |
71 | $seatMap = json_decode($template['seat_map'] ?? '{}', true); |
当 find() 返回 null 后,$template['seat_map'] 在 PHP 8.0+ 抛出 TypeError。
Tertiary Bug — 类型不匹配静默失败
| 文件 | 行号 | 问题代码 |
|---|---|---|
AdminGoodsSaveHandle.php |
77 | in_array($r['id'], ...) 类型不一致 |
selected_rooms[] 从前端传来是字符串(如 "room_0"),而 $r['id'] 可能是整数。类型不匹配时 in_array() 永远返回 false,静默导致 selectedRoomIds 为空数组。
后端根因
幽灵 spec 在 AdminGoodsSaveHandle.php:88 的 continue 处产生:当模板不存在时,continue 跳过 snapshot 重建,但 config 块本身未被移除,残存在 vr_goods_config 中。
后端修复建议(已合并)
// AdminGoodsSaveHandle.php:83-90(已修复)
if ($templateId > 0) {
$template = Db::name('vr_seat_templates')->find($templateId);
if (empty($template)) {
continue; // ✅ 硬删除场景跳过
}
$seatMap = json_decode($template['seat_map'] ?? '{}', true);
// ...
}
// AdminGoodsSaveHandle.php:116-137(已修复)
array_filter($allRooms, function ($r) use ($selectedRooms) {
$rid = $r['id'] ?? ''; // ✅ P0 修复:空安全
// 尝试直接匹配 + 前缀匹配 + 索引回退
// ...
})
2.3 SecurityEngineer — 安全审计(reviews/SecurityEngineer-AUDIT.md)
审计报告来源
reviews/SecurityEngineer-AUDIT.md—AdminGoodsSaveHandle.php根因分析 + 修复建议reviews/BackendArchitect-on-Issue-13-debug.md— "Undefined array key 'id'" 根因分析
审计结论(来源: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/BackendArchitect-on-Issue-13-debug.md |
| SecurityEngineer 安全审计 | reviews/SecurityEngineer-AUDIT.md |
| BackendArchitect Round 5 Review | reviews/BackendArchitect-on-FrontendDev-P1.md |
| 本汇总报告 | reviews/council-ghost-spec-summary.md |