From de7c25c6b9597f8fe01ba45f1ce93dd6dacba94a Mon Sep 17 00:00:00 2001 From: Council Date: Wed, 22 Apr 2026 01:36:39 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20Phase=203=20P0=20-=205=E7=BB=B4Spec?= =?UTF-8?q?=E9=87=8D=E6=9E=84=E6=96=87=E6=A1=A3=EF=BC=88=E6=BC=94=E6=92=AD?= =?UTF-8?q?=E5=AE=A4=E5=B1=82=E8=A1=A5=E5=85=A8=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/PLAN_5DIM_REFACTOR.md | 358 +++++++++++++++++++++++++++++++++++++ 1 file changed, 358 insertions(+) create mode 100644 docs/PLAN_5DIM_REFACTOR.md diff --git a/docs/PLAN_5DIM_REFACTOR.md b/docs/PLAN_5DIM_REFACTOR.md new file mode 100644 index 0000000..5480892 --- /dev/null +++ b/docs/PLAN_5DIM_REFACTOR.md @@ -0,0 +1,358 @@ +# 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` — 座位模板管理