在后台权限管理系统中,用户往往拥有多层级的资源授权关系。以常见的三级结构为例,D代表最细粒度的操作权限,E代表中间的业务模块,F代表顶层的业务线或组织单元。实际接口设计中,前端需要在进入页面时立刻拿到完整的D与E列表用于渲染树形菜单,而F由于数量可能上万,必须支持分页拉取。如何在一次会话中高效完成这件事,是本文要解决的问题。

一、分级授权数据的存储模型
多数系统采用关系型数据库保存授权记录。我们设计三张核心表:表auth_f存储顶层授权,表auth_e存储模块授权,表auth_d存储操作授权。它们通过外键形成层级引用。用户与授权的绑定关系存放在独立的映射表中,以便支持用户组继承等复杂场景。
下面给出简化后的表结构定义。注意这里只保留与查询相关的字段,真实环境还需加入租户ID、失效时间等控制字段。通过这种结构,我们可以在一次联表查询中拿到某个用户拥有的全部D和E,而F则单独处理。
CREATE TABLE auth_f ( id BIGINT PRIMARY KEY, name VARCHAR(64) NOT NULL, owner_user_id BIGINT NOT NULL ); CREATE TABLE auth_e ( id BIGINT PRIMARY KEY, f_id BIGINT NOT NULL, name VARCHAR(64) NOT NULL ); CREATE TABLE auth_d ( id BIGINT PRIMARY KEY, e_id BIGINT NOT NULL, code VARCHAR(64) NOT NULL ); CREATE TABLE user_auth_map ( user_id BIGINT NOT NULL, f_id BIGINT, e_id BIGINT, d_id BIGINT );
二、D与E列表的批量获取方案
由于D和E的数据量相对可控,通常采用单次查询返回。我们利用用户映射表做入口,通过LEFT JOIN把三层关系拼成扁平结果,再在内存中按f_id、e_id分组。这种方式只需一次数据库往返,避免了循环查询带来的N+1问题。
在代码层面,建议使用MyBatis或类似框架的嵌套结果映射,直接将数据反序列化为树形对象。如果框架不支持自动嵌套,也可以在Service层手动组装。下面的Java示例展示了如何执行查询并组装成层级结构。
public AuthStructure loadDE(Long userId) {
List<RawAuthRow> rows = authMapper.selectUserAuth(userId);
Map<Long, AuthF> fMap = new LinkedHashMap<>();
Map<Long, AuthE> eMap = new LinkedHashMap<>();
for (RawAuthRow r : rows) {
AuthF f = fMap.computeIfAbsent(r.getFId(), id -> new AuthF(id, r.getFName()));
AuthE e = eMap.computeIfAbsent(r.getEId(), id -> new AuthE(id, r.getEName()));
f.getEList().add(e);
e.getDList().add(r.getDCode());
}
return new AuthStructure(new ArrayList<>(fMap.values()));
}
这种方案的优点是延迟低、逻辑直观。缺点在于如果用户跨了大量F和E,单次返回的行数会膨胀。此时可考虑对D列表做按需加载,即首屏只返回E的ID,D通过单独接口按E的ID批量查询。
三、F列表的分页实现
F列表分页不能简单使用LIMIT OFFSET,因为在大数据量下深翻页会导致性能陡降。推荐基于游标(Cursor)或最后主键(Seek Method)分页。我们让前端首次请求不带游标,后续携带上次最后一条记录的ID,查询时使用WHERE id > lastId ORDER BY id LIMIT size。
游标分页要求排序字段具备唯一性且存在索引。若F的展示顺序依赖权重而非主键,可改用(weight, id)的复合游标。以下SQL演示了基于主键的Seek分页,能有效利用B+树索引,避免OFFSET扫描。
SELECT id, name
FROM auth_f
WHERE owner_user_id = #{userId}
AND id > #{lastId}
ORDER BY id ASC
LIMIT #{size};
在接口设计上,F分页应独立成Controller方法,与D、E的加载解耦。前端在渲染完树骨架后,监听滚动事件逐步加载F的更多节点。这样既保证了首屏速度,也控制了单次内存占用。
@GetMapping("/auth/f/page")
public PageResult<AuthF> pageF(@RequestParam Long userId,
@RequestParam(required = false) Long lastId,
@RequestParam int size) {
List<AuthF> list = authMapper.pageFByUser(userId, lastId, size);
Long nextCursor = list.size() < size ? null : list.get(list.size() - 1).getId();
return new PageResult<>(list, nextCursor);
}
四、缓存与性能优化建议
D和E的授权关系变更频率低,适合使用Redis缓存整个结构,缓存Key为用户ID加版本号。当管理员调整授权时,递增版本号即可让旧缓存失效。F列表由于分页独立,可按页缓存,但需注意用户授权变更时的批量清理。
另一个常见优化是冗余用户可访问的F的ID集合到用户画像中。这样分页查询可直接用IN或JOIN缩小范围,不必每次扫描全表。下表对比了三种分页策略在百万级F数据下的表现:
| 分页方式 | 深翻页性能 | 实现复杂度 | 适用场景 |
|---|---|---|---|
| OFFSET分页 | 差,随页数线性退化 | 低 | 数据量小后台 |
| 主键游标 | 稳定O(log n) | 中 | 顺序展示列表 |
| 权重复合游标 | 稳定O(log n) | 高 | 自定义排序大列表 |
综合来看,将D、E做一次性轻量返回,F走游标分页,并辅以合理的缓存,能够在保证开发效率的同时满足高效获取用户分级授权结构的需求。落地时请根据业务实际的数据规模和变更频率微调批大小与缓存时长。