diff --git a/docs/11_EDITOR_AND_INJECTION_DESIGN.md b/docs/11_EDITOR_AND_INJECTION_DESIGN.md new file mode 100644 index 0000000..848264d --- /dev/null +++ b/docs/11_EDITOR_AND_INJECTION_DESIGN.md @@ -0,0 +1,368 @@ +# 后台编辑器 + 商品发布注入方案设计 + +> 版本: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 顶层嵌入)。 + +### 区域 B:ShopXO 商品发布页(注入点) + +商户在 ShopXO 后台「商品管理 → 添加商品」: + +``` +添加商品 + [商品名称] [商品分类] + + ▼ 规格型号 ← ShopXO 原生区域,我们注入票务选择器 + 票务配置 ← 新增:插件注入的区域 + [请选择场馆 ▼] ← 场馆下拉 + [请选择分区 ▼] ← 分区多选(根据场馆联动) + + [商品详情 富文本编辑器] + ▼ 其他Tab(参数/图片等)← ShopXO 原生 +``` + +### 区域 C:插件商品详情页(已存在) + +用户在前台看到票务商品详情页,选座下单,这个已实现。 + +--- + +## 三、场馆配置管理(区域 A) + +### 3.1 数据结构 + +场馆 + 分区 + 座位,全部编码进 `vr_seat_templates.seat_map` 一个 JSON: + +```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 → `
` 容器内 + +商户视角: +``` +规格型号 + ○ 使用商品的规格 ← 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 源码中确认): +```php +// 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` 中新增: +```json +{ + "backend_hook": { + "plugins_view_admin_goods_save": [ + "\\app\\plugins\\vr_ticket\\hook\\AdminGoodsSave" + ] + } +} +``` + +### 4.4 场馆下拉数据来源 + +`AdminGoodsSave.php` 查询 `vr_seat_templates` 表,返回场馆列表(只返回顶层信息,不需要查所有座位): + +```php +$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_base(inventory=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 | ← A_1 座位 +| 2002 | 123 | 1 | 899 | ← A_2 座位 +| ... | 123 | 1 | 899 | ← A_6 座位 +| 3001 | 123 | 1 | 599 | ← B_1 座位 +| ... | 123 | 1 | 599 | ← B_6 座位 + +### 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: + +```json +{ + "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 | +|------|----------------|----------------| +| 谁创建 | 商户在商品编辑页手动添加 | 插件在发布时自动生成 | +| 谁看到 | 商户在后台规格管理看到 | 商户无感(我们注入的表单已覆盖场景) | +| 用户在前台看到 | 购物车/下单流程 | 票务选座 UI(ticket_detail.html)| +| 核销 | 不涉及 | 每座位一个 QR(vr_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-3:Spec 自动生成接入 + +- [ ] `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。插件把这些全部封装成"选场馆、选分区"两个动作。