Discuz! X3 二次开发核心教程

第一部分:准备工作与核心概念

在开始编码之前,你必须先理解 Discuz! 的一些核心概念和准备工作。

环境准备

你需要一个标准的 PHP 开发环境:

  • Web 服务器: Nginx 或 Apache
  • 数据库: MySQL 5.0+ 或 MariaDB
  • PHP: PHP 5.3+ (推荐 5.4 或 5.5,高版本可能不兼容)
  • Discuz! X3 源码: 从官方或可靠渠道获取。
  • 开发工具: VS Code, Sublime Text, PhpStorm 等。

目录结构初探

了解 Discuz! 的目录结构是二次开发的第一步,核心目录如下:

  • source/: 核心代码目录,所有的业务逻辑、数据处理都在这里。
    • class/: 核心类库文件,如 discuz 类(全局核心类)、table 类(数据表操作基类)等。
    • module/: 模块目录,按功能模块划分,如 forum/ (论坛), home/ (个人空间), admin/ (后台管理)。
    • function/: 全局函数库,包含大量常用函数。
  • static/: 静态资源目录,存放 CSS, JavaScript, 图片等文件。
  • template/: 模板目录,存放所有页面的 HTML 模板文件。
  • api/: 相关接口文件。
  • uc_client/, uc_server/: 用户中心相关代码。
  • config/: 配置文件目录,最重要的 config_global.php
  • install/: 安装程序目录。

核心概念

  • 入口文件: index.php,所有前台页面的请求都通过这个文件进入,它会加载核心类,并根据 URL 参数决定调用哪个模块和函数。

  • 全局核心类 - Discuz: source/class/discuz/discuz.class.php,这是 Discuz! 的“心脏”,负责初始化整个应用、加载配置、处理用户会话、权限验证等,几乎所有页面都会先实例化这个类。

  • $_G 全局变量: $_G 是一个超级全局数组,存储了当前运行环境的所有核心信息,如:

    • $_G['uid']: 当前登录用户 ID。
    • $_G['username']: 当前登录用户名。
    • $_G['group']: 当前用户组信息。
    • $_G['config']: 站点配置信息。
    • $_G['forum']: 当前论坛相关信息。
    • 你可以通过 $_G 在任何地方获取站点的全局状态。
  • 数据库操作 - DB: Discuz! 封装了 DB 类用于数据库操作,你不需要写原生 SQL,而是使用其提供的链式操作。

    • 示例:

      // 从 pre_common_member 表中查询 uid=1 的用户
      $member = DB::fetch_first("SELECT * FROM %t WHERE uid=%d", array('common_member', 1));
      // 使用链式操作(推荐)
      $member = DB::fetch_first("SELECT * FROM %t WHERE uid=%d", array('common_member', 1));
      // 或者
      $member = DB::table('common_member')->where('uid', 1)->get();
  • 缓存机制: Discuz! 使用了 cache 类来处理缓存,能极大提升性能,缓存文件通常位于 data/cache/ 目录下。

    • C::t('tablename')->fetch_by_...() 等方法在读取数据时,会自动检查缓存。
  • 钩子 - plugin: Discuz! 的插件系统是其扩展性的核心,它通过在特定位置设置“钩子”,允许插件代码在这些位置被执行,开发插件就是编写能挂载到这些钩子上的代码。


第二部分:实战开发 - 创建一个简单的“Hello World”页面

我们将通过创建一个自定义页面,来熟悉 Discuz! 的开发流程。

目标: 在 你的域名/hello 访问到一个显示 "Hello, Discuz! X3!" 的页面。

步骤 1: 创建控制器

控制器负责处理逻辑,我们将在 source/module/ 下创建一个自定义模块。

  1. source/module/ 目录下新建一个文件夹,命名为 mymodule

  2. source/module/mymodule/ 目录下,创建一个 PHP 文件,命名为 hello.php

    <?php
    /**
     * Discuz! X3 自定义模块示例
     */
    if(!defined('IN_DISCUZ')) {
        exit('Access Denied');
    }
    // class mymodule_hello 是必须的命名规范,模块名_文件名
    class mymodule_hello {
        // 构造函数,页面加载时自动执行
        public function __construct() {
            // 可以在这里做一些初始化操作
        }
        // 定义一个公共方法,方法名将作为 URL 中的 action 参数
        public function index() {
            // 1. 准备数据
            $message = 'Hello, Discuz! X3!';
            // 2. 加载模板
            // template/mymodule:hello 是模板路径,对应 template/mymodule/hello.htm
            include template('mymodule:hello');
        }
    }

代码解释:

  • if(!defined('IN_DISCUZ')) exit('Access Denied');: 这是 Discuz! 的安全检查,防止文件被直接访问。
  • class mymodule_hello: 类名必须遵循 模块名_文件名 的规则。
  • public function index(): 这个方法将作为我们页面的默认逻辑,URL 中的 action 参数会对应方法名。

步骤 2: 创建模板

模板负责展示数据。

  1. template/ 目录下新建一个文件夹,命名为 mymodule

  2. template/mymodule/ 目录下,创建一个 HTML 文件,命名为 hello.htm

    <!DOCTYPE html>
    <html>
    <head>
        <meta charset="UTF-8">
        <title>我的第一个 Discuz! 页面</title>
    </head>
    <body>
        <h1>{$_G['siteinfo']['name']} - 示例页面</h1>
        <p>{$_G['message']}</p>
    </body>
    </html>

代码解释:

  • {$_G['siteinfo']['name']}: 这是 Discuz! 的模板语法,用于输出全局变量 $_G 中的站点名称。
  • {$_G['message']}: 我们将在控制器中把变量赋值给 $_G,以便在模板中调用。

步骤 3: 修改控制器以传递数据

我们需要修改 hello.php,将 $message 变量传递给模板。

修改 source/module/mymodule/hello.php 中的 index() 方法:

public function index() {
    // 1. 准备数据
    $message = 'Hello, Discuz! X3!';
    // 2. 将变量传递给模板,推荐使用 $_G
    $_G['message'] = $message;
    // 3. 加载模板
    include template('mymodule:hello');
}

步骤 4: 访问页面

你可以通过浏览器访问 你的域名/index.php?mymodule-hello-index 来查看你的页面了。

  • index.php: 入口文件。
  • mymodule: 模块名,对应 source/module/mymodule/
  • hello: 控制器文件名,对应 source/module/mymodule/hello.php
  • index: 控制器中的方法名。

如果一切正常,你应该能看到一个标题为你的站点名称,内容为 "Hello, Discuz! X3!" 的页面。


第三部分:进阶主题

数据库操作实战

假设我们要在页面上显示论坛的最新 5 个主题。

  1. 修改控制器 (source/module/mymodule/hello.php):

    public function index() {
        // 从 pre_forum_post 表中查询最新的 5 个主题
        // 注意:表名前缀 'pre_' 是动态的,使用 %t 可以自动替换
        $query = DB::query("SELECT * FROM %t WHERE fid!=0 AND first=1 ORDER BY tid DESC LIMIT 5", array('forum_post'));
        $threads = array();
        while ($thread = DB::fetch($query)) {
            $threads[] = $thread;
        }
        // 将主题数组传递给模板
        $_G['threads'] = $threads;
        include template('mymodule:hello');
    }
  2. 修改模板 (template/mymodule/hello.htm):

    <!DOCTYPE html>
    <html>
    <head>
        <meta charset="UTF-8">
        <title>我的第一个 Discuz! 页面</title>
    </head>
    <body>
        <h1>{$_G['siteinfo']['name']} - 示例页面</h1>
        <p>{$_G['message']}</p>
        <hr>
        <h2>最新 5 个主题</h2>
        <ul>
        <!-- 使用 foreach 循环输出主题列表 -->
        {loop $_G['threads'] $thread}
            <li>
                <a href="forum.php?mod=viewthread&tid={$thread[tid]}" target="_blank">
                    {$thread[subject]}
                </a>
                - 发布于 {date($thread[dateline], 'Y-m-d H:i')}
            </li>
        {/loop}
        </ul>
    </body>
    </html>

    模板语法解释:

  • {loop $array $value}: 循环遍历数组。
  • {$thread[tid]}: 输出数组元素的值。
  • {date($timestamp, 'format')}: Discuz! 提供的日期格式化函数。

后台开发

后台开发与前台类似,但目录和权限不同。

  1. 目录: 后台控制器在 source/admincp/ 目录下。
  2. 入口: 后台入口是 admin.php
  3. 权限: 后台操作必须检查管理员权限,通常通过 checkauth()checkprima() 函数实现。

简单示例:在后台添加一个菜单项和页面

  1. 修改菜单文件: source/language/lang_admincp.php (或对应的语言包文件),在 $_G['adminmenu']['plugin'] 数组中添加你的菜单项。

    'plugin' => array(
        'name' => '插件',
        'url' => 'admin.php?action=plugins',
        'sub' => array(
            // ... 其他子菜单
            array('name' => '我的模块', 'url' => 'admin.php?action=mymodule'), // 添加这一行
        ),
    ),
  2. 创建后台控制器: 在 source/admincp/ 下创建 mymodule.php

    <?php
    if(!defined('IN_DISCUZ') || !defined('IN_ADMINCP')) {
        exit('Access Denied');
    }
    class admincp_mymodule {
        public function run() {
            // 检查权限
            if(!getstatus($_G['group']['allowadminplugin'], 5)) { // 假设权限位是第5位
                cpmsg('plugin_nopermission', '', 'error');
            }
            // 这里可以写后台逻辑,比如保存设置等
            if(submitcheck('settingsubmit')) {
                // 处理表单提交...
                cpmsg('settings_updated', 'action=mymodule', 'succeed');
            }
            // 加载后台模板
            include template('admin/mymodule:settings');
        }
    }
  3. 创建后台模板: 在 template/admin/ 下创建 mymodule_settings.htm

    <form method="post" autocomplete="off" action="admin.php?action=mymodule">
        <input type="hidden" name="formhash" value="{FORMHASH}" />
        <input type="hidden" name="settingsubmit" value="1" />
        <div class="bm bml">
            <div class="bm_h cl">
                <h2>我的模块设置</h2>
            </div>
            <div class="bm_c">
                <!-- 这里可以放设置表单 -->
                <table class="tb tb2">
                    <tr>
                        <th>一个示例设置</th>
                        <td><input type="text" class="px" name="setting[example]" value="{$setting[example]}" /></td>
                    </tr>
                </table>
            </div>
        </div>
        <div class="btnarea">
            <button type="submit" class="pn pnc"><strong>提交</strong></button>
        </div>
    </form>

第四部分:调试与技巧

  1. 开启调试模式: 在 config/config_global.php 中,将 $_config['debug'] 设置为 12,可以开启详细的错误报告和 SQL 查询日志,非常有助于调试。
  2. 使用 print_rvar_dump: 在代码中适当位置使用 print_r('<pre>'); print_r($_G); print_r('</pre>'); 来查看变量内容。
  3. 阅读官方源码: 最好的学习方式就是阅读 Discuz! 自身模块的源码,source/module/forum/ 下的文件,看看它是如何实现帖子列表、详情页等功能的。
  4. 利用缓存: 修改模板后,需要清除 data/template/ 下的缓存文件才能看到效果,修改配置或数据后,可能需要清除 data/cache/ 下的缓存。

Discuz! X3 的二次开发虽然基于较老的技术,但其架构设计依然有其合理性,掌握其核心要点:

  • 入口与路由: index.phpmodule 目录结构。
  • 全局状态: $_G 变量的使用。
  • 数据层: DB 类的链式操作。
  • 表现层: template 目录和模板语法。
  • 扩展性: plugin 钩子系统。

从简单的页面开始,逐步尝试数据库交互和后台功能,你很快就能上手 Discuz! X3 的二次开发了,祝你开发顺利!