简介:一套模仿百度百科体验的开源百科建站系统,面向需要搭建私有知识库、运营百科类站点或研究百科产品设计的开发者与站长。资源核心为HDWiki,是基于PHP的开源百科建站程序,支持词条创建、搜索、分类与审核发布;附带安装说明.txt和document文档目录,可指导环境配置、数据库初始化与站点定制,部署门槛适中。压缩包采用zip格式,整体约1.95MB,结构紧凑,便于快速获取和部署。目前已有384人学习/浏览,适合作为个人或团队知识管理工具,也可用作学习百科系统数据模型、权限控制与内容审核流程的参考案例。通过实际部署,可打造自有词条库,自定义编辑规则与审核机制,获得接近百度百科的创建、维护和查阅体验,同时积累PHP项目实践中的安装排错与二次开发经验。
1. 仿百科词条系统:为什么“像百度”不是重点,词条底座才是
做过内容型网站的开发者,基本都动过“自己搭一个百科系统”的念头。拿到这套资源,第一反应是它把界面做成了百度百科的样子:顶部搜索框、左侧分类、词条页面的目录锚点、相关词条推荐,确实像。但拆完代码你会发现,“超级模仿”四个字指的是产品形态,不是技术深度——整个系统真正值钱的部分,是它把百科系统最核心的三件事做完了:词条的数据结构怎么设计、分类导航怎么组织、检索和热词怎么联动。
换句话说,这套资源适用的不是想炫技的人,而是想用最快速度拿到一个“能跑、能加词条、能搜、能分类”的百科站点底座的从业者。不管你是给学生做课程设计、给内部团队搭知识库,还是自己练手想理解词条类系统的前后端交互,这套代码都比从零写省下两三天时间。下面拆的是我实际部署和改代码时的记录,含建表、配置、避坑,按步骤走能完整复现。
2. 词条系统的数据建模:表结构、内容模型与检索方案的取舍
2.1 词条主表的结构:category、keyword、summary、content 四个字段是骨架
打开这套系统的 SQL 初始化文件,第一个感受是表结构设计得相当收敛——它不是把百度百科几百个字段搬过来,而是用一张主表加两张辅助表把词条系统的闭环撑起来。主表字段大致是 id、category_id、keyword、summary、content、hits、create_time、update_time,再加一个状态字段。这里面最容易被新手忽略的是 keyword 和 summary 的分工。
CREATE TABLE `article` ( `id` int(11) NOT NULL AUTO_INCREMENT, `category_id` int(11) NOT NULL DEFAULT '0' COMMENT '所属分类ID', `keyword` varchar(100) NOT NULL COMMENT '词条名称', `summary` varchar(255) NOT NULL DEFAULT '' COMMENT '词条一句话摘要', `content` mediumtext COMMENT '词条正文内容', `hits` int(11) NOT NULL DEFAULT '0' COMMENT '点击量', `status` tinyint(1) NOT NULL DEFAULT '1' COMMENT '状态:1显示 0隐藏', `create_time` int(11) NOT NULL DEFAULT '0' COMMENT '创建时间', `update_time` int(11) NOT NULL DEFAULT '0' COMMENT '更新时间', PRIMARY KEY (`id`), KEY `category_id` (`category_id`), KEY `keyword` (`keyword`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;这段建表 SQL 的逻辑重点在于:keyword 字段上建立了普通索引,支撑标题精确匹配和前缀匹配;category_id 上的索引服务于分类页的词条列表查询。hits 字段是给热词排序用的计数,不在查询条件里做运算,这样写更利于索引命中。status 字段建议保留,前台编辑错了词条可以直接下架,不用删数据。
实际使用中有一个容易踩的坑:表名如果是 article,部分框架或主机面板会把 article 当作保留字或冲突名,建议改成baike_entry或baike_article这类带前缀的名字。还有 create_time 用 int 存时间戳,前台展示时要记得用date('Y-m-d', $row['create_time'])转换,这套系统里所有时间字段都是时间戳格式,直接输出会变成一串数字。
2.2 内容模型选型:富文本 HTML 还是 Markdown,两种方案怎么选
这套系统装的是富文本编辑器,提交的内容以 HTML 片段的形式直接存进 content 字段。这么做有一个明显的好处:词条里的标题、加粗、表格、图片样式在详情页直接输出就能渲染,不需要额外解析步骤,实现成本最低。但它也有一个非常现实的问题——富文本提交的内容如果不过滤,XSS 风险是实打实的。
我调试时在内容里插入了一段<script>alert('xss')</script>,详情页直接弹窗。解决办法是在提交端做一层白名单过滤:只保留 p、br、strong、em、h2、h3、ul、ol、li、table、img 这些常用标签,其他标签一律剥掉。过滤函数不复杂,用 strip_tags 配合白名单参数就能做,别直接存用户提交的原始 HTML。
如果你打算把这套系统用在更正式的场景,我建议把内容模型升级为双轨:一种是继续存 HTML,适合编辑人员用富文本编辑器的后台维护;另一种是存 Markdown,前台输出时用 Parsedown 或类似库转 HTML。Markdown 方案的优点是内容更干净,版本对比更直观,缺点是编辑端要换编辑器,对不太熟悉 Markdown 的运营人员有一定门槛。我的习惯是:给技术团队用的知识库系统必须上 Markdown,给普通编辑用的内容站继续用富文本。
2.3 搜索与词条关联:LIKE 查询的边界和 hotword 表的配合方式
这套系统的搜索实现走的是最直接的方案:用户输入关键词后,在 keyword 字段上做 LIKE 查询,同时把 summary 和 content 也纳入匹配范围,后台再用一个热词表把高频搜索词记录下来。搜索页的核心查询大概是这样的:
SELECT id, keyword, summary FROM baike_entry WHERE keyword LIKE '%" . $keyword . "%' OR summary LIKE '%" . $keyword . "%' ORDER BY hits DESC;这里必须说一个性能细节:keyword 字段上虽然建了索引,但 LIKE 查询只要通配符在关键词前面就不会走索引,数据量到几万条以后搜索响应会明显变慢。这套系统里的数据量如果控制在几千条以内,完全没有任何问题;但如果想认真运营一个上万词条的知识库,就得换方案。一个低成本的升级路线是引入全文索引,把 content 也纳入检索范围,后面第六章会专门说怎么改。
hotword 表的设计思路倒是值得保留:每次用户搜索时,把搜索词写进一张独立的表,字段就两个,keyword 和 search_count。如果 keyword 已存在就加一,不存在就插入。词条详情页的“相关搜索”就是从这张表里取 search_count 最高的前十个词。这比直接在日志表里 SELECT COUNT(*) 再 GROUP BY 高效得多,相当于用空间换时间,是典型的热词计数方案。
3. 从压缩包到可访问的站点:环境部署、配置参数与首屏加载路径
3.1 本地环境准备与目录结构:这套系统跑在什么环境里
这套系统是典型的 PHP + MySQL 组合,没有用到框架,入口文件直接摆在根目录,属于那种“解压就能跑”的老派结构。本地测试我建议装集成环境,Windows 上用 WAMP,macOS 上用 MAMP,版本选择 PHP 7.4 或 8.0 都可以,MySQL 5.7 或 8.0 都没问题。PHP 版本不建议低于 7.0,因为旧版本对面向对象和错误处理的支持会让调试变麻烦。
解压后的目录结构很直观:根目录是一个 index.php 入口,docs 或 template 目录里放着模板文件,admin 目录是后台管理,uploads 或 images 目录存上传的图片。这套系统的页面渲染逻辑是老式写法——index.php 里直接接收 GET 参数,用 switch 或 if-else 判断要显示哪个页面,然后 include 对应的模板文件。没有路由层,没有 MVC 分层,但正因为足够直白,用它来学词条系统的工作流程反而更清晰。
3.2 数据库导入与配置文件:config 里的每个参数具体管什么
数据库导入没什么特别之处,用 phpMyAdmin 创建一个数据库,推荐 utf8mb4 字符集,然后导入 SQL 文件即可。配置文件通常是根目录下的 config.php 或 inc/config.php,里面定义了数据库连接信息。我第一次打开这个文件的时候有点意外:它用 define 定义了四个常量,没有用 PDO,而是直接 new mysqli 连接。
<?php define('DB_HOST', '127.0.0.1'); define('DB_USER', 'root'); define('DB_PASS', 'your_password_here'); define('DB_NAME', 'baike_system'); define('DB_CHARSET', 'utf8mb4'); $conn = mysqli_connect(DB_HOST, DB_USER, DB_PASS, DB_NAME); if (!$conn) { die('数据库连接失败: ' . mysqli_connect_error()); } mysqli_set_charset($conn, DB_CHARSET); ?>参数说明:DB_HOST 写成 127.0.0.1 比 localhost 更稳,因为某些环境下 localhost 会触发 socket 连接而不是 TCP 连接,导致连不上。DB_CHARSET 一定要写 utf8mb4,因为词条内容里可能出现生僻字或特殊符号,utf8 只能存三字节字符,遇到四字节字符会报错或直接变乱码。mysqli_set_charset 这行是必要的,不设置的话连接默认字符集会跟随 MySQL 服务端配置,可能和网页端的 UTF-8 不一致,中文就会变成问号。
连接方式这里我建议改成 PDO 做兜底,不是为了炫技,而是因为 mysqli 的预处理写法稍繁琐,很多新手在这个项目里是直接把变量拼进 SQL 的,这是这套系统最大的安全隐患。改 PDO 只需要把连接部分替换掉,查询部分的改动也不大,后面避坑章节会展开说。
3.3 首页和词条列表页的加载路径:从 URL 参数到模板渲染
这套系统的首页加载逻辑非常符合“模仿百度百科”的产品设定:顶部一个搜索框,下面按分类列出词条入口,中间区域放热门词条。整个过程可以描述为:浏览器请求 index.php → 脚本用 GET 参数判断请求类型 → 查询数据库 → 填充模板变量 → include 输出页面。
<?php $action = isset($_GET['action']) ? $_GET['action'] : 'index'; $keyword = isset($_GET['keyword']) ? trim($_GET['keyword']) : ''; if ($action == 'search' && $keyword != '') { // 记录搜索词到 hotword 表 $sql = "INSERT INTO hotword (keyword, search_count) VALUES ('" . $keyword . "', 1) ON DUPLICATE KEY UPDATE search_count = search_count + 1"; mysqli_query($conn, $sql); // 查询词条列表 $sql = "SELECT id, keyword, summary, hits FROM baike_entry WHERE keyword LIKE '%" . $keyword . "%' OR summary LIKE '%" . $keyword . "%' ORDER BY hits DESC"; $result = mysqli_query($conn, $sql); } elseif ($action == 'detail' && isset($_GET['id'])) { $id = intval($_GET['id']); $sql = "SELECT * FROM baike_entry WHERE id = $id AND status = 1"; // 详情页点击量 +1 mysqli_query($conn, "UPDATE baike_entry SET hits = hits + 1 WHERE id = $id"); } else { // 首页:取全部分类和热门词条 $sql = "SELECT * FROM category ORDER BY sort_order ASC"; $sql2 = "SELECT id, keyword, summary FROM baike_entry ORDER BY hits DESC LIMIT 10"; } ?>这段代码里有一个值得注意的动作:搜索词计数用了ON DUPLICATE KEY UPDATE,它要求 hotword 表的 keyword 字段必须是 UNIQUE 索引才能生效。如果导入 SQL 文件后该索引缺失,这条语句会直接报错。所以在测试搜索功能之前,先到 phpMyAdmin 里确认 hotword 表结构,这是最容易被忽略的前置检查。
4. 让词条“可运营”:分类导航、词条编辑与热词权重的联动
4.1 分类表设计与前台导航:一维分类够了,别急着做多级分类
这款系统的分类表设计得比较克制:一张 category 表,字段只有 id、name、sort_order 三个核心字段。sort_order 控制分类在前台的排序顺序,数值越小越靠前。一维分类结构在大多数百科类场景下是够用的,文学、历史、科学、技术、娱乐,一行一个分类,词条归属清晰。多级分类看起来高级,但随之而来的是面包屑导航、层级路径、权限继承这些问题,一套仿百度百科的系统不需要承受这个复杂度,硬加只会把模板逻辑改得复杂,不容易维护。
分类导航在侧边栏的渲染逻辑也很简单:查询全部 category,然后 foreach 循环输出链接。有一个细节值得注意:分类页要展示每个分类下的词条数量,如果不做统计,就得在循环里对每个分类再执行一次SELECT COUNT(*) FROM baike_entry WHERE category_id = ?,这就是经典的 N+1 查询问题。数据量小时无所谓,词条多起来页面会变慢。推荐改法是用一次 JOIN 查询把分类和词条数一起取出来,一条 SQL 解决:
SELECT c.id, c.name, COUNT(a.id) AS entry_count FROM category c LEFT JOIN baike_entry a ON a.category_id = c.id GROUP BY c.id ORDER BY c.sort_order ASC;这样前台循环时直接用 entry_count 字段,不再重复连数据库。left join 保证没有词条的分类也能显示出来,数量显示为 0,这对运营侧很重要——空分类一眼就能看出来,方便及时补充内容或者隐藏分类。
4.2 词条发布与编辑流程:后台表单的字段映射与提交逻辑
后台的词条编辑页是这套系统操作频率最高的页面。表单字段和数据库字段一一对应:词条名、分类下拉框、摘要输入框、正文富文本编辑器、状态开关。提交时的核心处理逻辑需要在 admin 的 save_entry.php 里看完,我拆的时候发现它的处理顺序是:接收 POST 数据 → 过滤关键词 → 校验分类 → 拼接 SQL → 执行插入或更新。
<?php $id = isset($_POST['id']) ? intval($_POST['id']) : 0; $category_id = intval($_POST['category_id']); $keyword = trim($_POST['keyword']); $summary = trim($_POST['summary']); $content = $_POST['content']; $status = isset($_POST['status']) ? 1 : 0; // 白名单过滤富文本内容 $allowed_tags = '<p><br><strong><em><h2><h3><ul><ol><li><a><img><table><tr><td>'; $content = strip_tags($content, $allowed_tags); if ($id > 0) { $sql = "UPDATE baike_entry SET category_id=$category_id, keyword='$keyword', summary='$summary', content='$content', status=$status, update_time=" . time() . " WHERE id=$id"; } else { $sql = "INSERT INTO baike_entry (category_id, keyword, summary, content, status, create_time, update_time) VALUES ($category_id, '$keyword', '$summary', '$content', $status, " . time() . ", " . time() . ")"; } mysqli_query($conn, $sql); ?>注意这段代码里的 strip_tags 白名单:img 标签的 src 属性没有做协议校验,这意味着理论上用户仍可以提交javascript:协议或外部图片链接,大部分浏览器不会再执行 javascript 协议,但既然要做内容安全,顺手加固更稳妥。用 preg_replace 把所有属性剥掉,只留 src 和 alt,是更不用担忧的写法。
4.3 热词权重与点击量联动:同样一个字段,两种触发方式
词条详情页的右侧会展示“热门词条”或者“相关词条”,这个功能把两个数据源联动了:一个是词条表的 hits 字段,另一个是 hotword 表的 search_count 字段。词条 hits 负责展示高频阅读内容,hotword 负责展示用户真实搜索的词。两者互有重叠但定位不同:前者衡量内容价值,后者衡量需求入口。
在实现层面,这套系统有一点做得比较合理:详情页每次被访问,就在一个 UPDATE 语句里执行hits = hits + 1,然后用 SELECT 读数据。这个顺序很重要——先加一,再查询,显示器上的阅读数才会实时刷新。如果先 SELECT 再 UPDATE,当前页面的显示永远是上一次的数值,刷新两次才会更新,这算是一个不大不小的逻辑顺序坑。
热词表展示时有一个真实的排查场景:后台管理页能看到 search_count 整个数字,但前台显示的是“热度条”或排行编号。我一开始以为前台有 JS 在做热度换算,查完才发现没有 —— 前台直接按 search_count 排序输出,取前十条。也就是说:热度条是纯数据库排序的结果,不存在权重算法。这是好事,简单直接,适合项目初期。如果你想做更真实的“热度曲线”,再引入时间衰减因子,比如search_count / POW(2, (NOW() - last_search_time) / 86400),那就是另一个层面的工作了。
5. 避坑:从空词条到乱码,五个常见问题与排查记录
5.1 页面全部乱码,词条内容里的中文变成问号
刚部署完这套系统,最容易遇到的就是页面显示正常,但词条内容里的中文全部变成一串问号。原因通常是两个层面:连接字符合集没有设置成 utf8mb4,或者数据表本身的字符集建表时选成了 latin1。要先看 phpMyAdmin 里表结构的字符集是 utf8mb4 还是 latin1,再确认 config.php 里的 DB_CHARSET 是否生效。如果在浏览器端用开发者工具查看页面头,Content-Type 里声明的 charset 也要一起检查。解决顺序是:先改数据库表字符集为 utf8mb4,再保证 config.php 里执行了 mysqli_set_charset,最后确认页面文件本身的 meta 声明。三步都对齐后,乱码就不会出现。
5.2 新增词条后列表页不显示,直接 404
新增完一个词条,从前台怎么都搜不到。排查的时候先看后台列表有没有这条记录,如果有,多半是 status 字段的默认值问题。管理后台勾选了“显示”但没传值,PHP 端就把 status 写成了 0,前台查询条件是 status = 1,自然找不到。解决方法是给编辑表单的 status 控件加一个默认值,或者接收时用isset($_POST['status']) ? 1 : 0这种写法兜底。其次要留意分类 ID 是否为空 —— 如果下拉框没选中任何分类,category_id 为 0,分类页列表也查不到,但直接搜索又是能搜到的。
5.3 搜索功能报 SQL 错误:Unknown column ‘keyword’ in where clause
这个报错有意思,因为它指向的不是搜索逻辑本身,而是 hotword 表。搜索记录写入热词表时执行了INSERT INTO hotword ... ON DUPLICATE KEY UPDATE,如果 hotword 表的 keyword 字段没有 UNIQUE 索引,MySQL 就会报错。解决方式是进 phpMyAdmin 给 hotword 表的 keyword 字段加上唯一索引。顺带检查一下 hotword 表名是否和全局搜出来的表名完全一致——这套系统的 SQL 文件里每个建表语句你都要亲眼扫一遍,不能默认它一定存在。
5.4 富文本提交的内容显示时丢失段落和图片
这个问题出在提交端的 strip_tags 白名单过滤上,处理过的内容里经常把class="content"这段 HTML 里的样式标签给剥掉了,导致段落全挤在一起没有空行。另一个常见原因是 p 标签里嵌套了 div,而白名单里没有 div,嵌套结构被拆掉,浏览器渲染时样式错乱。解决方式是把 div 加进白名单(如果你要保留复杂排版),或者要求编辑统一用纯 p 标签排版。我建议后者,结构更干净,也更符合百科词条的阅读习惯。
5.5 后台能进,但前台点击词条进详情页报错
详情页报错信息通常指向 SQL 语句里的 id 不是数字,比如直接在 URL 后面加一个字符串参数。这套系统的详情页查询偶尔没有做 intval 强制转换,字符串拼进 SQL 会报 1064 语法错误。解决方法是所有从 $_GET 拿到的数值参数必须过一遍 intval 再进 SQL。这也算顺带提醒:别以为前台是只读页面就放松警惕,详情页同样需要防注入。我后面在 localhost 测完,准备上线前给所有接收参数的地方统一加了 intval 和 mysqli_real_escape_string,这一步是必须做的。
6. 验证与进阶:跑通一轮“词条全生命周期”演练,再加两个扩展
6.1 用一条真实词条走完闭合流程
我的习惯是拿到系统后不要直接用假数据,建一个真实词条走完闭环。它的流程是:后台添加一个词条,归属到“技术”分类,内容里包含三段带小标题的结构化文字,状态设为显示。然后再回前台,先搜索这个词条,确认能搜到,再进入详情页,确认正文渲染正确、阅读数从 0 变 1,最后到分类页确认这个词条出现在对应分类列表里。都通过,就说明核心链路是通的。
6.2 扩展一:把 LIKE 搜索升级为全文索引
词条数量到万级以后,后台管理页的全局搜索就会变慢。升级方案是把 baike_entry 表的 content 字段加上 FULLTEXT 索引,把搜索 SQL 改成 MATCH AGAINST:
ALTER TABLE baike_entry ADD FULLTEXT INDEX ft_content (keyword, summary, content); -- 查询改为: SELECT id, keyword, summary FROM baike_entry WHERE MATCH(keyword, summary, content) AGAINST('搜索引擎优化' IN NATURAL LANGUAGE MODE);全文索引对中文的支持依赖分词,MySQL 内置的全文解析器对中文分词效果一般,需要在测试环境验证排序是否准确。如果排序不理想,再考虑引入专业分词组件。上了全文索引后不要保留原来 LIKE 写法作为回退,两者并存会出现结果集不一致,反而让维护者迷惑。
6.3 扩展二:词条版本记录,给内容留后悔药
内容型系统最让我放心不下的不是上线时的功能缺失,而是编辑错误导致词条不可逆损坏。加一个 entry_history 表,把每次编辑前的 content 快照存下来,用 entry_id 关联主表。实现不复杂,在保存更新的代码里多写一行 SELECT,把旧内容 insert 到历史表,前台不展示,后台列表页加一个“历史版本”链接即可。这一招花不了半小时,但能给运营同事省下大量返工时间,我从那以后每个内容类项目都会强制自己先加版本记录再谈其他功能。希望帮到你。
本文还有配套的精品资源,点击获取