💎一站式轻松地调用各大LLM模型接口,支持GPT4、智谱、豆包、星火、月之暗面及文生图、文生视频 广告
# :-: 插件addons开发文档 ***** # 目录结构 ![](images/image_1623382229899.png) # 开发规范 ## 1\. 配置文件 ### 1.1 info.ini > 插件的基础信息 ~~~ ;目录名,唯一标识 name = addon_demo ;名称 title = addon_demo ;描述,选填 description = 测试插件 ;类型:admin_system=总后台插件,member_system=租户系统,member_bwwechat=租户bwwechat应用插件 ,租户后台和总后台都有操作节点的插件类型 all_system type = member_bwwechat ;作者 author = buwang ;版本 version = 1.0.0 ;状态:0=禁用,1=启用【禁用后不可访问】 status = 1 url = /addons/addon_demo/admin.Plugin/enable ~~~ ### 1.2 config.php > 插件的总后台的配置文件,根据此文件在admin总后台【系统插件】->【插件配置】渲染成相应的配置管理页面【**如果插件根目录存在config.html就会使用这个文件渲染;没有此文件则渲染系统的app/manage/view/admin/plugin/config.html文件**】 ![](https://img.kancloud.cn/18/26/1826914fc950a9a614a5f5c83c169efe_1871x295.png) ~~~ <?php return array ( 'tips' => array ( 'name' => 'tips', 'title' => '温馨提示:', 'type' => 'string', 'value' => '该提示将出现的插件配置头部,通常用于提示和说明', ), 'key2' => array ( 'name' => 'key2', 'title' => '应用key2', 'type' => 'number', 'content' => array ( ), 'value' => '8', 'verify' => 'required', 'msg' => '', 'tip' => '', 'ok' => '', 'extend' => '', ), 'key3' => array ( 'name' => 'key3', 'title' => 'key3', 'type' => 'datetime', 'content' => array ( ), 'value' => '2020-11-05 00:00:00', 'verify' => 'required', 'msg' => '', 'tip' => '', 'ok' => '', 'extend' => '', ), 'key4' => array ( 'name' => 'key4', 'title' => 'key4', 'type' => 'select', 'content' => array ( 1 => '显示', 0 => '不显示', ), 'value' => '1', 'verify' => 'required', 'msg' => '', 'tip' => '', 'ok' => '', 'extend' => '', ), 'key5' => array ( 'name' => 'key5', 'title' => 'key5', 'type' => 'checkbox', 'content' => array ( 1 => '显示', 0 => '不显示', ), 'value' => '1,0', 'verify' => 'required', 'msg' => '', 'tip' => '', 'ok' => '', 'extend' => '', ), 'key' => array ( 'name' => 'key', 'title' => '应用key', 'type' => 'string', 'content' => array ( ), 'value' => 'xxxxxxxxxxxxxxxx', 'verify' => 'required', 'msg' => '', 'tip' => '', 'ok' => '', 'extend' => '', ), 'secret' => array ( 'name' => 'secret', 'title' => '密钥secret', 'type' => 'string', 'content' => array ( ), 'value' => 'xxxxxxxxxxxxxxxx', 'verify' => 'required', 'msg' => '', 'tip' => '', 'ok' => '', 'extend' => '', ), 'sign' => array ( 'name' => 'sign', 'title' => '签名', 'type' => 'string', 'content' => array ( ), 'value' => 'xxxxxxxxxxxxxxxx', 'verify' => 'required', 'msg' => '', 'tip' => '', 'ok' => '', 'extend' => '', ), 'template' => array ( 'name' => 'template', 'title' => '短信模板', 'type' => 'string', 'content' => array ( ), 'value' => 'xxxxxxxxxxxxxxxx', 'verify' => 'required', 'msg' => '', 'tip' => '', 'ok' => '', 'extend' => '', ), ); ~~~ #### 配置支持类型 > 插件配置会根据配置config.php中的`type`值自动渲染相应的组件 | 类型 | 组件 | | text | 多行文本框 | | array | fieldlist列表框 | | string | 普通文本框 | | date | 日期框 | | datetime | 日期时间框 | | time | 时间框 | | number | 数字文本框 | | checkbox | 多选框 | | radio | 单选框 | | select | 普通下拉列表框(多选) | | image | 单图 | | images | 多图 | | file | 单文件上传 | | files | 多文件上传 | | bool | 开关 | ### 1.3 Plugin.php > 钩子实现类,钩子方法都写在这里 ~~~ <?php namespace addons\addon_demo; use buwang\base\BaseAddons; use think\Exception; use think\exception\HttpException; /** * 阿里云短信插件 * @author byron sampson */ class Plugin extends BaseAddons //BaseAddons继承think\Addons类【vendor包名为hnlg666/bwsaas-addons】 { // 该插件的基础信息[会和info.ini里面的信息执行合并array_merge()] public $info = [ ]; /** * 插件安装方法 * @return bool */ public function install() { return true; } /** * 插件卸载方法 * @return bool */ public function uninstall() { return true; } public function enabled() { // TODO: Implement enabled() method. } public function disabled() { // TODO: Implement disabled() method. } } ~~~ ### 1.4 config.html > 自定义渲染config.php中配置信息,不需要自定义时不需要此文件 默认渲染文件在 `app\manage\view\admin\plugin\config.html` ,可根据需要进行重写 ### 1.5 service.ini > 自定义插件需要的service服务类,系统会在访问时自动加载并注册 ## 2. 安装目录 /install ### 2.1 install.sql > 插件的数据库安装SQL文件 ### 2.2 menu.php > 角色组及节点文件【**此文件不用手动写,插件开发完成用命令行打包插件的时候自动生成**】 * 可设置多个角色组,且角色组的作用域scopes不必相同,租户购买时只插入 `scopes=member` 的角色组 * 插件安装时会为角色组的节点设置一个根节点,名称是插件的名称 ~~~ <?php $dir = ADDONS_DIR; return [ [ //必填,角色名称/功能名称 'name' => $dir . '门店功能', //必填,角色唯一标识,不可重复,在安装时会在前面追加插件的目录名 'group_name' => 'shop', //角色备注/功能描述 'remark' => '', //节点的上级节点的group_name,为空时节点显示在插件管理下 'parent' => '', //必填,角色组作用域admin/member,平台后台或者租户后台 'scopes' => 'member', //角色拥有的节点 'nodes' => [ [ "title" => '首页', //菜单url "menu_path" => '/addons/' . $dir . '/index/index', //实际 "name" => '/addons/' . $dir . '/Index/index', //权限标识,必填 唯一 "auth_name" => '', //附加参数 ?id=1&name=demo "param" => '', //打开方式 "target" => '_self', //是否菜单 1=是,0=否 "ismenu" => '1', //图标 "icon" => 'fa fa-file', //备注 "remark" => '', //子节点 'children' => [] ], [ "title" => '登录页', //菜单url "menu_path" => '/addons/' . $dir . '/index/login', //实际 "name" => '/addons/' . $dir . '/Index/login', //权限标识,必填 唯一 "auth_name" => '', //附加参数 ?id=1&name=demo "param" => '', //打开方式 "target" => '_self', //是否菜单 1=是,0=否 "ismenu" => '1', //图标 "icon" => 'fa fa-file', //备注 "remark" => '', //子节点 'children' => [] ], ] ] ]; ~~~ ### 2.3 static目录 > 静态资源目录,所有静态资源都放在此目录 安装时该目录下所有文件都会被复制到`public/static/addons/addon_demo`目录下 ## 3. 卸载目录 /uninstall ### uninstall.sql > 插件的数据库卸载SQL文件 ## 4.控制器 ### 4.1 PluginBaseController.php > 插件基础控制器 ~~~ <?php // +---------------------------------------------------------------------- // | Bwsaas // +---------------------------------------------------------------------- // | Copyright (c) 2015~2020 http://www.buwangyun.com All rights reserved. // +---------------------------------------------------------------------- // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 ) // +---------------------------------------------------------------------- // | Gitee ( https://gitee.com/buwangyun/bwsaas ) // +---------------------------------------------------------------------- // | Author: buwangyun <hnlg666@163.com> // +---------------------------------------------------------------------- // | Date: 2021-04-27 12:55:00 // +---------------------------------------------------------------------- namespace buwang\base; use app\manage\model\Token; use buwang\exception\AuthException; use buwang\service\AuthService; use think\facade\Config; use think\facade\View; /** * 控制器基础类 */ abstract class PluginBaseController extends Manage { //无需登录的方法,同时也就不需要鉴权了 protected $noNeedLogin = ['*']; //无需鉴权的方法,但需要登录 protected $noNeedRight = ['*']; //权限控制类 protected $auth_service = null; // 插件名 protected $addon = ''; //插件路径 protected $controller = null; //插件执行方法 protected $action = null; // 插件路径 protected $addon_path; // 视图模型实例 protected $view; // 插件配置 private $addon_config; // 插件信息 private $addon_info; //布局模板 可用相对地址 如 ../../../view/public/layout protected $layout = 'layout'; public function __construct() { if(!$this->app) $this->app = app(); if(!$this->request) $this->request = $this->app->request; //移除HTML标签 $this->request->filter('trim'); // 是否自动转换控制器和操作名 $convert = Config::get('url_convert'); $filter = $convert ? 'strtolower' : 'trim'; // 处理路由参数 //$var = $this->>app->request->rule()->getVars(); $var = $this->request->param(['addon', 'controller', 'action']); $addon = isset($var['addon']) ? $var['addon'] : ''; $controller = isset($var['controller']) ? $var['controller'] : ''; $action = isset($var['action']) ? $var['action'] : ''; //当前插件名,方法,控制器 $this->addon = $addon ? call_user_func($filter, $addon) : ''; $this->controller = $controller ? call_user_func($filter, $controller) : 'index'; $this->action = $action ? call_user_func($filter, $action) : 'index'; $this->addon_path = $this->app->addons->getAddonsPath() . $this->addon . DIRECTORY_SEPARATOR; $this->addon_config = "addon_{$this->addon}_config"; $this->addon_info = "addon_{$this->addon}_info"; //view模板,layout模板布局设置 $this->view = clone VIEW::engine('Think'); $view_data = Config::get('view'); $view_data['tpl_replace_string']['__ADDON__'] = '/static/addons/' . $this->addon . '/'; $this->view->config($view_data); // 如果有使用模板布局 //$this->layout && $this->>app->view->engine()->layout($this->layout); //自动加载插件目录下的composer包 if (empty(self::$vendorLoaded[$this->addon])) { $pluginVendorAutoLoadFile = $this->addon_path .'vendor'.DS.'autoload.php'; if (file_exists($pluginVendorAutoLoadFile)) { require_once $pluginVendorAutoLoadFile; } self::$vendorLoaded[$this->addon] = true; } $thisModule = "addons/{$this->addon}"; $thisController = $this->controller; $thisAction = $this->action; list($thisControllerArr, $jsPath) = [explode('.', $thisController), null]; foreach ($thisControllerArr as $vo) { empty($jsPath) ? $jsPath = parse_name($vo) : $jsPath .= '/' . parse_name($vo); } //自动加载的JS文件 $autoloadJs = file_exists(root_path('public') . "static/{$thisModule}/js/{$jsPath}.js") ? true : false; $thisControllerJsPath = "{$thisModule}/js/{$jsPath}.js"; $data = [ 'adminModuleName' => $thisModule,//插件请求基地址 'thisController' => parse_name($thisController), 'thisAction' => $thisAction, 'thisRequest' => parse_name("{$thisModule}/{$thisController}/{$thisAction}"), 'thisControllerJsPath' => "{$thisControllerJsPath}", 'autoloadJs' => $autoloadJs, 'isSuperAdmin' => 0, 'domain' => $this->request->domain(), 'version' => env('app_debug') ? time() : BW_VERSION, ]; //渲染到插件的模板里面的数据 $this->assign($data); $this->pluginAuth(); parent::initialize(); ///父类的调用必须放在设置模板路径之后 parent::__construct(); } //插件必备初始化函数 插件权限节点鉴权 private function pluginAuth() { //转化获取当前的路由节点地址 $node = "/addons/{$this->addon}/" . parse_name("{$this->controller}", 1) . "/{$this->action}"; $this->auth_service = AuthService::instance(); //传入当前请求的方法 $this->auth_service->setAction($this->action); //检测是否登录 $this->scopes = ''; $this->token = get_token($this->request); $code = 401; $err_msg = "未登录,请先去登录"; $err_code = 400000; $this->user = false; if ($this->token) { //不需要登录捕获422错误 //获取到token,解析token,捕获token签名不正确,签名尚未生效,签名在某个时间点之后才能用,token已经过期,token解析错误 try{ $jwtinfo = self::decodeToken($this->token); }catch (\Throwable $e){ $this->token = '';//token解析报错算token失效,当做未登录处理 $jwtinfo = null; } if($jwtinfo){ //和redis存储的token进行验证有效性 $this->user = Token::validateRedisToken($this->token, $jwtinfo); $code = Token::getErrorCode() == 400001 ? 401 : 200; $err_code = Token::getErrorCode() ?: 400000; $err_msg = Token::getError("未登录,请先去登录"); } } //赋值用户的登录状态 if($this->user) $this->isUserLogin =true; // 检测是否需要验证登录 if (!$this->auth_service->match($this->noNeedLogin)) { if (!$this->token || !$this->user) throw new AuthException($err_msg,401); // 判断是否需要验证权限 if (!$this->auth_service->match($this->noNeedRight)) { // 判断控制器和方法判断是否有对应权限 //目前只鉴权scopes为admin和member的后台相关节点 AuthService::auth($this->user['id'], $this->scopes, $node); } } } /** * 加载模板输出 * @param string $template * @param array $vars 模板文件名 * @return false|mixed|string 模板输出变量 */ protected function fetch($template = '', $vars = []) { return $this->view->fetch($template, $vars); } /** * 渲染内容输出 * @access protected * @param string $content 模板内容 * @param array $vars 模板输出变量 * @return mixed */ protected function display($content = '', $vars = []) { return $this->view->display($content, $vars); } /** * 模板变量赋值 * @access protected * @param mixed $name 要显示的模板变量 * @param mixed $value 变量的值 * @return $this */ protected function assign($name, $value = '') { if (is_array($name)) { $this->view->assign($name); } else { $this->view->assign([$name => $value]); } return $this; } /** * 初始化模板引擎 * @access protected * @param array|string $engine 引擎参数 * @return $this */ protected function engine($engine) { $this->view->engine($engine); return $this; } /** * 插件基础信息 * @return array */ final public function getInfo() { $info = Config::get($this->addon_info, []); if ($info) { return $info; } // 文件属性 $info = $this->info ?? []; // 文件配置 $info_file = $this->addon_path . 'info.ini'; if (is_file($info_file)) { $_info = parse_ini_file($info_file, true, INI_SCANNER_TYPED) ?: []; $_info['url'] = addons_url(); $info = array_merge($_info, $info); } Config::set($info, $this->addon_info); return isset($info) ? $info : []; } /** * 获取配置信息 * @param bool $type 是否获取完整配置 * @return array|mixed */ final public function getConfig($type = false) { $config = Config::get($this->addon_config, []); if ($config) { return $config; } $config_file = $this->addon_path . 'config.php'; if (is_file($config_file)) { $temp_arr = (array)include $config_file; if ($type) { return $temp_arr; } foreach ($temp_arr as $key => $value) { $config[$key] = $value['value']; } unset($temp_arr); } Config::set($config, $this->addon_config); return $config; } /** * 获取对象的属性字段及属性值 * @param [type] $property_scope 属性域 * @param boolean $static_excluded 是否排除静态属性 * @return array * @throws \ReflectionException|\Exception */ protected function getProperties($property_scope = null, $static_excluded = false) { // 校验反射域是否合法 if (isset($property_scope) && !in_array($property_scope, [ \ReflectionProperty::IS_STATIC, \ReflectionProperty::IS_PUBLIC, \ReflectionProperty::IS_PROTECTED, \ReflectionProperty::IS_PRIVATE, ])) { throw new \Exception("reflection class property scope illegal!"); } $properties_mapping = []; // 谈判官 $classRef = new \ReflectionClass($this); $properties = isset($property_scope) ? $classRef->getProperties($property_scope) : $classRef->getProperties(); foreach ($properties as $property) { // 为了兼容反射私有属性 $property->setAccessible(true); // 当不想获取静态属性时 if ($property->isStatic() && $static_excluded) { continue; } if(in_array($property->getName(),['noNeedLogin', 'noNeedRight'])){ // 将得到的类属性同具体的实例绑定解析,获得实例上的属性值 $properties_mapping[$property->getName()] = $property->getValue($this); } } return $properties_mapping; } } ~~~ * 登录用户信息 如果在插件的控制器中用到登录用户信息的地方,请直接使用`$this->user ` 判断是否登录 :` $this->isUserLogin` 用户token值:`$this->token` * 基类文件structure展示,具体看代码源码注释 ![](https://img.kancloud.cn/58/06/5806c277fae30d959b5e6349402cd2d7_507x905.png) ## 5.模型 模型使用方法与TP6中模型使用方法一致 ## 6.视图 视图使用方法可前往应用开发文档中查看 ## 实例 : 微信退款插件调用\[收费插件\] ### 1.需求 > 需要一个微信退款功能,写成插件钩子函数的形式调用,这种主要是钩子函数的插件没有菜单,控制器,视图和接口,只需要写简单配置一下 ### 2.目录结构 > 插件文件夹名为:wechat\_refund,则目录结构如下(因为没有管理菜单和接口,所以controller和view为空): ![](images/企业微信截图_16099160069552.png) ### 3.安装与卸载 > 没有数据表,所以也没有安装和卸载,install和uninstall文件夹留空 ### 4.config.php和config.html > config.html照着addon\_demo插件copy一份,除非有特殊需求需要改动配置展示的UI才需要更改,一般插件开发基本不需要改动,所以,这个插件只需要改动config.php即需要展示哪些配置。因为微信支付配置信息依赖于框架,所以该插件不需要任何配置 #### #### 4.1 config.php ~~~ <?php return array ( 'tips' => array ( 'name' => 'tips', 'title' => '温馨提示:', 'type' => 'string', 'value' => '暂无需要配置的选项', ), ); ~~~ ### 5.info.ini ~~~ ;目录名,唯一标识 name = wechat_refund ;名称 title = 微信插件 ;描述,选填 description = 微信插件 ;类型:admin_system=总后台插件,member_system=租户系统,member_bwwechat=租户bwwechat应用插件 ,租户后台和总后台都有操作节点的插件类型 all_system type = admin_system ;作者 author = buwang ;版本 version = 1.0.0 ;状态:0=禁用,1=启用 status = 1 url = /addons/addon_demo/admin.Plugin/enable.html ~~~ ### 6.Plugin.php > 除了安装回调方法**install()**,卸载回调方法**uninstall()**,启用回调方法**enabled()**,禁用回调方法**disabled()**为框架插件回调函数外,该类其他public对象方法均均可被**addon\_hook()**函数调用 > > 该事例中**wechat\_refund()**方法为退款hook函数 ~~~ <?php namespace addons\wechat_refund; use AlibabaCloud\Client\AlibabaCloud; use buwang\base\BaseAddons; use think\Exception; use think\exception\HttpException; use buwang\facade\WechatPay; /** * 微信退款插件 * @author byron sampson */ class Plugin extends BaseAddons // 需继承think\Addons类 { // 该插件的基础信息 public $info = [ ]; /** * 插件安装方法 * @return bool */ public function install() { return true; } /** * 插件卸载方法 * @return bool */ public function uninstall() { return true; } public function enabled() { // TODO: Implement enabled() method. } public function disabled() { // TODO: Implement disabled() method. } /**微信退款 * @param $param * @return array */ public function wechat_refund($param) { list($data) = $param; //判断禁用 $info = get_addons_info('wechat_refund'); if (!$info['status']) throw new \buwang\exception\AddonException('插件已被禁用'); //组装doPay调用参数 //组装退款调用参数 ... } /** * 实现的testhook钩子方法 * @return mixed */ // public function testhook($param) // { // // 调用钩子时候的参数信息 // print_r($param); // // 当前插件的配置信息,配置信息存在当前目录的config.php文件中,见下方 // print_r($this->getConfig()); // // 可以返回模板,模板文件默认读取的为插件目录中的文件。模板名不能为空! // return $this->fetch('info'); // } } ~~~ ### 7.menu.php > 不需要菜单则不需要写menu.php菜单文件**【开发每个节点的时候,自行在总平台后台的菜单节点管理里面添加,插件开发完成可用命令行生成插件包】** ### 8.调用 在需要调用退款功能的脚本代码段中执行addon\_hook()方法调用相关插件函数,调用addon\_exist()方法可检测函数是否存在,以下是代码段中完整调用示例 ~~~ //检测是否安装退款插件 if (!addon_exist('wechat_refund')) return $this->error(addon_error()); //调用插件退款 $payparm = [ 'miniapp_id' => $user['member_miniapp_id'],//租户应用id 'order_sn' => $product['order_sn'], //订单号 'price' => $product['pay_price'] * 100,//订单金额(分) 'refund_price' => $refund_price * 100, //退款金额(分) ]; try { $res1 = addon_hook('wechat_refund', [$payparm]); } catch (\buwang\exception\AddonException $e) { return $this->error('调用退款插件失败,原因:' . $e->getMessage()); } ~~~