vr-shopxo-plugin/council-output/EDITOR_RESEARCH.md

7.5 KiB
Raw Blame History

vr-shopxo-plugin 编辑器方案调研报告

版本v1.0 | 日期2026-04-15 | AgentBackendArchitect (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

$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 行:

{{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支持主题覆盖插件文件

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.htmlShopXO 默认主题目录),然后修改。但这是覆盖核心文件,升级 ShopXO 时会丢失。

推荐:不要完全替换页面。 改为在 base tab 内注入 ticket 专属表单,或添加新的 tab 项。


Q2-C: Save() 数据接收方式

Goods::Save()Goods.php:187-192

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

// 构建 $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 添加:

    "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.phpadmin控制器中不存在 item_type 判断逻辑——任务描述中关于此判断存在于后台的说法需要更正

Q1JSON 编辑器复杂度评估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人天