简介这是一套仿百度百科的百科建站资源核心为HDWiki基于PHP的开源百科系统可快速搭建知识分享与协作编辑平台适合个人站长、PHP二次开发者及需要建设团队知识库的用户。相比从零开发它能节省大量框架与后台工作并内置词条创建、审核、分类和检索等常见百科流程。压缩包约1.95MB主要文件类型包括安装引导文档、HDWiki程序代码以及使用/API文档覆盖环境配置、数据库和Web服务器部署环节便于对照操作目前已有384人浏览学习。通过这套资源可以学会HDWiki的安装配置与日常维护以此为基础仿照百度百科打造自己的在线词库开源方案也便于后续定制模板、扩展功能适合作为学习百科类系统架构的实践样本。部署完成后即可开放词条录入、编辑与审核形成可持续更新的个人或团队知识库。1. 一套能跑通的百科系统源码从词条管理到搜索它把骨架给你搭好了不少开发者看到「超级模仿」四个字第一反应是套皮站、只抄首页的那种静态页面。但真正把这份源码包解压开以后你会发现它模仿的不是皮而是百科类产品的核心链路词条的创建、编辑、审核、分类导航、全文搜索外加编辑历史留痕。也就是说它不是给你一个好看的壳而是把「一个知识条目从录入到被搜索到」的完整流程做成了可运行的系统。这套系统适合三类人一是拿它做课程设计或毕业设计的学生二是想快速搭一个内部知识库但不想从零写增删改查的开发者三是想研究百科类产品数据模型和搜索方案的前端转全栈。它的技术栈偏向经典 PHP MySQL好处是随便一台能跑 Apache 的机器就能部署坏处是如果你只会 Spring Boot 那一套可能需要花半小时适应它的文件组织方式。2. 系统架构与技术选型目录结构、运行环境与伪静态配置2.1 三层目录结构展示、业务与数据分离是这套源码最大的优点源码包解压后的目录不算复杂但分层意识比大多数课程设计要强。我建议你拿到手第一件事不是急着配数据库而是先把目录结构捋一遍后面改功能时才不会迷路。百科系统/ ├── index.php # 入口文件负责路由分发 ├── admin/ # 后台管理端 │ ├── article_edit.php # 词条编辑页 │ └── category_manage.php# 分类管理页 ├── includes/ # 公共库 │ ├── db.php # PDO 数据库连接 │ ├── auth.php # 登录态与权限控制 │ └── functions.php # 公共函数格式化、过滤等 ├── public/ # 对外静态资源 │ ├── css/ │ ├── js/ │ └── uploads/ # 词条配图上传目录 ├── views/ # 模板文件 │ ├── article.php # 词条详情页模板 │ ├── search.php # 搜索结果页模板 │ └── list.php # 分类列表页模板 └── storage/ # 运行时生成的文件 └── logs/ # 操作日志这套结构里的关键设计在于includes/和views/分离业务逻辑全在includes/下的公共库中页面模板只负责输出这样当你需要把页面从 PHP 原生写法迁到模板引擎时不需要动业务代码。此外public/uploads和storage/logs分开是有意的——上传文件需要被 Web 服务器直接访问而日志文件绝对不能被外部访问懂运维的人一看这个划分就知道原作者踩过权限的坑。2.2 技术选型对比为什么 PHP MySQL 在这个场景下仍然是合理的不少同学拿到源码后第一个疑问是为什么不用前后端分离为什么不用 Laravel/ThinkPHP我拆过的项目中百科类系统用原生 PHP 的比例相当高原因不在于性能而在于「可交付性」。对比维度原生 PHP MySQLPython Flask SQLiteJava Spring Boot MySQL部署门槛XAMPP/WAMP 双击启动拷目录即跑需安装 Python 环境和依赖包需 JDK Maven 打包环境配置繁琐中文分词搜索MySQL 全文索引 ngram 能扛SQLite 全文索引较弱依赖 Elasticsearch 才能好用学习成本会后端基础就能改需懂 Python 包管理需理解 Spring 全家桶适合规模万级词条千级词条十万级词条以上这套源码选择 PHP MySQL说明作者刻意把部署门槛压到了最低。对于那些只需要几千个词条、几个编辑账号同步维护的知识库场景这个选型不会成为瓶颈反而能让你把精力花在词条内容本身。当然如果你预判词条量会超过五万建议在源码基础上加一层 Redis 缓存这个我放在后面进阶章节细说。2.3 伪静态 URL让词条链接变成 /word/xxx.html百科类产品最显著的外部特征是词条 URL 是「静态化」的而不是index.php?id123。这套源码在后台已经实现了路由参数解析你需要在 Web 服务器层面把重写规则打开否则词条页虽然能访问但 URL 长且不利于分享和搜索引擎收录。Apache 环境下的.htaccess规则放在项目根目录即可IfModule mod_rewrite.c RewriteEngine On RewriteBase / # 已存在的真实文件直接访问不经过路由 RewriteCond %{REQUEST_FILENAME} !-f RewriteCond %{REQUEST_FILENAME} !-d # 将 /word/xxx.html 映射到 index.php 的 word 参数 RewriteRule ^word/(.*)\.html$ index.php?word$1 [L,QSA] /IfModule这段规则的核心是两条RewriteCond——它们保证项目里的真实图片、CSS、JS 文件不会被错误路由到 index.php。RewriteRule里的(.*)是贪婪匹配\.html后缀去掉后整段内容会作为word参数传给入口文件。[L,QSA]中的 L 表示这条规则命中后不再匹配后续规则QSA 表示保留原有的查询字符串。Nginx 环境的写法略有不同在server配置块内加上location / { try_files $uri $uri/ /index.php?word$uri; } location ~ \.php$ { fastcgi_pass 127.0.0.1:9000; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; include fastcgi_params; }注意 Nginx 的try_files和 Apache 的RewriteCond作用一致先找真实文件找不到才走路由。区别在于$uri会包含开头的斜杠所以源码里 index.php 需要对word参数做一次 ltrim 去斜杠处理。如果你改完配置还是 404优先检查 Nginx 的location块顺序——正则location ~ \.php$必须放在location /之后。3. 数据模型与词条管理五张核心表与编辑历史回滚设计3.1 词条主表结构摘要单独冗余正文用 MEDIUMTEXT打开源码的install.sql文件最先看到的是词条主表。这张表的设计直接决定了系统能支撑多少词条、搜索快不快、列表页会不会卡。下面是核心建表语句我做了一些注释精简CREATE TABLE article ( id INT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 词条ID, title VARCHAR(50) NOT NULL COMMENT 词条标题最多50字, summary VARCHAR(200) NOT NULL DEFAULT COMMENT 词条摘要列表页和搜索页展示用, content MEDIUMTEXT NOT NULL COMMENT 词条正文支持HTML格式, category_id SMALLINT UNSIGNED NOT NULL DEFAULT 0 COMMENT 所属分类, editor_id INT UNSIGNED NOT NULL DEFAULT 0 COMMENT 最近编辑用户ID, status TINYINT NOT NULL DEFAULT 0 COMMENT 0草稿 1待审核 2已发布 3已下架, hits INT UNSIGNED NOT NULL DEFAULT 0 COMMENT 点击量, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_title (title), KEY idx_category_status (category_id, status), KEY idx_update_time (update_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_general_ci;这里我把几个容易被忽略的设计点拆开讲。第一summary字段单独冗余而不是查询时用LEFT(content, 100)截取是因为列表页和搜索结果页会高频读取摘要如果每次从 MEDIUMTEXT 里截取数据库 IO 会明显上升。第二uk_title唯一索引保证同名词条不可能被重复创建但这也带来一个坑——编辑词条改名时如果新标题已存在会直接报错源码里对这一块的处理是前台先弹提示而不是直接抛 SQL 异常。第三idx_category_status是联合索引分类列表页的查询条件永远是WHERE category_id ? AND status 2这个索引能让列表查询走覆盖索引避免回表。3.2 分类与标签两级分类足够标签做词条关联推荐分类表在源码里的设计是经典的「父级 子级」两级结构而不是无限极。这是刻意的选择因为百科词条分类如果超过三级用户导航成本会剧增。CREATE TABLE category ( id SMALLINT UNSIGNED NOT NULL AUTO_INCREMENT, pid SMALLINT UNSIGNED NOT NULL DEFAULT 0 COMMENT 父级ID0表示顶级分类, name VARCHAR(20) NOT NULL, sort_order SMALLINT NOT NULL DEFAULT 0 COMMENT 排序权重数值越小越靠前, PRIMARY KEY (id), KEY idx_pid_sort (pid, sort_order) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;而标签表通过中间表与词条关联见第三张核心表CREATE TABLE tag ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, name VARCHAR(20) NOT NULL, PRIMARY KEY (id), UNIQUE KEY uk_name (name) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE article_tag ( article_id INT UNSIGNED NOT NULL, tag_id INT UNSIGNED NOT NULL, PRIMARY KEY (article_id, tag_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;如果你需要做「相关词条推荐」最简单的实现就是从article_tag中找同标签词条按点击数倒序取 6 条。源码里已经封装了一个get_related_articles($article_id)函数核心 SQL 是SELECT a.id, a.title FROM article a INNER JOIN article_tag at1 ON a.id at1.article_id INNER JOIN article_tag at2 ON at1.tag_id at2.tag_id WHERE at2.article_id ? AND a.id ! ? GROUP BY a.id ORDER BY a.hits DESC LIMIT 6。这里有个小坑GROUP BY a.id在 MySQL 5.7 之后默认开启了ONLY_FULL_GROUP_BY必须保证 SELECT 中的非聚合列都在 GROUP BY 中否则直接报错。源码里这条 SQL 恰好只查了一个a.id和a.title而a.title依赖a.id是函数依赖所以能跑通。3.3 编辑历史留痕整段存储而不是 diff 方案百科系统区别于普通 CMS 的核心能力是「词条编辑可追溯」。我在很多自建系统里见过用 diff 算法只存差异的写法表面上省空间但实际上每次展示历史版本都要重新拼接全文而且 diff 算法处理中文的长段落时经常出现「一大段全删了又全加上」的误判。这套源码选了最朴素也最可靠的做法每次编辑把整篇正文存档。CREATE TABLE article_log ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, article_id INT UNSIGNED NOT NULL COMMENT 词条ID, editor_id INT UNSIGNED NOT NULL COMMENT 编辑者用户ID, old_title VARCHAR(50) NOT NULL, new_title VARCHAR(50) NOT NULL, old_content MEDIUMTEXT NOT NULL, new_content MEDIUMTEXT NOT NULL, edit_reason VARCHAR(200) NOT NULL DEFAULT COMMENT 编辑理由非必填, edit_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_article_time (article_id, edit_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;回滚操作就变成了一次很简单的 UPDATE——把词条主表的内容改回old_content同时删掉这条日志记录。空间开销方面一个词条即便被编辑 100 次每次正文按 10KB 算100 次也就 1MB对于词条量在万级以内的站点完全不是问题。源码里管理后台的「历史版本对比」功能就是把这个表按article_id倒序查出来用nl2br()输出到两个并排的文本框里做视觉对比。你可以在此基础上引入差异高亮但优先级不高。4. 从 LIKE 到全文索引搜索模块的分词、防注入与排序策略4.1 第一阶段预处理语句与 LIKE 查询的正确姿势源码第一版搜索是直接WHERE title LIKE %关键词% OR content LIKE %关键词%这种写法对小数据量完全没问题。但有两个隐患必须处理一是预处理防注入二是通配符转义。先看代码$keyword trim($_GET[q]); $stmt $pdo-prepare( SELECT id, title, summary, hits FROM article WHERE status 2 AND (title LIKE :kw OR summary LIKE :kw) ORDER BY hits DESC LIMIT 20 ); $stmt-execute([:kw % . $keyword . %]); $results $stmt-fetchAll(PDO::FETCH_ASSOC);这里的:kw命名参数是 PDO 预处理的标准做法能杜绝 SQL 注入。但注意一个细节LIKE语句里的%和_本身是通配符如果用户在搜索框输入了100%%会被当作通配符匹配任意字符串。正确做法是在拼接前把这两个字符转义掉$escaped addcslashes($keyword, %_); $stmt-execute([:kw % . $escaped . %]);性能方面前置%会导致uk_title索引失效全表扫描在万级数据下还能接受但超过十万条就会明显卡顿。这也是源码把搜索限制在summary而不是content的原因——摘要短、匹配快只是牺牲了「全文中搜索」的能力。4.2 第二阶段MySQL 全文索引与 ngram 中文分词的配合当你发现 LIKE 搜索已经慢到超过 300ms就该上全文索引了。MySQL 5.7.6 之后内置的中文分词插件 ngram 让这件事变得很便宜不需要装第三方组件ALTER TABLE article ADD FULLTEXT INDEX ft_title_summary (title, summary) WITH PARSER ngram;ngram的默认分词长度是 2也就是说「百科系统」会被切成「百科」「科系」「系统」三个双字 token。这个默认值对大多数中文词条够用但如果你发现搜索精度不够或召回太少可以在 MySQL 配置文件my.cnf中调整[mysqld] ngram_token_size2注意ngram_token_size是一个全局参数修改后必须重启 MySQL 服务且需要重建全文索引否则不生效。搜索 SQL 也随之升级SELECT id, title, summary, MATCH(title, summary) AGAINST(:kw IN NATURAL LANGUAGE MODE) AS score FROM article WHERE status 2 AND MATCH(title, summary) AGAINST(:kw IN NATURAL LANGUAGE MODE) ORDER BY score DESC, hits DESC LIMIT 20;MATCH ... AGAINST返回的相关度分数是基于词频和逆文档频率的分数越高说明词条和搜索词越相关。这里有一个常见误用在WHERE条件的MATCH AGAINST和SELECT列表里的MATCH AGAINST是两次独立计算MySQL 会自动缓存结果不会真跑两遍所以你别嫌写两遍啰嗦这是标准写法。4.3 搜索排序优化给标题命中加权重MATCH(title, summary)有一个问题标题命中和摘要命中的权重是相同的但用户通常认为标题命中比摘要命中更相关。源码里并没有把这个优化做进去我一般会在它基础上做一次加权SELECT id, title, summary, MATCH(title, summary) AGAINST(:kw IN NATURAL LANGUAGE MODE) AS raw_score, IF(title LIKE :kw_title, 5, 0) AS title_bonus FROM article WHERE status 2 AND MATCH(title, summary) AGAINST(:kw IN NATURAL LANGUAGE MODE) ORDER BY (raw_score title_bonus) DESC, hits DESC LIMIT 20;这里用IF(title LIKE :kw_title, 5, 0)给标题完全包含关键词的词条额外加 5 分这个权重我调过很多次5 在这个数据量级上既不喧宾夺主也能保证标题命中排到前面。不过LIKE在这个 SQL 里是不会走索引的但它的作用范围已经被MATCH AGAINST过滤到几百条以内性能影响可以忽略。还有一个细节:kw_title参数不需要加%通配符因为用的是「完全包含」语义而非模糊匹配。前端高亮展示方面源码用的是最简单也最安全的方案$highlighted preg_replace( /( . preg_quote($keyword, /) . )/u, span classhighlight$1/span, htmlspecialchars($row[summary], ENT_QUOTES, UTF-8) );先用htmlspecialchars把摘要里的 HTML 标签转义掉再通过preg_quote对搜索词做正则转义最后替换成带span classhighlight的标记。这个顺序不能反——如果先高亮再转义高亮标签本身会被转义掉如果先转义再高亮正则不会匹配到被转义后的字符。我见过不少项目中高亮失效都是这个顺序搞反了。5. 部署与权限环境差异、三级账号体系与五个高频翻车点5.1 本地 XAMPP 与服务器 LNMP 的环境差异这套源码在本地和服务器上的部署表现并不完全一致主要差在三个地方。首先PHP 版本差异源码里用到了??合并运算符要求 PHP 7.0 以上如果服务器还是老掉牙的 5.6会直接白屏。其次Apache 与 Nginx 的伪静态规则不同这个我在 2.3 已经给了两套配置但需要注意的是 Nginx 环境下location块的顺序会影响 PHP 解析记得把location ~ \.php$放在最后。第三上传目录权限XAMPP 通常以当前用户运行 Apache读写文件没问题服务器上 Web 用户是www-data必须显式授权。本地部署最常见的坑是 PHP 短标签被关闭导致页面输出?裸代码现象打开词条页页面上方出现一堆类似 ?php echo $title;? 的源码文本或者干脆 500 错误。 原因源码里部分模板文件使用了 ? 短标签写法默认配置 short_open_tagOff 时会被 PHP 当作普通文本输出。 解决在 php.ini 中将 short_open_tagOn重启 PHP 服务即可如果你用的是 PHP-FPM改完要 systemctl restart php-fpm。这个坑在源码文档里没有写但几乎每个首次部署的人都会撞上因为课程设计里最常见的 PHP 版本是 7.4/8.0而这两个版本的默认配置都关闭了短标签。5.2 三级账号体系与会话状态管理源码的权限模型分了三级普通用户可以浏览和申请创建词条编辑员可以修改词条内容和提交审核管理员可以审核发布、下架词条和管理分类。权限控制集中在includes/auth.php中核心是一个基于 SESSION 的角色判断function check_role($min_role) { if (!isset($_SESSION[uid])) { header(Location: /login.php); exit; } $role_map [user 1, editor 2, admin 3]; if ($role_map[$_SESSION[role]] $role_map[$min_role]) { exit(权限不足该操作已被记录); } }调用方只需要在页面顶部执行check_role(editor)就能过滤掉普通用户访问编辑接口。这里我建议你实际部署时加一层 CSRF 校验源码里表单提交没有带 token如果站点是公网可访问的攻击者可以诱导管理员在不知情的情况下提交一个「删除词条」的 POST 请求。加 CSRF 的做法很简单在表单渲染时往 SESSION 扔一个随机串$_SESSION[csrf_token] bin2hex(random_bytes(16)); // 表单里输出 input typehidden namecsrf_token value... // 提交时验证 hash_equals($_SESSION[csrf_token], $_POST[csrf_token])5.3 五个高频翻车点排查记录第一搜索中文词条返回空结果。现象是搜「百科系统」什么也查不到搜英文和数字正常。原因是数据库连接字符集没有显式指定 utf8mb4PHP 默认连接的字符集是 latin1中文参数传到 MySQL 后直接变成乱码导致匹配失败。解决方法是 PDO 连接字符串追加;charsetutf8mb4并且确保建表语句中的DEFAULT CHARSETutf8mb4没有被手工改掉。第二词条发布后前台 404。现象是后台显示词条已发布但前台访问词条链接提示页面不存在。原因是词条状态字段status没有从 0 改成 2。排查顺序先查数据库SELECT id, status FROM article WHERE id ?如果 status 是 1待审核说明管理员没有在后台点审核通过如果 status 是 2 仍然 404那就查伪静态规则用index.php?wordxxx绕过路由直连试试直连能开就说明是 rewrite 的问题。第三上传图片后前台显示裂图。原因是public/uploads目录没有写权限PHP 已经报错但被error_reporting(0)吞掉了。解决方法是把目录权限改为 755属主改为 Web 运行用户chown -R www-data:www-data public/uploads。千万不能用chmod 777这会把上传目录变成任何人可写的后门网上扫描脚本专盯这种目录。第四词条编辑保存后内容丢失。现象是编辑长文章时保存成功但刷新后内容变短了。原因是 PHP 配置post_max_size太小默认 8M 在图片较多时不够用。解决方法是调大post_max_size和upload_max_filesize修改后重启 PHP 服务。这个坑的隐蔽之处在于表单提交时数据被 PHP 直接丢弃但又没有返回错误提示。第五搜索页出现 SQL 报错。现象是在搜索框输入带单引号的内容时页面报错。原因是你用的不是源码自带的 PDO 预处理版本而是老版本的字符串拼接版本。解决方法是确认includes/db.php用的是 PDO 预处理且所有 SQL 都走命名参数。如果发现还有mysqli_query拼接字符串的残留写法要全部改成预处理否则你的站点会被 SQL 注入直接脱库。6. 进阶玩法词条静态化、SEO 细节与内容安全双保险到这个阶段系统已经能稳定跑起来了词条能发、能搜、能回滚。但如果我是把它当作一个正式项目来交付还会再往下做三件事SEO 细节、内容安全和性能缓存。词条页的 SEO 主要靠三个细节。第一给index.php的路由输出独立的title和meta description而不是所有页面共用站点名$page_title $article[title] . _ . $site_name; $page_desc mb_substr($article[summary], 0, 120);第二生成sitemap.xml。写一个 cron 脚本每天凌晨遍历status 2的词条输出url节点把lastmod设为update_time。百度收录百科类页面非常看重 sitemap而且这个脚本不复杂半小时能写完。第三词条正文里做内链。在显示词条内容前用后台维护的一个「词条关键词 → 词条链接」映射表把正文中出现的词名自动加a标签。这个映射表不需要提前全量生成每次编辑词条时扫描一次发现正文里有其他已存在词条标题就自动记录。内容安全这块我建议你在源码基础上加三道闸。第一道是编辑入口验证码防止脚本机器人批量提交垃圾词条用简单的sessionGD库生成四位数字即可。第二道是敏感词过滤维护一个关键词表在article_edit.php的保存逻辑里用str_replace或正则把命中的词替换成*。第三道是编辑审核确认即使是编辑员提交的修改也要求管理员二次确认后才能正式发布这一步在源码里已经有雏形只是默认没开在后台配置里找到review_required开关打开即可。性能方面最有效的优化是词条页静态化。百科站 90% 的流量都打在词条详情页上而这些页面内容变化频率极低。你可以做一层文件缓存每个词条首次访问时把渲染完的 HTML 存到cache/word_123.html后续请求直接用file_get_contents输出并且设置Last-Modified响应头。在词条被编辑时删除对应缓存文件即可。这比引入 Redis 更简单适合这台服务器上没装 Redis 的情况。我在自己第一次部署这套系统的时候就因为图省事把 uploads 目录直接chmod 777结果第二天日志里就出现了来自外部 IP 的上传记录。从那以后我每次上线都强制走一遍完整的权限巡检先检查short_open_tag再确认uploads属主不是 root最后用curl模拟一次完整搜索和编辑流程。这套流程帮我躲过了不少低级问题希望也能帮到你。本文还有配套的精品资源点击获取