ThinkPHP5.1+Bootstrap4后台管理系统:从RBAC权限到菜单渲染
简介基于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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;注意admin_node里的type字段这很关键。后台管理系统源码里的菜单和按钮权限通常共用这一张表type1的节点渲染成左侧菜单type2的节点只做按钮控制。用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或5useCurve改成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 classhold-transition sidebar-mini nav classnavbar navbar-expand navbar-dark bg-primary a classnavbar-brand href{:url(Index/index)}后台管理系统/a div classcollapse navbar-collapse justify-content-end span classnavbar-text当前用户{$RealName|default}/span a classbtn btn-outline-light btn-sm ml-2 href{:url(Login/logout)}退出/a /div /nav div classcontainer-fluid div classrow aside classcol-md-2 bg-light sidebar {include filepublic/menu /} /aside main classcol-md-10 {block namecontent}默认内容{/block} /main /div /div script src__STATIC__/bootstrap/js/bootstrap.bundle.min.js/script /body{block namecontent}是模板继承的关键占位符子模板通过{extend namepublic/base /}扩展再用同名block填充内容。__STATIC__替换规则在config/template.php里通过tpl_replace_string配好常见做法是映射到/static这样CSS和JS文件不会被URL规则干扰。注意Bootstrap4.x本身没有sidebar样式要自己用col-md-2加自定义CSS控制宽度和滚动不能指望官方组件直接给你一个侧边栏。3.2 菜单查询避免N1一次取全再递归左侧菜单最典型的坑是每次加载都执行几十条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 typebutton classbtn 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上的其他参数拼接到分页链接里比如要加一个搜索条件keywordabc如果忘了query翻到第二页搜索条件就丢了。分页模板在TP5.1里常用render()输出默认样式是Bootstrap3的pagination类要想和Bootstrap4.x对齐需要在模板里覆盖分页主题。如果输出链接里出现?page2和自带p2并存请在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控制器分页代码图片上传后无法访问上传目录在runtimemove目标路径删除按钮点击无反应公共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。这一步做完这份后台管理系统源码才算真正可以交到运维手上。本文还有配套的精品资源点击获取