vr-shopxo-plugin/docs/11_EDITOR_AND_INJECTION_DES...

13 KiB
Raw Blame History

后台编辑器 + 商品发布注入方案设计

版本v1.0 | 日期2026-04-15 | 状态:待大头确认后执行


一、整体架构一句话

插件在 ShopXO 后台建一套"场馆配置"管理界面,用户发布票务商品时,选场馆 → 插件自动生成海量 Spec 并注入商品,用户全程不碰 ShopXO 原生 Spec 管理。


二、三大核心区域

┌─────────────────────────────────────────────────────────────────┐
│  ShopXO 原生后台                                                   │
│                                                                   │
│  ┌─────────────────┐    ┌──────────────────────┐                │
│  │ 商品管理         │    │ 插件专属后台(新增)     │                │
│  │  发布/编辑商品   │ ←→ │ 场馆配置管理            │                │
│  │  (注入点)      │    │  座位分区模板编辑器     │                │
│  └─────────────────┘    │  场次配置              │                │
│                         └──────────────────────┘                │
└─────────────────────────────────────────────────────────────────┘

区域 A插件专属后台新增

商户在 ShopXO 后台左侧菜单进入「VR票务」

VR票务
  ├── 场馆配置        ← 新增
  ├── 座位模板        ← 已存在Phase 2
  ├── 电子票管理      ← 已存在
  ├── 核销员管理      ← 已存在
  └── 核销记录        ← 已存在

场馆配置管理的内容:

  • 场馆名称、地址、图片
  • 该场馆下的分区Zone列表VIP区/看台区/普通区
  • 每个分区的基础价格、颜色配置
  • 座位排布(每排几个座位,用字母+数字标记如 A_1, A_2

数据落地vr_seat_templates 表的 seat_map JSON 字段venue 信息作为 JSON 顶层嵌入)。

区域 BShopXO 商品发布页(注入点)

商户在 ShopXO 后台「商品管理 → 添加商品」:

添加商品
  [商品名称] [商品分类]
  
  ▼ 规格型号           ← ShopXO 原生区域,我们注入票务选择器
    票务配置           ← 新增:插件注入的区域
    [请选择场馆 ▼]    ← 场馆下拉
    [请选择分区 ▼]    ← 分区多选(根据场馆联动)
    
  [商品详情 富文本编辑器]
  ▼ 其他Tab参数/图片等)← ShopXO 原生

区域 C插件商品详情页已存在

用户在前台看到票务商品详情页,选座下单,这个已实现。


三、场馆配置管理(区域 A

3.1 数据结构

场馆 + 分区 + 座位,全部编码进 vr_seat_templates.seat_map 一个 JSON

{
  "venue": {
    "name": "国家体育馆",
    "address": "北京市朝阳区",
    "image": "/uploads/vr/venue/1.jpg"
  },
  "map": ["AAAAAA", "BBBBBB", "CCCCCC"],
  "zones": {
    "A": { "price": 899, "color": "#e74c3c", "label": "VIP区" },
    "B": { "price": 599, "color": "#3498db", "label": "看台区" },
    "C": { "price": 299, "color": "#2ecc71", "label": "普通区" }
  },
  "row_labels": ["A", "B", "C"],
  "sections": [
    { "char": "A", "name": "VIP区", "color": "#e74c3c" },
    { "char": "B", "name": "看台区", "color": "#3498db" },
    { "char": "C", "name": "普通区", "color": "#2ecc71" }
  ]
}

关键理解venue 信息和 seat_map 存在同一个 JSON 里,不拆表。vr_seat_templates 表的每一行 = 一个场馆配置(包含分区和座位布局)。

3.2 场馆配置管理页面(表单可视化编辑器)

商户在插件后台「场馆配置 → 添加」,看到以下表单:

【场馆基本信息】
  场馆名称:[________________________]
  场馆地址:[________________________]
  场馆图片:[上传按钮]
  
【分区配置】(可以加/减分区)
  ┌──────────────────────────────────────────────┐
  │ 分区 A         │ 标签VIP区  │ 单价899元   │ 颜色:[红]  │
  ├──────────────────────────────────────────────┤
  │ 分区 B         │ 标签:看台区 │ 单价599元   │ 颜色:[蓝]  │
  ├──────────────────────────────────────────────┤
  │ 分区 C         │ 标签:普通区 │ 单价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 与 ShopXO Spec 的关系

商户不需要知道 Spec 是什么。 他们只知道"我在场馆配置里建了一个场馆,里面有 A/B/C 三个区"。

Spec 是插件内部的事情。


四、商品发布页注入(区域 B

4.1 注入点

利用 ShopXO 钩子 plugins_view_admin_goods_save在商品发布页的「规格型号」tab 里注入我们的票务配置面板。

注入位置:商品发布页 → 「规格型号」Tab → <div class="am-form-group"> 容器内

商户视角:

规格型号
  ○ 使用商品的规格     ← ShopXO 原生(普通商品选这个)
  ● 使用票务配置       ← 新增(票务商品选这个)
  
  ▼ 票务配置(仅当"使用票务配置"选中时展开)
    场馆:[请选择场馆 ▼]         ← 来自 vr_seat_templates 表
    分区:[□VIP区  □看台区  □普通区]   ← 根据所选场馆联动

4.2 注入原理

ShopXO admin Goods::SaveInfo() 
  → 调用 hook plugins_view_admin_goods_save
  → 触发 vr_ticket/hook/AdminGoodsSave.php
  → 返回票务配置面板 HTML
  → 插入 saveinfo.html 的 base tab 内

关键代码路径(已在 ShopXO 源码中确认):

// Goods.php:159-167
$hook_name = 'plugins_view_admin_goods_save';
$assign[$hook_name.'_data'] = MyEventTrigger($hook_name, [...]);
MyViewAssign($assign);
return MyView(); // 模板里用 {{$plugins_view_admin_goods_save_data}} 输出

4.3 钩子注册

plugin.json 中新增:

{
  "backend_hook": {
    "plugins_view_admin_goods_save": [
      "\\app\\plugins\\vr_ticket\\hook\\AdminGoodsSave"
    ]
  }
}

4.4 场馆下拉数据来源

AdminGoodsSave.php 查询 vr_seat_templates 表,返回场馆列表(只返回顶层信息,不需要查所有座位):

$templates = Db::name('vr_seat_templates')
    ->field('id, name, seat_map')
    ->where(['status' => 1])
    ->select();
    
foreach ($templates as &$t) {
    $seatMap = json_decode($t['seat_map'], true);
    $t['venue_name'] = $seatMap['venue']['name'] ?? $t['name'];
}

五、商品发布完整流程

场景:商户发布一张票务商品

Step 1:商户进入 ShopXO 后台 → 商品管理 → 添加商品

Step 2:填写基础信息

商品名称:周杰伦 VR 虚拟演唱会
商品分类VR演出绑定到票务插件的分类
商品类型:[票务 ▼](已由插件注入的字段)

Step 3在「规格型号」Tab 选票务配置

规格型号
  ● 使用票务配置
  
  场馆:[国家体育馆 ▼]     ← AdminGoodsSave 注入的下拉
  分区:[✓VIP区  ✓看台区]  ← 多选,根据场馆联动

Step 4:点击发布

Save() 被调用
  ↓
GoodsService::GoodsSave() 执行标准商品保存逻辑
  ↓
触发钩子 plugins_service_goods_save_handle
  ↓
AdminGoodsSaveHandle() 收到 POST 数据
  ├── 提取 venue_id 和选中的 zone chars
  ├── 调用 SeatSkuService::BatchGenerate(goods_id, venue_id, zones)
  │     └── 为每个 zone 的每个座位生成一行 goods_spec_baseinventory=1
  │         └── 同时写入 goods_spec_value$vr-场馆 / $vr-分区 / $vr-时段 / $vr-座位号)
  ├── 更新 vr_seat_templates.spec_base_id_map
  └── 返回(让标准保存流程继续)
  ↓
商品保存完成
  ↓
订单后续流程不变(已有实现)

商户的感知我在下拉里选了个场馆和分区点发布商品就上线了。Spec 生成是静默的、无感的。


六、Spec 生成后的内部结构(技术细节)

以"国家体育馆 + VIP区(A) + 看台区(B)"为例:

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

goods_spec_type每行 = 一个规格维度)

id goods_id name value
1 123 $vr-场馆 [{"name":"国家体育馆"}]
2 123 $vr-分区 [{"name":"VIP区"},{"name":"看台区"}]
3 123 $vr-座位号 [{"name":"A_1"},{"name":"A_2"},...,{"name":"B_6"}]

spec_base_id_map内存/缓存)

商户在前台选座时,前端根据 seatKey"A_1")查表得到对应的 spec_base_id

{
  "A_1": 2001, "A_2": 2002, ..., "A_6": 2006,
  "B_1": 3001, "B_2": 3002, ..., "B_6": 3012
}

七、商品编辑时的处理(优先级低)

问题商户编辑已发布票务商品时ShopXO 后台会显示琳琅满目的 Spec 列表(几千个座位 SKU吓死人。

解决方向(暂不实现,先让创建流程跑通):

  1. 编辑页加载时,解析 spec_base_id_map,还原出 venue + zone 信息
  2. 在票务配置面板里回显"当前绑定的场馆 + 分区"
  3. 若商户修改了 venue/zone重新生成 Spec或提示"需先解绑"
  4. 如果不修改Spec 列表保持不动

目前策略:先让创建流程跑通,编辑流程后续迭代。


八、与 ShopXO 原生 Spec 管理的关系

维度 ShopXO 原生 Spec 我们的票务 Spec
谁创建 商户在商品编辑页手动添加 插件在发布时自动生成
谁看到 商户在后台规格管理看到 商户无感(我们注入的表单已覆盖场景)
用户在前台看到 购物车/下单流程 票务选座 UIticket_detail.html
核销 不涉及 每座位一个 QRvr_tickets 表)

商户永远不需要进入 ShopXO 原生的"规格管理"界面来管理票务座位。 票务 Spec 的完整生命周期由插件控制。


九、实施步骤

Phase 3-1后台场馆配置管理新增 admin 页面)

  • 新建 admin/controller/Venue.php
  • 新建 admin/view/venue/list.html(场馆列表)
  • 新建 admin/view/venue/save.html场馆表单编辑器venue + zone + 座位排布)
  • 升级 vr_seat_templates.seat_map JSON 结构(加入 venue 顶层)
  • 将现有测试数据的 seat_map 迁移为带 venue 的格式

Phase 3-2商品发布页注入

  • plugin.json 注册 plugins_view_admin_goods_save 钩子
  • 新建 hook/AdminGoodsSave.php(注入票务配置面板 HTML
  • 场馆下拉联动分区多选Vue3轻量
  • 注册 plugins_service_goods_save_handle 钩子处理保存数据

Phase 3-3Spec 自动生成接入

  • SeatSkuService::BatchGenerate() 接入商品发布流程(传入 goods_id
  • spec_base_id_map 写入 vr_seat_templates
  • extension_data 写入 order_goods选座信息追溯

Phase 3-4优先级低商品编辑回显

  • 编辑页加载时解析 spec_base_id_map还原 venue + zone
  • 编辑页票务配置面板回显

十、总结

商户操作:插件后台建场馆配置
    ↓
发布商品时:选场馆 + 分区
    ↓
插件静默:生成海量 Spec每座位1个 SKU写入商品
    ↓
用户前台:看到票务选座 UI无感知 Spec
    ↓
购买选座:每座位 → 1 个 order_goods → 1 张 QR

商户不需要知道 Spec不需要碰规格管理不需要理解 SKU。插件把这些全部封装成"选场馆、选分区"两个动作。