2026-04-14 07:29:22 +00:00
|
|
|
|
# 选座系统 + 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 = 1(FOR UPDATE)
|
|
|
|
|
|
→ 订单完成
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
2026-04-14 07:44:30 +00:00
|
|
|
|
## 二、核心架构:venue_data 直接写入 sxo_goods
|
2026-04-14 07:29:22 +00:00
|
|
|
|
|
2026-04-14 07:44:30 +00:00
|
|
|
|
### 2.1 为什么不用 ShopXO 分类?
|
2026-04-14 07:29:22 +00:00
|
|
|
|
|
2026-04-14 07:44:30 +00:00
|
|
|
|
ShopXO 分类是**商品类型**(演唱会/话剧/周边),不是**具体场馆**。
|
|
|
|
|
|
多场馆 × 每个场馆不同座位配置 → 分类不够用。
|
2026-04-14 07:29:22 +00:00
|
|
|
|
|
2026-04-14 07:44:30 +00:00
|
|
|
|
**最优解:直接在 sxo_goods 表加字段,完整配置存在商品里。**
|
2026-04-14 07:29:22 +00:00
|
|
|
|
|
2026-04-14 07:44:30 +00:00
|
|
|
|
### 2.2 数据库改动:sxo_goods 新增 venue_data 字段
|
|
|
|
|
|
|
|
|
|
|
|
```sql
|
|
|
|
|
|
ALTER TABLE sxo_goods
|
|
|
|
|
|
ADD COLUMN venue_data LONGTEXT COMMENT '票务插件:场馆+场次+座位配置JSON';
|
2026-04-14 07:29:22 +00:00
|
|
|
|
```
|
2026-04-14 07:44:30 +00:00
|
|
|
|
|
|
|
|
|
|
`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 }
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-04-14 07:29:22 +00:00
|
|
|
|
```
|
|
|
|
|
|
|
2026-04-14 07:44:30 +00:00
|
|
|
|
**每个商品 = 1 个演出场次 = 完整的票务配置**
|
|
|
|
|
|
- 商家创建"周杰伦北京鸟巢演唱会2026-06-01场次"商品时,venue_data 包含:venue 信息 + 座位图 + spec_base_id_map
|
|
|
|
|
|
- 用户打开商品详情 → `sxo_goods.venue_data` 已经在商品数据里 → 直接渲染选座 UI
|
2026-04-14 07:29:22 +00:00
|
|
|
|
|
2026-04-14 07:44:30 +00:00
|
|
|
|
### 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(场馆表)
|
2026-04-14 07:29:22 +00:00
|
|
|
|
|
|
|
|
|
|
```sql
|
|
|
|
|
|
CREATE TABLE vr_venues (
|
2026-04-14 07:44:30 +00:00
|
|
|
|
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',
|
2026-04-14 07:29:22 +00:00
|
|
|
|
seat_base_price INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '基础票价',
|
2026-04-14 07:44:30 +00:00
|
|
|
|
status TINYINT UNSIGNED NOT NULL DEFAULT 1,
|
|
|
|
|
|
add_time INT UNSIGNED NOT NULL DEFAULT 0,
|
|
|
|
|
|
upd_time INT UNSIGNED NOT NULL DEFAULT 0
|
2026-04-14 07:29:22 +00:00
|
|
|
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='VR演唱会场馆';
|
|
|
|
|
|
```
|
|
|
|
|
|
|
2026-04-14 07:44:30 +00:00
|
|
|
|
**vr_venues 的作用**:
|
|
|
|
|
|
- 场馆基础信息统一管理(名称/地址)
|
|
|
|
|
|
- seat_map_json 是场馆的"硬件配置"(场地座位布局是固定的)
|
|
|
|
|
|
- 多个 session 共用一个 venue,venue_data 在每个 session/goods 里存一份副本
|
2026-04-14 07:29:22 +00:00
|
|
|
|
|
2026-04-14 07:44:30 +00:00
|
|
|
|
### 2.6 插件后台:场馆 + 商品票务配置编辑器
|
2026-04-14 07:29:22 +00:00
|
|
|
|
|
|
|
|
|
|
商家在 ShopXO 后台插件入口管理:
|
|
|
|
|
|
|
2026-04-14 07:44:30 +00:00
|
|
|
|
1. **场馆管理**:`vr_venues` CRUD,上传/编辑座位图(字符串地图编辑器)
|
|
|
|
|
|
2. **商品票务配置**:选择 venue → 选择/创建 session → 系统将 venue.seat_map_json + session 信息整合写入 `sxo_goods.venue_data`
|
|
|
|
|
|
3. **SKU 批量生成**:根据 seat_map 生成/更新 `sxo_goods_spec_base` 中的座位 SKU(spec_base_id_map)
|
|
|
|
|
|
|
|
|
|
|
|
### 2.7 插件 Hook
|
2026-04-14 07:29:22 +00:00
|
|
|
|
|
|
|
|
|
|
```php
|
2026-04-14 07:44:30 +00:00
|
|
|
|
// plugins_service_goods_handle_begin — 保存商品时,拦截 venue_data
|
2026-04-14 07:29:22 +00:00
|
|
|
|
public static function GoodsSaveHandle(&$params, &$goods, $goods_id)
|
|
|
|
|
|
{
|
2026-04-14 07:44:30 +00:00
|
|
|
|
if (!empty($params['venue_data'])) {
|
|
|
|
|
|
// ShopXO 会自动将 venue_data 写入 sxo_goods.venue_data 字段
|
|
|
|
|
|
}
|
2026-04-14 07:29:22 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-14 07:44:30 +00:00
|
|
|
|
// plugins_service_goods_data — 读取商品时,注入票务配置
|
|
|
|
|
|
public static function GoodsDataHandle(&$data, &$goods_id)
|
2026-04-14 07:29:22 +00:00
|
|
|
|
{
|
2026-04-14 07:44:30 +00:00
|
|
|
|
// venue_data 已存在 sxo_goods.venue_data,直接可用
|
|
|
|
|
|
// 前端模板通过 $goods.venue_data 直接访问
|
2026-04-14 07:29:22 +00:00
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
2026-04-14 07:44:30 +00:00
|
|
|
|
### 2.8 商品详情页加载流程
|
2026-04-14 07:29:22 +00:00
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
用户打开票务商品详情页
|
|
|
|
|
|
│
|
2026-04-14 07:44:30 +00:00
|
|
|
|
→ 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
|
2026-04-14 07:29:22 +00:00
|
|
|
|
│
|
2026-04-14 07:44:30 +00:00
|
|
|
|
→ 步骤3:调 ShopXO Buy API → spec_base_id + goods_id
|
|
|
|
|
|
│ └── ShopXO BuyService::OrderInsertHandle() 原子扣库存
|
2026-04-14 07:29:22 +00:00
|
|
|
|
│
|
2026-04-14 07:44:30 +00:00
|
|
|
|
→ 步骤4:支付成功 → 插件生成 ticket_code + QR
|
2026-04-14 07:29:22 +00:00
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 三、与 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 表
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
**绑定关系**:
|
2026-04-14 07:44:30 +00:00
|
|
|
|
- `sxo_goods.venue_data.spec_base_id_map` ← JSON 映射(seat_id → spec_base_id),完整配置存在商品表
|
2026-04-14 07:29:22 +00:00
|
|
|
|
- `sxo_goods_spec_base` ← 每个座位一个 SKU(inventory=1,price=座位价格)
|
|
|
|
|
|
- ShopXO `BuyService::OrderInsertHandle` ← 原子扣 inventory,天然防超卖
|
|
|
|
|
|
|
2026-04-14 07:44:30 +00:00
|
|
|
|
### 3.2 场次/座位变更时的 SKU 联动
|
2026-04-14 07:29:22 +00:00
|
|
|
|
|
2026-04-14 07:44:30 +00:00
|
|
|
|
- **新增座位**:调用 ShopXO `GoodsSpecificationsInsert()` 新增 spec_base
|
|
|
|
|
|
- **删除座位**:将对应 spec_base.inventory 置为 0(软删除)
|
2026-04-14 07:29:22 +00:00
|
|
|
|
- **价格变更**:更新 `sxo_goods_spec_base.price`
|
2026-04-14 07:44:30 +00:00
|
|
|
|
- **配置更新后**:重新生成 `sxo_goods.venue_data.spec_base_id_map` 并保存
|
2026-04-14 07:29:22 +00:00
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 四、实现优先级
|
|
|
|
|
|
|
|
|
|
|
|
| 阶段 | 内容 | 工作量 |
|
|
|
|
|
|
|---|---|---|
|
2026-04-14 07:44:30 +00:00
|
|
|
|
| **Phase A** | sxo_goods 加 venue_data LONGTEXT 字段 + vr_venues/vr_sessions 表 + CRUD | 小 |
|
2026-04-14 07:29:22 +00:00
|
|
|
|
| **Phase B** | 场馆座位图编辑器(字符串地图) | 中 |
|
|
|
|
|
|
| **Phase C** | Vue 3 选座组件(渲染 + 交互) | 中 |
|
|
|
|
|
|
| **Phase D** | spec_base_id_map 绑定逻辑 | 中 |
|
|
|
|
|
|
| **Phase E** | 实时座位状态轮询/推送 | 小 |
|
|
|
|
|
|
|
|
|
|
|
|
**AI 可完全主导全部 phases(A-E)**。
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 五、关键约束确认
|
|
|
|
|
|
|
|
|
|
|
|
| 维度 | 限制 | 结论 |
|
|
|
|
|
|
|---|---|---|
|
|
|
|
|
|
| spec_type 数量 | 无硬限制 | ✅ 想加几个加几个 |
|
|
|
|
|
|
| 单规格选项数 | 无硬限制 | ✅ 500座/场馆没问题 |
|
|
|
|
|
|
| SKU 组合总数 | MySQL 无压力 | ✅ 3×2×500=3000行 OK |
|
|
|
|
|
|
| TEXT 字段容量 | 无实际限制 | ✅ JSON 存几千选项 OK |
|
|
|
|
|
|
| ShopXO 后台扩展 | 通过插件 Hook | ✅ 完全可行 |
|
|
|
|
|
|
| 自提点独立库存 | ShopXO 不支持 | ✅ 用 spec 替代(每座位独立库存)|
|