vr-shopxo-plugin/reviews/BackendArchitect-on-phase2.md

8.5 KiB
Raw Blame History

BackendArchitect Phase 2 技术评估 Findings

Agent: council/BackendArchitect | Date: 2026-04-21 | Round 2


B1: GoodsCartService::Save API 契约分析

结论:Save() 方法未找到,但找到了真正的下单入口

关键发现ShopXO 的票务下单流程 不经过购物车,而是直接 POST 到 index/buy/index

// Buy.php Index() — 真正的入口
public function Index()
{
    if($this->data_post)
    {
        // 将数据存储到缓存,以 user_id 为 key
        BuyService::BuyDataStorage($this->user['id'], $this->data_post);
        return MyRedirect(MyUrl('index/buy/index'));
    }
    // 读取缓存,展示订单确认页
    $buy_data = BuyService::BuyDataRead($this->user['id']);
}

真正接收的数据结构(来自 BuyService::BuyInitialize

// BuyService.php ~line 51 — 参数契约
$p = [
    [
        'checked_type' => 'empty',
        'key_name' => 'goods_data',   // ← 核心字段
        'error_msg' => MyLang('common_service.buy.buy_goods_data_error_tips'),
    ],
];
// goods_data 格式:
// [{goods_id, spec, stock, ...}]
// 或从 base64 解码json_decode(base64_decode(urldecode($params['goods_data'])), true)

BuyService::BuyInitialize 处理流程

foreach($params['goods_data'] as $v) {
    // 1. 规格解析 — GoodsSpecificationsHandle()
    // 期望: {goods_id, spec: [{type, value}], stock, extension_data?, ...}
    $goods['spec'] = self::GoodsSpecificationsHandle($v);

    // 2. 调用 GoodsService::GoodsSpecDetail(spec: [{type, value}])
    // ← 关键:这里通过 spec.value 匹配 GoodsSpecValue 表,而不是 spec_base_id
    //    如果 spec 为空但商品有多规格,必须报错
    //    如果 spec 不为空但商品无规格,也必须报错

    // 3. 从返回的 spec_base 获取 inventory, price, spec_base_id
    $goods['inventory'] = $goods_base['data']['spec_base']['inventory'];
    $goods['price'] = $goods_base['data']['spec_base']['price'];
    $goods['spec_base_id'] = $goods_base['data']['spec_base']['id'];
}

结论B1

ShopXO 的 spec 匹配机制是 type:value 匹配,不是 spec_base_id 直接传递。

GoodsSpecDetail 内部逻辑:

  1. params['spec'] 提取 value 数组 → spec = array_column($params['spec'], 'value')
  2. WHERE goods_id=X AND value IN (...) 查询 GoodsSpecValue 表 → 得到 goods_spec_base_id
  3. GoodsSpecBase 读取最终规格记录

B2: ticket_detail.html submit() 参数校验

当前代码submit 函数)

var goodsParamsList = this.selectedSeats.map(function(seat, i) {
    var specBaseId = self.specBaseIdMap[seat.seatKey] || self.sessionSpecId;
    return {
        goods_id: self.goodsId,
        spec_base_id: parseInt(specBaseId) || 0,   // ← 直接传 ID
        stock: 1,
        extension_data: JSON.stringify({...})
    };
});
// 重定向到 checkoutUrl:
var checkoutUrl = this.requestUrl + '?s=index/buy/index' +
    '&goods_params=' + encodeURIComponent(goodsParams);  // ← 拼到 URL

问题 1严重goods_params 是 URL 参数,而不是 POST body。

  • ShopXO Buy::Index() 通过 $this->data_post 判断是否 POST
  • URL 参数在 $_GET,不在 $_POST,所以 $this->data_post 可能是 false
  • 应该用 <form method="POST"> 提交,或者用 JS fetch('/?s=index/buy/index', {method:'POST', body: JSON.stringify(...)})
  • 当前的重定向方式 $location.href = checkoutUrl → GET 请求,无法触发 POST 分支

问题 2中等BuyService::BuyInitialize 期望的字段是 goods_data,不是 goods_params

问题 3严重BuyInitialize 期望的 spec 格式是 [{type, value}],不是 spec_base_id

  • 当前代码直接传 spec_base_id,不经过 ShopXO 的规格匹配逻辑
  • ShopXO 会调用 GoodsSpecDetail({id, spec: [{type, value}]}),通过 value 匹配规格
  • 如果 specBaseIdMap 存储的是规格值而非 {type, value} 对象,则不兼容

问题 4中等extension_data 不是标准字段ShopXO 的订单系统不会处理。


B3: ShopXO spec 加载标准端点

关键端点GoodsService::GoodsSpecDetail

参数

[
    'id' => goods_id,
    'spec' => [          // ← 必须是 [{type, value}] 格式
        ['type' => '场次', 'value' => '2026-04-21 19:00'],
        ['type' => '座位区', 'value' => 'A区'],
    ],
    'stock' => 1        // 可选,数量
]

返回

{
    "code": 0,
    "data": {
        "spec_base": {
            "id": 2001,
            "price": 599.00,
            "inventory": 50,
            "original_price": 799.00,
            "buy_min_number": 1,
            "buy_max_number": 5
        }
    }
}

spec 加载链路

  1. 直接调用(后端 PHPGoodsService::GoodsSpecDetail(['id'=>X, 'spec'=>[...]])
  2. 前端 AJAXShopXO 有 API 端点 api/goods/spec-detail(需验证)
  3. ShopXO 标准流程:用户选择规格 → 前端拼 spec=[{type:'场次',value:'...'}] → 提交 goods_data

spec 数据来源

$goods_spec_dataSeatSkuService::GetGoodsViewData() 传入前端PHP 渲染):

// ticket_detail.html 顶部
var specData = <?php echo json_encode($goods_spec_data ?? []); ?> || [];
// specData[0]: {spec_id, spec_name, price, ...}

B4: ticket_detail.html 加载真实库存的方案

方案对比

方案 复杂度 实时性 风险
A. 直接调用插件 APIAJAX 需新增插件端点 /api/vr-ticket/sold-seats
B. ShopXO 标准 spec 加载流程 需理解 ShopXO 规格匹配机制
C. PHP 后端预渲染(当前) 页面加载时已固定

推荐方案(最小实现)

插件新增 API 端点

GET /?s=api/vr-ticket/sold-seats&goods_id=X&spec_base_id=Y
Response: {sold_seats: ["A_1", "A_2", "B_5"]}

前端在选中场次后调用此接口,标记已售座位。

关于 ShopXO spec 机制的说明

VR 票务的 spec_base_id_map 存储的是每个座位对应的 GoodsSpecBase.id。但 ShopXO 的 GoodsSpecDetail 是通过 {type, value} 匹配规格的,不是直接接受 spec_base_id

这意味着:如果 VR 票务已经生成了 GoodsSpecBase 记录,GoodsSpecDetail 可以通过 spec=[{type:'座位', value:'A_1'}] 来查询,但更直接的方式是让插件自己维护座位→规格的映射,并提供独立的 API。


B5: 根因总结

Issue 1P0— 购物车提交格式错误

根因submit() 把 goods_params 拼到 URLGETBuy::Index() 只在 $this->data_post 时处理数据 → POST 分支永远不触发。

其次BuyService::BuyInitialize 期望 goods_data 字段,且 spec 必须是 [{type, value}] 格式,而不是 spec_base_id

修复方案(后端)

  1. 新增插件端点 index/buy/indexapi/vr-ticket/buy-direct,专门处理 VR 票务的 POST 提交
  2. 或者修改 submit() 为表单 POST 提交,但需处理 ShopXO 的 CSRF 保护

Issue 2P1— 缩放时舞台不跟随

根因.vr-stage.vr-seat-rows 容器外CSS transform: scale() 只作用于容器内子元素。

修复方案(前端):将 .vr-stage 移入 .vr-seat-rows 容器,或创建共享的 zoom wrapper详见 FrontendDev findings

Issue 3P1— spec 加载问题

根因ShopXO 的规格匹配通过 spec.value 字符串匹配,而非直接接受 spec_base_id。VR 票务场景下,每个座位对应独立的 GoodsSpecBaseShopXO 标准流程需要为每个座位生成 ShopXO 规格记录。

修复方案:插件需要维护座位→规格映射,并在选中场次后通过 AJAX 加载已售座位数据(新增插件 API

Issue 4P2— 商品详情/图片加载

根因ShopXO 商品详情页通过 Goods.phpIndex() 方法加载,$goods['content_web'] 等字段由 ShopXO 处理。

修复方案:需要确认 ticket_detail.html 是否需要 ShopXO 的商品内容渲染,如果需要,应该在插件模板中引入 ShopXO 的商品内容组件。


推荐的修复优先级

  1. P0立即修复Issue 1 — submit() 的 GET→POST 问题,导致下单无法工作
  2. P1Issue 2 — 舞台缩放视觉问题
  3. P1Issue 3 — spec 加载/已售座位显示
  4. P2Issue 4 — 商品详情(可延后)

BackendArchitect findings — Round 2 完成