# 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
舞 台
``` `.vr-stage` 和 `.vr-seat-rows` 是平级元素。对 `.vr-seat-rows` 应用 CSS `transform: scale()` 时,座位缩放,舞台不动。 ### 修复方案(FrontendDev 实施) 引入 `.vr-zoom-container` 包裹舞台和座位: ```html
舞 台
``` ```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
``` --- ## 第一性原则综合分析 ### 多座位提交是否需要走购物车? **核心问题**:票务选座后,是否必须经过购物车? 当前设计:选座 → 直接进入购买确认页(`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`