12 KiB
12 KiB
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字段,只有 flatsections[] - GoodsSpecType 里没有
$vr-演播室记录 - SPEC_DIMS 常量只有 4 维(缺演播室)
- buildSeatSpecMap() 无法输出演播室维度
- 前后端代码虽有
roomsfallback 预留,但从未真正启用
二、数据模型现状
2.1 当前 seat_map JSON(goods_id=112,座位模板 ID=1)
{
"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
{
"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 常量(目标值)
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() 目标输出
$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,会自动适配新 JSONticket_detail.html:262— 已有seatMapData.rooms[0].mapfallbackticket_detail.html:269-272— 已有roomsfallback 逻辑
五、Migration 执行步骤
假设 goods_id=112,座位模板 ID=1。
Step 1:更新 seat_map JSON(座位模板)
-- 查看当前 seat_map
SELECT id, name, seat_map FROM vrt_vr_seat_templates WHERE id=1;
-- 更新 JSON 结构:加 rooms 层
-- 旧结构 flat sections/map/seats → 移入 rooms[0]
JSON 转换伪代码:
$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维)
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 表结构:
-- goods_spec_base_id → 哪个 SKU
-- name → 维度名(如 $vr-演播室)
-- value → 维度值(如 主厅)
-- md5_key → 唯一键
生成逻辑(参考 buildSeatSpecMap):
- 遍历所有 GoodsSpecBase(goods_id=112, inventory>0)
- 从 extends.seat_key 解析 room_id, row_label, col_num
- 从 seat_map JSON 反查 roomName(通过 room_id)
- 生成 5 条 GoodsSpecValue
Step 4:验证
- 访问商品详情页,检查 specTypeList 是否包含 5 个维度
- 检查前端演播室选择器是否正确渲染
- 选择座位后 submit,检查 goods_data 中的 spec 数组是否有 5 个维度
- 检查 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() 变更
// 现有
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 数组格式(不变)
spec: seatInfo.spec // 5维完整数组
七、已知约束
- 当前是单 room 模式(rooms[0]),演播室选择器默认选主厅,用户不可切换。未来可扩展多 room。
- GoodsSpecValue 为 0 的根因:BatchGenerate() 没有写 GoodsSpecValue,只有 GoodsSpecBase。这是之前就存在的问题,不是本次引入的。本次修复 BatchGenerate 的同时也要补全 GoodsSpecValue。
- 命名统一:
$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— 座位模板管理