Files
leaudit-platform-backend/docs/权限与地区隔离/用户与地区权限完整设计方案.md
T
2026-05-09 20:04:08 +08:00

813 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 用户与地区权限完整设计方案
> 本文档为 `leaudit-platform` 当前阶段的正式权限设计稿,已经按实际业务收口。
> 核心结论:**只做单地区归属 + 角色权限 + 数据范围隔离**,不再做多地区用户模型,不再通过前端端口推断地区。
---
## 1. 这份设计最终解决什么问题
当前新系统要解决的,不是一个抽象的“大而全权限平台”,而是一个非常明确的业务问题:
- 用户登录后,系统知道“这个人是谁”
- 系统知道“这个人属于哪个地区”
- 系统知道“这个人是什么角色”
- 接口查询时,系统自动限制他能看到哪些数据
- 前端菜单、按钮、接口能力,按角色和权限点控制
也就是说,新系统最终采用的是:
- `RBAC + 单地区数据隔离`
而不是:
- 多地区用户模型
- 自定义地区集合授权
- 通过前端端口识别地区
- 复杂租户树 / 组织树权限
---
## 2. 先说最终结论
### 2.1 当前真实业务模型
当前业务可以收敛为下面四件事:
1. 一个用户只挂一个主地区
2. 数据隔离主要按这个地区做
3. 角色只有少量几类,不需要发明新角色
4. 权限判断分两层:
- 功能权限:能不能访问某接口 / 菜单 / 动作
- 数据权限:即使能访问,也不一定能看所有地区的数据
### 2.2 当前应保留的角色
按你最新确认,当前只保留下面这几类业务角色:
- `provincial_admin`
- `admin`
- `common`
技术上可以保留一个可选的:
- `super_admin`
但它只建议作为系统级运维 / 超级初始化账号使用,不建议作为日常业务角色依赖。
### 2.3 明确不要的东西
这次设计里,明确不做下面这些复杂化模型:
- `city_admin`
- `review_manager`
- `review_user`
- `rule_admin`
- `home_region_code`
- `accessible_regions`
- `user_regions`
- `user_region_scope`
- `CUSTOM_REGIONS`
- 前端端口映射地区
- 一个用户多地区授权集合
这些概念要么不是当前真实业务需要,要么会把系统越做越重。
---
## 3. 老系统真实逻辑,应该继承什么
结合对老项目 `/home/wren-dev/Porject/docauditai` 的代码分析,老系统的真实权限结构不是“只有 6 张表”,而是:
- 用户主表:`sso_users`
- 角色表:`roles`
- 用户角色关系:`user_role`
- 路由菜单:`sys_routes`
- 角色路由关系:`role_route`
- 权限点:`permissions`
- 角色权限点关系:`role_permissions`
- 数据范围:`roles.data_scope``role_permissions.data_scope`
- 用户地区:`sso_users.area`
所以新系统最合理的做法不是推翻重来,而是:
- 继承老系统的 `RBAC + area 数据隔离`
- 去掉老系统里不够清晰、靠约定隐式生效的部分
- 在新系统里把“地区”“角色”“权限点”“数据范围”写清楚
### 3.1 老系统真正值得继承的 4 个点
1. 登录入口虽然支持 OAuth 和账密两种模式,但最终都统一落到:
- `sso_users`
- `user_role`
- `roles`
- JWT
2. 菜单权限和动作权限始终分层:
- 菜单 / 页面:`sys_routes + role_route`
- 接口 / 动作:`permissions + role_permissions`
3. 数据范围虽然分散,但真实主轴一直是:
- `sso_users.area`
- 业务表 `area/region`
- `data_scope`
4. 老系统本质上就是:
- `RBAC + area-based data scope`
也就是说,新系统不需要重新发明一个“全新权限平台”,而是要把这条老主线收口得更清楚。
### 3.2 老系统不再继续继承的部分
这些老逻辑在新系统里不建议继续保留:
- 靠前端端口、部署环境、隐式约定推断地区
- 多套登录入口分别维护不同用户模型
- 模糊的多地区授权集合
- 没写清楚边界的隐藏 data scope 规则
新系统应改成:
- 一个用户一个主地区
- 一个统一用户主表
- 一套明确 RBAC 结构
- 一套明确的数据范围规则
---
## 4. 新系统最终权限模型
## 4.1 总体结构
新系统建议正式采用下面这套结构:
- `sso_users`:用户主表
- `roles`:角色表
- `user_role`:用户角色关系
- `sys_routes`:菜单 / 路由定义
- `role_route`:角色菜单关系
- `permissions`:接口 / 动作权限点
- `role_permissions`:角色权限点关系
数据隔离不单独建“用户地区关系表”,而是直接依赖:
- `sso_users.area`
- 业务表中的 `region`
### 4.2 权限判断分层
每次请求都分两步:
#### 第一步:功能权限判断
判断用户有没有权访问:
- 某个页面
- 某个接口
- 某个动作
这个由:
- `roles`
- `role_route`
- `role_permissions`
决定。
#### 第二步:数据范围判断
即使用户有接口权限,也要再判断他能看哪些数据。
这个由:
- 用户角色对应的 `data_scope`
- 用户自身的 `sso_users.area`
- 业务数据的 `region`
共同决定。
---
## 5. 地区模型正式收口
## 5.1 用户地区字段只保留一个主字段
当前阶段,用户只保留一个地区归属字段:
- `sso_users.area`
它的语义正式定义为:
- 用户主归属地区
- 用户默认数据隔离地区
- 非省级角色的数据过滤依据
它**不再表示**
- 前端访问入口地区
- 临时切换地区
- 多地区集合
### 5.2 业务数据地区字段
业务表保留自己的地区字段,例如:
- `leaudit_documents.region`
- `leaudit_runs.region`(如果后续补)
- `leaudit_rule_bindings.region`
这些字段的语义是:
- 该业务数据归属哪个地区
### 5.3 地区过滤规则
- 如果角色数据范围是 `ALL`,则不按地区过滤
- 如果角色数据范围是 `DEPT`,则只能看 `region = sso_users.area` 的数据
- 如果角色数据范围是 `SELF`,则只能看自己创建 / 上传 / 归属自己的数据
这里的 `DEPT` 虽然沿用老系统命名,但在新系统里应明确理解为:
- **同地区**
- 不是传统组织架构里的“同部门”
也就是说:
- `DEPT = same area`
---
## 6. 角色定义
## 6.1 `provincial_admin`
建议语义:
- 省级管理员
- 可看全省数据
- 可管理地区级业务配置
- 可查看全局文档、任务、评查结果
建议默认数据范围:
- `ALL`
## 6.2 `admin`
建议语义:
- 地区管理员
- 只能看自己地区数据
- 可管理本地区用户日常业务、文档、规则绑定、评查结果
建议默认数据范围:
- `DEPT`
## 6.3 `common`
建议语义:
- 普通业务用户
- 通常只能处理自己提交或自己负责的数据
- 也可以视业务需要,局部开放为“看本地区”
建议默认数据范围:
- 默认 `SELF`
- 某些只读类权限点可单独放宽为 `DEPT`
## 6.4 `super_admin`(可选)
建议语义:
- 系统超级管理员 / 初始化管理员
- 不参与日常地区业务流程
- 仅用于系统维护、初始化、紧急排障
建议默认数据范围:
- `ALL`
---
## 7. 数据范围模型
## 7.1 建议保留三种 scope
为了兼容老系统思路,并保持简单,正式只保留三种:
- `ALL`
- `DEPT`
- `SELF`
### 7.2 含义定义
#### `ALL`
- 查看全部数据
- 不做地区过滤
#### `DEPT`
- 实际表示“同地区”
- 查询自动加条件:`region = 当前用户.area`
#### `SELF`
- 只能查看自己的数据
- 查询自动加条件,例如:
- `created_by = 当前用户.id`
-`uploaded_by = 当前用户.id`
-`owner_id = 当前用户.id`
### 7.3 推荐生效顺序
如果一个用户有多个角色、多条权限记录,建议按以下规则收敛:
1. 先确认功能权限是否命中
2. 再收集该权限点对应的所有 `data_scope`
3. 取范围最大的那个作为最终可见范围
范围大小建议定义为:
- `ALL > DEPT > SELF`
例如:
- 用户既是 `common`,又临时带一个 `admin` 角色
- 对某个接口命中了两条权限:`SELF``DEPT`
- 那最终按 `DEPT` 生效
---
## 8. 建议保留 / 改造的表结构
以下不是要求把库推倒重建,而是“新系统正式语义定义”。
## 8.1 `sso_users`
建议核心字段至少明确为:
```sql
CREATE TABLE sso_users (
id BIGSERIAL PRIMARY KEY,
sub VARCHAR(128) NOT NULL UNIQUE,
username VARCHAR(128),
nick_name VARCHAR(128),
email VARCHAR(256),
phone_number VARCHAR(64),
password_hash VARCHAR(255),
status SMALLINT NOT NULL DEFAULT 0,
is_leader BOOLEAN NOT NULL DEFAULT FALSE,
area VARCHAR(64),
tenant_id BIGINT NULL,
department_id BIGINT NULL,
source_type VARCHAR(32) NOT NULL DEFAULT 'oauth',
source_payload JSONB NULL,
try_count INT NOT NULL DEFAULT 0,
try_login_time TIMESTAMPTZ NULL,
last_login_at TIMESTAMPTZ NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
deleted_at TIMESTAMPTZ NULL
);
```
### `sso_users` 字段解释
- `sub`:统一认证唯一标识,OAuth 用户核心身份键
- `username`:登录名 / 工号 / 本地账号名
- `nick_name`:姓名
- `area`:用户归属地区,当前阶段最关键业务字段
- `source_type``oauth` / `local` / `imported`
- `source_payload`:保留外部登录原始字段快照
- `status`:是否启用
### 关键约束
- `area` 必须是后台可信字段
- `area` 不能再由前端端口决定
- OAuth 登录后,如果需要更新地区,也必须来自后端组织同步或后台管理维护
## 8.2 `roles`
```sql
CREATE TABLE roles (
id BIGSERIAL PRIMARY KEY,
role_name VARCHAR(64) NOT NULL UNIQUE,
description TEXT,
data_scope VARCHAR(16) NOT NULL DEFAULT 'SELF',
status SMALLINT NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
```
建议初始化:
```sql
INSERT INTO roles (role_name, description, data_scope) VALUES
('provincial_admin', '省级管理员', 'ALL'),
('admin', '地区管理员', 'DEPT'),
('common', '普通用户', 'SELF'),
('super_admin', '系统超级管理员', 'ALL');
```
## 8.3 `user_role`
```sql
CREATE TABLE user_role (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL REFERENCES sso_users(id),
role_id BIGINT NOT NULL REFERENCES roles(id),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE (user_id, role_id)
);
```
## 8.4 `sys_routes`
```sql
CREATE TABLE sys_routes (
id BIGSERIAL PRIMARY KEY,
path VARCHAR(255) NOT NULL,
name VARCHAR(128) NOT NULL,
component VARCHAR(255),
parent_id BIGINT NULL,
sort_order INT NOT NULL DEFAULT 0,
visible BOOLEAN NOT NULL DEFAULT TRUE,
enabled BOOLEAN NOT NULL DEFAULT TRUE,
meta JSONB NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
```
## 8.5 `role_route`
```sql
CREATE TABLE role_route (
id BIGSERIAL PRIMARY KEY,
role_id BIGINT NOT NULL REFERENCES roles(id),
route_id BIGINT NOT NULL REFERENCES sys_routes(id),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE (role_id, route_id)
);
```
## 8.6 `permissions`
```sql
CREATE TABLE permissions (
id BIGSERIAL PRIMARY KEY,
permission_code VARCHAR(128) NOT NULL UNIQUE,
permission_name VARCHAR(128) NOT NULL,
resource_type VARCHAR(64) NOT NULL,
method VARCHAR(16) NULL,
path VARCHAR(255) NULL,
description TEXT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
```
建议权限编码风格:
- `documents.list`
- `documents.upload`
- `documents.detail`
- `documents.delete`
- `rules.binding.manage`
- `users.manage`
## 8.7 `role_permissions`
```sql
CREATE TABLE role_permissions (
id BIGSERIAL PRIMARY KEY,
role_id BIGINT NOT NULL REFERENCES roles(id),
permission_id BIGINT NOT NULL REFERENCES permissions(id),
data_scope VARCHAR(16) NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE (role_id, permission_id)
);
```
这里的 `data_scope` 允许覆盖 `roles.data_scope`
推荐生效规则:
- 优先取 `role_permissions.data_scope`
- 如果为空,再回退到 `roles.data_scope`
这样可以实现:
- 角色整体是 `SELF`
- 但对某个只读接口单独放宽为 `DEPT`
---
## 9. JWT 与登录态设计
## 9.1 JWT 应携带什么
JWT 不要继续塞一堆不稳定字段,但至少应携带:
```json
{
"user_id": 1001,
"sub": "oauth-sub-xxx",
"username": "zhangsan",
"nick_name": "张三",
"area": "潮州",
"roles": ["admin"],
"permissions": ["documents.list", "documents.upload"],
"is_super_admin": false,
"exp": 1770000000
}
```
### 核心要求
- `area` 要进 JWT,便于接口层直接做地区过滤
- `roles` 要进 JWT,便于快速做角色识别
- `permissions` 可按系统实现选择是否进 token
- 若放入 JWT,接口判断更快
- 若不放入 JWT,则请求时查库
## 9.2 登录来源
统一登录支持:
- OAuth 登录
- 账号密码登录
但两种登录最后都必须落到同一套用户主数据:
- `sso_users`
- `user_role`
- `roles`
- `area`
### 明确禁止
禁止再按下面方式确定地区:
- 前端端口
- 前端域名
- 页面参数
- 浏览器本地缓存里的地区值
---
## 10. 接口契约建议
下面给的是新系统正式推荐接口契约。
## 10.1 登录返回
### `POST /api/auth/login`
#### 返回示例
```json
{
"code": 200,
"message": "ok",
"data": {
"accessToken": "jwt-token",
"tokenType": "bearer",
"expiresIn": 7200,
"userInfo": {
"userId": 1001,
"sub": "oauth-sub-xxx",
"username": "zhangsan",
"nickName": "张三",
"area": "潮州",
"roles": ["admin"],
"permissions": [
"documents.list",
"documents.upload",
"documents.detail"
]
}
}
}
```
## 10.2 当前用户信息
### `GET /api/auth/me`
#### 返回示例
```json
{
"code": 200,
"message": "ok",
"data": {
"userId": 1001,
"username": "zhangsan",
"nickName": "张三",
"area": "潮州",
"roles": ["admin"],
"permissions": ["documents.list", "documents.upload"]
}
}
```
## 10.3 用户列表
### `GET /api/users/list`
查询规则:
- `provincial_admin` / `super_admin`:可查全部用户
- `admin`:只能看本地区用户
- `common`:通常不开放
#### 建议查询条件
- `username`
- `nickName`
- `area`
- `roleName`
- `status`
## 10.4 文档列表
### `GET /api/documents/list`
查询规则:
- `ALL`:不加地区过滤
- `DEPT`:自动加 `region = 当前用户.area`
- `SELF`:自动加 `uploaded_by = 当前用户.id`
**注意:前端传了 `region` 也不能绕过后端数据权限。**
---
## 11. 后端权限判定顺序
建议每个受保护接口按下面顺序执行。
### 11.1 第一步:解析 JWT
拿到:
- `user_id`
- `area`
- `roles`
- `permissions`
### 11.2 第二步:校验功能权限
例如访问:
- `/api/documents/list`
要求权限码:
- `documents.list`
如果没有这个权限,直接返回 403。
### 11.3 第三步:确定最终数据范围
按顺序取值:
1. 当前权限点对应的 `role_permissions.data_scope`
2. 角色默认 `roles.data_scope`
多角色时,取范围最大的那个。
### 11.4 第四步:注入数据过滤条件
例如文档列表:
- `ALL` → 不加过滤
- `DEPT``WHERE d.region = :current_user_area`
- `SELF``WHERE d.uploaded_by = :current_user_id`
### 11.5 第五步:执行查询
这样做可以避免:
- 前端绕过地区限制
- 前端伪造地区参数
- 页面隐藏了但接口仍可越权访问
---
## 12. 与文档业务表的关系
## 12.1 为什么地区过滤只靠 `sso_users.area`
因为你已经确认,当前业务不是“一个用户可看多个地区”。
所以没必要引入:
- 用户地区关系表
- 多地区授权集合
- 区域切换态
当前最稳的做法就是:
- 用户一个 `area`
- 文档一个 `region`
- 查询时直接比对
## 12.2 文档上传时怎么处理地区
上传时建议规则:
- 如果前端未传 `region`,则默认取当前用户 `area`
- 如果前端传了 `region`
- `provincial_admin` 可指定任意合法地区
- `admin` 只能传与自己 `area` 相同的值
- `common` 通常不允许跨地区指定
这样能保证:
- 数据归属清晰
- 省级管理员可代地区上传
- 地区管理员不能越区写数据
---
## 13. 现在不建议做的事
以下内容当前都不建议提前上:
- 多地区切换
- 自定义用户地区集合
- 按前端入口决定地区
- 复杂组织树联动到每个业务接口
- 为每个业务再单独造一套权限表
- 设计很多当前不存在的角色
原因很简单:
- 真实业务还没要求到这一步
- 这些模型一旦上了,后续维护复杂度会明显增加
- 现阶段最重要的是把“登录 -> 用户 -> 地区 -> 角色 -> 数据过滤”这条链路先做稳定
---
## 14. 推荐落地顺序
## 14.1 第一阶段:先把模型定义收口
确认并冻结:
- 用户地区字段就是 `sso_users.area`
- 角色就是 `provincial_admin / admin / common`
- 数据范围就是 `ALL / DEPT / SELF`
## 14.2 第二阶段:统一登录返回结构
确保 OAuth 登录、账密登录都统一返回:
- 用户基本信息
- `area`
- `roles`
- `permissions`
## 14.3 第三阶段:把接口权限检查和数据范围注入补齐
优先补以下接口:
- `/api/auth/me`
- `/api/documents/list`
- `/api/documents/detail`
- `/api/documents/upload`
- `/api/users/list`
## 14.4 第四阶段:补文档与前端说明
把以下内容写成开发规范:
- 哪些接口需要什么权限码
- 哪些接口如何按 `area` 过滤
- 前端不允许自己决定地区
- 上传 / 列表 / 详情的地区行为规则
---
## 15. 最终版设计结论
新系统最终建议正式采用:
- `RBAC + 单地区隔离`
具体落地为:
- 用户主表核心字段:`sso_users.area`
- 角色:`provincial_admin / admin / common`,可选 `super_admin`
- 数据范围:`ALL / DEPT / SELF`
- 地区过滤:按 `业务表.region` 对比 `sso_users.area`
- 功能权限:`roles + role_route + permissions + role_permissions`
- 不再做前端端口识别地区
- 不再做多地区用户模型
这是当前最符合你们真实业务、最接近老系统有效逻辑、同时又足够轻量可落地的一版方案。