简介:这是一套基于ThinkPHP5开发的轻量级图书管理系统Demo源码,面向PHP初学者与Web全栈入门者,帮助快速掌握MVC架构、前后端交互及常见业务功能实现。项目采用ThinkPHP5作为后端框架,EasyUI构建后台管理界面,Bootstrap搭建前台展示页,并大量运用jQuery异步加载提升用户体验;完整覆盖图书信息的增删改查,支持字符串(单行/多行/富文本)、数字、日期、图片与文件等六类字段类型,集成UEditor富文本编辑器、验证码登录、Excel导出及统一上传方法(uploadPhoto/uploadFile),代码结构清晰、复用性强。压缩包为RAR格式,大小9.57MB,包含全部可运行源码文件(无文件总数统计,但含核心控制器、模型、视图及静态资源)。目前已有553人学习下载,适合用于课程设计、毕业项目参考或ThinkPHP+EasyUI技术栈的实战演练。
1. 这不是“玩具系统”:ThinkPHP5图书管理系统demo源码的真实价值与适用边界
很多人看到“demo”二字就下意识划走,觉得不过是教学示例、功能残缺、代码堆砌。但这个基于ThinkPHP5的图书管理系统源码,恰恰卡在真实项目落地前最关键的临界点上——它用生产级架构承载了完整业务闭环:从管理员登录验证码校验、多字段类型CRUD(含UEditor富文本、图片/文件双上传通道)、多条件组合模糊查询、分页渲染,到Excel导出,全部跑在ThinkPHP5标准MVC分层结构里,且控制器统一继承含uploadPhoto()和uploadFile()方法的基类。这意味着你拿到手的不是概念验证,而是可直接解压、配库、改配置就能跑通的最小可行产品(MVP)骨架。适合PHP初学者理解TP5路由+模型+视图联动机制,也适合中级开发者快速复用其上传组件、EasyUI表格封装、Bootstrap响应式布局结构,甚至作为毕业设计或内部工具原型的起点。它不解决高并发或微服务拆分,但把单体Web应用中90%的通用模块都踩实了。
2. ThinkPHP5 MVC架构与EasyUI+Bootstrap双前端协同实现原理
2.1 ThinkPHP5核心分层结构解析:为什么基类封装uploadPhoto/uploadFile是关键设计
该demo严格遵循ThinkPHP5的MVC分层规范:application/目录下controller/处理请求逻辑,model/定义数据表映射,view/存放模板文件。但真正体现工程化思维的是application/common/controller/Base.php基类——所有业务控制器(如BookController.php)均继承于此。基类中uploadPhoto()和uploadFile()方法并非简单调用think\File,而是做了三层封装:
// application/common/controller/Base.php protected function uploadPhoto($file, $savePath = 'uploads/photo/') { $info = $file->validate(['size'=>15728640,'ext'=>'jpg,png,gif'])->move(ROOT_PATH . 'public/' . $savePath); if ($info) { return $savePath . $info->getSaveName(); // 返回相对路径供数据库存储 } else { $this->error('图片上传失败:' . $file->getError()); } }提示:
validate()参数中size单位为字节(15MB),ext限定扩展名,move()自动创建子目录并返回think\File对象;getSaveName()返回带时间戳哈希的文件名(如20230815/5f3a1b2c.jpg),避免重名覆盖。若需支持WebP格式,只需将ext参数改为'jpg,png,gif,webp'。
此设计规避了在每个控制器中重复写文件校验逻辑,同时将路径拼接、错误抛出等细节收口。对比常见误用——直接在控制器里用$_FILES原始数组操作,此处通过TP5的think\File对象实现了类型安全与异常统一捕获。数据库字段cover_img存储的是相对路径(如uploads/photo/20230815/5f3a1b2c.jpg),前端通过/public/入口访问,既保证安全性(用户无法直接执行PHP文件),又降低CDN接入成本。
2.2 EasyUI后台界面与Bootstrap前台的职责分离策略
后台管理界面采用EasyUI 1.5.x(非最新版),其核心优势在于成熟的DataGrid组件对分页、排序、行内编辑的原生支持。查看application/view/admin/book/index.html,关键代码如下:
<!-- EasyUI DataGrid定义 --> <table id="dg" class="easyui-datagrid" >// application/controller/Admin/BookController.php public function edit($id = 0) { $book = BookModel::get($id); $this->assign('book', $book); // 将UEditor配置注入模板 $this->assign('ueditor_config', [ 'serverUrl' => url('admin/ueditor/upload'), 'initialFrameWidth' => '100%', 'initialFrameHeight' => 400 ]); return $this->fetch(); }<!-- application/view/admin/book/edit.html --> <script type="text/javascript" src="/static/ueditor/ueditor.config.js"></script> <script type="text/javascript" src="/static/ueditor/ueditor.all.min.js"></script> <script type="text/javascript"> var ue = UE.getEditor('content', {$ueditor_config|json_encode}); </script>逻辑说明:
serverUrl指向TP5路由admin/ueditor/upload,该路由对应UeditorController::upload()方法,需解析UEditor发送的action参数(如uploadimage、uploadfile)并调用对应上传逻辑;initialFrameWidth设为100%确保响应式缩放;json_encode确保PHP数组正确转为JS对象。若UEditor上传失败,需检查public/static/ueditor/php/config.json中的imageAllowFiles是否包含.webp等新增格式。
多字段类型CRUD体现在数据库表books的设计:title(VARCHAR)、price(DECIMAL)、publish_date(DATE)、description(TEXT)、content(LONGTEXT用于UEditor)、cover_img(VARCHAR)、attachment(VARCHAR)。TP5模型BookModel.php中通过$type属性声明类型:
// application/model/BookModel.php protected $type = [ 'price' => 'float', 'publish_date' => 'date', ];这使得$book->price = '29.9';自动转为浮点数,$book->publish_date = '2023-01-01';转为日期对象,避免手动floatval()或strtotime()转换。对于富文本字段content,TP5默认不做过滤,需在BookModel::create()前调用htmlspecialchars_decode()防止XSS——demo中未显式处理,生产环境必须补上。
3. 本地部署全流程:从MySQL建库到验证码登录验证
3.1 环境准备与ThinkPHP5运行时配置修改
该demo要求PHP 5.6+(推荐7.2)、MySQL 5.6+、Apache/Nginx。首先确认PHP扩展已启用:
# Linux命令行检查必要扩展 php -m | grep -E "(pdo_mysql|mbstring|curl|gd|xml)" # 若缺失,Ubuntu下执行: sudo apt install php-mysql php-mbstring php-curl php-gd php-xml解压DemoLibraryBasedThinkPHP5.rar后,进入public/目录,修改index.php中TP5入口文件路径(若部署在子目录):
// public/index.php 第12行 define('APP_PATH', __DIR__ . '/../application/'); // 若网站根目录为 /library/,则改为: // define('APP_PATH', __DIR__ . '/../../application/');关键配置在application/config.php中:
// 数据库配置 'database' => [ 'type' => 'mysql', 'hostname' => '127.0.0.1', 'database' => 'library_db', // 需提前创建 'username' => 'root', 'password' => '', 'hostport' => '3306', 'charset' => 'utf8mb4', // 必须设为utf8mb4支持emoji 'prefix' => 'tp5_', // 表前缀,与SQL文件一致 ], // 调试模式(开发阶段开启,上线必须关闭) 'app_debug' => true, // URL伪静态规则(Apache需启用mod_rewrite) 'url_route_must' => false,参数说明:
charset设为utf8mb4而非utf8,因MySQL的utf8实际只支持3字节编码,无法存储微信昵称中的emoji;prefix值必须与library.sql中建表语句的前缀匹配(如CREATE TABLE tp5_books);app_debug开启后可查看SQL执行日志,但暴露敏感信息,上线前务必设为false。
3.2 MySQL建库与SQL导入:字段类型与索引优化实操
执行library.sql前,先创建数据库并指定字符集:
-- 创建数据库(推荐使用utf8mb4_unicode_ci排序规则) CREATE DATABASE library_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; -- 导入SQL文件(Linux命令行) mysql -u root -p library_db < /path/to/library.sqllibrary.sql中关键表结构分析:
CREATE TABLE `tp5_books` ( `id` int(11) NOT NULL AUTO_INCREMENT, `title` varchar(200) NOT NULL COMMENT '书名', `author` varchar(100) DEFAULT NULL COMMENT '作者', `price` decimal(10,2) DEFAULT '0.00' COMMENT '价格', `publish_date` date DEFAULT NULL COMMENT '出版日期', `description` text COMMENT '简介', `content` longtext COMMENT '详情(UEditor)', `cover_img` varchar(255) DEFAULT NULL COMMENT '封面图路径', `attachment` varchar(255) DEFAULT NULL COMMENT '附件路径', `status` tinyint(1) DEFAULT '1' COMMENT '状态:1启用0禁用', `create_time` int(11) DEFAULT NULL COMMENT '创建时间', PRIMARY KEY (`id`), KEY `idx_title_author` (`title`,`author`) USING BTREE, -- 多列联合索引加速模糊查询 KEY `idx_status` (`status`) -- 状态字段单独索引 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;优化说明:
idx_title_author联合索引覆盖了最常用的WHERE title LIKE '%xxx%' AND author LIKE '%yyy%'查询,比单列索引更高效;status字段因常用于WHERE status=1筛选,单独建索引避免全表扫描。若查询中publish_date范围筛选频繁,可增加KEY idx_date (publish_date)。
3.3 验证码登录与Excel导出功能验证步骤
管理员登录地址为http://your-domain.com/public/admin/login.html。验证码生成逻辑在application/controller/Admin/LoginController.php中:
public function verify() { $verify = new \think\captcha\Captcha([ 'length' => 4, // 验证码位数 'fontSize' => 20, 'useCurve' => false, // 关闭干扰曲线提升识别率 'codeSet' => '0123456789', // 仅数字,降低用户输入错误 ]); return $verify->entry(); }验证步骤:
- 访问
/public/admin/login.html,观察右上角验证码图片是否正常显示(若404检查public/static/captcha/目录权限);- 输入默认账号
admin密码123456,提交时F12查看Network面板,确认/admin/login/check返回{"code":1,"msg":"登录成功"};- 登录后进入图书列表页,点击右上角“导出Excel”,检查
application/controller/Admin/BookController.php中export()方法是否调用PHPExcel库(demo使用1.8.2版本),生成文件名为books_20230815.xlsx。
Excel导出核心代码:
// application/controller/Admin/BookController.php public function export() { $list = BookModel::where('status', 1)->select(); $objPHPExcel = new \PHPExcel(); $objPHPExcel->getActiveSheet()->setCellValue('A1', 'ID') ->setCellValue('B1', '书名') ->setCellValue('C1', '作者'); $row = 2; foreach ($list as $item) { $objPHPExcel->getActiveSheet()->setCellValue('A'.$row, $item['id']) ->setCellValue('B'.$row, $item['title']) ->setCellValue('C'.$row, $item['author']); $row++; } header('Content-Type: application/vnd.ms-excel'); header('Content-Disposition: attachment;filename="books_'.date('Ymd').'.xlsx"'); $objWriter = \PHPExcel_IOFactory::createWriter($objPHPExcel, 'Excel2007'); $objWriter->save('php://output'); }注意:
PHPExcel库需通过Composer安装或手动放入extend/目录;header()前不能有任何输出(包括空格),否则报错“Cannot modify header information”。
4. 多条件组合模糊查询与分页性能调优实战
4.1 模糊查询SQL生成机制与LIKE优化陷阱
图书列表页的多条件组合查询在BookController::list()中实现:
public function list() { $where = []; $title = input('title'); $author = input('author'); $min_price = input('min_price'); $max_price = input('max_price'); if (!empty($title)) { $where['title'] = ['like', "%{$title}%"]; } if (!empty($author)) { $where['author'] = ['like', "%{$author}%"]; } if (!empty($min_price) && !empty($max_price)) { $where['price'] = ['between', [$min_price, $max_price]]; } $list = BookModel::where($where)->order('id desc')->paginate(15, false, ['query' => request()->param()]); return json(['rows' => $list->items(), 'total' => $list->total()]); }逻辑说明:
input()函数自动过滤XSS,['like', "%{$title}%"]生成WHERE title LIKE '%xxx%';paginate(15)每页15条,['query' => request()->param()]保留URL参数(如?title=php&author=张三)实现分页链接携带条件。但LIKE '%xxx%'会导致索引失效——即使有idx_title_author索引,前导%仍触发全表扫描。
性能调优方案:
- 对高频搜索字段(如
title)添加全文索引(MySQL 5.6+):
查询时改用ALTER TABLE tp5_books ADD FULLTEXT(title, author);MATCH AGAINST:if (!empty($title)) { $where['MATCH(title, author)'] = ['AGAINST', $title]; } - 若必须用
LIKE,限制搜索长度:前端JS校验title.length >= 2,后端加strlen($title) >= 2判断,避免LIKE '%'全表扫描。
4.2 分页参数安全校验与大数据量下的游标分页改造
当前分页使用LIMIT offset, size,当offset过大(如第1000页)时性能骤降。TP5的paginate()默认生成SELECT * FROM tp5_books LIMIT 14985,15,MySQL需扫描14985行才返回结果。
游标分页改造步骤:
- 修改
BookController::list(),用last_id替代page参数:$last_id = input('last_id', 0, 'intval'); $list = BookModel::where('id', '>', $last_id) ->where($where) ->order('id asc') ->limit(15) ->select(); $next_last_id = empty($list) ? 0 : end($list)['id']; return json(['list' => $list, 'next_last_id' => $next_last_id]); - 前端EasyUI DataGrid需改写加载逻辑,将
page参数替换为last_id,并用next_last_id作为下一页的起始ID。
优势:
WHERE id > 14985可直接利用主键索引,查询速度恒定O(log n);缺点是无法跳转任意页,需配合“首页/上一页/下一页”按钮。若业务强制要求跳页,可缓存热门页码(如第1-10页)的id范围到Redis。
5. 图片/文件上传安全加固与UEditor XSS防护技巧
5.1 uploadPhoto/uploadFile方法的二次校验与存储路径隔离
基类中的uploadPhoto()虽有validate(),但需补充服务器端MIME类型校验,防止伪造文件头:
// application/common/controller/Base.php protected function uploadPhoto($file, $savePath = 'uploads/photo/') { // 1. 扩展名校验(客户端可伪造,必须服务端再校验) $ext = strtolower($file->getExtension()); $allowed_exts = ['jpg','jpeg','png','gif','webp']; if (!in_array($ext, $allowed_exts)) { $this->error("不支持的图片格式:{$ext}"); } // 2. MIME类型校验(读取文件头) $mime = $file->getMime(); $allowed_mimes = ['image/jpeg','image/png','image/gif','image/webp']; if (!in_array($mime, $allowed_mimes)) { $this->error("MIME类型不合法:{$mime}"); } // 3. 文件内容校验(检测是否为真实图片) $content = file_get_contents($file->getRealPath()); if (substr($content, 0, 2) !== "\xFF\xD8" && substr($content, 0, 3) !== "\x89\x50\x4E") { $this->error("文件内容非JPEG/PNG格式"); } $info = $file->validate(['size'=>15728640,'ext'=>$allowed_exts])->move(ROOT_PATH . 'public/' . $savePath); if ($info) { return $savePath . $info->getSaveName(); } else { $this->error('图片上传失败:' . $file->getError()); } }安全说明:
getExtension()获取扩展名,getMime()读取HTTP头,file_get_contents()读取文件头字节——JPEG以\xFF\xD8开头,PNG以\x89\x50\x4E开头。三重校验可拦截99%的恶意文件上传。savePath设为uploads/photo/而非uploads/,实现图片与文件存储路径物理隔离,降低提权风险。
5.2 UEditor富文本内容过滤与XSS攻击防御
UEditor提交的content字段可能包含恶意JS,TP5默认不过滤。在BookModel::create()前插入过滤逻辑:
// application/model/BookModel.php public function setContentAttr($value) { // 移除script、iframe、onerror等危险标签和属性 $value = preg_replace('/<script[^>]*?>.*?<\/script>/is', '', $value); $value = preg_replace('/<iframe[^>]*?>.*?<\/iframe>/is', '', $value); $value = preg_replace('/on\w+\s*=\s*["\'][^"\']*["\']/i', '', $value); // 允许style属性但过滤javascript:协议 $value = preg_replace('/<[^>]+style\s*=\s*["\'][^"\']*["\'][^>]*>/i', '', $value); return htmlspecialchars($value, ENT_QUOTES, 'UTF-8'); }防护逻辑:
setContentAttr()是TP5的自动写入器,当$book->content = $raw_html时自动触发;正则表达式清除<script>、<iframe>标签及onclick等事件属性;htmlspecialchars()将<转为<,彻底阻断XSS。若需保留部分HTML(如<b>、<p>),可改用HTMLPurifier库,但会增加部署复杂度。
最后一步,检查public/uploads/目录权限:Linux下执行chmod -R 755 public/uploads/,确保Web服务器用户(如www-data)有读写权限,但禁止执行权限(chmod -R -x public/uploads/),杜绝上传PHP木马被执行。
本文还有配套的精品资源,点击获取