摘要 :前面几篇,我们的数据都存储在变量、数组甚至文件中,但真正的网站需要持久化、可查询、高性能的数据存储。数据库就是为此而生。本篇将带你走进关系型数据库 MySQL 的世界,从安装和基本概念开始,学会用 phpMyAdmin 可视化管理数据库,掌握 SQL 的"增删改查"(CRUD)操作。然后,用 PHP 的 PDO 扩展连接数据库,执行查询,并用预处理语句彻底杜绝 SQL 注入攻击。学完本篇,你将能够为博客、商城等应用构建坚实的数据后端,真正打通"前端表单 → PHP 处理 → 数据库存储"的全链路。
一、引言:为什么需要数据库?
回忆我们上一篇文章的注册系统,用户数据被保存在一个 users.json 文件中。这种方式问题很多:查询困难(想找到某个用户必须读取整个文件)、并发冲突(多个用户同时写入可能互相覆盖)、无法高效排序和统计、数据量大了性能会急剧下降。
数据库就像一个专业化的仓库:它把数据按规则分类存放(表),提供标准化的语言进行存取(SQL),并且有锁机制保证多人同时操作不冲突。MySQL 是使用最广泛的开源关系型数据库之一,与 PHP 搭配组成了经典的 LAMP/LEMP 技术栈。
二、MySQL 简介与安装确认
2.1 什么是 MySQL?
MySQL 是一个关系型数据库管理系统(RDBMS),把数据组织成一张张表(类似 Excel 工作表),表之间有联系(通过键)。它使用结构化查询语言(SQL)来操作数据。
核心概念:
数据库(Database):一个项目或应用的数据集合。
表(Table):数据库里的具体存储单元,由行和列组成。
行(Row):一条记录,比如一个用户、一篇文章。
列(Column):记录的属性,比如用户名、密码、邮箱。
主键(Primary Key):唯一标识一行记录的字段,通常用自增整数。
外键(Foreign Key):关联另一张表的字段。
2.2 确认 MySQL 是否安装
之前我们使用了 XAMPP 集成环境,它已经包含了 MySQL/MariaDB(后者是 MySQL 的分支,完全兼容)。打开 XAMPP 控制面板,点击 MySQL 一行的 Start,看到端口 3306 变为绿色,表示数据库已启动。
也可以打开命令行(Windows 下点击 XAMPP 的 Shell),输入:
复制代码
mysql -u root
直接回车(默认无密码),进入 MySQL 命令行界面,输入 SELECT VERSION(); 查看版本。
三、SQL 入门:数据库的"普通话"
3.1 创建数据库和表
假设我们要做一个博客系统,先建数据库 blog,并创建第一张表 users。
sql
复制代码
-- 创建数据库(如果不存在)
CREATE DATABASE IF NOT EXISTS blog DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- 使用该数据库
USE blog;
-- 创建用户表
CREATE TABLE IF NOT EXISTS users (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50) NOT NULL UNIQUE,
password VARCHAR(255) NOT NULL,
email VARCHAR(100) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
解释:
id:自增主键,每插入一行自动 +1。
username:最长 50 字符,不能为空,值唯一。
password:最长 255(为了存储哈希后的密码)。
email:最长 100。
created_at:时间戳,默认插入时间。
ENGINE=InnoDB:支持事务和外键。
CHARSET=utf8mb4:支持 emoji 等 4 字节 Unicode。
3.2 插入数据(INSERT)
sql
复制代码
INSERT INTO users (username, password, email)
VALUES ('zhangsan', 'hashed_password_here', 'zhangsan@example.com');
批量插入:
sql
复制代码
INSERT INTO users (username, password, email) VALUES
('lisi', 'pass2', 'lisi@test.com'),
('wangwu', 'pass3', 'wangwu@test.com');
3.3 查询数据(SELECT)
查所有用户:
sql
复制代码
SELECT * FROM users;
查指定列:
sql
复制代码
SELECT username, email FROM users;
带条件:
sql
复制代码
SELECT * FROM users WHERE id = 1;
SELECT * FROM users WHERE username LIKE '%zhang%'; -- 模糊搜索
排序与限制:
sql
复制代码
SELECT * FROM users ORDER BY created_at DESC LIMIT 10; -- 最新10条
3.4 更新数据(UPDATE)
sql
复制代码
UPDATE users SET email = 'newemail@test.com' WHERE id = 2;
务必加 WHERE! 否则整表数据都会修改。
3.5 删除数据(DELETE)
sql
复制代码
DELETE FROM users WHERE id = 3;
同样,忘记 WHERE 会清空整张表,要极其小心。
3.6 更多实用 SQL
COUNT():计数,如 SELECT COUNT(*) FROM users;
SUM()、AVG()、MAX()、MIN():聚合函数。
GROUP BY:分组统计,如 SELECT city, COUNT(*) FROM users GROUP BY city;
JOIN:连接多表查询(下一篇会深入)。
ALTER TABLE:修改表结构,如添加列。
四、使用 phpMyAdmin 可视化管理
虽然命令行很强大,但对初学者来说,phpMyAdmin 更加直观。XAMPP 已内置它。
打开浏览器,访问 http://localhost/phpmyadmin。
左侧点击 新建 ,输入数据库名 blog,选择 utf8mb4_unicode_ci,点击创建。
在新建的 blog 数据库中,创建表,输入名称 users,字段数 5,点击执行。
填写各字段的名称、类型、长度/值、索引等,参考上面的 SQL 定义。
点击保存,表创建成功。
点击顶部的 SQL 标签,可以直接输入 SQL 语句执行;也可以使用 插入 、浏览 等功能。
虽然图形化很方便,但一定要结合 SQL 学习,因为代码中操作数据库仍然需要写 SQL。
五、PHP 连接 MySQL 的方式:从 mysql_ 到 PDO
5.1 历史回顾与现状
mysql 扩展 :PHP 5.5.0 起已废弃,PHP 7.0 移除。绝不使用。
mysqli 扩展:改进版,支持面向对象和过程化接口,只支持 MySQL。可以用,但推荐 PDO。
PDO(PHP Data Objects) :数据库抽象层,支持 12 种数据库(MySQL, SQLite, PostgreSQL 等),提供统一的接口。支持预处理语句,安全性高。本文只讲 PDO。
5.2 PDO 连接数据库
php
复制代码
$host = '127.0.0.1';
$dbname = 'blog';
$username = 'root';
$password = '';
$charset = 'utf8mb4';
try {
$dsn = "mysql:host=$host;dbname=$dbname;charset=$charset";
$pdo = new PDO($dsn, $username, $password, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, // 抛出异常便于调试
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, // 默认返回关联数组
PDO::ATTR_EMULATE_PREPARES => false, // 使用真正的预处理
]);
echo "数据库连接成功!";
} catch (PDOException $e) {
die("数据库连接失败:" . $e->getMessage());
}
?>
关键配置解释:
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION:出错时抛异常,比手动检查 errorInfo 方便。
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC:fetch 时默认返回以列名为键的数组。
PDO::ATTR_EMULATE_PREPARES => false:禁用模拟预处理,让数据库原生支持预处理,更安全。
5.3 测试连接并执行简单查询
php
复制代码
// 数据库连接配置
$host = '127.0.0.1';
$dbname = 'blog';
$dbUser = 'root';
$dbPass = '';
$charset = 'utf8mb4';
$pdo = null;
try {
// 构造DSN连接字符串
$dsn = "mysql:host=$host;dbname=$dbname;charset=$charset";
// PDO连接配置项
$options = [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, // 异常模式报错
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, // 默认返回关联数组
PDO::ATTR_EMULATE_PREPARES => false, // 关闭模拟预处理,防SQL注入
];
// 实例化PDO对象建立连接
$pdo = new PDO($dsn, $dbUser, $dbPass, $options);
echo "
数据库连接成功!
";} catch (PDOException $e) {
// 捕获连接异常,终止后续数据库操作
die("
数据库连接失败:" . htmlspecialchars($e->getMessage()) . "
");}
// 查询用户表数据
try {
$stmt = $pdo->query("SELECT * FROM users");
$users = $stmt->fetchAll();
if (empty($users)) {
echo "
暂无用户数据
";} else {
foreach ($users as $user) {
$username = htmlspecialchars($user['username'] ?? '');
$email = htmlspecialchars($user['email'] ?? '');
echo "用户名:{$username},邮箱:{$email}
";
}
}
} catch (PDOException $e) {
echo "
查询失败:" . htmlspecialchars($e->getMessage()) . "
";}
?>
query() 适合无变量的查询。但涉及用户输入,必须使用预处理语句。
六、预处理语句:安全与高效的基石
6.1 为什么需要预处理?
如果你直接拼接 SQL:
php
复制代码
$username = $_POST['username'];
$sql = "SELECT * FROM users WHERE username = '$username'";
黑客可以输入 ' OR '1'='1,导致 SQL 变成 SELECT * FROM users WHERE username = '' OR '1'='1',返回所有用户。这就是 SQL 注入。
预处理语句将 SQL 结构和数据分开发送,数据不会破坏 SQL 语法,从根本上杜绝注入。
6.2 基本预处理步骤
php
复制代码
// 准备 SQL 模板,用 ? 或命名占位符
$stmt = $pdo->prepare("SELECT * FROM users WHERE username = :username");
// 绑定参数
$username = $_POST['username'];
$stmt->bindParam(':username', $username);
// 执行
$stmt->execute();
// 获取结果
$user = $stmt->fetch(); // 获取一条记录
?>
6.3 使用问号占位符
php
复制代码
$stmt = $pdo->prepare("SELECT * FROM users WHERE id = ?");
$stmt->execute([$id]); // 按顺序绑定
$user = $stmt->fetch();
6.4 插入数据(INSERT)预处理
php
复制代码
$username = $_POST['username'];
$password = password_hash($_POST['password'], PASSWORD_DEFAULT);
$email = $_POST['email'];
$sql = "INSERT INTO users (username, password, email) VALUES (:username, :password, :email)";
$stmt = $pdo->prepare($sql);
$stmt->execute([
':username' => $username,
':password' => $password,
':email' => $email,
]);
echo "新用户 ID:" . $pdo->lastInsertId();
?>
execute 接受一个关联数组,键对应命名占位符。lastInsertId() 获取自增主键值。
6.5 更新和删除
php
复制代码
// 更新
$sql = "UPDATE users SET email = :email WHERE id = :id";
$stmt = $pdo->prepare($sql);
$stmt->execute([':email' => $newEmail, ':id' => $userId]);
// 删除
$sql = "DELETE FROM users WHERE id = :id";
$stmt = $pdo->prepare($sql);
$stmt->execute([':id' => $userId]);
返回受影响的行数:$stmt->rowCount()。
七、综合实战一:将注册登录系统升级为数据库版
在 blog 数据库中执行:
sql
复制代码
CREATE DATABASE IF NOT EXISTS blog DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE blog;
CREATE TABLE IF NOT EXISTS users (
id INT PRIMARY KEY AUTO_INCREMENT COMMENT '用户主键ID',
username VARCHAR(50) NOT NULL UNIQUE COMMENT '用户名',
password VARCHAR(255) NOT NULL COMMENT '加密密码',
email VARCHAR(100) NOT NULL UNIQUE COMMENT '邮箱',
avatar VARCHAR(255) DEFAULT NULL COMMENT '头像文件相对路径',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '注册时间'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
7.1 数据库配置与公共连接文件
新建 db.php:
php
复制代码
/**
* 数据库公共连接文件
* 单例模式获取PDO连接,全局复用,避免重复创建连接
* @return PDO
*/
function getPdo(): PDO
{
// 静态变量仅首次调用实例化
static $pdo = null;
if ($pdo === null) {
// 数据库配置项
$host = '127.0.0.1';
$dbname = 'blog';
$dbUser = 'root';
$dbPass = '';
$charset = 'utf8mb4';
// DSN连接字符串
$dsn = "mysql:host=$host;dbname=$dbname;charset=$charset";
// PDO安全配置
$options = [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false,
];
$pdo = new PDO($dsn, $dbUser, $dbPass, $options);
}
return $pdo;
}
后续所有页面引入 require 'db.php';,调用 getPdo() 获取连接。
7.2 注册页面(register_db.php)
php
复制代码
require 'db.php';
session_start();
// 安全响应头
header("X-XSS-Protection: 1; mode=block");
header("X-Frame-Options: DENY");
// 生成CSRF令牌,防止跨站提交
if (empty($_SESSION['csrf_token'])) {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
$errors = [];
$inputFill = [
'username' => '',
'email' => ''
];
// 上传目录自动创建
$uploadDir = __DIR__ . '/uploads/avatar/';
if (!file_exists($uploadDir)) {
mkdir($uploadDir, 0755, true);
}
$avatarPath = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// CSRF校验
$postToken = $_POST['csrf_token'] ?? '';
if (!hash_equals($_SESSION['csrf_token'], $postToken)) {
$errors['global'] = '非法请求,禁止跨站重复提交';
}
// 获取并清洗输入
$username = trim($_POST['username'] ?? '');
$password = $_POST['password'] ?? '';
$password2 = $_POST['password2'] ?? '';
$email = trim($_POST['email'] ?? '');
// 回填输入框
$inputFill['username'] = $username;
$inputFill['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 {
// 校验真实图片MIME
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mime = $finfo->file($file['tmp_name']);
$allowMime = ['image/jpeg', 'image/png'];
if (!in_array($mime, $allowMime)) {
$errors['avatar'] = '仅支持 JPG / PNG 图片头像';
} elseif ($file['size'] > 1048576) {
$errors['avatar'] = '头像文件不能超过 1MB';
} else {
// 生成唯一文件名防覆盖
$ext = $mime === 'image/png' ? 'png' : 'jpg';
$fileName = md5(uniqid(microtime(true), true) . $username) . '.' . $ext;
$destFile = $uploadDir . $fileName;
if (move_uploaded_file($file['tmp_name'], $destFile)) {
$avatarPath = 'uploads/avatar/' . $fileName;
} else {
$errors['avatar'] = '头像保存失败,检查目录权限';
}
}
}
}
// 无前端错误再操作数据库
if (empty($errors)) {
try {
$pdo = getPdo();
// 查询用户名是否已注册
$checkSql = "SELECT id FROM users WHERE username = :username";
$checkStmt = $pdo->prepare($checkSql);
$checkStmt->execute([':username' => $username]);
if ($checkStmt->fetch()) {
$errors['username'] = '该用户名已被占用,请更换';
} else {
// 密码哈希加密存储
$hashPwd = password_hash($password, PASSWORD_DEFAULT);
// 插入用户数据(新增avatar字段)
$insertSql = "INSERT INTO users (username, password, email, avatar) VALUES (:u, :p, :e, :avatar)";
$insertStmt = $pdo->prepare($insertSql);
$insertStmt->execute([
':u' => $username,
':p' => $hashPwd,
':e' => $email,
':avatar' => $avatarPath
]);
// 获取新增用户ID,写入会话自动登录
$uid = $pdo->lastInsertId();
$_SESSION['logged_in'] = true;
$_SESSION['user_id'] = $uid;
$_SESSION['username'] = $username;
$_SESSION['avatar'] = $avatarPath;
// 跳转欢迎页
header('Location: welcome_db.php', true, 302);
exit;
}
} catch (PDOException $e) {
// 生产环境隐藏原始数据库报错
$errors['global'] = '注册失败,服务器繁忙,请稍后重试';
}
}
}
?>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: "Microsoft YaHei", sans-serif;
}
body {
background-color: #f4f6f9;
padding: 60px 20px;
}
.card {
width: 420px;
margin: 0 auto;
background: #fff;
padding: 32px;
border-radius: 10px;
box-shadow: 0 2px 12px rgba(0,0,0,0.07);
}
h2 {
text-align: center;
color: #2d3748;
margin-bottom: 24px;
}
.global-error {
background: #fee;
color: #dc2626;
padding: 10px;
border-radius: 6px;
margin-bottom: 16px;
text-align: center;
}
.item {
margin-bottom: 16px;
}
label {
display: block;
margin-bottom: 6px;
color: #4a5568;
}
input {
width: 100%;
padding: 10px 12px;
border: 1px solid #cbd5e0;
border-radius: 6px;
font-size: 15px;
}
.err-text {
color: #dc2626;
font-size: 13px;
margin-top: 4px;
display: block;
}
button {
width: 100%;
padding: 11px;
background: #2563eb;
color: white;
border: none;
border-radius: 6px;
font-size: 16px;
cursor: pointer;
}
button:hover {
background: #1d4ed8;
}
.link {
text-align: center;
margin-top: 16px;
font-size: 14px;
}
.link a {
color: #2563eb;
text-decoration: none;
}
7.3 登录页面(login_db.php)
php
复制代码
require 'db.php';
session_start();
header("X-XSS-Protection: 1; mode=block");
header("X-Frame-Options: DENY");
$error = '';
$fillUser = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$username = trim($_POST['username'] ?? '');
$password = $_POST['password'] ?? '';
$fillUser = $username;
// 非空校验
if (empty($username) || empty($password)) {
$error = '用户名和密码均不能为空';
} else {
try {
$pdo = getPdo();
// 预处理查询用户
$sql = "SELECT id, username, password, avatar FROM users WHERE username = :u";
$stmt = $pdo->prepare($sql);
$stmt->execute([':u' => $username]);
$user = $stmt->fetch();
// 校验账号与密码哈希
if ($user && password_verify($password, $user['password'])) {
$_SESSION['logged_in'] = true;
$_SESSION['user_id'] = $user['id'];
$_SESSION['username'] = $user['username'];
$_SESSION['avatar'] = $user['avatar'];
header('Location: welcome_db.php', true, 302);
exit;
} else {
$error = '用户名或密码不正确';
}
} catch (PDOException $e) {
$error = '登录异常,请稍后重试';
}
}
}
?>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: "Microsoft YaHei", sans-serif;
}
body {
background-color: #f4f6f9;
padding: 60px 20px;
}
.login-card {
width: 380px;
margin: 0 auto;
background: #fff;
padding: 32px;
border-radius: 10px;
box-shadow: 0 2px 12px rgba(0,0,0,0.07);
}
h2 {
text-align: center;
color: #2d3748;
margin-bottom: 24px;
}
.tip-error {
background: #fee;
color: #dc2626;
padding: 10px;
border-radius: 6px;
margin-bottom: 16px;
text-align: center;
}
.form-row {
margin-bottom: 16px;
}
label {
display: block;
margin-bottom: 6px;
color: #4a5568;
}
input {
width: 100%;
padding: 10px 12px;
border: 1px solid #cbd5e0;
border-radius: 6px;
font-size: 15px;
}
button {
width: 100%;
padding: 11px;
background: #2563eb;
color: white;
border: none;
border-radius: 6px;
font-size: 16px;
cursor: pointer;
}
button:hover {
background: #1d4ed8;
}
.reg-link {
text-align: center;
margin-top: 16px;
font-size: 14px;
}
.reg-link a {
color: #2563eb;
text-decoration: none;
}
7.4 欢迎页面与退出
welcome_db.php:
php
复制代码
session_start();
// 未登录拦截跳转
if (empty($_SESSION['logged_in']) || $_SESSION['logged_in'] !== true) {
header('Location: login_db.php', true, 302);
exit;
}
$userName = htmlspecialchars($_SESSION['username'], ENT_QUOTES);
$avatar = $_SESSION['avatar'] ?? '';
?>
* {
margin: 0;
padding: 0;
font-family: "Microsoft YaHei";
}
body {
background: #f4f6f9;
padding-top: 100px;
text-align: center;
}
.box {
width: 420px;
margin: 0 auto;
background: #fff;
padding: 40px;
border-radius: 10px;
box-shadow: 0 2px 12px rgba(0,0,0,0.07);
}
h2 {
color: #2d3748;
margin-bottom: 20px;
}
.avatar-wrap {
margin: 24px 0;
}
.avatar-img {
width: 140px;
height: 140px;
border-radius: 50%;
object-fit: cover;
border: 3px solid #2563eb;
}
.avatar-empty {
width: 140px;
height: 140px;
border-radius: 50%;
background: #e2e8f0;
display: inline-flex;
align-items: center;
justify-content: center;
color: #666;
font-size:14px;
}
.logout-btn {
display: inline-block;
margin-top: 24px;
padding: 10px 24px;
background: #6c757d;
color: #fff;
text-decoration: none;
border-radius: 6px;
}
.logout-btn:hover {
background: #5a6268;
}
logout_db.php:
php
复制代码
session_start();
// 清空会话数据
$_SESSION = [];
// 销毁浏览器Session Cookie
if (ini_get("session.use_cookies")) {
$cookieParam = session_get_cookie_params();
setcookie(
session_name(),
'',
time() - 86400,
$cookieParam["path"],
$cookieParam["domain"],
$cookieParam["secure"],
$cookieParam["httponly"]
);
}
// 销毁服务器端会话文件
session_destroy();
// 跳转登录页
header('Location: login_db.php', true, 302);
exit;
?>
八、综合实战二:文章发布与列表(CRUD 完整示例)
继续完善博客系统,实现文章的增、查、改、删。
8.1 创建文章表
在 blog 数据库中执行:
sql
复制代码
USE blog;
CREATE TABLE posts (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
user_id INT UNSIGNED NOT NULL,
title VARCHAR(200) NOT NULL,
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;
外键约束确保文章必须有对应的用户,用户删除时其文章也删除。
8.2 发布文章(create_post.php)
php
复制代码
require 'db.php';
session_start();
header("X-XSS-Protection: 1; mode=block");
header("X-Frame-Options: DENY");
// 未登录拦截
if (empty($_SESSION['logged_in'])) {
header('Location: login_db.php', true, 302);
exit;
}
// CSRF令牌
if (empty($_SESSION['csrf_post'])) {
$_SESSION['csrf_post'] = bin2hex(random_bytes(32));
}
$errors = [];
$titleFill = '';
$contentFill = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// CSRF校验
$token = $_POST['csrf_token'] ?? '';
if (!hash_equals($_SESSION['csrf_post'], $token)) {
$errors[] = '非法提交请求';
}
$titleFill = trim($_POST['title'] ?? '');
$contentFill = trim($_POST['content'] ?? '');
// 表单校验
if (empty($titleFill)) $errors[] = '文章标题不能为空';
if (mb_strlen($titleFill) > 200) $errors[] = '标题不能超过200个字符';
if (empty($contentFill)) $errors[] = '文章内容不能为空';
if (empty($errors)) {
try {
$pdo = getPdo();
$stmt = $pdo->prepare("INSERT INTO posts (user_id, title, content) VALUES (:uid, :title, :content)");
$stmt->execute([
':uid' => $_SESSION['user_id'],
':title' => $titleFill,
':content' => $contentFill
]);
header('Location: list_posts.php', true, 302);
exit;
} catch (PDOException $e) {
$errors[] = '文章发布失败,请稍后重试';
}
}
}
?>
* {margin:0;padding:0;box-sizing:border-box;font-family:"Microsoft YaHei";}
body {background:#f5f7fa;padding:60px 20px;}
.card {width:620px;margin:0 auto;background:#fff;padding:32px;border-radius:10px;box-shadow:0 2px 12px rgba(0,0,0,0.08);}
h2 {text-align:center;margin-bottom:24px;color:#2d3748;}
.err-box {background:#fee;color:#dc2626;padding:10px;border-radius:6px;margin-bottom:16px;}
.item {margin-bottom:16px;}
label {display:block;margin-bottom:6px;color:#444;}
input, textarea {width:100%;padding:12px;border:1px solid #cbd5e0;border-radius:6px;font-size:15px;}
textarea {height:240px;resize:none;}
button {width:100%;padding:12px;background:#2563eb;color:#fff;border:none;border-radius:6px;font-size:16px;cursor:pointer;}
button:hover {background:#1d4ed8;}
.link-back {display:block;text-align:center;margin-top:16px;color:#666;text-decoration:none;}
8.3 文章列表(list_posts.php)
php
复制代码
require 'db.php';
session_start();
header("X-XSS-Protection: 1; mode=block");
header("X-Frame-Options: DENY");
$pdo = getPdo();
// 联表查询文章+作者
$sql = "SELECT p.id, p.title, p.created_at, u.username
FROM posts p
JOIN users u ON p.user_id = u.id
ORDER BY p.created_at DESC";
$stmt = $pdo->query($sql);
$postList = $stmt->fetchAll();
?>
* {margin:0;padding:0;box-sizing:border-box;font-family:"Microsoft YaHei";}
body {background:#f5f7fa;padding:40px 20px;}
.wrap {max-width:720px;margin:0 auto;}
.top-bar {display:flex;justify-content:space-between;align-items:center;margin-bottom:24px;}
h1 {color:#2d3748;font-size:24px;}
.btn-pub {padding:8px 16px;background:#2563eb;color:#fff;text-decoration:none;border-radius:6px;}
.post-item {background:#fff;padding:20px;border-radius:10px;box-shadow:0 1px 8px rgba(0,0,0,0.06);margin-bottom:16px;}
.post-title {font-size:18px;margin-bottom:8px;}
.post-title a {color:#2563eb;text-decoration:none;}
.post-meta {color:#666;font-size:14px;}
.empty-tip {text-align:center;padding:40px;color:#888;background:#fff;border-radius:10px;}
8.4 查看文章(view_post.php)
php
复制代码
require 'db.php';
session_start();
header("X-XSS-Protection: 1; mode=block");
header("X-Frame-Options: DENY");
$id = trim($_GET['id'] ?? '');
$pdo = getPdo();
$sql = "SELECT p.*, u.username, u.id as author_uid
FROM posts p
JOIN users u ON p.user_id = u.id
WHERE p.id = :pid";
$stmt = $pdo->prepare($sql);
$stmt->execute([':pid' => $id]);
$post = $stmt->fetch();
// 不存在拦截
if (!$post) {
header('Location: list_posts.php', true, 302);
exit;
}
$isAuthor = (!empty($_SESSION['logged_in']) && $_SESSION['user_id'] == $post['author_uid']);
?>
* {margin:0;padding:0;box-sizing:border-box;font-family:"Microsoft YaHei";}
body {background:#f5f7fa;padding:40px 20px;}
.box {max-width:700px;margin:0 auto;background:#fff;padding:36px;border-radius:10px;box-shadow:0 2px 12px rgba(0,0,0,0.08);}
h1 {text-align:center;margin-bottom:16px;color:#222;}
.meta {text-align:center;color:#666;margin-bottom:24px;padding-bottom:16px;border-bottom:1px solid #eee;}
.content {line-height:1.8;font-size:16px;color:#333;white-space:pre-wrap;}
.btn-group {margin-top:30px;text-align:center;}
.btn {display:inline-block;padding:8px 16px;margin:0 6px;text-decoration:none;border-radius:6px;}
.btn-edit {background:#059669;color:#fff;}
.btn-del {background:#dc2626;color:#fff;}
.btn-back {background:#666;color:#fff;}
8.5 编辑文章(edit_post.php)
php
复制代码
require 'db.php';
session_start();
header("X-XSS-Protection: 1; mode=block");
header("X-Frame-Options: DENY");
// 未登录拦截
if (empty($_SESSION['logged_in'])) {
header('Location: login_db.php', true, 302);
exit;
}
$id = trim($_GET['id'] ?? '');
$pdo = getPdo();
// 查询文章,校验归属
$stmt = $pdo->prepare("SELECT * FROM posts WHERE id = :pid");
$stmt->execute([':pid' => $id]);
$post = $stmt->fetch();
// 无文章/不是作者拦截
if (!$post || $post['user_id'] != $_SESSION['user_id']) {
header('Location: list_posts.php', true, 302);
exit;
}
// CSRF
if (empty($_SESSION['csrf_edit'])) {
$_SESSION['csrf_edit'] = bin2hex(random_bytes(32));
}
$errors = [];
$titleFill = $post['title'];
$contentFill = $post['content'];
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$token = $_POST['csrf_token'] ?? '';
if (!hash_equals($_SESSION['csrf_edit'], $token)) {
$errors[] = '非法请求';
}
$titleFill = trim($_POST['title'] ?? '');
$contentFill = trim($_POST['content'] ?? '');
if (empty($titleFill)) $errors[] = '标题不能为空';
if (mb_strlen($titleFill) > 200) $errors[] = '标题最多200字符';
if (empty($contentFill)) $errors[] = '内容不能为空';
if (empty($errors)) {
try {
$upd = $pdo->prepare("UPDATE posts SET title=:t, content=:c WHERE id=:id");
$upd->execute([
':t' => $titleFill,
':c' => $contentFill,
':id' => $id
]);
header("Location: view_post.php?id=$id", true, 302);
exit;
} catch (PDOException $e) {
$errors[] = '保存修改失败';
}
}
}
?>
* {margin:0;padding:0;box-sizing:border-box;font-family:"Microsoft YaHei";}
body {background:#f5f7fa;padding:60px 20px;}
.card {width:620px;margin:0 auto;background:#fff;padding:32px;border-radius:10px;box-shadow:0 2px 12px rgba(0,0,0,0.08);}
h2 {text-align:center;margin-bottom:24px;color:#2d3748;}
.err-box {background:#fee;color:#dc2626;padding:10px;border-radius:6px;margin-bottom:16px;}
.item {margin-bottom:16px;}
label {display:block;margin-bottom:6px;color:#444;}
input, textarea {width:100%;padding:12px;border:1px solid #cbd5e0;border-radius:6px;font-size:15px;}
textarea {height:240px;resize:none;}
button {width:100%;padding:12px;background:#059669;color:#fff;border:none;border-radius:6px;font-size:16px;cursor:pointer;}
button:hover {background:#047857;}
.link-back {display:block;text-align:center;margin-top:16px;color:#666;text-decoration:none;}
8.6 删除文章(delete_post.php)
php
复制代码
require 'db.php';
session_start();
header("X-XSS-Protection: 1; mode=block");
header("X-Frame-Options: DENY");
if (empty($_SESSION['logged_in'])) {
header('Location: login_db.php', true, 302);
exit;
}
$id = trim($_GET['id'] ?? '');
$pdo = getPdo();
$stmt = $pdo->prepare("SELECT id, title, user_id FROM posts WHERE id = :pid");
$stmt->execute([':pid' => $id]);
$post = $stmt->fetch();
// 拦截无权限/不存在
if (!$post || $post['user_id'] != $_SESSION['user_id']) {
header('Location: list_posts.php', true, 302);
exit;
}
// CSRF
if (empty($_SESSION['csrf_del'])) {
$_SESSION['csrf_del'] = bin2hex(random_bytes(32));
}
$err = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$token = $_POST['csrf_token'] ?? '';
if (!hash_equals($_SESSION['csrf_del'], $token)) {
$err = '非法操作';
} else {
try {
$del = $pdo->prepare("DELETE FROM posts WHERE id=:id");
$del->execute([':id' => $id]);
header('Location: list_posts.php', true, 302);
exit;
} catch (PDOException $e) {
$err = '删除失败,请重试';
}
}
}
?>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: "Microsoft YaHei";
}
body {
background: #f5f7fa;
padding: 80px 20px;
}
.card {
width: 460px;
margin: 0 auto;
background: #fff;
padding: 36px;
border-radius: 10px;
box-shadow: 0 2px 12px rgba(0,0,0,0.08);
text-align: center;
}
h2 {
color: #dc2626;
margin-bottom: 16px;
font-size: 20px;
}
.tip {
margin-bottom: 24px;
color: #444;
line-height: 1.7;
}
.err {
color: #dc2626;
margin-bottom: 16px;
}
/* 弹性容器,两个子容器平分宽度 */
.btn-group {
display: flex;
gap: 12px;
}
/* form 和 a 容器各占一半宽度 */
.btn-group form,
.btn-group .cancel-wrap {
flex: 1;
}
/* 按钮与链接统一尺寸,填满父容器 */
.btn-group button,
.btn-group a {
display: block;
width: 100%;
height: 44px;
line-height: 44px;
padding: 0;
border-radius: 6px;
font-size: 16px;
text-decoration: none;
border: none;
cursor: pointer;
text-align: center;
}
.btn-confirm {
background: #dc2626;
color: #fff;
}
.btn-cancel {
background: #666;
color: #fff;
}
删除确认
确定要永久删除文章:
= htmlspecialchars($post['title']) ?>?
删除后数据无法恢复!
访问:http://localhost/list_posts.php
九、高级但实用的知识点
9.1 事务(Transaction)
当一系列数据库操作必须全部成功或全部失败(如转账:A 扣钱,B 加钱),要用事务。
php
复制代码
$pdo->beginTransaction();
try {
$stmt1 = $pdo->prepare("UPDATE accounts SET balance = balance - 100 WHERE id = 1");
$stmt1->execute();
$stmt2 = $pdo->prepare("UPDATE accounts SET balance = balance + 100 WHERE id = 2");
$stmt2->execute();
$pdo->commit();
} catch (Exception $e) {
$pdo->rollBack();
throw $e;
}
?>
9.2 分页查询
利用 LIMIT 和 OFFSET 实现。
php
复制代码
$page = max(1, (int)($_GET['page'] ?? 1));
$perPage = 10;
$offset = ($page - 1) * $perPage;
$stmt = $pdo->prepare("SELECT * FROM posts ORDER BY id DESC LIMIT :limit OFFSET :offset");
$stmt->bindValue(':limit', $perPage, PDO::PARAM_INT);
$stmt->bindValue(':offset', $offset, PDO::PARAM_INT);
$stmt->execute();
$posts = $stmt->fetchAll();
// 获取总数计算总页数
$totalStmt = $pdo->query("SELECT COUNT(*) FROM posts");
$total = $totalStmt->fetchColumn();
$totalPages = ceil($total / $perPage);
?>
9.3 防止 SQL 注入的高级注意事项
即使是 ORDER BY 或 LIMIT,参数也应使用白名单验证,因占位符不能用于这些地方。
对于 IN (...) 查询,需要动态生成占位符并绑定。
仍然要使用 htmlspecialchars 输出数据库内容,防止 XSS。
十、总结
MySQL 基础:理解了数据库、表、行、列、主键、外键的概念,学会了使用 SQL 语句(CREATE、INSERT、SELECT、UPDATE、DELETE)。
phpMyAdmin:可视化操作数据库的便捷工具,适合初学者。
PDO 连接:掌握了安全稳定的连接方式,配置属性让错误处理更友好。
预处理语句:彻底解决 SQL 注入,使用命名占位符或问号绑定参数,并应用于增删改查。
综合实战:将注册登录系统升级为数据库版,并完整实现了博客文章的 CRUD,包括权限验证。
进阶技巧:事务、分页、更细致的注入防御。
如果这篇文章帮你解决了实操上的困惑,别忘记点击点赞、分享 ,也可以留言告诉我你遇到的其它问题,我会尽快回复。动手练习是掌握编程最快的方法,请务必亲手敲一遍本文的所有示例代码,并截图保存你的成果。你的关注是我坚持原创和细节共享的力量来源,谢谢大家。