vr-shopxo-plugin/docs/VR_GOODS_CONFIG_SPEC.md

298 lines
9.4 KiB
Markdown
Raw 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.

# vr_goods_config JSON 规格说明
> 版本v3.0 | 日期2026-04-20 | 状态:**已确认,待实现**
> 关联提交:待实现
---
## ⚠️ 重要v3.0 vs 旧版本的区别
**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
[
{
"version": 1.0,
"template_id": 4,
"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": [
{
"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" }
}
}
]
}
]
```
### 字段说明
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `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 |
---
## 二、selected_sections 格式说明
**格式**`string[]` — 选中分区的字符列表,应用于 `selected_rooms` 中的**当前选中房间**。
**为什么不是对象**`{"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 生成与存储
### 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_1776341371905_A_3": 2001,
"room_id_1776341371905_B_5": 2002,
"room_id_1776341444657_A_1": 2050
}
```
### 3.3 字段不冲突说明
`extends` 字段是 ShopXO 的标准扩展字段ShopXO 自己的 GoodsSave 更新 spec 时只修改核心字段price/inventory 等),不碰 `extends`。而我们的 BatchGenerate 在商品保存时全量重建 spec`extends` 由我们写入,不存在被覆盖的风险。
---
## 四、前端数据结构GetGoodsViewData 输出)
```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]
]
```
### 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::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 透传(已经是数组格式,无需改动) |
---
## 六、数据库迁移(如需要)
如后续需要在 `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 表路径 |