16 KiB
后台编辑器 + 商品发布注入方案设计
版本:v2.0 | 日期:2026-04-15 | 状态:待大头确认后执行
v2.0 更新:经 PM Auditor 代码级核查,修正 10 处与实际代码不符的描述(见文末勘误表)
一、整体架构一句话
插件在 ShopXO 后台建一套"场馆配置"管理界面,用户发布票务商品时,选场馆 → 插件自动生成海量 Spec 并注入商品,用户全程不碰 ShopXO 原生 Spec 管理。
二、三大核心区域
┌─────────────────────────────────────────────────────────────────┐
│ ShopXO 原生后台 │
│ │
│ ┌─────────────────┐ ┌──────────────────────┐ │
│ │ 商品管理 │ │ 插件专属后台(新增) │ │
│ │ 发布/编辑商品 │ ←→ │ 场馆配置管理 │ │
│ │ (注入点) │ │ 座位分区模板编辑器 │ │
│ └─────────────────┘ └──────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
区域 A:插件专属后台(新增)
商户在 ShopXO 后台左侧菜单进入「VR票务」:
VR票务
├── 场馆配置 ← Phase 3-1 新增(当前不存在)
├── 座位模板 ← 已存在(Phase 2)
├── 电子票管理 ← 已存在
├── 核销员管理 ← 已存在
└── 核销记录 ← 已存在
⚠️ 当前状态:
Venue.php控制器不存在。vr_seat_templates表只有name/category_id/seat_map/spec_base_id_map字段,venue信息目前不存在于数据库中。
区域 B:ShopXO 商品发布页(注入点)
⚠️ 待核实:商品发布页 Tab 名称需对照
saveinfo.html模板确认。当前描述"规格型号"Tab 可能不准确。
商户在 ShopXO 后台「商品管理 → 添加商品」:
添加商品
[商品名称] [商品分类]
▼ 票务配置 ← ShopXO 原生区域,我们注入票务选择器
[请选择场馆 ▼] ← 场馆下拉(来自 vr_seat_templates 表)
[请选择分区 ▼] ← 分区多选(根据场馆联动)
[商品详情 富文本编辑器]
▼ 其他Tab(参数/图片等)← ShopXO 原生
区域 C:插件商品详情页(已存在)
用户在前台看到票务商品详情页,选座下单,这个已实现。
三、场馆配置管理(区域 A)
3.1 数据结构(当前 vs 目标)
⚠️ 当前 seat_map 实际结构(从
SeatSkuService::BatchGenerate代码反推):
- 无
venue顶层字段- 无
zones顶层字段- 顶层字段为:
map、seats、sections、row_labels$vr-场馆的值目前硬编码为"国家体育馆"
目标结构(Phase 3-1 需调整):
{
"venue": { // ← Phase 3-1 新增:从 venue 表读取或直接存这里
"name": "国家体育馆",
"address": "北京市朝阳区",
"image": "/uploads/vr/venue/1.jpg"
},
"map": ["AAAAAA", "BBBBBB", "CCCCCC"],
"seats": { // ← 实际字段名,不是 zones
"A": { "price": 899, "color": "#e74c3c", "label": "VIP区" },
"B": { "price": 599, "color": "#3498db", "label": "看台区" },
"C": { "price": 299, "color": "#2ecc71", "label": "普通区" }
},
"sections": [ // ← 实际字段名,存储分区元信息
{ "char": "A", "name": "VIP区", "color": "#e74c3c" },
{ "char": "B", "name": "看台区", "color": "#3498db" },
{ "char": "C", "name": "普通区", "color": "#2ecc71" }
],
"row_labels": ["A", "B", "C"]
}
关键理解:
seats= 每行(row char)的默认价格/颜色/标签(整行统一)sections= 每个分区的元信息(char → name/color 映射)venue信息目前不存在于 seat_map,Phase 3-1 需要决策:是加到 seat_map 里,还是新建vr_venues表
3.2 场馆配置管理页面(表单可视化编辑器)
⚠️ 待实现:当前
Venue.php控制器不存在,Phase 3-1 需要新建。
商户在插件后台「场馆配置 → 添加」,看到以下表单:
【场馆基本信息】
场馆名称:[________________________]
场馆地址:[________________________]
场馆图片:[上传按钮]
【分区配置】(可以加/减分区)
┌──────────────────────────────────────────────────┐
│ Char │ 标签:VIP区 │ 单价:899元 │ 颜色:[红] │
├──────────────────────────────────────────────────┤
│ Char │ 标签:看台区 │ 单价:599元 │ 颜色:[蓝] │
├──────────────────────────────────────────────────┤
│ Char │ 标签:普通区 │ 单价:299元 │ 颜色:[绿] │
└──────────────────────────────────────────────────┘
[+ 添加分区] [- 删除分区]
【座位排布预览】
A A A A A A
B B B B B B
C C C C C C
每排座位数:[6___] 排数自动生成
【保存】 【取消】
技术实现:
- layui 表单 + Vue3 CDN(轻量,不破坏 ShopXO 后台已有的 jQuery/layui 结构)
- 约 500 行前端代码,1-1.5 人天
- 保存时:表单数据 → 编码成 JSON → 写入
vr_seat_templates.seat_map
3.3 venue 信息的设计决策(待讨论)
选项 A:venue 信息存入 seat_map.venue(JSON 顶层)
- 优点:简单,不改表结构
- 缺点:一个模板只能关联一个 venue
选项 B:新建 vr_venues 表,vr_seat_templates.venue_id 外键关联
- 优点:一个 venue 可对应多个模板(如鸟巢主场地 + 鸟巢室外场)
- 缺点:多一张表,多一套 CRUD
当前
vr_seat_templates表只有name字段同时承载"模板名"和"场馆名",暂未分离。
四、商品发布页注入(区域 B)
4.1 注入原理
ShopXO admin Goods::SaveInfo()
→ 调用 hook plugins_view_admin_goods_save(Goods.php:159)
→ 触发 vr_ticket/hook/AdminGoodsSave.php(Phase 3-2 新建)
→ 返回票务配置面板 HTML
→ 插入 saveinfo.html 的 base tab 内(<div class="am-form-group"> 容器)
4.2 钩子注册方式
⚠️ 重要修正:ShopXO 插件系统不支持
backend_hook字段。plugin.json实际使用hooks数组。
plugin.json 正确格式:
{
"hooks": [
"plugins_service_order_pay_success_handle_end",
"plugins_service_order_delete_success",
"plugins_view_admin_goods_save",
"plugins_service_goods_save_handle"
]
}
当前
plugin.json只有前两个钩子,后两个是 Phase 3-2 需要追加的。
4.3 AdminGoodsSave.php(待新建)
⚠️ 当前不存在:
vr_ticket/目录下无AdminGoodsSave.php文件。
Phase 3-2 需要新建:
- 文件路径:
plugins/vr_ticket/hook/AdminGoodsSave.php plugins_view_admin_goods_save钩子返回 HTML 注入票务表单plugins_service_goods_save_handle钩子处理票务数据保存
4.4 场馆下拉数据来源
AdminGoodsSave.php 查询 vr_seat_templates 表:
$templates = Db::name('vr_seat_templates')
->field('id, name, seat_map')
->where(['status' => 1])
->select();
当前 seat_map 中无 venue.name,所以下拉显示的是 vr_seat_templates.name 字段(如 "Bird Nest - Zone A")。
⚠️ Phase 3-1 需要决策:venue 独立后,下拉应显示 venue.name 而非模板 name。
五、商品发布完整流程
场景:商户发布一张票务商品
Step 1:商户进入 ShopXO 后台 → 商品管理 → 添加商品
Step 2:填写基础信息
商品名称:周杰伦 VR 虚拟演唱会
商品分类:VR演出
Step 3:在注入的票务配置面板选场馆和分区
票务配置
场馆:[Bird Nest - Zone A ▼] ← AdminGoodsSave 注入的下拉
分区:[✓VIP区 ✓看台区] ← 多选,根据场馆联动
Step 4:点击发布
商户点击"发布商品"
↓
Goods::Save() 调用 GoodsService::GoodsSave()(Goods.php:187)
↓
GoodsService::GoodsSave() 执行标准商品保存逻辑
↓
触发钩子 plugins_service_goods_save_handle(GoodsService.php:1550)
↓
AdminGoodsSaveHandle() 收到 POST 数据
├── 提取 template_id 和选中的分区 chars
├── 调用 SeatSkuService::BatchGenerate($goodsId, $templateId)
│ └── 注意:BatchGenerate() 签名是 (int $goodsId, int $seatTemplateId)
│ └── 当前版本:按 template 全量生成(不支持按 zones 过滤)
│ └── 为每个座位生成一行 sxo_goods_spec_base(inventory=1)
│ └── 同时写入 sxo_goods_spec_value($vr-场馆/$vr-分区/$vr-时段/$vr-座位号)
├── 更新 vr_seat_templates.spec_base_id_map(持久化,非内存)
└── 返回(让标准保存流程继续)
↓
商品保存完成
↓
订单后续流程不变(已有实现)
六、Spec 生成后的内部结构(技术细节)
⚠️ 表前缀:ShopXO 原生表使用
sxo_前缀,插件自定义表使用{$prefix}vrt_。
以"国家体育馆 + VIP区(A) + 看台区(B)"为例:
sxo_goods_spec_base(每行 = 一个座位 SKU)
| spec_base_id | goods_id | inventory | price |
|---|---|---|---|
| 2001 | 123 | 1 | 899 |
| 2002 | 123 | 1 | 899 |
| ... | 123 | 1 | 899 |
| 3001 | 123 | 1 | 599 |
| ... | 123 | 1 | 599 |
sxo_goods_spec_type(每行 = 一个规格维度)
| id | goods_id | name | value |
|---|---|---|---|
| 1 | 123 | $vr-场馆 | [{"name":"国家体育馆"}] |
| 2 | 123 | $vr-分区 | [{"name":"VIP区"},{"name":"看台区"}] |
| 3 | 123 | $vr-时段 | [{"name":"待选场次"}] |
| 4 | 123 | $vr-座位号 | [{"name":"A_1"},{"name":"A_2"},...,{"name":"B_6"}] |
vr_seat_templates.spec_base_id_map(持久化字段)
⚠️ 修正:
spec_base_id_map不是内存/缓存,是持久化到vr_seat_templates表的字段。
商户在前台选座时,前端根据 seatKey(如 "A_1")查表得到对应的 spec_base_id:
{
"A_1": 2001, "A_2": 2002, ..., "A_6": 2006,
"B_1": 3001, "B_2": 3002, ..., "B_6": 3012
}
七、BatchGenerate 实际接口(已实现代码)
⚠️ 关键修正:文档之前描述的参数签名与实际代码不符。
实际签名(SeatSkuService.php:34):
public static function BatchGenerate(int $goodsId, int $seatTemplateId): array
返回格式:
[
'code' => 0,
'data' => [
'total' => 18,
'generated' => 12,
'spec_base_id_map' => ['A_1' => 2001, 'A_2' => 2002, ...]
]
]
与文档描述的差异:
- 文档说:
(goods_id, venue_id, zones[])— ❌ 错误 - 实际是:
(int $goodsId, int $seatTemplateId)— 全量按模板生成,不支持按 zones 过滤
⚠️ 如果 Phase 3-2 需要支持"按分区选择生成",需要修改
BatchGenerate()增加 zones 过滤逻辑。
八、商品编辑时的处理(优先级低)
问题:商户编辑已发布票务商品时,ShopXO 后台会显示琳琅满目的 Spec 列表(几千个座位 SKU)。
解决方案(暂不实现,先让创建流程跑通):
方案 A(推荐):商品表新增 vr_ticket_config JSON 字段
- 保存时:只存
{ template_id, zones[], created_at } - 编辑时:从此字段还原venue + zone 选择状态,不从 spec_base 反推
- 需要迁移:新增
sxo_goods.vr_ticket_config字段
方案 B:直接用 sxo_goods.extension_data(如果 ShopXO 支持)
⚠️ 当前商品表无
vr_ticket_config字段。方案 A 需要新建数据库迁移。
九、与 ShopXO 原生 Spec 管理的关系
| 维度 | ShopXO 原生 Spec | 我们的票务 Spec |
|---|---|---|
| 谁创建 | 商户在商品编辑页手动添加 | 插件在发布时自动生成 |
| 表前缀 | sxo_ |
sxo_goods_spec_base / sxo_goods_spec_value |
| 商户感知 | 在后台规格管理看到 | 无感(我们注入的表单已覆盖场景) |
| 用户在前台看到 | 购物车/下单流程 | 票务选座 UI(ticket_detail.html) |
| 核销 | 不涉及 | 每座位一个 QR(vr_tickets 表) |
商户永远不需要进入 ShopXO 原生的"规格管理"界面来管理票务座位。
十、实施步骤
Phase 3-1:后台场馆配置管理(新建 admin 页面)
- 决策:venue 信息存在 seat_map 内还是独立表?
- 新建
admin/controller/Venue.php - 新建
admin/view/venue/list.html(场馆列表) - 新建
admin/view/venue/save.html(表单编辑器:venue + zone + 座位排布) - 升级
vr_seat_templates.seat_mapJSON 结构(加入 venue 顶层) - 将现有测试数据迁移为带 venue 的格式
Phase 3-2:商品发布页注入
- 在
plugin.json的hooks数组中追加钩子(不用backend_hook) - 新建
hook/AdminGoodsSave.php(注入票务配置面板 HTML) - 新建
hook/AdminGoodsSaveHandle.php(处理保存数据) - 场馆下拉联动分区多选(Vue3,轻量)
- 确认商品发布页实际 Tab 名称(待对照
saveinfo.html)
Phase 3-3:Spec 自动生成接入
BatchGenerate()增加按 zones 过滤参数(如需要)extension_data写入 order_goods(选座信息追溯)
Phase 3-4(优先级低):商品编辑回显
- 新建
sxo_goods.vr_ticket_config字段迁移 - 编辑页加载时解析
vr_ticket_config,还原 venue + zone - 编辑页票务配置面板回显
十一、勘误表(v1.0 → v2.0)
| # | v1.0 描述 | 实际代码 | v2.0 修正 |
|---|---|---|---|
| 1 | plugin.json 用 backend_hook 注册 |
ShopXO 用 hooks 数组 |
改为追加到 hooks 数组 |
| 2 | AdminGoodsSave.php 已存在或待实现 |
文件不存在 | 明确为 Phase 3-2 新建任务 |
| 3 | seat_map 顶层有 venue 字段 |
不存在,$vr-场馆 硬编码 |
新增设计决策:venue 存在哪 |
| 4 | seat_map 顶层有 zones 字段 |
实际是 seats 和 sections |
全文修正字段名 |
| 5 | BatchGenerate(goods_id, venue_id, zones[]) |
(int $goodsId, int $seatTemplateId) 全量生成 |
修正签名,注明全量/过滤差异 |
| 6 | goods_spec_base 前缀是 vrt_ |
ShopXO 原生表是 sxo_ |
修正前缀,说明 sxo_ vs vrt_ |
| 7 | vr_ticket_config 字段存在 |
不存在,需新建迁移 | 明确为 Phase 3-4 新建任务 |
| 8 | 场馆配置是 Phase 2 已完成 | Venue.php 不存在,Phase 3-1 待实现 |
修正当前状态标注 |
| 9 | spec_base_id_map 是内存/缓存 |
持久化到 vr_seat_templates.spec_base_id_map |
修正存储位置描述 |
| 10 | 商品分类需绑定票务配置 | 代码中未强制校验 | 删除此约束描述 |