218 lines
8.3 KiB
Markdown
218 lines
8.3 KiB
Markdown
# Council Phase 2 技术评估报告
|
||
|
||
> 协作产出 | 日期:2026-04-21
|
||
> 参与 Agent:BackendArchitect、FrontendDev、FirstPrinciples
|
||
|
||
---
|
||
|
||
## 问题总览
|
||
|
||
| # | 问题 | 优先级 | 根因分类 |
|
||
|---|------|--------|---------|
|
||
| 1 | 购物车提交格式错误 | P0 | API 传递方式 + 参数契约不匹配 |
|
||
| 2 | 缩放时舞台元素不跟随 | P1 | DOM 结构导致 CSS transform 不共享 |
|
||
| 3 | spec 加载问题(已回滚) | P1 | ShopXO 规格匹配机制 + API 链路不明确 |
|
||
| 4 | 商品详情/图片加载 | P2 | 模板未引入 ShopXO 商品内容组件 |
|
||
|
||
---
|
||
|
||
## Issue 1(P0):购物车提交格式错误
|
||
|
||
### 根因分析(三层)
|
||
|
||
**第一层(致命)**:`location.href` 产生 GET 请求,但 `Buy::Index()` 只在 `$this->data_post` 时处理下单逻辑。
|
||
|
||
```php
|
||
// Buy.php:58-61
|
||
public function Index()
|
||
{
|
||
if($this->data_post) {
|
||
BuyService::BuyDataStorage($this->user['id'], $this->data_post);
|
||
return MyRedirect(MyUrl('index/buy/index'));
|
||
} else {
|
||
// GET 分支:从 session 读取,URL 参数从未被读取
|
||
$buy_data = BuyService::BuyDataRead($this->user['id']);
|
||
}
|
||
}
|
||
```
|
||
|
||
→ `goods_params` URL 参数从未被读取,`BuyDataStorage` 从未被调用,`BuyDataRead` 返回空 → "商品数据为空"错误。
|
||
|
||
**第二层(严重)**:字段名不匹配。
|
||
- 前端发送:`goods_params`(JSON 数组)
|
||
- ShopXO 期望:`goods_data`(JSON 数组)
|
||
|
||
**第三层(中等)**:规格匹配机制不兼容。
|
||
- 当前:`spec_base_id: parseInt(specBaseId)` — 直接传 ID
|
||
- ShopXO:`spec: [{type, value}]` — 通过 type:value 字符串匹配 GoodsSpecValue 表
|
||
|
||
### 推荐修复(前后端)
|
||
|
||
**前端(BackendArchitect + FrontendDev 联合)**:
|
||
|
||
```javascript
|
||
// 方案 A:隐藏表单 POST(最小化变更)
|
||
submit: function() {
|
||
var goods_data = this.selectedSeats.map(function(seat, i) {
|
||
return {
|
||
goods_id: self.goodsId,
|
||
spec: [{type: '座位', value: seat.seatKey}], // ← ShopXO 规格格式
|
||
stock: 1,
|
||
extension_data: JSON.stringify({attendee: attendeeData[i], seat: {...}})
|
||
};
|
||
});
|
||
|
||
var form = document.createElement('form');
|
||
form.method = 'POST';
|
||
form.action = MyUrl('index/buy/index');
|
||
var input = document.createElement('input');
|
||
input.name = 'goods_data';
|
||
input.value = JSON.stringify(goods_data);
|
||
form.appendChild(input);
|
||
document.body.appendChild(form);
|
||
form.submit();
|
||
}
|
||
```
|
||
|
||
**后端(BackendArchitect)**:
|
||
- 方案 B(推荐):在插件中新增 `VrTicketBuy` 控制器,复用 BuyService 链路,但绕过 ShopXO 的 spec 匹配(直接通过 spec_base_id 查询)
|
||
- 方案 C:在 `BuyService::BuyInitialize` 中增加插件扩展点,允许 vr_ticket 插件注入自定义的规格处理逻辑
|
||
|
||
### API 设计建议
|
||
|
||
当前实现是「用 GET 做 POST 的事」。正确的做法是:
|
||
1. 隐藏表单 POST `goods_data` 到 `index/buy/index`(ShopXO 原生)
|
||
2. 或者插件新增端点,POST `goods_data` 到 `plugins/vr_ticket/buy-direct`
|
||
|
||
---
|
||
|
||
## Issue 2(P1):缩放时舞台元素不跟随
|
||
|
||
### 根因分析
|
||
|
||
```html
|
||
<div class="vr-seat-map-wrapper">
|
||
<div class="vr-stage">舞 台</div> <!-- 舞台:wrapper 直接子元素 -->
|
||
<div class="vr-seat-rows" id="seatRows"></div> <!-- 座位行 -->
|
||
</div>
|
||
```
|
||
|
||
CSS `transform: scale()` 只作用于应用元素的子树。`.vr-stage` 和 `.vr-seat-rows` 是平级,没有共同的 transform 容器。
|
||
|
||
### 推荐修复(FrontendDev)
|
||
|
||
**方案:将舞台和座位行包裹在同一 zoom 容器内**
|
||
|
||
```html
|
||
<div class="vr-seat-map-wrapper">
|
||
<div class="vr-zoom-container" id="zoomContainer">
|
||
<div class="vr-stage">舞 台</div>
|
||
<div class="vr-seat-rows" id="seatRows"></div>
|
||
</div>
|
||
</div>
|
||
```
|
||
|
||
```css
|
||
.vr-zoom-container {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
transform-origin: center top;
|
||
transition: transform 0.2s ease;
|
||
}
|
||
```
|
||
|
||
JS 缩放时,操作 `#zoomContainer` 的 `transform: scale(X)`,舞台和座位同步缩放。
|
||
|
||
**注意**:舞台的 `border-radius: 50% 50% 0 0 / 20px 20px 0 0` 弧形在缩放后可能变形,需要在 zoom 场景下调整。
|
||
|
||
---
|
||
|
||
## Issue 3(P1):spec 加载问题
|
||
|
||
### 根因分析
|
||
|
||
**问题 A**:ShopXO 的 `GoodsSpecDetail` 通过 `spec.value` 字符串匹配规格,而非直接接受 `spec_base_id`。
|
||
|
||
```php
|
||
// GoodsService.php:2749-2757
|
||
$spec = array_column($params['spec'], 'value'); // ['A_1', 'VIP']
|
||
$where['value'] = $spec;
|
||
$ids = Db::name('GoodsSpecValue')->where($where)->column('goods_spec_base_id');
|
||
```
|
||
|
||
VR 票务场景下,每个座位对应独立的 `GoodsSpecBase` 记录(inventory=1)。要通过 ShopXO 标准流程加载,需要为每个座位生成 `GoodsSpecValue` 记录(type='座位', value='A_1')。
|
||
|
||
**问题 B**:插件 API 端点未建立,导致前端 `loadSoldSeats()` 是 TODO stub。
|
||
|
||
### 推荐修复
|
||
|
||
**后端(BackendArchitect)**:新增插件 API 端点
|
||
```
|
||
GET /?s=api/vr-ticket/sold-seats&goods_id=X&spec_base_id=Y
|
||
Response: {code: 0, data: {sold_seats: ["A_1", "A_2", "B_5"]}}
|
||
```
|
||
|
||
前端在选中场次后调用此接口,标记已售座位。
|
||
|
||
**关于 ShopXO spec 机制**:如果 VR 票务已经生成了 `GoodsSpecBase` 记录,最直接的方式是让插件维护座位→规格的映射,并提供独立的已售座位查询 API,而不是依赖 ShopXO 的规格匹配流程。
|
||
|
||
---
|
||
|
||
## Issue 4(P2):商品详情/图片加载
|
||
|
||
### 根因分析
|
||
|
||
`ticket_detail.html` 是插件独立模板,ShopXO 商品的 `content_web` 和图片数据由 `Goods.php Index()` 加载,但插件模板可能未正确引入这些数据。
|
||
|
||
### 推荐修复
|
||
|
||
确认 ticket_detail.html 是否需要 ShopXO 商品内容渲染。如果需要,应该在模板中引入 ShopXO 的商品内容组件:
|
||
- 商品详情:`$goods['content_web']` 由 GoodsService 处理
|
||
- 商品图册:通过 `ResourcesService` 获取
|
||
|
||
如果票务 UI 不需要 ShopXO 商品内容区(票务详情页有自己的布局),则此问题可降级为「确认不需要」。
|
||
|
||
---
|
||
|
||
## 第一性原则视角的关键提醒(FirstPrinciples)
|
||
|
||
1. **P0 的真正来源**:submit() 的 URL 重定向方式错了,修复后 Buy 链路本身是可用的——不需要重构 spec 系统。
|
||
|
||
2. **spec_base_id_map 是性能缓存**:不是业务必需。如果 `onOrderPaid` 能通过 seatKey 查询到 spec_base_id,map 可以去掉。保留它是合理的优化,但需要确保同步机制。
|
||
|
||
3. **购物车对票务无价值**:当前实现已经在用 Buy 链路,不是 Cart 链路。说明直觉上的「绕过购物车」需求其实不存在——只是 submit() 的传递方式错了。
|
||
|
||
4. **已售座位展示是 P1,不是 P0**:真正的 P0 是 `onOrderPaid` 防双售。前端是否实时显示已售状态,是体验优化,不是业务正确性的根本。
|
||
|
||
5. **多场次 bug**:`GetGoodsViewData()` 只返回第一个配置的场次(取 `validConfigs[0]`)。如果一个商品有多个场次配置,只显示第一个——这是潜在的 bug。
|
||
|
||
6. **最小修复范围**:只需修复 submit() 的传递方式(隐藏表单 POST),不需要重构 spec 系统,不需要引入实时已售座位更新(除非 spec 加载方案已实施)。
|
||
|
||
---
|
||
|
||
## 修复优先级与分工
|
||
|
||
| 优先级 | 问题 | 负责方 | 修复说明 |
|
||
|--------|------|--------|---------|
|
||
| P0 | Issue 1 submit() | BackendArchitect + FrontendDev | 改用隐藏表单 POST,复用 Buy 链路 |
|
||
| P1 | Issue 2 舞台缩放 | FrontendDev | 新增 zoom wrapper 容器 |
|
||
| P1 | Issue 3 spec 加载 | BackendArchitect | 新增插件 API 端点 |
|
||
| P2 | Issue 4 商品详情 | 延后 | 确认是否需要 |
|
||
|
||
---
|
||
|
||
## 附录:ShopXO Buy 链路关键代码索引
|
||
|
||
| 文件 | 行号 | 说明 |
|
||
|------|------|------|
|
||
| `Buy.php` | 56-62 | Index() — POST/GET 分支,BuyDataStorage/BuyDataRead |
|
||
| `BuyService.php` | ~51 | BuyGoods — goods_data 参数校验 + base64 解码 |
|
||
| `BuyService.php` | ~173 | GoodsSpecificationsHandle — 规格解析 |
|
||
| `BuyService.php` | ~104-109 | GoodsSpecDetail 调用 — 通过 spec.value 匹配 |
|
||
| `GoodsService.php` | 2720-2795 | GoodsSpecDetail — type:value 查询 GoodsSpecValue |
|
||
| `BuyService.php` | 1932-1936 | BuyDataStorage — session 缓存(21600s TTL) |
|
||
|
||
---
|
||
|
||
*Council Phase 2 技术评估报告 — 由 BackendArchitect、FrontendDev、FirstPrinciples 协作完成* |