首页 / 资讯中心 / 文章详情

基于ThinkPHP6和Layui的RBAC后台管理系统脚手架实践

基于ThinkPHP6和Layui的RBAC后台管理系统脚手架实践 ★ FEATURED ARTICLE
easyadmin是一套基于ThinkPHP6和Layui快速开发后台管理系统的实践项目核心目标是把登录、权限、菜单、用户管理这些后台系统里几乎不变的基础设施沉淀成一套可复用脚手架。如果你做过几个管理后台大概率会对这段话有共鸣表面上项目之间业务完全不同但页面结构、用户体系、操作权限、数据列表这几块却惊人地相似。与其每次从零开始搭一套不如先把自己的通用能力抽出来再根据业务往上填。相比市面上偏重的后台框架我更喜欢这种组合。原因很简单数据量通常没到需要拆服务的地步团队里主要就是PHP开发者人手紧张业务需求往往过一两个版本就换一批。一套能快速改动的后台能省下大量沟通和交接成本。easyadmin在内部已经被用来搭建了几个运营管理后台从模型设计到页面表单的骨架基本是同一套流程。这篇文章就把这套系统的设计思路、核心模块、关键实现和踩坑过程完整梳理一遍。1. 这套后台系统的定位和设计思路1.1 为什么我会自造一套后台管理脚手架先交代背景。前几年我接过不少管理系统项目有客户管理后台、订单处理后台、内部工单后台表面看起来业务完全不同但代码结构翻来覆去就那么几块登录和退出、用户表和角色表、菜单和操作权限、文件上传、操作日志。几乎每个项目都从零开始写一遍写完之后还会因为页面风格不统一被反复提需求。于是我开始把这些共用的代码抽出来整理成一套初始后台后续接新项目时直接在这个基础上改。easyadmin就是这么来的。这套系统的定位不是要做成一个通用产品而是“能让我和团队在最快时间内交付一个可运营的后台”。所以它不追求复杂设计核心诉求就三条有清晰的认证流程、有可扩展的权限体系、有足够快的CRUD生成方式。如果这三点能满足大多数后台项目的基础部分一周内就能稳定下来。后续所有业务模块都围绕这三条展开页面风格、接口返回格式、错误码定义也都被这套体系约束住了不会出现一个后台五种写法的混乱局面。在开始动手之前我还特意明确了一个边界easyadmin只处理“后台管理系统通用的那一层”也就是登录、权限、菜单、上传、日志和数据字典。具体业务里的订单状态机、结算规则、复杂报表一概不放进通用层避免把框架越做越重最后沦为谁也不愿意改的遗留系统。1.2 为什么选ThinkPHP6加Layui这个组合选型的时候也考虑过别的方案最后基本确定ThinkPHP6加Layui理由很现实。第一团队里大部分人是PHP背景让他们去学一套重型前端框架往往代价很高。第二后台系统的关键交互并非极复杂的实时联动而是表单、表格、弹窗、树形选择这类常规场景Layui自带的组件已经覆盖很全不需要引入打包构建流程。第三TP6的多应用模式和中间件设计刚刚好既能保持前后端在同一个工程里部署也能把后台接口权限收得很干净。TP6在项目结构上提供了多应用支持这意味着我可以在同一个PHP工程里同时保留前台页面应用和admin后台应用两者共用配置、共用数据库连接、共用模型层。相比传统的单入口多模块写法多应用让admin和其他业务入口的中间件、路由规则都能独立配置权限校验不会污染到用户端接口。Layui则承担了后端的列表渲染、表单验证、弹窗交互配合jQuery生态上手门槛很低。直接加载静态CSS和JS就能工作不需要Node环境对服务器资源也很友好。需要提醒的是Layui 2.x后续的维护节奏相对平缓社区版本一直在更新但如果你准备做五年以上的系统最好把Layui相关组件收敛到一个独立目录并封装自己的公共JS函数减少后续替换前端框架的成本。easyadmin就是这么处理的前端页面只通过统一的JS封装和固定数据格式与后端交互即便换了一套UI库后端代码基本不用动。2. 基础模块拆分和权限模型设计2.1 用户、角色、权限的表结构如何拆几乎所有后台系统都绕不开用户和角色。easyadmin采用经典RBAC模型但不是教科书里那种复杂的多级权限表而是做了一点简化把菜单和权限标识合并到一张表里因为后台系统里“能看哪些菜单”和“能执行哪些操作”通常是绑在一起的。表结构设计如下。CREATE TABLE admin_user ( id INT NOT NULL AUTO_INCREMENT, username VARCHAR(50) NOT NULL COMMENT 账号, password VARCHAR(255) NOT NULL COMMENT 密码哈希, nickname VARCHAR(50) NOT NULL DEFAULT COMMENT 姓名, status TINYINT NOT NULL DEFAULT 1 COMMENT 状态1启用 0禁用, create_time INT NOT NULL DEFAULT 0, update_time INT NOT NULL DEFAULT 0, PRIMARY KEY (id), UNIQUE KEY username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE admin_role ( id INT NOT NULL AUTO_INCREMENT, role_name VARCHAR(30) NOT NULL COMMENT 角色名, code VARCHAR(30) NOT NULL COMMENT 角色标识, status TINYINT NOT NULL DEFAULT 1, remark VARCHAR(255) NOT NULL DEFAULT , PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE admin_menu ( id INT NOT NULL AUTO_INCREMENT, parent_id INT NOT NULL DEFAULT 0 COMMENT 父级菜单ID, title VARCHAR(50) NOT NULL COMMENT 菜单名称, name VARCHAR(50) NOT NULL COMMENT 路由标识如 user/index, icon VARCHAR(50) NOT NULL DEFAULT COMMENT 图标类名, type TINYINT NOT NULL DEFAULT 1 COMMENT 1菜单 2按钮, sort INT NOT NULL DEFAULT 0 COMMENT 排序, status TINYINT NOT NULL DEFAULT 1, create_time INT NOT NULL DEFAULT 0, PRIMARY KEY (id), KEY parent_id (parent_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE admin_role_user ( role_id INT NOT NULL, user_id INT NOT NULL, PRIMARY KEY (role_id,user_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE admin_role_menu ( role_id INT NOT NULL, menu_id INT NOT NULL, PRIMARY KEY (role_id,menu_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;为什么不用冗余字段直接把角色ID挂在用户表上因为现实中一个运营人员往往需要同时具备多个角色比如既要“订单客服”的权限也要“数据统计”的权限一对多关联表才能支持这种配置。菜单和按钮放同一张表是为了让权限分配页面能在同一棵树上勾选不需要维护两套数据。按钮记录通过type字段区分比如type2的代表“删除用户”按钮权限其对应的name写成类似user/delete这样的规则标识。在关联表设计上没有单独建权限表而是用admin_role_menu直接关联菜单ID。这样角色可以勾选的集合就是菜单表里所有的菜单和按钮够直观也够用。假如以后遇到更细的“只允许查看自己创建的订单”这类数据范围控制再加一个data_scope字段或者单独的数据权限表即可不会动到现有结构。2.2 菜单树、权限标识和动态菜单生成的联动后台登录成功之后系统需要知道这个人能看到哪些菜单、能不能点删除按钮。实际执行时会根据当前用户ID查出角色ID再通过角色查出菜单ID最终得到这个用户被授权的name标识集合。这个集合就是权限判断的核心依据。$roleIds Db::name(admin_role_user) -where(user_id, $userId) -column(role_id); $menuIds $roleIds ? Db::name(admin_role_menu)-whereIn(role_id, $roleIds)-column(menu_id) : []; $perms $menuIds ? Db::name(admin_menu) -whereIn(id, $menuIds) -where(status, 1) -column(name) : [];拿到权限标识集合之后还要再查一次完整菜单数据按parent_id组装成树结构方便前端侧边栏渲染。树组装的递归函数并不复杂但需要注意排序和状态过滤最好一次性把启用状态的菜单全查出来再在内存里组装不要循环查数据库。function buildMenuTree(array $menus, int $parentId 0): array { $tree []; foreach ($menus as $menu) { if ((int)$menu[parent_id] $parentId) { $children buildMenuTree($menus, (int)$menu[id]); if ($children) { $menu[children] $children; } $tree[] $menu; } } return $tree; }菜单表中的name字段就是权限标识。列表接口通常对应user/index新增对应user/add保存对应user/save删除对应user/delete规则完全由后端统一把控。前端Layui渲染侧边栏时遍历树节点按钮的显隐则通过后端在渲染时注入权限集合前端不做过多逻辑。这样做的好处是权限判断只信任后端绕过前端直接调接口也会被拦截。由于角色权限和菜单数据都属于低频变化的数据实际项目里我会在登录时把当前用户的权限标识集合写进缓存有效时间设为两小时。角色表或菜单表一旦发生变更需要主动清理相关缓存否则就会出现“改了角色权限用户刷新页面后依然没变化”的情况。这个问题后面在排查章节会详细展开。3. 基于TP6的落地过程3.1 初始化项目与应用目录规划初始化easyadmin项目时我习惯先把基础工程建好再装多应用扩展。ThinkPHP6默认是单应用模式要使用多应用必须先安装官方扩展包然后用配置文件开启多应用开关。composer create-project topthink/think easyadmin cd easyadmin composer require topthink/think-multi-app开启之后在config/app.php中设置auto_multi_app true然后创建app/admin目录作为后台应用。目录结构大概这样app/ index/ controller/Index.php admin/ controller/ middleware/ model/ validate/ route/ config/ public/ static/ layui/ common.js index.phppublic目录是Web服务器的唯一入口根目录layui的静态文件放在public/static下面这样访问路径就是/static/layui/...不会被路由干扰。后台应用的所有控制器都放在app/admin/controller里中间件单独放middleware目录模型和验证器也各自独立维护起来很清楚。多应用下URL默认长这样http://域名/admin/user/index其中user是控制器名index是方法名。如果想让路由更干净也可以在app/admin/route里手动定义路由别名。easyadmin没有把所有控制器都丢到路由文件里而是让控制器和URL保持默认对应关系因为后台模块页面多、增删频繁逐个写路由维护成本太高。只在需要做外部参数校验或特殊跳转时我才会单独加路由规则。3.2 登录态保留和统一返回格式登录是整个后台安全的第一道门。用户提交账号密码后后端先查用户表用password_verify校验密码哈希值再判断账号状态。校验通过后生成一个随机token把用户基本信息、角色列表、权限标识集合写入缓存过期时间设为2小时。token返回给前端后后续请求都通过请求头里的Authorization字段携带。public function login() { $username trim($this-request-post(username, )); $password $this-request-post(password, ); $admin AdminUser::where(username, $username)-find(); if (!$admin || !password_verify($password, $admin-password)) { return json([code 1, msg 账号或密码错误, data []]); } if ((int)$admin-status ! 1) { return json([code 1, msg 账号已被禁用, data []]); } $userInfo [ id $admin-id, username $admin-username, nickname $admin-nickname, ]; $userInfo[perms] $this-getUserPermissions($admin-id); $token md5($admin-id . - . time() . uniqid()); cache(admin_token_ . $token, $userInfo, 7200); return json([code 0, msg 登录成功, data [token $token]]); }TP6里每个控制器方法直接返回Json对象即可但为了让前端调用更省心easyadmin统一封装了Jsuccess和Jerror两个快捷函数所有接口返回结构都是{code: 0, msg: 操作成功, data: {...}}。code0表示成功非0表示失败。Layui的表格组件有一个特殊约定它要求返回结构是{code: 0, msg: , count: 总数, data: 数据列表}所以在列表接口里也会按这个结构返回保证前端table组件能直接识别。在登录链路里中间件是身份校验的核心。easyadmin在app/admin/middleware.php中注册了CheckLogin中间件每个请求进来时都会去解析请求头中的Authorization字段查出缓存中的用户信息并挂到$request-adminUser上。如果查不到token对应缓存就返回code401前端收到后自动跳转到登录页。登录和退出这两个控制器方法要放入白名单避免无限重定向。3.3 基于中间件的权限校验实现光有登录态还不够后台必须有操作权限控制。easyadmin的权限校验有两条线一条是针对菜单渲染的可见性控制另一条是针对接口的访问控制。接口访问控制我选择用中间件实现这样业务控制器里不用每个方法都去手动判断漏写的概率会小很多。class CheckPermission { public function handle($request, \Closure $next) { $admin $request-adminUser; if (isset($admin[is_super_admin]) $admin[is_super_admin]) { return $next($request); } $path trim($request-pathinfo(), /); // 例如 admin/user/delete去掉第一个应用名得到 user/delete $parts explode(/, $path); array_shift($parts); $routeMark strtolower(implode(/, $parts)); $perms $admin[perms] ?? []; if (!in_array($routeMark, $perms, true)) { return json([code 403, msg 没有操作权限, data []]); } return $next($request); } }这段逻辑的核心是让URL路径和菜单表中的name对应起来。假设某个菜单记录的name是user/delete那么访问/admin/user/delete时中间件去掉应用名前缀后得到的规则标识就是user/delete此时只需要判断当前用户的权限集合中是否存在这个值。如果不存在直接返回403不会进入控制器。这里要特别提醒一个大小写问题。TP6默认URL访问是不区分控制器大小写的但中间件里做字符串比较时必须统一转成小写。否则菜单里配置了User/Delete实际请求路径是user/delete权限判断就会失败。我建议在建表时就约束所有菜单name统一小写代码里也统一用strtolower做兜底避免线上因为大小写问题导致权限误判。super admin角色可以在角色表里增加一个is_super_admin字段或者在系统配置里写死一个超级角色ID判断时直接放行方便管理人员排查问题。3.4 登录防爆破和敏感操作日志后台系统的安全不能只靠权限模型登录暴力破解和操作留痕同样重要。easyadmin在登录接口里加了失败次数限制同一个账号五分钟内连续失败超过五次就锁定十五分钟。这个限制可以用缓存轻松实现缓存key用账号名拼接失败次数即可。操作日志用中间件统一记录所有写操作都会在响应返回后写入一张admin_action_log表。字段包括操作人ID、请求方法、请求路径、请求参数、IP和耗时。这个日志不是为了审计抠细节而是出了问题能快速回溯到底是谁在什么时间改了哪条数据。配合TP6的afterWrite事件还可以把日志异步落库避免每次都多一次数据库写入影响接口速度。前端操作按钮不需要为日志做任何改造全部由中间件自动处理。登录接口需要额外加一个渲染验证码的逻辑。我用的方案是后端用think-captcha扩展生成图片登录页里放入img标签点击切换验证码。校验时先确认验证码是否通过再走账号密码验证。Layui表单的验证码输入框并不需要复杂的自定义组件只要在form表单里正常提交后端按常规参数接收即可。4. 快速开发业务模块的通用实战4.1 一个通用后端控制器基类怎么写后台系统最耗时的往往是增删改查的重复代码。easyadmin的做法是把通用CRUD逻辑抽到一个基类控制器里新的业务模块只需要继承基类并指定“用哪个模型”“搜索哪些字段”就能获得列表、新增、编辑、删除、批量删除这些基础接口。abstract class CrudBaseController extends BaseController { protected $model; protected $search []; public function lists() { $page max(1, (int)$this-request-param(page, 1)); $limit min(100, max(1, (int)$this-request-param(limit, 20))); $keyword trim($this-request-param(keyword, )); $query $this-model::order(id desc); if ($keyword ! $this-search) { $query-where(function ($q) use ($keyword) { foreach ($this-search as $field) { $q-whereOr($field, like, % . $keyword . %); } }); } $res $query-paginate([ list_rows $limit, page $page, ])-toArray(); return json([ code 0, msg , count $res[total], data $res[data], ]); } public function save() { $data $this-request-post(); $validate $this-getValidate(); if ($validate !$validate-check($data)) { return json([code 1, msg $validate-getError(), data []]); } if (isset($data[id])) { $this-model::update($data); } else { $this-model::create($data); } return json([code 0, msg 保存成功, data []]); } public function delete() { $ids $this-request-post(ids/a, []); if (!$ids) { return json([code 1, msg 请选择要删除的数据, data []]); } $this-model::whereIn(id, $ids)-delete(); return json([code 0, msg 删除成功, data []]); } }这个基类的价值不只是省几行代码更重要的是规范了接口返回格式和错误提示方式。新接手的人只要照着现有模块改不需要去猜列表接口该返回什么结构、删除接口是POST还是GET。每个业务子类只需声明自己的模型和搜索字段专属的验证逻辑放到独立的验证器里再到子类里覆写getValidate方法灵活性并不会被基类钳制住。当然通用基类也有一个前提业务逻辑足够简单。像财务结算这种复杂场景直接继承基类反而别扭那就不要在子类里调通用save而是在控制器里写自己独立的save方法。easyadmin的原则是“通用层覆盖80%的常规接口剩下20%的业务特例单独处理”不要为了让代码看起来统一就硬套基类。4.2 前端页面的标准套路Layui表格加弹窗表单后端接口就绪后前端页面也是一个固定模板。列表页使用Layui的table.render渲染数据顶部工具栏放“新增”和“搜索”按钮操作列放“编辑”“删除”。每一行数据的主键通过>table.render({ elem: #data-table, url: /admin/user/lists, method: post, headers: { Authorization: Bearer getToken() }, parseData: function (res) { return { code: res.code, msg: res.msg, count: res.count, data: res.data }; }, cols: [[ { checkbox: true }, { field: id, title: ID, width: 80, sort: true }, { field: username, title: 账号 }, { field: nickname, title: 姓名 }, { title: 操作, toolbar: #row-toolbar, width: 160 } ]] });弹窗新增和编辑使用layer.open打开一个iframe页面iframe里加载独立表单。表单提交后通过parent.layui.table.reload(data-table)刷新父页面表格同时关闭弹窗。这种iframe弹窗方案虽然看起来传统但胜在稳定无需复杂的状态管理在Layui生态里已经是被验证了很多年的做法。关于弹窗表单的提交有一个细节很容易踩坑进入iframe后Layui的form.render()必须在页面加载完成后执行否则下拉框和日期选择器不会正常初始化。如果表单里使用了动态渲染的下拉选项还需要在渲染完成后重新调用一次form.render(select)。这个问题在本地开发时往往看不出来部署到服务器之后因为CSS和JS加载顺序变化症状就变得明显了。4.3 文件上传和数据字典的公共化后台开发还有一个高频需求就是文件上传。商品图片、头像、附件都走统一接口前端用Layui的upload.render实现文件选择后端接收到文件后按日期分目录存储到public/static/upload下。文件命名统一用日期 随机字符串避免中文文件名和重复文件名带来的各种问题。数据字典也是一个容易重复造轮子的功能。比如订单状态、审核状态、用户类型这些值其实都是成组出现的easyadmin单独建了一张admin_dict_type和admin_dict_data表后台提供配置界面列表页面通过字典类型编码获取下拉选项不需要为每种状态重新做一张表。这个设计特别适合内部系统因为运营人员可能随时需要加一个新的状态值让他们自己去后台配置比改代码上线快得多。公共化的原则是“高频且规则一致”。登录、权限、列表、上传、字典这些功能在几乎所有后台都会出现放公共层没问题但每个业务独特的规则判断和计算逻辑不要强行抽公共。easyadmin最初的版本就是什么都想抽公共方法结果公共类越改越复杂最后不得不重构。公共层保持简单业务层保持灵活才是长期维护的正确姿势。5. 实操中反复踩到的坑与经验5.1 改角色权限后不生效的缓存问题权限缓存是easyadmin里最容易被忽略的坑。角色给某个用户加了“删除用户”权限结果这个用户刷新页面后依然提示没有权限。原因很简单登录时已经把权限标识集合写进了缓存接口权限判断读取的是缓存里的旧数据而不是数据库里的最新配置。解决方法也不复杂在角色保存、菜单保存、用户角色变更的地方主动清理这个用户对应的权限缓存。更好的做法是引入“权限版本号”每次角色或菜单变更时版本号加一用户登录时缓存里同时保存版本号每次请求前对比版本号不一致就重新拉取数据库。不过这套机制对简单项目有点过度设计日常情况下直接清理这几个用户的token缓存就够了。另外还要注意文件缓存的有效期。PHP没有类似Redis的过期清理机制TP6官方文件缓存组件是惰性过期的也就是读的时候发现过期才会重写。如果你在Linux服务器上部署缓存目录权限不对还会直接导致写入失败表现为登录成功后接口依然返回401。这个问题排查起来特别隐蔽我建议把runtime目录权限设置为可写并在中间件里打日志直接输出缓存写入结果能省掉不少调试时间。5.2 Layui表格分页参数和TP6分页的对接错位Layui table默认向上传page和limit两个参数而ThinkPHP6的paginate方法默认接收的参数名也是page和list_rows。如果不做处理直接调用paginate($limit)容易忽略当前页数导致无论点第几页都返回第一页。easyadmin的做法是先手动读取page和limit再显式传给paginate避免依赖默认值。还有一类问题出现在列表接口的搜索功能上。前端常常会通过自定义参数向表格URL传额外参数比如?status1但TP6的分页查询在生成SQL时只会带上分页参数不会自动带上这些自定义条件。如果需要在分页接口里做组合筛选必须用where条件手动叠加并且在paginate的选项里把查询参数也合并进去否则翻页后筛选条件就丢了。$query $this-model::where(status, $status); $res $query-paginate([ list_rows $limit, page $page, query $this-request-get(), ]);query选项的作用是把当前URL上的额外查询参数附着到分页链接上这样前端即使通过URL翻页也不会丢失筛选条件。实际项目里如果使用Layui table重新加载数据前端会把参数以post方式提交过来此时query选项不必须但保留这个习惯可以在换用GET请求时不出问题。5.3 Nginx伪静态和多应用路径规则部署上线时的常见问题就是后台页面可以打开但列表接口返回404或者静态资源全部无法访问。大多数原因是Nginx的root指向了项目根目录而不是public目录或者重写规则没有把真实存在的文件排除掉。easyadmin的正确目录访问规划是root指向public静态文件直接由Nginx处理PHP请求统一重写到index.php入口文件。server { listen 80; server_name admin.example.com; root /var/www/easyadmin/public; index index.php index.html; location / { if (!-e $request_filename) { rewrite ^/(.*)$ /index.php?s/$1 last; } } location ~ \.php$ { include fastcgi_params; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; fastcgi_pass 127.0.0.1:9000; } }这段配置里最重要的一行是rewrite ^/(.*)$ /index.php?s/$1 last;它把用户访问的/admin/user/lists改写成/index.php?s/admin/user/listsTP6就能正确解析出应用名、控制器名和方法名。如果去掉if (!-e $request_filename)判断那么真实存在的CSS、JS文件也会被重写到PHP入口静态资源自然全军覆没。部署时还容易遇到的一个坑是PHP版本问题。ThinkPHP6对PHP版本要求不太低但如果你用了已经停止支持的扩展或函数上线后就会看到白屏或500错误。建议部署前在命令行执行php -v查看版本并检查是否安装了必需的扩展。遇到500错误先打开TP6的调试模式查看runtime日志不要盲目刷新页面猜问题。以下是几个我在实际运维中经常遇到的情况汇总现象大概率原因排查思路后台能登录列表接口返回500PHP缺少PDO或JSON扩展检查扩展安装情况页面样式正常接口404Nginx重写规则缺失确认rewrite和root配置修改权限无效果权限缓存未清理清缓存或刷新token上传文件失败public/static/upload无写入权限检查目录权限页面反复弹出登录框token缓存过期或中间件读取失败核对Authorization字段和缓存配置5.4 表单提交重复和前后端参数不一致Layui弹窗表单保存后如果用户快速连续点击提交按钮会产生多条重复数据。前端在提交按钮上做防重复处理一开始就标记状态提交过程中禁用按钮接口返回后再恢复。后端也要做一层兜底比如唯一性字段上建唯一索引或者在save方法里先查询是否存在相同记录。前后端参数不一致属于团队协作里最容易出现的问题。easyadmin在基类里严格要求所有接口参数使用小写下划线命名前端字段名也保持完全一致。比如数据中create_time在前端就是create_time不会出现后端返回camelCase然后前端又做一层映射的麻烦。这种约定虽然不是技术难题但真的能省掉大量联调时间。从根上解决这个问题可以在统一返回函数里增加一层字段过滤把模型里不该暴露的字段定的黑名单列出来。大多数后台列表并不需要把所有字段都返回给前端只返回表格需要的字段就好既减少流量也减少敏感数据泄露的风险。easyadmin在每个模型里都定义了hidden属性TP6会在序列化时自动过滤掉这些字段。6. easyadmin的后续扩展方向easyadmin目前已经覆盖了登录认证、RBAC权限、菜单管理、用户管理、通用CRUD、文件上传、操作日志和数据字典这些后台基础设施。在这个基础上继续扩展时我建议优先考虑三个方向接口文档自动生成、定时任务接入和消息通知模块。接口文档这块如果团队前后端配合比较多可以引入开源方案通过注解自动生成接口文档。easyadmin的控制器方法统一返回相同数据格式非常容易让文档生成器识别参数和返回结构。定时任务可以用TP6自带的命令行指令把需要周期执行的脚本统一放到一个命令类里。消息通知则负责把操作日志触发的告警、待办提醒发送到企业微信或钉钉群具体接入要看你公司内部用的协作工具。数据报表是每次需求都绕不开的另一个方向。easyadmin目前是业务数据落库后由前端表格直接展示但如果数据量上去建议在MySQL层增加汇总表用定时任务或者业务事件实时更新汇总数据而不是每次查询都用count和sum硬算。汇总表结构、业务统计口径和报表权限绑定这三个问题先想清楚再做报表功能就顺多了。最后分享一个我踩了比较久才明白的点。easyadmin这类项目真正值钱的并不是登录、权限、CRUD这几个功能点而是“确定性和统一性”。只要团队内部把响应格式、权限规则、命名规范这三件事定下来后面新增的每个模块都是同一套写法人换一两个也不会乱。现在我接新需求的时候几乎不讨论后台基础功能怎么做资源全部集中在业务流程上。这个习惯比任何框架选型都重要。
阅读完成 · 觉得有帮助?
咨询建站