2026-04-15 13:15:45 +00:00
|
|
|
|
# 后台编辑器 + 商品发布注入方案设计
|
|
|
|
|
|
|
2026-04-15 13:49:07 +00:00
|
|
|
|
> 版本:v3.0 | 日期:2026-04-15 21:45 | 状态:**已确认,待执行**
|
|
|
|
|
|
>
|
|
|
|
|
|
> v3.0 更新(2026-04-15 21:45 大头确认):
|
|
|
|
|
|
> 1. venue 信息放入 seat_map 顶层(不独立建表)
|
|
|
|
|
|
> 2. BatchGenerate() 必须支持按分区过滤(enabledZones 参数)
|
|
|
|
|
|
> 3. 新增 sxo_goods.vr_ticket_config JSON 字段,老商品兼容
|
|
|
|
|
|
> 4. 增加美化版 seat_map 示例 JSON
|
2026-04-15 13:15:45 +00:00
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 一、整体架构一句话
|
|
|
|
|
|
|
2026-04-15 13:49:07 +00:00
|
|
|
|
**插件在 ShopXO 后台建一套"场馆配置"管理界面,用户发布票务商品时,选场馆 → 选分区 → 插件自动只生成这些分区的海量 Spec 并注入商品,用户全程不碰 ShopXO 原生 Spec 管理。**
|
2026-04-15 13:15:45 +00:00
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 二、三大核心区域
|
|
|
|
|
|
|
|
|
|
|
|
```
|
2026-04-15 13:49:07 +00:00
|
|
|
|
┌─────────────────────────────────────────────────────────────┐
|
|
|
|
|
|
│ ShopXO 原生后台 │
|
|
|
|
|
|
│ │
|
|
|
|
|
|
│ ┌──────────────────┐ ┌────────────────────────┐ │
|
|
|
|
|
|
│ │ 商品管理 │ ←→ │ 插件专属后台(新增) │ │
|
|
|
|
|
|
│ │ 发布/编辑商品 │ │ 场馆配置管理 │ │
|
|
|
|
|
|
│ │ (注入点) │ │ 座位分区模板编辑器 │ │
|
|
|
|
|
|
│ └──────────────────┘ └────────────────────────┘ │
|
|
|
|
|
|
└─────────────────────────────────────────────────────────────┘
|
2026-04-15 13:15:45 +00:00
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 区域 A:插件专属后台(新增)
|
|
|
|
|
|
|
|
|
|
|
|
商户在 ShopXO 后台左侧菜单进入「VR票务」:
|
|
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
VR票务
|
2026-04-15 13:49:07 +00:00
|
|
|
|
├── 场馆配置 ← Phase 3-1 新增
|
2026-04-15 13:15:45 +00:00
|
|
|
|
├── 座位模板 ← 已存在(Phase 2)
|
|
|
|
|
|
├── 电子票管理 ← 已存在
|
|
|
|
|
|
├── 核销员管理 ← 已存在
|
|
|
|
|
|
└── 核销记录 ← 已存在
|
|
|
|
|
|
```
|
|
|
|
|
|
|
2026-04-15 13:49:07 +00:00
|
|
|
|
> ⚠️ **当前状态**:`Venue.php` 控制器不存在,Phase 3-1 从零新建。
|
2026-04-15 13:15:45 +00:00
|
|
|
|
|
|
|
|
|
|
### 区域 B:ShopXO 商品发布页(注入点)
|
|
|
|
|
|
|
2026-04-15 13:49:07 +00:00
|
|
|
|
> ⚠️ **待核实**:商品发布页 Tab 名称需对照 `saveinfo.html` 模板确认。
|
2026-04-15 13:39:56 +00:00
|
|
|
|
|
2026-04-15 13:15:45 +00:00
|
|
|
|
商户在 ShopXO 后台「商品管理 → 添加商品」:
|
|
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
添加商品
|
|
|
|
|
|
[商品名称] [商品分类]
|
2026-04-15 13:49:07 +00:00
|
|
|
|
|
|
|
|
|
|
▼ 票务配置 ← ShopXO 原生区域,插件注入票务选择器
|
|
|
|
|
|
场馆:[请选择场馆 ▼] ← 来自 vr_seat_templates 表
|
|
|
|
|
|
分区:[□VIP区 □看台区 □普通区] ← 多选,根据场馆联动
|
|
|
|
|
|
|
2026-04-15 13:15:45 +00:00
|
|
|
|
[商品详情 富文本编辑器]
|
|
|
|
|
|
▼ 其他Tab(参数/图片等)← ShopXO 原生
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
### 区域 C:插件商品详情页(已存在)
|
|
|
|
|
|
|
2026-04-15 13:49:07 +00:00
|
|
|
|
用户在前台看到票务商品详情页,选座下单,已实现。
|
2026-04-15 13:15:45 +00:00
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 三、场馆配置管理(区域 A)
|
|
|
|
|
|
|
2026-04-15 13:49:07 +00:00
|
|
|
|
### 3.1 seat_map 数据结构(v3.0 最终版)
|
2026-04-15 13:15:45 +00:00
|
|
|
|
|
|
|
|
|
|
```json
|
|
|
|
|
|
{
|
2026-04-15 13:49:07 +00:00
|
|
|
|
"venue": {
|
2026-04-15 13:15:45 +00:00
|
|
|
|
"name": "国家体育馆",
|
2026-04-15 13:49:07 +00:00
|
|
|
|
"address": "北京市朝阳区奥体中心",
|
|
|
|
|
|
"image": "/uploads/vr/venue/national-stadium.jpg"
|
2026-04-15 13:15:45 +00:00
|
|
|
|
},
|
2026-04-15 13:49:07 +00:00
|
|
|
|
"map": [
|
|
|
|
|
|
"AAAAAA",
|
|
|
|
|
|
"BBBBBB",
|
|
|
|
|
|
"CCCCCC"
|
|
|
|
|
|
],
|
|
|
|
|
|
"seats": {
|
2026-04-15 13:15:45 +00:00
|
|
|
|
"A": { "price": 899, "color": "#e74c3c", "label": "VIP区" },
|
|
|
|
|
|
"B": { "price": 599, "color": "#3498db", "label": "看台区" },
|
|
|
|
|
|
"C": { "price": 299, "color": "#2ecc71", "label": "普通区" }
|
|
|
|
|
|
},
|
2026-04-15 13:49:07 +00:00
|
|
|
|
"sections": [
|
|
|
|
|
|
{ "char": "A", "name": "VIP区", "color": "#e74c3c" },
|
2026-04-15 13:15:45 +00:00
|
|
|
|
{ "char": "B", "name": "看台区", "color": "#3498db" },
|
|
|
|
|
|
{ "char": "C", "name": "普通区", "color": "#2ecc71" }
|
2026-04-15 13:39:56 +00:00
|
|
|
|
],
|
|
|
|
|
|
"row_labels": ["A", "B", "C"]
|
2026-04-15 13:15:45 +00:00
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
2026-04-15 13:49:07 +00:00
|
|
|
|
**字段说明**:
|
|
|
|
|
|
|
|
|
|
|
|
| 字段 | 类型 | 说明 |
|
|
|
|
|
|
|------|------|------|
|
|
|
|
|
|
| `venue` | object | 场馆信息。**v3.0 新增**。name 即 `$vr-场馆` spec 值,不再硬编码 |
|
|
|
|
|
|
| `map` | string[] | 每排座位数字符串(A=6座,B=6座,C=6座),决定总座位数 |
|
|
|
|
|
|
| `seats` | object | **字段名是 seats 不是 zones**。每行(char)的默认 price/color/label |
|
|
|
|
|
|
| `sections` | object[] | 每个分区的元信息(char → name/color 映射),前端分区选择器用此渲染多选框 |
|
|
|
|
|
|
| `row_labels` | string[] | 行标签,用于前端渲染座位坐标("A_1", "A_2"...)|
|
2026-04-15 13:15:45 +00:00
|
|
|
|
|
2026-04-15 13:49:07 +00:00
|
|
|
|
**`$vr-场馆` spec 值来源**:从 `seat_map.venue.name` 读取,Phase 3-1 改造 `BatchGenerate`。
|
2026-04-15 13:15:45 +00:00
|
|
|
|
|
2026-04-15 13:49:07 +00:00
|
|
|
|
**与旧数据的兼容性**:
|
|
|
|
|
|
- 存量 seat_map 无 `venue` 顶层 → 降级取 `vr_seat_templates.name` 作为 `$vr-场馆` 值
|
|
|
|
|
|
- 存量 seat_map 无 `sections` 顶层 → 降级从 `seats` 推导
|
2026-04-15 13:39:56 +00:00
|
|
|
|
|
2026-04-15 13:49:07 +00:00
|
|
|
|
### 3.2 美化版 seat_map 示例(Phase 3-1 表单填充示例)
|
|
|
|
|
|
|
|
|
|
|
|
**示例 1:大型演唱会(3 区,每区 6 座)**
|
|
|
|
|
|
```json
|
|
|
|
|
|
{
|
|
|
|
|
|
"venue": {
|
|
|
|
|
|
"name": "鸟巢",
|
|
|
|
|
|
"address": "北京市朝阳区国家体育场南路1号",
|
|
|
|
|
|
"image": "/uploads/vr/venue/bird-nest.jpg"
|
|
|
|
|
|
},
|
|
|
|
|
|
"map": ["AAAAAA", "BBBBBB", "CCCCCC"],
|
|
|
|
|
|
"seats": {
|
|
|
|
|
|
"A": { "price": 1299, "color": "#e74c3c", "label": "内场VIP" },
|
|
|
|
|
|
"B": { "price": 899, "color": "#9b59b6", "label": "内场A区" },
|
|
|
|
|
|
"C": { "price": 599, "color": "#3498db", "label": "内场B区" }
|
|
|
|
|
|
},
|
|
|
|
|
|
"sections": [
|
|
|
|
|
|
{ "char": "A", "name": "内场VIP", "color": "#e74c3c" },
|
|
|
|
|
|
{ "char": "B", "name": "内场A区", "color": "#9b59b6" },
|
|
|
|
|
|
{ "char": "C", "name": "内场B区", "color": "#3498db" }
|
|
|
|
|
|
],
|
|
|
|
|
|
"row_labels": ["A", "B", "C"]
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
**示例 2:小型剧场(2 区,每区 4 座)**
|
|
|
|
|
|
```json
|
|
|
|
|
|
{
|
|
|
|
|
|
"venue": {
|
|
|
|
|
|
"name": "上海音乐厅",
|
|
|
|
|
|
"address": "上海市黄浦区延安东路523号",
|
|
|
|
|
|
"image": "/uploads/vr/venue/shanghai-concert-hall.jpg"
|
|
|
|
|
|
},
|
|
|
|
|
|
"map": ["AAAA", "BBBB"],
|
|
|
|
|
|
"seats": {
|
|
|
|
|
|
"A": { "price": 499, "color": "#f39c12", "label": "前排" },
|
|
|
|
|
|
"B": { "price": 299, "color": "#1abc9c", "label": "后排" }
|
|
|
|
|
|
},
|
|
|
|
|
|
"sections": [
|
|
|
|
|
|
{ "char": "A", "name": "前排", "color": "#f39c12" },
|
|
|
|
|
|
{ "char": "B", "name": "后排", "color": "#1abc9c" }
|
|
|
|
|
|
],
|
|
|
|
|
|
"row_labels": ["A", "B"]
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
**示例 3:开放部分分区(电影院场景)**
|
|
|
|
|
|
```json
|
|
|
|
|
|
{
|
|
|
|
|
|
"venue": {
|
|
|
|
|
|
"name": "IMAX 影城",
|
|
|
|
|
|
"address": "北京市朝阳区建国路88号",
|
|
|
|
|
|
"image": "/uploads/vr/venue/imax-cinema.jpg"
|
|
|
|
|
|
},
|
|
|
|
|
|
"map": ["AAAAAA", "BBBBBB", "CCCCCC", "DDDDDD", "EEEEE"],
|
|
|
|
|
|
"seats": {
|
|
|
|
|
|
"A": { "price": 99, "color": "#e74c3c", "label": "情侣座" },
|
|
|
|
|
|
"B": { "price": 69, "color": "#3498db", "label": "VIP厅" },
|
|
|
|
|
|
"C": { "price": 49, "color": "#2ecc71", "label": "普通座" },
|
|
|
|
|
|
"D": { "price": 49, "color": "#2ecc71", "label": "普通座" },
|
|
|
|
|
|
"E": { "price": 39, "color": "#95a5a6", "label": "边座" }
|
|
|
|
|
|
},
|
|
|
|
|
|
"sections": [
|
|
|
|
|
|
{ "char": "A", "name": "情侣座", "color": "#e74c3c" },
|
|
|
|
|
|
{ "char": "B", "name": "VIP厅", "color": "#3498db" },
|
|
|
|
|
|
{ "char": "C", "name": "普通座", "color": "#2ecc71" },
|
|
|
|
|
|
{ "char": "D", "name": "普通座", "color": "#2ecc71" },
|
|
|
|
|
|
{ "char": "E", "name": "边座", "color": "#95a5a6" }
|
|
|
|
|
|
],
|
|
|
|
|
|
"row_labels": ["A", "B", "C", "D", "E"]
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
> 示例 3 场景:发布商品时只选 A+B 分区,C/D/E 暂不开放。`BatchGenerate(enabledZones=['A','B'])` 只生成 12 个座位 SKU。
|
|
|
|
|
|
|
|
|
|
|
|
### 3.3 场馆配置管理页面
|
|
|
|
|
|
|
|
|
|
|
|
> ⚠️ **待实现**:当前 `Venue.php` 控制器不存在。
|
|
|
|
|
|
|
|
|
|
|
|
商户在插件后台「场馆配置 → 添加」看到的表单:
|
2026-04-15 13:15:45 +00:00
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
【场馆基本信息】
|
2026-04-15 13:49:07 +00:00
|
|
|
|
场馆名称:[_______________________________]
|
|
|
|
|
|
场馆地址:[_______________________________]
|
2026-04-15 13:15:45 +00:00
|
|
|
|
场馆图片:[上传按钮]
|
2026-04-15 13:49:07 +00:00
|
|
|
|
|
|
|
|
|
|
【分区配置】(增删分区,拖拽排序)
|
2026-04-15 13:39:56 +00:00
|
|
|
|
┌──────────────────────────────────────────────────┐
|
2026-04-15 13:49:07 +00:00
|
|
|
|
│ Char │ 标签 │ 单价(元)│ 颜色 │ │
|
2026-04-15 13:39:56 +00:00
|
|
|
|
├──────────────────────────────────────────────────┤
|
2026-04-15 13:49:07 +00:00
|
|
|
|
│ A │ 内场VIP │ 1299 │ [■红 ▼]│ [删除] │
|
2026-04-15 13:39:56 +00:00
|
|
|
|
├──────────────────────────────────────────────────┤
|
2026-04-15 13:49:07 +00:00
|
|
|
|
│ B │ 内场A区 │ 899 │ [■紫 ▼]│ [删除] │
|
|
|
|
|
|
├──────────────────────────────────────────────────┤
|
|
|
|
|
|
│ C │ 内场B区 │ 599 │ [■蓝 ▼]│ [删除] │
|
2026-04-15 13:39:56 +00:00
|
|
|
|
└──────────────────────────────────────────────────┘
|
2026-04-15 13:49:07 +00:00
|
|
|
|
[+ 添加分区]
|
2026-04-15 13:15:45 +00:00
|
|
|
|
|
2026-04-15 13:49:07 +00:00
|
|
|
|
【座位排布预览】(实时渲染)
|
|
|
|
|
|
🔴 🔴 🔴 🔴 🔴 🔴 ← A区(VIP,1299元)
|
|
|
|
|
|
🟣 🟣 🟣 🟣 🟣 🟣 ← B区(内场A区,899元)
|
|
|
|
|
|
🔵 🔵 🔵 🔵 🔵 🔵 ← C区(内场B区,599元)
|
2026-04-15 13:15:45 +00:00
|
|
|
|
|
2026-04-15 13:49:07 +00:00
|
|
|
|
每排座位数:[6___] 总座位数:18
|
2026-04-15 13:15:45 +00:00
|
|
|
|
|
2026-04-15 13:49:07 +00:00
|
|
|
|
【保存】 【取消】
|
|
|
|
|
|
```
|
2026-04-15 13:39:56 +00:00
|
|
|
|
|
2026-04-15 13:49:07 +00:00
|
|
|
|
**技术实现**:
|
|
|
|
|
|
- layui 表单 + Vue3 CDN(轻量,不破坏 ShopXO 后台 jQuery/layui 结构)
|
|
|
|
|
|
- ~500 行前端代码,1-1.5 人天
|
|
|
|
|
|
- 保存:表单 → seat_map JSON → 写入 `vr_seat_templates.seat_map`
|
|
|
|
|
|
- 颜色选择器:预设色盘(不引入额外依赖)
|
2026-04-15 13:15:45 +00:00
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 四、商品发布页注入(区域 B)
|
|
|
|
|
|
|
2026-04-15 13:39:56 +00:00
|
|
|
|
### 4.1 注入原理
|
2026-04-15 13:15:45 +00:00
|
|
|
|
|
|
|
|
|
|
```
|
2026-04-15 13:49:07 +00:00
|
|
|
|
ShopXO admin Goods::SaveInfo()
|
2026-04-15 13:39:56 +00:00
|
|
|
|
→ 调用 hook plugins_view_admin_goods_save(Goods.php:159)
|
2026-04-15 13:49:07 +00:00
|
|
|
|
→ 触发 vr_ticket/hook/AdminGoodsSave.php
|
2026-04-15 13:15:45 +00:00
|
|
|
|
→ 返回票务配置面板 HTML
|
2026-04-15 13:49:07 +00:00
|
|
|
|
→ 插入 saveinfo.html base tab <div class="am-form-group"> 内
|
2026-04-15 13:15:45 +00:00
|
|
|
|
```
|
|
|
|
|
|
|
2026-04-15 13:39:56 +00:00
|
|
|
|
### 4.2 钩子注册方式
|
2026-04-15 13:15:45 +00:00
|
|
|
|
|
2026-04-15 13:49:07 +00:00
|
|
|
|
> ⚠️ **重要**:ShopXO **不支持** `backend_hook` 字段,实际使用 `hooks` 数组。
|
2026-04-15 13:15:45 +00:00
|
|
|
|
|
2026-04-15 13:49:07 +00:00
|
|
|
|
**plugin.json 追加**(Phase 3-2):
|
2026-04-15 13:15:45 +00:00
|
|
|
|
```json
|
|
|
|
|
|
{
|
2026-04-15 13:39:56 +00:00
|
|
|
|
"hooks": [
|
|
|
|
|
|
"plugins_service_order_pay_success_handle_end",
|
|
|
|
|
|
"plugins_service_order_delete_success",
|
|
|
|
|
|
"plugins_view_admin_goods_save",
|
|
|
|
|
|
"plugins_service_goods_save_handle"
|
|
|
|
|
|
]
|
2026-04-15 13:15:45 +00:00
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
2026-04-15 13:39:56 +00:00
|
|
|
|
### 4.3 AdminGoodsSave.php(待新建)
|
|
|
|
|
|
|
2026-04-15 13:49:07 +00:00
|
|
|
|
> ⚠️ **待实现**:`vr_ticket/` 下当前无此文件。
|
2026-04-15 13:39:56 +00:00
|
|
|
|
|
2026-04-15 13:49:07 +00:00
|
|
|
|
**文件**:`plugins/vr_ticket/hook/AdminGoodsSave.php`
|
2026-04-15 13:39:56 +00:00
|
|
|
|
|
2026-04-15 13:49:07 +00:00
|
|
|
|
职责:
|
|
|
|
|
|
- 查询 `vr_seat_templates` 表,构造场馆下拉 + 分区多选 HTML
|
|
|
|
|
|
- 返回 layui + Vue3 CDN 的票务配置面板
|
2026-04-15 13:15:45 +00:00
|
|
|
|
|
2026-04-15 13:49:07 +00:00
|
|
|
|
**数据来源**:
|
2026-04-15 13:15:45 +00:00
|
|
|
|
```php
|
|
|
|
|
|
$templates = Db::name('vr_seat_templates')
|
2026-04-15 13:49:07 +00:00
|
|
|
|
->field('id, name, seat_map, category_id')
|
2026-04-15 13:15:45 +00:00
|
|
|
|
->where(['status' => 1])
|
|
|
|
|
|
->select();
|
2026-04-15 13:39:56 +00:00
|
|
|
|
|
2026-04-15 13:49:07 +00:00
|
|
|
|
foreach ($templates as &$t) {
|
|
|
|
|
|
$seatMap = json_decode($t['seat_map'], true);
|
|
|
|
|
|
// v3.0: venue.name 优先,否则降级取模板 name
|
|
|
|
|
|
$t['venue_name'] = $seatMap['venue']['name'] ?? $t['name'];
|
|
|
|
|
|
// sections 用于渲染分区多选框
|
|
|
|
|
|
$t['zones'] = $seatMap['sections'] ?? [];
|
|
|
|
|
|
}
|
|
|
|
|
|
```
|
2026-04-15 13:15:45 +00:00
|
|
|
|
|
2026-04-15 13:49:07 +00:00
|
|
|
|
### 4.4 AdminGoodsSaveHandle.php(待新建)
|
2026-04-15 13:15:45 +00:00
|
|
|
|
|
2026-04-15 13:49:07 +00:00
|
|
|
|
**文件**:`plugins/vr_ticket/hook/AdminGoodsSaveHandle.php`
|
2026-04-15 13:15:45 +00:00
|
|
|
|
|
2026-04-15 13:49:07 +00:00
|
|
|
|
触发时机:`plugins_service_goods_save_handle`(GoodsService.php:1550)
|
2026-04-15 13:15:45 +00:00
|
|
|
|
|
2026-04-15 13:49:07 +00:00
|
|
|
|
职责:
|
|
|
|
|
|
```php
|
|
|
|
|
|
// 接收 POST: { template_id, selected_zones: ['A', 'C'] }
|
|
|
|
|
|
$templateId = $params['vr_template_id'] ?? 0;
|
|
|
|
|
|
$zones = $params['vr_zones'] ?? []; // e.g. ['A', 'C']
|
|
|
|
|
|
|
|
|
|
|
|
// 1. 生成 SKU(只生成选中分区)
|
|
|
|
|
|
$result = SeatSkuService::BatchGenerate($goodsId, $templateId, $zones);
|
|
|
|
|
|
// goodsId 从 $params['id'] 或新建商品的返回值中获取
|
|
|
|
|
|
|
|
|
|
|
|
// 2. 写入 vr_ticket_config 快照(Phase 3-3 新增字段)
|
|
|
|
|
|
$snapshot = [
|
|
|
|
|
|
'template_id' => $templateId,
|
|
|
|
|
|
'selected_zones' => $zones,
|
|
|
|
|
|
'spec_base_id_map' => $result['data']['spec_base_id_map'],
|
|
|
|
|
|
'created_at' => time(),
|
|
|
|
|
|
];
|
|
|
|
|
|
// Db::name('goods')->where('id', $goodsId)->update(['vr_ticket_config' => json_encode($snapshot)]);
|
2026-04-15 13:15:45 +00:00
|
|
|
|
```
|
|
|
|
|
|
|
2026-04-15 13:49:07 +00:00
|
|
|
|
---
|
2026-04-15 13:15:45 +00:00
|
|
|
|
|
2026-04-15 13:49:07 +00:00
|
|
|
|
## 五、商品发布完整流程
|
|
|
|
|
|
|
|
|
|
|
|
**场景**:商户发布一张票务商品(只开放 A 区和 C 区)
|
2026-04-15 13:15:45 +00:00
|
|
|
|
|
|
|
|
|
|
```
|
2026-04-15 13:49:07 +00:00
|
|
|
|
Step 1:商户进入 ShopXO 后台 → 商品管理 → 添加商品
|
|
|
|
|
|
|
|
|
|
|
|
Step 2:填写基础信息
|
|
|
|
|
|
商品名称:周杰伦 VR 虚拟演唱会
|
|
|
|
|
|
商品分类:VR演出
|
|
|
|
|
|
|
|
|
|
|
|
Step 3:在票务配置面板选场馆和分区
|
|
|
|
|
|
票务配置
|
|
|
|
|
|
场馆:[鸟巢 ▼]
|
|
|
|
|
|
分区:[✓VIP区(A) □看台区(B) ✓普通区(C)]
|
|
|
|
|
|
|
|
|
|
|
|
Step 4:点击发布
|
|
|
|
|
|
Save() → GoodsService::GoodsSave()
|
|
|
|
|
|
→ 触发 plugins_service_goods_save_handle
|
|
|
|
|
|
→ AdminGoodsSaveHandle 收到 POST
|
|
|
|
|
|
→ BatchGenerate(goodsId=123, templateId=1, enabledZones=['A','C'])
|
|
|
|
|
|
→ 只生成 A 区(6座)和 C 区(6座)= 12 个 SKU
|
|
|
|
|
|
→ 写入 sxo_goods_spec_base
|
|
|
|
|
|
→ 写入 vr_ticket_config JSON 快照
|
|
|
|
|
|
→ 商品保存完成
|
|
|
|
|
|
|
|
|
|
|
|
Step 5:用户前台看到选座页
|
|
|
|
|
|
→ 用户点击 A_1 座位
|
|
|
|
|
|
→ specBaseIdMap['A_1'] = 2001(来自 vr_ticket_config.snapshot)
|
|
|
|
|
|
→ goodsParams: [{goods_id:123, spec_base_id:2001, stock:1}]
|
|
|
|
|
|
→ 下单
|
2026-04-15 13:15:45 +00:00
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
2026-04-15 13:49:07 +00:00
|
|
|
|
## 六、Spec 生成后内部结构(技术细节)
|
2026-04-15 13:39:56 +00:00
|
|
|
|
|
2026-04-15 13:49:07 +00:00
|
|
|
|
> ⚠️ **表前缀**:ShopXO 原生表用 `sxo_`,插件自定义表用 `{$prefix}vrt_`。
|
2026-04-15 13:15:45 +00:00
|
|
|
|
|
2026-04-15 13:39:56 +00:00
|
|
|
|
### sxo_goods_spec_base(每行 = 一个座位 SKU)
|
2026-04-15 13:15:45 +00:00
|
|
|
|
|
|
|
|
|
|
| spec_base_id | goods_id | inventory | price |
|
|
|
|
|
|
|---|---|---|---|
|
2026-04-15 13:49:07 +00:00
|
|
|
|
| 2001 | 123 | 1 | 1299 | ← A_1 座位
|
|
|
|
|
|
| 2002 | 123 | 1 | 1299 | ← A_2 座位
|
|
|
|
|
|
| ... | 123 | 1 | 1299 | ← A_6 座位
|
|
|
|
|
|
| 5001 | 123 | 1 | 599 | ← C_1 座位
|
|
|
|
|
|
| ... | 123 | 1 | 599 | ← C_6 座位
|
|
|
|
|
|
> 注:B 区座位未生成(商户只选了 A + C)
|
2026-04-15 13:15:45 +00:00
|
|
|
|
|
2026-04-15 13:39:56 +00:00
|
|
|
|
### sxo_goods_spec_type(每行 = 一个规格维度)
|
2026-04-15 13:15:45 +00:00
|
|
|
|
|
|
|
|
|
|
| id | goods_id | name | value |
|
|
|
|
|
|
|---|---|---|---|
|
2026-04-15 13:49:07 +00:00
|
|
|
|
| 1 | 123 | $vr-场馆 | `[{"name":"鸟巢"}]` |
|
|
|
|
|
|
| 2 | 123 | $vr-分区 | `[{"name":"VIP区"},{"name":"普通区"}]` |
|
2026-04-15 13:39:56 +00:00
|
|
|
|
| 3 | 123 | $vr-时段 | `[{"name":"待选场次"}]` |
|
2026-04-15 13:49:07 +00:00
|
|
|
|
| 4 | 123 | $vr-座位号 | `[{"name":"A_1"},...{"name":"C_6"}]` |
|
2026-04-15 13:15:45 +00:00
|
|
|
|
|
2026-04-15 13:49:07 +00:00
|
|
|
|
### vr_ticket_config(sxo_goods 表新增字段)
|
2026-04-15 13:15:45 +00:00
|
|
|
|
|
|
|
|
|
|
```json
|
|
|
|
|
|
{
|
2026-04-15 13:49:07 +00:00
|
|
|
|
"template_id": 1,
|
|
|
|
|
|
"selected_zones": ["A", "C"],
|
|
|
|
|
|
"spec_base_id_map": {
|
|
|
|
|
|
"A_1": 2001, "A_2": 2002, ..., "A_6": 2006,
|
|
|
|
|
|
"C_1": 5001, "C_2": 5002, ..., "C_6": 5006
|
|
|
|
|
|
},
|
|
|
|
|
|
"created_at": 1744659200
|
2026-04-15 13:15:45 +00:00
|
|
|
|
}
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
2026-04-15 13:49:07 +00:00
|
|
|
|
## 七、BatchGenerate 接口(含 v3.0 改造)
|
|
|
|
|
|
|
|
|
|
|
|
### 7.1 当前已实现版本(全量,v2.0)
|
2026-04-15 13:39:56 +00:00
|
|
|
|
|
2026-04-15 13:49:07 +00:00
|
|
|
|
> ⚠️ 以下为当前 main 分支代码,**不支持**按分区过滤(Phase 3-3 需改造):
|
2026-04-15 13:39:56 +00:00
|
|
|
|
|
|
|
|
|
|
```php
|
|
|
|
|
|
public static function BatchGenerate(int $goodsId, int $seatTemplateId): array
|
2026-04-15 13:49:07 +00:00
|
|
|
|
// 行为:按模板全部座位生成 SKU
|
2026-04-15 13:39:56 +00:00
|
|
|
|
```
|
|
|
|
|
|
|
2026-04-15 13:49:07 +00:00
|
|
|
|
### 7.2 Phase 3-3 改造版本(按分区过滤)
|
|
|
|
|
|
|
2026-04-15 13:39:56 +00:00
|
|
|
|
```php
|
2026-04-15 13:49:07 +00:00
|
|
|
|
public static function BatchGenerate(
|
|
|
|
|
|
int $goodsId,
|
|
|
|
|
|
int $seatTemplateId,
|
|
|
|
|
|
array $enabledZones = [] // e.g. ['A', 'C'] — 空=全量
|
|
|
|
|
|
): array
|
|
|
|
|
|
|
|
|
|
|
|
// 内部逻辑改造:
|
|
|
|
|
|
// foreach ($seatMap['map'] as $rowChar => $rowSeats) {
|
|
|
|
|
|
// if (!empty($enabledZones) && !in_array($rowChar, $enabledZones)) {
|
|
|
|
|
|
// continue; // 跳过未选中的分区
|
|
|
|
|
|
// }
|
|
|
|
|
|
// foreach (range(1, strlen($rowSeats)) as $col) {
|
|
|
|
|
|
// // 生成该座位 SKU...
|
|
|
|
|
|
// }
|
|
|
|
|
|
// }
|
2026-04-15 13:39:56 +00:00
|
|
|
|
```
|
|
|
|
|
|
|
2026-04-15 13:49:07 +00:00
|
|
|
|
**与旧版差异**:
|
2026-04-15 13:39:56 +00:00
|
|
|
|
|
2026-04-15 13:49:07 +00:00
|
|
|
|
| 维度 | 旧版(全量) | 新版(v3.0) |
|
|
|
|
|
|
|------|------------|-------------|
|
|
|
|
|
|
| 参数 | `(goodsId, templateId)` | `(goodsId, templateId, enabledZones)` |
|
|
|
|
|
|
| 行为 | 所有座位都生成 | 只生成 enabledZones 内的分区座位 |
|
|
|
|
|
|
| 兼容性 | — | enabledZones 为空时退化为全量(向后兼容)|
|
2026-04-15 13:39:56 +00:00
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
2026-04-15 13:49:07 +00:00
|
|
|
|
## 八、vr_ticket_config 字段(v3.0 新增)
|
2026-04-15 13:39:56 +00:00
|
|
|
|
|
2026-04-15 13:49:07 +00:00
|
|
|
|
### 8.1 数据库迁移
|
2026-04-15 13:39:56 +00:00
|
|
|
|
|
2026-04-15 13:49:07 +00:00
|
|
|
|
```sql
|
|
|
|
|
|
-- EventListener.php 中追加(Phase 3-3)
|
|
|
|
|
|
ALTER TABLE {$prefix}goods
|
|
|
|
|
|
ADD COLUMN vr_ticket_config JSON COMMENT '票务配置快照:template_id/selected_zones/spec_base_id_map' AFTER extension_data;
|
|
|
|
|
|
```
|
2026-04-15 13:15:45 +00:00
|
|
|
|
|
2026-04-15 13:49:07 +00:00
|
|
|
|
**老商品兼容**:存量商品 `vr_ticket_config = NULL`,编辑页检测到为空时提示"请重新选择"。
|
2026-04-15 13:15:45 +00:00
|
|
|
|
|
2026-04-15 13:49:07 +00:00
|
|
|
|
### 8.2 生命周期
|
2026-04-15 13:15:45 +00:00
|
|
|
|
|
2026-04-15 13:49:07 +00:00
|
|
|
|
| 时机 | 操作 |
|
|
|
|
|
|
|------|------|
|
|
|
|
|
|
| 发布商品时 | `AdminGoodsSaveHandle` 写入 `vr_ticket_config` |
|
|
|
|
|
|
| 编辑商品时 | 从此字段还原"选择了哪些场馆/分区" → 不展示琳琅满目的 spec_base 列表 |
|
|
|
|
|
|
| 删除商品时 | 钩子 `plugins_service_order_delete_success` 同步清理关联 spec |
|
2026-04-15 13:15:45 +00:00
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
2026-04-15 13:39:56 +00:00
|
|
|
|
## 九、与 ShopXO 原生 Spec 管理的关系
|
2026-04-15 13:15:45 +00:00
|
|
|
|
|
2026-04-15 13:49:07 +00:00
|
|
|
|
| 维度 | ShopXO 原生 Spec | 票务 Spec |
|
|
|
|
|
|
|------|----------------|----------|
|
|
|
|
|
|
| 谁创建 | 商户手动添加 | 插件自动生成(静默) |
|
2026-04-15 13:39:56 +00:00
|
|
|
|
| 表前缀 | `sxo_` | `sxo_goods_spec_base` / `sxo_goods_spec_value` |
|
2026-04-15 13:49:07 +00:00
|
|
|
|
| 商户感知 | 在后台规格管理看到 | 无感(注入的表单已覆盖场景) |
|
|
|
|
|
|
| 用户在前台 | 购物车/下单流程 | 票务选座 UI |
|
|
|
|
|
|
| 核销 | 不涉及 | 每座位一个 QR(vrt_vr_tickets)|
|
2026-04-15 13:15:45 +00:00
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
2026-04-15 13:39:56 +00:00
|
|
|
|
## 十、实施步骤
|
2026-04-15 13:15:45 +00:00
|
|
|
|
|
2026-04-15 13:39:56 +00:00
|
|
|
|
### Phase 3-1:后台场馆配置管理(新建 admin 页面)
|
2026-04-15 13:15:45 +00:00
|
|
|
|
|
2026-04-15 13:49:07 +00:00
|
|
|
|
- [x] 决策已确认:venue 信息存入 seat_map 顶层
|
2026-04-15 14:06:42 +00:00
|
|
|
|
- [x] 新建 `admin/controller/Venue.php`(CRUD + preview 调试接口)
|
|
|
|
|
|
- [x] 新建 `admin/view/venue/list.html`(场馆列表页)
|
|
|
|
|
|
- [x] 新建 `admin/view/venue/save.html`(Vue3 交互式表单编辑器:分区增删/座位排布/实时预览)
|
|
|
|
|
|
- [x] `BatchGenerate()` venue.name 动态读取(不再硬编码"国家体育馆")
|
2026-04-15 14:16:42 +00:00
|
|
|
|
- [x] 将现有测试数据迁移为带 venue 的格式(vr_seat_templates id=1)
|
|
|
|
|
|
- name = '国家体育馆',venue.name = '国家体育馆',3 区 × 6 座 = 18 座
|
2026-04-15 13:49:07 +00:00
|
|
|
|
- [ ] `BatchGenerate` 读取 `seat_map.venue.name`(不再硬编码)
|
2026-04-15 13:15:45 +00:00
|
|
|
|
|
|
|
|
|
|
### Phase 3-2:商品发布页注入
|
|
|
|
|
|
|
2026-04-15 13:49:07 +00:00
|
|
|
|
- [ ] `plugin.json` 的 `hooks` 数组追加 2 个钩子
|
|
|
|
|
|
- [ ] 新建 `hook/AdminGoodsSave.php`(注入票务配置 HTML)
|
2026-04-15 13:39:56 +00:00
|
|
|
|
- [ ] 新建 `hook/AdminGoodsSaveHandle.php`(处理保存数据)
|
2026-04-15 13:49:07 +00:00
|
|
|
|
- [ ] 场馆下拉 + 分区多选联动(Vue3,轻量)
|
|
|
|
|
|
- [ ] 确认商品发布页实际 Tab 名称(对照 `saveinfo.html`)
|
2026-04-15 13:15:45 +00:00
|
|
|
|
|
2026-04-15 13:49:07 +00:00
|
|
|
|
### Phase 3-3:Spec 自动生成接入 + 按分区过滤
|
2026-04-15 13:15:45 +00:00
|
|
|
|
|
2026-04-15 13:49:07 +00:00
|
|
|
|
- [ ] `BatchGenerate()` 增加 `enabledZones` 参数
|
|
|
|
|
|
- [ ] `AdminGoodsSaveHandle` 从 POST 提取选中分区,传给 `BatchGenerate`
|
|
|
|
|
|
- [ ] 数据库迁移:`sxo_goods` 新增 `vr_ticket_config` JSON 字段
|
|
|
|
|
|
- [ ] 发布时写入 `vr_ticket_config` 快照
|
2026-04-15 13:15:45 +00:00
|
|
|
|
|
|
|
|
|
|
### Phase 3-4(优先级低):商品编辑回显
|
|
|
|
|
|
|
2026-04-15 13:49:07 +00:00
|
|
|
|
- [ ] 编辑页加载时解析 `vr_ticket_config`,还原场馆 + 分区状态
|
2026-04-15 13:15:45 +00:00
|
|
|
|
- [ ] 编辑页票务配置面板回显
|
2026-04-15 13:49:07 +00:00
|
|
|
|
- [ ] 若修改了 venue/zone:提示重新生成 spec 或自动重建
|
|
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 十一、v3.0 确认决策
|
|
|
|
|
|
|
|
|
|
|
|
| 决策点 | 确认结果 |
|
|
|
|
|
|
|--------|---------|
|
|
|
|
|
|
| venue 信息存在哪 | 存入 `seat_map.venue` 顶层,不独立建表 |
|
|
|
|
|
|
| 是否支持按分区过滤 | **必须支持**,Phase 3-3 改造 BatchGenerate 增加 enabledZones 参数 |
|
|
|
|
|
|
| 数据库扩展兼容性 | 新增 `sxo_goods.vr_ticket_config` JSON 字段,老商品留 NULL |
|
|
|
|
|
|
| 老 seat_map 兼容性 | 降级处理:无 venue → 取模板 name;无 sections → 从 seats 推导 |
|
2026-04-15 13:15:45 +00:00
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
2026-04-15 13:49:07 +00:00
|
|
|
|
## 十二、勘误表(v1.0 → v2.0 → v3.0)
|
|
|
|
|
|
|
|
|
|
|
|
| # | v1.0 描述 | 实际/修正 |
|
|
|
|
|
|
|---|---------|---------|
|
|
|
|
|
|
| 1 | `plugin.json` 用 `backend_hook` 注册 | ShopXO 用 `hooks` 数组 ✅ v2.0 已修正 |
|
|
|
|
|
|
| 2 | seat_map 有 `venue` 顶层字段 | 不存在,v3.0 决策新增 ✅ |
|
|
|
|
|
|
| 3 | seat_map 有 `zones` 顶层字段 | 实际是 `seats` + `sections` ✅ v2.0 已修正 |
|
|
|
|
|
|
| 4 | BatchGenerate 全量生成 | v3.0 决策:必须支持按分区过滤 ✅ |
|
|
|
|
|
|
| 5 | `goods_spec_base` 前缀是 `vrt_` | 原生表是 `sxo_` ✅ v2.0 已修正 |
|
|
|
|
|
|
| 6 | `vr_ticket_config` 字段存在 | 不存在,v3.0 决策新增 ✅ |
|
|
|
|
|
|
| 7 | 场馆配置管理是 Phase 2 已完成 | 待实现,Phase 3-1 ✅ v2.0 已修正 |
|
|
|
|
|
|
| 8 | spec_base_id_map 是内存/缓存 | 持久化到 `vr_seat_templates.spec_base_id_map` ✅ v2.0 已修正 |
|
|
|
|
|
|
| 9 | 商品分类需绑定票务配置 | 代码未强制校验,删除此约束 ✅ v2.0 已修正 |
|
|
|
|
|
|
| 10 | 无 vr_ticket_config 导致编辑页 spec 堆满 | v3.0 决策:新增字段解决 ✅ |
|