本文目录导读:

- 方案一:基于“用户-角色-数据权限”的RBAC模型(最常用)
- 方案二:基于“用户级别”的简单控制(适合中小项目)
- 方案三:基于“数据权限表”的细粒度控制(复杂,适合SaaS)
- 方案四:基于“数据标签/分类”的权限控制(适合内容类报表)
- 安全与优化建议
- 总结表格
在PHP项目中控制报表的数据查看范围,核心思路是基于用户角色/权限,在SQL查询层面动态过滤数据,不要在前端通过JS隐藏或控制,因为那样是不安全的。
以下是几种主流的实现方案,按复杂度和适用场景排序:
基于“用户-角色-数据权限”的RBAC模型(最常用)
这是大多数企业级应用的标准做法,用户拥有角色,角色拥有“数据权限范围”。
数据库表结构设计(核心表)
- user (id, name, role_id, dept_id...)
- role (id, name, data_scope) ——
data_scope是权限范围字段 - dept (id, name, parent_id, level...) —— 部门/组织架构表
data_scope字段设计(常见枚举值)
| 值 | 含义 | 描述 |
|---|---|---|
| 1 | 仅本人 | 只能查看自己的数据(如销售自己的订单) |
| 2 | 本部门 | 查看自己所在部门的数据 |
| 3 | 本部门及以下 | 部门树(包含子部门) |
| 4 | 全部 | 管理员/超级角色 |
核心代码逻辑(PHP示例)
<?php
/**
* 获取报表查询的SQL WHERE条件(权限过滤)
* @param string $user_id 当前用户ID
* @param string $table_alias 表别名(如 orders o)
* @return string 返回SQL WHERE条件字符串
*/
function getDataPermissionCondition($user_id, $table_alias = '') {
// 1. 查询当前用户信息及角色
$user = DB::table('user as u')
->leftJoin('role as r', 'u.role_id', '=', 'r.id')
->where('u.id', $user_id)
->first(['u.dept_id', 'r.data_scope']);
if (!$user) return '1=0'; // 用户不存在,无权限
$dept_id = $user->dept_id;
$data_scope = $user->data_scope; // 对应方案二中的角色级别
$alias = $table_alias ? $table_alias . '.' : '';
// 2. 根据scope动态构建条件
switch ($data_scope) {
case 1: // 仅本人
return $alias . 'create_by = ' . $user_id;
case 2: // 本部门
return $alias . 'dept_id = ' . $dept_id;
case 3: // 本部门及子部门
// 获取所有下级部门ID (需要递归函数)
$dept_ids = getDeptAndChildIds($dept_id);
$dept_ids_str = implode(',', $dept_ids);
return $alias . 'dept_id IN (' . $dept_ids_str . ')';
case 4: // 全部
return '1=1'; // 不过滤
default:
return '1=0'; // 无权限
}
}
// 递归获取部门及其子部门ID(假设有dept表)
function getDeptAndChildIds($dept_id) {
$ids = [$dept_id];
$children = DB::table('dept')->where('parent_id', $dept_id)->pluck('id');
foreach ($children as $child) {
$ids = array_merge($ids, getDeptAndChildIds($child));
}
return $ids;
}
在报表查询中使用
<?php
public function getSalesReport($request) {
$user_id = session('user_id');
// 获取权限条件
$permission_where = getDataPermissionCondition($user_id, 'o');
// 拼接完整SQL(使用参数化查询防止注入)
$sql = "SELECT o.order_id, o.amount, u.name as sales_name
FROM orders o
LEFT JOIN user u ON o.sales_id = u.id
WHERE {$permission_where} -- 自动过滤
AND o.create_time BETWEEN ? AND ?
ORDER BY o.create_time DESC";
$data = DB::select($sql, [$request->start, $request->end]);
return response()->json($data);
}
基于“用户级别”的简单控制(适合中小项目)
如果公司组织架构扁平,可以用数字表示访问深度。
设计表
-- user表增加 level 字段 ALTER TABLE `user` ADD `level` TINYINT(1) NOT NULL DEFAULT '0' COMMENT '数据级别: 0=普通, 1=部门主管, 2=总监, 3=超级管理员';
报表查询控制
<?php
// 根据用户级别,生成不同SQL
switch ($user->level) {
case 0: // 普通员工
$where = "create_by = {$user_id}";
break;
case 1: // 部门主管,查看本部门
$where = "dept_id = {$user->dept_id}";
break;
case 2: // 总监,查看本部门及子部门
$dept_ids = getDeptAndChildIds($user->dept_id);
$where = "dept_id IN (" . implode(',', $dept_ids) . ")";
break;
case 3: // 超级管理员
$where = "1=1"; // 查看全部
break;
}
缺点:不够灵活,如果用户既需要本人查看又需要跨部门查看,需要扩展方案。
基于“数据权限表”的细粒度控制(复杂,适合SaaS)
每个数据行可以指定多个用户或角色的可见性。
数据权限表结构
CREATE TABLE `data_permission` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`user_id` int(11) DEFAULT NULL, -- 允许查看的用户(NULL表示所有人)
`role_id` int(11) DEFAULT NULL, -- 允许查看的角色
`dept_id` int(11) DEFAULT NULL, -- 允许查看的部门
`resource_type` varchar(50) NOT NULL, -- 资源类型(如 'order', 'report')
`resource_id` int(11) NOT NULL, -- 资源ID(如订单ID)
PRIMARY KEY (`id`),
KEY `idx_resource` (`resource_type`, `resource_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
查询逻辑
<?php
// 获取当前用户的权限ID集合
$user_permission_ids = DB::table('data_permission')
->where(function($query) use ($user_id, $role_id, $dept_id) {
// 满足任意一个条件即可
$query->where('user_id', $user_id)
->orWhere('role_id', $role_id)
->orWhere('dept_id', $dept_id)
->orWhereNull('user_id'); // 公开数据
})
->where('resource_type', 'report') // 仅报表
->pluck('resource_id');
// 报表查询
$sql = "SELECT * FROM reports WHERE id IN (" . implode(',', $user_permission_ids) . ")";
缺点:数据量大时,权限表可能迅速膨胀,通常需要结合缓存。
基于“数据标签/分类”的权限控制(适合内容类报表)
将报表关联到分类(如区域、产品线),用户角色关联到分类。
关联表
-- 报表分类表
CREATE TABLE `report_category` (
`id` int(11) NOT NULL,
`name` varchar(100) NOT NULL,
PRIMARY KEY (`id`)
);
-- 用户可访问的分类
CREATE TABLE `user_category` (
`user_id` int(11) NOT NULL,
`category_id` int(11) NOT NULL,
PRIMARY KEY (`user_id`, `category_id`)
);
-- 报表关联分类
ALTER TABLE `report` ADD `category_id` int(11) DEFAULT NULL;
查询逻辑
<?php
// 获取用户允许的分类ID
$category_ids = DB::table('user_category')
->where('user_id', $user_id)
->pluck('category_id')
->toArray();
if (empty($category_ids)) {
return []; // 无权限
}
$sql = "SELECT * FROM report WHERE category_id IN (" . implode(',', $category_ids) . ")";
安全与优化建议
- 永远不要在前端控制可见性:前端JS控制的可见性可以被绕过,必须由后端SQL过滤。
- 使用参数化查询:即使拼接字符串,也要确保
$dept_ids是数字,或使用ORM的whereIn。永远不要直接拼接SQL字符串中的用户输入。 - 缓存部门树:如果部门层级很深,每次递归查询数据库效率低,建议将部门树缓存到Redis或内存中。
- 权限粒度选择:
- 行级权限(能看哪些订单):用方案一/三/四。
- 列级权限(能看到报表的哪些字段):需要额外的字段过滤逻辑(如根据角色返回不同字段集合)。
- 统一入口:建议编写一个 查询构建器(Query Builder) 或 中间件,在报表查询前自动注入权限条件,避免每个Controller都写重复代码。
总结表格
| 项目 | 方案一(RBAC) | 方案二(级别) | 方案三(权限表) | 方案四(标签) |
|---|---|---|---|---|
| 复杂度 | 中 | 低 | 高 | 中 |
| 灵活性 | 高 | 低 | 非常高 | 中 |
| 性能 | 好 | 最好(无子查询) | 差(当数据量大时) | 好 |
| 适用场景 | 大多数企业后台 | 初创/简单项目 | SaaS、数据隔离 | 内容管理系统 |
推荐选择:绝大多数项目从 方案一(RBAC + 部门) 开始,结构清晰,可维护性好,如果后续需要更细粒度的控制(如指定某些报表仅部分人可见),再升级到方案三。