本文目录导读:

在PHP项目中实现部门权限,通常有几种主流的设计模式,选择哪一种取决于项目的复杂度和团队协作要求。
下面从简单到复杂提供三种方案,并附带核心代码示例和数据库设计思路。
核心概念
在实现之前,需要理解两个核心概念:
- 数据隔离:同一个数据表(如订单、文档)中,不同部门只看到本部门的行。
- 功能隔离:不同部门能访问的菜单、操作按钮不同(如财务部能看到“导出薪资”按钮,技术部看不到)。
基于部门ID的简单隔离(适合小型项目或API)
这是最直接的方式,在用户登录时记录 department_id,查询数据时自动附加条件。
数据库设计:
-- 用户表 CREATE TABLE `users` ( `id` int(11) NOT NULL AUTO_INCREMENT, `name` varchar(50) DEFAULT NULL, `department_id` int(11) DEFAULT NULL, `role_id` int(11) DEFAULT NULL, -- 角色ID PRIMARY KEY (`id`) ) ENGINE=InnoDB; -- 部门表 CREATE TABLE `departments` ( `id` int(11) NOT NULL AUTO_INCREMENT, `name` varchar(50) DEFAULT NULL, `parent_id` int(11) DEFAULT NULL, -- 支持层级 PRIMARY KEY (`id`) ) ENGINE=InnoDB; -- 业务数据表(以订单为例) CREATE TABLE `orders` ( `id` int(11) NOT NULL AUTO_INCREMENT, `order_no` varchar(50) DEFAULT NULL, `department_id` int(11) DEFAULT NULL, -- 记录归属部门 `created_by` int(11) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB;
PHP代码实现(ThinkPHP/Laravel风格):
// 用户登录后将部门信息存入Session或全局上下文
$_SESSION['auth'] = [
'user_id' => 1,
'department_id' => 10,
'is_admin' => false
];
// 查询数据时自动过滤
class OrderService {
public function getOrderList($params) {
$currentDept = $_SESSION['auth']['department_id'];
// 如果是普通用户,只能看到本部门数据
$query = DB::table('orders')
->where('department_id', $currentDept);
// 管理员可以跨部门查看(可根据需求增加参数)
if ($_SESSION['auth']['is_admin']) {
$query = DB::table('orders'); // 取消部门限制
}
return $query->get();
}
}
优点:实现简单,适合数据量不大的内部系统。 缺点:难以实现跨部门协作、数据共享、复杂树形部门继承。
RBAC + 部门权限过滤器(适合中大型项目)
在经典RBAC(角色-用户-权限)基础上,增加部门范围(Scope) 概念。
核心思想:用户有一个角色,角色有权限,但查询数据时,需要根据部门参数决定数据范围(仅本人、本部门、本部门及子部门、全公司)。
数据库设计(扩展版):
-- 用户与部门关联
CREATE TABLE `user_department` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`user_id` int(11) DEFAULT NULL,
`department_id` int(11) DEFAULT NULL,
`is_primary` tinyint(1) DEFAULT 1, -- 是否主部门,一个人可能属于多个部门
PRIMARY KEY (`id`)
) ENGINE=InnoDB;
-- 权限资源表(增加部门范围字段)
CREATE TABLE `permissions` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(100) DEFAULT NULL, -- 'order.view' / 'order.edit'
`scope` enum('self', 'dept', 'all') DEFAULT 'self', -- 部门范围限制
PRIMARY KEY (`id`)
) ENGINE=InnoDB;
-- 角色-权限关联(略)
-- 用户-角色关联(略)
PHP中间件实现(Laravel Middleware示例):
class DepartmentScopeMiddleware
{
public function handle($request, Closure $next)
{
$user = Auth::user();
$permission = $request->route()->getAction()['permission'];
// 查询用户对该权限的scope
$scope = $user->getPermissionScope($permission); // 返回 'self', 'dept', 'all'
// 将scope注入到全局(或通过依赖注入)
app()->instance('current_scope', $scope);
app()->instance('user_department_id', $user->department_id);
return $next($request);
}
}
// 模型查询时自动应用
class Order extends Model
{
public function scopeAllowed(Builder $query)
{
$scope = app('current_scope');
$userDept = app('user_department_id');
if ($scope === 'self') {
return $query->where('created_by', Auth::id());
} elseif ($scope === 'dept') {
// 支持子部门可见:获取本部门及所有子部门ID
$deptIds = Department::getAllChildrenIds($userDept);
array_push($deptIds, $userDept);
return $query->whereIn('department_id', $deptIds);
}
// scope === 'all' 不限制
return $query;
}
}
// 控制器中使用
Order::allowed()->where('status', 1)->get();
优点:粒度细,可配置性强,支持子部门可见性。
缺点:增加了权限设计复杂度,需要维护scope配置。
数据级权限 + 策略(适合数据量大、部门层级深的SaaS)
使用数据权限策略 配合部门树 实现,这是企业级系统最常用的方案。
核心思想:
- 建一个部门树表,包含
lft,rgt(左右值)或path字段,方便快速查询所有子部门。 - 建一个数据权限策略表,定义“用户A可以看哪些部门的哪些数据”。
高级数据库设计:
-- 部门树(闭包表方式,方便查询) CREATE TABLE `department_closures` ( `ancestor` int(11) NOT NULL, `descendant` int(11) NOT NULL, `depth` int(11) NOT NULL DEFAULT 0, PRIMARY KEY (`ancestor`, `descendant`) ); -- 数据权限策略表 CREATE TABLE `data_permissions` ( `id` int(11) NOT NULL AUTO_INCREMENT, `user_id` int(11) NOT NULL, `target_type` varchar(50) NOT NULL, -- 'order' / 'document' / 'customer' `department_id` int(11) DEFAULT NULL, -- 可访问的部门ID,NULL表示所有 `allow` tinyint(1) NOT NULL DEFAULT 1, -- 允许/禁止 PRIMARY KEY (`id`), KEY `idx_user_target` (`user_id`, `target_type`) ) ENGINE=InnoDB;
核心查询算法:
class DataPermissionService
{
// 获取用户对某个实体类型可访问的部门ID列表
public function getAccessibleDeptIds($userId, $targetType)
{
// 1. 获取该用户的所有数据权限
$permissions = DataPermission::where('user_id', $userId)
->where('target_type', $targetType)
->get();
$deptIds = [];
foreach ($permissions as $perm) {
if ($perm->department_id === null) {
// 允许所有部门(管理员)
return Department::pluck('id')->toArray();
}
// 如果是允许,则计算该部门及其所有子部门
if ($perm->allow) {
$children = Department::getAllDescendantIds($perm->department_id);
array_push($children, $perm->department_id);
$deptIds = array_merge($deptIds, $children);
} else {
// 拒绝权限:从列表中移除这些部门
$rejectedDepts = Department::getAllDescendantIds($perm->department_id);
$deptIds = array_diff($deptIds, $rejectedDepts);
}
}
return array_unique($deptIds);
}
}
// 查询时使用:
$deptIds = (new DataPermissionService)->getAccessibleDeptIds($user->id, 'order');
$orders = Order::whereIn('department_id', $deptIds)->get();
优点:灵活度最高,支持白名单/黑名单、继承、精确到单个用户和单个部门。 缺点:查询性能依赖索引和部门树设计,需要额外维护闭包表。
| 方案 | 适用场景 | 实现难度 | 性能 | 灵活性 |
|---|---|---|---|---|
| 部门ID简单隔离 | 内部管理系统、用户少、部门层级不深 | |||
| RBAC+Scope | 通用后台管理、角色清晰、需要细粒度 | |||
| 策略表+闭包表 | SaaS系统、多租户、复杂继承和拒绝权限 |
关键经验点
- 不要重复计算:每次查询都去计算一次部门树很慢,使用闭包表(方案三)或Redis缓存部门树路径。
- 前端辅助:后台部门选择器建议使用树形下拉(基于
parent_id递归生成),减少前端交互错误。 - 测试数据:务必测试“一个人属于多个部门”的情况,以及“子部门继承父部门权限”的场景,这是最容易出bug的地方。
- 日志审计:所有涉及“跨部门查看数据”的操作(如管理员强制查看其他部门),建议记录操作日志,方便追责。
如果你的项目是新项目且部门层级不超过3层,建议从“方案一”快速启动,随着业务增长再渐进重构为“方案二”,避免一开始就过度设计。