
简介一套基于PHP与MySQL开发的图书管理系统完整源码涵盖前端页面与后端处理逻辑面向计算机专业学生、PHP初学者以及需要快速搭建图书管理功能的开发者。系统通常包含图书信息录入、借阅与归还管理、读者管理、管理员登录等核心模块可据此理解Web开发中前后端交互、数据库设计及增删改查的基础实现。压缩包约为39.55MB其中包含可直接部署的PHP代码文件、MySQL数据库导出脚本以及前端样式与页面资源结构上清晰区分前端展示目录与后端逻辑目录便于对照学习、二次修改和本地部署。目前已有3440人学习下载适合用于课程设计、毕业设计或技能练习。通过实际运行这套源码读者能掌握从数据库建表到页面调用的完整流程积累项目部署与调试经验也可将系统改造优化后作为个人作品展示。1. 图书管理系统为什么还在用 PHPMySQL能跑通借阅闭环才是核心PHPMYSQL 的图书管理系统是新手第一个能触摸“前端后端”完整链路的练手项目也是课设、毕设和面试作品里出现频率最高的 PHP 源码之一。说它简单的人大多只做过单表增删改查真正把登录鉴权、图书库存、借书还书、逾期状态串成闭环才能在别人给这套源码加需求时不翻车。下面这套方案按“可复现”来组织本地环境怎么搭、三张表怎么建、后端接口怎么写、前端页面怎么联调、上线前要避开哪些坑。目标是让你拿到任意一份类似的 PHPMySQL 图书管理系统源码都能快速跑起来并改造成自己的作品。2. 环境与目录先让 PHP 和 MySQL 在本地握手成功2.1 版本选型PHP 8.x 配 MySQL 5.7 最稳不要在这步较劲动手之前把版本定下来能省掉后面一半的翻车。常见做法是使用集成环境phpStudy、XAMPP一键拉起 PHP 和 MySQL也可以照着 MySQL 安装教程手动安装。我一般选 PHP 8.1 MySQL 5.7 的组合PHP 8.x 对旧版写法兼容性尚可性能比 PHP 5.6 和 7.0 明显更好MySQL 5.7 资料多、稳定性经过大量验证InnoDB 的事务支持对付图书借阅这种小并发场景绰绰有余。提示不要一上来就追 PHP 8.3 / MySQL 8.4 的最新内核。图书管理系统是学习型项目核心诉求是“能复现、报错能搜到答案”。网上现成的排查方案大多基于 PHP 7.4 / 8.0 / 8.1 和 MySQL 5.7版本太新遇到特性差异时连搜都搜不到同款。装完后在命令行确认版本php -v mysql --version如果php命令不被识别说明 PHP 没有加入 PATH。别急着改环境变量先确认集成环境自带的 php.exe 所在路径用全路径执行也可以。确认版本后注意两个常见端口问题Apache 默认占用 80 端口MySQL 占用 3306。如果本地有 IIS、Docker 或其他服务占用了端口集成环境启动会失败日志里会明确提示 bind 失败此时去修改对应配置文件里的端口号即可不要硬杀系统进程。2.2 最小目录与第一个可访问入口先写一个能连库的 index.php一套“图书管理系统全套源码”的目录不需要一开始就很复杂。常见做法是共用根目录但把 Web 可访问的入口和 PHP 业务源码分开我习惯这样组织library/ ├── public/ # 前端入口也是 Web 服务器的根目录 │ ├── index.php # 入口跳转到登录页或列表页 │ ├── login.php # 登录页 │ ├── dashboard.php # 登录后的主页面 │ └── assets/ # css / js / 图片 ├── src/ # 后端源码 │ ├── config.php # 数据库配置 │ ├── db.php # PDO 连接类 │ ├── auth.php # 登录鉴权函数 │ └── functions.php # 图书 CRUD、借还书函数 └── sql/ └── init.sql # 建库建表脚本把 public 设为 Web 根目录而不是整个 library是为了不让用户通过 URL 直接访问 config.php 和 sql 目录。这一步很多人忽略如果根目录直接暴露源码数据库密码等于放在浏览器地址栏里裸奔。先写最基础的 index.php只做一件事连库执行一条最简单的查询。?php // public/index.php require __DIR__ . /../src/db.php; $stmt $pdo-query(SELECT 1 AS ok); $row $stmt-fetch(PDO::FETCH_ASSOC); echo $row[ok] 1 ? database ok : database fail;这段代码有两个细节要注意。第一require __DIR__ . /../src/db.php用的是绝对路径拼接而不是../src/db.php这种相对路径。__DIR__表示当前文件所在目录即使换目录部署、被软链接访问也不会因为“当前工作目录”变化而找不到文件。第二SELECT 1不依赖任何业务表用来验证 PHP 与 MySQL 的连通性比一上来就查图书表更容易定位问题。要让这个入口真正生效还得把 Web 服务器的根目录指到 public。以 Apache 为例常见做法是配置虚拟主机VirtualHost *:80 DocumentRoot D:/www/library/public ServerName library.local /VirtualHost配置完成后重启服务浏览器打开http://library.local看到 “database ok”说明 PHP 和 MySQL 已经握手成功可以进入建表环节。如果你习惯用 phpStudy 这类面板也可以在面板里创建网站时直接把根目录选到 public效果一样。3. 数据层设计图书管理系统最核心的三张表与建表 SQL3.1 三张表的关系book、user、borrow为什么不能只建一张表写代码前先想清楚数据模型。图书管理系统的业务名词只有三个书、读者用户、借阅记录。很多人图省事在一张表里塞“书名 借书人 借出日期 归还日期”看起来数据齐了实际上当一本书被不同人先后借过、或者一个读者连借五本时一张表会产生大量冗余改书名要改多处统计历史借阅更是灾难。正确的拆分是三个主表book图书实体保存书名、作者、分类、馆藏总数、当前可借数量user登录账号与读者信息包含角色字段区分管理员和普通会员borrow一次借阅行为只存 user_id 和 book_id 两个外键以及借出日期、应还日期、归还日期、状态。这种设计对应“一对多”关系一个读者可以拥有多条借阅记录一本书也可以在不同时间被不同人借阅。借阅记录表里不重复存书名和读者名只存 ID这样修改书名时只需更新 book 表一处历史借阅记录自动关联到新值。为什么不做第四张“分类表”对于课设级别的系统分类用 book 表里的一个 category 字段就够省一次 JOIN查询更快。想要完整分类管理功能的读者可以在 V2.0 自行演进第一阶段先跑通主链路不给自己加戏。3.2 建库建表与初始数据字段设计和索引取舍下面是一份直接可执行的 init.sql。字符集统一使用 utf8mb4排序规则使用 utf8mb4_unicode_ci这是中文环境下最不容易出乱码的组合。CREATE DATABASE IF NOT EXISTS library DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci; USE library; CREATE TABLE user ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50) NOT NULL UNIQUE, password_hash VARCHAR(255) NOT NULL, email VARCHAR(100) DEFAULT , role ENUM(admin, member) NOT NULL DEFAULT member, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB; CREATE TABLE book ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, title VARCHAR(200) NOT NULL, author VARCHAR(100) NOT NULL DEFAULT , category VARCHAR(50) NOT NULL DEFAULT 未分类, total INT UNSIGNED NOT NULL DEFAULT 0 COMMENT 馆藏总数, available INT UNSIGNED NOT NULL DEFAULT 0 COMMENT 当前可借数量, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, INDEX idx_category (category) ) ENGINEInnoDB; CREATE TABLE borrow ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, user_id INT UNSIGNED NOT NULL, book_id INT UNSIGNED NOT NULL, borrow_date DATE NOT NULL, due_date DATE NOT NULL, return_date DATE DEFAULT NULL, status TINYINT NOT NULL DEFAULT 0 COMMENT 0借出中 1已归还 2逾期, FOREIGN KEY (user_id) REFERENCES user(id), FOREIGN KEY (book_id) REFERENCES book(id), INDEX idx_user_status (user_id, status) ) ENGINEInnoDB;逐段说明几个关键设计UNSIGNED用在自增主键上正整数的范围翻一倍对图书管理系统这种数据量完全够用也避免主键出现负数这种低级错误。password_hash VARCHAR(255)是为password_hash()预留的长度。PHP 的 bcrypt 哈希固定 60 个字符但改用 Argon2 时输出更长留 255 以后换算法不用改表。available 字段是核心冗余字段它等于“馆藏总数 - 借出中的数量”。直接存一个值查询列表时不用实时 JOIN 计算速度更快。代价是必须由程序保证它和 borrow 表状态一致第 4 章会用事务处理。borrow 表的status TINYINT用数字而不是字符串从 0 到 2 表示借出中、已归还、逾期。注意逾期不是独立事件而是“应还日期已过且未归还”最稳妥的判断条件是due_date CURDATE() AND return_date IS NULL不要让定时任务天天去改状态避免脚本漏跑导致数据失真。外键约束在 InnoDB 下默认生效它能阻止插入 user_id / book_id 不存在的数据。索引方面borrow 表的(user_id, status)联合索引专门服务“某读者当前有哪些书没还”这类高频查询单查 book 时主键已足够。建表后插入初始数据一个管理员账号、两本用于联调的书。管理员密码不能手写明文用 PHP 一行命令生成哈希?php // 一次性脚本生成初始账号密码的 bcrypt 哈希 echo password_hash(admin123, PASSWORD_DEFAULT), PHP_EOL;把脚本保存为 gen_pwd.php命令行执行php gen_pwd.php把输出的哈希字符串填到下面的 SQL 中INSERT INTO user (username, password_hash, role) VALUES (admin, 替换成上面生成的哈希, admin); INSERT INTO book (title, author, category, total, available) VALUES (深入理解计算机系统, Randal E. Bryant, 计算机, 5, 5), (MySQL必知必会, Ben Forta, 数据库, 3, 3);注意 total 和 available 的初始值是一样的新采购的书全部在架上此时可借数量等于馆藏总数。借出时 available 减一还书时加一。后面的借还书逻辑都围绕这两个字段展开也是并发问题的高发区。4. 后端逻辑登录鉴权、图书 CRUD 与借还书的 PHP 实现4.1 数据库连接层为什么选 PDO 而不是 mysqliPHP 连 MySQL 有两条路mysqli 和 PDO。网上很多老源码用 mysqli它确实足够简单但我的建议是统一用 PDO理由只有一个预处理语句prepared statement是防 SQL 注入最省事的方式PDO 能让你写出“默认安全”的代码而 mysqli 只有时刻记得参数化时才是安全的。连写三次字符串拼接注入漏洞就回来了。?php // src/db.php $host 127.0.0.1; $db library; $user root; $pass root; $dsn mysql:host$host;dbname$db;charsetutf8mb4; $options [ PDO::ATTR_DEFAULT_FETCH_MODE PDO::FETCH_ASSOC, PDO::ATTR_ERRMODE PDO::ERRMODE_EXCEPTION, PDO::ATTR_EMULATE_PREPARES false, ]; try { $pdo new PDO($dsn, $user, $pass, $options); } catch (PDOException $e) { exit(数据库连接失败 . $e-getMessage()); }三个参数逐一说明PDO::ATTR_DEFAULT_FETCH_MODE PDO::FETCH_ASSOC查询结果默认返回关联数组。不设置的话PDO 默认返回索引数组加关联数组两份数据内存翻倍取字段时还容易混淆下标。PDO::ERRMODE_EXCEPTION出错时抛异常。很多老源码用or die()一报错就把 SQL 和数据库结构打印到页面上这个习惯必须改异常可以被上层 catch 后统一处理。PDO::ATTR_EMULATE_PREPARES false关闭本地预处理模拟让真实的参数化查询生效。这是 PDO 防注入的关键开关。开着时 PDO 会用字符串拼接模拟预处理某些编码组合下存在被绕过的风险。4.2 登录接口与 Session 鉴权密码哈希是底线登录是前后端联调的第一个动作。典型流程前端把用户名和密码 POST 到 login.php后端查 user 表验证通过后写入 Session再把跳转地址返回给浏览器。?php // public/login.php session_start(); require __DIR__ . /../src/db.php; if ($_SERVER[REQUEST_METHOD] POST) { $username trim($_POST[username] ?? ); $password $_POST[password] ?? ; $stmt $pdo-prepare(SELECT * FROM user WHERE username ? LIMIT 1); $stmt-execute([$username]); $user $stmt-fetch(); if ($user password_verify($password, $user[password_hash])) { session_regenerate_id(true); $_SESSION[user_id] (int)$user[id]; $_SESSION[role] $user[role]; $_SESSION[username] $user[username]; header(Location: dashboard.php); exit; } $error 用户名或密码错误; } ?逻辑顺序是先按用户名查出整行再用password_verify()比对哈希。密码错误时只提示“用户名或密码错误”不区分“用户不存在”和“密码错误”避免通过接口枚举有效用户名。session_regenerate_id(true)放在登录成功后是必须操作PHP 默认的 Session ID 在登录前可能已经被别人拿到登录后重新生成 ID相当于给这次会话换了一把新钥匙能抵御会话固定攻击。注意不要用$_SESSION[is_login] true这种标志位。登录后如果用户角色变化标志位不会自动更新权限判断就会失灵。这里直接存 user_id 和 role每次需要鉴权的页面都重新查库或校验 Session 中的角色值。如果读者想在本地调试这段代码推荐直接用 PhpStorm 跑内置服务器断点打到password_verify这一行可以直观看到请求参数和 Session 变化比到处var_dump高效得多。4.3 借书与还书库存扣减的事务处理图书管理系统最容易翻车的就是借书逻辑。表面上只是“往 borrow 表插一条记录”实际牵涉三步检查读者有借书资格、检查图书有库存、插入借阅记录并扣减 available。这三步必须在一个数据库事务里完成否则就会出现“借阅记录插进去了库存没减”的脏数据。?php // src/functions.php function borrowBook(PDO $pdo, int $userId, int $bookId, string $dueDate): array { try { $pdo-beginTransaction(); $stmt $pdo-prepare( SELECT total, available FROM book WHERE id ? FOR UPDATE ); $stmt-execute([$bookId]); $book $stmt-fetch(); if (!$book) { throw new RuntimeException(图书不存在); } if ($book[available] 0) { throw new RuntimeException(库存不足); } $stmt $pdo-prepare( INSERT INTO borrow (user_id, book_id, borrow_date, due_date) VALUES (?, ?, CURDATE(), ?) ); $stmt-execute([$userId, $bookId, $dueDate]); $stmt $pdo-prepare( UPDATE book SET available available - 1 WHERE id ? ); $stmt-execute([$bookId]); $pdo-commit(); return [ok true]; } catch (Throwable $e) { $pdo-rollBack(); return [ok false, error $e-getMessage()]; } }这段代码是整套系统的核心三个点要讲透SELECT ... FOR UPDATE是行级锁。它把目标图书的记录锁住直到当前事务提交或回滚。两个用户同时借最后一本书时第二个请求会等待第一个事务结束后重新读取此时 available 已经是 0直接抛“库存不足”。不加 FOR UPDATE两个请求都可能读到 available1都执行插入最后 available 变成 -1这就是常见的“超借”事故。事务边界要尽量小。beginTransaction 到 commit 之间只做“检查、插入、更新”三个数据库动作不调用外部接口、不发送邮件、不写文件。事务内动作越多行锁持有时间越长并发能力越差。函数返回数组而不是直接输出 JSON是为了让控制器层决定响应格式。网页端可以跳转页面接口端可以返回 JSON业务函数保持单纯方便复用。还书逻辑与借书对称在同一事务里更新 borrow 表的 return_date 和 status再把 book 表的 available 加一。这里不再重复贴代码注意还书时也需要 FOR UPDATE 锁行防止归还的同时有人发起借阅造成库存计算竞态。5. 前端联调与高频翻车排查列表渲染、表单提交和背后的 5 个坑5.1 前端页面与接口联调fetch 拿 JSON 渲染图书列表“前端 后端”全套源码里的前端常见做法是原生 HTML/CSS/JavaScript 配合必要的 PHP 模板动态区域用 fetch 请求 JSON 后渲染。图书列表页是最典型场景加载时向 list_books.php 请求数据前端负责画表格实现局部刷新。?php // public/list_books.php header(Content-Type: application/json; charsetutf-8); require __DIR__ . /../src/db.php; $stmt $pdo-query( SELECT b.id, b.title, b.author, b.category, b.total, b.available FROM book b ORDER BY b.id DESC LIMIT 50 ); echo json_encode($stmt-fetchAll(), JSON_UNESCAPED_UNICODE);!-- public/dashboard.php 中的列表渲染片段 -- div idbook-list/div script fetch(list_books.php) .then(res res.json()) .then(books { const box document.getElementById(book-list); let html table classtbl trth书名/thth作者/thth分类/th th可借/总数/thth操作/th/tr; books.forEach(b { html tr td${b.title}/td td${b.author}/td td${b.category}/td td${b.available}/${b.total}/td tda hrefborrow.php?book_id${b.id}借阅/a/td /tr; }); html /table; box.innerHTML html; }) .catch(err console.error(加载失败, err)); /script这里两个细节决定成败。后端输出 JSON 时必须带JSON_UNESCAPED_UNICODE否则 PHP 会把中文转成\uXXXX浏览器虽然能正确解析但在开发者工具里看响应全是转义字符排查问题非常痛苦。前端用模板字符串拼接 HTML只适合几十条数据一旦图书达到几百本必须改造为分页接口否则首屏渲染会明显变慢。5.2 乱码页面全是问号的三级排查现象浏览器里中文显示成“??”或乱码登录后读者姓名、书名全都不对。原因要按三层排查第一层是数据库表和字段的字符集第二层是 PHP 连接数据库时 DSN 里的 charset第三层是页面本身的 meta 字符集声明。三层必须一致中文才能在“数据库 → PHP → 浏览器”这条链路上完整走通。解决步骤是自下而上确认。先执行SHOW CREATE DATABASE library看库字符集再检查 db.php 里的 DSN 是否写了charsetutf8mb4最后看页面源码有没有meta charsetutf-8。我遇到最多的情况是前两层都对唯独忘了写 meta 标签浏览器按系统默认编码解析中文直接挂掉。在排查这类问题时按顺序检查不跳步能少走很多弯路。5.3 SQL 注入和密码明文源码级安全隐患现象图书数据被人清空、后台被登录、数据库被拖走。原因代码里用字符串拼接 SQL典型写法是SELECT * FROM user WHERE username $_POST[username]用户提交一段 OR 11就能绕过校验另一个隐患是密码明文存储数据库一旦泄露所有账号直接暴露。解决SQL 统一换成 PDO 预处理按第 4 章的写法用?占位符绑定参数用户输入永远只被当作数据不会被拼进 SQL 语法。密码统一用password_hash()存储、password_verify()校验不要再用 MD5 这类已被秒破的算法。这两处改完之后源码的整体安全性已经超过大多数网上的 PHP 免费建站模板。5.4 并发与超借available 变成负值现象只剩 1 本的书两个人同时点借阅两条记录都插入成功available 变成 -1。原因借书逻辑没有使用事务和行锁。两个请求同时读到 available1各自执行插入库存字段被重复扣减。解决使用 4.3 的SELECT ... FOR UPDATE事务写法。更进一步可以在表结构上给 available 加约束CHECK (available 0)但注意 MySQL 5.7 对 CHECK 约束的执行并不严格它主要起文档作用真正的防线是事务锁。做完后可以用两个浏览器同时提交借阅请求测试不会再出现负数库存。5.5 路径引用失效换目录就白屏现象本地跑得好好的传到服务器或换个子目录后页面打开全是 404CSS 和 JS 全部加载失败。原因页面里写了相对路径如href./assets/style.css后端用了include ../xx.php这种相对写法。URL 层级一变相对路径就找不到目标文件。解决在入口文件里定义两个全局常量?php // public/config.php define(BASE_URL, /library/public/); define(APP_ROOT, __DIR__ . /../);模板里的静态资源一律写成? BASE_URL ?assets/style.cssPHP 引入文件统一用APP_ROOT . src/xxx.php。项目挪到任何目录只需要改 config.php 里这一行 BASE_URL其余代码不用动。这一步是源码工程化与随手网盘下载源码之间的一道分水岭。6. 从本地到上线用最小用例验证系统可部署再谈可扩展方向6.1 用三个用例验证核心链路是否闭环本地跑通后上线前用一组固定操作验证业务闭环管理员登录 → 新增一本测试书 → 用会员账号借出这本书 → 检查列表页可借数量减一 → 归还 → 数量恢复。这组用例覆盖了鉴权、图书 CRUD、借还书事务和列表渲染任何一步出错都能直接定位到对应模块比装完就上线稳妥得多。6.2 部署到服务器时的三个关键操作第一把 db.php 里的127.0.0.1、用户名、密码改成线上数据库的真实值DSN 里的 charsetutf8mb4 保留第二把 Web 根目录指向 public而不是整个项目目录否则 config.php 会暴露在公网第三给 public 目录设置 755 权限给可能上传文件的 assets 目录设置 755 或 775PHP 源码文件设置为 644 即可。注意不要在服务器上使用 root 作为数据库账号单独建一个只有 library 库权限的账号更稳妥。6.3 php 源码再往前走一步日志、权限与读者分级一套能交出去的图书管理系统源码除了基础功能还可以加三个实用模块借阅日志表记录每一次操作行为方便管理员追溯异常读者角色分级让普通用户只能借阅、管理员才能维护图书和查看所有借阅记录逾期罚金计算可以在还书时实时算天数不同等级读者享受不同借阅期限。我自己的习惯是把这些扩展都放到 V2.0 阶段做先把基础闭环跑稳再逐步叠加。希望这篇笔记能帮你把本地那套 PHPMySQL 图书管理系统真正跑起来也能让你在改别人源码时少踩几个同样的坑。本文还有配套的精品资源点击获取