资讯详情 Discuz免登录发布入库插件:原理、实现与避坑指南
📅 2026/10/12 1:03:11
简介针对Discuz3.4站点内容采集与自动发布需求这款免登录发布入库插件为论坛站长和采集工具使用者提供了直接写入帖子与门户的接口方案。插件区分UTF8、GBK两种编码版本可匹配不同字符集站点避免乱码发布帖子与门户内容各自使用独立接口职责清晰便于在火车头、简数等采集工具中灵活调用同时支持post_password密码验证能有效避免接口被滥用。压缩包共5个文件以4个PHP脚本和1个TXT说明为主配置说明与接口示例一目了然整体仅28KB部署非常轻量。附带的说明文档提供基本调用指引配合独立接口可实现从采集规则配置到入库的全流程对接。已有839人学习下载适合需要将采集内容快速免登录入库Discuz3.4的中高级站点管理员或需要深度定制发布逻辑的开发者借助该插件可省去后台手动发帖流程实现集中化内容管理。1. 免登录发布入库插件到底解决了什么问题一个站点同步需求的真实痛点如果你管理过 Discuz3.4 站点一定遇到过这种尴尬内容团队在外部编辑后台把稿件排好版转头要去论坛一篇一篇手动发发完论坛还要去门户再发一遍。手动发布意味着要登录后台、过验证码、卡发帖间隔编辑稍微手抖一下格式就乱了。后来我帮某内容团队做过一个免登录发布入库插件核心就一句话外部系统通过一个带令牌的 PHP 接口把已经排好的内容直接写进 Discuz 的 thread、post、portal_article 表帖子门户都支持全程不需要登录后台。反直觉的是与其模拟真实浏览器去发帖不如绕开前端逻辑直接入库速度和稳定性都高一个量级。这个方案适合做站群聚合、多端内容分发、采集入库的开发者也适合被重复发帖折磨得想在 Discuz 和 CMS 之间打通一条管道的站长。2. 免登录发布的核心原理与选型接口调用还是直接写库2.1 两个方案的本质区别API网关 vs 数据库直写先说清楚Discuz3.4 官方没有专门的免登录发帖 API所以市面上说的“免登录发布”无非两条路。第一条路叫模拟登录。用脚本带着用户名密码登录 Discuz 后台或前台发布页然后模拟表单提交。这条路问题很多Discuz 的登录有验证码、防灌水、发帖间隔甚至有安全问答任何一个环节变化脚本就要跟着改。更麻烦的是模拟登录本质是在“替用户操作”一旦站点开了用户组发帖审核脚本发出来的帖子还是会进待审列表等于白做。我只在调试老项目时用过这条路的轻量版生产环境从没敢依赖过它。第二条路叫直接入库。插件在 Discuz 项目里开一个 PHP 入口外部系统带着内容数据和令牌请求这个入口插件解析后把数据组织成 Discuz 的表结构用 Discuz 自身的 DB 类写入。这个方案绕过了登录、验证码、防灌水这些前端约束速度上一条帖子加一条回帖大概几十毫秒。代价是你必须自己补上 Discuz 发帖时自动算好的统计字段、缓存和审核状态这部分做不好就是翻车现场。我一般会按这个标准选型如果外部系统能提供结构化的纯文本或者 UBB 内容并且不依赖 Discuz 编辑器里的附件上传流程就选直接入库如果必须把内容完整交给 Discuz 的编辑器、分类模型去处理那可以在插件里调用 Discuz 内部的发帖函数比如论坛模块的submit_thread()。但调用内部函数也算广义入库至少不是走浏览器登录。下面所有代码都围绕“直接入库 调用内部统计函数”这个组合来写因为这是免登录发布最可靠、最可控的路径。2.2 为什么多数场景选直接入库绕开验证码、权限和防灌水很多人一听“入库”就害怕怕漏字段把数据写坏。但真正做过一次就会发现Discuz 发帖链路里 70% 的代码都在处理验证码、权限、防灌水、附件上传、敏感词过滤这些边缘逻辑。这些逻辑在正常人工发帖时是保护在程序化内容同步时就是层层广告跳过审批的墙。直接入库相当于给 Discuz 开了一个后门。后门这个词听着不好听工程上叫“内部发布通道”。你必须自己承担 Discuz 原本自动做的几件事发帖数加一、版块最后一条回复更新、用户主题数加一、搜索索引刷新、门户栏目文章数加一。这些在人工发帖时由 application layer 自动处理我写插件时用一个函数把它们全部补回来。常见的错误是只插了 forum_post 表论坛首页能看到帖子但版块统计清零用户个人页发帖数也不涨。第 3 章会专门写这块代码。也正因为是后门安全责任全在插件作者身上。我的习惯是令牌用 64 位随机字符串并且只允许指定 IP 访问入口每次请求还要带一个签名后面第 5 章避坑会展开讲。免登录不代表裸奔这句话我每次给客户讲方案时都会重复。2.3 插件整体架构路由入口、鉴权中间件、数据映射层写插件之前先把入口文件的结构定下来。我的插件在 Discuz 根目录下放一个free_publish.php它干了三件事。第一初始化 Discuz 环境。Discuz3.4 的环境初始化已经从早期的 includecommon.inc.php换成了class_core.php必须用新的方式。第二做鉴权。接收 POST 请求校验令牌和签名失败直接返回 JSON不往下走。第三做数据映射。外部系统传来的字段比如title、content、category_id要映射成 Discuz 数据表的字段名比如门户扩容字段portal_article_title里的title、uid、category_id。一个最小的入口文件长这样?php // free_publish.php 放在 Discuz 根目录 define(IN_DISCUZ, true); require_once ./source/class/class_core.php; $discuz C::app(); $discuz-init(); // 1. 鉴权令牌必须先比对 $token isset($_POST[token]) ? trim($_POST[token]) : ; if ($token ! CHANGE_ME_64_CHAR_TOKEN) { exit(json_encode([code 403, msg invalid token])); } // 2. 解析内容主体 $payload isset($_POST[payload]) ? $_POST[payload] : ; $data json_decode($payload, true); if (!is_array($data)) { exit(json_encode([code 400, msg bad payload])); } // 3. 路由target 决定走论坛还是门户 $target $data[target] ?? forum; if ($target portal) { // 第 4 章实现 $result free_post_portal($data); } else { // 第 3 章实现 $result free_post_thread($data); } echo json_encode($result);这段代码里的C::app()是 Discuz3.4 标准的应用初始化入口必须在所有数据库操作之前调用。$discuz-init()之后才能安全使用C::t()系列表操作类。你可能会问为什么不直接写一个方法类把所有逻辑封装起来我确实会在后期封装但第一阶段建议先用函数把流程跑通。原因很简单免登录发布这种插件最容易翻车的不是代码结构而是字段错配先用平铺的函数方便你一条条对字段跑通了再重构不迟。令牌字符串我这里写的是占位符上线前必须换成自己的随机值并且绝对不要提交进 Git 仓库。你可以在服务器环境变量里读或者在配置文件里维护。入口文件只做路由和鉴权不做具体入库逻辑这样以后要加其他内容类型比如日志、微社区、群组直接在路由里扩展就行。3. 帖子免登录发布入库从数据表关系到可运行PHP函数3.1 先搞清帖子发布要写哪些表forum_thread 与 forum_post 的关联论坛发帖Discuz3.4 里最核心的是两张表。forum_thread存主题也就是标题、作者、浏览数、回复数、最后回复时间这些汇总信息。forum_post存帖子内容一条主题的原文在这里面之后的每条回帖也在这里面。两张表通过tid关联主题的tid就是回帖的tid而主题原文在forum_post里的first字段是1回帖是0。如果你同时插入多张表顺序不能反。必须先插入forum_thread拿到系统返回的自增tid再插forum_post否则post表里无法关联到tid。这个顺序错了前台看起来就是“帖子不存在”。另外一个常被忽略的是分表问题。Discuz 在配置里可以启用帖子分表比如forum_post_1、forum_post_2同一时间只有一张活跃表。直接用 SQL 插forum_post很容易插进一张不再使用的表。我建议用 Discuz 自带的C::t(forum_post)-insert()而不是原生 SQL因为C::t(forum_post)底层会自动匹配正确的分表省掉很多坑。3.2 核心入库代码用PHP写一个最小可用的发布函数下面是发布主题的最小编码。这个函数接收fid、标题、内容、作者 ID、作者名以及可选字段返回新主题的tid。function free_post_thread($data) { global $_G; $fid intval($data[fid]); $subject trim($data[subject]); $message trim($data[message]); $authorid intval($data[authorid] ?? 0); $author trim($data[author] ?? ); // 防止作者ID无效导致孤儿数据 $member C::t(common_member)-fetch($authorid); if (empty($member)) { $authorid 1; $member C::t(common_member)-fetch($authorid); $author $member[username]; } $timestamp time(); $thread [ fid $fid, typeid intval($data[typeid] ?? 0), sortid intval($data[sortid] ?? 0), author $author, authorid $authorid, subject $subject, dateline $timestamp, lastpost $timestamp, lastposter $author, views 0, replies 0, displayorder 0, status 0, recommend 0, heat 0, ]; $tid C::t(forum_thread)-insert($thread, true); $post [ fid $fid, tid $tid, first 1, author $author, authorid $authorid, subject $subject, dateline $timestamp, message $message, useip $_G[clientip] ?? 127.0.0.1, portable 0, status 0, ]; C::t(forum_post)-insert($post); // 更新版块统计、用户统计这里用 Discuz 自带函数 require_once libfile(function/forum); updateforum($fid); updatepost($authorid); return $tid; }这段代码里有个细节C::t(forum_thread)-insert($thread, true)第二个参数传true表示插入成功后返回自增主键tid。如果你漏掉这个参数函数返回的可能是影响行数那后面forum_post就找不到tid了。updateforum和updatepost是 Discuz 自带的数据统计更新函数分别负责版块的主题数、回复数、最后发帖时间以及用户的主题数和发帖数。很多人自己写代码时喜欢直接UPDATE forum_forum SET threadsthreads1其实不如调用官方函数可靠因为官方函数还同步了版块缓存。useip字段我从全局变量里取拿到的是请求 IP如果入口是 CLI 方式调用的就回退到127.0.0.1。注意$_G[clientip]在 Discuz3.4 里是$_G[clientip]还是$_G[client_ip]我确认过3.4 里是$_G[clientip]。如果你用的是老版本可能会是$_G[client_ip]需要按实际版本调整。3.3 必须处理的字段fid、uid、typeid、message的HTML转换fid是版块 ID外部系统不一定知道 Discuz 内部的fid我一般会在插件里做一个版块映射表外部传来的是“科技”“游戏”“问答”这样的分类名插件里写一个map_forum($name)去查forum_forum表找到对应的fid。这样外部系统不用关心底层表结构以后改名也只在映射表里改。authorid和author一定要一起传。Discuz 的帖子详情页显示作者名用的是forum_post.author而列表页用的是forum_thread.author两个字段都要正确写入。不要只写authorid不写author那样首页能看到标题点进去作者空白。message字段的格式是最容易出问题的。Discuz 默认的帖子内容编辑器是 UBB不是 HTML。如果你从外部 CMS 拿到的内容是带div、p的 HTML直接放进forum_post.message前台会把你整段 HTML 当作代码显示。我处理过最典型的翻车是采集工具把 HTML 标签全丢到帖子里整个帖子看过去全是代码。正确做法是先把 HTML 转成 Discuz 能识别的 UBB或者干脆先做一次标签剥离只保留纯文本。如果确实需要保留格式我建议只保留[b]、[color]这类 Discuz 内置标签用正则把对应的 HTML 标签替换成 UBB 标签。下面是个小示例function html_to_ubb($html) { $html preg_replace(/b(.*?)\/b/is, [b]$1[/b], $html); $html preg_replace(/i(.*?)\/i/is, [i]$1[/i], $html); $html preg_replace(/u(.*?)\/u/is, [u]$1[/u], $html); $html strip_tags($html, br); $html str_replace([br, br/, br /], \n, $html); return $html; }这段转换只覆盖最常用的标签别指望把复杂的排版全转过去。如果要处理表格、图片、代码块建议用成熟的 HTML 转换扩展而不是自己用正则硬拼。转换之后要注意 Discuz 的敏感词过滤人工发帖时系统会拦截敏感词免登录入库绕过了拦截。我一般会调用 Discuz 的censor()函数处理一遍如果命中了敏感词就返回错误让外部系统修改后重新提交而不是带着敏感词入库。4. 门户发布入库表结构差异与两套逻辑的统一4.1 门户和帖子表结构差异一览门户发布和论坛发布在 Discuz 里是两套完全独立的存储结构。论坛以tid为中心门户以aid为中心。下面这张表列出我常用的几个字段对应关系能帮你快速找到映射论坛字段forum_thread / forum_post门户字段portal_article_title / portal_article_content内容IDtidaidportal_article_title.aid所属版块thread.fid栏目portal_article_title.category_id标题thread.subject标题portal_article_title.title正文post.message正文portal_article_content.content作者IDthread.authorid作者IDportal_article_title.uid作者名thread.author作者名portal_article_title.username发布时间thread.dateline发布时间portal_article_title.dateline状态控制thread.status状态控制portal_article_title.status门户的内容存储在portal_article_title和portal_article_content两张表里类似于论坛的thread和post。title表存摘要信息content表把文章正文按段落拆成多行每行有个pageorder字段。如果你只发布一篇没有分页的文章就只插一条content记录即可。4.2 门户发布入库代码标题、内容、作者与缩略图门户发布的核心函数如下我仍然把校验放在入口文件里这里只做入库。function free_post_portal($data) { $catid intval($data[catid]); $title trim($data[title]); $content trim($data[content]); $uid intval($data[authorid] ?? 0); $username trim($data[author] ?? ); $timestamp time(); $member C::t(common_member)-fetch($uid); if (empty($member)) { $uid 1; $member C::t(common_member)-fetch($uid); $username $member[username]; } $art [ category_id $catid, title $title, uid $uid, username $username, dateline $timestamp, status intval($data[status] ?? 0), showstyle 0, cover trim($data[cover] ?? ), allowcomment intval($data[allowcomment] ?? 0), ]; $aid C::t(portal_article_title)-insert($art, true); C::t(portal_article_content)-insert([ aid $aid, uid $uid, username $username, dateline $timestamp, content $content, pageorder 1, ]); // 更新栏目文章数 C::t(portal_category)-update_count($catid, 1); return $aid; }注意portal_article_title表里有默认的showstyle这个字段控制文章显示样式取0表示用默认排版。cover是封面图 URL如果没有就留空字符串。门户文章正文的content字段比较宽松Discuz 门户前台默认支持一定的 HTML 展示这跟论坛不一样。如果你传的是纯富文本 HTML一般能直接显示如果你传的是纯文本建议在外层包一个p标签否则前台可能挤成一团。portal_category的update_count是我写的一个扩展方法因为 Discuz3.4 里栏目分类表并没有内置一个 sprintf 的重统计函数。你可以在类里自己执行一条更新语句也可以直接操作缓存把portal_category相关缓存删除让 Discuz 下次访问时自己重建。如果你不熟悉update_count怎么实现最简单的做法是删除栏目缓存C::t(common_syscache)-delete(portalcategory)前台会自动重建。4.3 将两套发布逻辑合并成一个统一入口入口文件里的路由在第 2 章已经写了一半现在补全。我建议所有外部请求只传一个target参数forum代表发帖子portal代表发门户文章。这样外部系统只需关心自己的业务逻辑不需要知道 Discuz 内部到底有哪几张表。if ($target portal) { $aid free_post_portal($data); echo json_encode([code 0, aid $aid, url portal.php?modviewaid . $aid]); } else { $tid free_post_thread($data); echo json_encode([code 0, tid $tid, url forum.php?modviewthreadtid . $tid]); }这个统一入口有个好处以后如果要支持发群组、发日志只需要继续扩展$target的分支鉴权和日志逻辑完全复用。5. 避坑与排查免登录发布最容易翻车的5个场景5.1 坑一帖子写入成功但前台列表看不到版块里空荡荡现象直接去数据库查询forum_thread能查到新帖子后台也能看到但打开论坛首页版块列表里没有这条新主题。原因Discuz 的版块和主题列表有缓存你绕过正常发帖逻辑写入数据时缓存没有自动失效。另外如果版块开启了“发帖审核”那么forum_thread里modstatus字段需要是1才会出现在待审核里而不是直接显示在版块列表。解决最直接的办法是删除版块缓存让 Discuz 从数据库重新读取。在发布函数最后加一段C::t(forum_forum)-update($fid, [lastpost $timestamp, lastposter $author]); C::t(common_syscache)-delete(forum_ . $fid);第二个办法是检查版块设置。外部系统发布时如果要以“已通过审核”的状态直接显示必须确保目标版块没有开启发帖审核或者在主题里把modstatus设为0。Discuz3.4 的forum_thread.modstatus默认是0表示正常但如果版块开的审核这个字段会被设置为1这时候前台列表就不会显示。我的经验是免登录入库的帖子本质是受信任内容上线前先和站点管理员确认目标版块的审核配置否则你要么接受“入库后还要后台审核”要么在插件里把modstatus强制写成0。但注意强制写成0也会绕过审核机制内容检查一定要在外部系统完成不能把未经审核的内容直接发出去。5.2 坑二帖子发布成功但版块统计数、用户发帖数不增加现象帖子能打开但版块名旁边显示的主题数、回复数不变用户个人主页的发帖数也不变。原因只插了forum_thread和forum_post没有触发 Discuz 的统计更新。人工发帖时updateforum和updatepost会被自动调用免登录入库跳过了这一步。解决在第 3 章的代码里我调用了updateforum($fid)和updatepost($authorid)这两个函数在source/function/function_forum.php里定义所以我在函数里加了一行require_once libfile(function/forum);。如果你发现调用后依然不生效检查一下是否用了函数名updateforumDiscuz3.4 里是这个名字一些老版本的写法是update_forum_stat别搞混。更隐蔽的问题是如果你发布了一个“回复”而不是主题比如给已有主题追加回帖那么需要更新的字段更多包括主题的lastpost、lastposter、replies版块的posts用户的posts。如果你漏掉replies帖子的回复数永远是 0用户还会以为回复没写进去。5.3 坑三门户文章发布后频道页看不到但直链能打开现象通过portal.php?modviewaid123能直接打开文章但门户首页分类下面看不到新文章分类统计里的文章数也没变。原因门户栏目是带缓存的portal_category里记录了每个栏目的文章数和最后更新时间你入库时没有更新它。另一个常见原因是portal_article_title.status字段不是正常值。门户文章 status 为0表示已发布1表示审核中还可能为其他值表示回收站我见过很多人默认写成1结果前台看不到。解决入库后更新portal_category的计数和缓存最简单的方式是直接删除分类缓存C::t(common_syscache)-delete(portalcategory);如果你的门户插件比较多还可以把整个频道的模块缓存也删掉。注意 Discuz 门户页面可能是由后台的“模块管理”生成静态的 HTML 片段的有些页面是定时生成就算你删了缓存也要等下次计划任务更新。这时候别慌去后台更新一下全景缓存再强制刷新首页一般能解决。5.4 坑四免登录接口令牌泄露被刷成了灌水机器现象接入第三天突然多了几百条垃圾帖内容五花八门明显不是你的业务系统发的。原因接口令牌用的是类似123456这种弱值而且入口没有任何 IP 限制。外部系统调用时也有可能出现令牌被放在 URL 参数里日志里被记下来然后被脚本爬走。免登录发布插件的本质是给外部系统开了后门一旦令牌暴露谁都能往数据库里写内容这是最严重的安全事故。解决做三件事。第一令牌换成至少 32 字节以上的随机字符串最好用 PHP 的random_bytes()生成。第二在入口判断请求 IP配置一个允许调用发布接口的 IP 白名单。第三加签名参数外部系统调用时把请求参数和令牌拼接做一次 MD5 或 HMAC 签名入口这边再算一遍比对。用签名而不是裸令牌的优势是令牌不出现在请求内容里哪怕请求被抓包也无法直接重用。function verify_signature($params, $secret) { $sign $params[sign] ?? ; unset($params[sign]); ksort($params); $str http_build_query($params) . $secret; return hash_equals(md5($str), $sign); }调用时外部系统需要按相同规则计算签名这里面的secret就是你的令牌IP 白名单仍然保留。别偷懒宁可配置麻烦一点也不要让接口裸奔。hash_equals可以防止时序攻击这是我坚持用的一个点。5.5 坑五外部系统是UTF-8Discuz是GBK入库后乱码现象标题正常正文一段一段全是问号或乱码甚至整个接口返回 500。原因Discuz 站点字符集为 GBK你的外部系统和文件是 UTF-8 编码直接写入数据库后链接层字符集不匹配导致乱码。很多人在本地测试时整个环境都是 UTF-8一上服务器就出事因为服务器上的 Discuz 可能用的是 GBK 包。解决在入口文件初始化之后检测 Discuz 的字符集然后调用 Discuz 自带的diconv()方法转码。如果外部数据本身就是 UTF-8 而 Discuz 是 GBK则在入库前对所有文本字段做一次转换$to_encoding strtoupper(CHARSET); // 例如 GBK $data[subject] diconv($data[subject], UTF-8, $to_encoding); $data[message] diconv($data[message], UTF-8, $to_encoding);反过来如果 Discuz 是 UTF-8 而外部系统是 GBK也一样用diconv反向转。最保险的做法是让外部系统统一只输出 UTF-8插件内部根据 Discuz 实际字符集动态转换而不是在外部系统写死。我在第 2 章入口的代码里故意没有加转码就是提醒你这一步很容易漏。6. 把插件做成可维护的长期方案日志、验证与二次开发接口插件能跑不算本事能长时间不出事才算。我最后分享三个习惯都是被生产环境毒打后的经验。第一每次请求写日志。我在入口文件的Publish操作前后各记一条日志内容包含请求来源、目标类型、传入的主题标题、返回的tid或aid、耗时。不需要记录完整 payload那会导致日志里全是文章正文又大又难翻。用一句话记录核心信息就够。出问题时先查日志里的返回码再对比数据库记录半小时就能定位问题。第二上线前做一次链路验证。测试帖子要点开详情页、列表页、版块统计、用户页确认数据完整。测试门户文章要验证首页列表、分类页、上一篇/下一篇导航。我一般是先发一条带特殊标记的测试帖比如标题里加__TEST__发布后过几分钟再发一条回帖然后检查forum_thread.replies是否更新成1。这条链路走通插件基本就稳了。第三预留二次开发入口。我把今天写的free_post_thread和free_post_portal提到一个公共类里类名可以叫FreePublish入口文件只是做鉴权后调用类的方法。以后想增加“定时发布”“自动生成摘要”“同步附件”只需要在类里加方法不用动入口逻辑。我习惯给这个类加上一个before_publish和after_publish的钩子这样外部系统也能在发布前后做自己的处理。这个设计我是从很多开源插件的过滤器里学来的比自己定义一个无扩展点的函数要灵活得多。哦对了还有一个小技巧回滚误发布。免登录入库没有“后悔药”删帖子要自己实现。我会在插件里加一个隐藏的actiondelete参数传入正确的tid或aid后调用 Discuz 自带的删除函数。删除比发布更危险所以这个接口我会加双重令牌校验绝不能沿用发布用的令牌。这套方案上线到现在我们用同一套代码同时解决过论坛和门户两种内容同步外部系统只需要改一个target参数就能选择内容去向。你现在接手这个标题时最该记住的一句话是免登录发布的本体不是写入而是补全 Discuz 在人工发帖时自动完成的那些统计、缓存和状态。把这些补全了你甚至不需要关注别人的插件卖多少钱自己维护一套更放心。希望帮到你。本文还有配套的精品资源点击获取