# 老系统 `docauditai` 用户权限架构深度分析

本文档用于系统性梳理老项目：

- `/home/wren-dev/Porject/docauditai`

中的用户、认证、角色、路由、权限、地区、数据范围逻辑。

目标不是简单列表，而是明确回答：

- 老系统真实的用户权限架构是什么
- 它依赖哪些表和哪些业务规则
- 新系统应该继承什么、舍弃什么、升级什么

---

## 1. 结论先行

老系统并不是一个“只有 6 张 RBAC 表”的简单权限系统。

它真实的架构是：

- 统一登录入口：OAuth + 账密登录
- 用户主表：`sso_users`
- 角色体系：`roles + user_role`
- 菜单路由：`sys_routes + role_route`
- 权限点：`permissions + role_permissions`
- 数据权限：`role_permissions.data_scope`
- 地区隔离：大量业务依赖 `sso_users.area`
- 组织同步：登录时通过 MQ / 组织表同步 `area / tenant / dep`

也就是说，老系统本质上是：

- `RBAC + area-based data scope`

而不是纯粹的：

- `用户 - 角色 - 菜单`

---

## 2. 核心表与模块

从代码实际使用看，老系统核心不只是以下 6 张表：

- `sys_routes`
- `sso_users`
- `roles`
- `role_route`
- `role_permissions`
- `user_role`

还必须加上：

- `permissions`
- `route_permission`
- 业务表中的 `area` 字段
- `roles.data_scope`
- `role_permissions.data_scope`

相关代码位置：

- 认证：`app/routes/auth.py`
- JWT：`app/auth/auth.py`
- 路由权限：`app/rbac/route_permission.py`
- 权限检查：`app/rbac/permission_checker_v2.py`
- 数据范围：`app/rbac/data_scope_injector_v2.py`
- RBAC API：`app/routes/v3/rbac.py`

---

## 3. 登录架构

## 3.1 统一登录入口

老系统登录主入口是：

- `POST /auth/login`

代码：

- `app/routes/auth.py`

统一登录入口自动识别两种模式：

- OAuth 登录
- 账号密码登录

识别规则：

- 请求体包含 `userInfo.sub` → OAuth 登录
- 请求体包含 `username + password` → 账密登录

对应代码：

- `app/routes/auth.py:111`
- `app/routes/auth.py:154`
- `app/routes/auth.py:158`

这说明老系统在登录入口设计上已经做到了统一。

---

## 3.2 OAuth 登录逻辑

OAuth 登录处理函数：

- `app/routes/auth.py:_handle_oauth_login`

主要流程：

1. 读取前端传入的 `userInfo`
2. 通过 `sub` 查询 `sso_users`
3. 如果用户不存在，则自动创建用户
4. 如果用户已存在，则更新用户基本资料
5. 查询用户角色
6. 生成 JWT
7. 返回统一登录响应

对应代码位置：

- `app/routes/auth.py:412`
- `app/routes/auth.py:451`
- `app/routes/auth.py:518`
- `app/routes/auth.py:563`
- `app/routes/auth.py:601`
- `app/routes/auth.py:611`

### OAuth 用户创建规则

当用户不存在时：

- 自动插入 `sso_users`
- 自动分配默认角色 `common`

对应代码：

- `app/routes/auth.py:471`
- `app/routes/auth.py:512`

这意味着：

- 老系统支持 OAuth 首登自动建号
- 并且会自动授予最低默认权限

---

## 3.3 账号密码登录逻辑

密码登录处理函数：

- `app/routes/auth.py:_handle_password_login`

主要流程：

1. 根据 `username/sub` 查询 `sso_users`
2. 校验密码
3. 校验状态 / 删除标记
4. 同步组织信息（如果有）
5. 查询角色
6. 生成 JWT
7. 返回统一响应

对应代码：

- `app/routes/auth.py:655`
- `app/routes/auth.py:689`
- `app/routes/auth.py:723`
- `app/routes/auth.py:745`
- `app/routes/auth.py:756`
- `app/routes/auth.py:798`

可以看出，老系统并没有把账密登录和 OAuth 登录做成两套完全不同的人群模型。

它们最终都归并到：

- `sso_users`
- `user_role`
- `roles`
- JWT

这是一个非常重要的架构特征。

---

## 4. JWT 模型

JWT 逻辑在：

- `app/auth/auth.py`

JWT Payload 中实际携带的关键字段有：

- `user_id`
- `username`
- `user_role`
- `permissions`（可选）
- `sub`
- `nick_name`
- `email`
- `phone_number`
- `ou_id`
- `ou_name`
- `is_leader`
- `area`

对应代码：

- `app/auth/auth.py:61`
- `app/auth/auth.py:94`
- `app/auth/auth.py:101`
- `app/auth/auth.py:106`

JWT 解码后，同样会恢复成：

- `User`
- `TokenData`

对应：

- `app/auth/auth.py:24`
- `app/auth/auth.py:46`
- `app/auth/auth.py:119`
- `app/auth/auth.py:171`

### 结论

老系统 JWT 不只是认证 token，它还承担了：

- 用户身份
- 用户角色
- 用户地区
- 组织基础信息

其中 `area` 是最关键的业务字段之一。

---

## 5. 用户地区到底怎么来的

这是老系统最关键的逻辑之一。

### 5.1 不是靠前端端口

老系统的地区不是通过前端访问端口推断。

它主要通过组织主数据反查得到。

### 5.2 OAuth 登录时的地区来源

OAuth 登录会调用：

- `_get_user_org_info_from_mq`

位置：

- `app/routes/auth.py:280`

查询流程：

1. 用 `ou_id + nickname` 查询 `um_personinfo`
2. 获取：
   - `tenant_uuid`
   - `dep_uuid`
   - `org_uuid`
3. 再查：
   - `um_tenant`
   - `um_department`
4. 最终组装：
   - `area`
   - `tenant_name`
   - `dep_name`
   - `dep_short_name`
   - `ou_name`

其中 `area` 的来源是：

- `tenant_short_name`
- 或 `tenant_name`

对应代码：

- `app/routes/auth.py:206`
- `app/routes/auth.py:263`
- `app/routes/auth.py:280`
- `app/routes/auth.py:380`
- `app/routes/auth.py:435`

### 5.3 用户首次创建时

新用户创建时，组织信息会直接写入 `sso_users`：

- `area`
- `tenant_name`
- `dep_name`
- `dep_short_name`
- `ou_name`

对应：

- `app/routes/auth.py:471`
- `app/routes/auth.py:480`
- `app/routes/auth.py:512`

### 5.4 老用户每次登录时

如果用户已存在，每次 OAuth 登录仍会同步这些字段：

- `area`
- `tenant_name`
- `dep_name`
- `dep_short_name`
- `ou_name`

对应：

- `app/routes/auth.py:518`
- `app/routes/auth.py:543`
- `app/routes/auth.py:552`
- `app/routes/auth.py:593`

### 结论

老系统对地区的真实定义是：

- 地区是组织主数据的一部分
- 地区由后端通过 MQ/组织系统同步
- 登录时会把地区同步进 `sso_users.area`
- 之后大量业务依赖这个 `area`

这说明：

- 老系统是“后端主数据决定地区”
- 不是“前端端口决定地区”

---

## 6. 用户主表 `sso_users` 的真实角色

从代码实际用法来看，`sso_users` 不只是登录表。

它承担了：

- 统一身份映射表
- 用户基础档案表
- 地区信息存储表
- 组织信息缓存表
- 登录失败次数 / 锁定状态记录表

常见使用字段：

- `id`
- `sub`
- `username`
- `nick_name`
- `phone_number`
- `email`
- `ou_id`
- `ou_name`
- `is_leader`
- `status`
- `deleted_at`
- `password`
- `try_count`
- `try_login_time`
- `area`
- `tenant_name`
- `dep_name`
- `dep_short_name`

对应代码位置：

- `app/routes/auth.py:451`
- `app/routes/auth.py:689`
- `app/routes/auth.py:914`

### 结论

老系统把 `sso_users` 作为了：

- 用户唯一主表
- 认证与组织信息的汇聚中心

这一点新系统应该继承。

---

## 7. 角色体系是什么

角色相关表：

- `roles`
- `user_role`

### 7.1 用户和角色关系

一个用户可以拥有多个角色。

角色查询通常通过：

- `user_role -> roles`

对应代码：

- `app/routes/auth.py:840`
- `app/routes/auth.py:852`
- `app/routes/auth.py:862`

### 7.2 OAuth 新用户默认角色

新用户默认自动分配：

- `common`

对应代码：

- `app/routes/auth.py:512`

### 7.3 角色本身带数据范围

老系统 `roles` 表本身带：

- `data_scope`

在 RBAC API 中大量出现：

- `RoleService.list_roles`
- `RoleService.get_role`

对应代码：

- `app/services/rbac/role_service.py:98`
- `app/services/rbac/role_service.py:171`
- `app/services/rbac/user_role_service.py:217`

### 结论

老系统角色不只是页面访问角色，它还是：

- 权限角色
- 数据范围角色

也就是说：

- 一个角色同时决定功能权限和数据权限

---

## 8. 菜单 / 路由权限模型

### 8.1 路由表和角色路由表

老系统页面菜单权限主要基于：

- `sys_routes`
- `role_route`

主逻辑在：

- `app/rbac/route_permission.py`

### 8.2 `get_user_routes(user_id)` 的真实行为

流程：

1. 查用户所有角色
2. 用角色查 `role_route`
3. join `sys_routes`
4. 只返回启用路由
5. 组装成树形结构
6. 还会把该路由相关的权限点附在 `permissions` 字段里

对应：

- `app/rbac/route_permission.py:22`
- `app/rbac/route_permission.py:47`
- `app/rbac/route_permission.py:63`
- `app/rbac/route_permission.py:100`
- `app/rbac/route_permission.py:127`

### 8.3 路由不仅是菜单，还带权限上下文

老系统会在返回路由树时，把页面对应的权限点也挂上去。

这意味着前端不是只拿“菜单”，而是拿：

- 菜单结构
- 页面权限上下文

这是很成熟的一种设计。

---

## 9. 功能权限模型

### 9.1 老系统不是只有 `role_permissions`

真正的功能权限核心是：

- `permissions`
- `role_permissions`

其中：

- `permissions`：权限定义表
- `role_permissions`：角色授权表

### 9.2 权限检查器 V2

代码：

- `app/rbac/permission_checker_v2.py`

它的逻辑是：

1. 查用户所有角色
2. 通过 `role_permissions` 找到权限
3. join `permissions`
4. 读 `permission_key`
5. 支持：
   - 精确匹配
   - 通配符权限
   - `GRANT`
   - `DENY`

对应：

- `app/rbac/permission_checker_v2.py:61`
- `app/rbac/permission_checker_v2.py:147`
- `app/rbac/permission_checker_v2.py:199`

### 9.3 权限键格式

权限键采用：

- `module:resource:action`

例如：

- `document:list:read`
- `document:delete:delete`
- `dify:dataset:read`
- `system:rbac:manage`

这套结构非常清晰，推荐新系统继续保留。

### 结论

老系统功能权限的真实设计是：

- `permissions.permission_key` 做标准权限定义
- `role_permissions` 决定角色拥有哪些权限
- 路由权限只是页面层，不能替代 `permissions`

---

## 10. 数据权限模型

这是老系统最关键但最容易被忽略的一层。

### 10.1 数据权限来源

数据权限核心在：

- `role_permissions.data_scope`
- `roles.data_scope`
- 用户 `area`

### 10.2 数据范围注入器

代码：

- `app/rbac/data_scope_injector_v2.py`

定义了三种数据范围：

- `ALL`
- `DEPT`
- `SELF`

对应：

- `app/rbac/data_scope_injector_v2.py:24`

### 10.3 三种范围含义

#### `ALL`

- 查看全部数据
- 不加过滤条件

#### `DEPT`

- 查看本地区数据
- 实现方式是：按 `area` 过滤

#### `SELF`

- 只能查看本人数据
- 实现方式是：按 `user_id` 过滤

对应：

- `app/rbac/data_scope_injector_v2.py:170`
- `app/rbac/data_scope_injector_v2.py:228`

### 10.4 本地区的真实含义

老系统里 `DEPT` 虽然名称叫部门范围，但实际很多地方是：

- 按地区 `area` 过滤

也就是说：

- 它更像“本地市范围”
- 而不是严格的部门树范围

这说明老系统的数据权限其实是：

- `ALL / 地区 / 本人`

而不是严格组织树。

---

## 11. `area` 在老系统中的真实地位

### 11.1 `area` 是核心业务字段

老系统中，`area` 被用于：

- 用户归属地区
- 数据权限过滤条件
- 业务记录写入默认地区
- Dify 知识库选择
- Dify 对话应用过滤
- 评查点地区隔离

### 11.2 Dify 知识库访问

代码：

- `app/routes/v3/dify_area_dataset.py`

逻辑：

- 普通用户：按 `current_user.area` 查本地区知识库
- 省级管理员：可看全部

对应：

- `app/routes/v3/dify_area_dataset.py:39`
- `app/routes/v3/dify_area_dataset.py:57`
- `app/routes/v3/dify_area_dataset.py:67`

### 11.3 Dify 对话应用过滤

代码：

- `app/routes/v3/dify_chat_apps.py`

逻辑：

- 根据 `current_user.area` 返回本地区应用
- 同时可以返回省级应用

对应：

- `app/routes/v3/dify_chat_apps.py:29`
- `app/routes/v3/dify_chat_apps.py:60`

### 11.4 PostgREST 转发层的 area 注入

代码：

- `app/exceptions/global_exc.py`

老系统在某些写操作中，甚至会自动把用户 `area` 写进业务数据。

典型例子：

- `evaluation_points` 表写入时自动填充 `area`

并且：

- `provincial_admin` 会被硬编码成 `省级`

对应代码：

- `app/exceptions/global_exc.py:250`
- `app/exceptions/global_exc.py:292`
- `app/exceptions/global_exc.py:307`

### 结论

在老系统中：

- `area` 不是附属字段
- 而是整套业务隔离体系的核心字段之一

---

## 12. 中间件与鉴权行为

### 12.1 JWT 中间件

代码：

- `app/middleware/jwt_auth.py`

行为：

- 白名单路径跳过
- 其他请求必须有 Bearer Token
- 中间件先做基础校验
- 然后将当前用户信息塞进：
  - `request.state.current_user`

塞入字段包括：

- `user_id`
- `username`
- `nick_name`
- `email`
- `phone_number`
- `ou_id`
- `ou_name`
- `is_leader`
- `user_role`
- `area`

对应：

- `app/middleware/jwt_auth.py:49`
- `app/middleware/jwt_auth.py:75`
- `app/middleware/jwt_auth.py:88`
- `app/middleware/jwt_auth.py:89`

### 12.2 路由层再做细粒度权限校验

真正功能权限由：

- `require_permission_v2`

处理，位置：

- `app/rbac/decorators_v2.py`

它会：

- 从 `request.state.current_user` 取 `user_id`
- 调 `PermissionCheckerV2.check_permission`
- 无权限则 403

对应：

- `app/rbac/decorators_v2.py:56`
- `app/rbac/decorators_v2.py:92`

### 结论

老系统是两层鉴权：

- 中间件：验 token
- 装饰器/依赖：验功能权限

这个分层设计是合理的。

---

## 13. 老系统角色体系的实际业务语义

根据代码表现，老系统大致存在以下角色层次：

- `provincial_admin`
- `admin`
- `common`
- 以及其他业务角色

### 13.1 `provincial_admin`

特点：

- 高权限角色
- 数据范围通常为 `ALL`
- 某些业务场景会被特殊处理为“省级”
- 可访问全部地区的数据 / 配置

体现位置：

- `app/exceptions/global_exc.py:292`
- `app/routes/v3/dify_area_dataset.py:67`

### 13.2 `admin`

特点：

- 更像市级管理员
- 数据范围通常为本地区
- 很多场景按 `area` 做限制

体现位置：

- `app/services/rbac/user_role_service.py:91`
- `app/exceptions/global_exc.py:229`

### 13.3 `common`

特点：

- 默认普通用户
- 新用户自动分配
- 权限最低
- 数据范围通常为 `SELF` 或较弱的本地区权限

体现位置：

- `app/routes/auth.py:512`
- `app/routes/auth.py:601`

---

## 14. 老系统真实的架构图

如果按实际行为抽象，老系统可以概括为：

### 14.1 用户身份层

- `sso_users`
- `sub`
- OAuth / 本地账密登录

### 14.2 角色层

- `roles`
- `user_role`

### 14.3 页面菜单层

- `sys_routes`
- `role_route`

### 14.4 功能权限层

- `permissions`
- `role_permissions`
- `permission_key`
- `grant_type`

### 14.5 数据权限层

- `role_permissions.data_scope`
- `roles.data_scope`
- 用户 `area`
- 业务表 `area`

### 14.6 组织归属层

- `ou_id`
- `ou_name`
- `tenant_name`
- `dep_name`
- `dep_short_name`
- 通过 MQ / 组织表同步

### 结论

老系统不是简单 RBAC，而是：

- `RBAC + 地区数据隔离 + 组织同步`

---

## 15. 老系统优点

### 15.1 登录入口统一

OAuth 和账密最终走同一条认证主链。

### 15.2 用户主数据集中

所有用户最终都汇总到：

- `sso_users`

### 15.3 路由权限设计成熟

- `sys_routes + role_route`
- 支持树形菜单
- 支持隐藏路由
- 支持角色路由缓存

### 15.4 功能权限设计成熟

- `permissions`
- `role_permissions`
- `grant / deny`
- 通配符匹配

### 15.5 数据范围是正式模型，不是临时过滤

虽然比较粗糙，但已经有：

- `ALL`
- `DEPT`
- `SELF`

### 15.6 地区来自后端主数据

老系统地区来源比“前端端口推断”可靠得多。

---

## 16. 老系统缺点

### 16.1 `area` 过载过重

`area` 同时承担：

- 用户地区
- 数据权限过滤条件
- 业务默认地区
- 规则隔离条件
- 知识库分配条件

语义过重。

### 16.2 数据范围表达力不足

只有三档：

- `ALL`
- `DEPT`
- `SELF`

无法优雅表达：

- 多地区访问
- 指定地区集合
- 复杂跨区权限

### 16.3 大量业务写死 `area` 逻辑

导致：

- 架构耦合重
- 后续改动成本高
- 组织逻辑与业务逻辑混杂

### 16.4 存在角色硬编码

例如：

- `provincial_admin -> 省级`

这类逻辑写死在业务层，不够优雅。

### 16.5 `DEPT` 实际上更像地区范围，不是部门范围

命名容易误导。

---

## 17. 对新系统的启示

## 17.1 必须继承的部分

新系统应继承老系统的这些优点：

- 统一登录入口
- OAuth / 账密统一落主用户表
- `sys_routes + role_route`
- `permissions + role_permissions`
- 标准 `permission_key`
- `grant / deny`
- 数据范围模型概念
- 组织同步决定地区

## 17.2 必须升级的部分

新系统不能原样照搬老系统的缺点。

必须升级为：

- 不再只有一个 `area` 字段承载所有数据权限语义
- 把用户默认地区和可访问地区分开
- 把数据权限从 `DEPT` 升级为正式 `region scope`
- 把组织同步保留，但不要在各业务层到处散写地区逻辑

---

## 18. 新系统设计应如何继承老系统

从老系统出发，新系统最合理的方向不是推翻，而是升级：

### 18.1 保留骨架

- `sso_users`
- `roles`
- `user_role`
- `sys_routes`
- `role_route`
- `permissions`
- `role_permissions`

### 18.2 替换数据权限实现

将老系统：

- `ALL / DEPT / SELF`

升级为：

- `ALL`
- `SELF`
- `HOME_REGION`
- `CUSTOM_REGIONS`
- `PROVINCIAL`

### 18.3 组织同步仍保留

地区来源仍然应该以后端组织主数据为准，而不是前端推断。

### 18.4 将 `area` 升级为正式 `region_code`

业务表中的地区字段建议统一收口成稳定 code，而不是直接到处使用中文字符串。

---

## 19. 一句话总结

老系统 `docauditai` 的真实用户权限架构不是“6 张表的简单角色系统”，而是：

- 以 `sso_users` 为用户主数据中心
- 通过 OAuth / 账密统一登录
- 通过组织系统同步 `area / tenant / dep`
- 用 `roles + user_role + sys_routes + role_route + permissions + role_permissions`
  管功能权限
- 用 `data_scope + area`
  管数据权限

本质是：

- `RBAC + 地区数据权限 + 组织同步`

这就是新系统设计时真正应该继承的老逻辑骨架。

