# Phase 3 P0 — 5维 Spec 重构:演播室层补全 > 版本:v1.0 | 日期:2026-04-22 | 状态:**P0 阻塞** --- ## 一、问题定义 ### 1.1 现象 当前商品详情页(ticket_detail.html)可以选择: - 场次 - 场馆 - 分区(A区/B区/C区) - 座位 但设计文档要求的层级是: ``` 场次 → 场馆 → 演播室 → 分区 → 座位号 ``` **演播室(第3层)完全消失**。用户在整个购买流程中感知不到这一层的存在。 ### 1.2 影响 - seat_map JSON 没有 `rooms` 字段,只有 flat `sections[]` - GoodsSpecType 里没有 `$vr-演播室` 记录 - SPEC_DIMS 常量只有 4 维(缺演播室) - buildSeatSpecMap() 无法输出演播室维度 - 前后端代码虽有 `rooms` fallback 预留,但从未真正启用 --- ## 二、数据模型现状 ### 2.1 当前 seat_map JSON(goods_id=112,座位模板 ID=1) ```json { "venue": { "name": "国家体育馆", "address": "北京市朝阳区奥体中心", "image": "" }, "sections": [ {"char": "A", "name": "VIP区", "color": "#e74c3c"}, {"char": "B", "name": "看台", "color": "#3498db"}, {"char": "C", "name": "普通", "color": "#2ecc71"} ], "map": ["AAAAAA", "BBBBBB", "CCCCCC"], "seats": { "A": {"price": 899, "color": "#e74c3c", "label": "VIP"}, "B": {"price": 599, "color": "#3498db", "label": "看台"}, "C": {"price": 299, "color": "#2ecc71", "label": "普通"} }, "row_labels": ["A", "B", "C"] } ``` **问题**:`sections`、`map`、`seats` 都是 flat 结构,没有 `rooms` 嵌套层。 ### 2.2 目标 seat_map JSON ```json { "venue": { "name": "国家体育馆", "address": "北京市朝阳区奥体中心", "image": "" }, "rooms": [ { "id": "room_001", "name": "主厅", "sections": [ {"char": "A", "name": "VIP区", "color": "#e74c3c"}, {"char": "B", "name": "看台", "color": "#3498db"}, {"char": "C", "name": "普通", "color": "#2ecc71"} ], "map": ["AAAAAA", "BBBBBB", "CCCCCC"], "seats": { "A": {"price": 899, "color": "#e74c3c", "label": "VIP"}, "B": {"price": 599, "color": "#3498db", "label": "看台"}, "C": {"price": 299, "color": "#2ecc71", "label": "普通"} } } ] } ``` **变更说明**: - `sections`、`map`、`seats` 从 flat 移入 `rooms[0]` - `rooms[].id` = 演播室标识(`room_001`) - `rooms[].name` = 演播室名称(`主厅`) - 保留 flat `sections/map/seats` 作为 fallback(Admin.php:646 和 ticket_detail.html:262 已有兼容逻辑) - 未来可扩展多个 room(多厅模式) ### 2.3 当前 GoodsSpecType(goods_id=112) | ID | name | 含义 | value 示例 | |-----|----------------|-------------|-----------| | 1942 | `$vr-场馆` | 场馆 | `[{"name":"国家体育馆","images":""}]` | | 1943 | `$vr-分区` | 分区 | `[{"name":"A区","images":""},{"name":"B区","images":""},{"name":"C区","images":""}]` | | 1944 | `$vr-时段` | 场次时间 | `[{"name":"2026-05-01 19:00","images":""}]` | | 1945 | `$vr-座位号` | 座位号 | `[{"name":"待选座位","images":""}]` | **注意**:数据库里是 `$vr-时段`,SPEC_DIMS 里是 `$vr-场次`,需统一命名。 ### 2.4 GoodsSpecValue(goods_id=112) **当前:0 条**。 BatchGenerate() 虽然写了 GoodsSpecBase(SKU 存在,有价格/库存),但没有写 GoodsSpecValue(维度连接表),导致 buildSeatSpecMap() 只能从 seat_map JSON 反推维度,演播室完全丢失。 --- ## 三、目标 5 维 Spec 结构 ### 3.1 SPEC_DIMS 常量(目标值) ```php const SPEC_DIMS = [ '$vr-场次', // 第1维:场次时间(来自 GoodsSpecType) '$vr-场馆', // 第2维:场馆名(来自 GoodsSpecType) '$vr-演播室', // 第3维:演播室(新增!) '$vr-分区', // 第4维:分区/区号(来自 seat_map.rooms[].sections) '$vr-座位号', // 第5维:座位号(来自 seat_key 的 row_col 部分) ]; ``` **命名统一**:`$vr-场次` 替代 `$vr-时段`。 ### 3.2 seat_key 格式(不变) ``` {room_id}_{row_label}_{col_num} 例:room_001_A_1 → 主厅 A排1号 ``` ### 3.3 GoodsSpecType 目标记录(goods_id=112) | 顺序 | name | 来源 | value 说明 | |-----|----------------|-------------|-----------| | 1 | `$vr-场次` | 商品规格维度 | 场次时间列表 | | 2 | `$vr-场馆` | 商品规格维度 | 场馆名 | | 3 | `$vr-演播室` | seat_map.rooms[].name | 演播室列表 | | 4 | `$vr-分区` | seat_map.rooms[].sections[].name | 分区列表 | | 5 | `$vr-座位号` | seat_key row_col | 座位号(自动生成)| ### 3.4 buildSeatSpecMap() 目标输出 ```php $seatSpecMap['room_001_A_1'] = [ 'spec_base_id' => 123, 'price' => 899.00, 'inventory' => 1, 'spec' => [ ['type' => '$vr-场次', 'value' => '2026-05-01 19:00'], ['type' => '$vr-场馆', 'value' => '国家体育馆'], ['type' => '$vr-演播室', 'value' => '主厅'], // ← 新增 ['type' => '$vr-分区', 'value' => 'VIP区'], ['type' => '$vr-座位号', 'value' => 'A1'], ], 'venueName' => '国家体育馆', 'roomId' => 'room_001', 'roomName' => '主厅', // ← 新增 'section' => ['char' => 'A', 'name' => 'VIP区', 'color' => '#e74c3c'], 'rowLabel' => 'A', 'colNum' => 1, ]; ``` --- ## 四、受影响文件清单 ### 4.1 数据库(需 Migration) | 表 | 操作 | 说明 | |----|------|------| | `vrt_goods_spec_type` | 清空 goods_id=112 的旧记录,重新插入5条 | 加 `$vr-演播室`,统一 `$vr-场次` | | `vrt_goods_spec_base` | 保留(已有 sku + seat_key) | 不改结构 | | `vrt_goods_spec_value` | 清空重建,按5维生成 | 连接 spec_base_id 和维度 | | `vrt_vr_seat_templates` | 更新 seat_map JSON | 加 rooms 层 | | `vrt_vr_goods_config` | 检查 config JSON 是否受影响 | 通常只存 template_id 和快照 | > 当前数据量极小(1个模板,0条 GoodsSpecValue),可直接 truncate 后重生成。 ### 4.2 PHP 文件 | 文件 | 行号 | 改动 | |------|------|------| | `SeatSkuService.php` | 29 | `SPEC_DIMS` 改为5维,加 `$vr-演播室`,`$vr-场次` 替代 `$vr-时段` | | `SeatSkuService.php` | ~171-178 | `batchGenerate()` 按5维提取维度,需加演播室提取 | | `SeatSkuService.php` | ~270 | `whereIn('name', SPEC_DIMS)` 过滤5个维度 | | `SeatSkuService.php` | ~306 | 插入缺失维度逻辑,按5维顺序 | | `SeatSkuService.php` | ~522-700 | `buildSeatSpecMap()` 完全重写:从 rooms[] 结构读取,加 roomName 提取 | | `SeatSkuService.php` | ~648-665 | switch case 加 `$vr-演播室` | | `BaseService.php` | 187-190 | 维度默认值数组加 `$vr-演播室` | | `TicketService.php` | 65,68 | `$vr-座位号` / `$vr-分区` 不变 | | `Admin.php` | ~176-191 | VenueSave 保存 seat_map 时自动包 rooms 层(fallback已有) | ### 4.3 前端文件 | 文件 | 改动 | |------|------| | `ticket_detail.html` | `specTypeList` 增加 `$vr-演播室` 选择器;`renderAllSelectors()` 渲染演播室选择器 | | `ticket_detail.html` | `filterSeats()` 增加 `currentRoom` 过滤条件 | | `ticket_detail.html` | `submit()` 的 spec 数组加 `$vr-演播室` 维度 | ### 4.4 不需要改的文件(fallback 已存在) - `Admin.php:646-653` — 已有 `$seatMap['rooms']` fallback,会自动适配新 JSON - `ticket_detail.html:262` — 已有 `seatMapData.rooms[0].map` fallback - `ticket_detail.html:269-272` — 已有 `rooms` fallback 逻辑 --- ## 五、Migration 执行步骤 > 假设 goods_id=112,座位模板 ID=1。 ### Step 1:更新 seat_map JSON(座位模板) ```sql -- 查看当前 seat_map SELECT id, name, seat_map FROM vrt_vr_seat_templates WHERE id=1; -- 更新 JSON 结构:加 rooms 层 -- 旧结构 flat sections/map/seats → 移入 rooms[0] ``` JSON 转换伪代码: ```php $old = json_decode($old_seat_map, true); $new = [ 'venue' => $old['venue'], 'rooms' => [[ 'id' => 'room_001', 'name' => '主厅', 'sections' => $old['sections'] ?? [], 'map' => $old['map'] ?? [], 'seats' => $old['seats'] ?? [], ]], // 保留 flat fallback(兼容旧代码) 'sections' => $old['sections'] ?? [], 'map' => $old['map'] ?? [], 'seats' => $old['seats'] ?? [], ]; ``` ### Step 2:重建 GoodsSpecType(5维) ```sql DELETE FROM vrt_goods_spec_type WHERE goods_id=112; INSERT INTO vrt_goods_spec_type (goods_id, name, value, add_time) VALUES (112, '$vr-场次', '[{"name":"2026-05-01 19:00","images":""}]', UNIX_TIMESTAMP()), (112, '$vr-场馆', '[{"name":"国家体育馆","images":""}]', UNIX_TIMESTAMP()), (112, '$vr-演播室', '[{"name":"主厅","images":""}]', UNIX_TIMESTAMP()), (112, '$vr-分区', '[{"name":"A区","images":""},{"name":"B区","images":""},{"name":"C区","images":""}]', UNIX_TIMESTAMP()), (112, '$vr-座位号', '[{"name":"待选座位","images":""}]', UNIX_TIMESTAMP()); ``` ### Step 3:重建 GoodsSpecValue(连接 goods_spec_base 和维度) 当前 GoodsSpecBase 有 sku + extends.seat_key。需要生成 5 条 GoodsSpecValue 记录,每条对应一个维度。 GoodsSpecValue 表结构: ```sql -- goods_spec_base_id → 哪个 SKU -- name → 维度名(如 $vr-演播室) -- value → 维度值(如 主厅) -- md5_key → 唯一键 ``` 生成逻辑(参考 buildSeatSpecMap): 1. 遍历所有 GoodsSpecBase(goods_id=112, inventory>0) 2. 从 extends.seat_key 解析 room_id, row_label, col_num 3. 从 seat_map JSON 反查 roomName(通过 room_id) 4. 生成 5 条 GoodsSpecValue ### Step 4:验证 1. 访问商品详情页,检查 specTypeList 是否包含 5 个维度 2. 检查前端演播室选择器是否正确渲染 3. 选择座位后 submit,检查 goods_data 中的 spec 数组是否有 5 个维度 4. 检查 BuyService::BuyGoods 能正确解析 5 维 goods_data --- ## 六、前端交互变更 ### 6.1 新的选择器层级 ``` [场次选择器] ← goods_spec_data(已有) [场馆选择器] ← specTypeList['$vr-场馆'].options(已有) [演播室选择器] ← specTypeList['$vr-演播室'].options(新增!) [分区选择器] ← specTypeList['$vr-分区'].options(已有) [座位图] ← 按 currentRoom/currentSection 过滤(已有 filterSeats,需加 room 过滤) ``` ### 6.2 filterSeats() 变更 ```javascript // 现有 if (self.currentVenue) { matchVenue = ... } if (self.currentSection) { matchSection = ... } // 新增 if (self.currentRoom) { matchRoom = false; for (var i = 0; i < seatInfo.spec.length; i++) { if (seatInfo.spec[i].type === '$vr-演播室' && seatInfo.spec[i].value === self.currentRoom) { matchRoom = true; break; } } } ``` ### 6.3 submit() spec 数组格式(不变) ```javascript spec: seatInfo.spec // 5维完整数组 ``` --- ## 七、已知约束 1. **当前是单 room 模式**(rooms[0]),演播室选择器默认选主厅,用户不可切换。未来可扩展多 room。 2. **GoodsSpecValue 为 0 的根因**:BatchGenerate() 没有写 GoodsSpecValue,只有 GoodsSpecBase。这是之前就存在的问题,不是本次引入的。本次修复 BatchGenerate 的同时也要补全 GoodsSpecValue。 3. **命名统一**:`$vr-时段` → `$vr-场次`,涉及 DB 数据和 SPEC_DIMS 常量。 --- ## 八、验收标准 - [ ] seat_map JSON 有 `rooms[]` 结构,sections/map/seats 移入 rooms[0] - [ ] GoodsSpecType 有 5 条记录,包含 `$vr-演播室` - [ ] SPEC_DIMS 是 5 维数组 - [ ] buildSeatSpecMap() 输出 seatSpecMap 包含 roomName 和 `$vr-演播室` 维度 - [ ] 前端有演播室选择器 - [ ] filterSeats() 按 currentRoom 过滤 - [ ] submit() 提交的 spec 数组有 5 个维度 - [ ] BuyService::BuyGoods 能正确处理 5 维 goods_data --- ## 九、相关文档 - `docs/SESSION_REPORT_20260421_PHASE2_FIX.md` — Phase 2 会话记录 - `docs/FULL_PLAN.md` — 完整开发计划 - `docs/PLAN_GHOST_SPEC_FIX.md` — 幽灵 spec 修复(相关) - `shopxo/app/plugins/vr_ticket/service/SeatSkuService.php` — 核心服务 - `shopxo/app/plugins/vr_ticket/admin/Admin.php` — 座位模板管理