简介:一套基于PHP 7.2.3的树型论坛源码包,内置采集顶帖机,面向需要快速搭建社区论坛并实现内容自动更新的站长、个人开发者或运维人员。全包共计一千四百九十七个文件,压缩后大小约八点五五兆,以PHP、超文本标记语言、JavaScript、CSS等前后端代码为主,辅以海量GIF、PNG、JPG图片素材,另含ASPX、SQL、XML等辅助文件,可完整支撑论坛部署与二次开发。当前已有二百八十六人学习下载,适合有一定PHP基础、希望低成本获得论坛系统及自动化运营工具的读者。包内不仅包含论坛核心页面与数据库脚本,还提供后台管理模块,如开奖号码更新、自动顶帖设置、管理页等,并附有相应的后台逻辑代码,便于理解采集与顶帖机制并做定制修改。目录结构清晰,源码完整,既可本地安装调试,也可直接用于生产环境。
1. 树型论坛 + 采集顶帖机:PHP 7.2.3 的组合为什么还值得部署
“php7.2.3树型论坛,带采集顶帖机,完整给大家”这套资源,表面看是十年前的旧代码,实际解决的是今天很多小团队仍会遇到的三个问题:讨论如何按层级呈现、内容从哪来、板块怎么保持活跃。树型论坛用“回复套回复”的结构把问答和排查过程保存下来,比平铺列表更能还原上下文;采集顶帖机负责把外部话题源抓回本地,再按节奏刷新板块排序,让一个无人值守的社区看起来始终有人在维护。我帮某公司的内部知识库做过类似部署,结论很明确:一台 1 核 2G 的旧服务器,跑这套组合比跑任何新框架都轻松。它适合个人站长、企业内部社区维护者,也适合想研究老 PHP 项目改造的开发者。
2. 树型数据结构怎么选:邻接表、嵌套集与混合方案的取舍
树型论坛和普通论坛的根本区别在数据库。普通论坛的回复是平铺列表,只需要一个主题 id 就能把回帖全查出来;树型论坛要求展示“回复的回复”,每一层都要支持展开和收缩。如果数据结构选错,前期看不出来,帖子量上万后性能会迅速崩掉。这一章把三种常见方案讲透,并给出我实际部署时的选择。
2.1 邻接表:用 parent_id 建模,最简单也最容易忽略索引
邻接表的思路非常直接:每一条帖子记录都带一个 parent_id 字段,指向自己的父帖,根主题的 parent_id 为 0。这是树型论坛最常见的实现,也是“采集顶帖机”这类辅助脚本最容易配合的结构,因为所有更新都集中在单行。建表 SQL 如下:
CREATE TABLE `post` ( `id` int(11) NOT NULL AUTO_INCREMENT, `topic_id` int(11) NOT NULL COMMENT '主题根帖ID', `parent_id` int(11) NOT NULL DEFAULT '0' COMMENT '父帖ID,0为根主题', `user_id` int(11) NOT NULL COMMENT '发帖人ID', `content` text NOT NULL COMMENT '正文', `created_at` datetime NOT NULL COMMENT '发帖时间', `updated_at` datetime NOT NULL COMMENT '最后更新时间', `sort_weight` int(11) NOT NULL DEFAULT '0' COMMENT '顶帖权重', PRIMARY KEY (`id`), KEY `idx_topic_parent` (`topic_id`, `parent_id`), KEY `idx_created` (`created_at`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;这段 DDL 里最容易漏掉的是复合索引idx_topic_parent。如果只给 parent_id 建单列索引,当你要查某个主题下全部帖子并按父帖分组时,MySQL 要做两次索引回表,数据量上万之后明显变慢。把 topic_id 和 parent_id 放进同一个索引,是为了让“按主题查整个树”和“按父帖查子节点”这两类高频查询都走索引。另一个细节是 sort_weight 字段,它专门给顶帖机留了一个排序入口,后面第 4 章会详细讲它的用法。
邻接表的短板也很明确:要拿“某个节点下面所有后代”,必须递归查询或多次查库。一个三层深的回复树,在代码里就是三层循环查库。几千条帖子时无所谓,几十万条时就会出现性能问题。因此社区里常有人说邻接表只适合“浅树”。我的判断是:对中小论坛来说,单主题下回复通常不超过几百条,邻接表加递归完全够用,不必为了理论上的优雅去上复杂模型。
2.2 嵌套集模型:左右值区间一次查出整棵子树
嵌套集模型的核心是给每个节点分配两个整数:左值 lft 和右值 rgt。建树时对整棵树做深度优先遍历,进入节点时左值加一,离开节点时右值加一。这样任意节点的左右值之间,正好包含它所有子孙节点的左右值。查询整棵子树只需要一条 SQL:
SELECT id, content FROM post WHERE lft BETWEEN 2 AND 11 ORDER BY lft ASC;这条语句一次性带回区间内全部节点,天然按深度优先顺序排列,渲染时直接缩进即可。嵌套集的查询性能确实漂亮,无论树多深,一次查询搞定。但代价在写入:插入一个新节点,要把它之后所有节点的 lft、rgt 都加 2,这个更新范围随帖子总量增长,可能一次插入更新几千行。论坛偏偏是写入频繁的场景,用户每发一帖、每回一帖都是写入。我身边有开发者把嵌套集用在论坛上,高峰时段数据库锁等待明显上升,最后换回邻接表才消停。嵌套集更适合“结构固定、几乎不插入新枝叶”的知识库,而不是开放讨论的论坛。
两种模型的取舍可以用一张表格概括:
| 对比项 | 邻接表 | 嵌套集 |
|---|---|---|
| 查询整棵子树 | 递归多次查库 | 一次范围查询 |
| 插入/回复成本 | 低,只加一行 | 高,要更新整个区间 |
| 实现难度 | 容易理解 | 需要维护左右值 |
| 适合场景 | 论坛、评论区 | 知识库、目录树 |
核心矛盾是:查询希望一次拿全,写入希望代价低,两个诉求在树型结构里天然互斥。选择哪种模型,本质上是在问自己的业务读多还是写多。
2.3 混合方案:邻接表 + path 字段,中小论坛最省心
我实际采用的是邻接表加一个 path 字段。path 保存从根到当前节点的完整路径,用点号分隔。根节点 id=1,它的子节点 id=5 的 path 是1.5.;孙节点 id=9 的 path 是1.5.9.。建表时补一列:
ALTER TABLE `post` ADD COLUMN `path` varchar(255) NOT NULL DEFAULT '' COMMENT '根到自身的路径,用点号分隔';写入新回复的完整流程分三步:查出父帖的 path,拼上父帖 id,再连同正文一起插入。代码如下:
<?php $stmt = $pdo->prepare('SELECT path FROM post WHERE id = ?'); $stmt->execute([$parentId]); $parentPath = $stmt->fetchColumn(); $newPath = $parentPath . $parentId . '.'; $pdo->beginTransaction(); $pdo->prepare( 'INSERT INTO post (topic_id, parent_id, user_id, content, path) VALUES (?, ?, ?, ?, ?)' )->execute([$topicId, $parentId, $userId, $content, $newPath]); $pdo->commit();这里我先把 path 算好再插入,避免插入后再回填的多余 UPDATE。但前提是父帖的 id 已经存在。如果父帖是本轮刚插入的,你要先插父帖、取回lastInsertId(),再回填子帖 path,两步都要放在同一个事务里,否则中途报错会导致 path 为空,树形显示断层。
有了 path 字段,查询子树就变得非常直接:
SELECT * FROM post WHERE path LIKE '1.5.%' ORDER BY path ASC, created_at ASC;这比递归查询稳定,写入成本又远低于嵌套集。虽然 LIKE 前缀查询无法走普通索引的完全匹配,但因为前缀固定,MySQL 会做 range 扫描,万级数据量下性能完全够用。这也是我把它作为中小论坛默认方案的原因:既有邻接表的写入效率,又有接近嵌套集的查询便利。
树形渲染的 PHP 代码也很简洁,用递归函数按父帖 id 逐层拉取:
<?php function renderTree($pdo, $parentId, $level = 0) { $stmt = $pdo->prepare('SELECT * FROM post WHERE parent_id = ? ORDER BY created_at ASC'); $stmt->execute([$parentId]); foreach ($stmt->fetchAll() as $row) { echo str_repeat(' ', $level) . htmlspecialchars($row['content']) . "\n"; renderTree($pdo, $row['id'], $level + 1); } }这个渲染函数每一层只查一次库,子树深度等于查库次数。对中小论坛来说足够,真要优化,就把 path 字段也查出来,用一次 LIKE 查询把整棵树取回内存再排序渲染,那就是后话了。
3. 部署到 PHP 7.2.3:环境兼容性、Web 配置与 PDO 连接
拿到声称“完整”的资源包后,真正的挑战往往不是代码,而是让这套老代码在今天的操作系统上跑起来。PHP 7.2.3 官方生命周期早已结束,主流镜像源基本删除了二进制包,部署必须围绕兼容性展开。本章按环境、Web 服务、数据库连接三块讲。
3.1 老版本 PHP 的兼容性边界:扩展、MySQL 8 与 Docker 兜底
第一步先说结论:如果你手头没有已经装好的 PHP 7.2 环境,最常见的做法是用 Docker 拉取php:7.2.3-apache镜像,而不是去系统源里硬装。因为旧版的依赖包和今天的新版本库大概率对不上,硬装往往会陷入“缺一个依赖装一个,装完又发现版本冲突”的循环。Docker 的最小环境如下:
FROM php:7.2.3-apache RUN docker-php-ext-install pdo_mysql mysqli构建并启动:
docker build -t php723-forum . docker run -d -p 8080:80 \ -v /opt/forum:/var/www/html \ --name forum723 php723-forum-v把宿主机上的代码目录挂载进容器,改代码不需要重建镜像。挂载目录的权限默认继承宿主机,要保证容器内的 PHP 进程能读取。这里有一个几乎必踩的兼容性坑:PHP 7.2 连接 MySQL 8 时,会因为认证插件不兼容而失败。MySQL 8 默认使用caching_sha2_password,PHP 7.2 的 mysqlnd 只认旧协议mysql_native_password。解决办法是把连接账号的认证方式切回旧协议:
ALTER USER 'forum_user'@'localhost' IDENTIFIED WITH mysql_native_password BY 'password';改完无需重启 MySQL,但已有连接要重新建立。如果你连的是云数据库,账号的 host 段可能不是localhost而是%,要按实际来源改对。我在部署时还遇到过一次数据库版本更老的场景,直接换用 MySQL 5.7 镜像,认证问题就不会出现,代价是放弃了 MySQL 8 的新特性。取舍标准很简单:如果项目只做内容社区,不需要窗口函数和 JSON 特性,MySQL 5.7 反而是更稳妥的底座。
3.2 Web 服务配置:Nginx rewrite 与 Apache .htaccess 的选择
老论坛项目通常自带 .htaccess,说明它默认跑 Apache。Apache 环境最简单,只要开启 mod_rewrite,并允许目录配置覆盖即可。关键配置如下:
<Directory /var/www/html> AllowOverride All Require all granted </Directory>如果换成 Nginx,必须把 .htaccess 的规则手动翻译成 location 块。大多数老项目的 URL 规则可以收敛成“所有非真实文件路径都交给 index.php”。我的最小配置如下:
server { listen 80; server_name forum.example; root /var/www/html; index index.php; location / { try_files $uri $uri/ /index.php?$query_string; } location ~ \.php$ { include fastcgi_params; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; fastcgi_pass 127.0.0.1:9000; } }try_files是这里的灵魂:依次尝试真实文件、真实目录,最后回退到 index.php。这样forum.example/post/123才能被 PHP 入口接收,由代码解析出帖子 id。fastcgi_pass必须对应实际 PHP-FPM 的监听方式,如果用 Unix socket,则写fastcgi_pass unix:/run/php/php7.2-fpm.sock;。
遇到问题时的排查顺序我建议固定下来:502 看 socket 路径,404 看 rewrite,500 看 PHP 错误日志。很多人一遇到 502 就怀疑代码,实际上绝大多数情况是 socket 路径写错,去php-fpm.conf里确认 listen 值即可。用这个顺序排错,能省掉一半的瞎折腾。
3.3 连库与字符集:PDO 参数和 utf8mb4 的显式声明
老代码里常见的mysql_connect函数在 PHP 7 里已被删除,部署时必须全部替换成 PDO 或 mysqli。我建议统一用 PDO,原因有两个:预处理语句更利于防注入,顶帖机批量写入时也更简洁。连接模板如下:
<?php $dsn = 'mysql:host=127.0.0.1;port=3306;dbname=forum;charset=utf8mb4'; $pdo = new PDO($dsn, 'forum_user', 'password', [ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, ]);charset=utf8mb4必须写进 DSN。PHP 7.2 的 mysqlnd 默认字符集是 latin1,如果你建的表是 utf8mb4,连接层不声明的话,中文数据取出来就会变成乱码。这个问题的迷惑性在于:数据库里的数据是好的,命令行查询也正常,唯独 PHP 输出到页面时全是问号。我当初也以为是表建错了字符集,反复改表结构都没用,最后加了一行 charset 参数才解决。这类“数据没坏但显示乱”的问题,通常不在存储层,而在连接层编码丢失,记住这句话能省半天排查时间。
部署前还可以做一个快速自检清单:
| 检查项 | 预期结果 |
|---|---|
| php -v 版本号 | 7.2.3 或 7.2.x |
| php -m 扩展列表 | 包含 pdo_mysql、mysqli、dom |
| 数据库连接测试 | PDO 能正常 select 中文 |
| 伪静态地址 | 帖子详情页可打开 |
这四步全过,环境基本就稳了。剩下的问题大概率出在业务逻辑和采集规则上。
4. 采集顶帖机三块核心逻辑:抓取、权重排序与定时触发
这一章是整个资源包里最有含金量的部分。“采集”负责把外部内容抓回来变成话题,“顶帖”负责把旧帖子重新顶到列表前面。拆开讲是三块逻辑:内容采集、权重排序、定时任务。
4.1 采集模块:DOM 解析、去重与反反爬的最小写法
采集模块的目标是把外部内容源抓成本地草稿,再经审核或自动规则形成论坛话题。任务拆成三步:拉取 HTML、提取标题正文、按唯一键去重。老项目常用正则完成,但真实网页的标签嵌套错乱时常让正则翻车,我更推荐 DOMDocument。一次最小采集过程如下:
<?php $ctx = stream_context_create([ 'http' => [ 'timeout' => 10, 'header' => "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64)\r\n" ] ]); $html = file_get_contents($sourceUrl, false, $ctx); $doc = new DOMDocument(); libxml_use_internal_errors(true); $doc->loadHTML('<?xml encoding="utf-8" ?>' . $html); $xpath = new DOMXPath($doc); $titleNode = $xpath->query('//h1[@class="article-title"]')->item(0); $bodyNode = $xpath->query('//div[@class="article-content"]')->item(0); if ($titleNode && $bodyNode) { $title = trim($titleNode->textContent); $body = trim($bodyNode->textContent); $hash = md5($title . $body); $check = $pdo->prepare('SELECT id FROM draft WHERE content_hash = ?'); $check->execute([$hash]); if (!$check->fetch()) { $pdo->prepare('INSERT INTO draft (title, content, source_url, status) VALUES (?, ?, ?, 0)') ->execute([$title, $body, $sourceUrl]); } }这里有两处容易被忽略的处理。第一是libxml_use_internal_errors(true),真实网页 HTML 几乎都不符合规范,不加这行 DOMDocument 会输出一堆 warning,严重时直接中断脚本。第二是$ctx里的 User-Agent,很多目标站会拦截没有 UA 的请求,加上常规浏览器的 UA 能通过大部分基础防护。
如果加了 UA 仍然抓不到内容,打开浏览器开发者工具看目标站真实 DOM,用元素面板复制 XPath 再回填代码。这个循环是采集器的常态:目标站改版一次,规则就要跟着修一次。因此我建议把 XPath 规则集中写进配置文件,不要散落在业务代码里。另外,如果目标站返回的是 JSON 接口而不是 HTML,直接用json_decode处理即可,不必强套 DOM。判断依据很简单:右键查看网页源码,如果看到的是 JSON 字符串,就走数组解析路径。
4.2 顶帖触发:冷却时间、权重衰减与死循环防护
顶帖机绝不是把updated_at刷成当前时间那么简单。无差别刷新会让首页全是同一批内容,排序失去意义。更合理的做法是引入冷却时间和权重衰减两个概念。冷却时间表示一个帖子距上次顶帖至少间隔多久才能再被顶;权重衰减让每次顶帖对排序的影响逐渐减小,避免脚本反复把同一帖子顶在首页。
一段可运行的示例逻辑:
<?php $stmt = $pdo->query( "SELECT id, sort_weight FROM post WHERE last_bump_at < DATE_SUB(NOW(), INTERVAL 30 MINUTE) AND status = 1 ORDER BY sort_weight ASC, last_bump_at ASC LIMIT 10" ); $update = $pdo->prepare( "UPDATE post SET sort_weight = sort_weight + 1, last_bump_at = NOW(), updated_at = NOW() WHERE id = ?" ); foreach ($stmt->fetchAll() as $post) { $update->execute([$post['id']]); }注意这里的排序方向:我让 sort_weight 越小越靠前,每次顶帖把权重加 1,被顶次数多的帖子逐渐失去排序优势。如果你把方向写反,顶帖机会专门挑权重最高或最低的帖子反复操作,首页列表会越跑越歪。这个方向问题不报错、不提示,唯一排查途径是看日志里 sort_weight 的变化趋势。
另一个不可忽略的点是LIMIT 10。批量更新没有上限的脚本,在帖子多时会一次性扫过全表,把所有列表刷新成同一时间点,这是线上事故的常见起点。给所有批量更新加 limit,是我处理这类脚本的底线原则。冷却时长 30 分钟是保守值,板块少可以放到 60 分钟,板块多帖子量大也可以缩到 10 分钟。调参标准只有一个:首页每次刷新要有新内容,但不能被脚本刷成广告墙。
4.3 定时任务:crontab 的拆分执行与日志隔离
采集和顶帖的节奏不同,不建议写进同一个脚本。采集适合低频,每小时一次足够;顶帖涉及排序更新,频率稍高但要避开高峰。在 crontab 里拆开是常见做法:
# 每小时第 5 分钟执行采集 5 * * * * /usr/bin/php /var/www/html/cron/collect.php >> /var/log/forum_collect.log 2>&1 # 每小时第 25 分钟执行顶帖 25 * * * * /usr/bin/php /var/www/html/cron/bump.php >> /var/log/forum_bump.log 2>&1第一处细节:路径必须写绝对路径。crontab 执行环境的 PATH 和登录 shell 不同,不写绝对路径时经常出现“脚本明明能跑,但 cron 里就是无声失败”。第二处细节:两条任务要隔开时间,避免采集和顶帖同时抢数据库资源。对低配服务器,这个 20 分钟缓冲很重要。
日志分开写是必须的,>> /var/log/forum_collect.log和>> /var/log/forum_bump.log各自独立。两个任务混用一个日志文件时,出问题很难分清是采集失败还是顶帖失败,等于给自己挖坑。我每周检查一次两个 log 的大小,如果某个日志连续几天没有新增,用ps aux | grep php查看是否有残留进程,通常是脚本卡死在某个请求上。
cron 参数可以按下表调整:
| 参数 | 采集任务 | 顶帖任务 |
|---|---|---|
| 执行频率 | 每小时一次 | 每 30 分钟一次 |
| 单次数量上限 | 建议 20 条 | 建议 10 帖 |
| 失败重试次数 | 2 次 | 不重试 |
| 日志保留时间 | 7 天 | 7 天 |
这两个数字不是死的。我见过一个偏内容型的社区,采集改成每两小时一次,顶帖保持 30 分钟,效果反而更好,因为目标站更新本来就不频繁。
5. 运行中最常见的 5 个坑:现象、原因与排查步骤
5.1 采集结果一直为 0:目标站反爬与页面结构变更
现象:采集脚本能执行、不报错,但草稿表始终没有新记录。
原因:两种情况最常见。一是目标站加了基础反爬,比如校验 User-Agent 或限制单 IP 访问频率;二是目标站改版,原来匹配的 XPath 定位路径失效。
解决:先加 UA 和超时,再不行就打开目标页面用开发者工具检查真实 DOM,更新 XPath。如果访问频率被限制,就降低采集频率,两次采集间隔至少 10 分钟。到这一步,不需要急着上复杂手段,论坛采集目标通常是公开资讯页,合理 UA 加频率就够了。经验法则是:先确认“能不能抓到”,再谈“抓得多快”。把目标 HTML 保存到本地再测试 XPath,比反复改线上代码效率高很多。
5.2 PHP 7.2 连不上 MySQL 8:认证插件不匹配
现象:PDO 抛异常,报错信息出现caching_sha2_password或Authentication method unknown。
原因:PHP 7.2 的 mysqlnd 驱动实现的是旧认证协议,而 MySQL 8 默认启用新认证插件。
解决:给指定连接账号切换认证方式,SQL 在第 3.1 小节给过。这里补充一句:如果你用了连接池或长连接,改完认证方式后必须重启 PHP-FPM 或清理持久连接,否则旧连接还带着旧状态,看起来就是“改了没生效”。我当时连续执行了三次 ALTER USER 都没效果,最后重启 PHP-FPM 才恢复正常。这个问题在长连接场景下最容易误导人,看起来像认证配置没改对,实际上是连接缓存没清干净。
5.3 帖子地址全部 404:伪静态规则未生效
现象:首页正常,点击帖子详情页全部 404。
原因:rewrite 规则没有把post/123这类 URL 交给入口文件。Apache 环境多半是 AllowOverride 没开启;Nginx 环境则是没有配置 try_files,或配置写错了位置。
解决:按顺序查两点。先访问原始动态地址,比如index.php?mod=post&id=123,如果能打开,说明 PHP 逻辑没问题,问题一定在重写层。再看 Web 服务配置,nginx -t验证语法,Apache 则检查 AllowOverride 是否生效。很多 404 的真相是配置改对了但没重启服务,“重启一下就好”这个说法虽然听起来像玄学,实际只是配置未重新加载。
5.4 数据库连接被打满:顶帖任务重复执行与缺少 limit
现象:数据库连接数飙升,线程列表里全是同一个 UPDATE 语句,post 表的last_bump_at出现大量相同时间值。
原因:顶帖脚本没有互斥锁,上一轮还没跑完下一轮 cron 又启动;同时 UPDATE 缺少 limit,单次更新大量行占住连接。
解决:第一,给顶帖任务加执行锁。第二,所有批量 UPDATE 必须带 LIMIT。互斥锁最简单的实现是文件锁:
<?php $lockFile = '/tmp/bump.lock'; if (file_exists($lockFile)) { $age = time() - filemtime($lockFile); if ($age < 3600) { exit("上一轮任务还在运行,退出\n"); } } touch($lockFile); try { // 顶帖逻辑... } finally { unlink($lockFile); }这里用文件修改时间判断任务是否仍在运行,比单纯检查文件存在更可靠。脚本如果异常退出,残留 lock 文件会导致后续任务永远被拦截;加时间判断后,超过 1 小时的残留锁自动失效,相当于一道后悔药。finally 块保证正常退出时清理锁,异常时也尽量释放,避免长期锁死。
5.5 附件上传报权限错误:PHP-FPM 运行用户与目录属主不一致
现象:发文字帖正常,一上传图片就报 “failed to open stream: Permission denied”。
原因:PHP-FPM 默认以 www-data 用户运行,上传目录却可能属于 root 或其他用户,PHP 进程没有写权限。
解决:修改目录属主并设置合理权限:
sudo chown -R www-data:www-data /var/www/html/uploads sudo chmod -R 755 /var/www/html/uploads不要图方便直接给 777。777 会让任何进程都能写目录,一旦 Web 目录出现可执行脚本,攻击者可直接写文件执行命令。正确做法是让目录属主等于 PHP-FPM 运行用户,权限 755 或 775。如果 PHP-FPM 是自定义用户启动的,要改成对应用户。这一个细节能同时解决上传失败和大部分安全问题。
6. 落地最后一步:把采集顶帖与论坛主程序拆成两个进程
系统稳定运行一个月后,最大的隐患往往不是单条功能,而是耦合。采集脚本偶发超时、顶帖脚本消耗 CPU,如果这些动作都嵌套在 Web 请求里,用户的访问就会被连带拖慢。我最后做的事,是把采集和顶帖全部移出 Web 路由,变成独立 CLI 进程。
调整后的目录结构:
/var/www/html ├── index.php # Web 入口,只处理浏览和发帖 ├── app/ │ ├── Forum.php # 论坛核心类,渲染树、发帖回帖 │ └── Db.php # PDO 连接单例 ├── cron/ │ ├── collect.php # 采集脚本,CLI 运行 │ └── bump.php # 顶帖脚本,CLI 运行 └── config.php # 数据库配置、抓取源、冷却时间关键改动是 cron 下的脚本不再走 web 路由,而是直接加载 config.php 和 app/Forum.php,独立执行自己的任务。这样做的收益很实际:访客请求和后台脚本互不阻塞,采集卡住时论坛页面照样秒开。与此同时,命令行调试也变简单了:
php cron/collect.php --dry-run php cron/bump.php --limit 5--dry-run只打印将要采集的内容而不写入数据库,--limit控制单次顶帖数量。这两个参数需要自己实现,实现成本很低,但带来的调试价值很大。没有它们之前,每次调试都要改代码、跑一次、看数据;有了之后,改参数就能反复试,很快能找到合适的冷却时间。
验证拆分是否成功的标准很简单:运行php cron/bump.php --limit 5,观察数据库查询日志,确认它只更新目标字段;然后浏览器连续刷新首页,对比脚本运行前后的页面响应时间。如果页面响应没有明显波动,说明资源隔离已经生效。我最后在 config.php 里保留所有参数的注释说明,包括冷却分钟数、采集间隔、抓取源的 XPath。这些脚本三个月后你可能自己都忘记为什么当初把冷却时间设成 30 分钟,有注释的配置比任何文档都好用。这套方案帮我把一台旧服务器稳稳托住了约 200 人同时在线的讨论场景,CPU 峰值没超过 40%。我最大的教训是:自动化的每一步都要有日志和上限,没有这两样的自动化,迟早会变成事故。希望你在部署里能少踩几个坑,让这套老组合稳稳跑起来。
本文还有配套的精品资源,点击获取