插件机制

实例讲解

在开始插件开发前, 请您务必详细阅读插件开发手册
马甲插件是一款能够让用户在论坛中以多个帐号身份进行使用、交流的插件。论坛开启马甲插件后, 用户无需进行退出操作即可迅速切换到其他帐号。下面以马甲插件为例进行插件开发说明。

前台实现帐号切换、马甲设置等功能, 如图:

后台实现管理、锁定、查找等管理功能, 如图:

插件开发流程介绍说明:

  • 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']['use		rgroups']);
    		if(in_array('', $myrepeatsusergroups)) {
    			$myrepeatsusergroups = array();
    		}
    		$userlist = array();
    
    		/* 对当前登录用户进行马甲验证, 即当前用户组不再权限许可范围内, 但其他帐号所在用户组有权限, 则当		前用户也有使用权限。*/
    		if(!in_array($_G['groupid'], $myrepeatsusergroups)) {
    			if(!isset($_G['cookie']['myrepeat_rr'])) {
    
    				/* 这里需要注意一下你所建的数据表对象的构建, 即 source/plugin/myrepeats/t				able/下的 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\',\'a			jaxwaitid\');}'.
    		'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, showmyrepeat		s)">'.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['us	ername']);
    	$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.'&id	entifier=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='.$plugin		id.'&identifier=myrepeats&pmod=admincp&srchuid='.$myrepeat['uid'].'">'.$users[$myre		peat['uid']]['username'].'</a></td>'.'<td>'.$_G['cache']['usergroups'][$users[$myre		peat['uid']]['groupid']]['grouptitle'].'</td>'.'<td><a href="'.ADMINSCRIPT.'?action		=plugins&operation=config&do='.$pluginid.'&identifier=myrepeats&pmod=admincp&srchre		peat='.rawurlencode($myrepeat['username']).'" title="'.htmlspecialchars($myrepeat['		comment']).'">'.$myrepeat['username'].'</a>'.'</td>'.'<td>'.($myrepeat['lastswitch'		] ? $myrepeat['lastswitch'] : '').'</td>'.'<td><a id="d'.$i.'" onclick="ajaxget(thi		s.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=admin		cp&uid='.$myrepeat['uid'].'&username='.$myrepeat['usernameenc'].'&op=delete">['.$la		ng['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>