2026-04-19 23:24:57 +00:00
|
|
|
|
# vr_goods_config JSON 规格说明
|
|
|
|
|
|
|
2026-04-20 00:30:00 +00:00
|
|
|
|
> 版本:v3.0 | 日期:2026-04-20 | 状态:**已确认,待实现**
|
|
|
|
|
|
> 关联提交:待实现
|
2026-04-19 23:24:57 +00:00
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
2026-04-20 00:30:00 +00:00
|
|
|
|
## ⚠️ 重要:v3.0 vs 旧版本的区别
|
2026-04-19 23:24:57 +00:00
|
|
|
|
|
2026-04-20 00:30:00 +00:00
|
|
|
|
**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"]`(每个房间一个选中分区列表)。
|
2026-04-19 23:24:57 +00:00
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
2026-04-20 00:30:00 +00:00
|
|
|
|
## 一、vr_goods_config 完整结构
|
2026-04-19 23:24:57 +00:00
|
|
|
|
|
|
|
|
|
|
```json
|
|
|
|
|
|
[
|
|
|
|
|
|
{
|
2026-04-20 00:30:00 +00:00
|
|
|
|
"version": 1.0,
|
2026-04-19 23:24:57 +00:00
|
|
|
|
"template_id": 4,
|
2026-04-20 00:30:00 +00:00
|
|
|
|
"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": []
|
2026-04-19 23:24:57 +00:00
|
|
|
|
},
|
|
|
|
|
|
"rooms": [
|
|
|
|
|
|
{
|
|
|
|
|
|
"id": "room_id_1776341371905",
|
|
|
|
|
|
"name": "1号放映室VV",
|
|
|
|
|
|
"map": [
|
|
|
|
|
|
"AAAAB__BBB_BAAAA",
|
|
|
|
|
|
"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" }
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
]
|
|
|
|
|
|
}
|
|
|
|
|
|
]
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 字段说明
|
|
|
|
|
|
|
|
|
|
|
|
| 字段 | 类型 | 必填 | 说明 |
|
|
|
|
|
|
|------|------|------|------|
|
2026-04-20 00:30:00 +00:00
|
|
|
|
| `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) |
|
2026-04-19 23:24:57 +00:00
|
|
|
|
|
2026-04-20 00:30:00 +00:00
|
|
|
|
---
|
2026-04-19 23:24:57 +00:00
|
|
|
|
|
2026-04-20 00:30:00 +00:00
|
|
|
|
## 二、selected_sections 格式说明
|
2026-04-19 23:24:57 +00:00
|
|
|
|
|
2026-04-20 00:30:00 +00:00
|
|
|
|
**格式**:`string[]` — 选中分区的字符列表,应用于 `selected_rooms` 中的**当前选中房间**。
|
2026-04-19 23:24:57 +00:00
|
|
|
|
|
2026-04-20 00:30:00 +00:00
|
|
|
|
**为什么不是对象**:`{"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 适配(详见第五章)
|
2026-04-19 23:24:57 +00:00
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
2026-04-20 00:30:00 +00:00
|
|
|
|
## 三、spec_base_id_map 生成与存储
|
2026-04-19 23:24:57 +00:00
|
|
|
|
|
2026-04-20 00:30:00 +00:00
|
|
|
|
### 3.1 当前断路问题
|
2026-04-19 23:24:57 +00:00
|
|
|
|
|
|
|
|
|
|
```
|
2026-04-20 00:30:00 +00:00
|
|
|
|
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 不匹配 → 永远查不到
|
2026-04-19 23:24:57 +00:00
|
|
|
|
```
|
|
|
|
|
|
|
2026-04-20 00:30:00 +00:00
|
|
|
|
### 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 最终格式**:
|
2026-04-19 23:24:57 +00:00
|
|
|
|
```json
|
|
|
|
|
|
{
|
2026-04-20 00:30:00 +00:00
|
|
|
|
"room_id_1776341371905_A_3": 2001,
|
|
|
|
|
|
"room_id_1776341371905_B_5": 2002,
|
|
|
|
|
|
"room_id_1776341444657_A_1": 2050
|
2026-04-19 23:24:57 +00:00
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
2026-04-20 00:30:00 +00:00
|
|
|
|
### 3.3 字段不冲突说明
|
|
|
|
|
|
|
|
|
|
|
|
`extends` 字段是 ShopXO 的标准扩展字段,ShopXO 自己的 GoodsSave 更新 spec 时只修改核心字段(price/inventory 等),不碰 `extends`。而我们的 BatchGenerate 在商品保存时全量重建 spec,`extends` 由我们写入,不存在被覆盖的风险。
|
2026-04-19 23:24:57 +00:00
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
2026-04-20 00:30:00 +00:00
|
|
|
|
## 四、前端数据结构(GetGoodsViewData 输出)
|
2026-04-19 23:24:57 +00:00
|
|
|
|
|
2026-04-20 00:30:00 +00:00
|
|
|
|
```php
|
|
|
|
|
|
[
|
|
|
|
|
|
'vr_seat_template' => [
|
|
|
|
|
|
'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' => $config // 原始 vr_goods_config[0]
|
|
|
|
|
|
]
|
2026-04-19 23:24:57 +00:00
|
|
|
|
```
|
2026-04-20 00:30:00 +00:00
|
|
|
|
|
|
|
|
|
|
### 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),
|
|
|
|
|
|
];
|
|
|
|
|
|
}
|
2026-04-19 23:24:57 +00:00
|
|
|
|
```
|
|
|
|
|
|
|
2026-04-20 00:30:00 +00:00
|
|
|
|
### 前端 JS 使用方式
|
2026-04-19 23:24:57 +00:00
|
|
|
|
|
|
|
|
|
|
```javascript
|
2026-04-20 00:30:00 +00:00
|
|
|
|
// 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 格式(每座一行)
|
2026-04-19 23:24:57 +00:00
|
|
|
|
{
|
2026-04-20 00:30:00 +00:00
|
|
|
|
goods_id: goodsId,
|
|
|
|
|
|
spec_base_id: specBaseId, // ← 用 seatKey 查到
|
|
|
|
|
|
stock: 1,
|
|
|
|
|
|
extension_data: JSON.stringify({ attendee, seat: {...} })
|
2026-04-19 23:24:57 +00:00
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
2026-04-20 00:30:00 +00:00
|
|
|
|
---
|
2026-04-19 23:24:57 +00:00
|
|
|
|
|
2026-04-20 00:30:00 +00:00
|
|
|
|
## 五、需要修改的文件
|
|
|
|
|
|
|
|
|
|
|
|
| 文件 | 改动 |
|
|
|
|
|
|
|------|------|
|
|
|
|
|
|
| `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 透传(已经是数组格式,无需改动) |
|
2026-04-19 23:24:57 +00:00
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
2026-04-20 00:30:00 +00:00
|
|
|
|
## 六、数据库迁移(如需要)
|
2026-04-19 23:24:57 +00:00
|
|
|
|
|
2026-04-20 00:30:00 +00:00
|
|
|
|
如后续需要在 `goods_spec_base` 加专用列(非必须,`extends` 已够用):
|
2026-04-19 23:24:57 +00:00
|
|
|
|
|
2026-04-20 00:30:00 +00:00
|
|
|
|
```sql
|
|
|
|
|
|
ALTER TABLE `{{prefix}}goods_spec_base` ADD COLUMN `seat_key` VARCHAR(100) DEFAULT NULL COMMENT '座位键:roomId_row_col';
|
2026-04-19 23:24:57 +00:00
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
2026-04-20 00:30:00 +00:00
|
|
|
|
## 七、版本判断(降级兼容)
|
|
|
|
|
|
|
|
|
|
|
|
```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);
|
|
|
|
|
|
}
|
2026-04-19 23:24:57 +00:00
|
|
|
|
|
2026-04-20 00:30:00 +00:00
|
|
|
|
// v3.0 新格式
|
|
|
|
|
|
$version = $config['version'] ?? 1.0;
|
|
|
|
|
|
// ...
|
|
|
|
|
|
```
|
2026-04-19 23:24:57 +00:00
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
2026-04-20 00:30:00 +00:00
|
|
|
|
## 八、已确认的设计决策
|
2026-04-19 23:24:57 +00:00
|
|
|
|
|
2026-04-20 00:30:00 +00:00
|
|
|
|
| 决策 | 结论 |
|
|
|
|
|
|
|------|------|
|
|
|
|
|
|
| `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 表路径 |
|