同一个大屏页面(两个版本,些微不同)需要根据不同的租户,上一级领导看本级及其下面所有的单位全部内容。下一级租户只能限定看各自单位的内容。
代码逻辑:
1. 身份判断:谁是“领导”,谁是“下一级租户”?
这是所有权限控制的起点。代码通过 computed 属性 isCenterTernat 来完成这一核心判断。
const centerTenatId = ""; // 定义了“上级领导”的租户ID const isCenterTernat = computed(() => { // 从全局用户信息中,判断当前登录用户的租户ID是否是中心ID return userStore.info != null && userStore.info.tenant_id == centerTenatId; })
- 作用:
isCenterTernat成为了一个全局的“开关”或“身份标识”。后续所有的UI显示和逻辑判断,都基于这个true/false值。 - 优势:将身份逻辑集中管理,清晰明了。如果“领导”的ID需要变更,只需修改
centerTenatId这一处即可。
2. UI动态展示:给不同的人看不同的界面
有了身份标识,代码就能根据它来决定显示哪些操作控件,实现了界面的“千人千面”。
<!-- 上级领导看到的UI --> <el-tree-select v-if="isCenterTernat" <!-- 核心:只有领导才显示 --> v-model="selectedBoard" ... /> <!-- 下级租户看到的UI --> <el-cascader v-if="!isCenterTernat && selectedBoard !== '0' && isShow" <!-- 核心:只有非领导才显示 --> ... />
-
上级领导 (
isCenterTernat为true):- 看到一个树形选择器 (
el-tree-select),可以选择“上一级看板”或切换到任意一个“各单位看板”。 - 这给了他查看全局数据的最高权限。
- 看到一个树形选择器 (
-
下级租户 (
isCenterTernat为false):- 树形选择器被隐藏,他们无法切换到别的单位。
- 他们看到的是一个级联选择器 (
el-cascader),只能在自己单位内部选择部门,进行更细粒度的筛选。 - 这限制了他们只能查看自己单位范围内的数据。
3. 数据隔离与请求:确保拿到的数据是正确的
这是最关键的一步,确保界面和后端数据保持一致。代码通过一个核心状态 selectedBoard 来实现。
A. 状态的初始化
在组件挂载前,根据用户身份,自动设置一个默认的数据筛选范围。
1 onBeforeMount(() => { 2 if(userStore.info){ 3 if (userStore.info.tenant_id == centerTenatId){ // 如果是领导 4 selectedBoard.value = "0"; // 默认看“上一级看板”(看全部) 5 } else { // 如果是下级租户 6 selectedBoard.value = userStore.info.tenant_id; // 默认只能看自己的 7 } 8 } 9 })
B. 状态的联动与数据刷新
selectedBoard 变量就像一个“总阀门”,它的变化会触发所有相关数据的重新获取。
watch( () => selectedBoard.value, // 监听“总阀门”的变化 async (newVal) => { // 阀门一变,立刻重置并刷新所有依赖它的数据 selectCorp.value = ""; await initSelectCorp(); // 1. 重新加载法人单位列表 await initSelectOrgId(); // 2. 重新加载部门列表 await initProjectIds(...); // 3. 重新加载项目ID列表 } )
C. 将状态作为参数传递给后端
所有的数据获取函数,都会将 selectedBoard.value 作为 tenantId 参数发送给后端。
1 // 以 initSelectOrgId 为例 2 const initSelectOrgId = async () => { 3 const req = { 4 tenantId: selectedBoard.value, // 核心:把当前选中的租户ID传给后端 5 corpId: selectCorp.value || "", 6 type: createIncomeValue.value === "1" ? 1 : 0, 7 }; 8 const resp = await getQuerySelectProduce(req); 9 // ... 10 };
- 工作流程:
- 领导在
el-tree-select中选择了“单位A”,selectedBoard变为“单位A的ID”。 watch触发,调用initSelectOrgId。- 请求发往后端,参数是
{ tenantId: "单位A的ID", ... }。 - 后端API接收到
tenantId,查询数据库,只返回属于“单位A”的部门列表。 - 前端拿到数据,渲染界面。整个过程实现了数据层面的严格隔离。
- 领导在
总结与借鉴
- 权限判断集中化:通过
isCenterTernat这个computed属性,将复杂的权限逻辑封装成一个简单的布尔值,供全局使用。 - 状态驱动一切:
selectedBoard是整个页面的“单一数据源”和“总开关”。UI展示、数据请求、组件传参都围绕它展开,逻辑非常清晰,易于维护。 - 前后端协作默契:前端负责根据用户操作构建好带有
tenantId的请求,后端负责根据tenantId进行数据过滤。职责分明,共同完成了权限控制。 - 组件化思维:父组件(总指挥)管理所有状态和逻辑,子组件 (
Left,Center,Right) 只负责接收 props 并渲染,实现了高内聚、低耦合。
后端核心代码
1 // 1.获取当前登录用户的租户id 2 user := ginx.GetUser(c) 3 // 2.比对是否为上一级租户 4 // 2.1 如何为上一级租户 则不做处理 5 // 2.2 如果不为上一级心租户 则只能查询自己的 需要比对租户id,一样放行 不一样报错返回 6 if user.TenantID != constants.CenterTenantId { 7 //不是中心租户 8 if user.TenantID != params.TenantID { 9 // 查询的不是自己的数据 无权限 10 ginx.ResError(c, errors.ErrNoPerm) 11 return 12 } 13 }
步骤1:身份识别与参数解析
ginx.ParseJSON(c, ¶ms) // 1. 解析请求体,获取用户“想看什么”
user := ginx.GetUser(c) // 2. 从认证信息中,获取用户“是谁”
步骤2:权限校验(核心关卡)
函数 建立了一个分层的权限模型:
这个逻辑可以翻译成以下决策树:
-
你是“中心领导”(
user.TenantID == constants.CenterTenantId)吗?- 是 -> 直接通过。你有查看所有数据的特权,
params.TenantID是什么无所谓。 - 否 -> 进入下一步检查。
- 是 -> 直接通过。你有查看所有数据的特权,
-
(普通用户)你请求的数据是你自己的吗?(
user.TenantID == params.TenantID)- 是 -> 通过。你可以查看自己的数据。
- 否 -> 拒绝!你试图访问别人的数据,立即返回
ErrNoPerm权限错误。
步骤3:执行业务与返回结果
- 执行业务逻辑:权限验证通过后,才调用真正的业务逻辑层
ToubiaoBll去数据库查询数据。 - 返回结果:将查询到的结果返回给前端。
代码亮点与最佳实践
-
分层架构清晰:
- API层(
GetToubiaoCountInfo)只负责HTTP协议处理和权限校验。 - BLL层(
ToubiaoBll)只负责核心业务逻辑。 - 职责分离,代码易于维护和测试。
- API层(
-
防御性编程:
- 权限前置:在任何业务逻辑执行之前就完成权限校验,避免了无效的数据库查询和资源浪费。
- 不信任前端:后端是权限的最后一道防线,它永远只相信服务器端的认证信息(
user.TenantID),这是安全的基石。
-
权限逻辑的集中管理:
constants.CenterTenantId将“超级租户”这个概念硬编码为一个常量。这样,当需要更换“中心”的标识时,只需修改一处代码即可。
-
明确且专业的错误处理:
- 对于权限问题,返回专门的
ErrNoPerm错误。这让前端调用者可以非常清晰地区分是“请求参数错了”还是“权限不够”,便于进行不同的错误处理和用户提示。
- 对于权限问题,返回专门的
浙公网安备 33010602011771号