尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

ThinkPHP6后台管理:表驱动CRUD与RBAC权限实战解析

ThinkPHP6后台管理:表驱动CRUD与RBAC权限实战解析 简介这是一套基于ThinkPHP 6开发的后台权限管理系统定位对标Laravel-Admin适合熟悉PHP、希望在工作或学习中快速搭建可复用后台的开发者。系统内置Composer一键安装、丰富的配置项只需连接数据库即可自动生成增删改查同步产出菜单与权限把管理员、角色、权限、菜单、应用管理等基础模块固化下来显著降低重复编码成本。压缩包共120个文件以93个PHP业务逻辑文件、18个HTML模板文件为主另含Markdown说明、JSON配置、License许可等辅助文件整体仅111KB轻量紧凑。已有2113人学习浏览内容覆盖数据表生成、权限分配、菜单渲染、应用装卸等后台开发关键环节前端基于ElementUI和form-create也保留传统Web开发方式还支持Swoole模式可结合服务发现与API网关组件演化为微服务管理后台。对想复盘ThinkPHP6后台架构、学习RBAC权限设计和模块化开发的中级开发者是一份高性价比的参考实现。1. thinkphp后台管理系统把后台开发从手写变成生成给一个中后台项目新增一张业务表常见流程是先建数据表再复制上一个控制器的代码改表名字段再复制一个表单页面改输入项再去菜单表里插记录最后到角色里把新菜单勾上。这套流程做过几遍就会发现真正在动脑的只有业务规则其余都是机械劳动。thinkphp后台管理系统think-admin做的是把这部分机械劳动标准化基于ThinkPHP 6对标laravel-admin的交互模式接入数据库结构后自动生成增删改查控制器、表单规则、菜单和权限节点一个业务表从建表到可管理就是一条命令的事。它自带管理员、角色、权限、菜单、应用管理支持模块化安装卸载也能在swoole模式下作为微服务管理后台使用。适合正在用ThinkPHP做业务交付的团队也适合想快速拿到一套RBAC底座的个人开发者。2. Composer安装与初始化配置加载顺序和目录定位在包管理层面think-admin按Composer包发布安装动作分三步走composer require 你的包名/think-admin php think admin:publish php think admin:install包名以实际发布到packagist上的为准这里用占位符。admin:publish会把配置、路由、视图和静态资源发布到应用目录admin:install负责执行数据库迁移、写入初始管理员和默认菜单。这套动作和laravel-admin的install流程是同一个思路好处是升级时只更新vendor里的代码业务侧的config和view文件不会被覆盖。依赖要求是PHP 7.4以上、ThinkPHP 6.0以上数据库推荐MySQL 5.7或8.0要跑swoole模式的话还需要装php swoole扩展。如果历史项目还停留在thinkphp 3.2先解决php8环境下的兼容性再谈迁移think-admin的依赖锁定在TP6这条线上。2.1 一键安装后项目里多了哪些东西安装完成后再看项目目录会多出几个固定位置路径作用config/admin.php后台主配置路由前缀、认证守卫、登录失败锁定都在这里route/admin.php后台路由按admin前缀统一注册app/admin/controller后台控制器目录内置CrudBase基类app/admin/form表单规则目录一个业务表对应一个FormRule文件public/admin后台静态资源与入口页面2.1.1 控制器和表单规则目录的分工controller目录放控制器和控制器基类业务表生成的控制器继承CrudBaseform目录放表单规则文件负责把数据库字段翻译成前端组件配置。这套分工意味着改字段展示不用动控制器改业务逻辑不用动表单文件两层解耦。安装包里自带一批视图文件index.html是传统多页模式的后台主页index_app.html是单页模式入口index_old.html是给历史项目兼容用的旧版入口layout_container.html是页面布局容器左侧菜单和顶栏在这一层menu.html、roles.html、users.html分别对应菜单管理、角色管理、管理员管理三个核心页面。看到这组文件基本能确认安装包是完整的后面加业务页面时直接参照menu.html的写法注册进layout_container就行。2.2 配置项从.env到config/admin.php的读取链路ThinkPHP 6的配置加载是分层的。.env里写的是数据库连接、APP_DEBUG这类环境变量config/admin.php里的配置在服务启动时读取前后台通用配置放这里// config/admin.php return [ route [ prefix admin, // 后台访问前缀 deny [admin/passport/login], // 免认证路由 ], auth [ guard admin, // 认证守卫名 expire 7200, // 登录态有效期单位秒 login_fail [ limit 5, // 连续失败次数 lock 900, // 锁定秒数 ], ], ];这里prefix决定了后台URL前缀deny数组放不需要登录就能访问的路由比如登录页本身expire控制登录态过期时间。login_fail这组是我常见的一种加固做法控制同一账号连续登录失败5次后锁定15分钟防止口令被扫。改这些值时要注意如果.env里存在同名配置会以.env的值为准这是TP6的Env覆盖规则排错时先从这一层查起。2.3 初始化管理员账号的安全动作admin:install执行到最后会输出初始管理员账号使用前必须改两样初始密码和绑定的手机号/邮箱。提示默认管理员密码如果保持不变会被字典扫描直接命中登录后第一件事就是改密码再去config/admin.php里确认登录失败锁定是否打开。到这里后台已经能访问了。接下来进入核心部分表驱动CRUD生成是怎么实现的。3. 表驱动CRUD生成字段映射、代码生成与菜单权限落库think-admin和普通后台模板最大的区别是普通模板给你一套空壳业务表还是要自己写控制器think-admin是读表结构直接产出控制器、表单和菜单。主线的核心逻辑集中在生成器里。3.1 生成命令的两种用法# 为user表生成完整CRUD php think admin:make user # 指定表名和所属模块 php think admin:make --tableuser --moduleshop第一条命令扫描默认连接的user表在app/admin/controller下生成User控制器在app/admin/form下生成UserForm规则文件同时写入路由和菜单第二条命令把生成目标推到shop模块适合模块化开发。生成器不会覆盖已存在的控制器重复执行时会先检查目标文件所以调整生成结果时直接改文件不用怕重跑被覆盖。3.2 字段类型到表单组件的映射规则生成器判断字段用什么组件依赖数据库字段类型和字段注释。默认映射关系字段类型表单组件列表展示验证规则intInputNumber数字numbervarcharInput文本max:255textTextarea全文无datetimeDatePicker时间date_formattinyint / booleanSwitch开关标签in:0,1json不自动映射不展示需手动指定这套映射表是最容易出坑的地方。数据库里叫status的tinyint字段会映射成Switch但业务上status可能是0禁用、1启用、2待审核三态Switch只有两态。所以生成器留了一个出口在form目录下覆盖默认规则。生成器读取的是information_schemaMySQL 5.7和8.0没有行为差异但如果你把连接切到PostgreSQL或SQLite映射结果会不一样这块要注意。3.3 控制器基类CrudBase的运行逻辑生成器产出的控制器继承CrudBasenamespace app\admin\controller; class User extends CrudBase { protected string $table user; protected array $fields [name, email, status]; protected array $search [name like]; }CrudBase内置index、add、edit、delete四个动作。index接收分页参数和搜索条件搜索条件由$search决定怎么拼比如name like就是列表页搜索框传name关键字时拼成where name like %关键字%add和edit通过$fields做白名单过滤后写入不在白名单里的字段直接丢弃delete默认走逻辑删除。要增加一个时间范围搜索就在$search里加一条[created_at between]。这样设计的原因是90%的业务表在增删改查结构上是一致的真正不一样的只有搜索条件、字段校验和关联关系把固定流程收敛到基类业务控制器只需要十几行配置。3.4 权限节点怎么自动写进菜单表生成器执行完代码生成后会走一次菜单同步// MenuService 同步当前控制器的节点 $actions [index, add, edit, delete]; foreach ($actions as $action) { $node admin/{$controller}/{$action}; if (!Permission::where(node, $node)-find()) { Permission::create([ node $node, title $action, ]); } }这里有个容易被忽略的点权限节点用的是「控制器/动作」两级不是完整URL。原因是完整URL里带参数用URL匹配权限在参数变化时会误判节点匹配只关心请求到了哪个控制器方法不关心参数。thinkphp历史版本里披露过的多数漏洞集中在路由解析和反序列化入口权限校验用节点而不是URL也能减少一类因路由别名导致的绕过问题。节点命名规则在不同版本里可能是admin/user/index也可能是admin/user:index以生成结果为准中间件和节点表用同一套命名就不会错。3.5 手动覆盖自动映射的场景遇到三态status、json字段、外键关联这些情况在app/admin/form下写一个UserForm.php// app/admin/form/UserForm.php return [ [field name, type Input, title 姓名, validate require|max:255], [field status, type Select, title 状态, options [0 禁用, 1 启用, 2 待审核]], [field role_id, type Select, title 角色, options closure:role_options], ];表单规则文件的格式和form-create的rule是兼容的。options可以写静态数组也可以写closure:方法名去动态取值用户表分配角色时就是从这张表把角色列表拉出来的。自动映射满足不了需求时优先改这里不要动控制器。4. RBAC权限链路与后台管理页面users、roles、menu背后的关系一个后台能不能落地不取决于首页长什么样取决于权限模型撑不撑得住后续扩展。think-admin的管理员管理、角色管理、菜单管理三个页面正好对应RBAC模型的三个入口它们的数据关系是理解整套后台的钥匙。4.1 默认RBAC表结构与多对多关系安装完成后会落一组基础表核心表结构如下实际表名以迁移文件为准表职责关键字段admin_user管理员id, username, password, statusadmin_role角色id, name, statusadmin_menu菜单id, pid, title, href, appadmin_permission权限节点id, node, title, appadmin_user_role用户与角色中间表user_id, role_idadmin_role_menu角色与菜单中间表role_id, menu_idadmin_role_permission角色与权限中间表role_id, permission_id用户和角色是多对多角色和菜单是多对多角色和权限也是多对多。注意这里菜单和权限是两张表菜单表负责展示层权限表负责逻辑层。有的设计会把菜单和权限合成一张表能用但后面做「某角色可见某菜单但不可访问」这类需求时会很别扭。拆开的好处是前端菜单树和接口权限可以分别控制。如果你的版本里这两张表合并了说明作者做了简化实际使用不受影响做数据级权限时会多费些事。4.2 管理员分配角色与角色绑定菜单的代码路径users.html页面的核心操作是给用户分配角色提交到User控制器的assignRole方法axios.post(baseURL /admin/user/assignRole, { user_id: currentUserId, role_ids: roleTree.getCheckedKeys() }).then(res { if (res.code 0) layer.msg(分配成功); });服务端先校验user_id和role_ids的合法性再在事务里重建中间表public function assignRole() { $userId $this-request-post(user_id); $roleIds $this-request-post(role_ids, []); Db::transaction(function () use ($userId, $roleIds) { Db::table(admin_user_role)-where(user_id, $userId)-delete(); foreach ($roleIds as $rid) { Db::table(admin_user_role)-insert([ user_id $userId, role_id $rid, ]); } }); }roles.html页面打开时加载角色列表点击编辑弹出菜单树树里的勾选状态来自admin_role_menu和admin_role_permission两张表的并集提交时同样走先删后插的事务。这个模式的好处是幂等重复提交不会产生重复关联业务上不存在中间态。但这个模式有个边界要注意如果中间表有额外字段比如created_at先删后插会把扩展字段丢光。中间表只存两个外键时随便用有业务字段就得改成逐条对比再增删。4.3 权限中间件如何做节点匹配后台每个请求都会经过权限中间件完整链路是路由解析 → 登录态校验 → 权限节点匹配 → 控制器方法执行。中间件核心代码public function handle($request, \Closure $next) { $user $request-userInfo; // 超管直接放行 if ($user-isSuperAdmin()) { return $next($request); } $node $request-controller() . / . $request-action(); if (!$user-can($node)) { return json([code 403, msg 没有访问权限]); } return $next($request); }can方法从用户-角色-权限三层关系里取当前用户拥有的节点集合再用集合判断当前节点在不在里面。这套匹配看起来简单实际有两个坑一是控制器名和action名的大小写要标准化TP6的路由对大小写不敏感但节点比较是敏感的差一个大小写会直接403二是删除用户时要先清中间表再删主表否则admin_user_role里会留下指向已删除用户的孤儿数据。// 删除用户前先清理中间表 $user-roles()-detach(); $user-delete();如果用模型关联的detachTP6会把中间表记录清理干净如果图省事直接调User::destroy中间表不会自动清理。进入链路时管理员身份已经挂在request上这里直接用$request-userInfo取不要在控制器里按user_id重查一遍用户表一是浪费查询二是可能拿到已经变更的脏数据。4.4 模块化安装卸载时的关联清理应用管理页支持安装一个模块也支持卸掉。安装时会新增一批菜单和权限节点卸载时如果只删业务表不删菜单后台菜单树里就会挂着一堆空链接。卸载器里要做的清理动作-- 清理当前app相关的菜单和权限关联 DELETE FROM admin_role_menu WHERE menu_id IN (SELECT id FROM admin_menu WHERE app shop); DELETE FROM admin_role_permission WHERE permission_id IN (SELECT id FROM admin_permission WHERE app shop); DELETE FROM admin_menu WHERE app shop; DELETE FROM admin_permission WHERE app shop;三个删除顺序不能反先子后父。如果代码里用模型一对多事件做联动清理注意TP6的模型事件只在模型方法调用时触发query builder的delete不会触发模型事件。这一节正好踩到thinkphp关联删除文档里反复强调的点关联删除不是自动的必须显式调用detach或对应的关联方法。5. Swoole常驻内存与前端渲染层的两个实战调整5.1 开启swoole模式后必须处理的三类状态问题要跑swoole模式先确认扩展已经装载php -m | grep swoole。之后以常驻内存方式启动php think swoole:start常驻内存和传统php-fpm最本质的区别是一个进程连续处理多次请求。这会把三类问题暴露出来。控制器里不要用静态属性缓存登录用户信息一个进程内的静态变量会被后续请求复用A用户的数据可能串进B用户的请求facade对象在多个请求之间复用时要避免持有连接型资源长生命周期连接会在进程内存里越攒越多模型事件在swoole下行为正常但事件闭包里不要捕获跨请求的变量。调试时先关掉opcache否则改了代码不生效排查成本很高。作者在项目说明里推荐了一个自带服务注册发现和API网关的php库装上之后可以把think-admin作为微服务管理端注册到网关上后台业务本身不需要改动。5.2 elementui与form-create如何决定页面写法前端保留了传统web模式和单页模式两套入口。index.html是传统模式服务端模板渲染好页面直接输出适合快速迭代index_app.html是单页应用入口配合elementui交互上接近vue3后台管理系统那套桌面端体验。layout_container.html是两者的公共布局容器菜单和顶栏只写一份两个入口共用。form-create是表单单页化的关键生成器在form目录里写的规则文件可以直接转成form-create的render规则{ type: Input, field: name, title: 姓名, validate: [{ required: true, message: 请填写姓名 }] }传统模式下这段json由服务端模板循环渲染成HTML表单单页模式下这段json直接交给form-create渲染成组件。同一份表单规则两个渲染出口所以业务上只维护form目录里的规则文件就够了。比如给订单表加一个支付状态搜索下拉就是在OrderForm规则文件里加一项Select组件指定options映射传统模式和单页模式的列表页会同时出现这个筛选器不用分别维护两套前端逻辑。本文还有配套的精品资源点击获取
返回列表