From 6daa3323231bcb6cd59d9bf129b46567aa9817a5 Mon Sep 17 00:00:00 2001 From: Council Date: Mon, 20 Apr 2026 08:30:00 +0800 Subject: [PATCH] docs: v3.0 vr_goods_config spec + Phase2 plan v3.0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit v3.0 核心变更: - selected_sections 改为数组格式 [A,B] - spec_base_id_map 使用 goods_spec_base.extends 动态构建 - seat_key 格式: roomId_rowLabel_colNum(无 MD5) - 完整规格文档: docs/VR_GOODS_CONFIG_SPEC.md --- docs/PHASE2_PLAN.md | 193 +++++++++----------- docs/VR_GOODS_CONFIG_SPEC.md | 341 +++++++++++++++++++++++------------ 2 files changed, 308 insertions(+), 226 deletions(-) diff --git a/docs/PHASE2_PLAN.md b/docs/PHASE2_PLAN.md index cf575cb..d31304b 100644 --- a/docs/PHASE2_PLAN.md +++ b/docs/PHASE2_PLAN.md @@ -1,18 +1,18 @@ # Phase 2 — 计划与当前状态 -> 版本:v2.0 | 日期:2026-04-20 | 状态:执行中 -> 关联提交:c894e7018(模板渲染已通)、1b0ac3276(精简 footer) -> 关联文档:`docs/VR_GOODS_CONFIG_SPEC.md`(新 JSON 格式规格) +> 版本:v3.0 | 日期:2026-04-20 | 状态:实现准备就绪 +> 关联提交:c894e7018(模板渲染)、1b0ac3276(精简 footer) +> 关联文档:`docs/VR_GOODS_CONFIG_SPEC.md`(v3.0 JSON 格式,已确认) --- -## ⚠️ 重大更新(v2.0) +## ⚠️ v3.0 重大变更摘要 -**vr_goods_config JSON 格式已重新设计。** +- `vr_goods_config` 包含完整 `rooms[]` 快照,不再查 `vr_seat_templates` 表 +- `spec_base_id_map` 不入库,GetGoodsViewData 动态从 `goods_spec_base.extends->seat_key` 构建 +- `selected_sections` 改为数组格式 `["A","B"]`(不是对象) -旧格式依赖 `vr_seat_templates` 表实时查询,新格式在商品发布时将 `rooms` 快照存入商品表,前端不再跨表查询。 - -详见 `docs/VR_GOODS_CONFIG_SPEC.md`。 +完整规格见 `docs/VR_GOODS_CONFIG_SPEC.md`。 --- @@ -22,137 +22,112 @@ | 任务 | 提交 | 说明 | |------|------|------| -| 模板渲染 | c894e7018 | ThinkTemplate → PHP ModuleInclude,渲染正常 | -| 票务专用 footer | 1b0ac3276 | 精简 footer,移除 ShopXO 默认导航 | -| Goods.php 改法 | 7bd896764 | item_type=ticket → ticket_detail.html | -| onOrderPaid() 修复 | 7bd896764 | sxo_order_detail + JSON spec 解析 | +| 模板渲染 | c894e7018 | PHP ModuleInclude,渲染正常 | +| 票务专用 footer | 1b0ac3276 | 精简 footer | -### ⚠️ 进行中 +### ⚠️ 实现准备就绪(待动手) | 任务 | 说明 | 依赖 | |------|------|------| -| GetGoodsViewData() 重写 | 适配新 vr_goods_config JSON 格式 | VR_GOODS_CONFIG_SPEC.md 已确认 | -| 前端 JS 更新 | 适配 rooms[] 结构渲染 | GetGoodsViewData() 输出格式确定后 | -| loadSoldSeats() 实现 | 查询 vr_tickets,标记已售座位 | vr_tickets 表有数据后 | +| BatchGenerate 写入 extends | 存储 seat_key 到 goods_spec_base.extends | VR_GOODS_CONFIG_SPEC.md 已确认 | +| GetGoodsViewData 重写 | 动态构建 spec_base_id_map + 适配 selected_sections | 同上 | +| ticket_detail.html JS 更新 | seatKey 格式改为 roomId_rowLabel_colNum | 同上 | ### ❌ 未开始 -| 任务 | 说明 | -|------|------| -| AdminGoodsSaveHandle SKU 生成 | 根据 selected_rooms 生成 goods_spec_base 条目 | -| 后台 4 控制器联调 | SeatTemplate/Ticket/Verifier/Verification | -| 核销 API | POST /api/vr_ticket/verify | +| 任务 | +|------| +| 核销 API | +| 后台 4 控制器联调 | +| AdminGoodsSaveHandle selected_sections 透传确认 | --- -## 二、vr_goods_config 新格式(已确认) +## 二、vr_goods_config v3.0 结构(已确认) ```json -[ - { - "template_id": 4, - "selected_rooms": ["room_id_xxx"], - "selected_sections": { "room_id_xxx": ["A", "B"] }, - "rooms": [ - { - "id": "room_id_xxx", - "name": "1号放映室VV", - "map": ["AAAAB__BBB_BAAAA", "AAAAB__BBB_BAAAA"], - "sections": [ - { "char": "A", "name": "VIP区", "price": 100, "color": "#f06292" }, - { "char": "B", "name": "看台区", "price": 50, "color": "#4fc3f7" } - ], - "seats": { - "A": { "char": "A", "name": "VIP区", "price": 100, "color": "#f06292" }, - "B": { "char": "B", "name": "看台区", "price": 50, "color": "#4fc3f7" } - } - } - ], - "sessions": [ - { "start": "15:00", "end": "16:59" }, - { "start": "18:00", "end": "21:59" } - ] - } -] +{ + "version": 1.0, + "template_id": 4, + "selected_rooms": ["room_id_xxx"], + "selected_sections": ["A", "B"], + "sessions": [{ "start": "15:00", "end": "16:59" }], + "venue": { "name": "...", "address": "...", "location": {}, "images": [] }, + "rooms": [{ "id": "room_id_xxx", "name": "...", "map": [...], "sections": [...], "seats": {...} }] +} ``` -**核心变化**: -- `rooms[]` 包含完整座位图+sections+seats,不再查 `vr_seat_templates` 表 -- `selected_sections` 控制每个房间内渲染哪些分区 -- `spec_base_id_map` 格式:`{room_id}_{row}_{colNum}` → `spec_base_id` - -完整规格见 `docs/VR_GOODS_CONFIG_SPEC.md`。 +详细字段说明见 `docs/VR_GOODS_CONFIG_SPEC.md` 第一章。 --- -## 三、下一步工作 +## 三、spec_base_id_map 解决方案(已确认) -### Step 1(立即):重写 GetGoodsViewData() +### 断路根因 -**目标**:适配新 JSON 格式,从 `goods.vr_goods_config` 直接读取 rooms[],注入前端模板。 +BatchGenerate 生成 GoodsSpecBase.id 后,从未写入 spec_base_id_map。前端用 `roomId_row_col` 格式查,存储端从未按此格式写入。 + +### 解决方案:使用 goods_spec_base.extends + +``` +BatchGenerate 写入时: + extends.seat_key = "room_id_rowLabel_colNum" (无 MD5) + +GetGoodsViewData 读取时: + 遍历 goods_spec_base(inventory > 0) + → 解析 extends.seat_key + → 构建 spec_base_id_map[key] = id + +前端 submit() 时: + seatKey = seat.roomId + '_' + seat.rowLabel + '_' + seat.colNum + specBaseId = spec_base_id_map[seatKey] +``` + +详见 `docs/VR_GOODS_CONFIG_SPEC.md` 第三章。 + +--- + +## 四、下一步工作(实现顺序) + +### Step 1:BatchGenerate 写入 extends -**新输出格式**: ```php -[ - 'vr_seat_template' => [ - 'rooms' => [...], // 直接透传 vr_goods_config[0].rooms - 'sessions' => [...], // 直接透传 vr_goods_config[0].sessions - 'selected_sections' => {...}, // 直接透传 vr_goods_config[0].selected_sections - ], - 'goods_spec_data' => [...], // 场次+价格(用于前端卡片) - 'goods_config' => {...} // 原始 vr_goods_config[0] -] +// SeatSkuService::BatchGenerate() 中,insertGetId 前: +$extends = json_encode([ + 'seat_key' => $roomId . '_' . $rowLabel . '_' . $col +], JSON_UNESCAPED_UNICODE); + +// insertGetId 中加入: +'extends' => $extends, ``` -**降级兼容**:若 `vr_goods_config` 中无 `rooms` 字段,按旧逻辑查 `vr_seat_templates` 表。 +### Step 2:GetGoodsViewData 重写 -### Step 2(立即):更新 ticket_detail.html JS +```php +// 1. 读取 vr_goods_config[0] +// 2. 直接透传 rooms[] / sessions[] / selected_sections / venue +// 3. 动态构建 spec_base_id_map(从 goods_spec_base.extends) +// 4. 生成 goods_spec_data(场次+最低价聚合) +// 5. 返回 { vr_seat_template, goods_spec_data, goods_config } +``` -**目标**:适配 `rooms[]` 结构,多房间支持。 +### Step 3:ticket_detail.html JS -**改动点**: -1. `seatMap` → `rooms[]`(数组,每个房间一个座位图) -2. `renderSeatMap()` → `renderRoom(roomIndex)`(按房间渲染) -3. `specBaseIdMap` 格式变为 `{room_id}_{row}_{colNum}` → `spec_base_id` -4. `loadSoldSeats()` 实现:查 `vr_tickets.seat_info` 格式为 `room_id/rowLabel/colNum` - -### Step 3:AdminGoodsSaveHandle SKU 生成 - -**目标**:用户选择房间后,在商品保存时自动生成 `goods_spec_base` SKU 条目。 - -**逻辑**:展开 rooms[].map,生成每个座位的 SKU,存入 `spec_base_id_map`。 +```javascript +// seatKey 格式改为带 roomId: +var seatKey = seat.roomId + '_' + seat.rowLabel + '_' + seat.colNum; +// 例:"room_id_1776341371905_A_3" +var specBaseId = self.specBaseIdMap[seatKey] || 0; +``` --- -## 四、模板渲染当前状态 +## 五、模板渲染当前状态 | 项目 | 状态 | |------|------| -| 模板渲染 | ✅ 正常(PHP ModuleInclude 方案) | +| 模板渲染 | ✅ 正常 | | 票务 footer | ✅ 已精简 | -| 场次显示 | ❌ 待适配新 JSON 格式 | -| 座位图渲染 | ❌ 待适配 rooms[] 结构 | -| 已售座位标记 | ❌ 待实现 loadSoldSeats() | - ---- - -## 五、数据库表结构(当前) - -| 表名 | 用途 | 状态 | -|------|------|------| -| `vrt_vr_seat_templates` | 场馆模板(rooms 母版) | ✅ | -| `vrt_vr_tickets` | 电子票 | ✅ | -| `vrt_vr_verifiers` | 核销员 | ✅ | -| `vrt_vr_verifications` | 核销记录 | ✅ | -| `goods.vr_goods_config` | 商品配置快照(新 JSON 格式) | ⚠️ 待适配 | -| `goods_spec_base` | SKU 库存(ShopXO 原生平表) | ⚠️ 待自动生成 | - ---- - -## 六、已知风险 - -| 风险 | 影响 | 缓解 | -|------|------|------| -| vr_goods_config 仍是旧格式 | 场次/座位图不显示 | AdminGoodsSaveHandle 生成新格式后可解决 | -| 旧版 GetGoodsViewData 未适配新格式 | 前端无数据 | Step 1 完成后解决 | -| spec_base_id_map 格式变化 | 已选座位提交逻辑需同步更新 | Step 2 中同步更新 JS | +| 场次显示 | ❌ 待 GetGoodsViewData 重写 | +| 座位图渲染 | ❌ 待 GetGoodsViewData + JS 更新 | +| 已售座位标记 | ❌ 待 loadSoldSeats() 实现 | diff --git a/docs/VR_GOODS_CONFIG_SPEC.md b/docs/VR_GOODS_CONFIG_SPEC.md index 61e1c2b..ff7c638 100644 --- a/docs/VR_GOODS_CONFIG_SPEC.md +++ b/docs/VR_GOODS_CONFIG_SPEC.md @@ -1,27 +1,38 @@ # vr_goods_config JSON 规格说明 -> 版本:v2.0 | 日期:2026-04-20 | 状态:已确认,待实现 +> 版本:v3.0 | 日期:2026-04-20 | 状态:**已确认,待实现** +> 关联提交:待实现 --- -## 一、设计原则 +## ⚠️ 重要:v3.0 vs 旧版本的区别 -1. **商品发布时快照**:用户在后端选择场馆房间后,将完整的房间数据**复制一份**存入 `goods.vr_goods_config`。不从 `vr_seat_templates` 实时读取。 -2. **绝对一致性**:修改 `vr_seat_templates` 不影响已发布的商品。SKU(spec_base)和 `vr_goods_config` 一起过时、一起更新。 -3. **向下兼容**:保留 `template_id` 字段(用于标识来源),但不再用它去查 `vr_seat_templates` 表。 -4. **单一真相源**:前端渲染所需的所有数据(座位图、场次、价格)全部来自 `vr_goods_config` 的快照,不跨表查询。 +**v2.0(旧)**:rooms/sections/seats 存放在 `vr_seat_templates.seat_map` JSON 里,前端需要跨表查询。 + +**v3.0(新)**:完整快照直接嵌入 `goods.vr_goods_config`,前端完全不跨表。 + +**破坏性变更**:`selected_sections` 从**对象格式** `{"room_id": ["A","B"]}` 改为**数组格式** `["A","B"]`(每个房间一个选中分区列表)。 --- -## 二、vr_goods_config JSON 结构 +## 一、vr_goods_config 完整结构 ```json [ { + "version": 1.0, "template_id": 4, - "selected_rooms": ["room_id_1776341371905"], - "selected_sections": { - "room_id_1776341371905": ["A", "B"] + "selected_rooms": ["room_id_1776341371905", "room_id_1776341444657"], + "selected_sections": ["A", "B"], + "sessions": [ + { "start": "15:00", "end": "16:59" }, + { "start": "18:00", "end": "21:59" } + ], + "venue": { + "name": "测试 2", + "address": "测试地址", + "location": { "lng": "", "lat": "" }, + "images": [] }, "rooms": [ { @@ -41,10 +52,6 @@ "B": { "char": "B", "name": "看台区", "price": 50, "color": "#4fc3f7" } } } - ], - "sessions": [ - { "start": "15:00", "end": "16:59" }, - { "start": "18:00", "end": "21:59" } ] } ] @@ -54,137 +61,237 @@ | 字段 | 类型 | 必填 | 说明 | |------|------|------|------| -| `template_id` | int | ✅ | 来源场馆模板 ID(用于溯源,不用于查询) | -| `selected_rooms` | string[] | ✅ | 本商品启用的房间 ID 列表 | -| `selected_sections` | object | ✅ | key=房间ID,value=启用的分区字符列表(如 `["A","B"]`) | -| `rooms` | object[] | ✅ | 房间完整数据快照(直接复制自 `vr_seat_templates.rooms`) | -| `sessions` | object[] | ✅ | 本商品的场次列表 | - -### rooms.seats 字段说明 - -`seats` 是 `sections` 的快捷索引,key = `char`(座位字符),格式与 `sections` 条目相同: -```json -"seats": { - "A": { "char": "A", "name": "VIP区", "price": 100, "color": "#f06292" }, - "B": { "char": "B", "name": "看台区", "price": 50, "color": "#4fc3f7" } -} -``` - -### 向下兼容(旧格式迁移) - -旧格式(有 `vr_seat_templates` 表关联逻辑): -```json -{ - "template_id": 4, - "sessions": [{"start": "...", "end": "..."}] -} -``` -识别方式:`rooms` 字段不存在 → 降级读取 `vr_seat_templates` 表。 +| `version` | float | ✅ | 协议版本,当前 1.0,用于前向兼容判断 | +| `template_id` | int | ✅ | 来源场馆模板 ID(溯源用,不用于查询) | +| `selected_rooms` | string[] | ✅ | 本商品启用的房间 ID 列表(决定渲染哪些房间) | +| `selected_sections` | string[] | ✅ | 本商品选中的分区字符列表(当前房间的分区,`["A","B"]`) | +| `sessions` | object[] | ✅ | 本商品场次列表 | +| `venue` | object | ✅ | 场馆基本信息快照 | +| `rooms` | object[] | ✅ | 房间完整数据快照(直接复制自 vr_seat_templates.rooms) | --- -## 三、SKU 生成逻辑(AdminGoodsSaveHandle Hook) +## 二、selected_sections 格式说明 -商品保存时,根据 `selected_rooms` 数组,从 `vr_seat_templates.rooms` 取出对应房间,展开每个房间的 `map` 座位,生成 SKU 条目到 `goods_spec_base` + `goods_spec_value`。 +**格式**:`string[]` — 选中分区的字符列表,应用于 `selected_rooms` 中的**当前选中房间**。 -``` -rooms[room_id].map - └─ 每行字符串(如 "AAAAB__BBB_BAAAA") - └─ 每个非 _ / - 的字符 → 一个 SKU - ├─ goods_spec_base.id → 库存主键 - ├─ goods_spec_base.spec_name → "排:row, 座:colNum" - ├─ goods_spec_base.price → seats[char].price - └─ goods_spec_base.spec_type → "vrseat:{room_id}:{char}" +**为什么不是对象**:`{"room_id": ["A","B"]}`(v2.0 旧格式)改为 `["A","B"]`。 + +**简化理由**:用户在后端编辑商品时,一次只能编辑一个房间的分区选择。所以 `selected_sections` 是"当前房间的选中分区",不需要跨房间存储。 + +```php +// AdminGoodsSaveHandle 中的转换(旧格式 → 新格式) +// 旧格式(已废弃): +// $config['selected_sections'] = { "room_id_xxx": ["A","B"] } + +// 新格式(v3.0): +// 前端传入 selected_sections: ["A","B"] +// 直接存储,不做任何转换 +$selectedSections = $config['selected_sections'] ?? []; + +// BatchGenerate 适配(详见第五章) ``` -**spec_base_id_map(存到 vr_goods_config 的 rooms[] 中)格式:** +--- + +## 三、spec_base_id_map 生成与存储 + +### 3.1 当前断路问题 + +``` +BatchGenerate() → 生成 GoodsSpecBase.id + → 从未写入 spec_base_id_map ← 断路 + +Admin.php Save → 从 form 存旧格式 spec_base_id_map(空或无效) +GetGoodsViewData → 读 vr_seat_templates.spec_base_id_map(旧路径) +前端 JS → 用 "A_3" 格式查 → key 不匹配 → 永远查不到 +``` + +### 3.2 解决方案:使用 goods_spec_base.extends 字段 + +ShopXO 原生 `goods_spec_base` 表有 `extends` 字段(JSON 扩展数据),我们的 BatchGenerate 每次都重建全量 spec(delete + insert),所以写入 `extends` 不会被覆盖。 + +**存储时**(BatchGenerate 写入 GoodsSpecBase): +```php +$extends = json_encode([ + 'seat_key' => $roomId . '_' . $rowLabel . '_' . $col // 例:"room_id_xxx_A_3" +], JSON_UNESCAPED_UNICODE); + +Db::name('GoodsSpecBase')->insertGetId([ + 'goods_id' => $goodsId, + 'price' => $seatPrice, + 'inventory' => 1, + // ... 其他字段 + 'extends' => $extends, // ← 新增 +]); +``` + +**读取时**(GetGoodsViewData 动态构建 spec_base_id_map): +```php +$specs = Db::name('GoodsSpecBase') + ->where('goods_id', $goodsId) + ->where('inventory', '>', 0) + ->select(); + +$specBaseIdMap = []; +foreach ($specs as $spec) { + $ext = json_decode($spec['extends'] ?? '{}', true); + if (!empty($ext['seat_key'])) { + $specBaseIdMap[$ext['seat_key']] = intval($spec['id']); + } +} +``` + +**spec_base_id_map 最终格式**: ```json { - "{room_id}_{row}_{colNum}": goods_spec_base.id + "room_id_1776341371905_A_3": 2001, + "room_id_1776341371905_B_5": 2002, + "room_id_1776341444657_A_1": 2050 } ``` -> 注:具体 SKU 生成字段名/存储位置待 AdminGoodsSaveHandle 实现时确认。 +### 3.3 字段不冲突说明 + +`extends` 字段是 ShopXO 的标准扩展字段,ShopXO 自己的 GoodsSave 更新 spec 时只修改核心字段(price/inventory 等),不碰 `extends`。而我们的 BatchGenerate 在商品保存时全量重建 spec,`extends` 由我们写入,不存在被覆盖的风险。 --- -## 四、前端渲染数据流 +## 四、前端数据结构(GetGoodsViewData 输出) -``` -goods.vr_goods_config(快照) - └─ [0].rooms[] → 前端 JS rooms[] - └─ [0].sessions[] → 场次卡片 - └─ [0].selected_sections{} → 控制哪些分区渲染 -``` - -### 前端数据结构 - -```javascript -// GetGoodsViewData() 注入给模板 -{ - vr_seat_template: { - rooms: [...], // rooms 快照数组 - sessions: [...], // 场次列表 - selected_sections: {} // 分区过滤 - }, - goods_spec_data: [...], // 场次规格(price 来自 goods_spec_base) - goods_config: { ... } // 原始 vr_goods_config[0] -} -``` - -### loadSoldSeats(已选座位) - -从 `vr_tickets` 表查询该商品+当前场次已生成的票: -```sql -SELECT seat_info FROM vrt_vr_tickets -WHERE goods_id = :goods_id AND verify_status != 1 -``` -`seat_info` 格式:`"room_id/rowLabel/colNum"`(例:`room_id_xxx/A/3`) - ---- - -## 五、GetGoodsViewData() 重写要点 - -**输入**:`goods_id` - -**输出**: ```php [ 'vr_seat_template' => [ - 'rooms' => [...], // 来自 vr_goods_config[0].rooms - 'sessions' => [...], // 来自 vr_goods_config[0].sessions - 'selected_sections' => {...}, // 来自 vr_goods_config[0].selected_sections + 'venue' => $config['venue'], // 场馆信息 + 'rooms' => $config['rooms'], // 房间快照 + 'sessions' => $config['sessions'], // 场次列表 + 'selected_rooms' => $config['selected_rooms'], + 'selected_sections'=> $config['selected_sections'], + 'spec_base_id_map' => $specBaseIdMap, // 动态构建(见3.2) ], - 'goods_spec_data' => [...], // 场次+价格(用于前端场次卡片) - 'goods_config' => {...} // 原始 vr_goods_config[0] + 'goods_spec_data' => [...], // 场次+价格(用于前端场次卡片) + 'goods_config' => $config // 原始 vr_goods_config[0] ] ``` -**逻辑**: -1. 读取 `goods.vr_goods_config` JSON -2. 若 `rooms` 字段存在 → 直接使用(新格式) -3. 若 `rooms` 不存在 → 降级:按旧逻辑查 `vr_seat_templates` 表(旧格式兼容) -4. 场次价格从 `goods_spec_base` 表读取 +### goods_spec_data 生成逻辑 + +从 `goods_spec_base` 按场次维度聚合(每个座位 = 一条 GoodsSpecBase): + +```php +// 取每个场次的最低价格作为卡片显示价 +$specs = Db::name('GoodsSpecBase') + ->where('goods_id', $goodsId) + ->where('inventory', '>', 0) + ->select(); + +$sessionPrices = []; // sessionStr => minPrice +$sessionSpecs = []; // sessionStr => first spec_base_id + +foreach ($specs as $spec) { + // 从 goods_spec_value 找到场次维度值 + $sessionValue = Db::name('GoodsSpecValue') + ->where('goods_spec_base_id', $spec['id']) + ->where('value', 'like', '%-%:%') + ->find(); + + if ($sessionValue) { + $sessionStr = $sessionValue['value']; + if (!isset($sessionPrices[$sessionStr]) || $spec['price'] < $sessionPrices[$sessionStr]) { + $sessionPrices[$sessionStr] = floatval($spec['price']); + } + if (!isset($sessionSpecs[$sessionStr])) { + $sessionSpecs[$sessionStr] = intval($spec['id']); + } + } +} + +// 构建 goods_spec_data +$goodsSpecData = []; +foreach ($sessions as $s) { + $start = $s['start'] ?? ''; + $end = $s['end'] ?? ''; + $sessionStr = $start && $end ? "{$start}-{$end}" : ($start ?: $end); + $goodsSpecData[] = [ + 'spec_id' => $sessionSpecs[$sessionStr] ?? 0, + 'spec_name' => $sessionStr, + 'price' => $sessionPrices[$sessionStr] ?? floatval($goods['price'] ?? 0), + ]; +} +``` + +### 前端 JS 使用方式 + +```javascript +// ticket_detail.html JS +var specBaseIdMap = ; + +// submit() 时:根据选中座位查 spec_base_id +var seatKey = seat.roomId + '_' + seat.rowLabel + '_' + seat.colNum; +// 例:"room_id_1776341371905_A_3" +var specBaseId = specBaseIdMap[seatKey] || 0; + +// goods_params 格式(每座一行) +{ + goods_id: goodsId, + spec_base_id: specBaseId, // ← 用 seatKey 查到 + stock: 1, + extension_data: JSON.stringify({ attendee, seat: {...} }) +} +``` --- -## 六、需要更新的文件 +## 五、需要修改的文件 -| 文件 | 操作 | 说明 | -|------|------|------| -| `SeatSkuService.php` | 重写 GetGoodsViewData() | 新 JSON 格式解析 | -| `ticket_detail.html` | 更新 JS | rooms[] 结构渲染 + loadSoldSeats | -| `docs/VR_GOODS_CONFIG_SPEC.md` | 新建 | 本文档,记录 JSON 规格 | -| `docs/PHASE2_PLAN.md` | 更新 | 补充新格式 + 待办 | -| `docs/DEVELOPMENT_LOG.md` | 追加 | 记录本次 JSON 格式升级 | +| 文件 | 改动 | +|------|------| +| `SeatSkuService::BatchGenerate()` | 写入 `extends` JSON(含 `seat_key`) | +| `SeatSkuService::GetGoodsViewData()` | 动态从 `extends` 构建 `spec_base_id_map`;适配 `selected_sections` 数组格式 | +| `ticket_detail.html` JS | `seatKey` 格式改为 `roomId + '_' + rowLabel + '_' + colNum` | +| `AdminGoodsSaveHandle` | selected_sections 透传(已经是数组格式,无需改动) | --- -## 七、已确认的设计决策 +## 六、数据库迁移(如需要) -1. ✅ 商品发布时快照 `vr_seat_templates.rooms` 到 `goods.vr_goods_config.rooms` -2. ✅ `vr_goods_config` 包含完整的座位图+sections+seats 数据 -3. ✅ 前端不跨表查询,全部数据来自 `vr_goods_config` 快照 -4. ✅ `spec_base_id_map` 格式:`{room_id}_{row}_{colNum}` → `spec_base_id` -5. ✅ 座位已售状态:查 `vr_tickets.seat_info`(格式:`room_id/rowLabel/colNum`) -6. ⚠️ SKU 生成字段名/存储位置:待 AdminGoodsSaveHandle 实现时确认 +如后续需要在 `goods_spec_base` 加专用列(非必须,`extends` 已够用): + +```sql +ALTER TABLE `{{prefix}}goods_spec_base` ADD COLUMN `seat_key` VARCHAR(100) DEFAULT NULL COMMENT '座位键:roomId_row_col'; +``` + +--- + +## 七、版本判断(降级兼容) + +```php +$config = json_decode($goods['vr_goods_config'] ?? '', true); +if (empty($config)) { + return ['vr_seat_template' => null, 'goods_spec_data' => [], 'goods_config' => null]; +} + +$config = $config[0]; + +if (empty($config['rooms'])) { + // v1/v2 旧格式:降级读 vr_seat_templates 表 + return self::GetGoodsViewDataLegacy($goodsId, $config); +} + +// v3.0 新格式 +$version = $config['version'] ?? 1.0; +// ... +``` + +--- + +## 八、已确认的设计决策 + +| 决策 | 结论 | +|------|------| +| `selected_sections` 格式 | 数组 `["A","B"]`,不是对象 | +| `spec_base_id_map` 存储 | 不入库,GetGoodsViewData 动态构建 | +| seat_key 存储位置 | `goods_spec_base.extends->seat_key`(JSON) | +| seat_key 格式 | `{roomId}_{rowLabel}_{colNum}`(无 MD5) | +| goods_spec_data.price | 取该场次所有座位中的最低价(用于卡片显示) | +| 降级兼容 | `version` 字段判断,v1/v2 走旧 vr_seat_templates 表路径 |