359 lines
12 KiB
Markdown
359 lines
12 KiB
Markdown
|
|
# 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` — 座位模板管理
|