基于PHP后端搭建的简单博客系统介绍

第一部分:项目概述

1. 项目名称

  • 基于PHPStudy的简易个人博客系统

2. 项目简介

  • 这是一个采用经典WAMP(Windows/Apache/MySQL/PHP)架构的轻量级博客系统。系统部署在高度集成的PHPStudy环境中,实现了博客文章的动态发布、管理和展示等核心功能。该系统结构清晰、代码简单,非常适合作为Web开发的入门学习项目。

3. 项目目标

  • 主要通过搭建简单的博客系统,来理解掌握PHP原生语法、MySQL数据库操作以及前后端数据交互的基本原理。

第二部分:技术框架

技术选型 版本 说明
开发环境 PHPStudy v8 核心工具。集成了Apache、MySQL、PHP等必要组件,极大简化了环境配置流程,实现一键启动服务。
服务器 Apache 2.4.39 Web服务器,负责处理HTTP请求和响应。
后端语言 PHP 7.3.4 服务器端脚本语言,负责处理业务逻辑,如连接数据库、处理表单提交、生成动态页面。
数据库 MySQL 5.7.26 关系型数据库,用于存储博客的核心数据,如文章、评论、用户信息等。
前端技术 HTML, CSS 构建用户界面和交互。HTML负责页面结构,CSS负责样式美化。
数据库操作方式 PDO PHP连接和操作MySQL数据库的扩展。

第三部分:项目功能

  • 此项目包含的功能有基本的用户注册、登录、登出,以及文章写入、编辑、删除和评论功能,部分前端写出来的功能没有在后端实现。

1. 注册功能

1

2. 登录功能

2

3. 按发布顺序在首页展示用户文章

  • 在首页我们可以看到我们发布过的文章,包括文章的作者,发布时间等信息。

3

4. 写文章、编辑文章

  • 在控制台里我们可以编辑之前写过的文章,或者写新文章,又或者删除文章,同时可以看到文章的发布状态。

4

5. 评论功能

  • 在写好的文章下用户可以看到评论功能,并对文章做出评价。

5

第四部分:数据库设计

  • 下面是数据库部分的设计代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
-- 用户表
CREATE TABLE users (
id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50) UNIQUE NOT NULL,
email VARCHAR(100) UNIQUE NOT NULL,
password VARCHAR(255) NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
reset_token VARCHAR(255),
reset_token_expiry DATETIME
);

-- 博客文章表
CREATE TABLE posts (
id INT AUTO_INCREMENT PRIMARY KEY,
title VARCHAR(255) NOT NULL,
content TEXT NOT NULL,
author_id INT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (author_id) REFERENCES users(id)
);

-- 评论表
CREATE TABLE comments (
id INT AUTO_INCREMENT PRIMARY KEY,
post_id INT NOT NULL,
user_id INT NOT NULL,
content TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
parent_id INT, -- 用于回复功能
FOREIGN KEY (post_id) REFERENCES posts(id),
FOREIGN KEY (user_id) REFERENCES users(id),
FOREIGN KEY (parent_id) REFERENCES comments(id)
);

1. 用户表(users)

1
2
CREATE TABLE users (
id INT AUTO_INCREMENT PRIMARY KEY,
  • CREATE TABLE users:创建名为users的表

  • id INT AUTO_INCREMENT PRIMARY KEY:用户唯一标识符id,自动递增AUTO_INCREMENT的整数INT,作为主键PRIMARY KEY

1
username VARCHAR(50) UNIQUE NOT NULL,
  • username VARCHAR(50):用户名,限制长度为50个字符。

  • UNIQUE:数据库中的UNIQUE约束,确保一列或多列中所有的值都是唯一的,所以在约束应用的列中不能有重复的值。

  • NOT NULL:在UNIQUE约束中允许列中的值为空,所以这里用NOT NULL来规定列中的值不能为空值。

1
2
3
email VARCHAR(100) UNIQUE NOT NULL,
email VARCHAR(100) UNIQUE NOT NULL,
password VARCHAR(255) NOT NULL,
  • 与上面的写法基本类似,不再解释。
  • 这里的密码表没有使用UNIQUE约束是因为密码本就是只有用户自己知道,所以一致没有影响,而且VARCHAR(255)是因为我们需要存储HASH加密后的密码,所以长度限定为255个字符。
1
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
  • 当插入新的值时MySQL会自动将created_at字段的值设置成当前的日期和时间。
1
2
3
reset_token VARCHAR(255),
reset_token_expiry DATETIME
);
  • reset_token:密码重置令牌(用于“忘记密码”功能)。
  • reset_token_expiry:令牌过期时间。

2. 文章表(posts)

1
2
CREATE TABLE posts (
id INT AUTO_INCREMENT PRIMARY KEY,
  • 与用户表类似,文章的唯一标识符,自动递增主键。
1
title VARCHAR(255) NOT NULL,
  • 存放文章标题,最长255字符,不能为空。
1
content TEXT NOT NULL,
  • 存放文章内容,TEXT类型适合存储长文本,不能为空。
1
author_id INT NOT NULL,
  • 存放作者的用户ID,外键关联到users表。
1
2
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
  • created_at:文章创建时间,由MySQL自动设置。
  • updated_at:文章最后修改时间,由MySQL自动设置。
1
2
FOREIGN KEY (author_id) REFERENCES users(id)
);
  • 外键约束:确保author_id必须存在于users表的id中。

3. 评论表(comments)

1
2
CREATE TABLE comments (
id INT AUTO_INCREMENT PRIMARY KEY,
  • 评论的唯一标识符
1
post_id INT NOT NULL,
  • post_id:用来存放评论所属的文章ID。
1
user_id INT NOT NULL,
  • user_id:用来存放发表评论的用户ID。
1
content TEXT NOT NULL,
  • content:用来存放评论内容。
1
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
  • 存放评论发表时间,由MySQL自行设置。
1
parent_id INT,
  • parent_id:实现回复功能,指向被回复的评论ID。
1
2
3
4
FOREIGN KEY (post_id) REFERENCES posts(id),
FOREIGN KEY (user_id) REFERENCES users(id),
FOREIGN KEY (parent_id) REFERENCES comments(id)
);
  • 三个外键约束:确保数据完整性和关联性。

第五部分:后端实现

  • 整个博客系统的树形图展示如下:(其中CSS部分不会在本次汇报中展示)。

6

1. includes文件夹

  • 此文件夹下包含了两个文件auth.phpdb.php

1.1 db.php(数据库连接模块)

  • 这个文件负责建立应用程序与数据库之间的连接。它被其他需要操作数据库的页面(如调用上述 auth.php中函数的页面)所包含。
1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
$host = 'localhost:3316';
$dbname = 'blog_sql';
$username = 'blog';
$password = 'blog123';

try {
$pdo = new PDO("mysql:host=$host;dbname=$dbname;charset=utf8", $username, $password);
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
} catch(PDOException $e) {
die("数据库连接失败: " . $e->getMessage());
}
?>
  • 我们来看看这段代码怎样实现了应用程序与数据库之间的连接。
1
2
3
4
5
6
<?php
// 第1-4行:定义数据库连接参数
$host = 'localhost:3316'; // 数据库服务器地址和端口
$dbname = 'blog_sql'; // 要连接的数据库名称
$username = 'blog'; // 数据库用户名
$password = 'blog123'; // 数据库密码
  • 定义了连接数据库所需的配置信息。这样做的优点是修改配置时只需改动这一个文件。
1
2
3
4
// 第6-11行:尝试建立数据库连接
try {
// 第7行:创建PDO实例(即连接数据库)
$pdo = new PDO("mysql:host=$host;dbname=$dbname;charset=utf8", $username, $password);
  • 使用PDO扩展连接MySQL数据库。
    • new PDO(...):实例化一个PDO对象,传入数据源名称(DSN)、用户名和密码。
    • "mysql:host=$host;dbname=$dbname;charset=utf8"
      • mysql::指定数据库驱动程序为MySQL。
      • charset=utf8:设置连接字符集为UTF-8,这是防止中文乱码的关键
1
2
// 第8行:设置PDO错误属性
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
  • 配置PDO的错误处理模式。
  • PDO::ATTR_ERRMODE:这是要设置的属性,这个属性决定了 PDO 在遇到错误时的行为方式。他有三种错误模式也就是第二个参数的选择有三个:
    • PDO::ERRMODE_SILENT:静默模式,发生错误时不抛出异常,需要手动检查错误。
    • PDO::ERRMODE_WARNING:警告模式,发生错误时产生E_WARNING,脚本继续执行。
    • PDO::ERRMODE_EXCEPTION:异常模式,发生错误时抛出 PDOException。
    • 这里使用第三种模式是因为它能强制开发者处理错误,避免将敏感信息暴露给用户。
1
2
3
4
5
// 第9-11行:捕获并处理连接异常
} catch(PDOException $e) {
die("数据库连接失败: " . $e->getMessage());
}
?>

作用:异常处理,优雅地处理连接失败的情况。

  • catch(PDOException $e):捕获在 try块中抛出的 PDOException类型的异常。
  • die("数据库连接失败: " . $e->getMessage()):如果连接失败,立即终止脚本运行,并显示一个友好的错误信息以及从异常对象中获取的具体错误原因 $e->getMessage()

1.2 auth.php(用户认证模块)

  • 这个文件是博客系统的核心安全与身份验证模块,负责处理用户登陆状态、登录、注册和密码重置等关键功能,具体代码展示如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<?php
session_start();

function isLoggedIn() {
return isset($_SESSION['user_id']);
}

function login($username, $password, $pdo) {
$stmt = $pdo->prepare("SELECT * FROM users WHERE username = ? OR email = ?");
$stmt->execute([$username, $username]);
$user = $stmt->fetch();

if ($user && password_verify($password, $user['password'])) {
$_SESSION['user_id'] = $user['id'];
$_SESSION['username'] = $user['username'];
return true;
}
return false;
}

function register($userData, $pdo) {
$stmt = $pdo->prepare("SELECT id FROM users WHERE username = ? OR email = ?");
$stmt->execute([$userData['username'], $userData['email']]);

if ($stmt->fetch()) {
return "用户名或邮箱已存在";
}

$hashedPassword = password_hash($userData['password'], PASSWORD_DEFAULT);

$stmt = $pdo->prepare("INSERT INTO users (username, email, password) VALUES (?, ?, ?)");
if ($stmt->execute([$userData['username'], $userData['email'], $hashedPassword])) {
return true;
}
return "注册失败,请重试";
}
?>
  • 下面我们来一步步看看代码实现了什么功能。
1
2
3
<?php
// 第1行:开启会话(Session)
session_start();
  • session_start()函数:
    1. 用于在PHP中启动一个新的会话或者重用现有的会话。
    2. 会话是一种在多个页面之间存储信息的方式,通常用于存储用户信息,他必须在任何输出到浏览器之前调用。
    3. 会话机制是允许服务器在多个页面请求之间存储用户的基本信息,是实现用户登陆状态保持的基础。
1
2
3
4
// 第3-6行:检查用户是否已登录的函数
function isLoggedIn() {
return isset($_SESSION['user_id']);
}
  • 这是一个工具函数,用于快速检查当前访问者是否已经登录。

  • isset()函数用来检查一个变量是否已经被声明,并且不是一个空值。

  • 所以isset($_SESSION['user_id']);:用于检查变量$_SESSION中是否存在一个名为user_id的键。如果用户成功登录,这个值会被设置。

  • return:如果 user_id存在,返回 true,表示用户已登录;否则返回 false

1.2.1 用户登录函数
1
2
3
4
5
6
// 第8-19行:用户登录函数
function login($username, $password, $pdo) {
// 第9-11行:准备并执行SQL查询
$stmt = $pdo->prepare("SELECT * FROM users WHERE username = ? OR email = ?");
$stmt->execute([$username, $username]);
$user = $stmt->fetch();
  • 这个函数尝试使用用户名密码登录。

  • $pdo->prepare(...):创建一个预处理语句。使用占位符 ?可以有效防止SQL注入攻击,这是至关重要的安全措施。

  • $stmt->execute([$username, $username]):执行预处理语句,将两个 $username分别替换到SQL中的两个 ?位置。

  • $user = $stmt->fetch():从查询结果中获取一行数据(即用户记录),存储在 $user变量中。

1
2
3
4
5
6
7
// 第13-17行:验证密码并设置会话
if ($user && password_verify($password, $user['password'])) {
$_SESSION['user_id'] = $user['id'];
$_SESSION['username'] = $user['username'];
return true;
}
return false;
  • if ($user && ...):首先检查是否找到了对应的用户($user不为空),然后进行密码验证。
  • password_verify($password, $user['password']):函数将用户输入的明文密码$password与数据库中存储的经过哈希加密的密码$user['password']进行比对,如果使用MD5或者==进行比较可能会出现问题。
  • $_SESSION['user_id'] = ...:密码验证成功后,将用户的唯一标识 idusername存入会话变量。此后,在同一会话的任何页面中,都可以通过检查这些会话变量来判断用户身份,与开局信用的session_start()形成呼应。
1.2.2 用户注册函数
1
2
3
4
5
6
7
8
9
// 第21-35行:用户注册函数
function register($userData, $pdo) {
// 第22-25行:检查用户名或邮箱是否已存在
$stmt = $pdo->prepare("SELECT id FROM users WHERE username = ? OR email = ?");
$stmt->execute([$userData['username'], $userData['email']]);

if ($stmt->fetch()) {
return "用户名或邮箱已存在";
}
  • 注册用户时我们首先检查用户名或者邮箱是不是已经被使用过。
  • PDO的使用流程可以说和登录一样,首先是通过$pdo->prepare创建一个预处理SQL语句,通过?占位,之后就是通过execute()来替换?,并执行语句。
  • SELECT id ...:查询是否存在相同的用户名或邮箱。只需查询 id字段就足够了,效率高。
  • if ($stmt->fetch()):如果查询到任何结果,说明用户名或邮箱已被占用,函数立即返回一个错误消息字符串。
1
2
// 第27行:对密码进行哈希加密
$hashedPassword = password_hash($userData['password'], PASSWORD_DEFAULT);
  • password_hash(..., PASSWORD_DEFAULT):使用当前PHP的默认算法对明文密码进行哈希加密,一个系统基本的安全要求就是不要将明文密码存入数据库中。
1
2
3
4
5
6
7
// 第29-34行:执行插入操作
$stmt = $pdo->prepare("INSERT INTO users (username, email, password) VALUES (?, ?, ?)");
if ($stmt->execute([$userData['username'], $userData['email'], $hashedPassword])) {
return true;
}
return "注册失败,请重试";
}
  • INSERT INTO ...:准备插入数据的SQL语句。
  • $stmt->execute(...):执行插入操作。如果执行成功(通常返回 true),则返回 true表示注册成功;如果因未知原因失败(如数据库连接问题),则返回错误信息。

2. 博客根目录下文件

2.1 register.php(用户注册)

  • 这部分主要用来实现用户注册功能,包括前端和后端实现。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
<?php
require_once 'includes/db.php';
require_once 'includes/auth.php';

$error = '';

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$userData = [
'username' => $_POST['username'],
'email' => $_POST['email'],
'password' => $_POST['password']
];

$result = register($userData, $pdo);
if ($result === true) {
header('Location: login.php?registered=1');
exit;
} else {
$error = $result;
}
}
?>

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>注册 - 我的博客</title>
<link rel="stylesheet" href="css/style.css">
</head>
<body>
<header>
<nav>
<div class="logo">我的博客</div>
<ul class="nav-links">
<li><a href="index.php">首页</a></li>
<li><a href="login.php">登录</a></li>
</ul>
</nav>
</header>

<main class="container auth-container">
<h1>注册</h1>

<?php if ($error): ?>
<div class="error"><?= $error ?></div>
<?php endif; ?>

<form method="POST" class="auth-form">
<div class="form-group">
<label for="username">用户名:</label>
<input type="text" id="username" name="username" required>
</div>

<div class="form-group">
<label for="email">邮箱:</label>
<input type="email" id="email" name="email" required>
</div>

<div class="form-group">
<label for="password">密码:</label>
<input type="password" id="password" name="password" required>
</div>

<button type="submit" class="btn">注册</button>
</form>
</main>
</body>
</html>
  • 下面我们详细说明一下后端实现的过程。
1
2
3
<?php
require_once 'includes/db.php';
require_once 'includes/auth.php';
  • require_once是 PHP 中用于引入文件的语句,确保同一个文件只会被加载一次。
  • require_once 'includes/db.php';引入数据库连接文件。
  • require_once 'includes/auth.php'; 引入认证功能文件。
1
$error = '';
  • 初始化错误信息变量。
1
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
  • 检查是否为表单提交。
1
2
3
4
5
$userData = [
'username' => $_POST['username'],
'email' => $_POST['email'],
'password' => $_POST['password']
];
  • 构建用户数据数组。
1
$result = register($userData, $pdo);
  • 调用注册函数(我们在auth.php中定义的函数),传入用户数据和数据库连接。
1
2
3
4
5
6
7
8
9
    if ($result === true) {
// 注册成功,重定向到登录页面并带参数
header('Location: login.php?registered=1');
exit;
} else {
// 注册失败,保存错误信息
$error = $result;
}
}
  • 检查注册结果

2.2 login.php(用户登录)

  • 用户登录界面。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
<?php
require_once 'includes/db.php';
require_once 'includes/auth.php';

$error = '';

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (login($_POST['username'], $_POST['password'], $pdo)) {
header('Location: index.php');
exit;
} else {
$error = '用户名或密码错误';
}
}
?>

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>登录 - 我的博客</title>
<link rel="stylesheet" href="css/style.css">
</head>
<body>
<header>
<nav>
<div class="logo">我的博客</div>
<ul class="nav-links">
<li><a href="index.php">首页</a></li>
<li><a href="register.php">注册</a></li>
</ul>
</nav>
</header>

<main class="container auth-container">
<h1>登录</h1>

<?php if ($error): ?>
<div class="error"><?= $error ?></div>
<?php endif; ?>

<form method="POST" class="auth-form">
<div class="form-group">
<label for="username">用户名或邮箱:</label>
<input type="text" id="username" name="username" required>
</div>

<div class="form-group">
<label for="password">密码:</label>
<input type="password" id="password" name="password" required>
</div>

<button type="submit" class="btn">登录</button>
</form>

<p><a href="forgot-password.php">忘记密码?</a></p>
</main>
</body>
</html>
  • 前四行代码与 register.php一致。
1
2
3
4
5
6
if (login($_POST['username'], $_POST['password'], $pdo)) {
header('Location: index.php');
exit;
} else {
$error = '用户名或密码错误';
}
  • 调用auth.php中的login()函数,与数据库进行交互,用来判断登录信息,成功则跳转首页。
  • 安全机制:登录成功后立即跳转,防止重复提交。

2.3 index.php(首页文章列表)

  • 网站首页页面。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
<?php
require_once 'includes/db.php';
require_once 'includes/auth.php';

// 获取博客文章
$stmt = $pdo->prepare("
SELECT p.*, u.username
FROM posts p
JOIN users u ON p.author_id = u.id
ORDER BY p.created_at DESC
");
$stmt->execute();
$posts = $stmt->fetchAll();
?>

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>我的博客</title>
<link rel="stylesheet" href="css/style.css">
</head>
<body>
<header>
<nav>
<div class="logo">我的博客</div>
<ul class="nav-links">
<li><a href="index.php">首页</a></li>
<?php if (isLoggedIn()): ?>
<li><a href="dashboard/">控制台</a></li>
<li><a href="logout.php">退出</a></li>
<?php else: ?>
<li><a href="login.php">登录</a></li>
<li><a href="register.php">注册</a></li>
<?php endif; ?>
</ul>
</nav>
</header>

<main class="container">
<h1>最新文章</h1>

<?php foreach($posts as $post): ?>
<article class="post">
<h2><a href="post.php?id=<?= $post['id'] ?>"><?= htmlspecialchars($post['title']) ?></a></h2>
<div class="post-meta">
作者: <?= htmlspecialchars($post['username']) ?> |
发布时间: <?= date('Y-m-d H:i', strtotime($post['created_at'])) ?>
</div>
<div class="post-content">
<?= nl2br(htmlspecialchars(substr($post['content'], 0, 200))) ?>...
</div>
<a href="post.php?id=<?= $post['id'] ?>" class="read-more">阅读更多</a>
</article>
<?php endforeach; ?>
</main>

<footer>
<p>&copy; <?= date('Y') ?> MosHSasA_BLOG. 保留所有权利.</p>
</footer>
</body>
</html>
  • 调用文件不再赘述。
1
$stmt = $pdo->prepare("SELECT p.*, u.username FROM posts p JOIN users u ON p.author_id = u.id ORDER BY p.created_at DESC");
  • 准备SQL查询语句,获取文章和作者信息。
  • 数据库设计:使用JOIN关联查询,体现数据库关系设计。
1
2
$stmt->execute();
$posts = $stmt->fetchAll();
  • 执行查询并获取所有结果,这里使用了预处理语句防止SQL注入。

2.4 post.php(文章详情页)

  • 文章页面,可以从首页直接进入文章界面的后端支持。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
<?php
require_once 'includes/db.php';
require_once 'includes/auth.php';

// 获取文章详情
$postId = $_GET['id'] ?? 0;
$stmt = $pdo->prepare("
SELECT p.*, u.username
FROM posts p
JOIN users u ON p.author_id = u.id
WHERE p.id = ?
");
$stmt->execute([$postId]);
$post = $stmt->fetch();

if (!$post) {
header('HTTP/1.0 404 Not Found');
die('文章不存在');
}

// 获取评论
$stmt = $pdo->prepare("
SELECT c.*, u.username
FROM comments c
JOIN users u ON c.user_id = u.id
WHERE c.post_id = ?
ORDER BY c.created_at DESC
");
$stmt->execute([$postId]);
$comments = $stmt->fetchAll();

// 处理评论提交
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isLoggedIn()) {
$content = $_POST['content'];
$stmt = $pdo->prepare("INSERT INTO comments (post_id, user_id, content) VALUES (?, ?, ?)");
$stmt->execute([$postId, $_SESSION['user_id'], $content]);
header("Location: post.php?id=$postId");
exit;
}
?>

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?= htmlspecialchars($post['title']) ?> - 我的博客</title>
<link rel="stylesheet" href="css/style.css">
</head>
<body>
<header>
<nav>
<div class="logo">我的博客</div>
<ul class="nav-links">
<li><a href="index.php">首页</a></li>
<?php if (isLoggedIn()): ?>
<li><a href="dashboard/">控制台</a></li>
<li><a href="logout.php">退出</a></li>
<?php else: ?>
<li><a href="login.php">登录</a></li>
<li><a href="register.php">注册</a></li>
<?php endif; ?>
</ul>
</nav>
</header>

<main class="container">
<article class="post-full">
<h1><?= htmlspecialchars($post['title']) ?></h1>
<div class="post-meta">
作者: <?= htmlspecialchars($post['username']) ?> |
发布时间: <?= date('Y-m-d H:i', strtotime($post['created_at'])) ?>
</div>
<div class="post-content">
<?= nl2br(htmlspecialchars($post['content'])) ?>
</div>
</article>

<section class="comments-section">
<h2>评论 (<?= count($comments) ?>)</h2>

<?php if (isLoggedIn()): ?>
<form method="POST" class="comment-form">
<textarea name="content" placeholder="写下您的评论..." required></textarea>
<button type="submit" class="btn">发表评论</button>
</form>
<?php else: ?>
<p><a href="login.php">登录</a>后即可发表评论</p>
<?php endif; ?>

<div class="comments">
<?php foreach($comments as $comment): ?>
<div class="comment">
<div class="comment-header">
<strong><?= htmlspecialchars($comment['username']) ?></strong>
<span><?= date('Y-m-d H:i', strtotime($comment['created_at'])) ?></span>
</div>
<div class="comment-content">
<?= nl2br(htmlspecialchars($comment['content'])) ?>
</div>
</div>
<?php endforeach; ?>
</div>
</section>
</main>
</body>
</html>
  • 逐行分析一下。
1
$postId = $_GET['id'] ?? 0;
  • 获取URL参数,空合并运算符,避免未定义错误。
1
2
3
4
5
6
7
8
$stmt = $pdo->prepare("
SELECT p.*, u.username
FROM posts p
JOIN users u ON p.author_id = u.id
WHERE p.id = ?
");
$stmt->execute([$postId]);
$post = $stmt->fetch();
  • 与上面讲述过的类似,首先准备SQL查询语句,之后替换?为文章ID,最后从查询结果中获取一行数据。
1
2
3
4
if (!$post) {
header('HTTP/1.0 404 Not Found');
die('文章不存在');
}
  • 文章不存在时返回404错误。
1
2
3
4
5
6
7
8
9
$stmt = $pdo->prepare("
SELECT c.*, u.username
FROM comments c
JOIN users u ON c.user_id = u.id
WHERE c.post_id = ?
ORDER BY c.created_at DESC
");
$stmt->execute([$postId]);
$comments = $stmt->fetchAll();
  • 获取评论,与获取文章的方式大同小异,需要注意的时$stmt->fetchAll();这里时直接获取所有的评论,不仅仅只是一行数据。
1
2
// 处理评论提交
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isLoggedIn()) {
  • 条件判断:只有当同时满足以下两个条件时才执行评论提交。
    • 请求方法是 POST(通常是表单提交)。
    • 用户已登录(isLoggedIn()返回 true)。
1
$content = $_POST['content'];
  • 从POST请求中获取用户提交的评论内容。
1
2
$stmt = $pdo->prepare("INSERT INTO comments (post_id, user_id, content) VALUES (?, ?, ?)");
$stmt->execute([$postId, $_SESSION['user_id'], $content]);
  • 依旧使用预处理语句防止SQL注入,与前面的过程一致。
1
2
header("Location: post.php?id=$postId");
exit;
  • 页面重定向,评论提交成功后跳转回文章首页,exit确保脚本立即终止,避免后续代码执行。

2.5 forgot-password.php(密码找回功能)

  • 这个页面用做找回密码。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
<?php
require_once 'includes/db.php';
require_once 'includes/auth.php';

$message = '';

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$email = $_POST['email'];

$token = generateResetToken($email, $pdo);
if ($token) {
// 在实际应用中,这里应该发送邮件
$resetLink = "http://" . $_SERVER['HTTP_HOST'] . "/reset-password.php?token=" . $token;
$message = "重置链接已发送到您的邮箱: " . $resetLink;
} else {
$message = "邮箱不存在或发送失败";
}
}
?>

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>找回密码 - 我的博客</title>
<link rel="stylesheet" href="css/style.css">
</head>
<body>
<header>
<nav>
<div class="logo">我的博客</div>
<ul class="nav-links">
<li><a href="index.php">首页</a></li>
<li><a href="login.php">登录</a></li>
<li><a href="register.php">注册</a></li>
</ul>
</nav>
</header>

<main class="container auth-container">
<h1>找回密码</h1>

<?php if ($message): ?>
<div class="message"><?= $message ?></div>
<?php endif; ?>

<form method="POST" class="auth-form">
<div class="form-group">
<label for="email">请输入您的邮箱:</label>
<input type="email" id="email" name="email" required>
</div>

<button type="submit" class="btn">发送重置链接</button>
</form>

<p><a href="login.php">返回登录</a></p>
</main>
</body>
</html>
  • 引用其他文件和初始化信息变量这里不再赘述。
1
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
  • 检查是否是表单提交请求。
1
2
$email = $_POST['email'];
$token = generateResetToken($email, $pdo);
  • 获取用户邮箱并生成密码重置令牌,令牌机制可以避免直接修改密码更加安全。
1
2
3
4
if ($token) {
$resetLink = "http://" . $_SERVER['HTTP_HOST'] . "/reset-password.php?token=" . $token;
$message = "重置链接已发送到您的邮箱: " . $resetLink;
}
  • 生成充值链接并提示用户,但是这里实现的话需要更加繁琐的步骤,所以我将它直接回显在页面上,实际应用的情况下,时通过邮箱发送到用户的邮箱里。

2.6 logout.php(用户退出)

  • 这个时用户点击退出时跳转到的页面。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
<?php
// logout.php - 用户注销功能
session_start();

// 记录注销日志
if (isset($_SESSION['username'])) {
$username = $_SESSION['username'];
$logout_time = date('Y-m-d H:i:s');
$log_message = "用户 {$username}{$logout_time} 注销\n";

// 创建日志目录(如果不存在)
if (!is_dir('logs')) {
mkdir('logs', 0755, true);
}

// 记录到文件
file_put_contents('logs/logout.log', $log_message, FILE_APPEND | LOCK_EX);
}

// 销毁所有会话变量
$_SESSION = array();

// 删除会话cookie
if (ini_get("session.use_cookies")) {
$params = session_get_cookie_params();
setcookie(session_name(), '', time() - 42000,
$params["path"], $params["domain"],
$params["secure"], $params["httponly"]
);
}

// 销毁会话
session_destroy();
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>退出成功 - 我的博客</title>
<link rel="stylesheet" href="css/style.css">
<style>
/* 添加特定于注销页面的样式 */
.logout-container {
background: white;
padding: 2rem;
border-radius: 5px;
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
text-align: center;
max-width: 500px;
margin: 2rem auto;
}

.success-icon {
font-size: 4rem;
color: #27ae60;
margin-bottom: 1rem;
}

.auto-redirect {
margin-top: 1.5rem;
color: #7f8c8d;
}

.countdown {
font-weight: bold;
color: #e74c3c;
}

.btn-group {
display: flex;
gap: 1rem;
justify-content: center;
margin-top: 1.5rem;
}
</style>
</head>
<body>
<header>
<nav>
<div class="logo">我的博客</div>
<ul class="nav-links">
<li><a href="index.php">首页</a></li>
<li><a href="login.php">登录</a></li>
<li><a href="register.php">注册</a></li>
</ul>
</nav>
</header>

<main class="container">
<div class="logout-container">
<div class="success-icon">✓</div>
<h1>退出成功</h1>
<p>您已成功退出登录,感谢您使用我们的博客系统</p>

<div class="btn-group">
<a href="login.php" class="btn">重新登录</a>
<a href="index.php" class="btn" style="background: #95a5a6;">返回首页</a>
</div>

<p class="auto-redirect">页面将在 <span class="countdown" id="countdown">5</span> 秒后自动跳转到登录页面</p>
</div>
</main>

<footer>
<p>&copy; <?php echo date('Y'); ?> 我的博客. 保留所有权利.</p>
</footer>

<script>
// 自动跳转功能
let countdown = 5;
const countdownElement = document.getElementById('countdown');

const timer = setInterval(function() {
countdown--;
countdownElement.textContent = countdown;

if (countdown <= 0) {
clearInterval(timer);
window.location.href = 'login.php';
}
}, 1000);
</script>
</body>
</html>
  • 现在详细解释一下每行代码。
1
if (isset($_SESSION['username'])) {
  • 条件判断,检查会话中是否存在'username'变量,isset()函数检查变量是否已设置且不为null。
1
2
3
$username = $_SESSION['username'];
$logout_time = date('Y-m-d H:i:s');
$log_message = "用户 {$username}{$logout_time} 注销\n";
  • $username = $_SESSION['username']:从会话中获取用户名并赋值给局部变量。
  • $logout_time = date('Y-m-d H:i:s'):获取当前格式化的日期时间字符串。
    • date('Y-m-d H:i:s'):生成”年-月-日 时:分:秒”格式的时间戳。
  • $log_message = "用户 {$username} 于 {$logout_time} 注销\n":构造日志消息字符串。
1
2
3
if (!is_dir('logs')) {
mkdir('logs', 0755, true);
}
  • 用于创造日志目录。
  • if (!is_dir('logs')):检查’logs’目录是否存在。
  • mkdir('logs', 0755, true):创建目录的函数调用。
    • 参数一:'logs':要创建的目录名。
    • 参数二:0755:目录权限设置(所有者可读写执行,组和其他用户可读执行)。
    • 参数三:true:允许创建多级目录(递归创建)。
1
2
file_put_contents('logs/logout.log', $log_message, FILE_APPEND | LOCK_EX);
}
  • 写入到日志文件。
  • FILE_APPEND:常量,表示追加模式(不在文件开头写入)。
  • LOCK_EX:常量,表示独占锁,防止并发写入冲突。
  • |:位或运算符,组合多个选项。
1
$_SESSION = array();
  • 清空会话变量,将会话全局变量重新赋值为空数组,会清空所有会话数据,但是会话ID仍然存在。
1
if (ini_get("session.use_cookies")) {
  • ini_get("session.use_cookies"):获取PHP配置中是否使用Cookie来管理会话。
1
$params = session_get_cookie_params();
  • session_get_cookie_params():返回当前会话Cookie的详细参数数组。
1
2
3
4
setcookie(session_name(), '', time() - 42000,
$params["path"], $params["domain"],
$params["secure"], $params["httponly"]
);
  • 设置过期Cookie
    • setcookie():设置Cookie的函数。
    • session_name():获取当前会话的名称(通常是”PHPSESSID”)。
    • '':空值,清除Cookie内容。
    • time() - 42000:将过期时间设为过去(当前时间减42000秒),使浏览器立即删除Cookie。
    • 后续参数使用原Cookie的相同设置,确保正确删除。
1
session_destroy();
  • 彻底销毁会话,删除服务器上的会话数据文件。

3. dashboard文件夹

  • 此目录下包含博客系统控制台下的功能,包括文章的删除、编辑、新建和控制台首页

3.1 index.php(文章首页)

  • 控制台首页页面。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
<?php
require_once '../includes/db.php';
require_once '../includes/auth.php';

if (!isLoggedIn()) {
header('Location: ../login.php');
exit;
}

// 获取用户文章
$stmt = $pdo->prepare("
SELECT * FROM posts
WHERE author_id = ?
ORDER BY created_at DESC
");
$stmt->execute([$_SESSION['user_id']]);
$posts = $stmt->fetchAll();
?>

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>控制台 - 我的博客</title>
<link rel="stylesheet" href="../css/style.css">
<style>
.dashboard {
display: grid;
grid-template-columns: 250px 1fr;
gap: 2rem;
min-height: 80vh;
}

.sidebar {
background: #2c3e50;
color: white;
padding: 1.5rem;
border-radius: 5px;
}

.sidebar ul {
list-style: none;
}

.sidebar li {
margin: 1rem 0;
}

.sidebar a {
color: white;
text-decoration: none;
display: block;
padding: 0.5rem;
border-radius: 3px;
transition: background 0.3s;
}

.sidebar a:hover, .sidebar a.active {
background: #34495e;
}

.main-content {
padding: 0;
}

.posts-table {
width: 100%;
border-collapse: collapse;
background: white;
border-radius: 5px;
overflow: hidden;
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
}

.posts-table th, .posts-table td {
padding: 1rem;
text-align: left;
border-bottom: 1px solid #eee;
}

.posts-table th {
background: #f8f9fa;
font-weight: 600;
}

.btn {
padding: 0.5rem 1rem;
border: none;
border-radius: 3px;
cursor: pointer;
text-decoration: none;
display: inline-block;
font-size: 0.9rem;
}

.btn-primary {
background: #3498db;
color: white;
}

.btn-danger {
background: #e74c3c;
color: white;
}

.btn-edit {
background: #f39c12;
color: white;
}

.post-actions {
display: flex;
gap: 0.5rem;
}
</style>
</head>
<body>
<header>
<nav>
<div class="logo">我的博客</div>
<ul class="nav-links">
<li><a href="../index.php">首页</a></li>
<li><a href="logout.php">退出</a></li>
</ul>
</nav>
</header>

<main class="container">
<h1>控制台</h1>

<div class="dashboard">
<aside class="sidebar">
<ul>
<li><a href="index.php" class="active">文章管理</a></li>
<li><a href="new-post.php">写文章</a></li>
<li><a href="#">评论管理</a></li>
<li><a href="#">个人资料</a></li>
</ul>
</aside>

<div class="main-content">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1.5rem;">
<h2>我的文章</h2>
<a href="new-post.php" class="btn btn-primary">写新文章</a>
</div>

<?php if (count($posts) > 0): ?>
<table class="posts-table">
<thead>
<tr>
<th>标题</th>
<th>状态</th>
<th>发布时间</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<?php foreach($posts as $post): ?>
<tr>
<td>
<a href="../post.php?id=<?= $post['id'] ?>">
<?= htmlspecialchars($post['title']) ?>
</a>
</td>
<td>
<span style="color: <?= $post['status'] == 'published' ? '#27ae60' : '#f39c12' ?>">
<?= $post['status'] == 'published' ? '已发布' : '草稿' ?>
</span>
</td>
<td><?= date('Y-m-d H:i', strtotime($post['created_at'])) ?></td>
<td>
<div class="post-actions">
<a href="edit-post.php?id=<?= $post['id'] ?>" class="btn btn-edit">编辑</a>
<a href="delete-post.php?id=<?= $post['id'] ?>"
class="btn btn-danger"
onclick="return confirm('确定要删除这篇文章吗?')">删除</a>
</div>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php else: ?>
<div style="text-align: center; padding: 3rem; background: white; border-radius: 5px;">
<p>您还没有发布任何文章</p>
<a href="new-post.php" class="btn btn-primary">开始写第一篇文章</a>
</div>
<?php endif; ?>
</div>
</div>
</main>
</body>
</html>
  • 下面我们逐行看一下代码。

  • 引入部分不再赘述。

1
2
3
4
if (!isLoggedIn()) {
header('Location: ../login.php');
exit;
}
  • 验证用户是否登录,如果没有登录则重定向回登陆首页。
1
2
3
4
5
6
7
$stmt = $pdo->prepare("
SELECT * FROM posts
WHERE author_id = ?
ORDER BY created_at DESC
");
$stmt->execute([$_SESSION['user_id']]);
$posts = $stmt->fetchAll();
  • 准备SQL语句透过用户ID获取所有文章并展示在前端页面上。

3.2 new-post.php(新建文章页面)

  • 提供创建新文章的界面和处理逻辑,与编辑页面结构相似但功能独立。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
<?php
require_once '../includes/db.php';
require_once '../includes/auth.php';

if (!isLoggedIn()) {
header('Location: ../login.php');
exit;
}

$error = '';
$success = '';

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$title = trim($_POST['title']);
$content = trim($_POST['content']);
$status = $_POST['status'];

if (empty($title) || empty($content)) {
$error = '标题和内容不能为空';
} else {
$stmt = $pdo->prepare("
INSERT INTO posts (title, content, author_id, status)
VALUES (?, ?, ?, ?)
");

if ($stmt->execute([$title, $content, $_SESSION['user_id'], $status])) {
$success = '文章发布成功!';
// 清空表单
$title = $content = '';
} else {
$error = '发布失败,请重试';
}
}
}
?>

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>写文章 - 我的博客</title>
<link rel="stylesheet" href="../css/style.css">
<style>
.editor-container {
background: white;
padding: 2rem;
border-radius: 5px;
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
}

.form-group {
margin-bottom: 1.5rem;
}

label {
display: block;
margin-bottom: 0.5rem;
font-weight: bold;
}

input[type="text"], textarea {
width: 100%;
padding: 0.75rem;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 1rem;
font-family: inherit;
}

textarea {
min-height: 300px;
resize: vertical;
}

.form-actions {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 2rem;
}

.status-select {
padding: 0.5rem;
border: 1px solid #ddd;
border-radius: 4px;
}
</style>
</head>
<body>
<header>
<nav>
<div class="logo">我的博客</div>
<ul class="nav-links">
<li><a href="../index.php">首页</a></li>
<li><a href="index.php">控制台</a></li>
<li><a href="logout.php">退出</a></li>
</ul>
</nav>
</header>

<main class="container">
<h1>写文章</h1>

<?php if ($error): ?>
<div class="error"><?= $error ?></div>
<?php endif; ?>

<?php if ($success): ?>
<div class="message"><?= $success ?></div>
<?php endif; ?>

<div class="editor-container">
<form method="POST">
<div class="form-group">
<label for="title">文章标题</label>
<input type="text" id="title" name="title"
value="<?= isset($title) ? htmlspecialchars($title) : '' ?>"
placeholder="请输入文章标题" required>
</div>

<div class="form-group">
<label for="content">文章内容</label>
<textarea id="content" name="content"
placeholder="请输入文章内容" required><?= isset($content) ? htmlspecialchars($content) : '' ?></textarea>
</div>

<div class="form-actions">
<div>
<label for="status">状态:</label>
<select id="status" name="status" class="status-select">
<option value="published">发布</option>
<option value="draft">草稿</option>
</select>
</div>

<div>
<button type="submit" class="btn btn-primary">发布文章</button>
<a href="index.php" class="btn">取消</a>
</div>
</div>
</form>
</div>
</main>

<script>
// 简单的字数统计
document.getElementById('content').addEventListener('input', function() {
const charCount = this.value.length;
document.getElementById('charCount').textContent = charCount;
});
</script>
</body>
</html>
  • 我们依旧来逐行看一下。

  • 验证登录、引入文件和初始化变量$error和变量$success部分跳过。

1
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
  • 检查当前请求是否为POST方法。
1
2
3
$title = trim($_POST['title']);
$content = trim($_POST['content']);
$status = $_POST['status'];
  • 从POST数据中获取标题、内容和状态。
  • trim()函数去除字符串两端的空白字符。
  • 数据来源:$_POST超全局数组,包含通过POST方法提交的表单数据。
1
2
3
if (empty($title) || empty($content)) {
$error = '标题和内容不能为空';
}
  • 输入验证检查标题和内容是否为空,empty()函数检查变量是否为”空”(空字符串、0、null等),如果任一字段为空,设置错误信息。
1
2
3
4
5
6
7
8
9
10
11
$stmt = $pdo->prepare("
INSERT INTO posts (title, content, author_id, status)
VALUES (?, ?, ?, ?)
");

if ($stmt->execute([$title, $content, $_SESSION['user_id'], $status])) {
$success = '文章发布成功!';
$title = $content = '';
} else {
$error = '发布失败,请重试';
}
  • 数据库操作,准备SQL插入语句。execute()方法执行预处理语句,传入参数数组,$_SESSION['user_id']从会话中获取当前用户ID,如果执行成功返回成功消息并清空标题和内容变量。反之,返回失败消息。

3.3 edit-post.php(文章编辑页面)

  • 这是一个混合型脚本,既处理文章更新逻辑,又显示编辑表单界面。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
<?php
require_once '../includes/db.php';
require_once '../includes/auth.php';

if (!isLoggedIn()) {
header('Location: ../login.php');
exit;
}

$error = '';
$success = '';

// 获取文章信息
$postId = $_GET['id'] ?? 0;
$stmt = $pdo->prepare("SELECT * FROM posts WHERE id = ? AND author_id = ?");
$stmt->execute([$postId, $_SESSION['user_id']]);
$post = $stmt->fetch();

if (!$post) {
header('Location: index.php');
exit;
}

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$title = trim($_POST['title']);
$content = trim($_POST['content']);
$status = $_POST['status'];

if (empty($title) || empty($content)) {
$error = '标题和内容不能为空';
} else {
$stmt = $pdo->prepare("
UPDATE posts
SET title = ?, content = ?, status = ?, updated_at = NOW()
WHERE id = ? AND author_id = ?
");

if ($stmt->execute([$title, $content, $status, $postId, $_SESSION['user_id']])) {
$success = '文章更新成功!';
// 重新获取文章信息
$stmt = $pdo->prepare("SELECT * FROM posts WHERE id = ?");
$stmt->execute([$postId]);
$post = $stmt->fetch();
} else {
$error = '更新失败,请重试';
}
}
}
?>

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>编辑文章 - 我的博客</title>
<link rel="stylesheet" href="../css/style.css">
<style>
.editor-container {
background: white;
padding: 2rem;
border-radius: 5px;
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
}

.form-group {
margin-bottom: 1.5rem;
}

label {
display: block;
margin-bottom: 0.5rem;
font-weight: bold;
}

input[type="text"], textarea {
width: 100%;
padding: 0.75rem;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 1rem;
font-family: inherit;
}

textarea {
min-height: 300px;
resize: vertical;
}

.form-actions {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 2rem;
}

.status-select {
padding: 0.5rem;
border: 1px solid #ddd;
border-radius: 4px;
}
</style>
</head>
<body>
<header>
<nav>
<div class="logo">我的博客</div>
<ul class="nav-links">
<li><a href="../index.php">首页</a></li>
<li><a href="index.php">控制台</a></li>
<li><a href="logout.php">退出</a></li>
</ul>
</nav>
</header>

<main class="container">
<h1>编辑文章</h1>

<?php if ($error): ?>
<div class="error"><?= $error ?></div>
<?php endif; ?>

<?php if ($success): ?>
<div class="message"><?= $success ?></div>
<?php endif; ?>

<div class="editor-container">
<form method="POST">
<div class="form-group">
<label for="title">文章标题</label>
<input type="text" id="title" name="title"
value="<?= htmlspecialchars($post['title']) ?>"
placeholder="请输入文章标题" required>
</div>

<div class="form-group">
<label for="content">文章内容</label>
<textarea id="content" name="content"
placeholder="请输入文章内容" required><?= htmlspecialchars($post['content']) ?></textarea>
</div>

<div class="form-actions">
<div>
<label for="status">状态:</label>
<select id="status" name="status" class="status-select">
<option value="published" <?= $post['status'] == 'published' ? 'selected' : '' ?>>发布</option>
<option value="draft" <?= $post['status'] == 'draft' ? 'selected' : '' ?>>草稿</option>
</select>
</div>

<div>
<button type="submit" class="btn btn-primary">更新文章</button>
<a href="index.php" class="btn">取消</a>
</div>
</div>
</form>
</div>
</main>
</body>
</html>
  • 逐行查看代码。
1
$postId = $_GET['id'] ?? 0;
  • 获取需要编辑的文章ID并查询文章详情。
1
2
3
$stmt = $pdo->prepare("SELECT * FROM posts WHERE id = ? AND author_id = ?");
$stmt->execute([$postId, $_SESSION['user_id']]);
$post = $stmt->fetch();
  • PDO使用老方法,只查询当前用户的文章。
1
2
3
4
if (!$post) {
header('Location: index.php');
exit;
}
  • 如果文章不存在或者没有访问权限,则重定向到首页页面。
1
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
  • 处理表单提交:POST请求。
1
2
3
$title = trim($_POST['title']);
$content = trim($_POST['content']);
$status = $_POST['status'];
  • 获取并清理表单验证。
1
2
3
if (empty($title) || empty($content)) {
$error = '标题和内容不能为空';
} else {
  • 数据验证。
1
2
3
4
5
6
7
8
$stmt = $pdo->prepare("
UPDATE posts
SET title = ?, content = ?, status = ?, updated_at = NOW()
WHERE id = ? AND author_id = ?
");

if ($stmt->execute([$title, $content, $status, $postId, $_SESSION['user_id']])) {
$success = '文章更新成功!';
  • 执行数据库更新。
1
2
3
4
5
6
$stmt = $pdo->prepare("SELECT * FROM posts WHERE id = ?");
$stmt->execute([$postId]);
$post = $stmt->fetch();
} else {
$error = '更新失败,请重试';
}
  • 重新查询获取最新数据。

3.4 delete-post.php(删除文章处理脚本)

  • 这是一个纯处理脚本,用于安全地删除用户自己的文章,不包含任何HTML显示内容。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php
require_once '../includes/db.php';
require_once '../includes/auth.php';

if (!isLoggedIn()) {
header('Location: ../login.php');
exit;
}

$postId = $_GET['id'] ?? 0;

if ($postId) {
// 确保用户只能删除自己的文章
$stmt = $pdo->prepare("DELETE FROM posts WHERE id = ? AND author_id = ?");
$stmt->execute([$postId, $_SESSION['user_id']]);
}

header('Location: index.php');
exit;
  • 逐行来看。
1
if ($postId) {
  • 通过前面获取的文章ID删除文章,只有当文章ID有效时才可以删除文章,这样可以确保用户只能删除自己的文章。
1
$stmt = $pdo->prepare("DELETE FROM posts WHERE id = ? AND author_id = ?");
  • 准备SQL删除语句(安全措施:同时验证文章ID和作者ID)。
1
$stmt->execute([$postId, $_SESSION['user_id']]);
  • 执行删除,传入文章ID和当前用户ID(防止删除他人文章)。
1
2
header('Location: index.php');
exit;
  • 删除完成后重定向回文章管理页面,并确保重定向后停止脚本。

第六部分:总结

​ 本项目成功实现了一个功能完整的简易博客系统,达到了学习和实践的目的。通过该项目我学习了PHP动态网站的开发过程、MySQL数据库的设计思路与操作,以及前端后端之间的交互过程。

​ 但时当前博客系统功能简单,内容也不完善仍存在部分缺陷,希望后续可以完善和改变。