vr-shopxo-plugin/reviews/council-ghost-spec-Frontend...

9.8 KiB
Raw Permalink Blame History

FrontendDev 调研报告:幽灵 spec 问题

日期2026-04-20 | Agentcouncil/FrontendDev


1. ticket_detail.html 的前端规格项构建

1.1 页面性质确认

ticket_detail.html客户前端购票页面(用于 C 端用户选座下单),不是后台商品编辑页面。后台编辑商品时出现的「规格不允许重复」错误发生在 ShopXO 标准后台,其触发点在 GoodsService.php:1859/1889/1925

前端购票页面的数据来源:

PHP 变量 来源SeatSkuService 用途
$vr_seat_template GetGoodsViewData() seat_mapspec_base_id_map
$goods_spec_data GetGoodsViewData() 场次session列表

前端 JS 接收这些数据:

ticket_detail.html:186-187
  seatMap: <?php echo json_encode($vr_seat_template['seat_map'] ?? []); ?>,
  specBaseIdMap: <?php echo json_encode($vr_seat_template['spec_base_id_map'] ?? []); ?>,

前端规格项(场次)构建逻辑(renderSessions(), ticket_detail.html:202-213

var specData = <?php echo json_encode($goods_spec_data ?? []); ?> || [];
// specData 格式: [{spec_id: 2001, spec_name: "08:00-23:59", price: 100}]
// 渲染为可点击的场次卡片

结论ticket_detail.html 本身不构建 ShopXO 规格spec表格其规格项仅为场次选择器。真正触发「规格不允许重复」的是 ShopXO 后台商品编辑页的 GoodsService.php


2. 模板不存在时前端对 template_snapshot 和 spec_base_id_map 的处理

2.1 后端 fallback 行为SeatSkuService.php

关键函数:GetGoodsViewData() (SeatSkuService.php:358-464)

模板不存在时的 fallback硬删除场景

// SeatSkuService.php:383-393
if (empty($seatTemplate)) {
    $config['template_id'] = null;
    $config['template_snapshot'] = null;
    Db::name('Goods')->where('id', $goodsId)->update([
        'vr_goods_config' => json_encode([$config], ...),
    ]);
    return [
        'vr_seat_template' => null,
        'goods_spec_data'  => [],
        'goods_config'    => $config,
    ];
}

执行效果

  1. template_id 被置为 null(写入 DB
  2. template_snapshot 被置为 null(写入 DB
  3. 返回给前端:vr_seat_template = nullgoods_spec_data = []

前端接收到的数据

seatMap: {}         // 空对象
specBaseIdMap: {}   // 空对象
goods_spec_data: [] // 空数组

前端渲染结果

  • renderSessions()sessionGrid 内为 goods_spec_data.length === 0显示提示「该商品暂无场次信息」ticket_detail.html:133
  • renderSeatMap()seatMap.map 为空,座位图区域显示「座位图加载失败」
  • 整个座位选择区域 UI 为空/失败状态

2.2 根因分析

模板不存在时,前端的 fallback 行为是正确的——前端展示空白购票页,用户无法选座。这符合"场馆已删除,无法购票"的业务预期。

真正的问题不在 ticket_detail.html(前端),而在:

  1. 后台商品编辑页ShopXO admin——保存时 AdminGoodsSaveHandle 如何处理 template_id=null 的情况
  2. vr_goods_config 的持久化清理——硬删除后 vr_goods_config 中的 config 块是否被正确清理

3. loadSoldSeats() 函数实现情况

状态:未实现(仅有 TODO 注释)

ticket_detail.html:375-383
loadSoldSeats: function() {
    // TODO: 从后端加载已售座位
    // $.get(this.requestUrl + '?s=plugins/vr_ticket/index/sold_seats', {
    //     goods_id: this.goodsId,
    //     spec_base_id: this.sessionSpecId
    // }, function(res) {
    //     // 标记已售座位
    // });
},

影响

  • soldSeats: {} 永远为空对象ticket_detail.html:189
  • renderSeatMap() 渲染座位时,无法从 soldSeats 读取已售标记
  • 已售座位只能通过 .sold class由 PHP 渲染)或 soldSeats 字典来标记,但两者都未生效
  • 结果:前端无法区分已售/可选座位——用户可能选中一个已售座位,提交后才发现无法购买

严重程度P2功能缺陷不影响「规格不允许重复」错误。


4. 编辑模式下前端对已删除场馆旧规格的处理

4.1 当前行为

当商品的 vr_goods_configtemplate_id 指向的场馆已被硬删除:

  1. GetGoodsViewData() 检测到模板不存在 → template_id=nulltemplate_snapshot=null → 写入 DB
  2. 前端收到 vr_seat_template=nullgoods_spec_data=[]
  3. ticket_detail.html 渲染空白购票页(无场次、无座位图)
  4. 前端没有特殊逻辑处理幽灵 spec——因为后端已经清理了 template_idtemplate_snapshot

4.2 问题点

ticket_detail.html 是前端购票页,不是编辑页。商品编辑(后台)由 ShopXO 标准后台处理VR 插件通过钩子介入。

幽灵 spec 的真正风险在于 AdminGoodsSaveHandle 的保存逻辑:

  • AdminGoodsSaveHandle.php:383-394(硬删除 fallback当模板不存在时continue 跳过 snapshot 重建,但 config 块本身未被移除
  • 如果 vr_goods_config 包含多个 config 块(如多场馆商品),硬删除场馆后该 config 块残存
  • 下次编辑时,该 config 块仍被读取,若前端重新选择了场馆,可能导致 spec 重复

5. 前端根因分析

5.1 「规格不允许重复」错误的真正触发点

该错误不在 ticket_detail.html,而在 ShopXO 后台商品编辑流程的 GoodsService.php:1859/1889/1925

触发条件:

  1. 用户在 ShopXO 后台编辑商品时,手动填写/复制了重复的规格值
  2. 表单提交到 GoodsService::GoodsSave() → spec 验证逻辑检查 specifications_value_* 参数
  3. 发现有重复值 → 返回「规格不允许重复」错误

5.2 与 VR 插件的关联

AdminGoodsSaveHandle 运行时(plugins_service_goods_save_thing_end),它会:

  1. 清空 GoodsSpecTypeGoodsSpecBaseGoodsSpecValueAdminGoodsSaveHandle.php:152-155
  2. template_id > 0 的 config 块执行 BatchGenerate

如果 template_idnull(硬删除后),BatchGenerate 跳过,但 vr_goods_config 中的 config 块仍然残存。此时商品 spec 表为空,不会出现「规格不允许重复」错误。

但如果用户在前端ShopXO 后台编辑页操作时ShopXO 的原生规格表单被填充了旧的 VR 规格数据,这些数据可能在保存时被 ShopXO 的原生规格逻辑验证并触发重复错误。


6. 修复方案

6.1 前端修复ticket_detail.html

loadSoldSeats() 建议实现

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; // {row_col: true, ...}
            self.markSoldSeats();
        }
    });
},
markSoldSeats: function() {
    var self = this;
    document.querySelectorAll('.vr-seat').forEach(function(el) {
        var key = el.dataset.rowLabel + '_' + el.dataset.colNum;
        if (self.soldSeats[key]) {
            el.classList.add('sold');
        }
    });
},

6.2 后端修复(建议 BackendArchitect 评估)

当模板被硬删除后,AdminGoodsSaveHandle 应清理整个 config 块:

// AdminGoodsSaveHandle.php:77-90 改进
if (empty($template)) {
    // 模板不存在时,移除整个 config 块(避免残存)
    unset($configs[$i]);
    continue;
}
$configs = array_values($configs); // 重排索引

或在 SeatSkuService::GetGoodsViewData() 中持久化清理:

// SeatSkuService.php:383-393 改进
if (empty($seatTemplate)) {
    // 模板不存在时,清除整个 config 块,而非仅置 null
    $vrGoodsConfig = json_decode($goods['vr_goods_config'] ?? '', true);
    unset($vrGoodsConfig[0]);
    $newConfig = array_values($vrGoodsConfig);
    Db::name('Goods')->where('id', $goodsId)->update([
        'vr_goods_config' => empty($newConfig) ? '' : json_encode($newConfig, ...),
    ]);
    return ['vr_seat_template' => null, 'goods_spec_data' => [], 'goods_config' => null];
}

7. 总结

问题 位置 严重度 说明
loadSoldSeats() 未实现 ticket_detail.html:375 P2 已售座位无法标记
模板不存在时 fallback 正确 SeatSkuService.php:383 后端已正确清理 template_id
「规格不允许重复」不在前端触发 GoodsService.php:1859 触发点在 ShopXO 后台服务层
config 块残留 AdminGoodsSaveHandle.php P2 硬删除后 config 块未移除
spec_base_id_map 不影响前端 ticket_detail.html:417 P3 前端通过 seatKey 查找,未使用 map

8. 文件路径索引

文件 行号 关键内容
SeatSkuService.php 358-464 GetGoodsViewData(),模板不存在 fallback
SeatSkuService.php 383-394 模板不存在时置 null 并更新 DB
AdminGoodsSaveHandle.php 77-145 config 块遍历和 snapshot 重建逻辑
AdminGoodsSaveHandle.php 152-155 清空原生 spec 表
AdminGoodsSaveHandle.php 158-173 BatchGenerate 循环(跳过 template_id=0
ticket_detail.html 186-189 前端 JS 接收 seatMap/specBaseIdMap
ticket_detail.html 202-213 renderSessions() 场次渲染
ticket_detail.html 375-383 loadSoldSeats() TODO未实现
ticket_detail.html 417 specBaseIdMap 查找(仅 Plan A 提交用)
GoodsService.php 1859 规格值列重复检测
GoodsService.php 1889 规格值重复检测
GoodsService.php 1925 规格名称重复检测