☰
基于layui与tp6的RBAC权限后台开发实战
2026/10/17 5:11:25 网站建设 项目流程

简介:一套基于 layui2.5.6 与 ThinkPHP6.0.2 的权限管理后台模板,面向需要快速搭建管理系统的 PHP 开发者,也适合刚接触 RBAC 权限模型的学习者。项目将前端组件库与后端框架结合,内置用户角色分配、权限控制等常见后台能力,可直接作为企业级应用的起点。压缩包约 6.1MB,包含 1098 个文件,其中 PHP 文件承担业务逻辑与核心控制器,JS/CSS/HTML 构成页面交互与界面,GIF/PNG/SVG 等提供图标素材,SQL 文件可用于初始化数据,整体目录结构规范,便于二次开发。已有 2283 人浏览学习。通过学习这份资源,可掌握 ThinkPHP6 环境配置、Composer 依赖管理、RBAC 权限设计思路,以及 layui 组件在实际后台项目中的调用方式;同时项目附带 .env、composer.json、README 等工程化配置文件,有助于规范开发流程,缩短从零搭建权限系统的时间。

1. 这套 layui2.5.6 + tp6.0.2 权限后台,解决的是权限乱象

很多所谓“带权限”的后台,不过是登录时判断一下是不是管理员,再把页面上的按钮临时隐藏掉。角色一多,菜单乱成麻,删错了节点还得去改代码。用 layui2.5.6 搭配 tp6.0.2 搭权限管理后台,是中小型项目里最稳的组合之一:前端组件够用、后端中间件清晰,能把“用户、角色、菜单按钮”拆成一张网,让不同人登录看到不同菜单、点到不同按钮。这篇文章不聊概念,直接给你能落地的建表、后端接口、前端渲染和一套踩坑笔记。适合刚起步的小团队、独立开发者,也适合在旧系统里做脚手架的工程师。

2. 权限模型选型与建表:先让 user/role/node 三个表背锅

2.1 为什么不用 is_admin 开关,RBAC 才是中小后台的定心丸

我见过不少后台的权限设计:user 表加一个is_admin字段,然后在每个控制器里判断if ($user->is_admin == 1)。这种做法的麻烦在于,权限规则散落在业务代码里,想给某个运营开一个导出的按钮权限,只能去改代码重发布。角色一旦多起来,is_admin根本说不清“谁能导出”“谁能删用户”“谁能改配置”。

RBAC(基于角色的访问控制)把权限抽象成“节点”:节点挂在角色上,用户只关联角色。权限判断从“写死在业务里的 if 判断”变成“查一次用户角色的节点表”,逻辑统一且能在后台可视化配置。对于 layui2.5.6 + tp6.0.2 这类轻量级组合,RBAC 并不重,三张表加两张关联表就够了。

这里我要强调一个原则:权限模型的第一要素不是“功能多炫”,而是“能查、能配、能审计”。RBAC 的节点表天然就是这个承载物,后台权限管理页面就能直接拿到树形数据渲染成勾选界面,这和 layui 的 tree 组件配合起来非常顺手。

2.2 三表 + 两张关联表的建表 SQL,以及字段设计理由

先上建表语句,这是整套权限后台的地基。注意节点表把“目录、菜单、按钮”都放一起,用type区分,这样维护起来只有一处。

-- 用户表 CREATE TABLE `user` ( `id` int unsigned NOT NULL AUTO_INCREMENT, `username` varchar(50) NOT NULL DEFAULT '' COMMENT '登录名', `password` varchar(100) NOT NULL DEFAULT '' COMMENT '密码hash', `nickname` varchar(50) NOT NULL DEFAULT '' COMMENT '昵称', `status` tinyint NOT NULL DEFAULT '1' COMMENT '0禁用 1启用', `create_time` int NOT NULL DEFAULT '0', `update_time` int NOT NULL DEFAULT '0', PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表'; -- 角色表 CREATE TABLE `role` ( `id` int unsigned NOT NULL AUTO_INCREMENT, `name` varchar(50) NOT NULL DEFAULT '' COMMENT '角色名', `remark` varchar(255) NOT NULL DEFAULT '' COMMENT '备注', `status` tinyint NOT NULL DEFAULT '1', `is_super` tinyint NOT NULL DEFAULT '0' COMMENT '是否超级角色 1是', `sort` int NOT NULL DEFAULT '0', `create_time` int NOT NULL DEFAULT '0', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='角色表'; -- 权限节点表:目录/菜单/按钮都放这里 CREATE TABLE `node` ( `id` int unsigned NOT NULL AUTO_INCREMENT, `pid` int unsigned NOT NULL DEFAULT '0' COMMENT '父级id', `title` varchar(50) NOT NULL DEFAULT '' COMMENT '节点标题', `route` varchar(150) NOT NULL DEFAULT '' COMMENT '路由标识,如 admin/user/index', `type` tinyint NOT NULL DEFAULT '2' COMMENT '1目录 2菜单 3按钮', `icon` varchar(50) NOT NULL DEFAULT '' COMMENT 'layui图标', `status` tinyint NOT NULL DEFAULT '1', `sort` int NOT NULL DEFAULT '0', PRIMARY KEY (`id`), KEY `idx_pid` (`pid`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='权限节点表'; -- 用户角色关联表 CREATE TABLE `user_role` ( `user_id` int unsigned NOT NULL DEFAULT '0', `role_id` int unsigned NOT NULL DEFAULT '0', PRIMARY KEY (`user_id`,`role_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 角色节点关联表 CREATE TABLE `role_node` ( `role_id` int unsigned NOT NULL DEFAULT '0', `node_id` int unsigned NOT NULL DEFAULT '0', PRIMARY KEY (`role_id`,`node_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

字段设计的几个关键点:route统一存小写路由标识,比如admin/user/index,菜单渲染和权限校验都靠它;type用数字区分层级,1 是顶级目录、2 是页面菜单、3 是操作按钮;is_super用于标识超级角色,避免在代码里写死角色 ID,后面踩坑章节会展开。

写进项目后,你还需要在role_node上做一层索引以外的优化:如果角色很多,查询权限时 IN 条件的数量会膨胀,建议按角色 ID 做缓存。另一个常见误区是给 node 表加path字段存整个父子路径,其实有pid之后,用递归组装树就足够,冗余字段反而容易在编辑时写错。

2.3 从一次点菜单到一次请求的完整链路

权限后台的请求链路,比普通 CRUD 多两个环节:菜单加载和中间件校验。我把完整路径画成下面的表格,新手照着这个表去排查问题思路会清晰很多。

环节参与者关键行为
登录LoginController校验账号密码,写会话和角色
拉取菜单NodeController::menu按角色查节点树,返回 JSON
渲染菜单layui tree渲染侧边栏,点击后 iframe 跳转
加载页面iframe 请求访问admin/user/index
校验权限AuthMiddleware比对路由标识,通过则放行
执行操作具体控制器按钮级操作再单独校验一次

这个链路里最容易忽略的是“按钮级权限”和“页面级权限”的区分:页面菜单只需要校验一次 pathinfo,但新增、删除按钮的操作请求往往走同一个控制器方法,不能只依赖菜单隐藏。正确做法是中间件负责页面粗粒度校验,控制器内部再用一个统一的权限检测函数校验具体操作。

3. tp6.0.2 后端:登录、节点收集与权限中间件

3.1 登录接口怎么写:验证账号密码、查角色、种会话

tp6.0.2 里写登录接口很简单,核心是密码校验和会话写入。密码不要再存 MD5,使用 PHP 自带的password_hash和password_verify,生成成本可控且不存在加盐问题。

<?php namespace app\controller; use think\facade\Db; use think\facade\Session; use think\facade\View; class Login { public function index() { return View::fetch('login/login'); } public function check() { $username = input('post.username'); $password = input('post.password'); $user = Db::name('user')->where('username', $username)->find(); if (!$user || !password_verify($password, $user['password'])) { return json(['code' => 1, 'msg' => '账号或密码错误']); } if ((int)$user['status'] !== 1) { return json(['code' => 1, 'msg' => '账号已禁用']); } // 查出该用户的全部角色,写入会话 $roleIds = Db::name('user_role')->where('user_id', $user['id'])->column('role_id'); Session::set('admin_uid', $user['id']); Session::set('admin_roles', $roleIds); return json(['code' => 0, 'msg' => '登录成功', 'url' => '/admin/index/index']); } }

逻辑说明:先按用户名查出一行,再password_verify校验,这一步不要改成先查用户再拼接 SQL,避免用户枚举。角色查询用column直接取出role_id一维数组,后续中间件和菜单接口都直接复用这个会话数组。

参数说明:如果将来要接入多点登录,建议把Session::set换成think\facade\Cache::set存 token,再在中间件里读 token 对应用户。对 tp6.0.2 而言,Session 默认存储在runtime/session下,单体后台够用,分布式部署就得换存到 Redis。

3.2 自动收集权限节点:用控制器方法扫一遍,省得手工维护

node 表不能靠手工录入,尤其是项目有几十个控制器时,手工维护必然漏。常见做法是做一个 think 命令,扫描app/controller目录下的类,把公开方法自动注册成节点。以下是一个可跑的简化版:

<?php namespace app\command; use think\console\Command; use think\console\Input; use think\console\Output; use think\facade\Db; class ScanNode extends Command { protected function configure() { $this->setName('scan:node')->setDescription('扫描控制器方法,自动生成权限节点'); } protected function execute(Input $input, Output $output) { $controllerDir = app()->getAppPath() . 'controller'; foreach (glob($controllerDir . '/*Controller.php') as $file) { $ctrlName = basename($file, '.php'); // 如 UserController $className = '\\app\\controller\\' . $ctrlName; if (!class_exists($className)) { continue; } $ref = new \ReflectionClass($className); $controllerRoute = strtolower(str_replace('Controller', '', $ctrlName)); // 控制器名作为一个菜单节点 $menuId = $this->addNode(0, $ctrlName, $controllerRoute, 2); foreach ($ref->getMethods(\ReflectionMethod::IS_PUBLIC) as $method) { if ($method->getDeclaringClass()->getName() != $className) { continue; // 只扫描当前类的方法 } $action = $method->getName(); $this->addNode( $menuId, $action, $controllerRoute . '/' . strtolower($action), 3 ); } } $output->writeln('节点扫描完成'); } protected function addNode($pid, $title, $route, $type) { $exists = Db::name('node') ->where('pid', $pid) ->where('route', $route) ->where('type', $type) ->find(); if ($exists) { return $exists['id']; } $id = Db::name('node')->insertGetId([ 'pid' => $pid, 'title' => $title, 'route' => $route, 'type' => $type, 'sort' => 0, 'status' => 1 ]); return $id; } }

逻辑说明:使用 ReflectionClass 获取控制器的公开方法,每个方法作为一个按钮节点挂在对应控制器菜单下。addNode做了幂等判断,重复执行命令不会生成重复数据。实际项目里可以把目录层级嵌套扫描,这里为了控制篇幅只扫首层控制器,思路完全一致。

参数说明:controllerRoute一定要strtolower,这和后端路由大小写归一化有关。另外,建议把不需要生成节点的基类控制器放到app/controller/BaseController之外的位置,或者在这里加一个方法名黑名单,过滤index方法里的__construct和父类方法。

3.3 权限中间件:一个请求进来先看路由是否在白名单

tp6.0.2 的中间件非常适合做权限校验。公共操作放白名单,其余请求都要核对当前 pathinfo 是否在角色权限路由里。

<?php namespace app\middleware; use think\facade\Session; use think\facade\Db; use think\Response; class AuthCheck { public function handle($request, \Closure $next) { $uid = Session::get('admin_uid'); if (!$uid) { return redirect('/login/index')->send(); } $route = strtolower(trim($request->pathinfo(), '/')); $whitelist = ['admin/index/index', 'admin/index/welcome']; if (in_array($route, $whitelist)) { return $next($request); } $roleIds = Session::get('admin_roles', []); // 超级角色直接放行 if ($this->isSuperRole($roleIds)) { return $next($request); } $perms = $this->loadPermsCache($roleIds); if (!in_array($route, $perms)) { abort(403, '无权访问该页面'); } return $next($request); } protected function isSuperRole($roleIds) { $count = Db::name('role') ->whereIn('id', $roleIds) ->where('is_super', 1) ->count(); return $count > 0; } protected function loadPermsCache($roleIds) { $cacheKey = 'perms_' . md5(implode('_', $roleIds)); $perms = cache($cacheKey); if ($perms !== false) { return $perms; } $nodeIds = Db::name('role_node') ->whereIn('role_id', $roleIds) ->column('node_id'); $perms = Db::name('node') ->whereIn('id', $nodeIds) ->whereIn('type', [2, 3]) ->column('route'); $perms = array_map('strtolower', $perms); cache($cacheKey, $perms, 3600); return $perms; } }

逻辑说明:请求进来后先拿 session 用户 ID,没有就是未登录,直接重定向到登录页。接着取 pathinfo 作为路由标识,白名单里的公共页面跳过校验。超级角色用的是is_super字段,而不是写死id == 1,后面会说到为什么。普通角色从缓存读取路由集合,做一次in_array即可,性能开销极小。

参数说明:abort(403)在 tp6.0.2 会抛出一个带状态码的异常,建议在app/ExceptionHandle.php里捕获 403 并渲染一个统一无权限页面,否则默认输出会让用户体验很生硬。缓存时间 3600 秒,角色权限调整后记得清理对应缓存。

4. layui2.5.6 前端:动态菜单、表格渲染和按钮权限

4.1 拿到节点树后,用 layui tree 渲染侧边栏菜单

后端先提供一个菜单接口,根据当前用户角色返回树形节点。在 tp6 控制器里递归组装树是常规操作。

namespace app\controller; use think\facade\Db; use think\facade\Session; class Node extends Base { public function menu() { $roleIds = Session::get('admin_roles', []); if ($this->isSuperRole($roleIds)) { $nodes = Db::name('node') ->where('status', 1) ->order('sort asc, id asc') ->select()->toArray(); } else { $nodeIds = Db::name('role_node') ->whereIn('role_id', $roleIds) ->column('node_id'); $nodes = Db::name('node') ->whereIn('id', $nodeIds) ->where('status', 1) ->order('sort asc, id asc') ->select()->toArray(); } $tree = $this->buildTree($nodes, 0); return json(['code' => 0, 'data' => $tree]); } protected function buildTree($nodes, $pid) { $list = []; foreach ($nodes as $node) { if ((int)$node['pid'] === (int)$pid) { $children = $this->buildTree($nodes, $node['id']); if (!empty($children)) { $node['children'] = $children; } $list[] = $node; } } return $list; } }

前端拿到这棵树后,用 layui 的tree.render渲染到侧边栏。注意 layui2.5.6 的 tree 组件已经支持data传参,图标可以用icon字段。

layui.use(['tree', 'jquery'], function () { var tree = layui.tree; var $ = layui.$; $.get('/admin/node/menu', function (res) { if (res.code !== 0) return; tree.render({ elem: '#side-menu', id: 'sideMenu', data: res.data, onlyIconControl: true, click: function (obj) { var data = obj.data; if (data.type === 2 && data.route) { // 菜单节点:让 iframe 跳转 parent.$('#content-frame').attr('src', '/' + data.route); } } }); }); });

逻辑说明:后端已经过滤了按钮节点,这里只需要判断 type 是菜单还是目录。点击菜单时把 route 拼到 iframe 的 src 上。为了显示层级,建议给 tree.render 的父容器预留好缩进样式,layui 默认会按 children 自动缩进,不需要额外写折叠逻辑。

参数说明:onlyIconControl: true表示只通过图标控制展开折叠,在某些后台布局里能减少误触。如果你用的是自定义 nav 渲染,注意把顶级菜单和子菜单分层输出 HTML,不能直接用 tree.render 的样式去套 layui-nav。

4.2 table 列表和自定义模板:有权限才显示操作按钮

页面权限由中间件守住了,但“新增、编辑、删除”按钮还得根据按钮权限动态渲染。在 layui2.5.6 里,最直接的做法是在页面初始化时把当前用户的按钮权限数组挂到 window 上,然后在 table 的templet里判断。

// 后端渲染页面时输出当前用户可用的按钮权限 window.AUTH = ['admin/user/edit', 'admin/user/delete']; function btnAuth(perm) { return window.AUTH.indexOf(perm) > -1; } table.render({ elem: '#user-table', url: '/admin/user/list', page: true, cols: [[ { field: 'id', title: 'ID' }, { field: 'username', title: '用户名' }, { field: 'nickname', title: '昵称' }, { title: '操作', width: 200, templet: function (d) { var html = ''; if (btnAuth('admin/user/edit')) { html += '<a class="layui-btn layui-btn-xs" lay-event="edit">编辑</a>'; } if (btnAuth('admin/user/delete')) { html += '<a class="layui-btn layui-btn-danger layui-btn-xs" lay-event="del">删除</a>'; } return html; }} ]], done: function () { // 给没有权限的按钮容器补一个“无权限”提示,避免表格空白显得突兀 } }); table.on('tool(user-table)', function (obj) { if (obj.event === 'edit') { // 打开编辑弹层 } if (obj.event === 'del') { layer.confirm('确认删除吗?', function (index) { obj.del(); layer.close(index); }); } });

逻辑说明:templet是 layui table 的行模板函数,返回字符串会被插入到对应单元格。这里不直接拼接<a>而是先判断权限,没有权限就什么都不输出,操作列就不会出现不该有的按钮。lay-event配合table.on('tool(...)')实现事件委托,不要在模板里写 onclick 全局函数。

参数说明:窗口AUTH数组建议在控制器渲染视图时通过assign塞到模板里,或者放 Cookie/隐藏 input。数据可以通过一个专门的接口异步读取,但页面加载会多一次请求,除非前后端严格分离,否则不推荐。

4.3 与 tp6 返回格式对齐:code/msg/count/data 缺一不可

layui table 对接口返回有固定预期:默认code=0表示成功,msg是提示信息,count是总数,data是当前页数据。tp6.0.2 的json()助手不会自动帮你补全,所以列表接口要按以下格式返回:

public function list() { $page = input('page/d', 1); $limit = input('limit/d', 10); $paginate = Db::name('user') ->where('status', 1) ->paginate($limit, false, ['page' => $page]); return json([ 'code' => 0, 'msg' => 'ok', 'count' => $paginate->total(), 'data' => $paginate->items() ]); }

逻辑说明:tp6 的paginate方法返回的是一个分页对象,用total()取总条数,items()取当前页数据数组。这里不需要额外实现 layui 的 parseData,只要把字段名对齐,组件会自动读取。

参数说明:如果你的控制器继承了基类,而基类里把输出格式统一包了一层{status:1, result:{...}},记得在 layui 的 table.render 里传入parseData做转换,或者在基类放行列表接口走原样 JSON。建议在 tp6 的公共库里写一个JsonService::success(),统一生成这套结构,避免每个控制器手动拼数组。

5. 权限后台避坑实录:5 个让新人也敢翻修的坑

5.1 节点表“中文名”和“路由”脱节,菜单变成天书

现象:用扫描命令生成节点后,侧边栏菜单显示UserController/index、GoodsController/add这类英文,用户看不懂,后台维护人员也不知道哪个菜单对应哪个功能。

原因:扫描命令只拿到了控制器名和方法名,没有同步维护中文标题。当时图省事直接把类名当 title 写入 node 表,后来就再没回头改。

解决:我后来在扫描命令里增加了对方法注释@title的读取,没有注释时用类名兜底。同时把菜单的 title 维护权交给后台编辑页,扫描命令只负责“补齐缺失节点”和“更新 route 变化”,不再覆盖 title。这样菜单文案由运营人员在界面上维护,代码里只保证路由标识正确。

5.2 tp6 路由大小写不一致,iframe 老是 404

现象:iframe 的 src 写的是admin/User/index,结果页面打不开;或者中间件里存的 route 是Admin/User/index,而当前 pathinfo 是小写,导致权限判断永远不通过。

原因:tp6 路由对 URL 有归一化处理,很多情况下路径会转为小写。菜单在数据库里的 route 又是由扫描命令生成的,如果生成时没有统一小写,就会出现大小写不一致的玄学问题。

解决:全项目统一强制小写。扫描节点时strtolower,登录菜单接口返回 route 前也strtolower,中间件里对比前再strtolower。另外在使用 layui iframe 布局时,src 路径开头不要加/index.php,直接用入口文件后的路径,避免多一层别名的换算。

5.3 权限中间件里每次都查全表,请求慢到发慌

现象:后台打开每个页面都明显卡顿,数据库日志里看到 role_node 表被频繁查询,请求一多 MySQL 的连接数立刻被打满。

原因:中间件里没有做任何缓存,每请求都重新查角色节点集合。对于只有几十个节点的权限表来说,QPS 稍微上来就会拖垮整体。

解决:增加缓存层,见 3.3 的loadPermsCache。用角色 ID 组合做缓存 key,权限变更时主动清理对应角色的缓存。注意:tp6 自带的cache()默认是文件缓存,多台服务器部署时请换成 Redis,否则每台机器的缓存不一致,会出现“这台机器有权限、那台机器没权限”的坑。

5.4 layui table 的 code 字段与 TP6 默认 json 返回对不上

现象:table.render 配置好了之后,表格一直显示“数据加载失败”,F12 看到接口返回的是{status:1, msg:"..."}或者{code:1, data:...}。

原因:layui table 默认认code字段且值为 0。tp6 的json()如果传了code:1会被当成业务失败;有时候控制器在基类里统一返回了对象,字段名和 layui 完全不一致。

解决:统一使用['code'=>0, 'msg'=>'', 'count'=>N, 'data'=>[]]格式。建议封装一个公共的Api类,在控制器里调用success($data)和error($msg),所有接口都走这一套。别在 table.render 里写特别长的 parseData,那是给别人造的坑。

5.5 super_admin 写死在代码里,索引全乱

现象:某次数据库调整把 role 表的 id=1 删掉后,整个后台的权限校验崩了,所有用户都变成无权限访问状态。

原因:中间件和菜单接口都写死了in_array(1, $roleIds)判断超级管理员,依赖了自增主键的稳定性。一旦 id=1 被删或环境迁移,超级权限就消失了。

解决:role 表增加is_super字段,超级角色在数据初始化时就标记好。所有需要跳过权限校验的地方都查这个字段,而不是硬编码 ID。这样就算数据库清掉重建,只要重新标记 is_super,权限体系立刻恢复。这个坑我吃过一次,现在每次给权限后台写代码都会先问一句:角色 ID 能变吗?能变就别写死。

6. 把按钮权限精确到“能看见能点”:统一标识与自检工具

最后一步,我想分享的是按钮权限落地的干活技巧。很多人做到上面那步就停了,但真正上线后会发现一个问题:菜单隐藏了,但懂接口的人可以直接 POST 请求删除接口,中间件本身并没有拦截。所以按钮级权限必须回到后端再验一次。

我的做法是给 tp6 写一个全局权限助手函数,控制器里做关键操作前直接调用。

if (!checkPerm('admin/user/delete')) { abort(403, '没有删除权限'); }

checkPerm的实现可以在公共函数文件中复用中间件那个缓存逻辑,读取当前登录用户的角色,然后查权限数组。这样前端隐藏按钮只是体验优化,真正安全在后端二次校验。

为了不让按钮标识乱掉,我还会跑一个自检脚本:扫描视图模板里所有btnAuth('...')调用,再跟 node 表的 route 列表做比对,把没收录的按钮标识全部列出来。

$dir = app()->getAppPath() . 'view'; $iterator = new RecursiveIteratorIterator( new RecursiveDirectoryIterator($dir) ); $usedPerms = []; foreach ($iterator as $file) { if ($file->getExtension() !== 'html') { continue; } $content = file_get_contents($file->getPathname()); preg_match_all('/btnAuth\(\s*[\'"]([^\'"]+)[\'"]\s*\)/', $content, $matches); $usedPerms = array_merge($usedPerms, $matches[1]); } $usedPerms = array_unique($usedPerms); $nodeRoutes = Db::name('node')->where('type', 3)->column('route'); $missing = array_diff($usedPerms, $nodeRoutes); if (!empty($missing)) { dump($missing); // 把缺失的按钮标识打出来 } else { echo '所有按钮权限标识都已收录'; }

这个自检脚本我每次写完权限后台都会跑一遍。它不解决运行时的性能问题,但能从源头杜绝“模板里用了权限标识但节点表没配”的脏数据。配合 3.2 的扫描命令,整个权限体系从生成到校验都是闭环的。

我的习惯是:新建项目时把所有权限相关代码收拢到一个auth目录里,控制器只调用checkPerm,模板只调用btnAuth,不让权限判断散落在业务代码中。这比任何框架层面的技巧都实在,后续接单、转手、加需求都不容易翻车。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询