ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

手写一套PHP个人博客源码:从数据库设计到部署实战

手写一套PHP个人博客源码:从数据库设计到部署实战 简介个人博客PHP源码是一份基于PHP与MySQL构建的开源博客项目适合PHP初学者、Web开发学习者作为实战练习。借助XAMPP这样的集成环境无需复杂配置即可在本地运行从零了解个人网站从页面渲染、用户注册登录到文章发布与后台管理的完整逻辑。压缩包为RAR格式共56个文件仅1.3MB包含19个PHP文件、13个JavaScript、5个CSS样式以及字体图标、图片等静态资源其中PHP负责业务与数据库交互JS与CSS分别完成动态效果和页面美化。资源已有1973人学习下载源码整理较规范包含后台管理、用户登录、文章发布、图片上传等模块并集成Bootstrap框架与Simditor富文本编辑器目录区分了核心类、页面文件与主题资源。通过阅读并运行这份源码可掌握SQL增删改查、会话管理、密码安全处理、前后端协作等关键技能也可在其基础上二次开发形成自己的博客系统。 直接跑题一句市面上随便搜“个人博客PHP源码”能出来一大堆打包好的系统但真正拿回来能跑、能改、能长期维护的其实不多。我前后帮朋友和自己倒腾过好几套个人博客系统从WordPress到Typecho再到纯手写的PHP源码都试过。这篇不是给你推荐某个现成的源码包而是把一套完整的PHP个人博客从需求拆解、表结构设计到核心代码怎么写、部署时踩过哪些坑整个链路捋一遍。你拿这套思路再去翻任何一套“个人博客PHP源码”都能看懂它为什么这么写也知道怎么改成自己的。1. 项目概述1.1 什么是个人博客PHP源码个人博客系统本质上是一个CMS内容管理系统的极简形态。它的核心需求只有三个能发文章、能管理文章、能展示文章。听起来简单但实际做得顺手涉及的细节并不少——分类、标签、评论、搜索、分页、SEO链接、后台登录每个点都能延伸出不少代码。PHP之所以在这个场景里格外合适是因为它的部署成本极低。一个PHP环境加一个MySQL库几乎任何一台虚拟主机都能跑。相比Java和Node.js那套复杂的环境要求PHP基本是“上传即用”。而且PHP的语法对新手友好改个功能不用像看Spring那样先搞懂一堆注解和依赖注入。所以如果你想要一套自己能完全掌控的博客源码PHP绝对是最省心的选择。1.2 这套源码要解决的核心问题我在动手写这套源码之前先列了一个需求清单你可以对照自己的情况来删减文章展示与管理发布、编辑、删除、置顶、草稿分类系统按分类归档文章标签系统多标签关联文章评论系统访客留言、审核机制全站搜索按标题或内容关键词搜索文章SEO支持伪静态URL、TDK设置、sitemap后台管理管理员登录、密码修改、文章管理、评论管理响应式前端手机端和PC端都不难看这个清单其实已经覆盖了市面上大多数PHP博客源码的功能点。你拿到任何一套源码先别急着装打开它的数据库配置文件看一眼表结构基本就知道作者是怎么设计功能的。我后面写的这套实现没有用任何框架就是原生PHP MySQL方便你看到底层的逻辑。2. 技术选型与整体架构思路2.1 用原生PHP而不是框架逻辑是什么我知道肯定有人要问现在PHP有Laravel、ThinkPHP这些成熟框架为什么不直接用我的答案很简单——博客系统是个恰到好处的“教学项目”。用框架当然可以但框架帮你屏蔽了太多底层细节你最后写完了可能还是不懂请求到底是怎么被处理的。原生PHP写博客核心逻辑一目了然所有请求通过入口文件index.php进入根据URL参数或伪静态规则确定要调用的控制器和方法控制器调用数据库操作类获取数据数据渲染到HTML模板返回给浏览器这条链路没有任何魔法每一步都看得见摸得着。而且博客系统的数据量通常不大优化的空间不大原生PHP的性能完全够用。我用原生PHP写这套源码单机跑个几十万篇文章没有任何压力。2.2 目录结构设计与MVC思想虽然不用框架但MVC的分层思想我建议你保留。这不是装X是真有用——把数据操作和页面展示混在一起写前期快后期改一个样式能把业务逻辑改出BUG来。我用的目录结构如下blog/ ├── index.php # 入口文件所有请求都从这里进 ├── config/ │ └── config.php # 数据库配置、站点配置 ├── includes/ │ ├── db.php # PDO数据库操作封装 │ ├── functions.php # 公共函数库 │ └── auth.php # 登录验证 ├── admin/ │ ├── index.php # 后台入口 │ ├── login.php # 登录页 │ ├── article_edit.php # 文章编辑 │ ├── article_list.php # 文章列表 │ └── comment_manage.php # 评论管理 ├── templates/ │ ├── header.php # 公共头部 │ ├── footer.php # 公共尾部 │ ├── index.php # 首页模板 │ ├── article.php # 文章详情页模板 │ ├── category.php # 分类页模板 │ └── search.php # 搜索页模板 └── uploads/ # 上传的图片等文件这个结构里templates目录里的文件只负责显示includes里的文件只负责业务和数据库操作admin目录里是后台管理的控制器和页面。前后台共用includes里的数据库类和工具函数避免重复造轮子。3. 数据库设计详解3.1 核心数据表结构博客系统的核心表就四张文章表、分类表、标签表、评论表。很多复杂的博客系统还有一个标签和文章的关联表。我直接给出我最终用的建表SQL你可以对照着理解字段的设计意图CREATE TABLE categories ( cid int(11) NOT NULL AUTO_INCREMENT, name varchar(50) NOT NULL COMMENT 分类名称, alias varchar(50) NOT NULL DEFAULT COMMENT 分类别名用于URL, sort int(11) NOT NULL DEFAULT 0 COMMENT 排序权重, PRIMARY KEY (cid), UNIQUE KEY alias (alias) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE articles ( aid int(11) NOT NULL AUTO_INCREMENT, title varchar(150) NOT NULL COMMENT 文章标题, content longtext NOT NULL COMMENT 文章内容, category_id int(11) NOT NULL DEFAULT 0 COMMENT 所属分类, views int(11) NOT NULL DEFAULT 0 COMMENT 浏览量, is_top tinyint(1) NOT NULL DEFAULT 0 COMMENT 是否置顶, status tinyint(1) NOT NULL DEFAULT 1 COMMENT 1发布 0草稿, created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (aid), KEY category_id (category_id), KEY created_at (created_at) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;标签表我用了最简单的方案——单独一张tags表存储标签名和标签别名。文章和标签的关系通过中间表关联。如果你只是个人用不想建第三张表也可以直接在文章表里加一个tags字段用逗号分隔存储。我实测下来个人博客用逗号分隔完全够用查询时用FIND_IN_SET就能搞定省了一张表也更直观。CREATE TABLE comments ( id int(11) NOT NULL AUTO_INCREMENT, article_id int(11) NOT NULL COMMENT 所属文章, username varchar(50) NOT NULL COMMENT 评论者名字, email varchar(100) NOT NULL DEFAULT COMMENT 评论者邮箱, content text NOT NULL COMMENT 评论内容, is_approved tinyint(1) NOT NULL DEFAULT 1 COMMENT 是否审核通过, created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY article_id (article_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;3.2 为什么字段要这么设计有几个细节我第一次写的时候忽略了后来吃亏了才补上第一所有表都用utf8mb4字符集。我最早用utf8结果评论里有人发了个emoji表情直接存不进去报错。utf8mb4才是完整的UTF-8支持四字节字符emoji和生僻字都不会出问题。第二文章内容用longtext类型。可能有人觉得text就够了但如果你写长文2万字的文章加一些代码块text的64KB上限会卡得很死。longtext有4GB的空间永远不用担心截断。第三created_at字段用DEFAULT CURRENT_TIMESTAMP这个可以减少一行PHP代码。很多新手喜欢在PHP里写date(Y-m-d H:i:s)再传进来其实MySQL自动生成更省事。4. 核心模块代码实现与实操4.1 数据库连接与基础PDO操作我强烈建议用PDO而不是mysqli。PDO支持预处理语句能有效防SQL注入而且代码写起来更简洁。我封装了一个最基本的数据库类?php // includes/db.php class DB { private static $pdo null; public static function getInstance() { if (self::$pdo null) { $config require __DIR__ . /../config/config.php; $dsn mysql:host{$config[host]};dbname{$config[dbname]};charsetutf8mb4; self::$pdo new PDO($dsn, $config[username], $config[password]); self::$pdo-setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); } return self::$pdo; } public static function query($sql, $params []) { $stmt self::getInstance()-prepare($sql); $stmt-execute($params); return $stmt; } public static function fetchAll($sql, $params []) { return self::query($sql, $params)-fetchAll(PDO::FETCH_ASSOC); } public static function fetch($sql, $params []) { return self::query($sql, $params)-fetch(PDO::FETCH_ASSOC); } }这里的关键点是静态方法单例模式。整个请求周期里数据库连接只建立一次不会重复握手。PDO::FETCH_ASSOC让查询结果直接就是关联数组操作起来很方便。4.2 入口文件与URL路由所有请求都进index.php然后我通过判断URL参数来分发。配合伪静态规则前台URL是/article/123.html这种形态?php // index.php $action isset($_GET[action]) ? $_GET[action] : home; $id isset($_GET[id]) ? intval($_GET[id]) : 0; switch ($action) { case home: $page isset($_GET[page]) ? max(1, intval($_GET[page])) : 1; $articleList getArticleList($page); include templates/index.php; break; case article: $article getArticleDetail($id); if (!$article) { http_response_code(404); exit(文章不存在或已删除); } updateViews($id); include templates/article.php; break; case category: $catAlias isset($_GET[alias]) ? $_GET[alias] : ; $categoryArticles getArticlesByCategory($catAlias, $page); include templates/category.php; break; case search: $keyword isset($_GET[q]) ? trim($_GET[q]) : ; $searchResults searchArticles($keyword); include templates/search.php; break; default: http_response_code(404); exit(页面不存在); }配合的Nginx伪静态规则location / { if (!-e $request_filename) { rewrite ^/article/(\d)\.html$ /index.php?actionarticleid$1 last; rewrite ^/category/([\w-])$ /index.php?actioncategoryalias$1 last; rewrite ^/page/(\d)$ /index.php?actionhomepage$1 last; } }Apache的话在.htaccess里写RewriteEngine On RewriteRule ^article/(\d)\.html$ index.php?actionarticleid$1 [L] RewriteRule ^category/([\w-])$ index.php?actioncategoryalias$1 [L] RewriteRule ^page/(\d)$ index.php?actionhomepage$1 [L]这个伪静态做两件事对外是更友好的URL对内是隐藏了index.php的存在感。注意正则里([\w-])这个匹配分类别名的时候一定要加上-因为分类别名经常用中划线。4.3 文章列表与分页实现分页是博客系统里最容易写乱的部分。核心思路是先统计总文章数除以每页条数得到总页数然后LIMIT计算出偏移量。我封装了一个通用函数?php function getArticleList($page, $pageSize 10) { $offset ($page - 1) * $pageSize; $sql SELECT a.aid, a.title, a.views, a.created_at, c.name as category_name, LEFT(a.content, 200) as excerpt FROM articles a LEFT JOIN categories c ON a.category_id c.cid WHERE a.status 1 ORDER BY a.is_top DESC, a.created_at DESC LIMIT $offset, $pageSize; return DB::fetchAll($sql); } function getTotalArticles() { return DB::fetch(SELECT COUNT(*) as total FROM articles WHERE status 1)[total]; } function getPagination($page, $totalArticles, $pageSize 10) { $totalPages max(1, ceil($totalArticles / $pageSize)); $prev $page 1 ? $page - 1 : 0; $next $page $totalPages ? $page 1 : 0; return [ totalPages $totalPages, currentPage $page, prev $prev, next $next ]; }分页的关键细节在LIMIT语句的偏移计算上。($page - 1) * $pageSize这个公式必须牢牢记住这是分页的核心我第一次就写成了$page * $pageSize导致第二页永远重复显示最后一篇文章。SQL里的LIMIT语法是LIMIT offset, countoffset从0开始第一页的offset是0第二页是10以此类推。4.4 文章详情与上一篇下一篇文章详情页除了展示正文一般还会带上一个“上一篇/下一篇”的导航。这个SQL用到的是时间比较?php $prevSql SELECT aid, title FROM articles WHERE status 1 AND created_at (SELECT created_at FROM articles WHERE aid $id) ORDER BY created_at ASC LIMIT 1; $nextSql SELECT aid, title FROM articles WHERE status 1 AND created_at (SELECT created_at FROM articles WHERE aid $id) ORDER BY created_at DESC LIMIT 1;用创建时间比较而不是直接用aid做加减原因是文章可能被删除直接用aid-1可能取到不存在的文章。用时间比较更准确。4.5 后台管理与安全加固后台部分我不把代码全贴出来但有几个安全点必须重点说第一个是密码加密。不要用MD5不要用SHA1直接用PHP内置的password_hash函数?php // 注册或修改密码时 $hashed password_hash($plainPassword, PASSWORD_DEFAULT); // 登录验证时 if (password_verify($inputPassword, $hashedFromDB)) { // 登录成功 }password_hash默认生成的是bcrypt加密每次生成的哈希值都不一样因为里面包含随机盐。这个函数的好处是即使数据库泄露了黑客想暴力破解也要花极大的代价。第二个是CSRF防护。后台所有表单都加一个token验证?php session_start(); // 生成token function generateCsrfToken() { if (empty($_SESSION[csrf_token])) { $_SESSION[csrf_token] bin2hex(random_bytes(32)); } return $_SESSION[csrf_token]; } // 表单里输出 echo input typehidden namecsrf_token value . generateCsrfToken() . ; // 接收时校验 if (!hash_equals($_SESSION[csrf_token], $_POST[csrf_token])) { die(非法请求); }第三个是SQL注入防护。前台展示的所有数据都要做输出转义?php function e($str) { return htmlspecialchars($str, ENT_QUOTES, UTF-8); }在模板里输出任何变量都用?php echo e($article[title]); ?这样就不会被XSS攻击。我在实测中看到过有人没做转义结果评论里塞了段JavaScript每次打开文章页面就弹广告——这就是典型的存储型XSS都是血泪教训。5. 部署与上线实操5.1 用宝塔面板部署源码的具体步骤现在个人博客大多部署在宝塔面板操作确实方便。我部署这套源码时的完整步骤在宝塔“网站”中添加站点PHP版本选择7.4或8.0以上在“数据库”中添加MySQL数据库记录下库名、用户名、密码把源码上传到站点根目录/www/wwwroot/你的域名/用phpMyAdmin或命令行导入之前建好的SQL文件修改config/config.php中的数据库连接信息在站点设置里配置伪静态为“Nginx”或“Apache”把上面的规则粘贴进去给uploads目录设置755权限访问站点检查前后台是否正常这个流程里最容易踩的坑是伪静态没配置好。很多人源码上传后文章页40499%是伪静态规则没生效或者规则写错了。宝塔里直接选ThinkPHP框架的伪静态并不通用最好自定义粘贴我上面给的规则。5.2 源码上传前要检查的三个细节第一检查PHP版本兼容性。我最早用PHP 5.6写的代码后来服务器升级到PHP 8.0发现很多地方报错——比如mysql_*系列的旧函数早就被移除了必须全部替换成PDO或mysqli。建议直接用PHP 8.0以上版本开发代码就用PHP 7的语法避免踩老坑。第二检查文件权限。上传目录、缓存目录必须是755或775属主是www用户否则图片上传不了。我把uploads目录的权限设成755后仍然上传失败排查了半天才发现是目录属主不对改成www:www就好了。第三检查配置文件路径。很多人把源码下载下来后忘了改config.php导致前台能打开页面但文章列表空白数据库连接报错。这个要注意区分清楚后台host是localhost还是127.0.0.1——宝塔里localhost可能出现socket连接问题换成127.0.0.1更稳定。5.3 上线后的基本优化上线不是终点我建议至少做两件优化开启OPcache加速PHP执行配置Nginx的gzip压缩。这两步能让博客打开速度快不少几乎零成本。OPcache就是让PHP脚本第一次编译后缓存起来下次请求直接用缓存。宝塔面板在PHP设置里就能开启opcache.enable1 opcache.memory_consumption128 opcache.max_accelerated_files4000gzip在站点配置里加一行gzip on; gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xmlrss image/svgxml;我测了一下开gzip后HTML体积能压缩60%以上图片类资源该损耗的还是损耗但文字内容效果明显。6. 常见问题与排查技巧实录6.1 安装与运行中的典型报错报错一提示“Database connection failed”这个错误基本就是config.php里的数据库配置不对。常见原因有数据库名写错、密码带特殊字符没转义、端口没写。另外注意数据库的地址不要用localhost换成127.0.0.1。报错二文章页出现404前面说过的伪静态问题。先检查伪静态规则有没有粘贴生效再看URL是/index.php?actionarticleid1还是/article/1.html如果是后者且404就是规则没生效。在宝塔里要确保站点已经切换到正确的Nginx配置且规则没有语法错误。报错三评论提交后页面空白大概率是SQL执行报错但错误信息被隐藏了。我先在本地环境把display_errors打开再看具体哪里运行时错误例如字段名拼错了。实际排查中我遇到最多的是数据库的字段名跟代码里的字段名对不上比如评论表用了content代码里用了commentSQL直接报错。报错四上传图片失败或前台图片不显示检查两个地方uploads目录是否存在且有写入权限文件路径是否正确。如果上传成功后找不到图片多半是保存路径写的是相对路径但文件实际存到了别处。我用的是绝对路径方式存储这样不管从哪个页面访问都不会出问题。6.2 性能排查的实测心得个人博客的并发量通常不大但还是会碰到“第一个请求很慢后面就快了”的情况。原因一般是冷启动时PHP脚本第一次编译再加上数据库首次连接握手比较耗时间。开了OPcache之后基本缓解。另一个常见的性能问题是数据库索引没建好。我最初在文章表里只建了主键索引后来文章多了打开分类页就很慢。后来在category_id和created_at上各加了索引查询速度立刻上来了几乎不需要额外优化就能流畅跑。6.3 数据备份与恢复的最佳实践个人博客最怕的就是数据丢失。我建议至少每周自动备份一次数据库。宝塔面板有定时备份功能可以设置每天凌晨把MySQL数据库备份到本地或云端存储。我一般备份两份一份在服务器本地保留最近3天一份传到对象存储永久保留。恢复时直接在宝塔的数据库管理里恢复备份文件即可。再补充一个小技巧文章内容的备份比数据库备份更直观用的时候导出Markdown或每天自动打包uploads目录也可以。7. 从源码到能跑的博客我最后想说的写这套PHP博客源码的过程实际上是把Web开发的常见知识点串了一遍HTTP请求的处理、数据库设计与操作、SQL防注入、XSS攻防、URL重写、会话管理、文件上传。每一个模块单独拎出来都不算难但组合到一起就形成了一个完整的系统。如果你只是急着要一个能用的博客直接装Typecho或WordPress会更省事改主题比写主题容易得多。但如果你想知道一套博客为什么能跑、源码里每个文件都在干嘛、想加功能的时候从哪儿下手那跟着本文的思路从零手写或者去拆解任何一套开源PHP博客源码都是很值得做的事情。我自己在写这套代码的过程中最大的体悟是不要小看“个人博客”这个项目。当年我在做数据库设计的时候以为建四张表就够了结果是后来又加了标签表、友情链接表、网站配置表……功能是越加越多先想清楚需求边界很重要。最后再分享一个我最近新加的小功能——统计文章阅读时长其实就是根据文章字数估算一个大概的阅读分钟数显示在文章开头。这个小功能在Typecho插件里都有现成的但自己能用PHP写一个出来那种满足感是直接用插件装不来的。本文还有配套的精品资源点击获取
返回列表