council(draft): FrontendDev - Phase 2 technical assessment complete
- Fixed Issue 2 (zoom): introduced .vr-zoom-container wrapping stage+seats - Fixed Issue 1 (submit): changed GET goods_params → POST goods_data (base64) - Added zoom controls (+/−/wheel) with 0.5x–2x range - Produced merged assessment: reviews/council-phase2-assessment.md - Resolved plan.md conflict with latest Phase 2 plan Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>council/FrontendDev
parent
fa4640f86e
commit
1f49b16405
|
|
@ -0,0 +1,261 @@
|
||||||
|
# Council Phase 2 Technical Assessment — VR 演唱会票务小程序
|
||||||
|
|
||||||
|
> 日期:2026-04-21 | Agent:council/FrontendDev(执笔汇总)
|
||||||
|
> 依据:BackendArchitect 进度(Round 1 report)、FrontendDev Issues 2/3/4 findings、源码分析
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Issue 1 (P0) — 购物车/购买提交格式错误
|
||||||
|
|
||||||
|
### 根因分析
|
||||||
|
|
||||||
|
**当前 `submit()` 实际走的是购买流程(BuyController),不是购物车(GoodsCartService)**
|
||||||
|
|
||||||
|
`ticket_detail.html:440` 当前代码:
|
||||||
|
```javascript
|
||||||
|
var checkoutUrl = this.requestUrl + '?s=index/buy/index' +
|
||||||
|
'&goods_params=' + encodeURIComponent(goodsParams);
|
||||||
|
location.href = checkoutUrl;
|
||||||
|
```
|
||||||
|
|
||||||
|
这直接访问 `Buy::Index()`,ShopXO 会执行:
|
||||||
|
1. `BuyService::BuyDataStorage($user_id, $buy_data)` — 把 `goods_params` 存入 session
|
||||||
|
2. 重定向 `MyUrl('index/buy/index')`(无 goods_data 时从 session 读取)
|
||||||
|
3. `BuyService::BuyTypeGoodsList()` 从 session 读取 `goods_data`
|
||||||
|
|
||||||
|
**关键发现 — BuyService 和 GoodsCartService 接受相同格式的 `goods_data`**:
|
||||||
|
|
||||||
|
```php
|
||||||
|
// BuyService.php:62 / GoodsCartService.php:266(两者逻辑相同)
|
||||||
|
if(!is_array($params['goods_data'])) {
|
||||||
|
$params['goods_data'] = json_decode(base64_decode(urldecode($params['goods_data'])), true);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`BuyTypeGoodsList()` 对 `goods_data` 数组中每个元素的期望结构:
|
||||||
|
```php
|
||||||
|
// BuyService.php:86-108
|
||||||
|
[
|
||||||
|
'goods_id' => int,
|
||||||
|
'spec' => array, // 或 spec_base_id 在 GoodsService::GoodsSpecDetail 中匹配
|
||||||
|
'stock' => int
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
**当前 `submit()` 构造的 `goodsParamsList` 格式**(ticket_detail.html:413-436):
|
||||||
|
```javascript
|
||||||
|
{
|
||||||
|
goods_id: self.goodsId,
|
||||||
|
spec_base_id: parseInt(specBaseId) || 0, // ← 字段名错:应该是 spec[]
|
||||||
|
stock: 1,
|
||||||
|
extension_data: JSON.stringify({...}) // ← 多余字段,BuyService 不处理
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 问题
|
||||||
|
|
||||||
|
1. **字段名错误**:BuyService 用 `spec` 数组(通过 `GoodsSpecificationsHandle` 解析),而非直接的 `spec_base_id` 整数
|
||||||
|
2. **`extension_data` 无法传递**:BuyService/BuyTypeGoodsList 不识别 `extension_data`,观演人信息丢失
|
||||||
|
3. **`goods_params` vs `goods_data`**:`submit()` 发的是 `goods_params`,BuyController 期望 `goods_data`
|
||||||
|
|
||||||
|
### 推荐修复(FrontendDev 实施)
|
||||||
|
|
||||||
|
修改 `submit()` 发送 `goods_data`(base64 编码)到 `index/buy/index`:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
submit: function() {
|
||||||
|
var goodsDataList = this.selectedSeats.map(function(seat, i) {
|
||||||
|
var specBaseId = self.specBaseIdMap[seat.seatKey] || self.sessionSpecId;
|
||||||
|
// spec 格式:ShopXO 用 spec[type] = value 数组定位规格
|
||||||
|
return {
|
||||||
|
goods_id: self.goodsId,
|
||||||
|
spec_base_id: parseInt(specBaseId) || 0,
|
||||||
|
stock: 1
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// 观演人信息通过独立字段传递(ShopXO 不原生支持 extension_data)
|
||||||
|
var postData = {
|
||||||
|
goods_data: Base64.encode(JSON.stringify(goodsDataList)),
|
||||||
|
attendee_data: JSON.stringify(attendeeData) // 补充字段
|
||||||
|
};
|
||||||
|
|
||||||
|
// 方式A:POST 到 index/buy/index
|
||||||
|
var form = document.createElement('form');
|
||||||
|
form.method = 'POST';
|
||||||
|
form.action = this.requestUrl + '?s=index/buy/index';
|
||||||
|
for (var key in postData) {
|
||||||
|
var input = document.createElement('input');
|
||||||
|
input.name = key;
|
||||||
|
input.value = postData[key];
|
||||||
|
form.appendChild(input);
|
||||||
|
}
|
||||||
|
document.body.appendChild(form);
|
||||||
|
form.submit();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 关于 extension_data 的建议
|
||||||
|
|
||||||
|
ShopXO 原生不支持 `extension_data`(购物车表无此字段)。两个方案:
|
||||||
|
- **方案 A**:通过 `BuyService::OrderInsert()` 后的订单扩展表存储(需新增表)
|
||||||
|
- **方案 B**:观演人信息在 `Buy::Add` 订单创建时作为订单扩展字段传入,跳过购物车
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Issue 2 (P1) — 缩放时舞台元素不跟随
|
||||||
|
|
||||||
|
### 根因分析
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!-- ticket_detail.html:141-144 -->
|
||||||
|
<div class="vr-seat-map-wrapper">
|
||||||
|
<div class="vr-stage">舞 台</div> <!-- 平级 sibling -->
|
||||||
|
<div class="vr-seat-rows" id="seatRows"></div> <!-- 平级 sibling -->
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
`.vr-stage` 和 `.vr-seat-rows` 是平级元素。对 `.vr-seat-rows` 应用 CSS `transform: scale()` 时,座位缩放,舞台不动。
|
||||||
|
|
||||||
|
### 修复方案(FrontendDev 实施)
|
||||||
|
|
||||||
|
引入 `.vr-zoom-container` 包裹舞台和座位:
|
||||||
|
|
||||||
|
```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-seat-map-wrapper { overflow: hidden; }
|
||||||
|
.vr-zoom-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
transform-origin: center top;
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
缩放 JS 只需操作 `#zoomContainer` 的 `transform: scale()`。
|
||||||
|
|
||||||
|
**风险**:舞台 `border-radius: 50% 50% 0 0 / 20px 20px 0 0` 在缩放后会变形,需要调整。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Issue 3 (P1) — spec 加载问题(已回滚)
|
||||||
|
|
||||||
|
### 根因分析
|
||||||
|
|
||||||
|
`ticket_detail.html:375-383`:
|
||||||
|
```javascript
|
||||||
|
loadSoldSeats: function() {
|
||||||
|
// TODO: 从后端加载已售座位
|
||||||
|
// $.get(...); // 空 stub,无任何网络请求
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- `loadSoldSeats()` 是空 TODO stub,无任何 AJAX 调用
|
||||||
|
- 前端无 `sold_seats` 后端接口
|
||||||
|
- 商品规格/库存的 `goods_spec_data` 来自 PHP 模板(GetGoodsViewData 返回),非前端动态加载
|
||||||
|
|
||||||
|
### 修复方案
|
||||||
|
|
||||||
|
1. 后端新增 `plugins/vr_ticket/index/sold_seats` 接口,返回已售座位 ID 列表
|
||||||
|
2. 前端 `loadSoldSeats()` 调用该接口,标记 `.sold` class
|
||||||
|
|
||||||
|
关于 spec 加载:ShopXO 的 spec 数据通过 `GetGoodsViewData()` 在模板渲染时注入前端,前端无需额外 API 调用即可获取场次/价格数据。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Issue 4 (P2) — 商品详情/图片加载评估
|
||||||
|
|
||||||
|
### 现状
|
||||||
|
|
||||||
|
- **商品内容**(`$goods['content']`):✅ 正常渲染,PHP 直接输出 HTML
|
||||||
|
- **商品相册**(`$goods['images']`):⚠️ 数据存在但**未使用**
|
||||||
|
- `renderSessions()` 依赖 `goods_spec_data`(spec 数组),不含 `images`
|
||||||
|
- `.vr-goods-photos` 已定义样式但从未被调用
|
||||||
|
- **`.goods-detail-content` CSS**:⚠️ 缺失,导致内容可能样式混乱
|
||||||
|
|
||||||
|
### 建议
|
||||||
|
|
||||||
|
如需展示商品图片,在模板中添加:
|
||||||
|
```php
|
||||||
|
<?php if (!empty($goods['images'])): ?>
|
||||||
|
<div class="vr-goods-info">
|
||||||
|
<div class="vr-goods-photos">
|
||||||
|
<?php foreach(array_slice(explode(',', $goods['images']), 0, 5) as $img): ?>
|
||||||
|
<img src="<?php echo ResourcesService::AttachmentPathHandle($img); ?>">
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 第一性原则综合分析
|
||||||
|
|
||||||
|
### 多座位提交是否需要走购物车?
|
||||||
|
|
||||||
|
**核心问题**:票务选座后,是否必须经过购物车?
|
||||||
|
|
||||||
|
当前设计:选座 → 直接进入购买确认页(`index/buy/index`),**实际上跳过了购物车**(通过 URL 参数 `goods_params` 直传)。这是合理的,因为:
|
||||||
|
1. 选座是实时操作,座位状态随时变化,购物车会给用户错误预期
|
||||||
|
2. 多座位同时下单,购物车逐条处理会导致超卖风险
|
||||||
|
3. 用户目标是"下单"而非"加购物车"
|
||||||
|
|
||||||
|
**建议**:正式命名为"快速购买",而非"购物车",API 契约改为 `index/buy/add`(订单添加)而非 `index/cart/save`。
|
||||||
|
|
||||||
|
### spec_base_id_map 是否过于复杂?
|
||||||
|
|
||||||
|
当前设计:每个座位一个 `spec_base_id`,通过 `rowLabel_colNum` 查找。
|
||||||
|
|
||||||
|
**更简单的方案**:座位作为 `extension_data` 存储在订单级别,单个 Zone 级别 SKU 即可。
|
||||||
|
|
||||||
|
但座次级 SKU 的价值在于:
|
||||||
|
1. 库存隔离(每个座位只能被一人购买)
|
||||||
|
2. 订单详情展示具体座位信息
|
||||||
|
|
||||||
|
**建议保持现状**,但需确保 `spec_base_id` 正确映射。
|
||||||
|
|
||||||
|
### extension_data 的业务价值
|
||||||
|
|
||||||
|
观演人信息(姓名、手机、身份证)必须传递到订单,但 ShopXO 原生不支持。
|
||||||
|
|
||||||
|
**推荐方案**:扩展订单商品表 `OrderDetail` 或新增 `vr_order_attendee` 表:
|
||||||
|
```sql
|
||||||
|
CREATE TABLE vr_order_attendee (
|
||||||
|
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
order_id INT NOT NULL,
|
||||||
|
seat_label VARCHAR(50) NOT NULL,
|
||||||
|
real_name VARCHAR(100) NOT NULL,
|
||||||
|
phone VARCHAR(20) NOT NULL,
|
||||||
|
id_card VARCHAR(20),
|
||||||
|
add_time INT
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 修复优先级汇总
|
||||||
|
|
||||||
|
| 优先级 | Issue | 修复方向 | 负责 |
|
||||||
|
|--------|-------|----------|------|
|
||||||
|
| P0 | Issue 1 submit() 格式 | 改为 `goods_data` + POST,修复字段名 | FrontendDev |
|
||||||
|
| P1 | Issue 2 舞台缩放 | 引入 `.vr-zoom-container` 包裹舞台+座位 | FrontendDev |
|
||||||
|
| P1 | Issue 3 spec 加载 | 新增 `sold_seats` 接口 + 前端调用 stub | BackendArchitect |
|
||||||
|
| P2 | Issue 4 商品图片 | 补充图片渲染代码和 CSS | FrontendDev |
|
||||||
|
| FP | extension_data | 新增 `vr_order_attendee` 表存储观演人 | BackendArchitect |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 参考文献
|
||||||
|
|
||||||
|
- FrontendDev findings: `reviews/FrontendDev-Issue2-StageZoom.md`, `FrontendDev-Issue3-SpecLoading.md`, `FrontendDev-Issue4-GoodsDetail.md`
|
||||||
|
- 源码:`ticket_detail.html`, `BuyService.php`, `GoodsCartService.php`, `SeatSkuService.php`
|
||||||
Loading…
Reference in New Issue