多校区多专业的实习平台,权限与数据边界怎么划
掘金 / 博客园版|架构复盘
一套实习管理平台,从一所学校用到多所学校、从单一专业用到全校,最先崩的往往不是性能,而是权限与数据边界。这篇记录我们在这件事上的三次重构。
金句:多租户的难点不是性能,是边界。

一、角色比想象的多
表面上只有学生、教师、管理员三种角色,实际跑起来是这样的:
| 角色 | 关心的数据 | 典型动作 |
|------|-----------|---------|
| 学生 | 自己的岗位、签到、日志、成绩 | 签到、写日志、查成绩 |
| 校内导师 | 自己带的学生(上限 30 人) | 批阅、联系、评分 |
| 企业导师 | 本单位实习生 | 评价、考勤确认 |
| 专业负责人 | 本专业所有批次 | 审核、统计 |
| 教务/实训中心 | 全校批次 | 备案、导出、督查 |
| 校领导 | 汇总指标 | 看板 |
| 主管部门(如接入) | 备案数据 | 只读 |
七类角色,横跨"人—组织—企业"三个维度。用一张 `role` 表加若干 `if` 是撑不住的。
二、初版:角色即权限,撞墙
初版是典型的 RBAC:用户 → 角色 → 菜单权限。
问题很快出现:同一个"校内导师"角色,A 老师带 12 个学生,B 老师带 30 个,他们的数据范围不同,但菜单权限一样。于是只能在接口里写 `if teacher_id == current_user`,散落在几十个查询里。
改一个场景就要改十几处,且容易漏。漏一处就是越权。
金句:权限写在 if 里,越权就藏在 else 里。
三、第二版:引入数据范围(scope)
第二版把权限拆成两层:
1. 功能权限:能不能进这个页面、调这个接口(RBAC)。
2. 数据范围:能看到哪些行(scope)。
scope 用声明式表达,与查询解耦:
```yaml
- role: mentor
resources: [placement, log, alert]
scope: "placement.mentor_id = ${uid}"
- role: major_leader
resources: [placement, log, score]
scope: "placement.major_id in ${my_majors}"
- role: academic
resources: ["*"]
scope: "placement.school_id = ${school_id}"
```
查询层统一注入 scope 条件,业务代码只写"我要什么",不写"我能看什么"。
这一版解决 80% 的问题。剩下的 20% 在跨组织场景:企业导师属于外部账号,只能看本单位实习生,而"本单位"是通过 `enterprise_id` 关联的,不是通过组织树。
四、第三版:把"数据边界"建模成一等公民
第三版做了两件事。
① 统一主体模型。 学生、教师、企业导师都是 `subject`,通过 `subject_relation` 挂到组织、专业、企业、批次上。数据范围不再按"角色"写死,而是按"关系"计算。
② 边界显式化。 每个业务对象带 `tenant_id`(学校)、`org_id`(院系)、`batch_id`(批次)。所有查询强制带 `tenant_id`,由框架层注入,业务代码无法绕过。
```java
// 框架层统一注入,业务层不感知
ScopeContext ctx = ScopeContext.current();
query.eq("tenant_id", ctx.tenantId());
query.apply(ctx.scopeOf("placement"));
```
这样做的收益是:新增一所学校、新增一个专业,不需要改权限代码,只需要建关系。
金句:边界不是画在文档里,是画在查询条件里。
五、审计:越权与误操作都要能查
权限系统只解决"能不能",审计解决"做没做"。我们记录了四类事件:
- 登录与切换身份(含企业导师首次激活)
- 数据导出(谁、哪个批次、多少条)
- 成绩修改(前后值 + 原因)
- 权限变更(谁给谁加了什么 scope)
导出这条最容易被忽略。实习档案一旦导出就脱离系统,必须留底。
金句:导出不是下载,是把数据主权交出去一次。
六、性能上的两个坑
① scope 条件导致索引失效。 早期把 `scope` 拼成字符串条件,导致 `tenant_id` 走了函数、索引用不上。后来改成参数化条件 + 组合索引 `(tenant_id, org_id, batch_id)`,列表页从 1.8s 降到 200ms 级。
② 统计口径重复计算。 全校看板每次都实时聚合签到与日志,数据量上来后拖垮库。改成按小时预聚合到 `stat_hourly`,看板读预聚合表,实时性用"最近 1 小时增量"补齐。
七、小结
多校区多专业的实习平台,架构上真正难的是"边界":谁能看、能看多少、看完留下什么。把 scope 声明化、把 tenant 强制化、把导出审计化,这三步做完,扩展新学校才不需要动代码。
金句:能举证的系统,才配叫管理系统。

浙公网安备 33010602011771号