vr-shopxo-plugin/reviews/council-ghost-spec-summary.md

9.0 KiB
Raw Permalink Blame History

幽灵 Spec 问题 — Council 调研汇总报告

日期2026-04-20 | AgentFrontendDev + BackendArchitect + SecurityEngineer 基于 main 分支 f84f95b56


一、问题定义

**「场馆删除后编辑商品出现规格重复错误」**的技术描述:

  1. 商品关联场馆模板 Avr_goods_config 中存储 template_idtemplate_snapshotspec_base_id_map
  2. 场馆 A 被硬删除,vr_seat_templates 表中无记录
  3. 编辑商品时前端检测到模板不存在,自动置空场馆选择
  4. 但旧的幽灵 spec来自已删除场馆的配置仍混入表单
  5. 提交时触发「规格不允许重复」

二、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=nulltemplate_snapshot=null 写入 DB前端收到空数据渲染空白购票页。

「规格不允许重复」触发点不在前端

该错误触发在 GoodsService.php:1859/1889/1925ShopXO 后台服务层),不在 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'] ?? []);

$rrooms 数组元素)缺少 '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:88continue 处产生:当模板不存在时,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.mdAdminGoodsSaveHandle.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-2AdminGoodsSaveHandle.php:77 直接访问 $r['id'] 无空安全保护 → "Undefined array key 'id'" 崩溃
  • 幽灵 spec 注入路径:硬删除后 continue 跳过AdminGoodsSaveHandle.php:88但 config 块残留于 vr_goods_config 数组,最终被写回 DBAdminGoodsSaveHandle.php:148-150
  • template_snapshot 可信度:来源是 vr_seat_templates 表,硬删除后被 GetGoodsViewData() 置 null可信
  • 无直接 XSS:后端输出均有编码,seatMapspecBaseIdMap 来自 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:375loadSoldSeats() 未实现 ⚠️ 待实现
P3 AdminGoodsSaveHandle.php:91-93json_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