fix: venue_data方案替换分类方案,vr_sessions职责明确化

refactor/vr-ticket-20260416
Council 2026-04-14 15:44:30 +08:00
parent d28a4dc511
commit 401f7b500d
2 changed files with 143 additions and 67 deletions

View File

@ -1,6 +1,6 @@
# ShopXO VR票务插件 — 架构文档 # ShopXO VR票务插件 — 架构文档
> 版本v1.22026-04-14 下午更新,座位地图 + 场馆绑定架构确认 > 版本v1.32026-04-14 更新venue_data 直接写入 sxo_goodsvr_sessions 职责明确
> 源码位置council-research/shopxo-eval/.worktrees/shopxo-evaluator/shopxo-src/ > 源码位置council-research/shopxo-eval/.worktrees/shopxo-evaluator/shopxo-src/
## 项目概述 ## 项目概述
@ -18,17 +18,21 @@
详见 `docs/06_SEAT_MAP_INTEGRATION.md` 详见 `docs/06_SEAT_MAP_INTEGRATION.md`
**核心发现** **核心架构2026-04-14 更新)**
1. **字符地图是行业标准**:场馆平面图 → 字符串地图(如 `aaa___aaa`)→ 前端渲染为 SVG/DOM 1. **字符地图是行业标准**:场馆平面图 → 字符串地图(如 `aaa___aaa`)→ 前端渲染为 SVG/DOM
2. **ShopXO 分类 = Venue Type 绑定**:每个"场馆类型"对应一个 ShopXO 分类,商品挂分类 = 绑定 venue type 2. **venue_data 直接存在 sxo_goods**:每个票务商品 = 1 场演出,`sxo_goods.venue_data`LONGTEXT存完整配置
3. **vr_venues 表**:商家在插件后台管理场馆,上传/编辑座位图 JSON 3. **venue_data JSON 内容**`venue`(场馆+座位图)+ `sessions[]`(场次列表)+ `spec_base_id_map`座位→SKU映射
4. **vr_sessions.seat_map_json**:每个场次存一份座位图配置 4. **vr_venues**:场馆主数据(名称/地址/座位图),多个商品/场次可复用同一个场馆
5. **spec_base_id_map**seat_id`"3_5"`)→ spec_base_id如 10003映射绑定 ShopXO 购买流程 5. **vr_sessions**:每个演出场次(日期+时间),共用 venue 的座位图,独立库存
6. **ShopXO spec 系统无硬限制**规格种类数、单规格选项数、SKU 组合数均无限制3场馆×2票种×500座位=3000 SKU 完全可行) 6. **spec_base_id_map**seat_id`"3_5"`)→ spec_base_id → ShopXO 购买流程
7. **ShopXO spec 系统无硬限制**3场馆×2票种×500座位=3000 SKU 完全可行
**绑定链路** **数据流**
``` ```
ShopXO 分类venue type← → vr_venues ← → vr_sessions ← → spec_base_id_map ← → ShopXO spec_base sxo_goods.venue_data JSON
├── venue.seat_map → 前端渲染座位图
├── sessions[] → 场次选择器
└── spec_base_id_map → 选座 → Buy API → 原子扣 spec_base.inventory
``` ```
## 核心技术发现2026-04-14 调研) ## 核心技术发现2026-04-14 调研)

View File

@ -102,93 +102,164 @@
--- ---
## 二、ShopXO 后台集成方案 ## 二、核心架构venue_data 直接写入 sxo_goods
### 2.1 核心设计:复用 ShopXO 分类作为 Venue Type ### 2.1 为什么不用 ShopXO 分类?
**每个 venue type = 一个 ShopXO 分类** ShopXO 分类是**商品类型**(演唱会/话剧/周边),不是**具体场馆**。
多场馆 × 每个场馆不同座位配置 → 分类不够用。
ShopXO 已有完整的商品多级分类系统(`sxo_goods_category`),我们直接复用: **最优解:直接在 sxo_goods 表加字段,完整配置存在商品里。**
``` ### 2.2 数据库改动sxo_goods 新增 venue_data 字段
sxo_goods_category
├── 演唱会 ```sql
│ ├── 杭州大剧院-演唱会场 ALTER TABLE sxo_goods
│ ├── 北京鸟巢-演唱会场 ← 绑定 seat_map_json ADD COLUMN venue_data LONGTEXT COMMENT '票务插件:场馆+场次+座位配置JSON';
│ └── 上海梅赛德斯-演唱会场
├── 话剧
│ ├── 人艺剧院
│ └── 国家大剧院
└── 电影(可选)
``` ```
**优点** `LONGTEXT` ≈ 4GB存完整座位图配置绑绑有余。
- 不需要改 `sxo_goods` 表结构 ShopXO 已有先例:`sxo_order.extension_data` 和 `sxo_goods_spec_base.extends` 都是 `LONGTEXT`
- 直接用 ShopXO 原生"分类选择器"(后台商品编辑已有,无需开发)
- 商家在 ShopXO 后台创建商品时,分类 = venue type 绑定一步到位
### 2.2 场馆表vr_venues ### 2.3 venue_data JSON 结构
```json
{
"venue": {
"id": 1,
"name": "北京鸟巢",
"address": "北京市朝阳区国家体育场南路1号",
"seat_map": {
"map": ["aaaaaaaaaaaa", "aaaaaaaaaaaa", "bbbbbb__bb", "bbbbbbbbbbbb"],
"row_labels": ["A", "B", "C", "D"],
"seats": {
"a": { "price": 599, "label": "VIP区", "classes": "seat-vip" },
"b": { "price": 399, "label": "普通区", "classes": "seat-normal" },
"_": null
},
"sections": [
{ "name": "VIP区", "color": "#FF6B6B", "rows": [0, 1] },
{ "name": "普通区", "color": "#4ECDC4", "rows": [2, 3] }
]
}
},
"sessions": [
{
"id": 1,
"datetime": "2026-06-01 19:30",
"price_overrides": { "a": 699, "b": 399 }
},
{
"id": 2,
"datetime": "2026-06-02 19:30",
"price_overrides": { "a": 599, "b": 299 }
}
],
"spec_base_id_map": {
"1_1": { "spec_base_id": 10001, "row": "A", "col": 1, "seat_type": "a", "price": 599 },
"1_2": { "spec_base_id": 10002, "row": "A", "col": 2, "seat_type": "a", "price": 599 },
"3_5": { "spec_base_id": 10003, "row": "C", "col": 5, "seat_type": "b", "price": 399 }
}
}
```
**每个商品 = 1 个演出场次 = 完整的票务配置**
- 商家创建"周杰伦北京鸟巢演唱会2026-06-01场次"商品时venue_data 包含venue 信息 + 座位图 + spec_base_id_map
- 用户打开商品详情 → `sxo_goods.venue_data` 已经在商品数据里 → 直接渲染选座 UI
### 2.4 vr_sessions 是什么?
`vr_sessions`(场次表)用于管理**同一场演出在不同时间的多场次**
```
商品周杰伦北京鸟巢2026演唱会
├── vr_sessions[1]2026-06-01 19:30 第一场
├── vr_sessions[2]2026-06-02 19:30 第二场
└── vr_sessions[3]2026-06-03 14:00 第三场(下午场,价格不同)
每个 session
- id / datetime具体时间
- price_overrides该场次的价格覆盖优先级高于 venue.seat_map.seats
- 复用 venue.seat_map 和 spec_base_id_map座位布局不变
- 独立库存(每个场次的 spec_base.inventory 独立追踪)
```
**如果一个商品只有单场演出**vr_sessions 可以简化为 venue_data 里的 sessions 数组。
**vr_sessions 独立表的价值**
- 多场次共用同一个 venue座位图不变
- 每个 session 有独立的 spec_base.inventory第一场售罄≠第三场售罄
- 每个 session 可以有不同的 price_overrides早鸟票/周末票等)
### 2.5 vr_venues场馆表
```sql ```sql
CREATE TABLE vr_venues ( CREATE TABLE vr_venues (
id BIGINT PRIMARY KEY AUTO_INCREMENT, id BIGINT PRIMARY KEY AUTO_INCREMENT,
category_id INT UNSIGNED NOT NULL COMMENT 'ShopXO分类ID绑定venue type',
name VARCHAR(180) NOT NULL COMMENT '场馆名称', name VARCHAR(180) NOT NULL COMMENT '场馆名称',
address VARCHAR(255) NOT NULL DEFAULT '' COMMENT '场馆地址', address VARCHAR(255) NOT NULL DEFAULT '' COMMENT '场馆地址',
seat_map_json LONGTEXT COMMENT '座位地图JSONmap[] + seats配置', seat_map_json LONGTEXT COMMENT '座位地图JSON',
seat_base_price INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '基础票价', seat_base_price INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '基础票价',
status TINYINT UNSIGNED NOT NULL DEFAULT 1 COMMENT '0下架 1上架', status TINYINT UNSIGNED NOT NULL DEFAULT 1,
add_time INT UNSIGNED NOT NULL DEFAULT 0, add_time INT UNSIGNED NOT NULL DEFAULT 0,
upd_time INT UNSIGNED NOT NULL DEFAULT 0, upd_time INT UNSIGNED NOT NULL DEFAULT 0
INDEX idx_category_id (category_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='VR演唱会场馆'; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='VR演唱会场馆';
``` ```
**关键字段** **vr_venues 的作用**
- `category_id`ShopXO 分类 IDShopXO 后台商品编辑时的"分类选择"结果直接对应 - 场馆基础信息统一管理(名称/地址)
- `seat_map_json`:座位地图配置,商家在插件后台导入/编辑 - seat_map_json 是场馆的"硬件配置"(场地座位布局是固定的)
- `seat_base_price`:基础票价 - 多个 session 共用一个 venuevenue_data 在每个 session/goods 里存一份副本
### 2.3 插件后台:场馆 + 座位图编辑器 ### 2.6 插件后台:场馆 + 商品票务配置编辑器
商家在 ShopXO 后台插件入口管理: 商家在 ShopXO 后台插件入口管理:
1. **场馆管理**:创建场馆,关联 ShopXO 分类,上传/绘制座位图
2. **座位图编辑器**:输入字符串地图(如 `aaaaaaaaaaaa`)或上传 SVG
3. **场次管理**:创建演出场次,选择场馆,设定时间、价格
### 2.4 插件 Hook向 ShopXO 商品保存/读取注入 venue 信息 1. **场馆管理**`vr_venues` CRUD上传/编辑座位图(字符串地图编辑器)
2. **商品票务配置**:选择 venue → 选择/创建 session → 系统将 venue.seat_map_json + session 信息整合写入 `sxo_goods.venue_data`
3. **SKU 批量生成**:根据 seat_map 生成/更新 `sxo_goods_spec_base` 中的座位 SKUspec_base_id_map
### 2.7 插件 Hook
```php ```php
// plugins_service_goods_handle_begin — 保存商品时 // plugins_service_goods_handle_begin — 保存商品时,拦截 venue_data
public static function GoodsSaveHandle(&$params, &$goods, $goods_id) public static function GoodsSaveHandle(&$params, &$goods, $goods_id)
{ {
// venue_id 由插件后台单独保存,不依赖 ShopXO 商品表 if (!empty($params['venue_data'])) {
// 关联vr_venues ← (goods_id) → ShopXO sxo_goods // ShopXO 会自动将 venue_data 写入 sxo_goods.venue_data 字段
}
} }
// plugins_service_goods_data — 读取商品时,注入 venue 信息 // plugins_service_goods_data — 读取商品时,注入票务配置
public static function GoodsDataHandle(&$data, $goods_id) public static function GoodsDataHandle(&$data, &$goods_id)
{ {
// 读取该商品关联的 venue + 场次列表 // venue_data 已存在 sxo_goods.venue_data直接可用
$data['vr_venues'] = VrVenueService::GetVenueByGoodsId($goods_id); // 前端模板通过 $goods.venue_data 直接访问
$data['vr_sessions'] = VrSessionService::GetSessionsByGoodsId($goods_id);
} }
``` ```
### 2.5 商品详情页加载流程 ### 2.8 商品详情页加载流程
``` ```
用户打开票务商品详情页 用户打开票务商品详情页
→ 触发 Goods.php Hook 判断 item_type=ticket site_type=3虚拟触发票务 Hook 注入选座区
→ 插件读取 goods_id 对应的 vr_venues + vr_sessions → 前端读取 $goods.venue_data
│ ├── venue.seat_map → 渲染座位图 SVG
│ ├── venue.sessions → 显示场次选择器
│ └── spec_base_id_map → 选座后查 SKU
→ 前端展示: → 步骤1用户选场次datetime
│ 步骤1选择场次日期+时间) │ └── 从 sessions[] 取 price_overrides 渲染座位图价格
│ 步骤2加载该场次座位图从 seat_map_json 渲染 SVG
│ 步骤3用户点击座位 → 获取 spec_base_id → 步骤2用户点击座位 → 获取 seat_id如 "3_5"
│ 步骤4调 ShopXO Buy API 购买该 SKU │ └── 查 spec_base_id_map → 拿到 spec_base_id
→ 步骤3调 ShopXO Buy API → spec_base_id + goods_id
│ └── ShopXO BuyService::OrderInsertHandle() 原子扣库存
→ 步骤4支付成功 → 插件生成 ticket_code + QR
``` ```
--- ---
@ -211,15 +282,16 @@ public static function BindSessionToSpecBase($session_id)
``` ```
**绑定关系** **绑定关系**
- `vr_sessions.spec_base_id_map` ← JSON 映射seat_id → spec_base_id - `sxo_goods.venue_data.spec_base_id_map` ← JSON 映射seat_id → spec_base_id,完整配置存在商品表
- `sxo_goods_spec_base` ← 每个座位一个 SKUinventory=1price=座位价格) - `sxo_goods_spec_base` ← 每个座位一个 SKUinventory=1price=座位价格)
- ShopXO `BuyService::OrderInsertHandle` ← 原子扣 inventory天然防超卖 - ShopXO `BuyService::OrderInsertHandle` ← 原子扣 inventory天然防超卖
### 3.2 场次变更时的 SKU 联动 ### 3.2 场次/座位变更时的 SKU 联动
- **场次新增座位**:调用 `GoodsSpecificationsInsert` 新增 spec_base - **新增座位**:调用 ShopXO `GoodsSpecificationsInsert()` 新增 spec_base
- **场次删除座位**:将对应 spec_base.inventory 置为 0软删除 - **删除座位**:将对应 spec_base.inventory 置为 0软删除
- **价格变更**:更新 `sxo_goods_spec_base.price` - **价格变更**:更新 `sxo_goods_spec_base.price`
- **配置更新后**:重新生成 `sxo_goods.venue_data.spec_base_id_map` 并保存
--- ---
@ -227,7 +299,7 @@ public static function BindSessionToSpecBase($session_id)
| 阶段 | 内容 | 工作量 | | 阶段 | 内容 | 工作量 |
|---|---|---| |---|---|---|
| **Phase A** | vr_venues / vr_sessions 表 + CRUD | 小 | | **Phase A** | sxo_goods 加 venue_data LONGTEXT 字段 + vr_venues/vr_sessions 表 + CRUD | 小 |
| **Phase B** | 场馆座位图编辑器(字符串地图) | 中 | | **Phase B** | 场馆座位图编辑器(字符串地图) | 中 |
| **Phase C** | Vue 3 选座组件(渲染 + 交互) | 中 | | **Phase C** | Vue 3 选座组件(渲染 + 交互) | 中 |
| **Phase D** | spec_base_id_map 绑定逻辑 | 中 | | **Phase D** | spec_base_id_map 绑定逻辑 | 中 |