266 lines
11 KiB
Markdown
266 lines
11 KiB
Markdown
# vr-shopxo-plugin 编辑器方案调研报告
|
||
|
||
> 版本:v1.0 | 日期:2026-04-15 | Agent:BackendArchitect (Q2) + FrontendDev (Q1)
|
||
|
||
## Q2:商品发布页替换方案(邪门方案)可行性 — BackendArchitect
|
||
|
||
### 核心代码路径
|
||
|
||
| 文件 | 作用 |
|
||
|------|------|
|
||
| `shopxo/app/admin/controller/Goods.php:82-177` | SaveInfo() 方法 |
|
||
| `shopxo/app/admin/controller/Goods.php:187-192` | Save() 方法 |
|
||
| `shopxo/app/service/GoodsService.php:1549-1565` | plugins_service_goods_save_handle 钩子 |
|
||
| `shopxo/app/admin/view/default/goods/saveinfo.html:505-510` | 钩子渲染位置 |
|
||
|
||
---
|
||
|
||
### Q2-A: 钩子调用位置分析
|
||
|
||
**`plugins_view_admin_goods_save` 在 SaveInfo() 中的位置**(Goods.php:159-167):
|
||
|
||
```php
|
||
$hook_name = 'plugins_view_admin_goods_save';
|
||
$assign[$hook_name.'_data'] = MyEventTrigger($hook_name, [
|
||
'hook_name' => $hook_name,
|
||
'is_backend' => true,
|
||
'goods_id' => isset($params['id']) ? $params['id'] : 0,
|
||
'data' => &$data,
|
||
'params' => &$params,
|
||
]);
|
||
// 紧接着:
|
||
MyViewAssign($assign);
|
||
return MyView(); // 渲染 saveinfo.html
|
||
```
|
||
|
||
**结论:钩子在模板渲染之前被调用,结果存入 `$assign['plugins_view_admin_goods_save_data']` 供模板使用。**
|
||
|
||
---
|
||
|
||
### Q2-B: 能否完全替换页面内容?
|
||
|
||
**关键发现:NO — 钩子仅是注入点,不是替换点。**
|
||
|
||
查看 `saveinfo.html` 模板结构(简化):
|
||
```
|
||
ModuleInclude('public/header')
|
||
<div class="content">
|
||
<form action="admin/goods/save" method="POST">
|
||
[商品名称输入框]
|
||
[商品分类选择器]
|
||
[nav_switch_btn: base/spec/parameters/photos/content/video/seo/use_guide]
|
||
<!-- base tab 内容 -->
|
||
<div class="am-form-group">
|
||
<label>...</label>
|
||
<div>
|
||
plugins_view_admin_goods_save ← 钩子在此注入
|
||
</div>
|
||
</div>
|
||
[SEO信息tab]
|
||
[popup submit按钮]
|
||
</form>
|
||
</div>
|
||
```
|
||
|
||
钩子注入位置在第 505-510 行:
|
||
```html
|
||
{{if !empty($plugins_view_admin_goods_save_data) and is_array(...)}}
|
||
{{foreach $plugins_view_admin_goods_save_data as $hook}}
|
||
{{$hook|raw}}
|
||
{{/foreach}}
|
||
{{else /}}
|
||
{{:ModuleInclude('public/not_data')}}
|
||
{{/if}}
|
||
```
|
||
|
||
**注入内容受限于 `<div class="am-form-group">` 容器内**,外层 `<form>`、Tab 导航、商品名称/分类等核心字段无法被替换。
|
||
|
||
### 替代方案:模板文件覆盖
|
||
|
||
`MyView()` 函数(common.php:984-991)支持主题覆盖插件文件:
|
||
```php
|
||
if(substr($view, 0, 16) == '../../../plugins') {
|
||
$plugins_view_file = APP_PATH.$group.DS.'view'.DS.$theme.DS.'plugins'.DS.str_replace(...);
|
||
if(@file_exists($plugins_view_file)) {
|
||
$view = $plugins_view_file; // 主题文件覆盖插件文件
|
||
}
|
||
}
|
||
```
|
||
但这只对 `plugins` 控制器的路径生效。SaveInfo() 是 `admin/goods` 路径,无法利用此机制。
|
||
|
||
**唯一可行的完全替换路径**:将 `saveinfo.html` 复制到 `app/admin/view/default/goods/saveinfo.html`(ShopXO 默认主题目录),然后修改。但这是覆盖核心文件,升级 ShopXO 时会丢失。
|
||
|
||
**推荐:不要完全替换页面。** 改为在 base tab 内注入 ticket 专属表单,或添加新的 tab 项。
|
||
|
||
---
|
||
|
||
### Q2-C: Save() 数据接收方式
|
||
|
||
**Goods::Save()(Goods.php:187-192)**:
|
||
```php
|
||
public function Save() {
|
||
$params = $this->data_request; // ← ThinkPHP 标准请求数据($_POST)
|
||
$params['admin'] = $this->admin;
|
||
return ApiService::ApiDataReturn(GoodsService::GoodsSave($params));
|
||
}
|
||
```
|
||
|
||
数据源是 ThinkPHP 的 `$this->data_request`,等价于标准 `$_POST`。**任何自定义表单都可以 POST 到 `admin/goods/save`,只要字段名符合 GoodsService 期望。**
|
||
|
||
**GoodsService::GoodsSave() 中钩子位置(GoodsService.php:1549-1565)**:
|
||
```php
|
||
// 构建 $data 数组(从 $params 提取 title, category_ids 等)
|
||
$data['title'] = $params['title'] ?? '';
|
||
// ... 更多字段 ...
|
||
|
||
// 商品保存处理钩子 — 在事务启动之前
|
||
$ret = EventReturnHandle(MyEventTrigger('plugins_service_goods_save_handle', [
|
||
'params' => &$params, // 引用:可修改
|
||
'data' => &$data, // 引用:可修改(影响最终 INSERT/UPDATE)
|
||
'spec' => &$specifications['data'],
|
||
'goods_id' => isset($params['id']) ? intval($params['id']) : 0,
|
||
]));
|
||
if(isset($ret['code']) && $ret['code'] != 0) {
|
||
return $ret; // ← 钩子可提前返回,阻止标准保存
|
||
}
|
||
```
|
||
|
||
**关键能力**:
|
||
1. `$data` 数组通过引用传入,插件可修改后影响最终 `INSERT/UPDATE`
|
||
2. 钩子返回 `['code'=>0, 'msg'=>'...', 'data'=>...]` 可阻止标准流程(直接返回)
|
||
3. 但 `$params['title']`、`$params['category_ids']` 等字段在钩子调用前已被提取进 `$data`
|
||
|
||
**两条可行路径**:
|
||
- **路径A(推荐)**:在 `$data` 中填入最小必需字段(title、category_ids 等),让标准 INSERT 继续执行,插件在钩子内完成票务数据保存
|
||
- **路径B**:钩子直接 `Db::startTrans()` 自己处理票务数据,然后 `return ['code'=>0]` 阻止标准流程
|
||
|
||
---
|
||
|
||
### Q2-D: 插件视图文件路径可行性
|
||
|
||
目前 `plugin.json` 中未注册 `plugins_view_admin_goods_save` 钩子(只有 `onOrderPaid`)。需要两步启用:
|
||
|
||
1. **注册钩子**:在 `plugin.json` 添加:
|
||
```json
|
||
"backend_hook": {
|
||
"plugins_view_admin_goods_save": ["\\app\\plugins\\vr_ticket\\hook\\AdminGoodsSave"]
|
||
}
|
||
```
|
||
|
||
2. **实现 AdminGoodsSave.php**:返回 HTML 字符串,注入到 saveinfo.html 的 base tab
|
||
|
||
**插件视图文件路径**:`plugins/vr_ticket/view/admin/goods/ticket_save.html`(如果走模板覆盖方案)
|
||
|
||
---
|
||
|
||
### item_type 字段验证
|
||
|
||
- `goods` 表已存在 `item_type` 字段(EventListener.php:129),值包括 `'normal'` / `'ticket'` / `'physical'`
|
||
- 前台 `app/index/controller/Goods.php:139` 已有 `item_type == 'ticket'` 判断
|
||
- **后台 `Goods.php`(admin控制器)中不存在 `item_type` 判断逻辑**——任务描述中关于此判断存在于后台的说法需要更正
|
||
|
||
---
|
||
|
||
## Q1:JSON 编辑器复杂度评估(FrontendDev)
|
||
|
||
### Q1.1: ShopXO DIY 组件中是否有现成 JSON 编辑器?
|
||
|
||
**结论:ShopXO DIY 编辑器为商业闭源组件,无公开源码,无 JSON 编辑能力。**
|
||
|
||
调研过程:
|
||
- `shopxo/public/static/diy/js/entry/index-d71f3dad.js` — 预构建的单个大型压缩包(4MB+ CSS + 完整 Vue3 运行时),无可读源码
|
||
- `shopxo/public/static/diy/js/chunk/` — 仅含 3 个空壳 JS 文件,总计 ~5 行代码
|
||
- `shopxo/sourcecode/index.html` — 空占位文件(ShopXO 源码包需从其商业平台单独获取)
|
||
- `shopxo/app/admin/view/default/diy/saveinfo.html` — 仅一行 `ModuleInclude`,无组件定义
|
||
|
||
**ShopXO DIY 编辑器是页面可视化装修工具**(拖拽组件排版),不是通用 JSON 编辑器。其 `custom` 组件类型支持输出 HTML 代码片段,不可复用。
|
||
|
||
---
|
||
|
||
### Q1.2: Vue3 + JSON Schema Form 编辑器复杂度估算
|
||
|
||
**现有插件的起点**:SeatTemplate 已使用**原始 textarea**(`save.html:40`)直接编辑 seat_map JSON,无任何可视化。
|
||
|
||
**引入"场馆"后的嵌套结构**:
|
||
```
|
||
venue { name, address, image }
|
||
└─ seat_map
|
||
├─ map[] string[]
|
||
├─ seats{} { [key]: { price, color, label, classes? } }
|
||
├─ row_labels[] string[]
|
||
├─ sections[] { name, color }[]
|
||
└─ zones{} { [key]: { ... } }
|
||
```
|
||
实际嵌套深度:**3 层**(venue → seat_map → seats/sections)。不是"4层",venue 和 seat_map 各算一层。
|
||
|
||
**实现方式对比**:
|
||
|
||
| 方案 | 代码量 | 工时 | 难度 | 可维护性 |
|
||
|------|--------|------|------|----------|
|
||
| **原始 textarea**(现状) | 0 | 0 | 无 | 差(无校验) |
|
||
| **表单可视化编辑器** | ~500行 Vue3 | ~1-1.5人天 | 中 | 好 |
|
||
| **JSON Schema + 校验** | ~1000行 Vue3 | ~2人天 | 高 | 好(需维护 schema) |
|
||
|
||
**推荐方案:表单可视化编辑器(中等方案)**
|
||
- venue 信息:独立字段(text + image upload)
|
||
- seat_map.map[]:动态行编辑器(每行一个 input,支持增删)
|
||
- seat_map.seats{}:键值对表格(行标签 → {price/color/label})
|
||
- seat_map.sections[]:卡片式列表(每区一张 card,含 color picker)
|
||
- 无需引入 JSON Schema 库,直接写 Vue3 render function
|
||
|
||
**技术实现路径**:
|
||
1. 创建 `plugins/vr_ticket/admin/view/goods/ticket_editor.html`(layui 表单 + Vue3 CDN)
|
||
2. 通过 `AdminGoodsSave.php` hook 注入到商品发布页 base tab
|
||
3. seat_map 作为单个 JSON 字段存储(不做拆表)
|
||
|
||
---
|
||
|
||
### Q1.3: JSON 编辑器 vs 拆表方案成本对比
|
||
|
||
**JSON 单表方案(推荐)**:
|
||
- 数据存储:1 个 `seat_map` JSON 字段在 `plugins_vr_seat_templates` 表
|
||
- 编辑器:自定义 Vue3 表单,500行代码,1-1.5人天
|
||
- 优点:schema 演进灵活(加字段不影响表结构),无 JOIN 查询
|
||
- 缺点:无数据库级数据完整性约束,查询特定座位需 JSON 函数
|
||
|
||
**拆多表方案**:
|
||
```
|
||
plugins_vr_venues (venue 信息)
|
||
└─ plugins_vr_seat_templates (seat_map FK)
|
||
├─ plugins_vr_seat_sections (座位区:区名/颜色)
|
||
└─ plugins_vr_seat_zone_mappings (座位字符→区的映射)
|
||
```
|
||
- 代码量:需建 3-4 张表 + 4 套 CRUD + 模型关联,~1000行 PHP,~800行前端
|
||
- 工时:~2.5-3人天(远超 JSON 方案)
|
||
- 优点:数据库约束强,适合超大规模(万级座位)
|
||
- 缺点:表结构变更成本高;这个场景下收益有限
|
||
|
||
**结论**:vr_ticket 插件的座位数通常 < 1000,JSON 单表方案**开发和维护成本均显著低于拆表方案**。拆表仅在需要单独查询座位库存、索引、或座位有独立业务属性时才值得。
|
||
|
||
---
|
||
|
||
## 最终推荐
|
||
|
||
### 推荐方案:**钩子注入 + 表单可视化编辑器(中等方案)**
|
||
|
||
**综合 Q1 + Q2 结论**:
|
||
|
||
| 评估维度 | 结论 |
|
||
|----------|------|
|
||
| 页面替换(邪门方案)| **不可行** — 钩子仅注入非替换,需覆盖核心模板失去升级兼容性 |
|
||
| 独立路由方案 | 可行但工作量大(~3人天) |
|
||
| **钩子注入 + JSON 表单编辑器** | **推荐** — 约 1-1.5人天,升级兼容,数据可控 |
|
||
| JSON vs 拆表 | **JSON 单表** — 开发成本低 50%+,维护简单 |
|
||
|
||
**具体实施路径**:
|
||
1. 在 `plugin.json` 注册 `plugins_view_admin_goods_save` 钩子
|
||
2. 实现 `AdminGoodsSave.php` 返回 ticket 专属表单 HTML(layui + Vue3 CDN)
|
||
3. 表单编辑器结构:`venue 字段组` + `seat_map 可视化编辑器`(非 textarea)
|
||
4. `plugins_service_goods_save_handle` 钩子接收并处理 ticket 数据(引用 `$data`)
|
||
5. `item_type='ticket'` 时前端走 `ticket_detail.html`(已有实现)
|
||
|
||
**不推荐**:
|
||
- 完全替换 saveinfo.html(失去升级兼容性)
|
||
- 拆多表(收益<成本)
|
||
- 引入 JSON Schema 库(过度工程化)
|