简介:基于ThinkPHP5.1与Bootstrap4.x开发的后台管理系统源码,定位为可直接运行学习的PHP项目,适合PHP初中级开发者、中小企业快速搭建后台管理平台。系统采用MVC设计模式,集成路由、自动验证、模型事件等框架能力,前端使用Bootstrap响应式布局,内置会员管理、文章管理、产品管理、微信接入、第三方登录等实用模块,可帮助读者理解前后端协作与完整后台交互流程。资源压缩包共1160个文件,以PHP业务逻辑、JS脚本、TPL模板、CSS样式、SQL数据库脚本等为主,另含配置与部署相关文件,整体大小8.41MB,目录结构清晰便于按功能检索。截至目前已有224人学习下载,适合用作毕业设计、项目练手或企业内后台系统二次开发的基础参考。
1. 为什么这套ThinkPHP5.1和Bootstrap4.x组合的后台管理系统源码还值得拆
一套ThinkPHP5.1和Bootstrap4.x开发的后台管理系统源码,放在今天依然有拆开看的价值。ThinkPHP5.1是TP5生命周期里改动较大的一代,它把容器、依赖注入和中间件从可选配置变成了框架核心;Bootstrap4.x又是从Less切到Sass、从高度依赖jQuery插件的时代走向组件化的一代。这两者组合在一起,正好卡在传统服务端渲染和现代前端框架之间的过渡带上。对需要维护存量项目的工程师来说,这套源码里能看到的控制器鉴权、RBAC节点表、菜单渲染逻辑,比直接去看Vue3后台管理系统更贴近日常生产环境。对刚接手PHP项目的同学来说,它把“登录-权限-菜单-列表-表单”这条主线完整走了一遍,适合当作第一份能跑通后台管理系统源码来深入读。关键不是里面的页面有多华丽,而是那套“服务端鉴权加模板渲染”的协作方式,在大量企业内部系统里仍然活跃着。
2. ThinkPHP5.1的初始化流程和RBAC权限表设计
在打开源码包之前,先想清楚一个问题:为什么很多后台管理系统源码选中ThinkPHP5.1而不是Laravel或者更老的TP3.2?常见理由是5.1版本放弃了单字母函数入口,把应用初始化收束到think\App,同时引入中间件和容器,这让权限校验可以做成统一拦截层,而不用在每个控制器构造函数里重复判断Session。这正是后台管理系统源码最值得先看的部分。
2.1 从composer依赖看这套源码的骨架
我一般会先翻composer.json,它决定了这套后台管理系统源码的运行边界。一个典型的TP5.1后台会这样声明依赖:
{ "require": { "php": ">=7.1.0", "topthink/framework": "5.1.*", "topthink/think-captcha": "2.*", "topthink/think-migration": "2.*" } }topthink/framework锁在5.1系列,说明应用使用了新的容器机制;think-migration通常被用来做数据库结构版本管理,而不是直接在数据库里手工建表。安装命令很简单:
composer install --no-dev --optimize-autoloader--optimize-autoloader会在生产环境生成更快的类映射表,这是TP5.1后台管理系统源码部署时常被漏掉的一项。如果composer.lock里PHP版本要求高于服务器版本,运行时会直接报类无法识别或语法错误,所以第一件要做的事就是确认PHP版本满足require段的声明。
2.2 五张表还是六张表:RBAC的最小表结构
后台管理系统源码里的权限部分,绝大多数沿用ThinkPHP早期案例里流传下来的五表RBAC:管理员表、角色表、节点表、角色-节点关联表、管理员-角色关联表。大多数实现里还会加一张管理员日志表,于是装完一数正好六张。我的建议是先用五张表打通流程,日志表后续再补。下面是一组可以直接执行的MySQL建表语句:
CREATE TABLE `admin_user` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT, `username` varchar(32) NOT NULL, `password` varchar(100) NOT NULL, `status` tinyint(1) NOT NULL DEFAULT '1', `last_login_ip` varchar(45) DEFAULT NULL, `create_time` int(10) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `admin_role` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT, `name` varchar(32) NOT NULL, `remark` varchar(255) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `admin_node` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT, `parent_id` int(10) unsigned NOT NULL DEFAULT '0', `name` varchar(64) NOT NULL COMMENT '节点标识,如 Admin/User/index', `title` varchar(32) NOT NULL COMMENT '菜单或按钮名称', `type` tinyint(1) NOT NULL DEFAULT '1' COMMENT '1菜单 2按钮', `sort` int(10) NOT NULL DEFAULT '0', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `admin_role_user` ( `role_id` int(10) unsigned NOT NULL, `user_id` int(10) unsigned NOT NULL, PRIMARY KEY (`role_id`,`user_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `admin_role_node` ( `role_id` int(10) unsigned NOT NULL, `node_id` int(10) unsigned NOT NULL, PRIMARY KEY (`role_id`,`node_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;注意admin_node里的type字段,这很关键。后台管理系统源码里的菜单和按钮权限通常共用这一张表:type=1的节点渲染成左侧菜单,type=2的节点只做按钮控制。用parent_id表达层级,而不是用level字段从1到3写死,这样以后要加四级菜单也不用改表结构。
2.3 登录控制器里的Session与权限中间件
表结构建好后,登录控制器一般会写在application/admin/controller/Login.php里。一个干净的登录动作应该这样写:
<?php namespace app\admin\controller; use think\Controller; use think\facade\Session; use app\admin\model\AdminUser; class Login extends Controller { public function index() { return $this->fetch('login'); } public function doLogin() { $username = input('post.username', '', 'trim'); $password = input('post.password', '', 'trim'); $captcha = input('post.captcha', '', 'trim'); if (!captcha_check($captcha)) { $this->error('验证码错误'); } $user = AdminUser::where('username', $username)->find(); // 用password_hash而不是md5,这是5.1项目里最常见的升级点 if (!$user || !password_verify($password, $user->password)) { $this->error('账号或密码错误'); } if ($user->status != 1) { $this->error('账号已停用'); } Session::set('admin_user', $user->toArray()); Session::set('admin_uid', $user->id); $this->success('登录成功', 'Index/index'); } public function logout() { Session::delete('admin_user'); Session::delete('admin_uid'); $this->redirect('Login/index'); } }password_verify对应注册时生成的password_hash,如果源码包里还是md5(md5($password))那种写法,说明这是老项目改版过来的,建议第一步就替换成password_hash。captcha_check来自topthink/think-captcha,它的默认配置写在config/captcha.php里,生产环境通常要改参数:length设成4或5,useCurve改成false,否则被OCR破解的概率很高。
权限中间件是比控制器内判断更值得复用的部分。TP5.1注册中间件时要修改app/middleware.php:
<?php return [ app\admin\middleware\AuthCheck::class ];然后在AuthCheck里写核心逻辑:
<?php namespace app\admin\middleware; use think\facade\Session; use think\Response; class AuthCheck { public function handle($request, \Closure $next) { // 白名单:登录页、验证码等接口跳过 $whitelist = ['login/index', 'login/dologin', 'captcha/index']; $path = strtolower($request->controller() . '/' . $request->action()); if (in_array($path, $whitelist)) { return $next($request); } if (!Session::has('admin_uid')) { return Response::create(['code' => -1, 'msg' => '请先登录'], 'json', 401); } return $next($request); } }$request->controller()拿到的是驼峰控制器名,所以要统一转小写再和白名单比对。这里没有写节点权限校验,因为一般会把节点的判断放在更靠后的行为里,避免每个请求都查一次菜单权限表。后台管理系统源码里最容易被忽略的是中间件优先级,中间件放在app/middleware.php下会全局生效,如果开发环境调试接口时发现自己被拦了,先看白名单有没有覆盖到对应路径。
3. Bootstrap4.x的布局与动态菜单渲染
权限校验通过后,后台管理系统源码的另一半功夫在前端。Bootstrap4.x的后台通常不是指用栅格系统搭一个页面,而是指一套以navbar、sidebar、content为骨架的固定布局,配合jQuery和bootstrap.bundle.min.js控制侧栏折叠和模态框。对于这套源码,我关注三件事:布局文件拆得够不够干净、菜单是不是从数据库读出来、表格操作区和分页控件是否复用了Bootstrap类名。
3.1 先把后台外壳模板拆成四个部分
我一般会把Bootstrap4.x后台的公共模板放在application/admin/view/public/base.html里,它至少包含以下骨架:
<body class="hold-transition sidebar-mini"> <nav class="navbar navbar-expand navbar-dark bg-primary"> <a class="navbar-brand" href="{:url('Index/index')}">后台管理系统</a> <div class="collapse navbar-collapse justify-content-end"> <span class="navbar-text">当前用户:{$RealName|default=''}</span> <a class="btn btn-outline-light btn-sm ml-2" href="{:url('Login/logout')}">退出</a> </div> </nav> <div class="container-fluid"> <div class="row"> <aside class="col-md-2 bg-light sidebar"> {include file="public/menu" /} </aside> <main class="col-md-10"> {block name="content"}默认内容{/block} </main> </div> </div> <script src="__STATIC__/bootstrap/js/bootstrap.bundle.min.js"></script> </body>{block name="content"}是模板继承的关键占位符,子模板通过{extend name="public/base" /}扩展,再用同名block填充内容。__STATIC__替换规则在config/template.php里通过tpl_replace_string配好,常见做法是映射到/static,这样CSS和JS文件不会被URL规则干扰。注意Bootstrap4.x本身没有sidebar样式,要自己用col-md-2加自定义CSS控制宽度和滚动,不能指望官方组件直接给你一个侧边栏。
3.2 菜单查询避免N+1:一次取全再递归
左侧菜单最典型的坑是每次加载都执行几十条where parent_id=...查询。后台管理系统源码里如果出现这种写法,上线第一天数据库就可能报警。正确做法是一次性取出当前用户有权限的节点,再在内存里递归组装:
<?php namespace app\admin\service; use app\admin\model\AdminNode; use think\facade\Session; class MenuService { public static function getTree($userId) { // 从Session或缓存读取,避免每次查表 $nodes = AdminNode::alias('n') ->join('admin_role_node rn', 'n.id = rn.node_id') ->join('admin_role_user ru', 'rn.role_id = ru.role_id') ->where('ru.user_id', $userId) ->where('n.type', 1) ->order('n.sort', 'asc') ->column('id,parent_id,title,name', 'id'); return self::buildTree($nodes, 0); } private static function buildTree($items, $pid) { $tree = []; foreach ($items as $item) { if ($item['parent_id'] == $pid) { $item['children'] = self::buildTree($items, $item['id']); $tree[] = $item; } } return $tree; } }这里用了column('字段列表', 'id'),第一个参数指定要返回的字段,第二个参数指定结果集索引键。注意如果字段列表里写了带别名的形式如n.id,则之后的$item['parent_id']取不到值,因为在TP5.1的column实现中,别名会影响结果集键名。第二个参数'id'让数组主键为节点ID,递归时$item['children']会追加成新字段,不影响原数据。菜单渲染到模板时,用递归函数配合include输出<li>标签,根据children是否为空决定要不要加折叠展开状态。
3.3 表格操作列和模态框的必改参数
Bootstrap4.x后台里最常见的操作按钮是编辑和删除。删除时如果直接写href跳转,很容易被爬虫误触,也少了二次确认。常见做法是把按钮伪装成button,通过><button type="button" class="btn btn-sm btn-danger" >$('#delModal').on('show.bs.modal', function (event) { var button = event.relatedTarget; var url = button.getAttribute('data-url'); $('#delConfirmBtn').attr('href', url); });
Bootstrap4.x的模态框事件名是show.bs.modal,对应代码里如果用了>$list = AdminUser::order('id', 'desc') ->paginate([ 'list_rows' => 15, 'query' => request()->get(), ]); return $this->fetch('user/index', ['list' => $list]);
query配置项会把当前URL上的其他参数拼接到分页链接里,比如要加一个搜索条件keyword=abc,如果忘了query,翻到第二页搜索条件就丢了。分页模板在TP5.1里常用render()输出,默认样式是Bootstrap3的pagination类,要想和Bootstrap4.x对齐,需要在模板里覆盖分页主题。如果输出链接里出现?page=2和自带p=2并存,请在config/page.php中只保留一种分页变量,否则会出现分页漂移。
4.2 快速搜索字段:防注入的where条件怎么写
很多后台管理系统源码在列表页顶部放了一个文本框,提交后进入搜索逻辑。新手喜欢拼->where("name like '%$keyword%'"),这在TP5.1里不是不能跑,但碰到%或_时会把LIKE语义弄错。正确做法是用数组查询:
$keyword = trim(input('get.keyword')); $map = []; if ($keyword !== '') { $map[] = ['username', 'like', "%{$keyword}%"]; } $list = AdminUser::where($map) ->order('id', 'desc') ->paginate(15, false, ['query' => request()->get()]);where($map)里的二维数组写法支持数组内多个条件,默认用AND连接。注意数组条件里不要用字符串键名,比如['username' => ['like', '%abc%']]在某些版本下会被解析成等值查询,必须使用[字段, 操作符, 值]的标准结构。搜索框里的值要经过trim,不然用户在前端只按了一个空格,搜索条件会一直带上空字符串,导致索引失效。
4.3 文件上传路径:不要写到public目录之外
Bootstrap4.x后台一般都会配一个富文本上传接口,源码包里常见的错误是把上传目录写在runtime下面。runtime在TP5.1里默认不被Web服务器访问,所以每次上传完图片在页面上都不显示。正确做法是把上传根目录定义为public/uploads:
$file = request()->file('file'); $info = $file->validate(['size' => 2097152, 'ext' => 'jpg,png,gif']) ->move(public_path() . 'uploads'); if ($info) { return json(['code' => 0, 'url' => '/uploads/' . $info->getSaveName()]); }public_path()在TP5.1中返回项目根目录下的public路径,不要写成PUBLIC_PATH常量,后者在5.1里已经废弃。validate里的size单位是字节,2MB就是2097152。getSaveName()返回带日期子目录的相对路径,这个路径可以直接拼到URL里,前提是Web服务器的root指向public目录。如果项目用虚拟目录部署,还要额外把/uploads映射到物理路径,否则始终404。
下表列出了后台管理系统源码联调时最常遇到的几类问题:
| 问题现象 | 根因 | 检查位置 |
|---|---|---|
| 翻页丢失搜索关键字 | paginate未带query | 控制器分页代码 |
| 图片上传后无法访问 | 上传目录在runtime | move目标路径 |
| 删除按钮点击无反应 | 公共JS重复绑定事件 | base.html脚本位置 |
| 验证码永远不刷新 | captcha配置缓存 | config/captcha.php |
| 搜索带空格查不到 | 未trim输入值 | 接收参数处 |
5. 上线前把后台管理系统源码的隐患逐项关掉
一个后台管理系统源码能在本地跑通,和能在生产环境稳定运行是两回事。下面三条是我接手这类项目时必做的操作,每一步都可以立刻执行。
先关掉调试模式。TP5.1的.env文件里如果设置app_debug = false,再把config/app.php里的'show_error_msg' => true,就能避免错误页泄露完整路径和SQL语句。这里有个容易踩的坑:关闭调试后,如果runtime目录存在旧的编译缓存,页面可能还是显示调试页。所以要顺手清除:
php think clear rm -rf runtime/*php think clear会清除缓存和日志,但它不删除编译后的路由缓存。对于后台管理系统源码,如果路由启用了缓存,要再执行php think route:clear。部署流程里我一般把这三句写进发布脚本,每次上线前自动执行。
把数据库配置从config里挪到.env。TP5.1支持.env覆盖配置,源码包如果不带.env文件,数据库密码通常直接写在config/database.php里。我的做法是用.env.example占位,部署时复制成.env再填真实值,同时把.env加进.gitignore。
[APP] app_debug = false [DATABASE] hostname = 127.0.0.1 database = admin_db username = root password = 填你的密码注意[DATABASE]段名必须和database.php中的读取逻辑一致。TP5.1约定段名用DATABASE,如果写成[DB],连接时会报“数据库未配置”。
最后收回不该暴露的入口文件。如果项目根目录下同时存在admin.php和index.php,只做伪静态是不够的,应该让Web服务器的DocumentRoot只指向public目录:
server { root /var/www/html/public; location / { if (!-e $request_filename) { rewrite ^(.*)$ /index.php?s=$1 last; } } location ~ \.php$ { fastcgi_param APP_ENV production; } }最后一件要养成的习惯是:登录后台后点开浏览器开发工具的Network面板,看接口的Session Cookie是不是被标记了HttpOnly和Secure。如果是,说明源码包里已经处理过Cookie安全性;如果不是,在config/session.php的cookie_params里把httponly设为true。这一步做完,这份后台管理系统源码才算真正可以交到运维手上。
本文还有配套的精品资源,点击获取