2.2 Discuz! X5.0:新增插件接口 第2部分
本篇涵盖:JSON编辑器区块 ~ 嵌入点注册(X5.0.4起生效)
JSON编辑器区块
X5.0 引入了全新的JSON编辑器,基于 Editor.js 内核,是区块风格(Block-Styled)编辑器,具有干净JSON输出的块样式,同时,让编辑器有更多的自由性和可扩展性。块是构成条目的结构单元,例如,Paragraph,Heading,Image,Video,List都是块。每个块由插件表示,以热插拔的形式灵活为编辑器扩展功能。
可以通过插件方式为JSON编辑器扩展功能区块,您需要把区块文件写到 /source/plugin/myplugin/editorblock 目录下。
区块类固定以 editorblock_自定义区块英文标识.php 命名, 如 editorblock_myBlock.php ,放在 editorblock 根目录,区块 js、css 静态资源放入 editorblock/tools/myBlock 目录下。
目录结构
editorblock // 固定命名
├── tools // 固定命名
├── myBlock // 自定义区块英文标识
├── myBlock.js // 自定义区块核心js
├── myBlock.css // 自定义区块核心css
├── editorblock_myBlock.php // 自定义区块核心类
区块核心类书写范例
以下是区块核心类书写范例,其中 getConfig() 、 getStyle() 、 getParser() 将支持后台在线编辑,即,用户可以自由定义区块展示结构与样式。
class editorblock_myBlock {
var $version = '1.0.5'; // 版本
var $name = '自定义区块名称'; // 区块名称
var $available = 1; // 默认启用状态 0:不启用 1:启用
var $columns = 1; // 默认是否支持多列 0:不支持 1:支持
var $identifier = 'myBlock'; // 自定义区块标识
var $description = '插入联系方式信息区块,可用于插入电话、微信号、QQ号等,添加内容格式为:类型标识(mobile、wechat、qq)/联系人名称/联系方式(电话、微信等)'; // 区块描述
var $filename = 'myBlock'; // 区块文件名,与自定义区块标识一致即可
var $copyright = '云诺'; // 版权,显示在后台JSON区块列表中
var $type = '0'; // 0:数据类型 1:图片类型 2:附件类型
function __construct() {
}
// 区块 数据结构 示例,参照 Editor.js 区块数据结构
function getParameter() {
return <<<EOF
{
"data": {
"channelType": "mobile", // mobile、wechat、qq
"channelName": "云诺",
"messageId": 123456
},
"id": "ZT8S70Q34G", // 区块id
"type": "myBlock" // 区块类型
}
EOF;
}
/*
* 区块 config 配置,参照 Editor.js 区块配置
*
* 结构(左顶头):
* {
* tools_自定义区块英文标识: {
* 自定义区块英文标识: {
* ...
* }
* }
* }
*/
function getConfig() {
return <<<EOF
{
tools_myBlock: {
myBlock: {
class: myBlock,
tunes: ['anchorTune']
},
}
}
EOF;
}
// 尚未启用
function getI18n() {
return <<<EOF
EOF;
}
// 区块在编辑器中的 显示样式 配置
// CSS样式代码部分必须严格书写在<style type="text/css">......</style>内部。
function getStyle() {
return <<<EOF
<style type="text/css">
.ce-block {
/* margin-bottom: 20px; */
}
.ce-block__content,.ce-toolbar__content {
/* max-width:calc(100% - 50px) */
margin-left: auto;
margin-right: auto;
}
.ce-myBlock {
position: relative;
float: left;
width: 280px;
height: auto;
padding: 15px 10px 15px 20px;
box-sizing: border-box;
border: 1px solid #3f9dffa3;
border-radius: 50px;
margin: 10px 15px;
}
.ce-myBlock dl {
margin: 0
}
.ce-myBlock dl dt {
width: 50px;
height: 50px;
float: left;
position: relative;
margin-right: 12px;
}
.ce-myBlock dl dt svg {
width: 100%;
height: 100%
}
.ce-myBlock dl dd {
float: left;
position: relative;
}
.ce-myBlock dl dd h3 {
height: 20px;
line-height: 20px;
font-size: 14px;
margin-top: 5px;
}
.ce-myBlock dl dd h3 em {
font-style: normal;
margin-left: 5px;
color: #fa5555;
font-size: 12px;
}
.ce-myBlock dl dd p.p1 {
color: #999;
}
.ce-myBlock dl dd p {
width: 185px;
height: 20px;
line-height: 20px;
margin-top: 5px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
</style>
EOF;
}
// 区块 解析模板 配置,解析模板开发方式见下文
function getParser($block = array()) {
global $_G;
return <<<EOF
<div class="ce-block ce-block--focused" data-id="{id}" [if tunes.anchorTune.anchor=notnull]id="{tunes.anchorTune.anchor}"[/if]>
<div class="ce-block__content">
<div class="ce-myBlock ">
<dl>
<dt>
[if data.channelType=mobile]
<svg t="1705559455679" class="icon" viewBox="0 0 1025 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="69723" width="80" height="80"><path d="M511.484254 0.066515C227.987504 0.066515-0.001023 227.399103-0.001023 510.114047c0 282.71392 227.987504 512.917906 511.485277 512.917906 283.461957 0 511.48016-230.203986 511.48016-512.917906C1022.964414 227.399103 794.94621 0.066515 511.484254 0.066515zM762.853281 772.349563l-57.003272 43.768853c-54.072523 16.044418-187.064466 42.271756-352.206644-190.872189-187.063443-252.120135-112.512517-394.946051-84.758406-419.698804l59.904345-42.270732 24.854061 7.306406 100.833523 145.69629-2.901073 23.320125-58.471716 40.803311c-26.28669 21.851681-8.772804 49.541323 10.237155 101.988836 17.543561 30.625508 57.003272 91.816149 83.290985 115.137297 45.297672 37.896098 65.777099 62.684668 96.468099 45.201481l55.505152-39.333844 21.977547 2.871397 105.142665 141.357472L762.853281 772.349563z" fill="#18CC73" p-id="69724"></path></svg>
[/if]
[if data.channelType=wechat]
<svg t="1705558999833" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="47545" width="80" height="80"><path d="M512 1.896C229.452 1.896 0 229.452 0 512s229.452 510.104 512 510.104S1022.104 794.548 1022.104 512 794.548 1.896 512 1.896z m-91.022 629.57c-26.548 0-49.304-5.688-75.852-11.377l-75.852 37.926 22.756-66.37c-54.993-37.926-87.23-87.23-87.23-147.912 0-104.296 98.607-185.837 218.074-185.837 108.089 0 201.007 64.474 219.97 153.6-7.585 0-13.274-1.896-20.859-1.896-104.296 0-185.837 77.748-185.837 172.563 0 15.17 1.896 30.34 7.585 45.511-7.585 3.793-15.17 3.793-22.755 3.793z m322.37 77.749l17.067 54.992-58.785-34.133c-22.756 5.689-43.615 11.378-66.37 11.378-104.297 0-185.838-70.163-185.838-157.393S530.963 424.77 635.26 424.77c98.608 0 185.837 70.163 185.837 159.29 0 47.407-32.237 91.021-77.748 125.155z" fill="#46BB36" p-id="47546"></path><path d="M318.578 379.26c0 17.066 13.274 30.34 30.34 30.34s30.341-13.274 30.341-30.34-13.274-30.341-30.34-30.341-30.341 13.274-30.341 30.34z m235.14 159.288c0 13.274 11.378 24.652 24.652 24.652 13.274 0 24.652-11.378 24.652-24.652 0-13.274-11.378-24.652-24.652-24.652-13.274-1.896-24.651 9.482-24.651 24.652z m-81.54-159.289c0 17.067 13.274 30.341 30.34 30.341 17.067 0 30.341-13.274 30.341-30.34 0-17.067-13.274-30.341-30.34-30.341-17.067 0-30.341 13.274-30.341 30.34zM675.08 538.55c0 13.273 11.378 24.651 24.652 24.651 13.274 0 24.652-11.378 24.652-24.652 0-13.274-11.378-24.652-24.652-24.652-13.274-1.896-24.652 9.482-24.652 24.652z" fill="#46BB36" p-id="47547"></path></svg>
[/if]
[if data.channelType=qq]
<svg t="1705559232025" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="57063" width="80" height="80"><path d="M511.500488 512.499512m-511.500488 0a511.500488 511.500488 0 1 0 1023.000976 0 511.500488 511.500488 0 1 0-1023.000976 0Z" fill="#1BC1FA" p-id="57064"></path><path d="M784.234146 572.440976c8.178014 0 16.273108 0.253752 24.282287 0.728288-16.961436-38.434466-42.247742-69.886751-58.977405-90.331785 2.822244-8.482716 11.30496-56.536788-25.443153-90.453666v-2.827239c0-132.850263-96.103149-231.785647-214.822213-231.785647-118.717065 0-214.82521 96.107145-214.825209 231.785647v2.827239c-33.915879 33.915879-25.437159 81.969951-19.782681 90.453666-25.444152 28.265397-73.49223 87.62343-76.320469 155.461182 0 16.961436 2.827239 45.226833 11.305959 56.531794 11.305959 14.133198 39.570357-2.827239 62.186272-48.054073 5.650482 19.788675 19.78368 53.709549 50.876316 93.281905-50.876316 11.302962-65.009514 62.186271-48.049077 90.450669 11.305959 19.788675 39.570357 33.920874 87.624429 33.920874 78.496343 0 115.815899-19.378076 134.378771-35.711126C492.644901 814.680414 490.520976 800.136617 490.520976 785.233171c0-117.522232 131.500581-212.792195 293.71317-212.792195z" fill="#FFFFFF" p-id="57065"></path><path d="M514.925143 819.204995c5.654478 0 11.309955 2.82624 14.132199 5.649483 16.960437 16.960437 53.709549 39.575352 138.50674 39.575352 48.054072 0 76.320468-16.960437 87.625428-33.920874 16.960437-28.264398 2.827239-79.147707-48.054072-90.450669 31.092636-39.572355 45.225834-73.493229 50.881311-93.281905 19.787676 45.226833 50.881311 62.18727 62.186271 48.054073 2.827239-11.30496 5.650482-39.570357 5.650482-56.531794-1.93511-23.223321-8.508691-45.121936-17.337069-65.128398-8.009179-0.475536-16.104273-0.729288-24.282287-0.729287-162.212589 0-293.713171 95.269963-293.71317 212.792195 0 14.903446 2.123926 29.447243 6.147996 43.485533a88.18688 88.18688 0 0 0 4.122973-3.864226c2.827239-2.823243 8.481717-5.649483 14.133198-5.649483z" fill="#FFFFFF" opacity=".4" p-id="57066"></path></svg>
[/if]
</dt>
<dd>
<h3 class="comm" imagentlist="">{data.channelName}<em></em></h3>
<p class="p1"><em>{data.messageId}</em></p>
</dd>
</dl>
</div>
</div>
</div>
EOF;
}
}
区块核心JS开发方式
以 Editor.js 编辑器区块开发方式开放即可。
区块附件数据存储要求
涉及附件上传的区块需按以下特定格式,将附件数据在data节点下的files节点里面单独存储一份,files节点仅单独列出涉及的所有附件信息,aid是Discuz附件表的aid字段,必填;其他业务数据根据需要正常存储即可。
推荐存储方式:
{
"data": {
...
"files":[
{
"aid":633,
"remote":0,
"directory":"forum",
"url":"202610/08/153417tndfb7s8ddinntws.jpg",
"thumb":"",
"key":"image_1791444857803_1"
},
{
"aid":634,
"remote":0,
"directory":"forum",
"url":"202610/08/153420e7wtpxc2z4j4o295.jpg",
"thumb":"",
"key":"image_1791444860603_2"
},
{
"aid":635,
"remote":0,
"directory":"forum",
"url":"202610/08/153423mb3nj115aafa9mnm.jpg",
"thumb":"",
"key":"image_1791444863661_3"
},
...
],
...
}
}
单独一个图片/附件的区块也可以存储为:
{
"data": {
...
"file":{
"aid":636,
"remote":0,
"directory":"forum",
"url":"202610/08/155642jnlnhnhslbex2ehx.jpg",
"thumb":"",
"key":"image_1791446202454_1"
},
...
}
}
区块解析模板开发
变量使用方式:{字段名} , 多层级使用 . 进行连接, 如:{字段名1.字段名2.字段名3} ;
变量使用方式示例如:{id} 、 {type} 、 {data.alignment} 、 {data.items.text} ;
支持 [loop] 循环,示例区块代码如:
{
"data": {
...
"items": [
{
"text" : "abc"
},
{
"text" : "def"
},
...
]
}
}
循环使用方式:
<ul>
[loop data.items]
<li>{text}</li>
[/loop]
</ul>
循环索引(放到循环体内部,自动计数):
[loopindex]
多列渲染(放到循环体内部,指定子数据块,仅用于多列渲染):
[column blocks]
IF判断方式:
[if data.items=1]
{text}块数据变量 或者 普通字符串
[/if]
URL转换绝对网址:
[url data.file.url]
### 区块解析模板样式CSS开发
如区块前端解析需要css样式支持,可编写独立的 myBlock.css 文件,并使用 viewthread_postbottom 嵌入点往页面中植入该css文件。
class plugin_myplugin_forum extends plugin_myplugin {
function viewthread_postbottom() {
$postbottom[0] = '<link rel="stylesheet" type="text/css" href="source/plugin/myplugin/editorblock/tools/myBlock/myBlock.css?'.getglobal('style/verhash').'" />';
return $postbottom;
}
}
支付
通过拓展支付,可以让应用无需做额外过多的开发即可之间接入 Discuz! 系统的支付体系。甚至自定义一个新的支付通道。
自定义支付入口
payment::enable:是否启用支付服务
payment::channels:获取所有支付渠道(微信支付,支付宝,...),包含未启用的支付渠道,适用于定制支付页面
payment::channels_switch:开启和关闭
payment::get:根据支付渠道名称,创建一个支付实例对象,适用于接入新的支付方式
payment::create_order:创建一个支付订单
payment::query_order:支付订单状态查询
payment::finish_order:完成一个支付订单,如:补单,重试
payment::retry_callback_order:业务回调重试
payment::refund:退款
payment::refund_status:退款状态查询
payment::transfer:转账
payment::transfer_status:转账状态查询
自定义支付通道
payment::channels_add:添加支付通道
payment::channels_delete:删除支付通道
在 pay/pay_xxx.php 定义支付发起和回调方法
namespace myplugin;
// sample 仅实现了 pay 方法,更多参见 /class/pay
class pay_test extends \pay_base {
public function callback($data, $order) {
...
}
public function pay($order) {
...
}
}
在 admin/payment/payment_xxx.php 中可以添加后台设置脚本
namespace myplugin\admin;
class payment_test {
var $name = 'xxx支付';
public function admincp() {
if(!submitcheck('submit')) {
showformheader('ec&operation=method&id=myplugin:test');
......
showsubmit('submit');
showtablefooter();
showformfooter();
} else {
......
}
}
}
安全验证组件
从 X5.0 开始,验证问答升级为安全验证,兼容全新方法,可拓展出更加复杂的验证系统
要增加新的安全验证组件,您需要将脚本写到 /source/plugin/myplugin/secqaa/ 目录下,脚本命名为 secqaa_xxx.php
class secqaa_name {
var $version = '1.0';//脚本版本号
var $name = 'name';//安全验证名称 (可填写语言包项目)
var $description = 'desc';//安全验证说明 (可填写语言包项目)
var $copyright = 'Discuz! Team';//版权 (可填写语言包项目)
var $settingurl = 'action=plugins&operation=config&identifier=sample&pmod=op1';//安全验证设置页URL
function create(&$question) {//返回安全验证的答案和问题 ($question 为问题,函数返回值为答案)
$question = '<div>&#x8F93;&#x5165;&#x20;&#x61;&#x6E;&#x73;&#x77;&#x65;&#x72;</div>';
return 'answer';//可返回答案也可以返回校验ID
}
function check($value, $answer) {//通过返回的 $value 和 create 返回的 $answer 校验是否正确
return $value == $answer;
}
}
此功能需要用户开启 Memory 缓存,如 Redis
安全验证场景
X5.0 增加了安全验证场景,可自定义新的安全验证场景,并配套相应设置页面和相应脚本实现一套完整的安全验证场景流程。
要增加新的安全验证场景,您需要将脚本写到 /source/plugin/myplugin/seccheck/ 目录下,脚本命名为 seccheck_xxx.php
class seccheck_name {
var $version = '1.0';//脚本版本号
var $name = 'seccheck_name';//安全验证场景名称 (可填写语言包项目)
var $copyright = 'Discuz! Team';//版权 (可填写语言包项目)
var $settingurl = 'action=plugins&operation=config&identifier=sample&pmod=op4';//安全验证场景设置页URL
function rule($param = []) {//安全验证显示规则,返回 true 表示此处显示安全验证
return true;
}
}
PHP 脚本通过以下方式判断是否需要显示验证码及安全验证
list($seccodecheck, $secqaacheck) = seccheck('sample:name', [$id]);
模板代码中通过以下代码调用输出
`{if $seccodecheck || $secqaacheck}`
`{block sectpl}`<div class="rfm"><table><tr><th><sec>: </th><td class="login_sec"><sec><sec></td></tr></table></div>`{/block}`
`{subtemplate common/seccheck}`
`{/if}`
提交后通过以下代码验证正确性
submitcheck('submit', 0, $seccodecheck, $secqaacheck)
RESTFul API 接口
X5.0 增加了基于 oAuth2 协议的开放 API 接口,开发者可以通过设计 XML 文件,即可增加 API 接口, 详见 API Gitee 文档
权限组件
从 X5.0 开始,通过插件可以扩展版块的权限机制,插件可以用新的维度控制用户是否可以进入版块、发帖、上传附件等操作。要增加权限组件您需要将脚本写到 /source/plugin/myplugin/perm 目录中,一个文件一个组件,每个文件都以 perm_ 开头命名,文件结构为:
class perm_a {
var $name = 'perm_a';
public function fetch_perm($uid) {
......
return true;
}
}
$name 为权限组件名称 fetch_perm 方法返回指定用户是否有权限
同时,X5.0 支持扩展权限类型,只需使用如下文件结构定义类型名称即可:
class perm_type {
var $typename = 'perm_type';
}
扩展的权限类型可用如下系统函数判断是否拥有权限:
forumperm($_G['forum']['extra']['perms']['perm_type']);
对象存储组件
X5.0 支持从上传底层接入对象存储,您需要将脚本写到 /source/plugin/myplugin/oss 目录中,创建一个 init.php 文件
class oss_plugin_sample {
const Name = 'oss_name';//OSS名称
const Desc = 'oss_desc';//OSS配置说明
public static function load($param) {
return new oss_class($param['oss_id'], $param['oss_key'], $param['oss_bucket'], $param['oss_endpoint'], $param['oss_url'], $param['oss_bucket_url']);
}
}
如需支持多个 OSS,可用下面的写法
class oss_plugin_sample {
const SubType = [
'oss1' => ['name' => 'oss_name1', 'desc' => 'oss_desc1'],
'oss2' => ['name' => 'oss_name2', 'desc' => 'oss_desc2'],
];
...
}
详细范例请参考 sample 插件
邮件发送组件
X5.0 可以用插件方式接管系统原有的邮件发送,您需要将脚本写到 /source/plugin/myplugin/mailsend/mailsend_xxx.php 中,例如:
class mailsend_xxx {
public function sendmail($toemail, $subject, $message = '', $from = '') {
// 发送邮件
runlog('SMTP', "--", 0);
return true;
}
}
使用时告知站长在 “邮件设置”>“邮件发送方式” 中选择 “通过插件发送”,“插件标识及脚本标识” 填写 “myplugin:xxx” 即可
超级分类信息
X5.0 中通过全新的形式加强了分类信息,不仅可以通过安装、卸载脚本直接安装分类信息字段,还可以为分类信息添加独立的列表页和内容页模板。
安装
require_once libfile('function/plugin');
threadtype_install(
name: 'sample', // 名称
fieldPrefix: 'sample', // 字段前缀
typeData: [
'name' => 'sample 分类信息',
'description' => 'sample 超级分类信息范例',
'special' => 1,
'modelid' => 0,
'expiration' => 0,
'template' => '内容页模板',
'stemplate' => '列表页模板',
'ptemplate' => '发帖模板',
'btemplate' => 'DIY模板',
'super' => serialize([ // 调用的独立模板
'forumdisplay' => 'sample:super_forumdisplay',
'viewthread' => 'sample:super_viewthread',
]),
],
fieldData: [ // 字段列表
[
'title' => '字段1',
'identifier' => 'sample_field1',
'type' => 'text',
],
[
'title' => '字段2',
'identifier' => 'sample_field2',
'type' => 'textarea',
],
],
);
卸载
require_once libfile('function/plugin');
threadtype_uninstall('sample');
管理平台新增组件
权限选择器组件
为了达到权限机制的统一,X5 中内置了权限选择器的系统组件,开发者可通过此组件直接显示出一个与版块权限设置同等功能系统组件,用于设置自己应用的权限,保存的设置项可直接通过 forumperm() 函数检测是否拥有权限。
showcomponent('setname', 'varname', $value, 'component_perm', 'comment', [
'permtype' => ['group', 'verify', 'account', 'tag', 'plugin'], //显示的权限类型,留空为显示全部权限类型
'formula' => true //按照权限公式表达式方式显示
]);
容量选择器组件
容量选择器是一个直接可以同时选择 KB、MB、GB 单位的组件,通过此组件,站长不必再了解容量单位的换算,直接选择相关的容量单位即可。
showcomponent('setname', 'varname', value, 'component_size', '', $conf);
$conf 参数默认为 “KB,MB,GB”,如单独显示2个单位可单独设置
微信统一方法(X5.0.4起生效)
给「微信生态功能」复用的统一入口(避免重复刷新 access_token 互相挤掉)。 微信的 client_credential(cgi-bin/token)是【同一 AppID 全局唯一、刷新即作废旧 token】的凭证。若各功能各自独立去刷新,会互相把对方的 token 打掉,导致间歇性失效。因此本方法提供【唯一的、带缓存与并发锁】的令牌入口。
统一的token获取机制
weixin::get_access_token($appid, $appsecret);
取得分享/模板消息/自定义菜单等后端 API 共用的 access_token,内部按 appid 隔离缓存、带文件锁防止并发刷新冲突。
统一的ticket获取机制
weixin::get_jsapi_ticket($appid, $appsecret);
取得 JS-SDK 用的 jsapi_ticket(基于上面的 token,自动复用)。
注意:本文件缓存的是「公众平台后端 API 凭证(cgi-bin/token)」,与核心 account_wechat 用于「网页授权登录(sns/oauth2/access_token)」的用户级 token 是【两种不同类型】,请勿混用,也无需复用。
统一的微信分享
在后台配置并开启微信分享功能后,页面后端使用
$_G['wxsharedata'] = [
'title' => '标题',
'desc' => '简介',
'link' => '链接地址',
'imgUrl'=> '图片地址',
];
定义分享的数据,页面自动处理数据。
嵌入点注册(X5.0.4起生效)
从 X5.0.4+ 版本开始可用全新的方式在直接注册钩子,关联钩子和指定类的方法
安装时注册嵌入点:
hook::register([
['hookid' => 'common', 'method' => ['sample\lib_hook', 'test1'], 'type' => 1, 'modid' => 'global::common'],
['hookid' => 'global_usernav_extra1', 'method' => ['sample\lib_hook', 'test1'], 'type' => 1, 'modid' => 'global::global'],
['hookid' => 'viewthread_avatartop', 'method' => ['sample\lib_hook', 'test2'], 'type' => 1, 'modid' => 'forum::viewthread'],
]);
hookid: 嵌入点 ID method: [类名, 方法名] modid: CURSCRIPT::CURMODULE type: 1=funcs,2=outputfuncs,3=messagefuncs hookType: 1=PC,2=mobile,空=所有
卸载时取消注册:
hook::unregister([
['hookid' => 'global_usernav_extra1', 'method' => ['sample\lib_hook', 'test'], 'type' => 1, 'modid' => 'global::global'],
]);
整理自 Discuz! 官方开放平台技术文档(open.dismall.com),由 WorkBuddy 整理发布。
