8.8 KiB
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 当前代码:
var checkoutUrl = this.requestUrl + '?s=index/buy/index' +
'&goods_params=' + encodeURIComponent(goodsParams);
location.href = checkoutUrl;
这直接访问 Buy::Index(),ShopXO 会执行:
BuyService::BuyDataStorage($user_id, $buy_data)— 把goods_params存入 session- 重定向
MyUrl('index/buy/index')(无 goods_data 时从 session 读取) BuyService::BuyTypeGoodsList()从 session 读取goods_data
关键发现 — BuyService 和 GoodsCartService 接受相同格式的 goods_data:
// 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 数组中每个元素的期望结构:
// BuyService.php:86-108
[
'goods_id' => int,
'spec' => array, // 或 spec_base_id 在 GoodsService::GoodsSpecDetail 中匹配
'stock' => int
]
当前 submit() 构造的 goodsParamsList 格式(ticket_detail.html:413-436):
{
goods_id: self.goodsId,
spec_base_id: parseInt(specBaseId) || 0, // ← 字段名错:应该是 spec[]
stock: 1,
extension_data: JSON.stringify({...}) // ← 多余字段,BuyService 不处理
}
问题
- 字段名错误:BuyService 用
spec数组(通过GoodsSpecificationsHandle解析),而非直接的spec_base_id整数 extension_data无法传递:BuyService/BuyTypeGoodsList 不识别extension_data,观演人信息丢失goods_paramsvsgoods_data:submit()发的是goods_params,BuyController 期望goods_data
推荐修复(FrontendDev 实施)
修改 submit() 发送 goods_data(base64 编码)到 index/buy/index:
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) — 缩放时舞台元素不跟随
根因分析
<!-- 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 包裹舞台和座位:
<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>
.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:
loadSoldSeats: function() {
// TODO: 从后端加载已售座位
// $.get(...); // 空 stub,无任何网络请求
}
loadSoldSeats()是空 TODO stub,无任何 AJAX 调用- 前端无
sold_seats后端接口 - 商品规格/库存的
goods_spec_data来自 PHP 模板(GetGoodsViewData 返回),非前端动态加载
修复方案
- 后端新增
plugins/vr_ticket/index/sold_seats接口,返回已售座位 ID 列表 - 前端
loadSoldSeats()调用该接口,标记.soldclass
关于 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-contentCSS:⚠️ 缺失,导致内容可能样式混乱
建议
如需展示商品图片,在模板中添加:
<?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 直传)。这是合理的,因为:
- 选座是实时操作,座位状态随时变化,购物车会给用户错误预期
- 多座位同时下单,购物车逐条处理会导致超卖风险
- 用户目标是"下单"而非"加购物车"
建议:正式命名为"快速购买",而非"购物车",API 契约改为 index/buy/add(订单添加)而非 index/cart/save。
spec_base_id_map 是否过于复杂?
当前设计:每个座位一个 spec_base_id,通过 rowLabel_colNum 查找。
更简单的方案:座位作为 extension_data 存储在订单级别,单个 Zone 级别 SKU 即可。
但座次级 SKU 的价值在于:
- 库存隔离(每个座位只能被一人购买)
- 订单详情展示具体座位信息
建议保持现状,但需确保 spec_base_id 正确映射。
extension_data 的业务价值
观演人信息(姓名、手机、身份证)必须传递到订单,但 ShopXO 原生不支持。
推荐方案:扩展订单商品表 OrderDetail 或新增 vr_order_attendee 表:
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