当前位置:首页>php>PHP 完整博客系统实战:从零搭建用户/文章/评论/通知全功能

PHP 完整博客系统实战:从零搭建用户/文章/评论/通知全功能

  • 2026-08-18 23:11:19
PHP 完整博客系统实战:从零搭建用户/文章/评论/通知全功能

资料在文章末尾

前七篇的知识点在这篇全部串联起来——从数据库设计到 OOP 架构,从表单安全到 AJAX 交互,最终产出一个接近生产环境的完整博客系统。用户注册登录、图形验证码、文章发布管理、评论点赞收藏、站内通知,全部从零实现。

一、为什么要做这个项目

从第一行 echo 到面向对象,零散的知识点只有落地到真实项目里才能真正消化。这篇会把前七篇的所有东西揉到一起,搭建一个功能接近生产环境的个人博客系统。你会看到 OOP 如何让代码模块化、多表关联如何支撑复杂业务、安全防护如何贯穿每个功能环节、AJAX 交互如何从零逐步落地。

功能总览

整个系统分五大模块,每个模块独立完整但彼此关联:

• 用户体系:注册/登录/退出、图形验证码、资料编辑、头像更换、密码修改、忘记密码重置、账号注销• 内容体系:文章发布/编辑/删除、封面图上传、文章列表分页、详情页展示• 交互体系:文章评论、评论点赞、一键点赞/取消、收藏/取消收藏、我的收藏列表• 通知体系:点赞收藏自动推送站内通知、未读数量提示、全部标记已读、一键清空• 安全体系:SQL注入防护、XSS转义、CSRF令牌、权限校验、文件上传安全

二、项目架构与数据库设计

2.1 目录结构

项目按 Core(核心类)→ Models(数据模型)→ 页面文件三层组织。上传文件单独放 uploads 目录,按头像和文章封面分子目录。

blog/├── Core/│   └── Database.php          # 数据库单例类├── Models/│   ├── User.php              # 用户模型│   ├── Post.php              # 文章模型│   ├── Comment.php           # 评论模型│   ├── CommentLike.php       # 评论点赞模型│   ├── Like.php              # 点赞模型│   ├── Favorite.php          # 收藏模型│   └── Notification.php      # 通知模型├── uploads/│   ├── avatar/               # 用户头像存储│   └── post/                 # 文章封面存储├── captcha.php               # 图形验证码生成├── register.php              # 用户注册├── login.php                 # 用户登录├── welcome.php               # 个人中心首页├── profile.php               # 修改个人资料├── change_password.php       # 修改登录密码├── forgot_password.php       # 忘记密码入口├── reset_password.php        # 重置密码页面├── home.php                  # 文章列表(分页)├── create_post.php           # 发布文章├── view_post.php             # 文章详情├── edit_post.php             # 编辑文章├── delete_post.php           # 删除文章├── add_comment.php           # 提交评论├── delete_comment.php        # 删除评论├── toggle_like.php           # 点赞切换├── toggle_favorite.php       # 收藏切换├── toggle_comment_like.php   # 评论点赞切换├── my_favorites.php          # 我的收藏列表├── notifications.php         # 我的通知列表├── delete_user.php           # 注销账号└── logout.php                # 退出登录

2.2 完整数据库设计

下面这段 SQL 一次性建完所有表,统一用 utf8mb4 编码(支持 emoji),外键级联删除保证数据一致性——删文章时关联的评论、点赞、收藏自动清掉。

CREATE DATABASE IF NOT EXISTS blog CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;USE blog;-- 用户表CREATE TABLE users (    id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,    username VARCHAR(50) NOT NULL UNIQUE,    password VARCHAR(255) NOT NULL,    email VARCHAR(100) NOT NULL UNIQUE,    avatar VARCHAR(255) NULL COMMENT '头像路径',    create_time DATETIME DEFAULT CURRENT_TIMESTAMP) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;-- 文章表CREATE TABLE posts (    id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,    user_id INT UNSIGNED NOT NULL,    title VARCHAR(200) NOT NULL,    cover VARCHAR(255) NULL COMMENT '文章封面图路径',    content TEXT NOT NULL,    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,    FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;-- 评论表CREATE TABLE comments (    id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,    post_id INT UNSIGNED NOT NULL,    user_id INT UNSIGNED NOT NULL,    content TEXT NOT NULL,    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,    FOREIGN KEY (post_id) REFERENCES posts(id) ON DELETE CASCADE,    FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;-- 点赞表(用户+文章唯一)CREATE TABLE post_likes (    id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,    post_id INT UNSIGNED NOT NULL,    user_id INT UNSIGNED NOT NULL,    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,    UNIQUE KEY uk_post_user (post_id, user_id),    FOREIGN KEY (post_id) REFERENCES posts(id) ON DELETE CASCADE,    FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;-- 评论点赞表(用户+评论唯一,防止重复点赞)CREATE TABLE comment_likes (    id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,    comment_id INT UNSIGNED NOT NULL COMMENT '关联评论ID',    user_id INT UNSIGNED NOT NULL COMMENT '点赞用户ID',    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,    UNIQUE KEY uk_comment_user (comment_id, user_id),    FOREIGN KEY (comment_id) REFERENCES comments(id) ON DELETE CASCADE,    FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;-- 收藏表(用户+文章唯一)CREATE TABLE post_favorites (    id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,    post_id INT UNSIGNED NOT NULL,    user_id INT UNSIGNED NOT NULL,    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,    UNIQUE KEY uk_post_user (post_id, user_id),    FOREIGN KEY (post_id) REFERENCES posts(id) ON DELETE CASCADE,    FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;-- 密码重置表CREATE TABLE password_resets (    id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,    email VARCHAR(100) NOT NULL,    token VARCHAR(100) NOT NULL,    expire_time INT UNSIGNED NOT NULL COMMENT '过期时间戳',    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,    KEY idx_email (email),    KEY idx_token (token)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;-- 站内通知表CREATE TABLE notifications (    id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,    user_id INT UNSIGNED NOT NULL COMMENT '接收通知的用户ID',    type VARCHAR(20) NOT NULL COMMENT '通知类型:like/comment',    related_post_id INT UNSIGNED NULL COMMENT '关联文章ID',    trigger_user_id INT UNSIGNED NOT NULL COMMENT '触发通知的用户ID',    content VARCHAR(255) NOT NULL COMMENT '通知内容',    is_read TINYINT UNSIGNED DEFAULT 0 COMMENT '0未读 1已读',    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,    FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,    FOREIGN KEY (trigger_user_id) REFERENCES users(id) ON DELETE CASCADE,    FOREIGN KEY (related_post_id) REFERENCES posts(id) ON DELETE CASCADE) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

实战提醒:点赞表和收藏表都用了 UNIQUE KEY 联合唯一索引,这样数据库层面就杜绝了重复点赞/收藏,比在 PHP 里先查再插靠谱得多。

三、核心基础层

3.1 数据库单例类 Core/Database.php

全局唯一数据库连接,避免重复创建连接浪费资源。所有模型通过 Database::getInstance()->getPdo() 获取同一个 PDO 实例。

<?phpnamespace Blog\Core;class Database{    private static ?self $instance = null;    private \PDO $pdo;    private function __construct()    {        $host = '127.0.0.1';        $dbname = 'blog';        $dbUser = 'root';        $dbPass = '';        $charset = 'utf8mb4';        $dsn = "mysql:host=$host;dbname=$dbname;charset=$charset";        $options = [            \PDO::ATTR_ERRMODE            => \PDO::ERRMODE_EXCEPTION,            \PDO::ATTR_DEFAULT_FETCH_MODE => \PDO::FETCH_ASSOC,            \PDO::ATTR_EMULATE_PREPARES   => false,        ];        $this->pdo = new \PDO($dsn, $dbUser, $dbPass, $options);    }    public static function getInstance(): self    {        if (self::$instance === null) {            self::$instance = new self();        }        return self::$instance;    }    public function getPdo(): \PDO    {        return $this->pdo;    }    private function __clone() {}}

3.2 自动加载规范

每个页面头部统一用这套 PSR-4 自动加载器,不需要 Composer 就能跑。Blog\Models\User 会自动映射到 Models/User.php 文件。

<?phpspl_autoload_register(function ($className) {    $prefix = 'Blog\\';    $baseDir = __DIR__ . DIRECTORY_SEPARATOR;    if (str_starts_with($className, $prefix)) {        $relativeClass = substr($className, strlen($prefix));        $file = $baseDir . str_replace('\\', DIRECTORY_SEPARATOR, $relativeClass) . '.php';        if (file_exists($file)) require $file;    }});session_start();

四、核心模型层

7 个模型类分别对应 7 张数据表,每个类封装自己的 CRUD 逻辑。由于篇幅原因,下面展示最核心的 User 模型(其他模型结构类似,完整代码在资料包里)。

4.1 用户模型 Models/User.php

User 模型是最复杂的,涵盖注册、查询、密码校验、资料更新、密码修改、密码重置全流程。注意 findByUsername 查询时不带 password 字段,需要校验密码时单独查——避免密码哈希不小心泄露到 Session 或日志里。

<?phpnamespace Blog\Models;use Blog\Core\Database;class User{    public ?int $id = null;    public string $username = '';    public string $email = '';    public ?string $avatar = null;    // 根据用户名查询    public static function findByUsername(string $username): ?self    {        $pdo = Database::getInstance()->getPdo();        $stmt = $pdo->prepare("SELECT id, username, email, avatar FROM users WHERE username = :u");        $stmt->execute([':u' => $username]);        $data = $stmt->fetch();        if (!$data) return null;        $user = new self();        $user->id = $data['id'];        $user->username = $data['username'];        $user->email = $data['email'];        $user->avatar = $data['avatar'];        return $user;    }    // 根据ID查询(含密码)    public static function findById(int $id): ?self    {        $pdo = Database::getInstance()->getPdo();        $stmt = $pdo->prepare("SELECT id, username, email, avatar, password FROM users WHERE id = :id");        $stmt->execute([':id' => $id]);        $data = $stmt->fetch();        if (!$data) return null;        $user = new self();        $user->id = $data['id'];        $user->username = $data['username'];        $user->email = $data['email'];        $user->avatar = $data['avatar'];        return $user;    }    // 注册新用户    public static function create(string $username, string $password, string $email, ?string $avatarPath = null): int    {        $pdo = Database::getInstance()->getPdo();        $hashPwd = password_hash($password, PASSWORD_DEFAULT);        $stmt = $pdo->prepare(            "INSERT INTO users (username, password, email, avatar) VALUES (:u, :p, :e, :avatar)"        );        $stmt->execute([            ':u' => $username,            ':p' => $hashPwd,            ':e' => $email,            ':avatar' => $avatarPath        ]);        return (int)$pdo->lastInsertId();    }    // 校验密码    public function verifyPassword(string $inputPwd): bool    {        if (!$this->id) return false;        $pdo = Database::getInstance()->getPdo();        $stmt = $pdo->prepare("SELECT password FROM users WHERE id = :id");        $stmt->execute([':id' => $this->id]);        $row = $stmt->fetch();        return $row && password_verify($inputPwd, $row['password']);    }    // 校验用户名/邮箱是否存在    public static function isExists(string $username, string $email): bool    {        $pdo = Database::getInstance()->getPdo();        $stmt = $pdo->prepare("SELECT id FROM users WHERE username = :u OR email = :e");        $stmt->execute([':u' => $username, ':e' => $email]);        return (bool)$stmt->fetch();    }    // 修改密码(需原密码)    public function updatePassword(string $oldPwd, string $newPwd): bool    {        if (!$this->id) return false;        if (!$this->verifyPassword($oldPwd)) return false;        $pdo = Database::getInstance()->getPdo();        $hash = password_hash($newPwd, PASSWORD_DEFAULT);        $stmt = $pdo->prepare("UPDATE users SET password = :p WHERE id = :id");        $stmt->execute([':p' => $hash, ':id' => $this->id]);        return $stmt->rowCount() > 0;    }    // 重置密码(无需原密码,忘记密码用)    public static function resetPasswordByEmail(string $email, string $newPwd): bool    {        $pdo = Database::getInstance()->getPdo();        $hash = password_hash($newPwd, PASSWORD_DEFAULT);        $stmt = $pdo->prepare("UPDATE users SET password = :p WHERE email = :e");        $stmt->execute([':p' => $hash, ':e' => $email]);        return $stmt->rowCount() > 0;    }    // 创建密码重置令牌(30分钟有效)    public static function createResetToken(string $email): string    {        $pdo = Database::getInstance()->getPdo();        $token = bin2hex(random_bytes(32));        $expire = time() + 1800;        $stmt = $pdo->prepare("DELETE FROM password_resets WHERE email = :e");        $stmt->execute([':e' => $email]);        $stmt = $pdo->prepare("INSERT INTO password_resets (email, token, expire_time) VALUES (:e, :t, :exp)");        $stmt->execute([':e' => $email, ':t' => $token, ':exp' => $expire]);        return $token;    }    // 校验重置令牌    public static function validateResetToken(string $token): ?string    {        $pdo = Database::getInstance()->getPdo();        $stmt = $pdo->prepare("SELECT email, expire_time FROM password_resets WHERE token = :t LIMIT 1");        $stmt->execute([':t' => $token]);        $row = $stmt->fetch();        if (!$row || $row['expire_time'] < time()) return null;        return $row['email'];    }    // 删除账号    public function delete(): void    {        if (!$this->id) return;        $pdo = Database::getInstance()->getPdo();        $stmt = $pdo->prepare("DELETE FROM users WHERE id = :uid");        $stmt->execute([':uid' => $this->id]);    }}

实战提醒:密码重置令牌用 bin2hex(random_bytes(32)) 生成 64 位随机字符串,比 md5(uniqid()) 安全得多——后者可预测,前者密码学安全。创建新令牌前先删旧令牌,保证同一邮箱只有一个有效令牌。

4.2 其他模型概览

• Post.php:分页查询(LIMIT + OFFSET + bindValue 绑定 INT)、总数统计、save() 方法自动判断新增/更新• Comment.php:按文章 ID 查评论列表(JOIN users 带出作者名)、新增评论、按 ID 查单条评论• Like.php / Favorite.php:toggle() 切换设计——已赞则删除、未赞则插入,利用 UNIQUE KEY 防重复• CommentLike.php:和 Like.php 结构一致,操作 comment_likes 表• Notification.php:send() 发通知(自己操作自己不发)、getUnreadCount() 未读数、markAllRead() 全部已读、clearAll() 清空

完整代码在配套资料包里,每个模型都是独立文件,可以直接放到项目里运行。

五、用户体系完整实现

5.1 图形验证码 captcha.php

用 PHP GD 库生成 4 位验证码图片,去除了容易混淆的 0/O、1/I 字符。验证码存 Session,登录时比对,比对完立即销毁。

<?phpob_clean();session_start();header("Content-Type: image/png");$width = 120;$height = 40;$image = imagecreatetruecolor($width, $height);$bgColor   = imagecolorallocate($image, 245, 247, 250);$textColor = imagecolorallocate($image, 37, 99, 235);$lineColor = imagecolorallocate($image, 200, 200, 200);imagefill($image, 0, 0, $bgColor);// 去除易混淆字符 0/O、1/I$chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';$code = '';for ($i = 0; $i < 4; $i++) {    $code .= $chars[mt_rand(0, strlen($chars) - 1)];}$_SESSION['captcha_code'] = strtolower($code);imagestring($image, 5, 32, 12, $code, $textColor);// 3条干扰线for ($i = 0; $i < 3; $i++) {    imageline($image, 0, mt_rand(0, $height), $width, mt_rand(0, $height), $lineColor);}imagepng($image);imagedestroy($image);exit;

踩坑提醒ob_clean() 必须放在最前面,清空之前所有输出。否则 header("Content-Type: image/png") 会报 "headers already sent" 错误,图片显示成乱码。文件末尾的 exit 也不能省,防止后面有空白字符输出。

5.2 用户注册 register.php

注册页支持头像上传、用户名/邮箱/密码校验、CSRF 防护。注册成功后自动写入 Session 跳转个人中心,不需要再手动登录一次。

<?phpspl_autoload_register(function ($className) {    $prefix = 'Blog\\';    $baseDir = __DIR__ . DIRECTORY_SEPARATOR;    if (str_starts_with($className, $prefix)) {        $relativeClass = substr($className, strlen($prefix));        $file = $baseDir . str_replace('\\', DIRECTORY_SEPARATOR, $relativeClass) . '.php';        if (file_exists($file)) require $file;    }});session_start();header("X-XSS-Protection: 1; mode=block");header("X-Frame-Options: DENY");use Blog\Models\User;if (empty($_SESSION['csrf_reg'])) {    $_SESSION['csrf_reg'] = bin2hex(random_bytes(32));}$errors = [];$fill = ['username' => '', 'email' => ''];$uploadDir = __DIR__ . '/uploads/avatar/';if (!is_dir($uploadDir)) @mkdir($uploadDir, 0755, true);$avatarPath = null;if ($_SERVER['REQUEST_METHOD'] === 'POST') {    if (!hash_equals($_SESSION['csrf_reg'], $_POST['csrf_token'] ?? '')) {        $errors['global'] = "非法提交请求";    }    $username = trim($_POST['username'] ?? '');    $password = $_POST['password'] ?? '';    $password2 = $_POST['password2'] ?? '';    $email = trim($_POST['email'] ?? '');    $fill['username'] = $username;    $fill['email'] = $email;    if (empty($username)) $errors['username'] = "用户名不能为空";    elseif (mb_strlen($username) < 3) $errors['username'] = "用户名至少3字符";    if (empty($password)) $errors['password'] = "密码不能为空";    elseif (strlen($password) < 6) $errors['password'] = "密码最少6位";    if ($password !== $password2) $errors['password2'] = "两次密码不一致";    if (empty($email)) $errors['email'] = "邮箱不能为空";    elseif (!filter_var($email, FILTER_VALIDATE_EMAIL)) $errors['email'] = "邮箱格式错误";    // 头像上传    if (isset($_FILES['avatar']) && $_FILES['avatar']['error'] !== UPLOAD_ERR_NO_FILE) {        $file = $_FILES['avatar'];        if ($file['error'] !== UPLOAD_ERR_OK) {            $errors['avatar'] = "文件上传失败";        } else {            $finfo = new finfo(FILEINFO_MIME_TYPE);            $mime = $finfo->file($file['tmp_name']);            if (!in_array($mime, ['image/jpeg','image/png'])) {                $errors['avatar'] = "仅支持jpg/png";            } elseif ($file['size'] > 1048576) {                $errors['avatar'] = "头像不超过1MB";            } else {                $ext = $mime === 'image/png' ? 'png' : 'jpg';                $name = md5(uniqid(true).$username).".".$ext;                move_uploaded_file($file['tmp_name'], $uploadDir.$name);                $avatarPath = "uploads/avatar/".$name;            }        }    }    if (empty($errors)) {        if (User::isExists($username, $email)) {            $errors['global'] = "用户名或邮箱已被注册";        } else {            $uid = User::create($username, $password, $email, $avatarPath);            $_SESSION['logged_in'] = true;            $_SESSION['user_id'] = $uid;            $_SESSION['username'] = $username;            $_SESSION['avatar'] = $avatarPath;            header("Location: welcome.php", true, 302);            exit;        }    }}?><!-- 下方为HTML表单,完整代码见资料包 -->

注册页的 HTML 部分包含用户名、密码、确认密码、邮箱、头像上传五个字段,每个字段下方都有错误提示位。完整 HTML 代码在资料包的 register.php 里。

5.3 用户登录 login.php

登录页整合了图形验证码,点击验证码图片可以刷新。验证码校验通过后立即 unset 销毁,防止重复使用。登录错误统一提示"用户名或密码错误",不告诉你是用户名错了还是密码错了——防止用户名枚举攻击。

<?php// 自动加载 + session_start(同上,省略)use Blog\Models\User;$error = '';$fillUser = '';if ($_SERVER['REQUEST_METHOD'] === 'POST') {    $username = trim($_POST['username'] ?? '');    $password = $_POST['password'] ?? '';    $captcha = strtolower(trim($_POST['captcha'] ?? ''));    $fillUser = $username;    if (empty($username) || empty($password)) {        $error = "用户名和密码不能为空";    } elseif (empty($captcha) || $captcha !== ($_SESSION['captcha_code'] ?? '')) {        $error = "验证码错误";    } else {        unset($_SESSION['captcha_code']); // 用完即销毁        $user = User::findByUsername($username);        if (!$user || !$user->verifyPassword($password)) {            $error = "用户名或密码错误";        } else {            $_SESSION['logged_in'] = true;            $_SESSION['user_id'] = $user->id;            $_SESSION['username'] = $user->username;            $_SESSION['avatar'] = $user->avatar;            header("Location: welcome.php", true, 302);            exit;        }    }}

5.4 个人中心 welcome.php

个人中心整合了头像展示、功能入口导航、未读通知红点提示。所有功能入口用网格布局排列,通知按钮右上角有红色数字角标显示未读数。

<?php// 自动加载 + session_start + 登录检查(省略)use Blog\Models\User;use Blog\Models\Notification;$userName = htmlspecialchars($_SESSION['username'], ENT_QUOTES);$avatar = $_SESSION['avatar'] ?? '';$unreadCount = Notification::getUnreadCount($_SESSION['user_id']);?><div class="box">    <h2>欢迎你,<?= $userName ?>!</h2>    <div class="avatar-wrap">        <?php if(!empty($avatar) && file_exists($avatar)): ?>            <img class="avatar-img" src="<?= htmlspecialchars($avatar, ENT_QUOTES) ?>" alt="头像">        <?php else: ?>            <div class="avatar-empty">暂无头像</div>        <?php endif; ?>    </div>    <div class="btn-wrap">        <a href="home.php" class="btn btn-blue">浏览文章</a>        <a href="create_post.php" class="btn btn-blue">发布文章</a>        <a href="my_favorites.php" class="btn btn-blue">我的收藏</a>        <a href="profile.php" class="btn btn-blue">修改资料</a>        <a href="change_password.php" class="btn btn-blue">修改密码</a>        <a href="notifications.php" class="btn btn-blue">            我的通知            <?php if($unreadCount > 0): ?>                <span class="badge"><?= $unreadCount ?></span>            <?php endif; ?>        </a>        <a href="logout.php" class="btn btn-gray">退出登录</a>        <a href="delete_user.php" class="btn btn-red">永久注销账号</a>    </div></div>

5.5 修改资料 / 修改密码 / 忘记密码 / 重置密码

这四个页面逻辑各不相同但互相关联:

• profile.php:修改邮箱和头像,用户名不可改(disabled 属性)。上传新头像会覆盖旧头像路径• change_password.php:需输入原密码验证身份,修改成功后立即销毁 Session 跳转登录页(强制重新登录)• forgot_password.php:输入注册邮箱,生成重置令牌存入 password_resets 表(30分钟有效)。生产环境应发邮件,这里直接返回链接方便测试• reset_password.php:从 URL 获取 token,验证有效后允许设置新密码。成功后删除令牌防止重复使用

实战提醒:change_password.php 修改成功后用 header("refresh:2;url=login.php") 延迟跳转,给用户 2 秒看到"修改成功"的提示。注意此时 Session 已经销毁,下方的表单 HTML 不能再输出——用 exit 终止脚本。

这四个文件的完整代码在资料包里,每个都是独立可运行的 PHP 文件。

六、文章体系完整实现

6.1 文章列表(分页)home.php

首页用 LIMIT + OFFSET 分页,每页 5 篇。封面有就显示缩略图,没有就显示灰色占位块。底部有上一页/下一页导航,当前页码显示"第 X / Y 页"。

<?php// 自动加载 + session_start(省略)use Blog\Models\Post;$page = max(1, (int)($_GET['page'] ?? 1));$perPage = 5;$postList = Post::getPaginated($page, $perPage);$totalPosts = Post::getTotalCount();$totalPages = max(1, ceil($totalPosts / $perPage));?><!-- 文章列表 + 分页导航 --><?php foreach($postList as $p): ?><div class="item">    <?php if(!empty($p->cover) && file_exists($p->cover)): ?>        <img class="item-cover" src="<?= htmlspecialchars($p->cover) ?>" alt="封面">    <?php else: ?>        <div class="item-cover"></div>    <?php endif; ?>    <div class="item-body">        <div class="title">            <a href="view_post.php?id=<?= $p->id ?>"><?= htmlspecialchars($p->title) ?></a>        </div>        <div class="meta">作者:<?= htmlspecialchars($p->authorName) ?> · 发布于 <?= $p->createdAt ?></div>    </div></div><?php endforeach; ?><!-- 分页 --><div class="pagination">    <?php if($page > 1): ?>        <a href="?page=<?= $page-1 ?>">上一页</a>    <?php else: ?>        <span class="disabled">上一页</span>    <?php endif; ?>    <span class="current">第 <?= $page ?> / <?= $totalPages ?> 页</span>    <?php if($page < $totalPages): ?>        <a href="?page=<?= $page+1 ?>">下一页</a>    <?php else: ?>        <span class="disabled">下一页</span>    <?php endif; ?></div>

6.2 发布文章 create_post.php

支持标题、封面上传(2MB 限制)、正文内容,带完整校验。封面上传和头像上传逻辑一样:finfo 检测真实 MIME、随机文件名、存入 uploads/post/ 目录。

<?php// 自动加载 + session_start + 登录检查(省略)use Blog\Models\Post;if (empty($_SESSION['csrf_post'])) {    $_SESSION['csrf_post'] = bin2hex(random_bytes(32));}$errors = [];$uploadDir = __DIR__ . '/uploads/post/';if (!is_dir($uploadDir)) @mkdir($uploadDir, 0755, true);if ($_SERVER['REQUEST_METHOD'] === 'POST') {    if (!hash_equals($_SESSION['csrf_post'], $_POST['csrf_token'] ?? '')) {        $errors[] = "非法请求";    }    $title = trim($_POST['title'] ?? '');    $content = trim($_POST['content'] ?? '');    if (empty($title)) $errors[] = "标题不能为空";    elseif (mb_strlen($title) > 200) $errors[] = "标题不能超过200字";    if (empty($content)) $errors[] = "正文不能为空";    // 封面上传    $coverPath = null;    if (isset($_FILES['cover']) && $_FILES['cover']['error'] === UPLOAD_ERR_OK) {        $file = $_FILES['cover'];        $finfo = new finfo(FILEINFO_MIME_TYPE);        $mime = $finfo->file($file['tmp_name']);        if (in_array($mime, ['image/jpeg','image/png']) && $file['size'] <= 2097152) {            $ext = $mime === 'image/png' ? 'png' : 'jpg';            $name = md5(uniqid(true)).".".$ext;            move_uploaded_file($file['tmp_name'], $uploadDir.$name);            $coverPath = "uploads/post/".$name;        } else {            $errors[] = "封面仅支持jpg/png,不超过2MB";        }    }    if (empty($errors)) {        $post = new Post();        $post->userId = $_SESSION['user_id'];        $post->title = $title;        $post->cover = $coverPath;        $post->content = $content;        $post->save();        header("Location: view_post.php?id=".$post->id, true, 302);        exit;    }}

6.3 文章详情页 view_post.php

详情页是整个项目最复杂的页面——整合了封面展示、正文渲染、点赞收藏按钮、评论区(带评论点赞/删除)、作者操作按钮(编辑/删除)。所有交互用 fetch API 实现 AJAX 无刷新操作。

<?php// 自动加载 + session_start(省略)use Blog\Models\Post;use Blog\Models\Comment;use Blog\Models\Like;use Blog\Models\Favorite;use Blog\Models\CommentLike;$id = (int)($_GET['id'] ?? 0);$post = Post::find($id);if(!$post){    header("Location: home.php", true, 302);    exit;}$isAuthor = !empty($_SESSION['logged_in']) && $_SESSION['user_id'] == $post->userId;$comments = Comment::getByPostId($id);$likeCount = Like::getCount($id);$favoriteCount = Favorite::getCount($id);$isLiked = false;$isFavorited = false;if (!empty($_SESSION['logged_in'])) {    $isLiked = Like::checkUserLike($id, $_SESSION['user_id']);    $isFavorited = Favorite::checkUserFavorite($id, $_SESSION['user_id']);}$csrfToken = $_SESSION['csrf_interact'] ?? bin2hex(random_bytes(32));$_SESSION['csrf_interact'] = $csrfToken;?><!-- 文章卡片 --><article class="post-card">    <?php if(!empty($post->cover) && file_exists($post->cover)): ?>        <img class="post-cover" src="<?= htmlspecialchars($post->cover) ?>" alt="封面">    <?php endif; ?>    <h1 class="post-title"><?= htmlspecialchars($post->title) ?></h1>    <div class="post-meta">        <span>作者:<?= htmlspecialchars($post->authorName) ?></span>        <span>发布于 <?= $post->createdAt ?></span>    </div>    <div class="post-content"><?= nl2br(htmlspecialchars($post->content)) ?></div>    <!-- 点赞收藏按钮 -->    <div class="interact-bar">        <button class="interact-btn js-post-like <?= $isLiked ? 'active' : '' ?>" data-id="<?= $post->id ?>">            👍 点赞 <span class="like-count"><?= $likeCount ?></span>        </button>        <button class="interact-btn js-post-favorite <?= $isFavorited ? 'active' : '' ?>" data-id="<?= $post->id ?>">            ⭐ 收藏 <span class="favorite-count"><?= $favoriteCount ?></span>        </button>    </div>    <?php if($isAuthor): ?>        <a href="edit_post.php?id=<?= $post->id ?>" class="btn btn-edit">编辑</a>        <a href="delete_post.php?id=<?= $post->id ?>" class="btn btn-del">删除</a>    <?php endif; ?></article><!-- 评论区 --><div class="comment-card">    <h2 class="comment-title">评论区(<span class="comment-total"><?= count($comments) ?></span>)</h2>    <div class="comment-list js-comment-list">        <?php foreach($comments as $c): ?>        <div class="comment-item" data-id="<?= $c->id ?>">            <div class="comment-header">                <span class="comment-name"><?= htmlspecialchars($c->authorName) ?></span>                <span class="comment-time"><?= $c->createdAt ?></span>            </div>            <div class="comment-content"><?= nl2br(htmlspecialchars($c->content)) ?></div>            <div class="comment-footer">                <button class="comment-like-btn js-comment-like" data-id="<?= $c->id ?>">                    👍 <span class="comment-like-count"><?= CommentLike::getCount($c->id) ?></span>                </button>                <?php if($isAuthor || $_SESSION['user_id'] ?? 0 == $c->userId): ?>                    <button class="comment-del-btn js-delete-comment" data-id="<?= $c->id ?>">删除</button>                <?php endif; ?>            </div>        </div>        <?php endforeach; ?>    </div>    <?php if(!empty($_SESSION['logged_in'])): ?>        <form class="comment-form">            <textarea placeholder="写下你的评论..." required></textarea>            <button type="submit">发表评论</button>        </form>    <?php else: ?>        <p class="login-tip"><a href="login.php">登录</a> 后即可发表评论</p>    <?php endif; ?></div><script>const csrfToken = '<?= htmlspecialchars($csrfToken) ?>';const isLoggedIn = <?= !empty($_SESSION['logged_in']) ? 'true' : 'false' ?>;// AJAX 通用请求function ajaxPost(url, data) {    return fetch(url, {        method: 'POST',        headers: {'Content-Type': 'application/x-www-form-urlencoded', 'X-Requested-With': 'XMLHttpRequest'},        body: new URLSearchParams(data).toString()    }).then(res => res.json());}// 文章点赞document.querySelector('.js-post-like').addEventListener('click', function() {    if (!isLoggedIn) { alert('请先登录'); return; }    ajaxPost('toggle_like.php', {post_id: this.dataset.id, csrf_token: csrfToken})        .then(res => {            if (res.code === 1) {                this.classList.toggle('active', res.is_liked);                this.querySelector('.like-count').textContent = res.count;            }        });});// 文章收藏、评论点赞、删除评论、发表评论逻辑类似// 完整 JS 代码见资料包</script>

实战提醒:评论的点赞和删除按钮用了事件委托(addEventListener 绑在父容器上),因为评论是 AJAX 动态追加的,直接绑事件会丢失。用 e.target.closest('.js-comment-like') 判断点击的是不是目标按钮。

6.4 编辑文章 / 删除文章

编辑文章 edit_post.php 和发布文章逻辑基本一样,区别是预填已有数据,save() 时走 UPDATE 分支。删除文章 delete_post.php 有确认页面,防止误点。两个页面都校验 $post->userId != $_SESSION['user_id'] 防越权操作。

七、交互与通知体系

7.1 评论提交 add_comment.php

评论提交支持 AJAX 和普通表单两种模式。AJAX 模式返回 JSON 数据(前端无刷新追加评论),普通模式跳转回文章页。通过 HTTP_X_REQUESTED_WITH 请求头判断是哪种模式。

<?php// 自动加载 + session_start(省略)use Blog\Models\Comment;if (empty($_SESSION['logged_in'])) {    echo json_encode(['code' => 0, 'msg' => '请先登录']);    exit;}$isAjax = isset($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH'] === 'XMLHttpRequest';if ($_SERVER['REQUEST_METHOD'] === 'POST') {    $postId = (int)($_POST['post_id'] ?? 0);    $content = trim($_POST['content'] ?? '');    $token = $_POST['csrf_token'] ?? '';    if (!hash_equals($_SESSION['csrf_interact'] ?? '', $token)) {        $msg = '非法请求';        if ($isAjax) { echo json_encode(['code' => 0, 'msg' => $msg]); exit; }        else die($msg);    }    if ($postId <= 0 || empty($content)) {        $msg = '评论内容不能为空';        if ($isAjax) { echo json_encode(['code' => 0, 'msg' => $msg]); exit; }        else { header("Location: view_post.php?id=$postId"); exit; }    }    $commentId = Comment::create($postId, $_SESSION['user_id'], $content);    if ($isAjax) {        echo json_encode([            'code' => 1,            'msg' => '评论成功',            'data' => [                'id' => $commentId,                'author_name' => $_SESSION['username'],                'content' => $content,                'created_at' => date('Y-m-d H:i:s'),                'like_count' => 0,                'can_delete' => true            ]        ]);        exit;    } else {        header("Location: view_post.php?id=$postId", true, 302);        exit;    }}

7.2 点赞切换 toggle_like.php

点赞成功后自动给文章作者发站内通知。关键细节:if ($userId === $triggerUserId) return;——自己赞自己的文章不发通知,避免骚扰。文章标题超 20 字会截断加省略号。

<?php// 自动加载 + session_start(省略)use Blog\Models\Like;use Blog\Models\Post;use Blog\Models\Notification;if (empty($_SESSION['logged_in'])) {    echo json_encode(['code' => 0, 'msg' => '请先登录']);    exit;}if ($_SERVER['REQUEST_METHOD'] === 'POST') {    $postId = (int)($_POST['post_id'] ?? 0);    $token = $_POST['csrf_token'] ?? '';    if (!hash_equals($_SESSION['csrf_interact'] ?? '', $token)) {        echo json_encode(['code' => 0, 'msg' => '非法请求']); exit;    }    $isLikedAfter = Like::toggle($postId, $_SESSION['user_id']);    $likeCount = Like::getCount($postId);    // 点赞成功时发通知给文章作者    if ($isLikedAfter) {        $post = Post::find($postId);        if ($post) {            $shortTitle = mb_substr($post->title, 0, 20);            if (mb_strlen($post->title) > 20) $shortTitle .= '…';            $content = "赞了你的文章《".$shortTitle."》";            Notification::send($post->userId, 'like', $_SESSION['user_id'], $postId, $content);        }    }    echo json_encode([        'code' => 1,        'is_liked' => $isLikedAfter,        'count' => $likeCount    ]);    exit;}

收藏切换 toggle_favorite.php、评论点赞 toggle_comment_like.php、删除评论 delete_comment.php 的结构和 toggle_like.php 基本一致,区别是操作的表和通知内容不同。完整代码在资料包里。

7.3 我的通知 notifications.php

通知列表页支持全部标记已读和一键清空。未读通知左侧有蓝色竖条标记,已读的没有。通知内容格式:"XXX 赞了你的文章《标题》"、"XXX 收藏了你的文章《标题》"、"XXX 赞了你的评论:「评论摘要」"。

<?php// 自动加载 + session_start + 登录检查(省略)use Blog\Models\Notification;// 全部标记已读if (isset($_GET['action']) && $_GET['action'] === 'readall') {    Notification::markAllRead($_SESSION['user_id']);    header("Location: notifications.php", true, 302); exit;}// 一键清空if (isset($_GET['action']) && $_GET['action'] === 'clearall') {    Notification::clearAll($_SESSION['user_id']);    header("Location: notifications.php", true, 302); exit;}$list = Notification::getUserAll($_SESSION['user_id']);?><?php foreach($list as $item): ?><div class="item <?= $item->isRead ? '' : 'unread' ?>">    <div class="item-content">        <strong><?= htmlspecialchars($item->triggerUserName) ?></strong>        <?= htmlspecialchars($item->content) ?>    </div>    <div class="item-time"><?= $item->createdAt ?></div></div><?php endforeach; ?>

八、辅助功能

退出登录 logout.php 三行代码搞定:清空 Session、销毁会话、跳转首页。注销账号 delete_user.php 有确认页面,确认后调用 $currentUser->delete() 删除用户——外键级联删除会自动清掉该用户的所有文章、评论、点赞、收藏数据。

// logout.php<?phpsession_start();$_SESSION = [];session_destroy();header("Location: home.php", true, 302);exit;

九、上传 GitHub 与部署上线

9.1 项目配置与 .gitignore

上线前需要把数据库配置抽到独立文件,用 .gitignore 排除敏感文件和上传目录。Git 不跟踪空目录,uploads 子目录下放 .gitkeep 占位。

# .gitignoreconfig.phpuploads/avatar/*uploads/post/*!uploads/**/.gitkeep.vscode/.idea/*.swp.DS_StoreThumbs.db*.logtmp/cache/phpinfo.phptest_*.php

9.2 Git 推送命令

git initgit add .git commit -m "完整PHP博客系统:用户/文章/评论/点赞/通知"git remote add origin https://github.com/你的用户名/php-blog-system.gitgit branch -M maingit push -u origin main

9.3 服务器部署(Ubuntu + Nginx + PHP-FPM)

生产环境部署完整流程:安装 Nginx + PHP-FPM + MariaDB → 克隆代码 → 创建数据库和专用用户 → 导入 SQL → 配置 Nginx 站点 → 设置权限 → 配置 php.ini 安全项。

# 安装环境sudo apt install nginx php8.1-fpm php8.1-mysql php8.1-gd php8.1-mbstring -ysudo apt install mariadb-server git -y# 克隆代码sudo git clone https://github.com/你的用户名/php-blog-system.git /var/www/blog# 创建数据库和用户sudo mysql -u root -pCREATE DATABASE blog DEFAULT CHARACTER SET utf8mb4;CREATE USER 'blog_user'@'localhost' IDENTIFIED BY '强密码';GRANT ALL PRIVILEGES ON blog.* TO 'blog_user'@'localhost';FLUSH PRIVILEGES;# 导入SQLmysql -u blog_user -p blog < /var/www/blog/install.sql# 权限设置sudo chown -R www-data:www-data /var/www/blog/sudo chmod -R 755 /var/www/blog/uploads/sudo chmod 600 /var/www/blog/config.php

9.4 Nginx 站点配置

server {    listen 80;    server_name 你的域名或IP;    root /var/www/blog;    index home.php index.html;    location ~ \.php$ {        include snippets/fastcgi-php.conf;        fastcgi_pass unix:/run/php/php8.1-fpm.sock;    }    # 禁止访问敏感文件    location ~* /config\.php$ { deny all; }    location ~* /\.git { deny all; }    location ~ /\.ht { deny all; }}

9.5 php.ini 安全配置

; 关闭错误显示(防止泄露路径)display_errors = Offlog_errors = Onerror_log = /var/log/php/error.log; Session 安全session.cookie_httponly = Onsession.cookie_samesite = "Lax"; 禁用危险函数disable_functions = exec,passthru,shell_exec,system,proc_open,popen; 上传限制upload_max_filesize = 2Mpost_max_size = 8M

十、功能测试验证

部署完成后,按以下模块逐项验证。每个模块都有对应的测试用例和预期结果。

10.1 用户注册测试

• 正常注册 → 成功跳转个人中心,数据库新增哈希密码• 重复用户名 → 提示"用户名或邮箱已被注册"• 邮箱格式错误 → 提示"邮箱格式错误"• 密码过短 → 提示"密码最少6位"• 头像上传 → 注册成功,uploads/avatar/ 有新文件

10.2 验证码与登录测试

10.3 密码管理测试

10.4 文章发布与管理测试

10.5 交互功能测试

10.6 通知系统测试

10.7 安全防护验证

• XSS 防护:评论输入 <script>alert(1)</script> → 页面正常显示文本,不弹窗• SQL 注入:登录框输入 ' or 1=1 -- → 登录失败,PDO 预处理拦截• 文件上传:php 脚本改名为 test.jpg.php → 上传失败,finfo 检测真实 MIME• CSRF:删除表单 csrf_token 隐藏域再提交 → 提示"非法请求"• Session:PHPSESSID 带 HttpOnly,JS 无法读取

十一、全文总结

完成这个项目后,你已经具备了独立开发中小型 Web 系统的完整能力:

• 语法基础:PHP 核心语法、数组字符串、表单处理、文件上传、会话管理• 数据库:表结构设计、多表关联、外键约束、PDO 预处理、分页查询• 编程思想:面向对象、命名空间、自动加载、单例模式、MVC 分层思想• 安全意识:SQL 注入、XSS、CSRF、文件上传漏洞的原理与防护• 项目能力:从需求分析、架构设计到功能落地的完整开发流程

从第一篇的 echo "Hello World" 到这篇的完整博客系统,PHP 入门之旅到此圆满收官。后续可以继续探索 API 开发、Laravel 框架、微服务架构等进阶方向。

文字整理过程中借助了 AI 工具辅助排版和代码校对,核心代码和知识点均经过实际验证。

本文配套资料包

代码合集
速查手册
实战练习题
面试考点卡
公众号回复php08获取

资料下载

-END-

最新文章

随机文章

基本 文件 流程 错误 SQL 调试
  1. 请求信息 : 2026-08-21 19:59:10 HTTP/2.0 GET : https://f.mffb.com.cn/a/506902.html
  2. 运行时间 : 0.197653s [ 吞吐率:5.06req/s ] 内存消耗:4,936.48kb 文件加载:140
  3. 缓存信息 : 0 reads,0 writes
  4. 会话信息 : SESSION_ID=45f63434f3bf1e955d53d0cf33aab184
  1. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/public/index.php ( 0.79 KB )
  2. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/autoload.php ( 0.17 KB )
  3. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/composer/autoload_real.php ( 2.49 KB )
  4. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/composer/platform_check.php ( 0.90 KB )
  5. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/composer/ClassLoader.php ( 14.03 KB )
  6. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/composer/autoload_static.php ( 4.90 KB )
  7. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-helper/src/helper.php ( 8.34 KB )
  8. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-validate/src/helper.php ( 2.19 KB )
  9. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/helper.php ( 1.47 KB )
  10. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/stubs/load_stubs.php ( 0.16 KB )
  11. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Exception.php ( 1.69 KB )
  12. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-container/src/Facade.php ( 2.71 KB )
  13. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/symfony/deprecation-contracts/function.php ( 0.99 KB )
  14. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/symfony/polyfill-mbstring/bootstrap.php ( 8.26 KB )
  15. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/symfony/polyfill-mbstring/bootstrap80.php ( 9.78 KB )
  16. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/symfony/var-dumper/Resources/functions/dump.php ( 1.49 KB )
  17. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-dumper/src/helper.php ( 0.18 KB )
  18. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/symfony/var-dumper/VarDumper.php ( 4.30 KB )
  19. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/App.php ( 15.30 KB )
  20. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-container/src/Container.php ( 15.76 KB )
  21. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/psr/container/src/ContainerInterface.php ( 1.02 KB )
  22. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/provider.php ( 0.19 KB )
  23. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Http.php ( 6.04 KB )
  24. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-helper/src/helper/Str.php ( 7.29 KB )
  25. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Env.php ( 4.68 KB )
  26. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/common.php ( 0.03 KB )
  27. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/helper.php ( 18.78 KB )
  28. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Config.php ( 5.54 KB )
  29. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/app.php ( 0.95 KB )
  30. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/cache.php ( 0.78 KB )
  31. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/console.php ( 0.23 KB )
  32. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/cookie.php ( 0.56 KB )
  33. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/database.php ( 2.48 KB )
  34. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/facade/Env.php ( 1.67 KB )
  35. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/filesystem.php ( 0.61 KB )
  36. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/lang.php ( 0.91 KB )
  37. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/log.php ( 1.35 KB )
  38. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/middleware.php ( 0.19 KB )
  39. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/route.php ( 1.89 KB )
  40. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/session.php ( 0.57 KB )
  41. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/trace.php ( 0.34 KB )
  42. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/config/view.php ( 0.82 KB )
  43. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/event.php ( 0.25 KB )
  44. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Event.php ( 7.67 KB )
  45. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/service.php ( 0.13 KB )
  46. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/AppService.php ( 0.26 KB )
  47. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Service.php ( 1.64 KB )
  48. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Lang.php ( 7.35 KB )
  49. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/lang/zh-cn.php ( 13.70 KB )
  50. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/initializer/Error.php ( 3.31 KB )
  51. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/initializer/RegisterService.php ( 1.33 KB )
  52. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/services.php ( 0.14 KB )
  53. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/service/PaginatorService.php ( 1.52 KB )
  54. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/service/ValidateService.php ( 0.99 KB )
  55. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/service/ModelService.php ( 2.04 KB )
  56. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-trace/src/Service.php ( 0.77 KB )
  57. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Middleware.php ( 6.72 KB )
  58. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/initializer/BootService.php ( 0.77 KB )
  59. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/Paginator.php ( 11.86 KB )
  60. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-validate/src/Validate.php ( 63.20 KB )
  61. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/Model.php ( 23.55 KB )
  62. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/model/concern/Attribute.php ( 21.05 KB )
  63. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/model/concern/AutoWriteData.php ( 4.21 KB )
  64. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/model/concern/Conversion.php ( 6.44 KB )
  65. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/model/concern/DbConnect.php ( 5.16 KB )
  66. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/model/concern/ModelEvent.php ( 2.33 KB )
  67. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/model/concern/RelationShip.php ( 28.29 KB )
  68. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-helper/src/contract/Arrayable.php ( 0.09 KB )
  69. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-helper/src/contract/Jsonable.php ( 0.13 KB )
  70. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/model/contract/Modelable.php ( 0.09 KB )
  71. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Db.php ( 2.88 KB )
  72. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/DbManager.php ( 8.52 KB )
  73. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Log.php ( 6.28 KB )
  74. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Manager.php ( 3.92 KB )
  75. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/psr/log/src/LoggerTrait.php ( 2.69 KB )
  76. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/psr/log/src/LoggerInterface.php ( 2.71 KB )
  77. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Cache.php ( 4.92 KB )
  78. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/psr/simple-cache/src/CacheInterface.php ( 4.71 KB )
  79. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-helper/src/helper/Arr.php ( 16.63 KB )
  80. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/cache/driver/File.php ( 7.84 KB )
  81. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/cache/Driver.php ( 9.03 KB )
  82. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/contract/CacheHandlerInterface.php ( 1.99 KB )
  83. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/Request.php ( 0.09 KB )
  84. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Request.php ( 55.78 KB )
  85. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/middleware.php ( 0.25 KB )
  86. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Pipeline.php ( 2.61 KB )
  87. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-trace/src/TraceDebug.php ( 3.40 KB )
  88. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/middleware/SessionInit.php ( 1.94 KB )
  89. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Session.php ( 1.80 KB )
  90. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/session/driver/File.php ( 6.27 KB )
  91. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/contract/SessionHandlerInterface.php ( 0.87 KB )
  92. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/session/Store.php ( 7.12 KB )
  93. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Route.php ( 23.73 KB )
  94. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/route/RuleName.php ( 5.75 KB )
  95. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/route/Domain.php ( 2.53 KB )
  96. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/route/RuleGroup.php ( 22.43 KB )
  97. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/route/Rule.php ( 26.95 KB )
  98. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/route/RuleItem.php ( 9.78 KB )
  99. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/route/app.php ( 1.72 KB )
  100. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/facade/Route.php ( 4.70 KB )
  101. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/route/dispatch/Controller.php ( 4.74 KB )
  102. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/route/Dispatch.php ( 10.44 KB )
  103. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/controller/Index.php ( 4.81 KB )
  104. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/app/BaseController.php ( 2.05 KB )
  105. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/facade/Db.php ( 0.93 KB )
  106. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/connector/Mysql.php ( 5.44 KB )
  107. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/PDOConnection.php ( 52.47 KB )
  108. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/Connection.php ( 8.39 KB )
  109. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/ConnectionInterface.php ( 4.57 KB )
  110. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/builder/Mysql.php ( 16.58 KB )
  111. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/Builder.php ( 24.06 KB )
  112. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/BaseBuilder.php ( 27.50 KB )
  113. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/Query.php ( 15.71 KB )
  114. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/BaseQuery.php ( 45.13 KB )
  115. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/TimeFieldQuery.php ( 7.43 KB )
  116. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/AggregateQuery.php ( 3.26 KB )
  117. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/ModelRelationQuery.php ( 20.07 KB )
  118. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/ParamsBind.php ( 3.66 KB )
  119. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/ResultOperation.php ( 7.01 KB )
  120. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/WhereQuery.php ( 19.37 KB )
  121. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/JoinAndViewQuery.php ( 7.11 KB )
  122. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/TableFieldInfo.php ( 2.63 KB )
  123. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-orm/src/db/concern/Transaction.php ( 2.77 KB )
  124. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/log/driver/File.php ( 5.96 KB )
  125. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/contract/LogHandlerInterface.php ( 0.86 KB )
  126. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/log/Channel.php ( 3.89 KB )
  127. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/event/LogRecord.php ( 1.02 KB )
  128. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-helper/src/Collection.php ( 16.47 KB )
  129. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/facade/View.php ( 1.70 KB )
  130. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/View.php ( 4.39 KB )
  131. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Response.php ( 8.81 KB )
  132. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/response/View.php ( 3.29 KB )
  133. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/Cookie.php ( 6.06 KB )
  134. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-view/src/Think.php ( 8.38 KB )
  135. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/framework/src/think/contract/TemplateHandlerInterface.php ( 1.60 KB )
  136. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-template/src/Template.php ( 46.61 KB )
  137. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-template/src/template/driver/File.php ( 2.41 KB )
  138. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-template/src/template/contract/DriverInterface.php ( 0.86 KB )
  139. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/runtime/temp/067d451b9a0c665040f3f1bdd3293d68.php ( 11.98 KB )
  140. /yingpanguazai/ssd/ssd1/www/f.mffb.com.cn/vendor/topthink/think-trace/src/Html.php ( 4.42 KB )
  1. CONNECT:[ UseTime:0.000912s ] mysql:host=127.0.0.1;port=3306;dbname=f_mffb;charset=utf8mb4
  2. SHOW FULL COLUMNS FROM `fenlei` [ RunTime:0.001468s ]
  3. SELECT * FROM `fenlei` WHERE `fid` = 0 [ RunTime:0.000725s ]
  4. SELECT * FROM `fenlei` WHERE `fid` = 63 [ RunTime:0.000571s ]
  5. SHOW FULL COLUMNS FROM `set` [ RunTime:0.001261s ]
  6. SELECT * FROM `set` [ RunTime:0.000534s ]
  7. SHOW FULL COLUMNS FROM `article` [ RunTime:0.001361s ]
  8. SELECT * FROM `article` WHERE `id` = 506902 LIMIT 1 [ RunTime:0.001813s ]
  9. UPDATE `article` SET `lasttime` = 1787313550 WHERE `id` = 506902 [ RunTime:0.009228s ]
  10. SELECT * FROM `fenlei` WHERE `id` = 64 LIMIT 1 [ RunTime:0.000729s ]
  11. SELECT * FROM `article` WHERE `id` < 506902 ORDER BY `id` DESC LIMIT 1 [ RunTime:0.001614s ]
  12. SELECT * FROM `article` WHERE `id` > 506902 ORDER BY `id` ASC LIMIT 1 [ RunTime:0.001682s ]
  13. SELECT * FROM `article` WHERE `id` < 506902 ORDER BY `id` DESC LIMIT 10 [ RunTime:0.003490s ]
  14. SELECT * FROM `article` WHERE `id` < 506902 ORDER BY `id` DESC LIMIT 10,10 [ RunTime:0.004788s ]
  15. SELECT * FROM `article` WHERE `id` < 506902 ORDER BY `id` DESC LIMIT 20,10 [ RunTime:0.003999s ]
0.201360s