干过这行的人应该都有同感一接到“做个后台管理”的需求最耗时间的往往不是业务本身而是那些绕不开的基础设施——登录、权限、菜单、操作日志、用户管理。这些模块每个项目都长差不多但每次都得重新写一遍。我大概在三四年前开始用基于ThinkPHP6和Layui的后台快速开发框架当时只是为了赶一个内部运营系统的交付节点结果这一用就延续了好几个项目中间也踩了不少坑今天把这些经验整理出来希望能帮到正在用或者准备用这套组合做后台管理的朋友。这篇文章主要围绕这类后台脚手架的几个核心价值点展开为什么是TP6和Layui、整个系统的目录和请求链路长什么样、RBAC权限是怎么设计出来的、如何用它快速落地一个业务模块以及二次开发和上线前必须注意哪些细节。适合PHP开发者、全栈工程师以及需要独立交付后台系统的同学参考。1. 选型背后的逻辑ThinkPHP6 Layui的组合为什么至今好用1.1 为什么是ThinkPHP6而不是其他PHP框架不是每个团队都要上Swoole常驻内存也不是每个项目都值得用Laravel那套重型组件生态。国内大量后台系统跑在便宜的云服务器甚至虚拟主机上ThinkPHP6的优势恰好就在这里部署门槛低、文档齐全、中文社区资料多得看不完一个熟悉PHP的开发者上手TP6基本不需要过渡期。ThinkPHP6在架构上和TP5比有了明显升级引入了容器Container、门面Facade、中间件Middleware等机制但使用方式依然是“控制器-模型-视图”的经典流程。多应用模式app\admin、app\api、app\index是TP6对后台这种多端场景很友好的设计后台管理走admin应用小程序或APP接口走api应用共用common中的公共逻辑代码隔离做得清清楚楚。实际体验下来TP6的查询构造器和模型事件也足够支撑中后台的增删改查场景。需要写复杂SQL的业务可以走Db::query原生查询常规逻辑用链式操作就够了。1.2 Layui在什么情况下依然是一把好刀很多从前后端分离项目转过来的同学一开始会嫌弃Layui觉得它“不够现代”。但要说清楚一个事实后台管理系统是典型的服务端渲染场景交互密度集中在表格、表单、弹窗、树形菜单这几类组件上Layui恰好把这些组件封装得足够顺手。它最大的优势是不依赖Node构建工具链没有npm install、没有webpack就是一个静态JS文件加一套CSS后端把页面模板渲染出来浏览器里自动加载组件。从维护角度看传统PHP团队接手上手也快。新来的同事打开页面F12看Network能看到清楚明了的请求响应不用理解Vue的响应式原理。Layui的table模块自带请求URL、解析返回、渲染分页开箱即用form模块一套lay-verify规则就能覆盖常用表单校验。我们后来也用Vue3ElementPlus重构过一个模块确实交互上更灵活但整体开发效率在常规后台模块上并没拉开明显差距。1.3 这套组合适合什么场景适合的是企业内部管理系统、运营后台、MIS系统这类典型的“用户少、功能多、逻辑稳定、快速交付”项目。比如工单流转、客户管理、内容发布、库存台账用Layui做出来的效果完全够用开发周期能以“天”为单位推进。不适合的场景也很明确强实时通信消息推送、协同编辑、复杂数据可视化大屏、移动端重交互H5。遇到这类需求后端仍可保留TP6但前端需要换成现代前端框架。好在TP6本身可以纯接口化输出JSON前端怎么换都不影响后台核心能力。2. 从部署目录看整套后台的骨架结构、路由与运行链路2.1 目录结构一眼分清哪些代码该放在哪里使用这套后台框架首先要把TP6多应用目录理解透。以下是我常用的目录规划project/ ├─ app/ │ ├─ admin/ # 后台管理应用 │ │ ├─ controller/ # 后台控制器 │ │ ├─ model/ # 数据模型 │ │ ├─ validate/ # 校验器 │ │ ├─ middleware.php # 后台模块中间件定义 │ │ └─ route/ # 后台路由文件 │ ├─ api/ # 接口应用 │ ├─ common.php # 公共函数 │ └─ event.php # 事件定义 ├─ config/ # 全局配置 ├─ public/ # 入口和静态资源 │ ├─ admin/ # layui静态资源 │ ├─ index.php # 唯一入口 │ └─ router.php # 开发服务器伪静态 ├─ runtime/ # 运行缓存目录 └─ view/ # 视图模板值得留意的是public目录下admin静态资源和管理后台代码的对应关系Layui、jQuery、自定义的admin.js放在public/admin下面控制器渲染的视图模板中通过静态路径直接引用这也是服务端渲染模式的核心——浏览器加载到的就是实实在在的文件不需要编译。2.2 一次后台请求的完整生命周期简单梳理一下从浏览器到页面渲染的路程用户访问 http://域名/admin/index/login.html这个URL经过伪静态解析后被路由到admin应用的Index控制器login方法在到达控制器之前系统会先执行app\admin\middleware.php中声明的中间件链。典型的注册代码如下// app/admin/middleware.php return [ \app\admin\middleware\CheckLogin::class, // 登录状态检测 \app\admin\middleware\CheckPermission::class, // 权限节点检测 \app\admin\middleware\OperationLog::class, // 操作日志 ];注意这个顺序不能随便调整。登录检测必须放在最外层因为后续的权限判断和日志记录都依赖当前登录用户信息。如果权限中间件放在登录中间件之前那么未登录用户访问任何地址都会先触发权限判断导致“非法访问”提示而不是“请先登录”这个问题后面章节会详细说。中间件通过后进入控制器控制器里调用模型层获取数据最后通过fetch方法渲染view目录下的模板模板中嵌入Layui组件代码浏览器拿到完整的HTML后由Layui接管交互。整个流程没有跨域问题、没有token过期刷新逻辑对开发者来说心智负担小很多。2.3 admin开发的潜规则用这套框架开发模块需要遵守一些约定才能最大化效率所有控制器继承统一的BaseController里面有通用的列表搜索、状态切换、排序等基础方法视图层必须有layout布局文件页面只写内容区块公共头部和侧边栏由layout统一渲染状态字段遵循约定比如status用0和1表示禁用与启用创建时间由框架自动写入。这些约定一旦形成新模块开发就变成了“复制粘贴改字段”的活。3. RBAC权限模型拆解表结构、中间件校验与动态菜单渲染3.1 权限相关表结构是怎么设计的RBAC基于角色的访问控制是后台系统权限设计的经典模型。这套框架中的权限体系由四张表组成管理员表、角色表、菜单权限表、角色菜单关联表。CREATE TABLE admin_user ( id int(11) unsigned NOT NULL AUTO_INCREMENT, username varchar(60) NOT NULL DEFAULT COMMENT 登录名, password varchar(100) NOT NULL COMMENT 加密密码, nickname varchar(60) NOT NULL DEFAULT COMMENT 昵称, avatar varchar(255) NOT NULL DEFAULT COMMENT 头像, role_id int(11) NOT NULL DEFAULT 0 COMMENT 角色ID, status tinyint(1) NOT NULL DEFAULT 1 COMMENT 1正常 0禁用, last_login_ip varchar(50) DEFAULT NULL, last_login_time int(11) DEFAULT NULL, create_time int(11) DEFAULT NULL, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE admin_role ( id int(11) unsigned NOT NULL AUTO_INCREMENT, role_name varchar(30) NOT NULL COMMENT 角色名称, remark varchar(255) DEFAULT , status tinyint(1) NOT NULL DEFAULT 1, sort int(11) NOT NULL DEFAULT 0, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE admin_menu ( id int(11) unsigned NOT NULL AUTO_INCREMENT, pid int(11) NOT NULL DEFAULT 0 COMMENT 父级ID, title varchar(30) NOT NULL COMMENT 菜单标题, icon varchar(60) DEFAULT COMMENT 图标class, path varchar(255) NOT NULL COMMENT 路由路径, permission varchar(100) DEFAULT COMMENT 权限标识, is_menu tinyint(1) NOT NULL DEFAULT 1 COMMENT 是否显示在菜单, status tinyint(1) NOT NULL DEFAULT 1, sort int(11) NOT NULL DEFAULT 0, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE admin_role_menu ( role_id int(11) NOT NULL, menu_id int(11) NOT NULL, PRIMARY KEY (role_id,menu_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;菜单表和权限节点公用一张表是这套设计很聪明的地方。菜单是一种“可见权限”按钮操作比如删除按钮也是一种“操作权限”都归入admin_menu只是用is_menu字段区分是否展示在左侧导航栏用permission字段记录权限标识。这样做的好处是权限管理的界面可以统一维护不管菜单还是按钮都能在同一个“菜单管理”页面配置。3.2 权限校验中间件登录后如何判断“有没有资格”权限判断的核心逻辑是获取当前请求的控制器和方法名拼出路径字符串再和当前用户的角色所拥有的菜单path列表比对如果匹配就放行。?php declare(strict_types1); namespace app\admin\middleware; use think\Request; use think\facade\Session; class CheckPermission { public function handle(Request $request, \Closure $next) { $admin Session::get(admin_user); if (empty($admin)) { return redirect((string)url(/admin/index/login)); } // 超级管理员直接放行 if ($admin[role_id] 1) { return $next($request); } $path str_replace(.html, , $request-pathinfo()); $menu \app\admin\model\AdminMenu::where(path, $path) -where(status, 1) -find(); // 这条路径不在菜单表里视为无需权限控制的公开接口 if (empty($menu)) { return $next($request); } // 根据角色ID查出已授权的菜单ID集合 $roleMenus \app\admin\model\AdminRoleMenu::where(role_id, $admin[role_id]) -column(menu_id); if (!in_array($menu[id], $roleMenus)) { return json([code 0, msg 无权限访问]); } return $next($request); } }这段代码的关键在于“路径即权限标识”的理念。菜单管理里新建一条菜单时path填的就是控制器/方法的访问路径比如customer/list。角色勾选菜单授权时只要勾了这条菜单对应路径的访问权就被授予。新增模块时不需要再单独配置一套权限系统只要菜单表里加了路径权限自动生效。这里有一个容易被忽略的细节不在菜单表里的路径会被直接放行。这意味着如果你有一类接口不想受权限控制比如登录验证码接口只要不录入菜单表即可。反过来一旦某个操作需要被控制就必须保证菜单表中的path和实际访问路径完全一致包括大小写、子目录层级否则会出现操作后数据被改掉但权限判断失败的情况。3.3 动态菜单左栏导航如何跟随权限变化菜单渲染的核心是把admin_menu表按pid递归组合成树然后根据当前角色的授权菜单ID过滤最后输出成Layui侧边栏接受的HTML结构。function buildMenuTree($list, $pid 0) { $tree []; foreach ($list as $item) { if ($item[pid] $pid) { $children buildMenuTree($list, $item[id]); if ($children) { $item[children] $children; } $tree[] $item; } } return $tree; }在视图模板里递归遍历这个树输出侧边栏ul classlayui-nav layui-nav-tree lay-filtersideNav {foreach $menus as $menu} {if isset($menu[children]) count($menu[children]) 0} li classlayui-nav-item a hrefjavascript:;i class{$menu.icon}/i {$menu.title}/a dl classlayui-nav-child {foreach $menu[children] as $child} dda href{$child.path}{$child.title}/a/dd {/foreach} /dl /li {else /} li classlayui-nav-item a href{$menu.path}i class{$menu.icon}/i {$menu.title}/a /li {/if} {/foreach} /ul菜单选中态通常用当前URL和菜单path对比来判断在模板里加一个current变量输出时给匹配到的菜单项追加layui-this样式。动态菜单的优势很直接不同角色登录后左侧看到的菜单天然不同提交表单里的部门选择、操作按钮也通过权限标识统一控制不需要在控制器里写死“谁能改谁能删”。4. 快速开发实战用一个业务模块串起全流程4.1 先建表再把模型和控制器塞进规范位置这里用一个典型的“客户管理”模块作为例子。常规后台开发流程是先建数据表再写控制器、模型、视图最后把菜单权限节点录入。假如系统里已经有一张customer表CREATE TABLE customer ( id int(11) unsigned NOT NULL AUTO_INCREMENT, name varchar(100) NOT NULL COMMENT 客户名称, phone varchar(20) DEFAULT COMMENT 联系电话, source varchar(30) DEFAULT COMMENT 来源渠道, remark text COMMENT 备注, status tinyint(1) NOT NULL DEFAULT 1, create_time int(11) DEFAULT NULL, update_time int(11) DEFAULT NULL, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;控制器文件放到 app/admin/controller/CustomerController.php继承框架的BaseController?php declare(strict_types1); namespace app\admin\controller; use app\admin\model\Customer; class CustomerController extends BaseController { protected $model; public function __construct() { parent::__construct(); $this-model new Customer(); } // 列表页 public function index() { return view(); } // 列表数据接口 public function list() { $param $this-request-param(); // 搜索条件组装 $where []; if (!empty($param[name])) { $where[] [name, like, % . $param[name] . %]; } if (!empty($param[source])) { $where[] [source, , $param[source]]; } $lists $this-model-where($where) -order(id desc) -paginate([ list_rows $param[limit] ?? 10, page $param[page] ?? 1, ]); return json([ code 0, msg , count $lists-total(), data $lists-items(), ]); } // 新增 public function add() { if ($this-request-isPost()) { $data $this-request-param(); // 校验 $this-model-save($data); return json([code 1, msg 新增成功]); } return view(); } // 编辑 public function edit($id) { if ($this-request-isPost()) { $data $this-request-param(); $this-model-update($data); return json([code 1, msg 更新成功]); } $info $this-model-find($id); return view(, [info $info]); } // 删除 public function delete() { $ids $this-request-param(ids/a); $this-model-where(id, in, $ids)-delete(); return json([code 1, msg 删除成功]); } }这里需要说明一个设计取舍列表接口返回的数据格式严格按照Layui table模块的约定即code0表示成功、count为总数、data为列表数据。Layui table渲染时只要URL指到这个list方法前端的表格就自动完成分页和填充不需要额外写渲染逻辑。这一条是“快速开发”的核心——后端把返回结构定好前端表格就能跑起来。4.2 列表视图一刻钟搞定增删改查页面列表页view/customer/index.html的写法非常有代表性几乎是所有后台模块的模板!DOCTYPE html html langzh-CN head meta charsetutf-8 title客户管理/title link relstylesheet href/admin/layui/css/layui.css /head body div classlayui-fluid stylepadding:15px; !-- 搜索栏 -- div classlayui-form layui-form-pane stylemargin-bottom:10px; div classlayui-inline input typetext namename placeholder客户名称 classlayui-input idsearchName /div div classlayui-inline select idsearchSource classlayui-input option value全部来源/option option value线上线上/option option value转介绍转介绍/option /select /div button classlayui-btn idsearchBtn搜索/button button classlayui-btn layui-btn-primary idresetBtn重置/button a href/customer/add.html classlayui-btn layui-btn-normal新增客户/a /div !-- 表格 -- table idcustomerTable lay-filtercustomerTable/table !-- 行内操作按钮模板 -- script typetext/html idrowOperate a classlayui-btn layui-btn-xs lay-eventedit编辑/a a classlayui-btn layui-btn-danger layui-btn-xs lay-eventdelete删除/a /script /div script src/admin/layui/layui.js/script script layui.use([table, form, layer], function(){ var table layui.table; var form layui.form; var $ layui.$; // 表格渲染 table.render({ elem: #customerTable, url: /customer/list.json, page: true, cols: [[ {field: id, title: ID, width: 60}, {field: name, title: 客户名称}, {field: phone, title: 联系电话}, {field: source, title: 来源渠道}, {field: status, title: 状态, templet: #statusTpl}, {title: 操作, toolbar: #rowOperate, width: 150} ]] }); // 搜索重置事件 $(#searchBtn).on(click, function(){ var name $(#searchName).val(); var source $(#searchSource).val(); table.reload(customerTable, { where: {name: name, source: source}, page: {curr: 1} }); }); // 行工具事件 table.on(tool(customerTable), function(obj){ var data obj.data; if (obj.event edit) { window.location.href /customer/edit_ data.id .html; } else if (obj.event delete) { $.post(/customer/delete.json, {ids: [data.id]}, function(res){ if (res.code 1) { layer.msg(res.msg); table.reload(customerTable); } }, json); } }); }); /script /body /htmlLayui的table模块在列表页这里就是一把梭URL直达、自动请求、自动分页。开发新模块时把这段模板复制过去改一下URL和列字段名就完成了80%的工作。编辑页面更简单基于同一个模板页面载入时用form.val方法回填后台传来的info数据// 编辑页模板挂在dom加载完成后执行 layui.use([form], function(){ var form layui.form; var info JSON.parse({:json_encode($info)}); form.val(editForm, { name: info.name, phone: info.phone, source: info.source, remark: info.remark }); form.render(); });唯一要注意的是select下拉的回显如果使用了自定义下拉选项比如来源渠道字段必须调用form.render()重新渲染表单组件否则Layui会把select包裹成自定义样式的div页面刷新后看到的值是上一次缓存里的旧值。这个“缓存回显”问题出现的频率相当高尤其是在同一个页面做多次编辑操作的时候。4.3 权限节点录入模块做完别忘了跨出最后一步控制器和视图都写完还不算结束还需要在后台的“菜单管理”里录入客户管理的菜单节点并把这个节点授权给对应的角色。录入时父ID选择“客户管理”作为顶级菜单path填写customer/index添加子节点时path写customer/add、customer/edit、customer/deletepermission字段可以填customer:add之类的标识。这一步如果遗漏造成的后果是即使代码写得再完整用户登录后左侧看不到入口直接访问URL也会发现角色没有权限。很多开发者第一次接触这套框架时都容易在“这个模块怎么多了也白屏”的困惑上浪费时间。后来的习惯是先加菜单节点再开发代码这样测试的时候路径和权限状态从一开始就是通的。5. 二次开发最容易翻车的五个地方与对应解法5.1 中间件注册顺序导致登录后权限全部失效有个隐蔽的问题容易在部署阶段遇到用户登录成功后访问任何带权限控制的页面都被判定为“无权限访问”但登录状态本身看起来是正常的。排查时需要先看app/admin/middleware.php中CheckPermission是否注册在CheckLogin之前。如果权限中间件先执行此时Session里还没有任何用户信息role_id取出来永远是null或0自然匹配不到任何授权菜单。表现为登录成功但寸步难行。解决方式很简单确认中间件数组中登录检测在第一位。另外权限中间件里不要直接用Session读取的用户信息判断“是否登录”应该先显式判断Session为空并直接软跳转到登录页这样逻辑更清晰也不会出现误报。5.2 列表搜索条件被覆盖多条件查询时条件丢失列表接口中常见的错误写法是把where赋值掉$where []; if (!empty($param[name])) { $where [name, like, % . $param[name] . %]; }当存在多个搜索条件时第二个条件会把第一个覆盖掉导致只生效最后一个。正确做法是像前面章节写的那样用二维数组结构不断追加条件最后一次性传给查询构造器。这种问题在功能调试时不易发现因为单条件搜索往往正常多条件一起搜才暴露。另外要注意的是参数的过滤搜索框可能传入空字符串如果不做empty判断会把name拼进where导致查询不到数据。统一用empty函数判断既过滤了空值也能确保数字0比如状态字段“禁用”不会被误判为“空”而丢掉条件。5.3 Layui table分页与TP6分页数据结构对不上这是前端表格常见的一个坎。TP6默认的paginate()返回的数据结构为{ total: 100, per_page: 10, current_page: 1, data: [] }而Layui table期望的格式是{code, count, data}。如果不做任何转换前端表格始终显示“数据加载失败”。处理办法是在控制器组装返回如前面list方法中的写法total取出作为countitems取出作为datacode固定为0。改好之后需要顺手把其他列表接口的返回结构一并对齐防止不同模块的前端表格出现同一问题。5.4 编辑弹窗中的select下拉回显错误列表页使用弹窗形式而非独立页面打开编辑功能时有一个非常典型的前端问题select下拉框的回显值不对。原因是Layui对select做了视觉重构原始select被隐藏显示层由Layui生成默认情况下Layui只渲染一次。打开弹窗后设置表单值时如果仅用form.val(filter, {...})而没有再次调用form.render(select)下拉框的显示内容不会更新用户看到的是上一次弹窗渲染出来的旧选项。每次为编辑弹窗赋值后都需要主动触发一次form.render(select)。这个细节在独立页面的编辑场景下不那么突出但凡是弹窗内嵌表单的模块几乎都会遇到。别忘了脚本执行顺序也很重要form.render必须放在form.val之后。5.5 Nginx下部署后404或者静态资源加载失败本地开发一切正常部署到LinuxNginx环境后访问后台页面出现404、静态资源加载失败、接口500等问题这通常是伪静态配置没跟上。TP6的路由依赖URL重写Nginx站点配置需要加入location / { if (!-e $request_filename) { rewrite ^(.*)$ /index.php?s$1 last; } }同时确保把站点根目录指向public文件夹而不是项目根目录。如果静态资源加载失败很大概率是root路径配置错误浏览器请求/admin/layui/css/layui.css时服务器去项目根目录找public/admin自然找不到。还有一个高频问题runtime目录没有写权限控制器一执行就报错终端上能看到“runtime is not writable”字样这个直接给public上一级目录加写权限即可。6. 性能、安全与上线前检查清单6.1 六条必须检查的安全项后台管理系统的风险敞口不像前端官网那么暴露但一旦暴露就是致命的。基于这套框架交付项目前我建议逐条过一遍默认管理员账号和密码必须在首次部署时强制修改。很多脚手架出厂账号是admin/123456内网系统容易被忽略外网系统一经扫描必然中招。登录接口要加验证码和失败次数限制。可以用TP6的缓存做登录失败计数同一IP连续失败5次后锁定30分钟同时记录日志。文件上传要做类型和大小双重限制最好把上传文件存到本地public之外的私有目录或云存储避免可执行文件直接通过URL被解析执行。敏感操作要记录操作日志。基于模板时把OperationLog中间件打开它会拦截所有请求把操作者、操作内容、请求参数、IP写入日志表排查问题时有据可查。数据库查询统一走TP6的查询构造器和参数绑定避免手动拼接SQL产生注入。后台接口要统一加CSRF防护。Layui的table请求不是表单提交但ajax请求也需要在header中携带token服务端中间件统一校验。6.2 提升响应速度的配置修改后台系统用户量通常不大但如果是生成报表、拉取大列表的模块还是应该做一些基础优化。操作缓存、开启OPcache、数据库连接池这三项做下来效果显著。首选是给列表查询加缓存。客户列表这种数据变化不频繁的接口用TP6的cache方法直接缓存查询结果设置五分钟左右的过期时间。其次TP6支持Redis驱动可以用它存session多台服务器部署时Session能共享不会出现用户在一台机器登录另一台机器掉线的问题。opcache配置简单PHP环境默认可以开能减少每次请求重复解析代码的开销。静态资源方面Layui的默认样式和组件代码体积不算小生产环境可以开启Gzip压缩或者把layui相关JS/CSS合并到一个文件减少HTTP请求数。如果团队有CDN直接把静态资源托管上去效果更好。6.3 一次线上小事故给我的教训之前给某公司做过一个内部订单管理系统上线时忘了改默认管理员密码。第二天客户的运维就反馈说发现凌晨有异常登录尝试IP来自海外。虽然那套后台没有对外暴露业务数据但被扫描器盯上的体验依然让人后背一凉。从那以后我给自己定了一个固定动作每次部署完第一件事就是进入数据库把admin用户密码改成高强度随机值然后把登录失败锁定的阈值从10次改到5次再把验证码强制开启。这件事也提醒我快速开发框架带来的效率一定要建立在把默认配置全部改干净的基础上不能图一时方便留后门。这套基于TP6加Layui的后台脚手架在中小型项目上的生产力是实打实的。一个完整业务模块从建表到上线权限配置理论上一个熟悉流程的开发者可以在一天之内交付这在前后端分离模式下很难实现。我用它交付过的项目少说也有五六个期间也经过从“讨厌这种老派前端”到“理解这种务实的嵌入方式”的心态转变。如果你正在选型或者刚接手这类项目建议先按上面第五、第六章节的思路把系统整体过一遍争取在正式开发业务模块前把底层行为吃透。最后再分享一个个人习惯每次新建一个模块先把菜单权限节点配置好再动代码。表面上这只是顺序问题实际上是强迫自己在开发之前就明确模块的结构——哪些页面需要列表权限、哪些操作需要按钮权限。后面写控制器时自然知道哪里该加判断哪里该留公开接口开发效率也高得多。