vr-shopxo-plugin/docs/06_SEAT_MAP_INTEGRATION.md

322 lines
11 KiB
Markdown
Raw Permalink 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.

# 选座系统 + ShopXO 后台集成架构
> 调研日期2026-04-14
> 关联文档ARCHITECTURE.md, 01_SHOPXO_TECHNICAL_RESEARCH.md
---
## 一、选座地图:行业标准做法
### 1.1 核心原理
**"字符地图"是业界通用方案**,不是我们发明的:
```
'aaa___aaa' ← a=可用座, _=过道/柱子/墙壁
'bbb__bbbbb' ← b=另一种座位(不同价格区)
'____________' ← 纯过道/无座位
```
加载时前端把每个字符翻译成可交互的 DOM/SVG 元素:
- 可选座位 → 可点击
- 过道 `_` → 渲染为空白间隔或装饰元素
- 不同字符类型 → 不同颜色/价格/状态
### 1.2 主流实现对比
| 方案 | 技术 | 优点 | 缺点 |
|---|---|---|---|
| **字符地图 + DOM/SVG** | 字符串地图 + div/SVG | 轻量、易编辑、易生成 | 复杂形状需精确计算 |
| **SVG 手绘** | 设计师导出 SVG | 座位形状自然 | 需要设计工具,导入复杂 |
| **Canvas** | Konva.js / Fabric.js | 性能好,适合超大型场馆 | 无 DOM 元素,交互复杂 |
| **seats.io** | 商业 SaaS | 功能完整 | 付费,不可定制 |
**推荐:字符地图 + Vue 3 SVG 渲染**自研AI 可完全生成)
### 1.3 座位地图 JSON 结构
```json
{
"venue_id": "venue_001",
"map": [
"aaaaaaaaaaaa",
"aaaaaaaaaaaa",
"bbbbbbbb__bb",
"bbbbbbbbbbbb",
"__cccccccccc__"
],
"row_labels": ["A", "B", "C", "D", "E"],
"seats": {
"a": { "price": 299, "label": "VIP区", "classes": "seat-vip" },
"b": { "price": 199, "label": "普通区", "classes": "seat-normal" },
"c": { "price": 99, "label": "后排区", "classes": "seat-back" },
"_": null
},
"sections": [
{ "name": "VIP区", "color": "#FF6B6B", "rows": [0, 1] },
{ "name": "普通区", "color": "#4ECDC4", "rows": [2, 3] }
],
"screen": { "label": "舞台/银幕", "position": "top" }
}
```
### 1.4 座位实时状态(动态层)
```json
{
"seats": {
"1_1": { "status": "available" },
"1_2": { "status": "sold" },
"1_3": { "status": "selected" },
"2_5": { "status": "locked" }
}
}
```
座位状态含义:
- `available` — 可选
- `sold` — 已售
- `selected` — 当前用户选中
- `locked` — 被其他用户临时锁定(可选,支持超时释放)
### 1.5 spec_base_id_map与 ShopXO SKU 绑定)
```json
{
"spec_base_id_map": {
"1_1": { "spec_base_id": 10001, "venue": "A区", "row": "A", "col": 1, "price": 299 },
"1_2": { "spec_base_id": 10002, "venue": "A区", "row": "A", "col": 2, "price": 299 },
"3_5": { "spec_base_id": 10003, "venue": "B区", "row": "C", "col": 5, "price": 199 }
}
}
```
**绑定流程**
```
用户在前端选座 seat_id="3_5"
→ 查 spec_base_id_map 拿到 spec_base_id=10003
→ 调 ShopXO Buy API: goods_id + spec_base_id
→ ShopXO 原子扣 spec_base.inventory = 1FOR UPDATE
→ 订单完成
```
---
## 二、核心架构venue_data 直接写入 sxo_goods
### 2.1 为什么不用 ShopXO 分类?
ShopXO 分类是**商品类型**(演唱会/话剧/周边),不是**具体场馆**。
多场馆 × 每个场馆不同座位配置 → 分类不够用。
**最优解:直接在 sxo_goods 表加字段,完整配置存在商品里。**
### 2.2 数据库改动sxo_goods 新增 venue_data 字段
```sql
ALTER TABLE sxo_goods
ADD COLUMN venue_data LONGTEXT COMMENT '票务插件:场馆+场次+座位配置JSON';
```
`LONGTEXT` ≈ 4GB存完整座位图配置绑绑有余。
ShopXO 已有先例:`sxo_order.extension_data` 和 `sxo_goods_spec_base.extends` 都是 `LONGTEXT`
### 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
CREATE TABLE vr_venues (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(180) NOT NULL COMMENT '场馆名称',
address VARCHAR(255) NOT NULL DEFAULT '' COMMENT '场馆地址',
seat_map_json LONGTEXT COMMENT '座位地图JSON',
seat_base_price INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '基础票价',
status TINYINT UNSIGNED NOT NULL DEFAULT 1,
add_time INT UNSIGNED NOT NULL DEFAULT 0,
upd_time INT UNSIGNED NOT NULL DEFAULT 0
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='VR演唱会场馆';
```
**vr_venues 的作用**
- 场馆基础信息统一管理(名称/地址)
- seat_map_json 是场馆的"硬件配置"(场地座位布局是固定的)
- 多个 session 共用一个 venuevenue_data 在每个 session/goods 里存一份副本
### 2.6 插件后台:场馆 + 商品票务配置编辑器
商家在 ShopXO 后台插件入口管理:
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
// plugins_service_goods_handle_begin — 保存商品时,拦截 venue_data
public static function GoodsSaveHandle(&$params, &$goods, $goods_id)
{
if (!empty($params['venue_data'])) {
// ShopXO 会自动将 venue_data 写入 sxo_goods.venue_data 字段
}
}
// plugins_service_goods_data — 读取商品时,注入票务配置
public static function GoodsDataHandle(&$data, &$goods_id)
{
// venue_data 已存在 sxo_goods.venue_data直接可用
// 前端模板通过 $goods.venue_data 直接访问
}
```
### 2.8 商品详情页加载流程
```
用户打开票务商品详情页
→ site_type=3虚拟触发票务 Hook 注入选座区
→ 前端读取 $goods.venue_data
│ ├── venue.seat_map → 渲染座位图 SVG
│ ├── venue.sessions → 显示场次选择器
│ └── spec_base_id_map → 选座后查 SKU
→ 步骤1用户选场次datetime
│ └── 从 sessions[] 取 price_overrides 渲染座位图价格
→ 步骤2用户点击座位 → 获取 seat_id如 "3_5"
│ └── 查 spec_base_id_map → 拿到 spec_base_id
→ 步骤3调 ShopXO Buy API → spec_base_id + goods_id
│ └── ShopXO BuyService::OrderInsertHandle() 原子扣库存
→ 步骤4支付成功 → 插件生成 ticket_code + QR
```
---
## 三、与 ShopXO spec 系统的衔接
### 3.1 座位图与 ShopXO SKU 的绑定时机
**场次创建时自动生成 SKU 映射**
```php
// 场次保存时,调用 SKU 绑定函数
public static function BindSessionToSpecBase($session_id)
{
// 1. 读取 vr_sessions.seat_map_json
// 2. 遍历 map[],为每个"非_"字符生成/查找 spec_base_id
// 3. 生成 spec_base_id_map 存入 vr_sessions
// 4. 调用 ShopXO GoodsSpecificationsInsert() 写入 spec_base 表
}
```
**绑定关系**
- `sxo_goods.venue_data.spec_base_id_map` ← JSON 映射seat_id → spec_base_id完整配置存在商品表
- `sxo_goods_spec_base` ← 每个座位一个 SKUinventory=1price=座位价格)
- ShopXO `BuyService::OrderInsertHandle` ← 原子扣 inventory天然防超卖
### 3.2 场次/座位变更时的 SKU 联动
- **新增座位**:调用 ShopXO `GoodsSpecificationsInsert()` 新增 spec_base
- **删除座位**:将对应 spec_base.inventory 置为 0软删除
- **价格变更**:更新 `sxo_goods_spec_base.price`
- **配置更新后**:重新生成 `sxo_goods.venue_data.spec_base_id_map` 并保存
---
## 四、实现优先级
| 阶段 | 内容 | 工作量 |
|---|---|---|
| **Phase A** | sxo_goods 加 venue_data LONGTEXT 字段 + vr_venues/vr_sessions 表 + CRUD | 小 |
| **Phase B** | 场馆座位图编辑器(字符串地图) | 中 |
| **Phase C** | Vue 3 选座组件(渲染 + 交互) | 中 |
| **Phase D** | spec_base_id_map 绑定逻辑 | 中 |
| **Phase E** | 实时座位状态轮询/推送 | 小 |
**AI 可完全主导全部 phasesA-E**
---
## 五、关键约束确认
| 维度 | 限制 | 结论 |
|---|---|---|
| spec_type 数量 | 无硬限制 | ✅ 想加几个加几个 |
| 单规格选项数 | 无硬限制 | ✅ 500座/场馆没问题 |
| SKU 组合总数 | MySQL 无压力 | ✅ 3×2×500=3000行 OK |
| TEXT 字段容量 | 无实际限制 | ✅ JSON 存几千选项 OK |
| ShopXO 后台扩展 | 通过插件 Hook | ✅ 完全可行 |
| 自提点独立库存 | ShopXO 不支持 | ✅ 用 spec 替代(每座位独立库存)|