网上开发discuz插件的教程太少了,好在官方有提供,而且有非常详细的手册说明,只是对于新手不太友好,但只要懂php、mysql、css,入门会很快,在某些视频平台也有视频教程,不过那都是2019年的了,比较老了,有兴趣也可以去看看。
下文有一个插件开发的实例,感兴趣可以先看看,官方有介绍插件的实现流程。
插件开发实例
在开始插件开发前, 请您务必详细阅读插件开发手册。
马甲插件是一款能够让用户在论坛中以多个帐号身份进行使用、交流的插件。论坛开启马甲插件后, 用户无需进行退出操作即可迅速切换到其他帐号。下面以马甲插件为例进行插件开发说明。
前台实现帐号切换、马甲设置等功能图:
后台实现管理、锁定、查找等管理功能, 如图:
插件开发流程介绍说明:
1.首先在 Discuz! 配置文件 config/config_global.php 底部加入代码:
$_config['plugindeveloper'] = 1;
开启开发者模式。( 论坛后台插件设置项会开启开发者模式。 新增加的插件也会有设计模式的设置选项。)
2.管理员进入后台应用 -> 插件 -> 插件设计, 开始进行新插件的设计, 添加初始化插件的基本信息。
添加完基本信息后, 在插件文件夹目录 source/plugin/ 下新建对应的文件夹(这里我们新建 myrepeats/ 文件夹)。 如果插件开发过程中需要语言包, 则后台开启设置后在 data/plugindata/ 下添加语言包文件, myrepeats.lang.php (myrepeats为插件初始化中添加的唯一标识符)来存储我们插件开发过程中用到的语言。
添加的语言包文件,初始化状态如下:
<?php
$scriptlang['myrepeats'] = array(
'login_strike' => "密码错误次数过多,请重新设置马甲账号信息并在 15 分钟后再尝试切换。",
/* 含有变量值的语言包一般用在脚本文件中调用, 其中变量可以在showmessage(), lang()等函数中某个参数以数组键值对的形式指定替换值。*/
例如:showmessage('myrepeats:adduser_succeed', 'home.php?mod=spacecp&ac=plugin&id=myrepeats:memcp', array('usernamenew' => stripslashes($usernamenew))); */
'adduser_succeed' => "马甲账号 {usernamenew} 已成功添加。",
);
$templatelang['myrepeats'] = array(
'myrepeats' => "我的马甲",
'adduser' => "添加马甲账号",
);
$installlang['myrepeats'] = array(
);
?>
$scriptlang数组中存储脚本文件的语言包,$templatelang 数组中存储模版文件的语言包,$installlang 数组中存储安装、升级、卸载脚本用的语言包。
3.接下来我们需要添加我们开发过程中需要用到的程序模块文件。明确需要使用 Discuz! 插件模块中的以下几项:
扩展项目 个人面板:可在个人面板上部增加一个菜单项。(实现个人设置面板部分)
程序脚本 页面嵌入:设置一个包含页面嵌入脚本的模块,模块文件名指派为 source/plugin/插件目录/插件模块名.class.php”。(通过嵌入点来实现头部用户信息中, 快捷切换马甲的效果。)
扩展项目 管理中心:可在后台 -> 插件栏目中为此插件增添一个管理模块。(在后台为我们的马甲插件添加一个管理项目。)
在刚才新建好的插件中添加以上三个插件模块, 如图:
以上指定好模块文件后, 我们需要在对应的插件文件夹 source/plugin/myrepeats/ 里新建我们刚才添加的模块脚本文件。
接下来添加插件中需要用到的变量。(马甲插件只需要一个用户组是否开启的状态。)如图:
根据需要开发的插件功能, 设计数据表结构。在自己的开发环境下建好数据表, 以便在后面的开发过程中使用。当你新建完数据库之后, 需要根据你构建的数据库, 在 source/plugin/插件目录/table/ 下构建你的数据库类对象, 以便在后续的开发中使用。(每一个数据表, 都需要构建一个单独的数据库类对象。)
现在前期工作基本完成了, 接下来开始编写脚本文件,开发需要的功能了。以马甲插件为例, 现在开始在页面头部用户资料栏添加一个马甲切换的功能。此功能需要用页面嵌入的模块来开发。 前期准备工作中已经新建了这个模块文件, 即 myrepeats.class.php 脚本文件。下面我们来看一看这个脚本文件的代码实现:
<?php
/* 所有与插件有关的程序,包括全部的前后台程序,因全部使用外壳调用, 请务必在第一行加入以下三行代码, 以免其被 URL 直接请求调用,产生安全问题。 */
if(!defined('IN_DISCUZ')) {
exit('Access Denied');
}
/* 全局嵌入点类(必须存在)*/
class plugin_myrepeats {
var $value = array(); //初始化返回值变量。
/* 嵌入点对象初始化函数, 属于php面向对象机制特性。这里的函数名和类名是一致的, 在初始化类的时候以便执行这个函数,对$value进行赋值,以便下面的global_usernav_extra1()函数调用。*/
function plugin_myrepeats() {
global $_G;
if(!$_G['uid']) {
return;
}
/* 读取可以使用马甲的用户组 usergroups 变量值。需要注意参数的读取方式,详情见插件手册-参数读取。 */
$myrepeatsusergroups = (array)dunserialize($_G['cache']['plugin']['myrepeats']['usergroups']);
if(in_array('', $myrepeatsusergroups)) {
$myrepeatsusergroups = array();
}
$userlist = array();
/* 对当前登录用户进行马甲验证, 即当前用户组不再权限许可范围内, 但其他帐号所在用户组有权限, 则当前用户也有使用权限。*/
if(!in_array($_G['groupid'], $myrepeatsusergroups)) {
if(!isset($_G['cookie']['myrepeat_rr'])) {
/* 这里需要注意一下你所建的数据表对象的构建, 即 source/plugin/myrepeats/table/下的 table_新建表名.php */
$users = count(C::t('#myrepeats#myrepeats')->fetch_all_by_username($_G['username']));
dsetcookie('myrepeat_rr', 'R'.$users, 86400);
} else {
$users = substr($_G['cookie']['myrepeat_rr'], 1);
}
if(!$users) {
return '';
}
}
/* 前台显示代码 */
$this->value['global_usernav_extra1'] = '<script>'.
'function showmyrepeats() {if(!$(\'myrepeats_menu\')) {'.
'menu=document.createElement(\'div\');menu.id=\'myrepeats_menu\';menu.style.display=\'none\';menu.className=\'p_pop\';'.
'$(\'append_parent\').appendChild(menu);'.
'ajaxget(\'plugin.php?id=myrepeats:switch&list=yes\',\'myrepeats_menu\',\'ajaxwaitid\');}'.
'showMenu({\'ctrlid\':\'myrepeats\',\'duration\':2});}'.
'</script>'.
/* 此处是对个人前台设置管理马甲程序模块的连接,需要注意下格式是固定的。 */
'<span class="pipe">|</span><a id="myrepeats" href="home.php?mod=spacecp&ac=plugin&id=myrepeats:memcp" class="showmenu cur1" onmouseover="delayShow(this, showmyrepeats)">'.lang('plugin/myrepeats', 'switch').'</a>'."\n";
}
/* 这里使用了嵌入点函数 global_usernav_extra1() 返回到它对应输的显示位置, 所有嵌入点函数及对应位置见手册。 */
function global_usernav_extra1() {
return $this->value['global_usernav_extra1'];
}
}
?>
上面的脚本在前台界面头部增加了马甲入口,如图:
它的连接地址指向前台个人设置页面。程序由 home.php 进入, 然后进入默认的个人设置流程里面。
上图所示的模板显示是直接调用的 source/plugin/myrepeats/template/ 文件夹下的 memcp.htm 模版文件, memcp.inc.php 是与之对应的脚本处理代码。入口处的马甲切换列表, 是ajax调用插件中的 switch.inc.php 扩展脚本处理返回的。以上个人设置和帐号切换流程都是正常的 php 逻辑代码处理流程,这里就不复述了。
以上前台的功能我们基本已经开发完成, 现在需要开始开发后台管理的功能, 即 admincp.inc.php, 此文件在前面已经添加。
<?php
if(!defined('IN_DISCUZ') || !defined('IN_ADMINCP')) {
exit('Access Denied');
}
/* 语言包文件已经引入, 这里直接读取语言包,赋值给变量 $Plang。 */
$Plang = $scriptlang['myrepeats'];
/* 锁定、删除处理流程 */
if($_GET['op'] == 'lock') {
/* 插件数据库表对象方法的调用和使用形式。 */
$myrepeat = C::t('#myrepeats#myrepeats')->fetch_all_by_uid_username($_GET['uid'], $_GET['username']);
$lock = $myrepeat['lock'];
$locknew = $lock ? 0 : 1;
C::t('#myrepeats#myrepeats')->update_locked_by_uid_username($_GET['uid'], $_GET['username'], $locknew);
ajaxshowheader();
echo $lock ? $Plang['normal'] : $Plang['lock'];
ajaxshowfooter();
} elseif($_GET['op'] == 'delete') {
C::t('#myrepeats#myrepeats')->delete_by_uid_usernames($_GET['uid'], $_GET['username']);
ajaxshowheader();
echo $Plang['deleted'];
ajaxshowfooter();
}
$ppp = 100;
$resultempty = FALSE;
$srchadd = $searchtext = $extra = $srchuid = '';
$page = max(1, intval($_GET['page']));
if(!empty($_GET['srchuid'])) {
$srchuid = intval($_GET['srchuid']);
$srchadd = "AND uid='$srchuid'";
} elseif(!empty($_GET['srchusername'])) {
$srchuid = C::t('common_member')->fetch_uid_by_username($_GET['srchusername']);
if($srchuid) {
$srchadd = "AND uid='$srchuid'";
} else {
$resultempty = TRUE;
}
} elseif(!empty($_GET['srchrepeat'])) {
$extra = '&srchrepeat='.rawurlencode($_GET['srchrepeat']);
$srchadd = "AND username='".addslashes($_GET['srchrepeat'])."'";
$searchtext = $Plang['search'].' "'.$_GET['srchrepeat'].'" '.$Plang['repeats'].' ';
}
if($srchuid) {
$extra = '&srchuid='.$srchuid;
$member = getuserbyuid($srchuid);
$searchtext = $Plang['search'].' "'.$member['username'].'" '.$Plang['repeatusers'].' ';
}
$statary = array(-1 => $Plang['status'], 0 => $Plang['normal'], 1 => $Plang['lock']);
$status = isset($_GET['status']) ? intval($_GET['status']) : -1;
if(isset($status) && $status >= 0) {
$srchadd .= " AND locked='$status'";
$searchtext .= $Plang['search'].$statary[$status].$Plang['statuss'];
}
if($searchtext) {
$searchtext = '<a href="'.ADMINSCRIPT.'?action=plugins&operation=config&do='.$pluginid.'&identifier=myrepeats&pmod=admincp">'.$Plang['viewall'].'</a> '.$searchtext;
}
/* 加载用户组缓存信息。 */
loadcache('usergroups');
/* 这里输出表格头部和表单 html 到当前位置。Discuz! 后台输出 html 界面函数, 可在后台函数库文件source/function/function_admincp.php 中查看具体输出内容。*/
showtableheader();
/* 本页面的地址连接,其中 do = $pluginid 为当前插件标识id, 此id为自动生成的id, 在书写本页面地址时需要注意此参数。*/
showformheader('plugins&operation=config&do='.$pluginid.'&identifier=myrepeats&pmod=admincp', 'repeatsubmit');
showsubmit('repeatsubmit', $Plang['search'], $lang['username'].': <input name="srchusername" value="'.htmlspecialchars($_GET['srchusername']).'" class="txt" /> '.$Plang['repeat'].': <input name="srchrepeat" value="'.htmlspecialchars($_GET['srchrepeat']).'" class="txt" />', $searchtext);
showformfooter();
$statselect = '<select onchange="location.href=\''.ADMINSCRIPT.'?action=plugins&operation=config&do='.$pluginid.'&identifier=myrepeats&pmod=admincp'.$extra.'&status=\' + this.value">';
foreach($statary as $k => $v) {
$statselect .= '<option value="'.$k.'"'.($k == $status ? ' selected' : '').'>'.$v.'</option>';
}
$statselect .= '</select>';
/* 界面具体内容显示输出。*/
echo '<tr class="header"><th>'.$Plang['username'].'</th><th>'.$lang['usergroup'].'</th><th>'.$Plang['repeat'].'</th><th>'.$Plang['lastswitch'].'</th><th>'.$statselect.'</th><th></th></tr>';
if(!$resultempty) {
$count = C::t('#myrepeats#myrepeats')->count_by_search($srchadd);
$myrepeats = C::t('#myrepeats#myrepeats')->fetch_all_by_search($srchadd, ($page - 1) * $ppp, $ppp);
$uids = array();
foreach($myrepeats as $myrepeat) {
$uids[] = $myrepeat['uid'];
}
$users = C::t('common_member')->fetch_all($uids);
$i = 0;
foreach($myrepeats as $myrepeat) {
$myrepeat['lastswitch'] = $myrepeat['lastswitch'] ? dgmdate($myrepeat['lastswitch']) : '';
$myrepeat['usernameenc'] = rawurlencode($myrepeat['username']);
$opstr = !$myrepeat['locked'] ? $Plang['normal'] : $Plang['lock'];
$i++;
echo '<tr><td><a href="'.ADMINSCRIPT.'?action=plugins&operation=config&do='.$pluginid.'&identifier=myrepeats&pmod=admincp&srchuid='.$myrepeat['uid'].'">'.$users[$myrepeat['uid']]['username'].'</a></td>'.'<td>'.$_G['cache']['usergroups'][$users[$myrepeat['uid']]['groupid']]['grouptitle'].'</td>'.'<td><a href="'.ADMINSCRIPT.'?action=plugins&operation=config&do='.$pluginid.'&identifier=myrepeats&pmod=admincp&srchrepeat='.rawurlencode($myrepeat['username']).'" title="'.htmlspecialchars($myrepeat['comment']).'">'.$myrepeat['username'].'</a>'.'</td>'.'<td>'.($myrepeat['lastswitch'] ? $myrepeat['lastswitch'] : '').'</td>'.'<td><a id="d'.$i.'" onclick="ajaxget(this.href, this.id, \'\');return false" href="'.ADMINSCRIPT.'?action=plugins&operation=config&do='.$pluginid.'&identifier=myrepeats&pmod=admincp&uid='.$myrepeat['uid'].'&username='.$myrepeat['usernameenc'].'&op=lock">'.$opstr.'</a></td>'.'<td><a id="p'.$i.'" onclick="ajaxget(this.href, this.id, \'\');return false" href="'.ADMINSCRIPT.'?action=plugins&operation=config&do='.$pluginid.'&identifier=myrepeats&pmod=admincp&uid='.$myrepeat['uid'].'&username='.$myrepeat['usernameenc'].'&op=delete">['.$lang['delete'].']</a></td></tr>';
}
}
showtablefooter();
/* 分页输出 */
echo multi($count, $ppp, $page, ADMINSCRIPT."?action=plugins&operation=config&do=$pluginid&identifier=myrepeats&pmod=admincp$extra");
?>
这个文件主要功能是对后台插件数据进行处理。开发者可以根据自己的需求, 设计此文件的代码结构。插件开发时需要注意, 后台提供了很多Discuz! 内置的函数来显示界面, 例如: showtableheader(), showformheader(), showsubmit() 等函数, 方便开发使用。具体用法请参照开发手册-后台页面开发。
这样整个插件的功能开发已经完成, 下面我们需要将我们制作好的插件导出即可。此时我们导出的是xml配置文件, 里面主要是插件的一些基本信息配置参数以及语言包内容。另外, 插件作者可以设计 2 个脚本文件用于插件的安装和卸载,文件名任意。脚本中可用 runquery() 函数执行 SQL 语句,表名可以直接写“cdb_”。插件作者只需在导出的 XML 文件结尾加上安装、卸载脚本的文件名即可
<item id="installfile"><![CDATA[install.php]]></item>
<item id="uninstallfile"><![CDATA[uninstall.php]]></item>
</item>
</root>
网站地址
开发插件需要一定的php、myql、css基础,不然看不懂代码,推荐在本站搜索相关的教程网站。