怎样在PHP项目中实现部门权限?

wen java案例 2

本文目录导读:

怎样在PHP项目中实现部门权限?

  1. 核心概念
  2. 方案一:基于部门ID的简单隔离(适合小型项目或API)
  3. 方案二:RBAC + 部门权限过滤器(适合中大型项目)
  4. 方案三:数据级权限 + 策略(适合数据量大、部门层级深的SaaS)
  5. 关键经验点

在PHP项目中实现部门权限,通常有几种主流的设计模式,选择哪一种取决于项目的复杂度和团队协作要求。

下面从简单到复杂提供三种方案,并附带核心代码示例和数据库设计思路。

核心概念

在实现之前,需要理解两个核心概念:

  1. 数据隔离:同一个数据表(如订单、文档)中,不同部门只看到本部门的行。
  2. 功能隔离:不同部门能访问的菜单、操作按钮不同(如财务部能看到“导出薪资”按钮,技术部看不到)。

基于部门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)

使用数据权限策略 配合部门树 实现,这是企业级系统最常用的方案。

核心思想

  1. 建一个部门树表,包含lftrgt(左右值)或path字段,方便快速查询所有子部门。
  2. 建一个数据权限策略表,定义“用户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系统、多租户、复杂继承和拒绝权限

关键经验点

  1. 不要重复计算:每次查询都去计算一次部门树很慢,使用闭包表(方案三)或Redis缓存部门树路径
  2. 前端辅助:后台部门选择器建议使用树形下拉(基于parent_id递归生成),减少前端交互错误。
  3. 测试数据:务必测试“一个人属于多个部门”的情况,以及“子部门继承父部门权限”的场景,这是最容易出bug的地方。
  4. 日志审计:所有涉及“跨部门查看数据”的操作(如管理员强制查看其他部门),建议记录操作日志,方便追责。

如果你的项目是新项目部门层级不超过3层,建议从“方案一”快速启动,随着业务增长再渐进重构为“方案二”,避免一开始就过度设计。

抱歉,评论功能暂时关闭!