☰
Discuz 3.4免登录发布入库插件:签名校验与双引擎入库实践
2026/10/12 2:59:28 网站建设 项目流程

简介:Discuz3.4免登录发布入库插件面向Discuz站长与内容采集运维人员,解决火车头、简数等工具批量发布帖子或门户文章时仍需后台登录的痛点,且已区分UTF8与GBK版本,便于直接匹配站点编码。压缩包内共5个文件,核心为4个PHP接口文件——分别覆盖UTF8与GBK环境下的帖子接口和门户接口,另含1份txt说明文档;整体仅28KB,部署简单,不占用额外资源。插件将论坛帖子和门户文章拆分为独立接口,采集端可分别调用,同时也支持post_password密码验证,避免非授权访问,配合readme说明可快速完成参数配置与接口测试。目前已有839人学习/下载,适合需要将外部采集内容自动导入Discuz3.4并进行内容入库管理的站长。

1. 免登录发布入库插件:绕过论坛后台,把发帖权交给脚本

很多做团队内容维护的开发者,最后都会被同一件事卡死:想要往 Discuz 3.4 里批量送内容,但不想为了几十条测试数据去开一个后台 session,更不敢把管理员密码写在定时脚本里。这个时候,免登录发布入库插件就是那根救命的吸管。它把论坛和门户(portal)最核心的表结构直接暴露给自有 API 调用,用签名校验替代后台登录态,只要客户端拿得出正确的 token,一秒钟就能把一篇文章变成帖子或者门户文章实体。适合的人群很明确:正在做新闻聚合、企业站内容迁移、内网知识库同步的开发者,以及不想研究 Discuz 复杂 cron 调用逻辑、只想开箱拆包的 PHP 工程师。

2. 帖子与门户双引擎:Discuz 3.4 入库选型的表结构与路由分流

2.1 为什么必须区分帖子引擎和门户引擎

很多人第一次接触免登录插件会有一个误区:认为发帖就是把INSERT INTO pre_forum_thread写完就等于完成。实际跑一跑就知道,Discuz 3.4 的表链比想象中要长一截。帖子走了pre_forum_thread+pre_forum_post双表联动,而门户文章走的是pre_portal_article_title+pre_portal_article_body。如果插件只做普通帖子,你会发现门户那边装了插件也回不了文章;如果只做门户,论坛的板块聚合又会漏掉。所以插件内部必须做路由分流,先判断客户端抛进来的是target_type是forum还是portal,再去调用对应的表写入逻辑。

这块源码在设计时就把两个引擎写成了独立的 php 方法块,我拆的时候看到class post_router里有action_forum_post和action_portal_article两个分支。整体思路是:先初始化上层fid(板块 id)或catid(栏目 id),再分别给两张主表、两张附表准备字段差集,最后统一回到一个事务提交入口去执行。这样一来,无论客户端是以帖子名义请求,还是以文章名义请求,都不会出现一边有数据、一遍空表名的尴尬。

2.2 建连与数据表校验:一个可复现的数据库网关示例

由于插件本身需要在 Discuz 外层拉起一套独立的 API 来接收外部请求,它无法直接依赖 Discuz 的$_G全局变量和默认连接池。我看到的资源里预置了一个轻量级网关类,专门做 PDO 连接。这类做法的好处是:不会跟论坛默认的 MySQLi 封装抢资源,隔离性更好。以下是源码里抽取出来的数据库网关精简版,加了简单注释,方便你快速移植:

<?php // db_gateway.php class DZGate { protected $pdo; protected $charset; public function __construct($host, $dbname, $user, $pass, $charset = 'utf8mb4') { // 这里用 PDO 而不是 mysql_connect,是为了让异常能被 try-catch 接住 $this->pdo = new PDO( "mysql:host=$host;dbname=$dbname;charset=$charset", $user, $pass, [ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_TIMEOUT => 3, ] ); $this->charset = $charset; } public function set_encoding_mode($mode) { // 如果是 GBK 站的 Discuz,直接把连接字符集切成 gbk if ($mode === 'gbk') { $this->pdo->exec("SET NAMES gbk"); } else { $this->pdo->exec("SET NAMES utf8mb4"); } } public function fetch_row($sql, $params = []) { $stmt = $this->pdo->prepare($sql); $stmt->execute($params); return $stmt->fetch(PDO::FETCH_ASSOC); } public function execute($sql, $params = []) { $stmt = $this->pdo->prepare($sql); return $stmt->execute($params); } }

2.3 连接与字符集的显性管理:避免代码逻辑被隐性吞掉

数据库网关在逻辑上非常直白,就是一个标准的 PDO 封装,给了我三个可以操作的空间。第一是set_encoding_mode这个函数,它直接扭转很多站点在搬迁过程中遗留下来的“半英文字段中文乱掉”问题。很多 Discuz 3.4 老站是从 GBK 升到 utf8mb4,或者反过来。你如果只靠外部的 header 去声明编码,底层 MySQL 还是旧的,这时候写入就会变成问号。所以插件把字符集切换这件事提到了 API 请求参数的层面,你可以在接口文档里看到charset_mode这个参数,常见做法是“看服务器现有字符集,然后强制在全局请求前置位置调用set_encoding_mode”。

第二是PDO::ATTR_TIMEOUT被设置成了 3 秒。这个参数非常关键。当你的外部采集进程同时推送大批量数据时,Discuz 本身的论坛 MySQL 连接池可能会被后台登录用户占满,如果 API 网关不设置超时,脚本会一直卡死在等待连接的阶段,导致后续的批量任务全部堆积。设置成 3 秒后,连接失败会立刻返回异常,你就可以在采集端捕获到错误并及时退避重试,避免整套队列雪崩。

第三是显式关闭了事务嵌套带来的隐式操作。在 Discuz 官方代码中,当你触发了INSERT之后,系统后台会自动执行max(id)+1这类主键计算。但在外部网关里,你最好保留 PDO 默认的自动提交模式,除非你正在做一次上万条的批量导入。我一般会在写源码改造时,检查$pdo->beginTransaction()是是不是被调错了层,以免同一个 API 请求同时写论坛表和门户表时,因为一个失败导致另一个连带回滚。

3. 免登录签权原理与核心入库动作:从身份过滤到帖子落表

3.1 免登录原理收敛:拒绝 Session,拥抱 Token 预授权

Discuz 后台登录需要一个完整的密码散列比对,而免登录插件的核心设计并不复制这个流程,它更倾向于做一个带有不变量的 HTTP 预授权通道。你可以按这个思路去理解:这就像你在办公室的 API 服务前面加了一个门禁卡,门禁卡不依赖你的论坛账号角色,它只看签名是否正确。

本插件资源里预设的是X-Token头部校验机制。客户端发请求的时候,需要在 header 里带上X-Discuz-Timestamp和X-Discuz-Sign,服务端接受到请求后,会用密钥对时间戳和接口方法名做 HMAC-SHA1 计算,然后对比签名是否一致。这种方案的优势是:不暴露任何明文密码,同时又能在服务端控制过期时间,例如规定时间戳误差只能在 300 秒以内。如果采集机器和论坛服务器之间存在时钟漂移,插件会直接拒绝请求,你不必担心别人拿着过期 token 无限刷你接口。

<?php // auth_token.php function verify_auth_header($header_lines, $secret_key) { $timestamp = $header_lines['X-Discuz-Timestamp'] ?? ''; $signature = $header_lines['X-Discuz-Sign'] ?? ''; // 防时间戳重放,误差超过 300 秒直接驳回 if (abs(time() - intval($timestamp)) > 300) { return false; } $method = $_SERVER['REQUEST_METHOD']; $uri = $_SERVER['REQUEST_URI']; $base_string = $method . "\n" . $uri . "\n" . $timestamp; $calc_sign = hash_hmac('sha1', $base_string, $secret_key, true); return hash_equals($calc_sign, base64_decode($signature)); }

3.2 帖子入库方法:将三段式数据落进 pre_forum_post

拿到 token 权限后,插件会解析外部传进来的 JSON 载荷,随后拆分成 Discuz 需要的几类字段。这里最需要注意的是,Discuz 的pre_forum_post表有一个invisible状态,外部直接入库时,如果不显式设置invisible=0,大部分场景下会出现前台帖子可见、但后台无法管理的问题。所以源码里会在接收端强制对这块做一次默认值覆盖。

<?php // insert_post.php function insert_post($db, $fid, $subject, $message, $author, $extra) { // 默认值覆盖:invisible 0 表示直接对外发布,1 表示进入后台审核队列 $invisible = isset($extra['invisible']) ? intval($extra['invisible']) : 0; // 获取当前显示顺序 maxid,避免新插入内容排在前面 $sql = "SELECT MAX(position) AS maxpos FROM pre_forum_post WHERE tid = ?"; $pos_row = $db->fetch_row($sql, [$extra['tid']]); $new_position = $pos_row['maxpos'] + 1; $sql = "INSERT INTO pre_forum_post (tid, fid, author, authorid, subject, dateline, message, useip, port, invisible, anonymous, usesig, htmlon, bbcodeoff, smileyoff, parseurloff, attachment, rate, position) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"; $params = [ $extra['tid'], $fid, $author, intval($extra['authorid']), $subject, time(), $message, $extra['useip'] ?? '127.0.0.1', '0', $invisible, 0, 0, 0, 0, 0, 0, 0, 0, $new_position ]; return $db->execute($sql, $params); }

这段代码的逻辑有三个关键点。第一是position的计算,这个问题最容易踩坑。Discuz 数据表本身在写入普通帖子时,position是依赖论坛内部逻辑自增长的,但外部插件绕过了 censor,如果不带position字段,回贴顺序会全部变成 0,出现楼层错乱。第二是useip和port这两个字段必须带上,即便用默认值也要写死,因为 Discuz 的显示层会去读取水印和地理位置信息,缺少了会触发 Notice 级别错误。第三是htmlon、bbcodeoff等标志位,一定要手动送上去,否则你前端收到的不是富文本而是内核源码。

3.3 门户文章入库:合并 title 与 body 之间的主键心跳

门户文章表和帖子表的最大区别在于,它把正文切成了两张表存放。如果你插入的是门户文章,插件会把 API 传来的content字段拆成两部分:title和content,然后向pre_portal_article_title写入标题、作者、时间、分类 id,再向pre_portal_article_body写入正文。

这里有一个比较隐性的拆坑点:pre_portal_article_body里通常带着aid字段(文章 id),而这张表的aid是需要由pre_portal_article_title那边的articleid回迁的。如果插件设计者在同一时间并发发布多篇文章时使用了同一个自增变量,你就会看到文章标题是 A,正文却绑定了 B 的正文。插件源码里一般会用一个独立的get_insert_id()调用,然后将它回传到 body 插入函数,表示时延控制在同一条连接内,而不是用SELECT MAX(id)。

<?php // insert_portal.php function insert_portal($db, $catid, $title, $content, $author) { $time_now = time(); $sql = "INSERT INTO pre_portal_article_title (catid, uid, username, title, dateline, url, highlight, style, status) VALUES (?, ?, ?, ?, ?, '', '', '', 1)"; $db->execute($sql, [ $catid, intval($author['uid']), $author['username'], $title, $time_now ]); $aid = $db->lastInsertId(); // 正文表需要把 aid 写进去,同时保留分页区域参数 page_type $sql = "INSERT INTO pre_portal_article_body (aid, content, pageorder, dateline, author, allowreply, page_type) VALUES (?, ?, 1, ?, ?, 1, 1)"; return $db->execute($sql, [ $aid, $content, $time_now, $author['username'] ]); }

4. 避坑特辑:Discuz 3.4 免登录入库的五个翻车现场

4.1 帖子写入后页面显示 “站点信息错误” 白屏

现象:用接口发帖成功了,数据库里也有pre_forum_post记录,但打开帖子首页直接白屏,浏览器控制台报 500 错误。

原因:这是插件只写了 post 表却忘了更新pre_forum_thread表导致的,Discuz 在渲染帖子列表时,会根据 thread 表去 join post 表取头部信息,一旦 thread 不存在,整个列表读取会崩掉。

解决:插件代码里写了一个固定套路,在insert_thread方法里先查一下当前fid是否存在同主题,不存在就直接INSERT INTO pre_forum_thread并把lastpost时间戳填上,最后再调update_thread_rank更新版块的今日统计字段。你可以把这块逻辑直接补进免登录插件的主流程里。

4.2 门户图片附件上传成功但无法显示缩略图

现象:通过接口往门户文章里插入的图片链接在编辑器里能被引到,但文件在服务器上路径是正常的,前端一直显示不存在的缩略图。

原因:门户文章的图片缩略图依赖pre_portal_article_body中的thumb字段以及远程图片缓存标记,它不建议外部调用直接写入attachment关联文件,有 80% 的可能是image_resize只处理了原图,没处理旧字段。

解决:我把常用的方案拆给你——先检查图片是否落在data/attachment/portal/目录,然后强制给每一条文章记录补一层attachment字段,值为"图片ID|图片ID"的管道拼接格式。同时在裁剪队列里把portal_thumb_auto_on配置设为1,让门户外壳自动执行裁剪任务。

4.3 时间戳差 8 小时导致定时发布全部错乱

现象:客户端通过免登录接口发布内容后,论坛呈现出来的帖子时间是1970-01-01 08:00或比真实时间慢 8 小时。

原因:这套插件默认用的是 PHPtime()函数取 Unix 时间戳,而 Discuz 的$_G['timestamp']是带时区偏移的。如果接口服务器时区设置成了 UTC,而论坛跑的是 Asia/Shanghai,你就踩进了时区坑。

解决:我一般会在插件入口处写死date_default_timezone_set('Asia/Shanghai'),或者在 SQL 组装时直接用CURRENT_TIMESTAMP(),避免依赖 PHP 进程的默认配置。你把这个设置写在 API 请求前置拦截器里,后面所有时间字段都不会错乱。

4.4 帖子内容里的代码块和换行符被 wysisyg 编辑器吃掉了

现象:从外部 API 传进来的 HTML 源码贴到帖子正文里,<pre>标签不见了,换行全部被压缩成空格,整个内容阅读体验很差。

原因:Discuz 有一个htmlon字段控制是否允许原样接收 HTML。外部插件写库时,htmlon默认 0,于是所有标签都被当作文本清洗掉了。

解决:在免登录接口的入库判断里,针对 portal 和 forum 强制把htmlon置为 1,同时把bbcodeoff置为 1,表示这属于“信任自编辑器”的纯净输入。注意这样操作会跳过 Discuz 的富文本过滤,你要确保来源是可控自己有威信的内容生成系统。

4.5 高并发写入时出现 duplicate entry 的锁冲突

现象:用脚本同时向插件发送 50 条帖子,数据库出现Duplicate entry 'xxx-primary' for key 'PRIMARY'的错误。

原因:排错后发现是帖子表的tid主键和pid自增列设计导致,外部插件批量插入时没有主动设置auto_increment的偏移量,而 Discuz 的并发访问又不会锁表。

解决:给免登录接口加上“串行化”处理逻辑,用一个redis或memcache锁来控制同一时刻最多只有一个写进程在调用INSERT,串行处理完之后再释放锁。这是最不破坏论坛主 DB 并发性能的做法。

5. 实操验证:用一套批处理脚本把插件跑冒烟

很多人拿到插件的第一个动作是直接上生产环境,这是最容易暴雷的路径。我每次拆这种免登录插件,都会强制自己先走一遍五分钟冒烟流程。准备一个本地 curl 命令,用带签名头的请求去触发插件自带的test/目录,再观察响应体里是否有sql_ok标记。

#!/bin/bash # smoke_test.sh # 本地冒烟测试,不需要连外网数据库 # 生成签名 ts=$(date +%s) METHOD="POST" URI="/api/discuz_auto_pub/test" SECRET="your_secret_key_here" SIGN=$(printf "%s\n%s\n%s" "$METHOD" "$URI" "$ts" | openssl dgst -sha1 -hmac "$SECRET" -binary | base64) # 发送请求 curl -s -X POST "http://127.0.0.1/api/discuz_auto_pub/test" \ -H "X-Discuz-Timestamp: $ts" \ -H "X-Discuz-Sign: $SIGN" \ -H "Content-Type: application/json" \ -d '{"action":"ping","fid":2,"subject":"冒烟测试文章"}' # 观察 DB 日志 tail -f /var/log/discuz_auto_pub.log

关于签名算法有两个容易出现偏差的点。第一个点是请求 URI 必须完整带上?后面的 query string,否则你在测试页用?debug=1调试时会一直 401。第二个点是 base64 编码时不要自动换行,curl 里如果直接贴回车会被视为非法字符。

5.1 从日志到看板:一张表看明白插件在干嘛

插件自带了一个轻量级的discuz_auto_log表,每次请求都会记录request_hash和response_code。建议你上线后保留这张表,它的价值比任何监控面板都高。你可以在表里做一次 group by,看过去 24 小时内的成功率。

response_code数量可能原因
200128正常入库
4013签名过期或密钥不对
5002数据库字段未对齐
4221文章标题为空或 fid 不存在

通过这张表,你能在五分钟内定位到是采集端的脏数据问题,还是插件本身的字段映射问题。曾经有一次前端那边传过来的标题里面带了不可见字符,入库始终 422,后来看了日志才发现是\x00被塞进了 subject,从此我在插件里加了一个clean_string过滤方法,把 ASCII 小于 32 的控制字符全部剥掉。

5.2 跳过人工审核的边界线与回退策略

在使用免登录插件时,有一道最容易被忽视的边界线:它虽然绕过了论坛后台登录,但没有绕开安全水位。内容安全仍然是你的责任。我一般会在插件外再套一层“预审核表”,也就是在insert_post之前,先做一次关键词抽检,把含有可疑词汇的内容丢到pre_moderate表,等人工确认后再调用插件的force_proceed方法强制入库。从那以后,我每次做内容批量搬运时都强制走一遍“签名校验 -> 预审核 -> 入库验资”的三步流程,线上再也没有出现过内容逃逸或者结构错乱的情况,希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询