从零开始学 PHP 系列(六):MySQL 数据库与 PHP 交互——让数据真正“住”进服务器

从零开始学 PHP 系列(六):MySQL 数据库与 PHP 交互——让数据真正“住”进服务器

摘要 :前面几篇,我们的数据都存储在变量、数组甚至文件中,但真正的网站需要持久化、可查询、高性能的数据存储。数据库就是为此而生。本篇将带你走进关系型数据库 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'] = '注册失败,服务器繁忙,请稍后重试';

}

}

}

?>

用户注册

新用户注册

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 = '登录异常,请稍后重试';

}

}

}

?>

账号登录

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'] ?? '';

?>

个人中心

欢迎你,

当前页面已登录保护,未登录用户无法访问

用户头像

暂无头像

安全退出登录

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[] = '文章发布失败,请稍后重试';

}

}

}

?>

发布文章

发布新文章

', array_map(fn($v)=>htmlspecialchars($v,ENT_QUOTES), $errors)) ?>

返回文章列表

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();

?>

全部文章

文章列表

发布新文章

暂无任何文章,快去发布第一篇吧

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']);

?>

<?= htmlspecialchars($post['title']) ?>

作者: | 发布时间:

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[] = '保存修改失败';

}

}

}

?>

编辑文章

编辑文章

', array_map(fn($v)=>htmlspecialchars($v,ENT_QUOTES), $errors)) ?>

取消返回文章

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 = '删除失败,请重试';

}

}

}

?>

确认删除文章

删除确认

确定要永久删除文章:


删除后数据无法恢复!

访问: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,包括权限验证。

进阶技巧:事务、分页、更细致的注入防御。

如果这篇文章帮你解决了实操上的困惑,别忘记点击点赞、分享 ,也可以留言告诉我你遇到的其它问题,我会尽快回复。动手练习是掌握编程最快的方法,请务必亲手敲一遍本文的所有示例代码,并截图保存你的成果。你的关注是我坚持原创和细节共享的力量来源,谢谢大家。

相关推荐

马品种登记相关小知识
bet36365首页

马品种登记相关小知识

📅 09-27 👁️ 9382
梁祝是什么意思
bet36365首页

梁祝是什么意思

📅 01-11 👁️ 6211
​斑鸠寿命
bet36365首页

​斑鸠寿命

📅 10-02 👁️ 613