docs: v3.0 vr_goods_config spec + Phase2 plan v3.0

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
council/ProductManager
Council 2026-04-20 08:30:00 +08:00
parent 977cc57aef
commit 6daa332323
2 changed files with 308 additions and 226 deletions

View File

@ -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_baseinventory > 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 1BatchGenerate 写入 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 2GetGoodsViewData 重写
### 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 3ticket_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 3AdminGoodsSaveHandle 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() 实现 |

View File

@ -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` 不影响已发布的商品。SKUspec_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=房间IDvalue=启用的分区字符列表(如 `["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 每次都重建全量 specdelete + 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 = <?php echo json_encode($vr_seat_template['spec_base_id_map'] ?? []); ?>;
// 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 表路径 |