vr-shopxo-plugin/docs/PLAN_5DIM_REFACTOR.md

359 lines
12 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 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 JSONgoods_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` 作为 fallbackAdmin.php:646 和 ticket_detail.html:262 已有兼容逻辑)
- 未来可扩展多个 room多厅模式
### 2.3 当前 GoodsSpecTypegoods_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 GoodsSpecValuegoods_id=112
**当前0 条**。
BatchGenerate() 虽然写了 GoodsSpecBaseSKU 存在,有价格/库存),但没有写 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重建 GoodsSpecType5维
```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. 遍历所有 GoodsSpecBasegoods_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` — 座位模板管理