229 lines
8.5 KiB
Markdown
229 lines
8.5 KiB
Markdown
# BackendArchitect Phase 2 技术评估 Findings
|
||
|
||
> Agent: council/BackendArchitect | Date: 2026-04-21 | Round 2
|
||
|
||
---
|
||
|
||
## B1: GoodsCartService::Save API 契约分析
|
||
|
||
### 结论:`Save()` 方法未找到,但找到了真正的下单入口
|
||
|
||
**关键发现**:ShopXO 的票务下单流程 **不经过购物车**,而是直接 POST 到 `index/buy/index`。
|
||
|
||
```php
|
||
// 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`):
|
||
```php
|
||
// 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 处理流程
|
||
|
||
```php
|
||
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 函数)
|
||
|
||
```javascript
|
||
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
|
||
|
||
**参数**:
|
||
```php
|
||
[
|
||
'id' => goods_id,
|
||
'spec' => [ // ← 必须是 [{type, value}] 格式
|
||
['type' => '场次', 'value' => '2026-04-21 19:00'],
|
||
['type' => '座位区', 'value' => 'A区'],
|
||
],
|
||
'stock' => 1 // 可选,数量
|
||
]
|
||
```
|
||
|
||
**返回**:
|
||
```json
|
||
{
|
||
"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. **直接调用**(后端 PHP):`GoodsService::GoodsSpecDetail(['id'=>X, 'spec'=>[...]])`
|
||
2. **前端 AJAX**:ShopXO 有 API 端点 `api/goods/spec-detail`(需验证)
|
||
3. **ShopXO 标准流程**:用户选择规格 → 前端拼 `spec=[{type:'场次',value:'...'}]` → 提交 `goods_data`
|
||
|
||
### spec 数据来源
|
||
|
||
`$goods_spec_data` 由 `SeatSkuService::GetGoodsViewData()` 传入前端(PHP 渲染):
|
||
```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. 直接调用插件 API(AJAX) | 低 | 高 | 需新增插件端点 `/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 1(P0)— 购物车提交格式错误
|
||
|
||
**根因**:submit() 把 `goods_params` 拼到 URL(GET),但 `Buy::Index()` 只在 `$this->data_post` 时处理数据 → POST 分支永远不触发。
|
||
|
||
**其次**:`BuyService::BuyInitialize` 期望 `goods_data` 字段,且 `spec` 必须是 `[{type, value}]` 格式,而不是 `spec_base_id`。
|
||
|
||
**修复方案(后端)**:
|
||
1. 新增插件端点 `index/buy/index` 或 `api/vr-ticket/buy-direct`,专门处理 VR 票务的 POST 提交
|
||
2. 或者修改 submit() 为表单 POST 提交,但需处理 ShopXO 的 CSRF 保护
|
||
|
||
### Issue 2(P1)— 缩放时舞台不跟随
|
||
|
||
**根因**:`.vr-stage` 在 `.vr-seat-rows` 容器外,CSS `transform: scale()` 只作用于容器内子元素。
|
||
|
||
**修复方案(前端)**:将 `.vr-stage` 移入 `.vr-seat-rows` 容器,或创建共享的 zoom wrapper(详见 FrontendDev findings)。
|
||
|
||
### Issue 3(P1)— spec 加载问题
|
||
|
||
**根因**:ShopXO 的规格匹配通过 `spec.value` 字符串匹配,而非直接接受 `spec_base_id`。VR 票务场景下,每个座位对应独立的 `GoodsSpecBase`,ShopXO 标准流程需要为每个座位生成 ShopXO 规格记录。
|
||
|
||
**修复方案**:插件需要维护座位→规格映射,并在选中场次后通过 AJAX 加载已售座位数据(新增插件 API)。
|
||
|
||
### Issue 4(P2)— 商品详情/图片加载
|
||
|
||
**根因**:ShopXO 商品详情页通过 `Goods.php` 的 `Index()` 方法加载,`$goods['content_web']` 等字段由 ShopXO 处理。
|
||
|
||
**修复方案**:需要确认 ticket_detail.html 是否需要 ShopXO 的商品内容渲染,如果需要,应该在插件模板中引入 ShopXO 的商品内容组件。
|
||
|
||
---
|
||
|
||
## 推荐的修复优先级
|
||
|
||
1. **P0(立即修复)**:Issue 1 — submit() 的 GET→POST 问题,导致下单无法工作
|
||
2. **P1**:Issue 2 — 舞台缩放视觉问题
|
||
3. **P1**:Issue 3 — spec 加载/已售座位显示
|
||
4. **P2**:Issue 4 — 商品详情(可延后)
|
||
|
||
---
|
||
|
||
*BackendArchitect findings — Round 2 完成* |