council(draft): BackendArchitect - Q2 editor research findings
Plugins_view_admin_goods_save is an injection point, not a replacement point. Save() accepts standard POST. Hook can intercept save flow. Final recommendation: hook injection + JSON editor. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>refactor/vr-ticket-20260416
parent
283076f5f2
commit
f55ba36e7b
|
|
@ -0,0 +1,197 @@
|
||||||
|
# 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)
|
||||||
|
|
||||||
|
> [待 FrontendDev 填写]
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 最终推荐
|
||||||
|
|
||||||
|
### 推荐方案:**插件钩子注入 + JSON Schema 编辑器**
|
||||||
|
|
||||||
|
**理由**:
|
||||||
|
|
||||||
|
1. **完全替换方案不可行**:`plugins_view_admin_goods_save` 是注入点而非替换点,位于 base tab 的 `<div class="am-form-group">` 内。要完全替换需覆盖核心 `saveinfo.html`,代价是失去 ShopXO 升级兼容性。
|
||||||
|
|
||||||
|
2. **注入 + 隐藏策略可行但脆弱**:通过钩子注入 HTML + JS 隐藏标准字段能实现视觉替换,但依赖 DOM 操作,维护成本高。
|
||||||
|
|
||||||
|
3. **Save() 数据流完全可控**:`plugins_service_goods_save_handle` 钩子在事务前以引用方式接收 `$data`,插件有两条清晰路径(填最小字段走标准流,或自行处理返回)完成数据保存。
|
||||||
|
|
||||||
|
4. **与 JSON 编辑器方案互补**:插件钩子注入 ticket 专属表单 + JSON Schema 编辑器处理 `seat_map` 嵌套数据,可以完整覆盖票务商品编辑场景。
|
||||||
|
|
||||||
|
### 备选:走插件独立路由
|
||||||
|
|
||||||
|
即 `admin/goods/saveinfo` → 重定向到插件自己的控制器方法(如 `plugins/vr_ticket/admin/goods/Save`),完全独立实现票务编辑器。不经过 ShopXO 的 `Goods::SaveInfo()` 和 `Goods::Save()`,干净隔离但需要开发独立的菜单和控制器。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 总结
|
||||||
|
|
||||||
|
| 维度 | 完全替换 | 钩子注入(推荐) | 独立路由 |
|
||||||
|
|------|----------|----------------|----------|
|
||||||
|
| 实现复杂度 | 高(需覆盖核心模板) | 中 | 高(重写全套) |
|
||||||
|
| 升级兼容性 | 差 | 好 | 好 |
|
||||||
|
| 数据保存可控性 | 好 | 好 | 好 |
|
||||||
|
| 开发工作量 | ~2人天 | ~1人天 | ~3人天 |
|
||||||
Loading…
Reference in New Issue