简介:一套面向PHP学习者和毕业设计的图书管理系统源代码,采用PHP+MySQL实现,覆盖图书录入、分类管理、模糊搜索、在线借阅、归还处理及用户权限控制等完整业务闭环,适合用于课程实践、毕业设计或作为企业级Web开发的入门范本。压缩包总计112个文件,包括50个php业务脚本、frm/myd/myi数据库文件、gif/jpg/css/js前端资源以及doc使用说明,整体仅624KB,轻量易部署,便于直接导入本地环境运行调试。目前已有6542人学习浏览,反馈具有很高的参考价值。通过这套源码,读者可以系统理解MVC分层、PDO数据库操作、Session会话管理、SQL防注入等关键技术点;同时,项目附带数据库表结构与说明文档,方便二次开发,快速改造成带分类、借阅、统计功能的个性化图书管理系统,是巩固PHP实战能力的高性价比素材。
1. PHP图书管理系统:为什么这套老代码到今天还有人翻出来改
一个十几年前写的老项目,至今还在被一届又一届学生、培训班学员和想快速交付外包的开发翻出来改,这就是PHP图书管理系统。搜索PHP源码,你几乎绕不开它:后台管书、管读者、管借还,前台查书,加一个登录就收工。它本身不难,难的是你拿到的源代码多半是PHP 5时代的写法,连mysql扩展都已经在PHP 7里被移除,放到今天的PHP 8环境几乎必报错。这篇笔记就干两件事:先把这套老代码跑起来,再把它改到能用的状态,顺带把借书还书、中文乱码、登录失效这些边角坑讲透。适合刚学PHP想拿项目练手的人,也适合接了小单子要快速改出一套能交货的系统。
2. 跑起来之前:这套系统的技术栈和常见目录结构
2.1 为什么这套系统万年不变地选原生PHP加MySQL
市面上的PHP图书管理系统源代码绝大多数是原生PHP,不带Laravel、ThinkPHP这些框架,连PDO都少见,到处是mysql_query。原因很现实:这代码的原始场景是教学和课设,教材十几年没换章节,老师教的就是include文件加SQL拼接;另一部分是外包交付,图的是部署简单——一台Apache、一个MySQL、把目录丢进htdocs就能跑。对读者来说,这反而是好事:你能看懂的代码才是能改的代码,一个框架包进来,光路由和依赖就能劝退一半新手。
那为什么是MySQL不是SQLite?当年这套系统的数据量假设是几千本书、几百个读者,MySQL的安装门槛低,phpMyAdmin导入SQL文件点两下就行。SQLite虽然更轻,但PHP老代码里的mysql扩展和它完全不兼容,改造成本大于收益。所以不要一上来就想着换数据库,老系统最忌讳大改底层。
另外你会发现,几乎每个版本的结构都是“前台一个index.php,后台一个admin文件夹”,没有MVC、没有路由、没有模板引擎。这不是懒惰,而是这套系统的业务复杂度撑不起更重的架构。图书管理就三类操作:增删改查图书、增删改查读者、借书还书。用最直白的include组织页面,反而让每一个功能的追踪路径都极短:登录页连登录校验,列表页连数据库查询,提交页连SQL语句,三跳以内就摸到底了。
2.2 读懂目录结构与请求流向
我经手过的这类源码,目录结构大同小异,通常长这样:
book-system/ ├── admin/ 管理员后台 │ ├── book_add.php │ ├── book_list.php │ ├── book_edit.php │ ├── reader_add.php │ ├── borrow_add.php │ └── ... ├── index.php 前台入口,通常是查书页 ├── login.php 登录页 ├── conn.php 数据库连接文件 ├── css/ 样式 └── book_db.sql 数据库脚本第一次拿到代码别急着改,先把请求流向捋一遍。以最常见的流程为例:用户在login.php提交表单,表单action指向同一个文件或admin_check.php,脚本里先session_start(),再去admin表比对用户名和密码,成功就把用户名写进$_SESSION,然后header跳转到admin/index.php。后台每个页面顶部通常include一个check_login.php(有的叫auth.php、check.php),里面就一行判断逻辑:如果$_SESSION['admin']没设,就die('请先登录')或跳回登录页。
这套东西谈不上什么架构,但能跑。你拿到源码第一件事,就是把登录页、连接文件、检查登录三个文件找出来,确认它们之间的include关系。很多版本里check_login.php不是被每个后台页面都include的,有些页面漏了这行,就成了一个未授权入口。改代码前先把这个缺陷记下来,后面的验证清单里要用到。
2.3 数据库表设计与字段对应关系
数据库脚本叫book_db.sql还是books.sql不重要,表几乎永远是四张:admin(管理员)、book(图书)、reader(读者)、borrow(借阅记录)。常见字段如下:
| 表 | 关键字段 | 说明 |
|---|---|---|
| admin | id, username, password | password通常存MD5 |
| book | id, name, author, price, number, sort | number是库存,sort是分类 |
| reader | id, name, sex, barcode, grade | barcode是借书证号 |
| borrow | id, reader_id, book_id, borrow_time, back_time, if_back | if_back为1表示已还 |
这里最容易被新手忽略的是borrow表和book表之间的库存联动:借书时要插入一条borrow记录、同时把book.number减1;还书时反过来。很多老版本直接用两条独立的UPDATE和INSERT搞定,中间没有事务保护,如果第二步失败,库存就对不上了。我在第四章会单独拉出来说,这里先记住:借还逻辑和数据一致性强相关。
还有一个细节:这套系统的表之间几乎没有外键约束。reader_id、book_id都是逻辑关联,删掉一个读者,他的borrow记录还在,甚至可能指向一个不存在的读者ID。这也是老代码的普遍毛病:物理外键会影响灵活性,但代价是脏数据全靠业务代码控制,改动时稍不注意就会出现孤儿记录。你导入SQL后,可以用一条LEFT JOIN把这种孤儿记录扫出来:
SELECT b.id, b.reader_id, b.book_id FROM borrow b LEFT JOIN reader r ON b.reader_id = r.id LEFT JOIN book bk ON b.book_id = bk.id WHERE r.id IS NULL OR bk.id IS NULL;注意:拿到SQL文件后先看建表语句里CHARSET是不是utf8,很多老脚本默认latin1,这是后面中文乱码的根源之一。
3. 本地部署:把源码跑通的最小操作步骤
3.1 环境选型:phpStudy还是XAMPP
这套老代码只认Apache加PHP加MySQL。Nginx加PHP-FPM也能跑,但老代码里大量使用.htaccess和相对路径URL,Nginx默认不处理.htaccess,会出现路径重写失效或目录访问403的问题,新手排查起来很头大。所以本地开发我一般直接用集成环境:Windows上装phpStudy或XAMPP,macOS上装MAMP也行,三者都能一键启动Apache和MySQL。
phpStudy的优势是自带PHP版本切换,遇到PHP5老代码可以直接切回PHP 5.6,这是你最重要的后悔药。XAMPP的优势是文档多、报错信息全,Windows下遇到问题一搜一大把解决方案。我的建议是:电脑上装phpStudy,把PHP 5.6和PHP 7.4都装上,测试老代码时切回5.6,改造完再切7.4验证一遍。
这里有个很实际的排查点:Apache默认监听80端口,如果本机装了IIS、Docker、或某些国产软件占用了80,Apache会启动失败。phpStudy里可以直接把端口改成8080,但改完访问地址要跟着变。另一个点是PHP版本切换后必须重启Apache,不然phpinfo里看到的还是旧版本。
3.2 放置源码与导入数据库
集成环境装好后,把代码目录整个拷进Web根目录。phpStudy默认是WWW,XAMPP是htdocs,目录名建议起成book,这样访问路径是http://localhost/book/,避免中文目录在旧版Apache下的编码问题。如果源码包是zip,解压时注意有没有双层目录——很多包解开后是book-system/book-system/xxx,这种要去掉一层再放进Web根目录。
接下来导入数据库,两种方式都行。命令行方式适合熟悉MySQL的人:
mysql -u root -p < book_db.sql按回车后会提示输入MySQL的root密码,本地集成环境默认密码一般是空或者root。导入成功后可以用下面的命令验证表是否存在:
SHOW DATABASES; USE book_db; SHOW TABLES;如果不想碰命令行,phpMyAdmin更适合新手:浏览器打开http://localhost/phpmyadmin,点“导入”,选择book_db.sql文件,字符集选utf8,执行即可。导入失败最常见的原因是SQL文件里带了CREATE DATABASE语句,而你当前的MySQL账号没有建库权限。解决方法是把这一行手动删掉,改成在phpMyAdmin里先手动建一个空的book_db库再导入。
还有一个隐蔽问题:SQL文件可能从某个Windows老环境里拷出来,编码是GBK。用文本编辑器打开乱码的话,说明文件本身不是UTF-8,导入前先用编辑器把编码转成UTF-8无BOM,否则导入后中文会直接变问号。
3.3 修改数据库连接配置
导入成功后,找数据库连接文件。这个文件在绝大部分版本里叫conn.php,也有叫config.php、db.php的,位置在项目根目录或include目录。打开后会看到类似这样的内容:
<?php $conn = mysqli_connect("localhost", "root", "", "book_db"); if (!$conn) { die("数据库连接失败: " . mysqli_connect_error()); } mysqli_set_charset($conn, "utf8"); ?>这是已经改造过的写法。如果是老源码,大概长这样:
<?php $conn = mysql_connect("localhost", "root", "", "book_db"); mysql_select_db("book_db", $conn); mysql_query("set names utf8"); ?>两种写法对应本地的PHP版本:老写法只在PHP 5.6及以下能用,新写法在PHP 5.4以上都能用。你需要改的就是第二、三、四个参数:数据库地址一般是localhost或127.0.0.1,用户名是root,密码按你集成环境实际填的来,库名要和导入时保持一致。
怎么快速确定整个项目里还有哪些地方在连接数据库?全局搜索mysql_connect或mysqli_connect,把出现的地方全部替换。有的项目不止一个连接文件,比如admin目录里还藏了一个admin_conn.php,漏改的话后台能开但一提交就报连接失败,这种症状最难定位。
3.4 登录后台并验证基础功能
连接配置好后,浏览器访问http://localhost/book/login.php,用源码自带的默认账号登录。默认账号一般是admin,密码123456或admin,也有直接写在SQL文件里的。如果登录时提示密码错误,用phpMyAdmin打开admin表,看password字段的值:如果是一段32位MD5,说明密码是加密存储的,不要试图在表里直接填明文。直接用这条SQL改:
UPDATE admin SET password = MD5('123456') WHERE username = 'admin';登录成功后,把整个后台过一遍:图书添加、图书列表、读者管理、借书还书。记录两个预期结果:添加一本新书后,图书列表里能看到且库存等于填写的册数;借出一本书后,图书库存减1,借阅记录里多一行。如果这两条符合预期,说明代码和库表是配套的,可以进入下一步改造。如果不符合,先检查SQL文件有没有被完整导入——很多源码的SQL文件有几十条INSERT语句,部分MySQL环境在导入中途会因为某条语句报错而停住,但phpMyAdmin只显示一个笼统的错误提示,你需要去翻book表实际行数对不对。
提示:如果登录后页面样式全乱,先看CSS路径是不是绝对路径。老代码里常见< link rel="stylesheet" href="/css/style.css">这种写法,项目放在子目录时浏览器会去域名根目录找css,改成相对路径css/style.css就好。
4. 改代码:四个核心模块的落地逻辑
4.1 登录会话:防止被绕过的那几行
先看登录逻辑。大多数版本的admin_login.php长这样:
session_start(); if (isset($_POST['submit'])) { $username = trim($_POST['username']); $password = md5(trim($_POST['password'])); $sql = "SELECT * FROM admin WHERE username = '{$username}' AND password = '{$password}'"; $result = mysqli_query($conn, $sql); if (mysqli_num_rows($result) == 1) { $_SESSION['admin'] = $username; header('Location: admin/index.php'); } else { $error = '用户名或密码错误'; } }逻辑拆开就三步:从POST里取用户名和密码,密码用MD5加密后和库里比对,比对成功就写session并跳转。注意$password = md5(...)这一行,说明数据库里存的也是MD5值。如果你改了表里的密码但用的是明文,这里永远登录失败。trim()去掉首尾空格是必要的,不然用户在输入框多打个空格就查不到记录。$_SESSION['admin']存的是用户名而不是布尔值true,这一步很关键:后续每个页面要显示“当前操作人”时直接读这个变量就够。
这个写法最大的问题是SQL注入:username传一个' or '1'='1 --就能直接登录,因为拼接后的SQL变成SELECT * FROM admin WHERE username='' or '1'='1 --' AND password='...',后面的条件被注释掉了。修复办法最基础的是用mysqli_real_escape_string转义:
$username = mysqli_real_escape_string($conn, trim($_POST['username'])); $password = md5(trim($_POST['password']));但转义只是保底,真正干净的方案是改成预处理语句,我在第六章给出完整改法。现在先记住:登录写session时要写一个用户可区分的标识,至少是用户名,不要只写$_SESSION['login'] = true,否则所有用户共用一把钥匙,后台也没法区分谁在操作。
4.2 图书列表与分页:LIMIT两个参数别写反
图书列表页是最常被改的页面,因为课设验收时老师十有八九会要求“分页显示”。常见实现如下:
$page = isset($_GET['page']) ? intval($_GET['page']) : 1; $pageSize = 8; $offset = ($page - 1) * $pageSize; $sql = "SELECT * FROM book ORDER BY id ASC LIMIT {$offset}, {$pageSize}"; $result = mysqli_query($conn, $sql); $totalSql = "SELECT COUNT(*) AS total FROM book"; $totalResult = mysqli_query($conn, $totalSql); $total = mysqli_fetch_assoc($totalResult)['total']; $totalPage = ceil($total / $pageSize);LIMIT的两个参数,第一个是偏移量(跳过多少条),第二个是每页条数。页码从1开始,所以当前页$page对应偏移量是($page-1)*$pageSize。intval把URL参数强制转成整数,防止page传一个负数或字符串把SQL搞崩。总页数用ceil向上取整,保证余数为0时不会多出一页空页。
这个分页在几千条记录时没问题,但如果你改成“搜索图书”功能,要记住:加WHERE条件后,offset计算不变,COUNT语句必须加上同样的WHERE条件,否则搜索后的总页数还是全表的,翻页会翻到一堆不相关的内容。另一个容易翻车的地方是排序和分页混用时,ORDER BY字段不固定,页码会跳动。常见做法是给排序字段也设白名单,只允许传id、name、price这几个固定值,其余一律回退到默认排序。
4.3 借书还书:库存扣减必须保证原子性
借书逻辑是这套系统里唯一需要点技术含量的地方。好的实现应该用事务包裹两步操作:
mysqli_begin_transaction($conn); try { $borrowTime = date('Y-m-d'); $backTime = date('Y-m-d', strtotime('+30 day')); $sql1 = "INSERT INTO borrow (reader_id, book_id, borrow_time, back_time, if_back) VALUES ({$readerId}, {$bookId}, '{$borrowTime}', '{$backTime}', 0)"; mysqli_query($conn, $sql1); $sql2 = "UPDATE book SET number = number - 1 WHERE id = {$bookId} AND number > 0"; $affected = mysqli_affected_rows($conn); if ($affected !== 1) { throw new Exception('库存不足或图书不存在'); } mysqli_commit($conn); } catch (Exception $e) { mysqli_rollback($conn); echo $e->getMessage(); }逻辑说明:先插入借阅记录,再把库存减1。关键在于UPDATE语句带了AND number > 0条件,并用mysqli_affected_rows检查实际影响的行数:只有真正扣成功才算数。如果库存已经是0,UPDATE不会报错但影响行数为0,这时抛出异常并回滚,借阅记录也不会插入。这就是事务的原子性:要么两步都成功,要么都不成功。老代码里很多是两条独立语句,中间不设防,借书时赶巧第二步失败,库存就永久错位了。
还书逻辑是对称的:UPDATE borrow SET if_back = 1 WHERE id = ...,再UPDATE book SET number = number + 1。还书时不需要检查库存上限,但要注意防超还:严谨一点应该在还书前先查borrow表里if_back是不是0,如果已经是1说明这条记录重复还了,直接提示操作无效就行。日期格式用date()生成,万一服务器时区不对,都会差上几个小时,这种问题不容易被肉眼发现。后台可以加一个“所有借阅截止日期”列表,专门用来排查这类数据质量问题。
4.4 搜索功能的SQL拼接边界
几乎每个课设都会要求加搜索。常见写法是把关键词拼进WHERE:
$keyword = $_GET['keyword']; $sql = "SELECT * FROM book WHERE name LIKE '%{$keyword}%' OR author LIKE '%{$keyword}%'";这个写法搜索结果没问题,但$keyword直接拼进LIKE里,用户输入一个%就把全表打出来了,输入一个_会把所有名字里带任意字符的书都匹配出来。更稳的写法是先用mysqli_real_escape_string转义:
$keyword = mysqli_real_escape_string($conn, $_GET['keyword']); $sql = "SELECT * FROM book WHERE name LIKE '%{$keyword}%' OR author LIKE '%{$keyword}%'";注意mysqli_real_escape_string第一个参数是连接对象,第二个才是字符串,和mysql_real_escape_string的参数顺序不一样,老代码改造时最容易栽在这个位置。搜索条件加好后,还要把同样的WHERE复制到COUNT语句里,否则分页页码依然对不上。
搜索页还有一个体验细节:输入空字符串时等价于全表查询,会把分页逻辑搞得很难看。一般做法是$keyword为空时直接不拼WHERE条件,查询走完整列表分支。做完这些,去浏览器里分别搜“PHP”“红楼梦”、输入空字符串、输入百分号%,能扛住这四种输入,才算过关。
5. 必踩的坑:老代码本地运行的5个常见翻车现场
5.1 报错Call to undefined function mysql_connect()
现象:页面白屏,错误日志里出现Call to undefined function mysql_connect()。 原因:源码用的是PHP 5时代的mysql扩展,PHP 5.5开始废弃、PHP 7.0彻底移除。PHP 7以上环境里这个函数不存在。 解决:想要最省事,本地把PHP版本切换回5.6(phpStudy里可以一键切换)。想要长期用,把全项目的mysql_connect、mysql_query、mysql_select_db、mysql_fetch_array改成mysqli_版本并调整参数顺序,连接语句参照3.3节的改法。这里特别提醒:mysql_select_db的mysqli版本是mysqli_select_db($conn, $dbname),参数顺序和原来相反,很多人在这一步反复报错。全局替换时一定要把每个函数单独核对一遍,不要只做前缀替换就收工。这是老代码翻新项目里最常见的血泪经验。
5.2 登录成功跳回登录页
现象:用户名密码都对了,也跳转了,但后台每打开一个页面又被弹回登录页。 原因:后台页面检查$_SESSION['admin']不存在就跳走。产生原因通常是session.save_path目录没有写入权限,或者代码里session_start()位置不对(比如在输出HTML之后才调用),也有的是登录成功后跳转地址写错。 解决:先确认检查登录的文件里有没有session_start(),没有就加上。再看php.ini里session.save_path配置的目录是否存在且可写。XAMPP在Windows下常见这个问题,把目录改成当前项目的tmp目录,或给原目录加写入权限即可。最后检查login.php里登录成功后header('Location: admin/index.php')这个地址,如果项目嵌套在子目录里,改成绝对路径如/book/admin/index.php,避免在子目录页面相对跳转时跑偏。
5.3 页面中文全是问号或乱码
现象:图书名、读者名显示成????,或干脆是乱码方块。 原因:SQL文件里的表是latin1编码,页面声明了utf8,两边不一致;也有的版本是页面没声明charset,浏览器默认按GBK解析。 解决:先看PHP文件里有没有header('Content-Type: text/html; charset=utf-8'),没有就在每个页面顶部加。再在连接数据库后执行mysqli_set_charset($conn, 'utf8')(对应老代码的mysql_query("set names utf8"))。如果库里已经存了乱码数据,用这条SQL把表转成utf8:
ALTER TABLE book CONVERT TO CHARACTER SET utf8 COLLATE utf8_general_ci;对已存在的乱码数据,CONVERT不一定能救回来,因为数据在写入时就已经丢了一部分字节。所以建库导入那一步就要选对字符集,这比事后补救省事得多。还有一种隐藏情况:php文件本身是GBK编码存储的,页面上所有中文字符串都是乱码,这种不是数据库问题,要用编辑器把文件编码另存为UTF-8无BOM,再刷新页面。
5.4 MySQL 8的认证插件连不上
现象:conn.php里账号密码都对,但报错“Authentication plugin 'caching_sha2_password' cannot be loaded”。 原因:MySQL 8默认用户认证插件是caching_sha2_password,而老版本PHP的mysqli客户端不支持这个新插件。 解决:两种。一是改MySQL侧,把root用户的认证插件改回mysql_native_password:
ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY '你的密码'; FLUSH PRIVILEGES;二是换PHP版本,PHP 7.2以上对MySQL 8的支持要完善得多。本地开发我一般建议双管齐下:PHP端用新版本,MySQL侧改插件保底。如果在服务器上改不了root,可以新建一个专用账号再改成native password,避免影响生产环境其他应用。改完插件后记得重启一次MySQL服务,某些版本的认证缓存不会立即刷新。
5.5 后台能登录,但所有提交都报500
现象:点添加图书、添加读者,报500错误或页面直接空白。 原因:多半是PHP短标签。老代码里大量使用 后再跟一个换行,这个换行会被当成输出内容,在header()跳转之前产生“headers already sent”报错,表现为登录后跳转失败。处理办法是把文件末尾的?>删掉,PHP本身允许不带关闭标签结尾,这能让输出缓冲更干净。
6. 收尾:验证系统有没有真的做对,以及三条进阶思路
如果上面都跑通了,先按下面的清单把系统过一遍,确认它处在可用状态。
| 验证项 | 操作路径 | 预期结果 |
|---|---|---|
| 登录 | 访问/login.php,输入admin和正确密码 | 跳转到后台首页,且刷新后不失效 |
| 防绕过 | 不登录直接访问/admin/book_list.php | 被弹回登录页或提示请先登录 |
| 添加图书 | 后台-图书管理-添加 | 列表出现新书,库存等于填写册数 |
| 借书 | 读者管理-选择读者-选择图书-提交 | 库存减1,借阅记录新增一行 |
| 还书 | 借阅管理-还书 | 库存加1,记录if_back变为1 |
| 搜索 | 前台输入“PHP”和“%” | 正常结果,不白屏不报错 |
每一项都符合预期,这套系统才算真正交付。接下来想往下再走,我建议按顺序做三件事。
第一,把所有SQL拼接改成预处理语句。不必把所有页面一次改完,先改登录和借书还书这两个入口,这是攻击者最容易盯上的地方。
$stmt = mysqli_prepare($conn, "SELECT * FROM admin WHERE username = ? AND password = ?"); mysqli_stmt_bind_param($stmt, "ss", $username, $password); mysqli_stmt_execute($stmt); $result = mysqli_stmt_get_result($stmt);第二,加一个逾期未还的提示。在后台首页加一段查询,把借阅时间超过30天且if_back为0的记录列出来,对图书管理员很有用。第三,把admin表的MD5密码升级成password_hash。顺手做的话,登录逻辑也要同步改成password_verify验证,这个改动虽然小,却是整套系统里安全含金量最高的一步。
老系统的好处是能被你看透。我自己的习惯是:拿到手先跑通,再改连接层,再补安全层,最后动业务逻辑,三层走完才敢说这套系统接住了。这套流程不光适用于图书管理系统,任何一份年代久远的PHP源码都适用,希望帮到你。
本文还有配套的精品资源,点击获取