# 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 库（过度工程化）
