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

19 KiB
Raw Permalink Blame History

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

版本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

一、整体架构一句话

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


二、三大核心区域

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

区域 A插件专属后台新增

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

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

⚠️ 当前状态Venue.php 控制器不存在Phase 3-1 从零新建。

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

⚠️ 待核实:商品发布页 Tab 名称需对照 saveinfo.html 模板确认。

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

添加商品
  [商品名称] [商品分类]

  ▼ 票务配置           ← ShopXO 原生区域,插件注入票务选择器
    场馆:[请选择场馆 ▼]     ← 来自 vr_seat_templates 表
    分区:[□VIP区  □看台区  □普通区]   ← 多选,根据场馆联动

  [商品详情 富文本编辑器]
  ▼ 其他Tab参数/图片等)← ShopXO 原生

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

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


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

3.1 seat_map 数据结构v3.0 最终版)

{
  "venue": {
    "name": "国家体育馆",
    "address": "北京市朝阳区奥体中心",
    "image": "/uploads/vr/venue/national-stadium.jpg"
  },
  "map": [
    "AAAAAA",
    "BBBBBB",
    "CCCCCC"
  ],
  "seats": {
    "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"]
}

字段说明

字段 类型 说明
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"...

$vr-场馆 spec 值来源:从 seat_map.venue.name 读取Phase 3-1 改造 BatchGenerate

与旧数据的兼容性

  • 存量 seat_map 无 venue 顶层 → 降级取 vr_seat_templates.name 作为 $vr-场馆
  • 存量 seat_map 无 sections 顶层 → 降级从 seats 推导

3.2 美化版 seat_map 示例Phase 3-1 表单填充示例)

示例 1大型演唱会3 区,每区 6 座)

{
  "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 座)

{
  "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开放部分分区电影院场景

{
  "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 控制器不存在。

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

【场馆基本信息】
  场馆名称:[_______________________________]
  场馆地址:[_______________________________]
  场馆图片:[上传按钮]

【分区配置】(增删分区,拖拽排序)
  ┌──────────────────────────────────────────────────┐
  │ Char │ 标签      │ 单价(元)│ 颜色      │       │
  ├──────────────────────────────────────────────────┤
  │ A    │ 内场VIP   │ 1299      │ [■红    ▼]│ [删除] │
  ├──────────────────────────────────────────────────┤
  │ B    │ 内场A区   │ 899       │ [■紫    ▼]│ [删除] │
  ├──────────────────────────────────────────────────┤
  │ C    │ 内场B区   │ 599       │ [■蓝    ▼]│ [删除] │
  └──────────────────────────────────────────────────┘
  [+ 添加分区]

【座位排布预览】(实时渲染)
  🔴 🔴 🔴 🔴 🔴 🔴    ← A区VIP1299元
  🟣 🟣 🟣 🟣 🟣 🟣    ← B区内场A区899元
  🔵 🔵 🔵 🔵 🔵 🔵    ← C区内场B区599元

  每排座位数:[6___]   总座位数18

【保存】  【取消】

技术实现

  • layui 表单 + Vue3 CDN轻量不破坏 ShopXO 后台 jQuery/layui 结构)
  • ~500 行前端代码1-1.5 人天
  • 保存:表单 → seat_map JSON → 写入 vr_seat_templates.seat_map
  • 颜色选择器:预设色盘(不引入额外依赖)

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

4.1 注入原理

ShopXO admin Goods::SaveInfo()
  → 调用 hook plugins_view_admin_goods_saveGoods.php:159
  → 触发 vr_ticket/hook/AdminGoodsSave.php
  → 返回票务配置面板 HTML
  → 插入 saveinfo.html base tab <div class="am-form-group"> 内

4.2 钩子注册方式

⚠️ 重要ShopXO 不支持 backend_hook 字段,实际使用 hooks 数组。

plugin.json 追加Phase 3-2

{
  "hooks": [
    "plugins_service_order_pay_success_handle_end",
    "plugins_service_order_delete_success",
    "plugins_view_admin_goods_save",
    "plugins_service_goods_save_handle"
  ]
}

4.3 AdminGoodsSave.php待新建

⚠️ 待实现vr_ticket/ 下当前无此文件。

文件plugins/vr_ticket/hook/AdminGoodsSave.php

职责:

  • 查询 vr_seat_templates 表,构造场馆下拉 + 分区多选 HTML
  • 返回 layui + Vue3 CDN 的票务配置面板

数据来源

$templates = Db::name('vr_seat_templates')
    ->field('id, name, seat_map, category_id')
    ->where(['status' => 1])
    ->select();

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'] ?? [];
}

4.4 AdminGoodsSaveHandle.php待新建

文件plugins/vr_ticket/hook/AdminGoodsSaveHandle.php

触发时机:plugins_service_goods_save_handleGoodsService.php:1550

职责:

// 接收 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)]);

五、商品发布完整流程

场景:商户发布一张票务商品(只开放 A 区和 C 区)

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}]
  → 下单

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

⚠️ 表前缀ShopXO 原生表用 sxo_,插件自定义表用 {$prefix}vrt_

sxo_goods_spec_base每行 = 一个座位 SKU

spec_base_id goods_id inventory price
2001 123 1 1299
2002 123 1 1299
... 123 1 1299
5001 123 1 599
... 123 1 599

B 区座位未生成(商户只选了 A + C

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":"C_6"}]

vr_ticket_configsxo_goods 表新增字段)

{
  "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
}

七、BatchGenerate 接口(含 v3.0 改造)

7.1 当前已实现版本全量v2.0

⚠️ 以下为当前 main 分支代码,不支持按分区过滤Phase 3-3 需改造):

public static function BatchGenerate(int $goodsId, int $seatTemplateId): array
// 行为:按模板全部座位生成 SKU

7.2 Phase 3-3 改造版本(按分区过滤)

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...
//     }
// }

与旧版差异

维度 旧版(全量) 新版v3.0
参数 (goodsId, templateId) (goodsId, templateId, enabledZones)
行为 所有座位都生成 只生成 enabledZones 内的分区座位
兼容性 enabledZones 为空时退化为全量(向后兼容)

八、vr_ticket_config 字段v3.0 新增)

8.1 数据库迁移

-- 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;

老商品兼容:存量商品 vr_ticket_config = NULL,编辑页检测到为空时提示"请重新选择"。

8.2 生命周期

时机 操作
发布商品时 AdminGoodsSaveHandle 写入 vr_ticket_config
编辑商品时 从此字段还原"选择了哪些场馆/分区" → 不展示琳琅满目的 spec_base 列表
删除商品时 钩子 plugins_service_order_delete_success 同步清理关联 spec

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

维度 ShopXO 原生 Spec 票务 Spec
谁创建 商户手动添加 插件自动生成(静默)
表前缀 sxo_ sxo_goods_spec_base / sxo_goods_spec_value
商户感知 在后台规格管理看到 无感(注入的表单已覆盖场景)
用户在前台 购物车/下单流程 票务选座 UI
核销 不涉及 每座位一个 QRvrt_vr_tickets

十、实施步骤

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

  • 决策已确认venue 信息存入 seat_map 顶层
  • 新建 admin/controller/Venue.phpCRUD + preview 调试接口)
  • 新建 admin/view/venue/list.html(场馆列表页)
  • 新建 admin/view/venue/save.htmlVue3 交互式表单编辑器:分区增删/座位排布/实时预览)
  • BatchGenerate() venue.name 动态读取(不再硬编码"国家体育馆"
  • 将现有测试数据迁移为带 venue 的格式vr_seat_templates id=1
    • name = '国家体育馆'venue.name = '国家体育馆'3 区 × 6 座 = 18 座
  • BatchGenerate 读取 seat_map.venue.name(不再硬编码)

Phase 3-2商品发布页注入

  • plugin.jsonhooks 数组追加 2 个钩子
  • 新建 hook/AdminGoodsSave.php(注入票务配置 HTML
  • 新建 hook/AdminGoodsSaveHandle.php(处理保存数据)
  • 场馆下拉 + 分区多选联动Vue3轻量
  • 确认商品发布页实际 Tab 名称(对照 saveinfo.html

Phase 3-3Spec 自动生成接入 + 按分区过滤

  • BatchGenerate() 增加 enabledZones 参数
  • AdminGoodsSaveHandle 从 POST 提取选中分区,传给 BatchGenerate
  • 数据库迁移:sxo_goods 新增 vr_ticket_config JSON 字段
  • 发布时写入 vr_ticket_config 快照

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

  • 编辑页加载时解析 vr_ticket_config,还原场馆 + 分区状态
  • 编辑页票务配置面板回显
  • 若修改了 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 推导

十二、勘误表v1.0 → v2.0 → v3.0

# v1.0 描述 实际/修正
1 plugin.jsonbackend_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 决策:新增字段解决