# :-: 插件addons开发文档
*****
# 目录结构

# 开发规范
## 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文件**】

~~~
<?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展示,具体看代码源码注释

## 5.模型
模型使用方法与TP6中模型使用方法一致
## 6.视图
视图使用方法可前往应用开发文档中查看
## 实例 : 微信退款插件调用\[收费插件\]
### 1.需求
> 需要一个微信退款功能,写成插件钩子函数的形式调用,这种主要是钩子函数的插件没有菜单,控制器,视图和接口,只需要写简单配置一下
### 2.目录结构
> 插件文件夹名为:wechat\_refund,则目录结构如下(因为没有管理菜单和接口,所以controller和view为空):

### 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());
}
~~~
- bwsaas框架介绍
- 框架安装配置指南
- 宝塔安装
- 环境配置要求
- 阿里云OSS配置
- 阿里云API短信配置
- 物流API配置
- 配置运营平台域名CDN加速
- 队列配置
- 安装常见问题
- 全局配置
- 界面UI展示
- 老版本layui主要界面
- 新版本ElementPlusUi租户后台管理
- 新版本ElementPlusUi总后台管理
- 新版本ElementPlusUi名牛云商城
- 目录结构
- 框架应用开发
- 开发配置管理
- 权限控制介绍
- 注意事项说明
- 代码开发规范
- 常见问题
- 一键生成后台管理CRUD
- 微信第三方开放平台申请
- 升级日志
- 版本升级指导
- 插件开发
- 开发流程
- 目录文件
- 插件addons的打包
- 插件分类
- 应用安装卸载购买
- 应用配置功能套餐
- SAAS框架二开
- 控制器
- 参数验证器使用
- 框架常用函数
- 支付相关
