feat: bootstrap user rbac foundation
This commit is contained in:
@@ -38,6 +38,7 @@ logs/
|
||||
uploads/
|
||||
temp/
|
||||
tmp/
|
||||
testfile/
|
||||
|
||||
# Celery
|
||||
celerybeat-schedule
|
||||
|
||||
+8
-4
@@ -11,11 +11,15 @@
|
||||
当前已整理:
|
||||
|
||||
- `文档上传与评查接口.md`
|
||||
- `文档列表接口设计分析.md`
|
||||
- `新系统版_documents_list接口.md`
|
||||
- `用户权限与权限点清单.md`
|
||||
- `用户权限初始化SQL.sql`
|
||||
|
||||
建议阅读顺序:
|
||||
|
||||
1. 先看 `文档上传与评查接口.md`
|
||||
2. 再看 `文档列表接口设计分析.md`
|
||||
3. 再结合 `docs/规则编辑/worker并发执行改造方案.md`
|
||||
4. 如果要理解底层数据结构,再看 `docs/leaudit/document_schema_design.md`
|
||||
2. 再看 `新系统版_documents_list接口.md`
|
||||
3. 再看 `用户权限与权限点清单.md`
|
||||
4. 如果要初始化角色与权限数据,再执行 `用户权限初始化SQL.sql`
|
||||
5. 如果要理解 worker 并发执行,再结合 `docs/规则编辑/worker并发执行改造方案.md`
|
||||
6. 如果要理解底层数据结构,再看 `docs/leaudit/document_schema_design.md`
|
||||
|
||||
@@ -0,0 +1,419 @@
|
||||
# 用户权限初始化 SQL 与权限点清单
|
||||
|
||||
这份文档对应:`docs/接口/用户权限初始化SQL.sql`
|
||||
|
||||
用途只有两个:
|
||||
|
||||
- 帮你快速初始化当前新系统最小可用的用户权限体系
|
||||
- 帮后续自己看代码、配角色、排查越权时不再忘记“每个权限点到底是干什么的”
|
||||
|
||||
当前这版严格按你确认过的业务收口:
|
||||
|
||||
- 只做 `RBAC + 单地区隔离`
|
||||
- 用户地区只认 `sso_users.area`
|
||||
- 角色只认 `provincial_admin`、`admin`、`common`
|
||||
- `super_admin` 仅作为可选系统维护角色
|
||||
- 数据范围只认 `ALL / DEPT / SELF`
|
||||
|
||||
---
|
||||
|
||||
## 1. 本次交付文件
|
||||
|
||||
- SQL:`docs/接口/用户权限初始化SQL.sql`
|
||||
- 说明:`docs/接口/用户权限与权限点清单.md`
|
||||
|
||||
---
|
||||
|
||||
## 2. SQL 初始化了什么
|
||||
|
||||
这份 SQL 会初始化 5 类内容:
|
||||
|
||||
1. `roles` 角色基础数据
|
||||
2. `sys_routes` 菜单 / 页面路由基础数据
|
||||
3. `permissions` 权限点基础数据
|
||||
4. `role_route` 角色可见菜单授权
|
||||
5. `role_permissions` 角色权限点授权
|
||||
|
||||
它的定位不是“把所有未来功能一次性配满”,而是:
|
||||
|
||||
- 先把当前 `leaudit-platform` 已经稳定的核心能力配起来
|
||||
- 后续有新接口,再继续增量追加权限点
|
||||
|
||||
---
|
||||
|
||||
## 3. 当前正式角色
|
||||
|
||||
## 3.1 `provincial_admin`
|
||||
|
||||
- 省级管理员
|
||||
- 默认数据范围:`ALL`
|
||||
- 能看全省文档、任务、评查结果、规则配置、用户与角色配置
|
||||
|
||||
## 3.2 `admin`
|
||||
|
||||
- 地区管理员
|
||||
- 默认数据范围:`DEPT`
|
||||
- 实际含义是“同地区”,不是组织部门
|
||||
- 只能处理 `region = 当前用户.area` 的业务数据
|
||||
|
||||
## 3.3 `common`
|
||||
|
||||
- 普通用户
|
||||
- 默认数据范围:`SELF`
|
||||
- 通常只能看自己上传 / 自己触发 / 自己归属的数据
|
||||
- 对部分只读接口,可以单独放宽到 `DEPT`
|
||||
|
||||
## 3.4 `super_admin`
|
||||
|
||||
- 可选系统超级管理员
|
||||
- 默认数据范围:`ALL`
|
||||
- 只建议用来做系统维护、初始化、排障
|
||||
- 不建议参与日常业务权限设计
|
||||
|
||||
---
|
||||
|
||||
## 4. 数据范围解释
|
||||
|
||||
## 4.1 `ALL`
|
||||
|
||||
- 不做地区过滤
|
||||
- 可看全部数据
|
||||
|
||||
## 4.2 `DEPT`
|
||||
|
||||
- 在本项目里,正式定义为“同地区”
|
||||
- 一般等价于:`业务表.region = sso_users.area`
|
||||
|
||||
## 4.3 `SELF`
|
||||
|
||||
- 只能看自己的数据
|
||||
- 一般等价于:
|
||||
- `created_by = 当前用户.id`
|
||||
- 或 `uploaded_by = 当前用户.id`
|
||||
- 或 `owner_id = 当前用户.id`
|
||||
|
||||
## 4.4 生效优先级
|
||||
|
||||
后端建议统一这样算:
|
||||
|
||||
1. 先看当前接口是否命中某条 `role_permissions`
|
||||
2. 若该条有 `data_scope`,优先用它
|
||||
3. 若该条没有 `data_scope`,回退到 `roles.data_scope`
|
||||
4. 多角色命中时,取可见范围最大的那个:`ALL > DEPT > SELF`
|
||||
|
||||
---
|
||||
|
||||
## 5. 当前菜单建议
|
||||
|
||||
SQL 里初始化了以下核心菜单:
|
||||
|
||||
- `/documents`
|
||||
- `/documents/list`
|
||||
- `/rules`
|
||||
- `/rules/sets`
|
||||
- `/audit`
|
||||
- `/audit/runs`
|
||||
- `/system`
|
||||
- `/system/users`
|
||||
- `/system/roles`
|
||||
|
||||
这套菜单不是说前端必须完全一模一样,而是作为当前后台权限建模的“标准路径集合”。
|
||||
|
||||
---
|
||||
|
||||
## 6. 当前权限点清单
|
||||
|
||||
下面是当前已经纳入初始化 SQL 的权限点。
|
||||
|
||||
## 6.1 认证类
|
||||
|
||||
### `auth.me`
|
||||
|
||||
- 接口:`GET /api/auth/me`
|
||||
- 用途:获取当前登录用户信息
|
||||
- 建议角色:全部登录用户都可访问
|
||||
|
||||
---
|
||||
|
||||
## 6.2 文档类
|
||||
|
||||
### `documents.upload`
|
||||
|
||||
- 接口:`POST /api/upload`
|
||||
- 用途:上传文档,可选自动触发评查
|
||||
- `provincial_admin`:`ALL`
|
||||
- `admin`:`DEPT`
|
||||
- `common`:`SELF`
|
||||
|
||||
### `documents.list`
|
||||
|
||||
- 接口:`GET /api/documents/list`
|
||||
- 用途:查看文档列表
|
||||
- `provincial_admin`:全量文档
|
||||
- `admin`:本地区文档
|
||||
- `common`:自己的文档
|
||||
|
||||
### `documents.detail`
|
||||
|
||||
- 接口:`GET /api/documents/{documentId}`
|
||||
- 用途:查看文档详情
|
||||
- 数据范围逻辑应和 `documents.list` 保持一致
|
||||
|
||||
### `documents.history`
|
||||
|
||||
- 接口:`GET /api/documents/{documentId}/versions`
|
||||
- 用途:查看同名文档历史版本
|
||||
- 数据范围逻辑应和文档主记录一致
|
||||
|
||||
### `documents.delete`
|
||||
|
||||
- 接口:`DELETE /api/documents/{documentId}`
|
||||
- 用途:删除文档
|
||||
- 建议只开放给 `provincial_admin` 和 `admin`
|
||||
|
||||
---
|
||||
|
||||
## 6.3 评查类
|
||||
|
||||
### `audit.run`
|
||||
|
||||
- 接口:`POST /api/audit/run`
|
||||
- 用途:手动触发评查
|
||||
- `provincial_admin`:可发起任意地区 run
|
||||
- `admin`:只能发起本地区 run
|
||||
- `common`:只能发起自己可见文档的 run
|
||||
|
||||
### `audit.run.status`
|
||||
|
||||
- 接口:`GET /api/audit/run/{runId}`
|
||||
- 用途:查看 run 当前执行状态
|
||||
|
||||
### `audit.result`
|
||||
|
||||
- 接口:`GET /api/audit/result/{runId}`
|
||||
- 用途:查看评查结果
|
||||
|
||||
这两个接口都要跟随文档 / run 的归属范围过滤,不能因为知道 `runId` 就越权读取。
|
||||
|
||||
---
|
||||
|
||||
## 6.4 规则类
|
||||
|
||||
### `rules.list`
|
||||
|
||||
- 接口:`GET /api/rule-sets`
|
||||
- 用途:查看规则集列表
|
||||
|
||||
### `rules.version.list`
|
||||
|
||||
- 接口:`GET /api/rule-sets/{ruleType}/versions`
|
||||
- 用途:查看某规则集的版本列表
|
||||
|
||||
### `rules.content`
|
||||
|
||||
- 接口:`GET /api/rule-sets/versions/{versionId}/content`
|
||||
- 用途:查看规则 YAML 正文
|
||||
|
||||
### `rules.validate`
|
||||
|
||||
- 接口:`POST /api/rule-sets/{ruleType}/validate`
|
||||
- 用途:校验规则 YAML
|
||||
|
||||
### `rules.version.create`
|
||||
|
||||
- 接口:`POST /api/rule-sets/{ruleType}/versions`
|
||||
- 用途:创建规则版本
|
||||
- 建议开放给 `provincial_admin`
|
||||
- 如确有地区自管需求,再按地区规则放给 `admin`
|
||||
|
||||
### `rules.publish`
|
||||
|
||||
- 接口:`POST /api/rule-sets/{ruleType}/publish`
|
||||
- 用途:发布规则版本
|
||||
- 建议只开放给 `provincial_admin`
|
||||
|
||||
### `rules.rollback`
|
||||
|
||||
- 接口:`POST /api/rule-sets/{ruleType}/rollback`
|
||||
- 用途:规则回滚
|
||||
- 建议只开放给 `provincial_admin`
|
||||
|
||||
### `rules.binding.list`
|
||||
|
||||
- 接口:`GET /api/rule-sets/bindings`
|
||||
- 用途:查看规则绑定
|
||||
|
||||
### `rules.binding.create`
|
||||
|
||||
- 接口:`POST /api/rule-sets/{ruleType}/bindings`
|
||||
- 用途:创建规则绑定
|
||||
|
||||
### `rules.binding.update`
|
||||
|
||||
- 接口:`PUT /api/rule-sets/bindings/{bindingId}`
|
||||
- 用途:更新规则绑定
|
||||
|
||||
### `rules.binding.delete`
|
||||
|
||||
- 接口:`DELETE /api/rule-sets/bindings/{bindingId}`
|
||||
- 用途:删除规则绑定
|
||||
|
||||
---
|
||||
|
||||
## 6.5 用户类
|
||||
|
||||
### `users.list`
|
||||
|
||||
- 接口:`GET /api/users/list`
|
||||
- 用途:查看用户列表
|
||||
- `provincial_admin`:全部用户
|
||||
- `admin`:本地区用户
|
||||
- `common`:通常不开放
|
||||
|
||||
### `users.create`
|
||||
|
||||
- 接口:`POST /api/users`
|
||||
- 用途:创建用户
|
||||
- 建议只开放给 `provincial_admin`
|
||||
|
||||
### `users.update`
|
||||
|
||||
- 接口:`PUT /api/users/{userId}`
|
||||
- 用途:修改用户信息
|
||||
- `admin` 仅能维护本地区用户
|
||||
|
||||
### `users.disable`
|
||||
|
||||
- 接口:`PUT /api/users/{userId}/disable`
|
||||
- 用途:禁用 / 启用用户
|
||||
- 建议只开放给 `provincial_admin`
|
||||
|
||||
### `users.roles.assign`
|
||||
|
||||
- 接口:`POST /api/users/{userId}/roles`
|
||||
- 用途:为用户分配角色
|
||||
- 建议只开放给 `provincial_admin`
|
||||
|
||||
---
|
||||
|
||||
## 6.6 RBAC 管理类
|
||||
|
||||
### `rbac.roles.list`
|
||||
|
||||
- 接口:`GET /api/rbac/roles`
|
||||
- 用途:查看角色列表
|
||||
|
||||
### `rbac.roles.update`
|
||||
|
||||
- 接口:`PUT /api/rbac/roles/{roleId}`
|
||||
- 用途:修改角色定义
|
||||
|
||||
### `rbac.permissions.list`
|
||||
|
||||
- 接口:`GET /api/rbac/permissions`
|
||||
- 用途:查看权限点列表
|
||||
|
||||
### `rbac.role_permissions.assign`
|
||||
|
||||
- 接口:`POST /api/rbac/roles/{roleId}/permissions`
|
||||
- 用途:给角色分配权限点
|
||||
|
||||
### `rbac.role_routes.assign`
|
||||
|
||||
- 接口:`PUT /api/rbac/roles/{roleId}/routes`
|
||||
- 用途:给角色分配菜单
|
||||
|
||||
这些建议默认只给:
|
||||
|
||||
- `provincial_admin`
|
||||
- `super_admin`
|
||||
|
||||
---
|
||||
|
||||
## 7. 角色默认授权概览
|
||||
|
||||
## 7.1 `provincial_admin`
|
||||
|
||||
建议拥有:
|
||||
|
||||
- 全部当前权限点
|
||||
- 数据范围全部 `ALL`
|
||||
|
||||
## 7.2 `admin`
|
||||
|
||||
建议拥有:
|
||||
|
||||
- 文档上传、列表、详情、删除
|
||||
- 评查触发、进度查看、结果查看
|
||||
- 规则查看、校验、部分绑定维护
|
||||
- 本地区用户列表与部分用户维护
|
||||
- 数据范围多数为 `DEPT`
|
||||
|
||||
## 7.3 `common`
|
||||
|
||||
建议拥有:
|
||||
|
||||
- 当前用户信息
|
||||
- 文档上传
|
||||
- 自己文档列表 / 详情 / 历史版本
|
||||
- 自己触发的评查任务与结果
|
||||
- 部分规则只读查看
|
||||
- 数据范围多数为 `SELF`
|
||||
|
||||
---
|
||||
|
||||
## 8. 后端接入要求
|
||||
|
||||
权限初始化 SQL 只是把“数据字典”配起来,真正生效还要靠后端统一遵守下面规则:
|
||||
|
||||
### 8.1 JWT 必须能拿到这些信息
|
||||
|
||||
至少要能拿到:
|
||||
|
||||
- `user_id`
|
||||
- `area`
|
||||
- `roles`
|
||||
- `permissions`
|
||||
|
||||
### 8.2 接口先做功能权限,再做数据过滤
|
||||
|
||||
不要只做页面隐藏,必须后端实际校验:
|
||||
|
||||
1. 有没有权限点
|
||||
2. 命中哪条 `data_scope`
|
||||
3. 按 `ALL / DEPT / SELF` 注入查询条件
|
||||
|
||||
### 8.3 前端传入的地区不能覆盖后端数据权限
|
||||
|
||||
例如:
|
||||
|
||||
- `GET /api/documents/list?region=省局`
|
||||
|
||||
如果当前用户只是潮州 `admin`,后端仍然只能返回潮州数据,不能因为前端传了别的地区就放开。
|
||||
|
||||
---
|
||||
|
||||
## 9. 建议你怎么用这份 SQL
|
||||
|
||||
建议按这个顺序:
|
||||
|
||||
1. 先在测试库执行 `docs/接口/用户权限初始化SQL.sql`
|
||||
2. 检查 `roles / permissions / role_permissions / role_route` 是否初始化成功
|
||||
3. 给几个测试账号分别挂 `provincial_admin / admin / common`
|
||||
4. 再用接口实际验证:
|
||||
- 文档列表是否按地区隔离
|
||||
- 当前用户信息是否正确返回 `area`
|
||||
- 上传 / 评查是否存在越权
|
||||
|
||||
---
|
||||
|
||||
## 10. 当前文档与总设计文档的关系
|
||||
|
||||
如果你后面忘了这套体系总原则,看这里:
|
||||
|
||||
- 总设计:`docs/用户与地区权限完整设计方案.md`
|
||||
- 老系统分析:`docs/老系统_docauditai_用户权限架构深度分析.md`
|
||||
- 本文:`docs/接口/用户权限与权限点清单.md`
|
||||
- SQL:`docs/接口/用户权限初始化SQL.sql`
|
||||
|
||||
这四份组合起来,就是当前新系统用户权限方案的完整本地留档。
|
||||
@@ -0,0 +1,293 @@
|
||||
-- 用户权限初始化 SQL(leaudit-platform)
|
||||
-- 目标:初始化最小可用的 RBAC + 单地区数据隔离模型
|
||||
-- 核心约定:
|
||||
-- 1. 用户地区只认 sso_users.area
|
||||
-- 2. 数据范围只认 ALL / DEPT / SELF
|
||||
-- 3. 当前业务角色只保留 provincial_admin / admin / common
|
||||
-- 4. super_admin 仅作为可选系统维护角色
|
||||
|
||||
BEGIN;
|
||||
|
||||
-- ------------------------------------------------------------
|
||||
-- 1) 角色初始化
|
||||
-- ------------------------------------------------------------
|
||||
INSERT INTO roles (role_name, description, data_scope, status, created_at, updated_at)
|
||||
VALUES
|
||||
('super_admin', '系统超级管理员(可选,仅系统维护使用)', 'ALL', 0, NOW(), NOW()),
|
||||
('provincial_admin', '省级管理员', 'ALL', 0, NOW(), NOW()),
|
||||
('admin', '地区管理员', 'DEPT', 0, NOW(), NOW()),
|
||||
('common', '普通用户', 'SELF', 0, NOW(), NOW())
|
||||
ON CONFLICT (role_name) DO UPDATE SET
|
||||
description = EXCLUDED.description,
|
||||
data_scope = EXCLUDED.data_scope,
|
||||
status = EXCLUDED.status,
|
||||
updated_at = NOW();
|
||||
|
||||
-- ------------------------------------------------------------
|
||||
-- 2) 菜单 / 路由初始化
|
||||
-- 说明:这里只放当前 leaudit-platform 已明确的核心菜单。
|
||||
-- ------------------------------------------------------------
|
||||
WITH upsert_routes AS (
|
||||
INSERT INTO sys_routes (path, name, component, parent_id, sort_order, visible, enabled, meta, created_at, updated_at)
|
||||
VALUES
|
||||
('/documents', '文档管理', 'Layout', NULL, 10, TRUE, TRUE, '{"icon":"files"}'::jsonb, NOW(), NOW()),
|
||||
('/documents/list', '文档列表', 'documents/list', NULL, 11, TRUE, TRUE, '{"icon":"table"}'::jsonb, NOW(), NOW()),
|
||||
('/rules', '规则管理', 'Layout', NULL, 20, TRUE, TRUE, '{"icon":"rule"}'::jsonb, NOW(), NOW()),
|
||||
('/rules/sets', '规则集管理', 'rules/sets', NULL, 21, TRUE, TRUE, '{"icon":"yaml"}'::jsonb, NOW(), NOW()),
|
||||
('/audit', '评查任务', 'Layout', NULL, 30, TRUE, TRUE, '{"icon":"audit"}'::jsonb, NOW(), NOW()),
|
||||
('/audit/runs', '评查运行', 'audit/runs', NULL, 31, TRUE, TRUE, '{"icon":"history"}'::jsonb, NOW(), NOW()),
|
||||
('/system', '系统管理', 'Layout', NULL, 90, TRUE, TRUE, '{"icon":"setting"}'::jsonb, NOW(), NOW()),
|
||||
('/system/users', '用户管理', 'system/users', NULL, 91, TRUE, TRUE, '{"icon":"user"}'::jsonb, NOW(), NOW()),
|
||||
('/system/roles', '角色权限', 'system/roles', NULL, 92, TRUE, TRUE, '{"icon":"shield"}'::jsonb, NOW(), NOW())
|
||||
ON CONFLICT (path) DO UPDATE SET
|
||||
name = EXCLUDED.name,
|
||||
component = EXCLUDED.component,
|
||||
sort_order = EXCLUDED.sort_order,
|
||||
visible = EXCLUDED.visible,
|
||||
enabled = EXCLUDED.enabled,
|
||||
meta = EXCLUDED.meta,
|
||||
updated_at = NOW()
|
||||
RETURNING id, path
|
||||
)
|
||||
SELECT COUNT(*) FROM upsert_routes;
|
||||
|
||||
-- ------------------------------------------------------------
|
||||
-- 3) 权限点初始化
|
||||
-- permission_code 建议全局唯一。
|
||||
-- ------------------------------------------------------------
|
||||
INSERT INTO permissions (permission_code, permission_name, resource_type, method, path, description, created_at, updated_at)
|
||||
VALUES
|
||||
('auth.me', '查看当前登录用户', 'api', 'GET', '/api/auth/me', '获取当前用户登录信息', NOW(), NOW()),
|
||||
|
||||
('documents.upload', '上传文档', 'api', 'POST', '/api/upload', '上传文档并可选自动触发评查', NOW(), NOW()),
|
||||
('documents.list', '查看文档列表', 'api', 'GET', '/api/documents/list', '查看文档列表(按数据范围过滤)', NOW(), NOW()),
|
||||
('documents.detail', '查看文档详情', 'api', 'GET', '/api/documents/{documentId}', '查看单个文档详情', NOW(), NOW()),
|
||||
('documents.history', '查看文档历史版本', 'api', 'GET', '/api/documents/{documentId}/versions', '查看同名文档历史版本', NOW(), NOW()),
|
||||
('documents.delete', '删除文档', 'api', 'DELETE', '/api/documents/{documentId}', '删除文档或逻辑删除', NOW(), NOW()),
|
||||
|
||||
('audit.run', '发起评查任务', 'api', 'POST', '/api/audit/run', '手动触发文档评查', NOW(), NOW()),
|
||||
('audit.run.status', '查看评查运行状态', 'api', 'GET', '/api/audit/run/{runId}', '查看 run 当前状态', NOW(), NOW()),
|
||||
('audit.result', '查看评查结果', 'api', 'GET', '/api/audit/result/{runId}', '查看评查结果明细', NOW(), NOW()),
|
||||
|
||||
('rules.list', '查看规则集列表', 'api', 'GET', '/api/rule-sets', '查看规则集列表', NOW(), NOW()),
|
||||
('rules.version.list', '查看规则版本列表', 'api', 'GET', '/api/rule-sets/{ruleType}/versions', '查看某规则集的版本列表', NOW(), NOW()),
|
||||
('rules.content', '查看规则正文', 'api', 'GET', '/api/rule-sets/versions/{versionId}/content', '查看某个版本的 YAML 内容', NOW(), NOW()),
|
||||
('rules.validate', '校验规则 YAML', 'api', 'POST', '/api/rule-sets/{ruleType}/validate', '校验规则 YAML 是否合法', NOW(), NOW()),
|
||||
('rules.version.create', '创建规则版本', 'api', 'POST', '/api/rule-sets/{ruleType}/versions', '创建新规则版本', NOW(), NOW()),
|
||||
('rules.publish', '发布规则版本', 'api', 'POST', '/api/rule-sets/{ruleType}/publish', '发布规则版本', NOW(), NOW()),
|
||||
('rules.rollback', '回滚规则版本', 'api', 'POST', '/api/rule-sets/{ruleType}/rollback', '回滚规则版本', NOW(), NOW()),
|
||||
('rules.binding.list', '查看规则绑定', 'api', 'GET', '/api/rule-sets/bindings', '查看规则绑定列表', NOW(), NOW()),
|
||||
('rules.binding.create', '创建规则绑定', 'api', 'POST', '/api/rule-sets/{ruleType}/bindings', '创建规则绑定', NOW(), NOW()),
|
||||
('rules.binding.update', '更新规则绑定', 'api', 'PUT', '/api/rule-sets/bindings/{bindingId}', '更新规则绑定', NOW(), NOW()),
|
||||
('rules.binding.delete', '删除规则绑定', 'api', 'DELETE', '/api/rule-sets/bindings/{bindingId}', '删除规则绑定', NOW(), NOW()),
|
||||
|
||||
('users.list', '查看用户列表', 'api', 'GET', '/api/users/list', '查看用户列表(按地区过滤)', NOW(), NOW()),
|
||||
('users.create', '创建用户', 'api', 'POST', '/api/users', '创建本地用户或同步用户', NOW(), NOW()),
|
||||
('users.update', '更新用户', 'api', 'PUT', '/api/users/{userId}', '更新用户基础信息', NOW(), NOW()),
|
||||
('users.disable', '禁用用户', 'api', 'PUT', '/api/users/{userId}/disable', '禁用或启用用户', NOW(), NOW()),
|
||||
('users.roles.assign', '分配用户角色', 'api', 'POST', '/api/users/{userId}/roles', '为用户分配角色', NOW(), NOW()),
|
||||
|
||||
('rbac.roles.list', '查看角色列表', 'api', 'GET', '/api/rbac/roles', '查看角色列表', NOW(), NOW()),
|
||||
('rbac.roles.update', '维护角色信息', 'api', 'PUT', '/api/rbac/roles/{roleId}', '维护角色定义', NOW(), NOW()),
|
||||
('rbac.permissions.list', '查看权限点列表', 'api', 'GET', '/api/rbac/permissions', '查看权限点列表', NOW(), NOW()),
|
||||
('rbac.role_permissions.assign', '分配角色权限', 'api', 'POST', '/api/rbac/roles/{roleId}/permissions', '分配角色权限点', NOW(), NOW()),
|
||||
('rbac.role_routes.assign', '分配角色菜单', 'api', 'PUT', '/api/rbac/roles/{roleId}/routes', '分配角色菜单', NOW(), NOW())
|
||||
ON CONFLICT (permission_code) DO UPDATE SET
|
||||
permission_name = EXCLUDED.permission_name,
|
||||
resource_type = EXCLUDED.resource_type,
|
||||
method = EXCLUDED.method,
|
||||
path = EXCLUDED.path,
|
||||
description = EXCLUDED.description,
|
||||
updated_at = NOW();
|
||||
|
||||
-- ------------------------------------------------------------
|
||||
-- 4) 角色菜单授权
|
||||
-- ------------------------------------------------------------
|
||||
WITH role_map AS (
|
||||
SELECT id, role_name FROM roles WHERE role_name IN ('super_admin', 'provincial_admin', 'admin', 'common')
|
||||
),
|
||||
route_map AS (
|
||||
SELECT id, path FROM sys_routes WHERE path IN (
|
||||
'/documents', '/documents/list',
|
||||
'/rules', '/rules/sets',
|
||||
'/audit', '/audit/runs',
|
||||
'/system', '/system/users', '/system/roles'
|
||||
)
|
||||
),
|
||||
seed(role_name, path) AS (
|
||||
VALUES
|
||||
('super_admin', '/documents'),
|
||||
('super_admin', '/documents/list'),
|
||||
('super_admin', '/rules'),
|
||||
('super_admin', '/rules/sets'),
|
||||
('super_admin', '/audit'),
|
||||
('super_admin', '/audit/runs'),
|
||||
('super_admin', '/system'),
|
||||
('super_admin', '/system/users'),
|
||||
('super_admin', '/system/roles'),
|
||||
|
||||
('provincial_admin', '/documents'),
|
||||
('provincial_admin', '/documents/list'),
|
||||
('provincial_admin', '/rules'),
|
||||
('provincial_admin', '/rules/sets'),
|
||||
('provincial_admin', '/audit'),
|
||||
('provincial_admin', '/audit/runs'),
|
||||
('provincial_admin', '/system'),
|
||||
('provincial_admin', '/system/users'),
|
||||
('provincial_admin', '/system/roles'),
|
||||
|
||||
('admin', '/documents'),
|
||||
('admin', '/documents/list'),
|
||||
('admin', '/rules'),
|
||||
('admin', '/rules/sets'),
|
||||
('admin', '/audit'),
|
||||
('admin', '/audit/runs'),
|
||||
('admin', '/system'),
|
||||
('admin', '/system/users'),
|
||||
|
||||
('common', '/documents'),
|
||||
('common', '/documents/list'),
|
||||
('common', '/audit'),
|
||||
('common', '/audit/runs')
|
||||
)
|
||||
INSERT INTO role_route (role_id, route_id, created_at)
|
||||
SELECT DISTINCT rm.id, tm.id, NOW()
|
||||
FROM seed s
|
||||
JOIN role_map rm ON rm.role_name = s.role_name
|
||||
JOIN route_map tm ON tm.path = s.path
|
||||
ON CONFLICT (role_id, route_id) DO NOTHING;
|
||||
|
||||
-- ------------------------------------------------------------
|
||||
-- 5) 角色权限点授权
|
||||
-- 说明:role_permissions.data_scope 优先级高于 roles.data_scope。
|
||||
-- ------------------------------------------------------------
|
||||
WITH role_map AS (
|
||||
SELECT id, role_name FROM roles WHERE role_name IN ('super_admin', 'provincial_admin', 'admin', 'common')
|
||||
),
|
||||
perm_map AS (
|
||||
SELECT id, permission_code FROM permissions
|
||||
),
|
||||
seed(role_name, permission_code, data_scope) AS (
|
||||
VALUES
|
||||
('super_admin', 'auth.me', 'ALL'),
|
||||
('super_admin', 'documents.upload', 'ALL'),
|
||||
('super_admin', 'documents.list', 'ALL'),
|
||||
('super_admin', 'documents.detail', 'ALL'),
|
||||
('super_admin', 'documents.history', 'ALL'),
|
||||
('super_admin', 'documents.delete', 'ALL'),
|
||||
('super_admin', 'audit.run', 'ALL'),
|
||||
('super_admin', 'audit.run.status', 'ALL'),
|
||||
('super_admin', 'audit.result', 'ALL'),
|
||||
('super_admin', 'rules.list', 'ALL'),
|
||||
('super_admin', 'rules.version.list', 'ALL'),
|
||||
('super_admin', 'rules.content', 'ALL'),
|
||||
('super_admin', 'rules.validate', 'ALL'),
|
||||
('super_admin', 'rules.version.create', 'ALL'),
|
||||
('super_admin', 'rules.publish', 'ALL'),
|
||||
('super_admin', 'rules.rollback', 'ALL'),
|
||||
('super_admin', 'rules.binding.list', 'ALL'),
|
||||
('super_admin', 'rules.binding.create', 'ALL'),
|
||||
('super_admin', 'rules.binding.update', 'ALL'),
|
||||
('super_admin', 'rules.binding.delete', 'ALL'),
|
||||
('super_admin', 'users.list', 'ALL'),
|
||||
('super_admin', 'users.create', 'ALL'),
|
||||
('super_admin', 'users.update', 'ALL'),
|
||||
('super_admin', 'users.disable', 'ALL'),
|
||||
('super_admin', 'users.roles.assign', 'ALL'),
|
||||
('super_admin', 'rbac.roles.list', 'ALL'),
|
||||
('super_admin', 'rbac.roles.update', 'ALL'),
|
||||
('super_admin', 'rbac.permissions.list', 'ALL'),
|
||||
('super_admin', 'rbac.role_permissions.assign', 'ALL'),
|
||||
('super_admin', 'rbac.role_routes.assign', 'ALL'),
|
||||
|
||||
('provincial_admin', 'auth.me', 'ALL'),
|
||||
('provincial_admin', 'documents.upload', 'ALL'),
|
||||
('provincial_admin', 'documents.list', 'ALL'),
|
||||
('provincial_admin', 'documents.detail', 'ALL'),
|
||||
('provincial_admin', 'documents.history', 'ALL'),
|
||||
('provincial_admin', 'documents.delete', 'ALL'),
|
||||
('provincial_admin', 'audit.run', 'ALL'),
|
||||
('provincial_admin', 'audit.run.status', 'ALL'),
|
||||
('provincial_admin', 'audit.result', 'ALL'),
|
||||
('provincial_admin', 'rules.list', 'ALL'),
|
||||
('provincial_admin', 'rules.version.list', 'ALL'),
|
||||
('provincial_admin', 'rules.content', 'ALL'),
|
||||
('provincial_admin', 'rules.validate', 'ALL'),
|
||||
('provincial_admin', 'rules.version.create', 'ALL'),
|
||||
('provincial_admin', 'rules.publish', 'ALL'),
|
||||
('provincial_admin', 'rules.rollback', 'ALL'),
|
||||
('provincial_admin', 'rules.binding.list', 'ALL'),
|
||||
('provincial_admin', 'rules.binding.create', 'ALL'),
|
||||
('provincial_admin', 'rules.binding.update', 'ALL'),
|
||||
('provincial_admin', 'rules.binding.delete', 'ALL'),
|
||||
('provincial_admin', 'users.list', 'ALL'),
|
||||
('provincial_admin', 'users.create', 'ALL'),
|
||||
('provincial_admin', 'users.update', 'ALL'),
|
||||
('provincial_admin', 'users.disable', 'ALL'),
|
||||
('provincial_admin', 'users.roles.assign', 'ALL'),
|
||||
('provincial_admin', 'rbac.roles.list', 'ALL'),
|
||||
('provincial_admin', 'rbac.roles.update', 'ALL'),
|
||||
('provincial_admin', 'rbac.permissions.list', 'ALL'),
|
||||
('provincial_admin', 'rbac.role_permissions.assign', 'ALL'),
|
||||
('provincial_admin', 'rbac.role_routes.assign', 'ALL'),
|
||||
|
||||
('admin', 'auth.me', 'DEPT'),
|
||||
('admin', 'documents.upload', 'DEPT'),
|
||||
('admin', 'documents.list', 'DEPT'),
|
||||
('admin', 'documents.detail', 'DEPT'),
|
||||
('admin', 'documents.history', 'DEPT'),
|
||||
('admin', 'documents.delete', 'DEPT'),
|
||||
('admin', 'audit.run', 'DEPT'),
|
||||
('admin', 'audit.run.status', 'DEPT'),
|
||||
('admin', 'audit.result', 'DEPT'),
|
||||
('admin', 'rules.list', 'DEPT'),
|
||||
('admin', 'rules.version.list', 'DEPT'),
|
||||
('admin', 'rules.content', 'DEPT'),
|
||||
('admin', 'rules.validate', 'DEPT'),
|
||||
('admin', 'rules.binding.list', 'DEPT'),
|
||||
('admin', 'rules.binding.create', 'DEPT'),
|
||||
('admin', 'rules.binding.update', 'DEPT'),
|
||||
('admin', 'users.list', 'DEPT'),
|
||||
('admin', 'users.update', 'DEPT'),
|
||||
|
||||
('common', 'auth.me', 'SELF'),
|
||||
('common', 'documents.upload', 'SELF'),
|
||||
('common', 'documents.list', 'SELF'),
|
||||
('common', 'documents.detail', 'SELF'),
|
||||
('common', 'documents.history', 'SELF'),
|
||||
('common', 'audit.run', 'SELF'),
|
||||
('common', 'audit.run.status', 'SELF'),
|
||||
('common', 'audit.result', 'SELF'),
|
||||
('common', 'rules.list', 'DEPT'),
|
||||
('common', 'rules.version.list', 'DEPT'),
|
||||
('common', 'rules.content', 'DEPT'),
|
||||
('common', 'rules.binding.list', 'DEPT')
|
||||
)
|
||||
INSERT INTO role_permissions (role_id, permission_id, data_scope, created_at)
|
||||
SELECT DISTINCT rm.id, pm.id, s.data_scope, NOW()
|
||||
FROM seed s
|
||||
JOIN role_map rm ON rm.role_name = s.role_name
|
||||
JOIN perm_map pm ON pm.permission_code = s.permission_code
|
||||
ON CONFLICT (role_id, permission_id) DO UPDATE SET
|
||||
data_scope = EXCLUDED.data_scope;
|
||||
|
||||
COMMIT;
|
||||
|
||||
-- ------------------------------------------------------------
|
||||
-- 6) 使用示例(按需执行,不建议直接整段上线)
|
||||
-- ------------------------------------------------------------
|
||||
-- 将一个现有用户提为地区管理员:
|
||||
-- INSERT INTO user_role (user_id, role_id, created_at)
|
||||
-- SELECT 1001, id, NOW() FROM roles WHERE role_name = 'admin'
|
||||
-- ON CONFLICT (user_id, role_id) DO NOTHING;
|
||||
|
||||
-- 查看角色及其默认数据范围:
|
||||
-- SELECT role_name, data_scope FROM roles ORDER BY id;
|
||||
|
||||
-- 查看某角色已分配的权限点:
|
||||
-- SELECT r.role_name, p.permission_code, rp.data_scope
|
||||
-- FROM role_permissions rp
|
||||
-- JOIN roles r ON r.id = rp.role_id
|
||||
-- JOIN permissions p ON p.id = rp.permission_id
|
||||
-- WHERE r.role_name = 'admin'
|
||||
-- ORDER BY p.permission_code;
|
||||
@@ -0,0 +1,777 @@
|
||||
# 用户与地区权限完整设计方案
|
||||
|
||||
> 本文档为 `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 数据隔离`
|
||||
- 去掉老系统里不够清晰、靠约定隐式生效的部分
|
||||
- 在新系统里把“地区”“角色”“权限点”“数据范围”写清楚
|
||||
|
||||
---
|
||||
|
||||
## 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`
|
||||
- 不再做前端端口识别地区
|
||||
- 不再做多地区用户模型
|
||||
|
||||
这是当前最符合你们真实业务、最接近老系统有效逻辑、同时又足够轻量可落地的一版方案。
|
||||
@@ -0,0 +1,878 @@
|
||||
# 用户权限开发 TaskList
|
||||
|
||||
> 目标:在 `leaudit-platform` 里,基于当前真实业务落地一套 **单地区隔离 + 角色权限 + 数据范围控制** 的用户体系,并能把老系统用户数据平滑迁移过来。
|
||||
|
||||
这份文档不是泛泛设计稿,而是后续开发的执行清单。
|
||||
|
||||
---
|
||||
|
||||
## 1. 当前结论先说清楚
|
||||
|
||||
当前这条线,应该按下面原则做,不要再发散:
|
||||
|
||||
- 用户地区只认 `sso_users.area`
|
||||
- 数据隔离只做单地区,不做多地区授权
|
||||
- 当前业务角色只保留:
|
||||
- `provincial_admin`
|
||||
- `admin`
|
||||
- `common`
|
||||
- `super_admin` 只作为可选系统维护角色
|
||||
- 数据范围只保留:
|
||||
- `ALL`
|
||||
- `DEPT`
|
||||
- `SELF`
|
||||
- 新系统要兼容老系统已有数据结构,不要为了“理论优雅”把老数据迁移难度抬高
|
||||
|
||||
---
|
||||
|
||||
## 2. 当前新系统现状盘点
|
||||
|
||||
## 2.0 本次实际检查结果(2026-04-29)
|
||||
|
||||
我已经实际检查了当前 `leaudit_platform` 新库和老库 `docauditai`,结论如下:
|
||||
|
||||
- 新库 `leaudit_platform` 原先 **完全没有** 这 7 张核心 RBAC 表:
|
||||
- `sso_users`
|
||||
- `roles`
|
||||
- `user_role`
|
||||
- `permissions`
|
||||
- `role_permissions`
|
||||
- `sys_routes`
|
||||
- `role_route`
|
||||
- 新库现阶段只有 `leaudit_*` 业务表,所以原有认证/权限代码其实处于“代码已写、库表未落地”的状态
|
||||
- 我已经补了可执行 SQL:
|
||||
- `scripts/user_rbac_schema_patch.sql`
|
||||
- `scripts/user_rbac_seed.sql`
|
||||
- `scripts/user_rbac_migration_audit.sql`
|
||||
- 我已经把 `schema_patch + seed` 执行进当前新库
|
||||
- 当前新库 RBAC 初始化结果:
|
||||
- `roles = 4`
|
||||
- `permissions = 30`
|
||||
- `role_permissions = 90`
|
||||
- `sys_routes = 9`
|
||||
- `role_route = 30`
|
||||
- `sso_users = 0`
|
||||
- `user_role = 0`
|
||||
|
||||
老库 `docauditai` 的迁移审计关键结果:
|
||||
|
||||
- `sso_users = 4106`
|
||||
- `roles = 3`
|
||||
- `user_role = 11`
|
||||
- `permissions = 133`
|
||||
- `role_permissions = 222`
|
||||
- `sys_routes = 34`
|
||||
- `role_route = 95`
|
||||
- 老库 **没有空地区、没有重复 sub、没有重复 username**
|
||||
- 老库 **4106 个用户里有 4098 个没有角色**
|
||||
- 老库真实角色分布非常集中:
|
||||
- `common = 6`
|
||||
- `admin = 4`
|
||||
- `provincial_admin = 1`
|
||||
|
||||
这说明:
|
||||
|
||||
- 老系统用户登录主数据是可迁的
|
||||
- 但角色并不是“每个用户都显式分配了 user_role”
|
||||
- 新系统迁移时必须补一轮“默认角色落地策略”,不能只机械复制 `user_role`
|
||||
|
||||
## 2.1 已有能力
|
||||
|
||||
当前新系统已经有这些基础:
|
||||
|
||||
- 已有 `sso_users` 登录查询逻辑
|
||||
- 已有 JWT 签发与鉴权
|
||||
- 已有 `user_role / roles / role_permissions / permissions` 的查询代码雏形
|
||||
- 已有受保护路由统一挂载 JWT 依赖:`fastapi_modules/fastapi_leaudit/controllers/__init__.py`
|
||||
- 已有认证入口:`/auth/login`、`/auth/password_login`
|
||||
|
||||
## 2.2 当前实际代码路径
|
||||
|
||||
当前这条线的核心代码在:
|
||||
|
||||
- `fastapi_modules/fastapi_leaudit/services/impl/authServiceImpl.py`
|
||||
- `fastapi_modules/fastapi_leaudit/services/impl/permissionServiceImpl.py`
|
||||
- `fastapi_common/fastapi_common_security/jwtService.py`
|
||||
- `fastapi_common/fastapi_common_security/security.py`
|
||||
- `fastapi_modules/fastapi_leaudit/controllers/auth/authController.py`
|
||||
|
||||
## 2.3 当前最关键的问题
|
||||
|
||||
### 问题 1:权限设计文档和实际代码字段名不一致
|
||||
|
||||
当前代码查的是:
|
||||
|
||||
- `roles.role_key`
|
||||
- `permissions.permission_key`
|
||||
- `role_permissions.grant_type`
|
||||
|
||||
但我前面新写的初始化 SQL 用的是:
|
||||
|
||||
- `roles.role_name`
|
||||
- `permissions.permission_code`
|
||||
|
||||
这说明:
|
||||
|
||||
- **现阶段不能直接执行那份初始化 SQL 到现有库**
|
||||
- 必须先按当前实际数据库字段重新收一版正式建表/初始化方案
|
||||
|
||||
### 问题 2:登录后只取“一个角色”
|
||||
|
||||
当前 `authServiceImpl.py` 里:
|
||||
|
||||
```python
|
||||
SELECT r.role_key ... LIMIT 1
|
||||
```
|
||||
|
||||
这意味着:
|
||||
|
||||
- 多角色用户会被截断成单角色
|
||||
- JWT 中的 `roles` 没真正使用起来
|
||||
- 后续 `ALL > DEPT > SELF` 的数据范围收敛逻辑无法成立
|
||||
|
||||
### 问题 3:JWT 里没有真正放入完整权限集
|
||||
|
||||
当前 JWT 虽然支持 `roles` 字段,但登录时只传了:
|
||||
|
||||
- `userRole`
|
||||
- `area`
|
||||
|
||||
没有把:
|
||||
|
||||
- 全量角色列表
|
||||
- 权限点列表
|
||||
- 最终数据范围摘要
|
||||
|
||||
稳定打进去。
|
||||
|
||||
这会导致:
|
||||
|
||||
- `/me` 接口还不完整
|
||||
- 前端拿不到统一用户权限视图
|
||||
- 后端每次都要重新查库,且现有逻辑并未闭环
|
||||
|
||||
### 问题 4:当前没有统一的数据范围注入层
|
||||
|
||||
现在只是“能查角色 / 权限”,但还没有真正统一实现:
|
||||
|
||||
- 文档列表自动按 `area` 过滤
|
||||
- 用户列表自动按 `area` 过滤
|
||||
- 评查 run / result 自动按文档归属校验
|
||||
|
||||
也就是说:
|
||||
|
||||
- **功能权限雏形有了,数据权限还没有真正落地**
|
||||
|
||||
### 问题 5:OAuth 登录仍然可能把 area 当外部输入覆盖
|
||||
|
||||
当前 OAuth 登录里:
|
||||
|
||||
- `Area=requestData.get("area")`
|
||||
- 然后直接 `UPDATE sso_users SET ... area = :area`
|
||||
|
||||
这是很危险的。
|
||||
|
||||
因为这意味着:
|
||||
|
||||
- 前端或调用方传什么 area,就可能把用户地区改掉
|
||||
- 数据隔离基础字段可能被外部请求污染
|
||||
|
||||
后续必须改成:
|
||||
|
||||
- area 只能来自后台可信源
|
||||
- 登录请求不能直接覆盖 area
|
||||
|
||||
### 问题 6:新用户自动创建后没有默认角色闭环
|
||||
|
||||
当前 OAuth 首登自动插入 `sso_users`,但代码里没有看到紧接着分配默认 `common` 角色的闭环。
|
||||
|
||||
风险:
|
||||
|
||||
- 新用户建出来了,但没有角色
|
||||
- 没角色则权限查询为空
|
||||
- 前端表现为“登录成功但啥也看不到”
|
||||
|
||||
### 问题 7:密码仍然是明文比对
|
||||
|
||||
当前 `PasswordLogin()` 里:
|
||||
|
||||
- 直接 `user.get("password") != Password`
|
||||
|
||||
这是明显安全风险。
|
||||
|
||||
即使当前先沿用旧系统,也至少要在 tasklist 里明确:
|
||||
|
||||
- 第一阶段兼容旧明文
|
||||
- 第二阶段升级为哈希存储与校验
|
||||
|
||||
### 问题 8:缺少统一的 `/api/auth/me`
|
||||
|
||||
当前文档里已经定义了 `/api/auth/me`,但现有 controller 里还没有真正落地。
|
||||
|
||||
这会影响:
|
||||
|
||||
- 前端初始化用户态
|
||||
- 角色/权限/地区统一回显
|
||||
- 登录后刷新恢复
|
||||
|
||||
---
|
||||
|
||||
## 3. 老系统可迁移数据结论
|
||||
|
||||
基于老项目 `docauditai` 的数据库快照和代码分析,当前可直接继承的核心表是:
|
||||
|
||||
- `sso_users`
|
||||
- `roles`
|
||||
- `user_role`
|
||||
- `permissions`
|
||||
- `role_permissions`
|
||||
- `sys_routes`
|
||||
- `role_route`
|
||||
|
||||
## 3.1 老系统 `sso_users` 关键字段
|
||||
|
||||
老系统 `sso_users` 目前至少包含:
|
||||
|
||||
- `id`
|
||||
- `sub`
|
||||
- `username`
|
||||
- `nick_name`
|
||||
- `phone_number`
|
||||
- `email`
|
||||
- `ou_id`
|
||||
- `ou_name`
|
||||
- `status`
|
||||
- `is_leader`
|
||||
- `password`
|
||||
- `try_count`
|
||||
- `try_login_time`
|
||||
- `area`
|
||||
- `tenant_name`
|
||||
- `dep_short_name`
|
||||
- `dep_name`
|
||||
- `mq_person_uuid`
|
||||
- `mq_account_uuid`
|
||||
- `mq_synced_at`
|
||||
- `created_at`
|
||||
- `updated_at`
|
||||
- `deleted_at`
|
||||
|
||||
### 迁移判断
|
||||
|
||||
这说明:
|
||||
|
||||
- 新系统**完全没必要重新发明用户主表**
|
||||
- 最优方案是:
|
||||
- 继续沿用 `sso_users`
|
||||
- 补必要索引/约束/注释
|
||||
- 修正其业务语义
|
||||
|
||||
## 3.2 老系统 `roles` 关键字段
|
||||
|
||||
老系统 `roles` 目前至少包含:
|
||||
|
||||
- `id`
|
||||
- `role_key`
|
||||
- `role_name`
|
||||
- `data_scope`
|
||||
- `description`
|
||||
- `parent_role_id`
|
||||
- `priority`
|
||||
- `is_system_role`
|
||||
- `permissions_cache`
|
||||
- `metadata`
|
||||
- `created_at`
|
||||
- `updated_at`
|
||||
|
||||
### 迁移判断
|
||||
|
||||
建议:
|
||||
|
||||
- 保留 `role_key` 作为机器标识
|
||||
- 保留 `role_name` 作为中文/展示名
|
||||
- 继续使用 `data_scope`
|
||||
- `parent_role_id / permissions_cache / metadata` 可以先保留但不作为第一阶段核心依赖
|
||||
|
||||
## 3.3 老系统 `permissions` 关键字段
|
||||
|
||||
老系统 `permissions` 目前至少包含:
|
||||
|
||||
- `id`
|
||||
- `permission_key`
|
||||
- `module`
|
||||
- `resource`
|
||||
- `action`
|
||||
- `description`
|
||||
- `display_name`
|
||||
- `permission_type`
|
||||
- `is_system`
|
||||
- `metadata`
|
||||
- `parent_id`
|
||||
- `sort_order`
|
||||
- `route_id`
|
||||
- `api_path`
|
||||
- `api_method`
|
||||
- `created_at`
|
||||
- `updated_at`
|
||||
|
||||
### 迁移判断
|
||||
|
||||
建议:
|
||||
|
||||
- 不要再新造 `permission_code`
|
||||
- 直接沿用 `permission_key`
|
||||
- 后续新系统全部按 `permission_key` 统一
|
||||
|
||||
## 3.4 老系统 `role_permissions` 关键字段
|
||||
|
||||
老系统 `role_permissions` 目前至少包含:
|
||||
|
||||
- `role_id`
|
||||
- `permission_id`
|
||||
- `grant_type`
|
||||
- `data_scope`
|
||||
- `condition_filter`
|
||||
- `metadata`
|
||||
- `created_at`
|
||||
- `updated_at`
|
||||
|
||||
### 迁移判断
|
||||
|
||||
建议:
|
||||
|
||||
- 继续保留 `grant_type`
|
||||
- 第一阶段只用 `GRANT`
|
||||
- `DENY` 能力先兼容不扩展
|
||||
- `condition_filter` 暂不作为第一阶段核心能力
|
||||
|
||||
---
|
||||
|
||||
## 4. 最终推荐表结构策略
|
||||
|
||||
这里不是从零建库,而是:
|
||||
|
||||
- **以老系统实际表结构为准**
|
||||
- 对新系统补齐缺失字段、索引、语义约束
|
||||
- 避免再造第二套平行用户权限模型
|
||||
|
||||
## 4.1 `sso_users` 目标策略
|
||||
|
||||
### 保留字段
|
||||
|
||||
- 保留老系统现有字段不动
|
||||
- 核心业务字段继续认:`area`
|
||||
|
||||
### 必须加的约束 / 索引
|
||||
|
||||
建议检查并补:
|
||||
|
||||
- `UNIQUE(sub)`
|
||||
- `INDEX(area)`
|
||||
- `INDEX(status)`
|
||||
- `INDEX(deleted_at)`
|
||||
- `INDEX(username)`
|
||||
|
||||
### 字段语义正式化
|
||||
|
||||
- `sub`:统一身份唯一标识
|
||||
- `username`:登录名 / 工号 / 展示账号
|
||||
- `nick_name`:真实姓名
|
||||
- `area`:用户主地区,也是默认数据隔离依据
|
||||
- `password`:短期兼容旧值,后续迁移为哈希
|
||||
|
||||
## 4.2 `roles` 目标策略
|
||||
|
||||
继续沿用:
|
||||
|
||||
- `role_key`:机器标识,例如 `provincial_admin`
|
||||
- `role_name`:中文展示名,例如“省级管理员”
|
||||
- `data_scope`:默认数据范围
|
||||
|
||||
建议正式保留的角色:
|
||||
|
||||
- `provincial_admin`
|
||||
- `admin`
|
||||
- `common`
|
||||
- `super_admin`(可选)
|
||||
|
||||
## 4.3 `permissions` 目标策略
|
||||
|
||||
继续沿用:
|
||||
|
||||
- `permission_key`
|
||||
- `api_path`
|
||||
- `api_method`
|
||||
- `route_id`
|
||||
|
||||
建议后续所有新权限都按下面格式命名:
|
||||
|
||||
- `auth:me:read`
|
||||
- `documents:list:read`
|
||||
- `documents:upload:write`
|
||||
- `audit:run:execute`
|
||||
- `rules:binding:update`
|
||||
|
||||
不要混用:
|
||||
|
||||
- 一部分 `documents.list`
|
||||
- 一部分 `document:read:all`
|
||||
|
||||
必须统一成一种风格。
|
||||
|
||||
### 我建议
|
||||
|
||||
直接收口成老系统已有风格:
|
||||
|
||||
- `module:resource:action`
|
||||
|
||||
这样能和老数据完全对齐。
|
||||
|
||||
## 4.4 `role_permissions` 目标策略
|
||||
|
||||
继续沿用:
|
||||
|
||||
- `grant_type`
|
||||
- `data_scope`
|
||||
|
||||
建议唯一约束明确为:
|
||||
|
||||
- `(role_id, permission_id, grant_type, data_scope)`
|
||||
|
||||
但在第一阶段实际配置时:
|
||||
|
||||
- 尽量只放 `GRANT`
|
||||
- `data_scope` 按权限点是否涉及数据决定
|
||||
|
||||
## 4.5 `user_role` 目标策略
|
||||
|
||||
继续沿用现有结构即可。
|
||||
|
||||
关键点不是表结构,而是后端逻辑必须支持:
|
||||
|
||||
- 一个用户可以挂多个角色
|
||||
- 登录时不能 `LIMIT 1`
|
||||
- 权限汇总和数据范围要支持多角色收敛
|
||||
|
||||
## 4.6 `sys_routes / role_route` 目标策略
|
||||
|
||||
继续沿用旧系统结构即可。
|
||||
|
||||
重点是:
|
||||
|
||||
- 前端菜单权限可以继续从这里出
|
||||
- 但 **接口权限不能只依赖菜单权限**
|
||||
- API 权限必须还是走 `permissions / role_permissions`
|
||||
|
||||
---
|
||||
|
||||
## 5. 当前逻辑漏洞清单(必须按优先级修)
|
||||
|
||||
## P0:必须先修
|
||||
|
||||
### 5.1 角色/权限字段命名统一
|
||||
|
||||
要统一到底使用:
|
||||
|
||||
- `role_key` 还是 `role_name`
|
||||
- `permission_key` 还是 `permission_code`
|
||||
|
||||
当前结论:
|
||||
|
||||
- 统一用 `role_key`
|
||||
- 统一用 `permission_key`
|
||||
|
||||
### 5.2 登录角色查询改成多角色
|
||||
|
||||
当前:
|
||||
|
||||
- `LIMIT 1`
|
||||
|
||||
必须改成:
|
||||
|
||||
- 查询全部角色
|
||||
- JWT 带 `roles`
|
||||
- 兼容保留一个 `user_role` 主角色字段给旧前端
|
||||
|
||||
### 5.3 OAuth 登录禁止直接覆盖 `area`
|
||||
|
||||
必须改成:
|
||||
|
||||
- 登录入参中的 `area` 不再直接写库
|
||||
- area 只能来自:
|
||||
- 老库已存在值
|
||||
- 后台管理维护
|
||||
- 后续可信组织同步
|
||||
|
||||
### 5.4 新用户自动分配默认角色
|
||||
|
||||
OAuth 首登新用户创建后,必须补:
|
||||
|
||||
- 自动赋 `common`
|
||||
|
||||
### 5.5 `/api/auth/me` 落地
|
||||
|
||||
要把当前用户的:
|
||||
|
||||
- 基本信息
|
||||
- `area`
|
||||
- `roles`
|
||||
- `permissions`
|
||||
|
||||
统一返回。
|
||||
|
||||
## P1:第二阶段紧跟着修
|
||||
|
||||
### 5.6 数据范围注入
|
||||
|
||||
至少要先覆盖:
|
||||
|
||||
- 文档列表
|
||||
- 文档详情
|
||||
- 用户列表
|
||||
- 评查状态 / 结果
|
||||
|
||||
### 5.7 密码校验升级
|
||||
|
||||
短期兼容旧明文,长期迁移到哈希:
|
||||
|
||||
- 登录时支持“旧明文兼容 + 新哈希优先”
|
||||
- 用户下次修改密码时写入哈希
|
||||
|
||||
### 5.8 权限缓存 / 权限聚合
|
||||
|
||||
后续可以考虑:
|
||||
|
||||
- JWT 轻载
|
||||
- 服务端缓存权限聚合结果
|
||||
|
||||
但这不是 P0。
|
||||
|
||||
---
|
||||
|
||||
## 6. 老系统用户数据迁移策略
|
||||
|
||||
## 6.1 迁移原则
|
||||
|
||||
不是“把老用户复制一份到新表”,而是:
|
||||
|
||||
- **尽可能复用原表结构**
|
||||
- 用增量清洗 + 角色校正 + 地区校正的方式迁移
|
||||
|
||||
## 6.2 迁移对象
|
||||
|
||||
第一阶段只迁这几类数据:
|
||||
|
||||
1. `sso_users`
|
||||
2. `roles`
|
||||
3. `user_role`
|
||||
4. `permissions`
|
||||
5. `role_permissions`
|
||||
6. `sys_routes`
|
||||
7. `role_route`
|
||||
|
||||
## 6.3 `sso_users` 迁移规则
|
||||
|
||||
### 必迁字段
|
||||
|
||||
- `id`
|
||||
- `sub`
|
||||
- `username`
|
||||
- `nick_name`
|
||||
- `phone_number`
|
||||
- `email`
|
||||
- `ou_id`
|
||||
- `ou_name`
|
||||
- `status`
|
||||
- `is_leader`
|
||||
- `password`
|
||||
- `area`
|
||||
- `tenant_name`
|
||||
- `dep_name`
|
||||
- `dep_short_name`
|
||||
- `created_at`
|
||||
- `updated_at`
|
||||
- `deleted_at`
|
||||
|
||||
### 可选迁字段
|
||||
|
||||
- `mq_person_uuid`
|
||||
- `mq_account_uuid`
|
||||
- `mq_synced_at`
|
||||
- `try_count`
|
||||
- `try_login_time`
|
||||
|
||||
### 迁移前校验
|
||||
|
||||
要先出统计:
|
||||
|
||||
- `sub` 是否有重复
|
||||
- `username` 是否有重复
|
||||
- `area` 是否为空
|
||||
- `status != 0` 的禁用用户数量
|
||||
- `deleted_at is not null` 的软删除用户数量
|
||||
|
||||
## 6.4 角色映射规则
|
||||
|
||||
要先梳理老库中真实 `role_key` 分布。
|
||||
|
||||
预期映射:
|
||||
|
||||
- `provincial_admin` → 保留
|
||||
- `admin` → 保留
|
||||
- `common` → 保留
|
||||
- 其他历史角色:
|
||||
- 先做映射表
|
||||
- 无法归类的先落到 `common`
|
||||
- 再人工复核
|
||||
|
||||
### 明确不再延续的角色
|
||||
|
||||
- `city_admin`
|
||||
- `review_manager`
|
||||
- `review_user`
|
||||
- `rule_admin`
|
||||
|
||||
如果老库真的还残留这些历史角色,不建议直接带入新系统业务角色定义,应该做映射清洗。
|
||||
|
||||
## 6.5 area 清洗规则
|
||||
|
||||
迁移前要先把 `sso_users.area` 跑一遍质量检查:
|
||||
|
||||
- 是否为空
|
||||
- 是否存在历史别名
|
||||
- 是否存在前后空格
|
||||
- 是否存在同义不同写法
|
||||
|
||||
最后要统一成一套稳定值,例如:
|
||||
|
||||
- `省局`
|
||||
- `梅州`
|
||||
- `云浮`
|
||||
- `揭阳`
|
||||
- `潮州`
|
||||
|
||||
---
|
||||
|
||||
## 7. 开发分阶段 TaskList
|
||||
|
||||
下面是正式开发顺序。
|
||||
|
||||
## 阶段 A:数据库基线收口
|
||||
|
||||
### A1. 先核对当前库真实表结构
|
||||
|
||||
目标:
|
||||
|
||||
- 以当前数据库真实字段为准
|
||||
- 不再拿“理想 SQL”直接执行
|
||||
|
||||
动作:
|
||||
|
||||
- 检查 `sso_users`
|
||||
- 检查 `roles`
|
||||
- 检查 `user_role`
|
||||
- 检查 `permissions`
|
||||
- 检查 `role_permissions`
|
||||
- 检查 `sys_routes`
|
||||
- 检查 `role_route`
|
||||
|
||||
产出:
|
||||
|
||||
- 一份“现网真实表结构快照”
|
||||
- 一份“目标结构差异清单”
|
||||
|
||||
### A2. 出正式迁移 SQL
|
||||
|
||||
目标:
|
||||
|
||||
- 不是重建表
|
||||
- 是补字段 / 补索引 / 补约束 / 补初始化数据
|
||||
|
||||
动作:
|
||||
|
||||
- 修正角色初始化 SQL
|
||||
- 修正权限初始化 SQL
|
||||
- 修正菜单初始化 SQL
|
||||
- 统一全部字段名到现有真实库
|
||||
|
||||
产出:
|
||||
|
||||
- `用户权限_schema_patch.sql`
|
||||
- `用户权限_seed.sql`
|
||||
|
||||
## 阶段 B:登录与 JWT 收口
|
||||
|
||||
### B1. 修 `AuthServiceImpl`
|
||||
|
||||
必须改:
|
||||
|
||||
- 多角色查询
|
||||
- 新用户默认角色分配
|
||||
- OAuth 登录禁止外部覆盖 `area`
|
||||
- 登录响应统一返回 `roles`
|
||||
- 兼容返回单个 `user_role`
|
||||
|
||||
### B2. 修 `JwtService`
|
||||
|
||||
必须改:
|
||||
|
||||
- token 中写入 `roles`
|
||||
- 保留 `user_role`
|
||||
- 写入 `area`
|
||||
- 视情况写入精简 `permissions`
|
||||
|
||||
### B3. 新增 `/api/auth/me`
|
||||
|
||||
返回:
|
||||
|
||||
- 用户基本信息
|
||||
- area
|
||||
- roles
|
||||
- permissions
|
||||
|
||||
## 阶段 C:权限聚合与数据范围引擎
|
||||
|
||||
### C1. 做统一权限聚合服务
|
||||
|
||||
目标:
|
||||
|
||||
- 给定 `user_id`
|
||||
- 返回:
|
||||
- 角色列表
|
||||
- 权限列表
|
||||
- 每个权限点最终 data_scope
|
||||
|
||||
### C2. 做统一数据范围判定器
|
||||
|
||||
目标:
|
||||
|
||||
- 输入:用户 + 权限点 + 业务资源
|
||||
- 输出:
|
||||
- `ALL`
|
||||
- `DEPT`
|
||||
- `SELF`
|
||||
|
||||
### C3. 先接到核心接口
|
||||
|
||||
优先改:
|
||||
|
||||
- `/api/documents/list`
|
||||
- `/api/audit/run/{RunId}`
|
||||
- `/api/audit/result/{RunId}`
|
||||
- `/api/users/list`
|
||||
|
||||
## 阶段 D:老用户数据迁移
|
||||
|
||||
### D1. 出迁移前检查 SQL
|
||||
|
||||
统计:
|
||||
|
||||
- 用户数
|
||||
- 角色分布
|
||||
- area 分布
|
||||
- 无角色用户
|
||||
- 空 area 用户
|
||||
- 重复 sub / username
|
||||
|
||||
### D2. 出迁移脚本
|
||||
|
||||
做法:
|
||||
|
||||
- 先备份
|
||||
- 再清洗 area
|
||||
- 再导入 / 映射角色
|
||||
- 再补默认角色
|
||||
- 再跑校验
|
||||
|
||||
### D3. 迁移后验收
|
||||
|
||||
验收项:
|
||||
|
||||
- 登录正常
|
||||
- `/api/auth/me` 正常
|
||||
- 文档列表按地区隔离正常
|
||||
- admin 不能跨区
|
||||
- provincial_admin 能看全局
|
||||
- common 只能看自己
|
||||
|
||||
## 阶段 E:安全与运维收口
|
||||
|
||||
### E1. 密码升级方案
|
||||
|
||||
- 兼容旧明文
|
||||
- 新密码写哈希
|
||||
- 后续批量升级
|
||||
|
||||
### E2. 审计日志
|
||||
|
||||
至少记录:
|
||||
|
||||
- 登录成功/失败
|
||||
- 角色变更
|
||||
- 用户地区变更
|
||||
- 权限分配变更
|
||||
|
||||
### E3. 文档留档
|
||||
|
||||
要把以下文档保持最新:
|
||||
|
||||
- `docs/用户与地区权限完整设计方案.md`
|
||||
- `docs/接口/用户权限与权限点清单.md`
|
||||
- `docs/用户权限开发TaskList.md`
|
||||
|
||||
---
|
||||
|
||||
## 8. 推荐的实际开发顺序
|
||||
|
||||
如果按“马上开始干”的节奏,建议严格按下面顺序:
|
||||
|
||||
1. 先检查当前库真实 RBAC 表结构
|
||||
2. 再把初始化 SQL 改成真实可执行版
|
||||
3. 再改登录与 JWT
|
||||
4. 再补 `/api/auth/me`
|
||||
5. 再做权限聚合与数据范围注入
|
||||
6. 再做老用户迁移脚本
|
||||
7. 最后再做密码升级和审计收口
|
||||
|
||||
这个顺序的原因很简单:
|
||||
|
||||
- 不先确认表结构,后面代码全会写偏
|
||||
- 不先收口登录态,前端无法稳定接入
|
||||
- 不先做数据范围,地区隔离只是纸面方案
|
||||
- 不最后做迁移,容易把脏数据带着跑
|
||||
|
||||
---
|
||||
|
||||
## 9. 我建议下一步立刻做什么
|
||||
|
||||
下一步最应该做的不是直接改 controller,而是:
|
||||
|
||||
### 第一步
|
||||
|
||||
先把**当前数据库里 7 张核心表的真实结构**完整导出来:
|
||||
|
||||
- `sso_users`
|
||||
- `roles`
|
||||
- `user_role`
|
||||
- `permissions`
|
||||
- `role_permissions`
|
||||
- `sys_routes`
|
||||
- `role_route`
|
||||
|
||||
### 第二步
|
||||
|
||||
基于真实结构,产出两份正式 SQL:
|
||||
|
||||
- schema patch SQL
|
||||
- seed SQL
|
||||
|
||||
### 第三步
|
||||
|
||||
再开始改:
|
||||
|
||||
- `authServiceImpl.py`
|
||||
- `jwtService.py`
|
||||
- `/api/auth/me`
|
||||
|
||||
也就是说,**下一阶段开发入口就是“数据库真相收口”**。
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -18,7 +18,7 @@ import jwt
|
||||
from fastapi_common.fastapi_common_logger import logger
|
||||
from fastapi_admin.config import JWT_SECRET_KEY, JWT_ALGORITHM
|
||||
|
||||
ACCESS_TOKEN_EXPIRE_MINUTES = 15
|
||||
ACCESS_TOKEN_EXPIRE_MINUTES = 60
|
||||
REFRESH_TOKEN_EXPIRE_DAYS = 7
|
||||
JWT_AUDIENCE = "leaudit-platform"
|
||||
JWT_ISSUER = "leaudit-platform"
|
||||
@@ -39,6 +39,7 @@ class JwtService:
|
||||
ouId: str = "",
|
||||
ouName: str = "",
|
||||
roles: list[str] | None = None,
|
||||
permissions: list[str] | None = None,
|
||||
area: str | None = None,
|
||||
userRole: str | None = None,
|
||||
deviceId: str | None = None,
|
||||
@@ -68,6 +69,7 @@ class JwtService:
|
||||
"ou_id": ouId,
|
||||
"ou_name": ouName,
|
||||
"roles": roles or [],
|
||||
"permissions": permissions or [],
|
||||
"area": area,
|
||||
"user_role": userRole,
|
||||
"iat": now,
|
||||
|
||||
@@ -5,19 +5,40 @@ from __future__ import annotations
|
||||
from typing import Any
|
||||
|
||||
import jwt
|
||||
from fastapi import Request
|
||||
from fastapi import HTTPException, Request, status
|
||||
|
||||
from fastapi_admin.config import JWT_SECRET_KEY, JWT_ALGORITHM
|
||||
from fastapi_common.fastapi_common_security.jwtService import JwtService
|
||||
|
||||
|
||||
def verify_access_token(RequestObj: Request) -> dict[str, Any]:
|
||||
"""验证 JWT access token 并返回 payload。"""
|
||||
"""验证 JWT access token 并返回 payload。
|
||||
|
||||
认证失败必须直接返回 401,不能静默放行为空 payload,
|
||||
否则受保护接口会在后续逻辑里变成“假鉴权”。
|
||||
"""
|
||||
auth = RequestObj.headers.get("Authorization", "")
|
||||
if not auth.startswith("Bearer "):
|
||||
return {}
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="缺少有效的 Authorization Bearer Token",
|
||||
)
|
||||
|
||||
token = auth.removeprefix("Bearer ").strip()
|
||||
try:
|
||||
payload = jwt.decode(token, JWT_SECRET_KEY, algorithms=[JWT_ALGORITHM])
|
||||
payload = JwtService.verify(token)
|
||||
if payload.get("type") != "access":
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="无效的访问令牌类型",
|
||||
)
|
||||
return payload
|
||||
except jwt.ExpiredSignatureError as exc:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="访问令牌已过期",
|
||||
) from exc
|
||||
except jwt.PyJWTError:
|
||||
return {}
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="访问令牌无效",
|
||||
)
|
||||
|
||||
@@ -4,18 +4,22 @@
|
||||
POST /auth/login — 统一登录(OAuth + 密码自动检测)
|
||||
POST /auth/password_login — 账密登录
|
||||
|
||||
响应格式按新项目规范使用 Result。
|
||||
前端当前统一按 ``success + message + data`` 解析登录结果,
|
||||
这里显式对齐该契约,避免登录成功却被前端误判为失败。
|
||||
"""
|
||||
|
||||
from fastapi import Request
|
||||
from typing import Any
|
||||
|
||||
from fastapi import Depends, Request
|
||||
from fastapi.responses import JSONResponse
|
||||
|
||||
from fastapi_common.fastapi_common_web.controller import BaseController
|
||||
from fastapi_common.fastapi_common_web.domain.responses import Result
|
||||
from fastapi_common.fastapi_common_web.exception.LeauditException import LeauditException
|
||||
from fastapi_common.fastapi_common_logger import logger
|
||||
from fastapi_common.fastapi_common_security.security import verify_access_token
|
||||
|
||||
from fastapi_modules.fastapi_leaudit.domian.Dto.auth.loginDto import PasswordLoginDTO, OAuthLoginDTO
|
||||
from fastapi_modules.fastapi_leaudit.domian.vo.auth.loginTokenVo import LoginTokenVO
|
||||
from fastapi_modules.fastapi_leaudit.domian.Dto.auth.loginDto import PasswordLoginDTO
|
||||
from fastapi_modules.fastapi_leaudit.services import IAuthService
|
||||
from fastapi_modules.fastapi_leaudit.services.impl.authServiceImpl import AuthServiceImpl
|
||||
|
||||
@@ -60,10 +64,13 @@ class AuthController(BaseController):
|
||||
Password=requestData["password"],
|
||||
)
|
||||
else:
|
||||
return JSONResponse(status_code=400, content={"code": 400, "message": "无效的登录请求格式", "data": None})
|
||||
return JSONResponse(
|
||||
status_code=400,
|
||||
content={"success": False, "message": "无效的登录请求格式", "data": None},
|
||||
)
|
||||
|
||||
return JSONResponse(status_code=200, content={
|
||||
"code": 200,
|
||||
"success": True,
|
||||
"message": "ok",
|
||||
"data": {
|
||||
"access_token": vo.access_token,
|
||||
@@ -74,10 +81,16 @@ class AuthController(BaseController):
|
||||
},
|
||||
})
|
||||
|
||||
except LeauditException as e:
|
||||
logger.error(f"登录失败: {e.message}")
|
||||
return JSONResponse(
|
||||
status_code=e.statusCode,
|
||||
content={"success": False, "message": e.message, "data": None},
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"登录失败: {e}")
|
||||
return JSONResponse(status_code=401, content={
|
||||
"code": 401, "message": str(e), "data": None,
|
||||
"success": False, "message": "登录失败,请稍后重试", "data": None,
|
||||
})
|
||||
|
||||
@self.router.post("/password_login")
|
||||
@@ -88,10 +101,22 @@ class AuthController(BaseController):
|
||||
dto = PasswordLoginDTO(**requestData)
|
||||
vo = await self.AuthService.PasswordLogin(Sub=dto.sub, Password=dto.password)
|
||||
return JSONResponse(status_code=200, content={
|
||||
"code": 200, "message": "ok", "data": vo.model_dump(),
|
||||
"success": True, "message": "ok", "data": vo.model_dump(),
|
||||
})
|
||||
except LeauditException as e:
|
||||
logger.error(f"密码登录失败: {e.message}")
|
||||
return JSONResponse(
|
||||
status_code=e.statusCode,
|
||||
content={"success": False, "message": e.message, "data": None},
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"密码登录失败: {e}")
|
||||
return JSONResponse(status_code=401, content={
|
||||
"code": 401, "message": str(e), "data": None,
|
||||
"success": False, "message": "登录失败,请稍后重试", "data": None,
|
||||
})
|
||||
|
||||
@self.router.get("/me", response_model=Result[dict[str, Any]])
|
||||
async def GetCurrentUser(payload: dict[str, Any] = Depends(verify_access_token)):
|
||||
"""获取当前登录用户信息。"""
|
||||
Data = await self.AuthService.GetCurrentUser(UserId=int(payload["user_id"]))
|
||||
return Result.success(data=Data)
|
||||
|
||||
@@ -20,3 +20,8 @@ class IAuthService(ABC):
|
||||
IsLeader: bool | None, Area: str | None, ExpiresIn: int) -> LoginTokenVO:
|
||||
"""OAuth 登录。"""
|
||||
...
|
||||
|
||||
@abstractmethod
|
||||
async def GetCurrentUser(self, UserId: int) -> dict:
|
||||
"""获取当前登录用户信息。"""
|
||||
...
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
"""认证服务实现。
|
||||
"""认证服务实现。"""
|
||||
|
||||
从旧项目 app/routes/auth.py 和 app/auth/auth.py 迁移,业务逻辑完全不变。
|
||||
仅重组为 Controller → Service(interface+impl) → Model 结构。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any
|
||||
|
||||
from fastapi_common.fastapi_common_logger import logger
|
||||
from fastapi_common.fastapi_common_sqlalchemy.database import GetAsyncSession
|
||||
from fastapi_common.fastapi_common_security.jwtService import JwtService
|
||||
from fastapi_common.fastapi_common_web.exception.LeauditException import LeauditException
|
||||
from fastapi_common.fastapi_common_sqlalchemy.database import GetAsyncSession
|
||||
from fastapi_common.fastapi_common_web.domain.responses import StatusCodeEnum
|
||||
from fastapi_common.fastapi_common_web.exception.LeauditException import LeauditException
|
||||
|
||||
from fastapi_modules.fastapi_leaudit.domian.vo.auth.loginTokenVo import LoginTokenVO
|
||||
from fastapi_modules.fastapi_leaudit.services.authService import IAuthService
|
||||
@@ -20,55 +21,73 @@ class AuthServiceImpl(IAuthService):
|
||||
async def PasswordLogin(self, Sub: str, Password: str) -> LoginTokenVO:
|
||||
"""账密登录。
|
||||
|
||||
校验 sso_users 表:sub + password + status=0 + deleted_at IS NULL。
|
||||
安全:统一错误提示"账号或密码错误",防止用户枚举。
|
||||
现阶段仍兼容旧库明文密码,后续应迁移到哈希校验。
|
||||
"""
|
||||
async with GetAsyncSession() as session:
|
||||
from sqlalchemy import select, text
|
||||
from sqlalchemy import text
|
||||
|
||||
result = await session.execute(
|
||||
text("SELECT id, sub, username, nick_name, phone_number, email, "
|
||||
"ou_id, ou_name, is_leader, password, status, deleted_at, "
|
||||
"try_count, try_login_time, area, tenant_name, dep_name, dep_short_name "
|
||||
"FROM sso_users WHERE sub = :sub"),
|
||||
text(
|
||||
"SELECT id, sub, username, nick_name, phone_number, email, "
|
||||
"ou_id, ou_name, is_leader, password, status, deleted_at, "
|
||||
"try_count, try_login_time, area, tenant_name, dep_name, dep_short_name "
|
||||
"FROM sso_users WHERE sub = :sub"
|
||||
),
|
||||
{"sub": Sub},
|
||||
)
|
||||
row = result.fetchone()
|
||||
|
||||
if not row:
|
||||
logger.warning(f"登录失败: 用户不存在 - sub={Sub}")
|
||||
logger.warning("登录失败: 用户不存在 - sub=%s", Sub)
|
||||
raise LeauditException(StatusCodeEnum.HTTP_401_UNAUTHORIZED, "账号或密码错误")
|
||||
|
||||
user = dict(row._mapping)
|
||||
|
||||
if user.get("deleted_at") is not None:
|
||||
logger.warning(f"登录失败: 账号已删除 - sub={Sub}")
|
||||
logger.warning("登录失败: 账号已删除 - sub=%s", Sub)
|
||||
raise LeauditException(StatusCodeEnum.HTTP_401_UNAUTHORIZED, "账号或密码错误")
|
||||
|
||||
if user.get("status") != 0:
|
||||
logger.warning(f"登录失败: 账号已禁用 - sub={Sub}, status={user.get('status')}")
|
||||
logger.warning("登录失败: 账号已禁用 - sub=%s, status=%s", Sub, user.get("status"))
|
||||
raise LeauditException(StatusCodeEnum.HTTP_401_UNAUTHORIZED, "账号或密码错误")
|
||||
|
||||
if user.get("password") != Password:
|
||||
logger.warning(f"登录失败: 密码错误 - sub={Sub}")
|
||||
logger.warning("登录失败: 密码错误 - sub=%s", Sub)
|
||||
raise LeauditException(StatusCodeEnum.HTTP_401_UNAUTHORIZED, "账号或密码错误")
|
||||
|
||||
await self._ensure_default_role(session, user["id"])
|
||||
return await self._buildLoginResponse(user, session)
|
||||
|
||||
async def OAuthLogin(self, Sub: str, Username: str | None, Nickname: str | None,
|
||||
Email: str | None, PhoneNumber: str | None,
|
||||
OuId: str | None, OuName: str | None,
|
||||
IsLeader: bool | None, Area: str | None, ExpiresIn: int) -> LoginTokenVO:
|
||||
"""OAuth 登录。验证 sub 是否存在,不存在则自动创建用户。"""
|
||||
async def OAuthLogin(
|
||||
self,
|
||||
Sub: str,
|
||||
Username: str | None,
|
||||
Nickname: str | None,
|
||||
Email: str | None,
|
||||
PhoneNumber: str | None,
|
||||
OuId: str | None,
|
||||
OuName: str | None,
|
||||
IsLeader: bool | None,
|
||||
Area: str | None,
|
||||
ExpiresIn: int,
|
||||
) -> LoginTokenVO:
|
||||
"""OAuth 登录。
|
||||
|
||||
当前阶段 area 不能被前端登录请求直接覆盖。
|
||||
如果用户不存在,则仅创建基础账号信息,地区字段留待可信后台来源补齐。
|
||||
"""
|
||||
del Area, ExpiresIn
|
||||
|
||||
async with GetAsyncSession() as session:
|
||||
from sqlalchemy import select, text
|
||||
from datetime import datetime, timezone
|
||||
from sqlalchemy import text
|
||||
|
||||
result = await session.execute(
|
||||
text("SELECT id, sub, username, nick_name, phone_number, email, "
|
||||
"ou_id, ou_name, is_leader, status, deleted_at, "
|
||||
"area, tenant_name, dep_name, dep_short_name "
|
||||
"FROM sso_users WHERE sub = :sub"),
|
||||
text(
|
||||
"SELECT id, sub, username, nick_name, phone_number, email, "
|
||||
"ou_id, ou_name, is_leader, status, deleted_at, area, "
|
||||
"tenant_name, dep_name, dep_short_name "
|
||||
"FROM sso_users WHERE sub = :sub"
|
||||
),
|
||||
{"sub": Sub},
|
||||
)
|
||||
row = result.fetchone()
|
||||
@@ -77,86 +96,209 @@ class AuthServiceImpl(IAuthService):
|
||||
user = dict(row._mapping)
|
||||
if user.get("deleted_at") is not None or user.get("status") != 0:
|
||||
raise LeauditException(StatusCodeEnum.HTTP_401_UNAUTHORIZED, "账号已被禁用或删除")
|
||||
# 更新最后登录信息
|
||||
|
||||
await session.execute(
|
||||
text("UPDATE sso_users SET username = :username, nick_name = :nick, "
|
||||
"email = :email, phone_number = :phone, ou_id = :ou_id, "
|
||||
"ou_name = :ou_name, is_leader = :is_leader, area = :area, "
|
||||
"updated_at = :now WHERE id = :id"),
|
||||
{"username": Username, "nick": Nickname, "email": Email,
|
||||
"phone": PhoneNumber, "ou_id": OuId, "ou_name": OuName,
|
||||
"is_leader": IsLeader, "area": Area,
|
||||
"now": datetime.now(timezone.utc), "id": user["id"]},
|
||||
text(
|
||||
"UPDATE sso_users SET username = :username, nick_name = :nick, "
|
||||
"email = :email, phone_number = :phone, ou_id = :ou_id, "
|
||||
"ou_name = :ou_name, is_leader = :is_leader, "
|
||||
"updated_at = :now WHERE id = :id"
|
||||
),
|
||||
{
|
||||
"username": Username or user.get("username") or Sub,
|
||||
"nick": Nickname or user.get("nick_name") or Username or Sub,
|
||||
"email": Email,
|
||||
"phone": PhoneNumber,
|
||||
"ou_id": OuId or user.get("ou_id") or "",
|
||||
"ou_name": OuName or user.get("ou_name") or "",
|
||||
"is_leader": IsLeader if IsLeader is not None else user.get("is_leader"),
|
||||
"now": datetime.now(timezone.utc),
|
||||
"id": user["id"],
|
||||
},
|
||||
)
|
||||
else:
|
||||
# 自动创建用户
|
||||
await session.execute(
|
||||
text("INSERT INTO sso_users (sub, username, nick_name, email, "
|
||||
"phone_number, ou_id, ou_name, is_leader, area, status) "
|
||||
"VALUES (:sub, :username, :nick, :email, :phone, :ou_id, "
|
||||
":ou_name, :is_leader, :area, 0)"),
|
||||
{"sub": Sub, "username": Username, "nick": Nickname, "email": Email,
|
||||
"phone": PhoneNumber, "ou_id": OuId, "ou_name": OuName,
|
||||
"is_leader": IsLeader, "area": Area},
|
||||
created = await session.execute(
|
||||
text(
|
||||
"INSERT INTO sso_users (sub, username, nick_name, email, phone_number, "
|
||||
"ou_id, ou_name, is_leader, status, created_at, updated_at) "
|
||||
"VALUES (:sub, :username, :nick, :email, :phone, :ou_id, "
|
||||
":ou_name, :is_leader, 0, :now, :now) RETURNING id"
|
||||
),
|
||||
{
|
||||
"sub": Sub,
|
||||
"username": Username or Sub,
|
||||
"nick": Nickname or Username or Sub,
|
||||
"email": Email,
|
||||
"phone": PhoneNumber,
|
||||
"ou_id": OuId or "",
|
||||
"ou_name": OuName or "",
|
||||
"is_leader": bool(IsLeader),
|
||||
"now": datetime.now(timezone.utc),
|
||||
},
|
||||
)
|
||||
await session.commit()
|
||||
user_id = created.scalar_one()
|
||||
await self._ensure_default_role(session, user_id)
|
||||
|
||||
result = await session.execute(
|
||||
text("SELECT id, sub, username, nick_name, phone_number, email, "
|
||||
"ou_id, ou_name, is_leader, area, tenant_name, dep_name, dep_short_name "
|
||||
"FROM sso_users WHERE sub = :sub"),
|
||||
{"sub": Sub},
|
||||
)
|
||||
row = result.fetchone()
|
||||
user = dict(row._mapping) if row else {}
|
||||
result = await session.execute(
|
||||
text(
|
||||
"SELECT id, sub, username, nick_name, phone_number, email, "
|
||||
"ou_id, ou_name, is_leader, area, tenant_name, dep_name, dep_short_name "
|
||||
"FROM sso_users WHERE sub = :sub"
|
||||
),
|
||||
{"sub": Sub},
|
||||
)
|
||||
row = result.fetchone()
|
||||
user = dict(row._mapping) if row else {}
|
||||
|
||||
if not user:
|
||||
raise LeauditException(StatusCodeEnum.HTTP_500_INTERNAL_SERVER_ERROR, "用户创建失败")
|
||||
|
||||
await self._ensure_default_role(session, user["id"])
|
||||
return await self._buildLoginResponse(user, session)
|
||||
|
||||
async def _buildLoginResponse(self, user: dict, session) -> LoginTokenVO:
|
||||
"""组装登录响应:查询角色 → 签发 JWT → 返回 LoginTokenVO。"""
|
||||
from sqlalchemy import text
|
||||
async def GetCurrentUser(self, UserId: int) -> dict:
|
||||
"""获取当前登录用户信息。"""
|
||||
async with GetAsyncSession() as session:
|
||||
from sqlalchemy import text
|
||||
|
||||
# 查询用户角色
|
||||
roleResult = await session.execute(
|
||||
text("SELECT r.role_key FROM user_role ur "
|
||||
"JOIN roles r ON ur.role_id = r.id "
|
||||
"WHERE ur.user_id = :uid LIMIT 1"),
|
||||
{"uid": user["id"]},
|
||||
)
|
||||
roleRow = roleResult.fetchone()
|
||||
userRole = roleRow[0] if roleRow else "common"
|
||||
result = await session.execute(
|
||||
text(
|
||||
"SELECT id, sub, username, nick_name, phone_number, email, "
|
||||
"ou_id, ou_name, is_leader, status, deleted_at, area, "
|
||||
"tenant_name, dep_name, dep_short_name "
|
||||
"FROM sso_users WHERE id = :uid"
|
||||
),
|
||||
{"uid": UserId},
|
||||
)
|
||||
row = result.fetchone()
|
||||
if not row:
|
||||
raise LeauditException(StatusCodeEnum.HTTP_404_NOT_FOUND, "用户不存在")
|
||||
|
||||
user = dict(row._mapping)
|
||||
if user.get("deleted_at") is not None or user.get("status") != 0:
|
||||
raise LeauditException(StatusCodeEnum.HTTP_401_UNAUTHORIZED, "账号已被禁用或删除")
|
||||
|
||||
await self._ensure_default_role(session, user["id"])
|
||||
identity = await self._loadUserIdentity(session, user["id"])
|
||||
return self._buildUserInfo(user, identity)
|
||||
|
||||
async def _buildLoginResponse(self, user: dict[str, Any], session) -> LoginTokenVO:
|
||||
"""组装登录响应:查询角色/权限 → 签发 JWT。"""
|
||||
identity = await self._loadUserIdentity(session, user["id"])
|
||||
user_info = self._buildUserInfo(user, identity)
|
||||
|
||||
# 签发 JWT
|
||||
expiresIn = 3600 # 默认 1 小时
|
||||
tokens = JwtService.generate(
|
||||
userId=user["id"],
|
||||
username=user.get("username") or user.get("sub", ""),
|
||||
nickName=user.get("nick_name") or "",
|
||||
ouId=user.get("ou_id") or "",
|
||||
ouName=user.get("ou_name") or "",
|
||||
roles=identity["roles"],
|
||||
permissions=identity["permissions"],
|
||||
area=user.get("area"),
|
||||
userRole=userRole,
|
||||
userRole=identity["primary_role"],
|
||||
)
|
||||
|
||||
return LoginTokenVO(
|
||||
access_token=tokens["access_token"],
|
||||
token_type="Bearer",
|
||||
expires_in=expiresIn,
|
||||
expires_in=tokens["expires_in"],
|
||||
issued_time=tokens.get("issued_time", ""),
|
||||
user_info={
|
||||
"user_id": user["id"],
|
||||
"sub": user.get("sub"),
|
||||
"username": user.get("username"),
|
||||
"nick_name": user.get("nick_name"),
|
||||
"email": user.get("email"),
|
||||
"phone_number": user.get("phone_number"),
|
||||
"ou_id": user.get("ou_id"),
|
||||
"ou_name": user.get("ou_name"),
|
||||
"is_leader": user.get("is_leader"),
|
||||
"area": user.get("area"),
|
||||
"role": userRole,
|
||||
},
|
||||
user_info=user_info,
|
||||
)
|
||||
|
||||
async def _ensure_default_role(self, session, user_id: int) -> None:
|
||||
"""确保用户至少拥有一个默认 common 角色。"""
|
||||
from sqlalchemy import text
|
||||
|
||||
role_count = await session.execute(
|
||||
text("SELECT COUNT(*) FROM user_role WHERE user_id = :uid"),
|
||||
{"uid": user_id},
|
||||
)
|
||||
if (role_count.scalar_one() or 0) > 0:
|
||||
return
|
||||
|
||||
common_role = await session.execute(
|
||||
text("SELECT id FROM roles WHERE role_key = 'common' LIMIT 1")
|
||||
)
|
||||
common_role_id = common_role.scalar_one_or_none()
|
||||
if common_role_id is None:
|
||||
logger.warning("默认角色 common 不存在,无法为用户 %s 自动分配角色", user_id)
|
||||
return
|
||||
|
||||
await session.execute(
|
||||
text(
|
||||
"INSERT INTO user_role (user_id, role_id, created_at, updated_at) "
|
||||
"VALUES (:uid, :rid, :now, :now) "
|
||||
"ON CONFLICT (user_id, role_id) DO NOTHING"
|
||||
),
|
||||
{"uid": user_id, "rid": common_role_id, "now": datetime.now(timezone.utc)},
|
||||
)
|
||||
logger.info("已为用户 %s 自动补默认角色 common", user_id)
|
||||
|
||||
async def _loadUserIdentity(self, session, user_id: int) -> dict[str, Any]:
|
||||
"""加载用户角色和权限聚合结果。"""
|
||||
from sqlalchemy import text
|
||||
|
||||
role_rows = await session.execute(
|
||||
text(
|
||||
"SELECT r.role_key, COALESCE(r.priority, 0) AS priority "
|
||||
"FROM user_role ur "
|
||||
"JOIN roles r ON ur.role_id = r.id "
|
||||
"WHERE ur.user_id = :uid "
|
||||
"ORDER BY COALESCE(r.priority, 0) DESC, r.id ASC"
|
||||
),
|
||||
{"uid": user_id},
|
||||
)
|
||||
roles = [row[0] for row in role_rows.fetchall()]
|
||||
if not roles:
|
||||
roles = ["common"]
|
||||
|
||||
perm_rows = await session.execute(
|
||||
text(
|
||||
"SELECT p.permission_key, rp.grant_type "
|
||||
"FROM user_role ur "
|
||||
"JOIN roles r ON ur.role_id = r.id "
|
||||
"JOIN role_permissions rp ON r.id = rp.role_id "
|
||||
"JOIN permissions p ON rp.permission_id = p.id "
|
||||
"WHERE ur.user_id = :uid"
|
||||
),
|
||||
{"uid": user_id},
|
||||
)
|
||||
|
||||
grants: set[str] = set()
|
||||
denies: set[str] = set()
|
||||
for permission_key, grant_type in perm_rows.fetchall():
|
||||
if grant_type == "DENY":
|
||||
denies.add(permission_key)
|
||||
else:
|
||||
grants.add(permission_key)
|
||||
|
||||
permissions = sorted(grants - denies)
|
||||
return {
|
||||
"roles": roles,
|
||||
"primary_role": roles[0],
|
||||
"permissions": permissions,
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _buildUserInfo(user: dict[str, Any], identity: dict[str, Any]) -> dict[str, Any]:
|
||||
"""组装统一用户信息。"""
|
||||
return {
|
||||
"user_id": user["id"],
|
||||
"sub": user.get("sub"),
|
||||
"username": user.get("username"),
|
||||
"nick_name": user.get("nick_name"),
|
||||
"email": user.get("email"),
|
||||
"phone_number": user.get("phone_number"),
|
||||
"ou_id": user.get("ou_id"),
|
||||
"ou_name": user.get("ou_name"),
|
||||
"is_leader": user.get("is_leader"),
|
||||
"area": user.get("area"),
|
||||
"user_role": identity["primary_role"],
|
||||
"roles": identity["roles"],
|
||||
"permissions": identity["permissions"],
|
||||
"tenant_name": user.get("tenant_name"),
|
||||
"dep_name": user.get("dep_name"),
|
||||
"dep_short_name": user.get("dep_short_name"),
|
||||
}
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
-- ==========================================================================
|
||||
-- LeAudit Platform RBAC / User 中文注释补丁
|
||||
-- 目标:补齐 sso_users / roles / user_role / permissions / role_permissions /
|
||||
-- sys_routes / role_route 的表注释与列注释
|
||||
-- ============================================================================
|
||||
|
||||
BEGIN;
|
||||
|
||||
-- --------------------------------------------------------------------------
|
||||
-- 1. sso_users
|
||||
-- --------------------------------------------------------------------------
|
||||
COMMENT ON TABLE sso_users IS '用户主表:统一承载认证身份、组织信息、登录信息与单地区隔离核心字段';
|
||||
COMMENT ON COLUMN sso_users.id IS '主键ID';
|
||||
COMMENT ON COLUMN sso_users.sub IS '统一身份唯一标识,OAuth/SSO 主键';
|
||||
COMMENT ON COLUMN sso_users.username IS '登录名/工号/展示账号';
|
||||
COMMENT ON COLUMN sso_users.nick_name IS '用户真实姓名';
|
||||
COMMENT ON COLUMN sso_users.phone_number IS '手机号';
|
||||
COMMENT ON COLUMN sso_users.email IS '邮箱地址';
|
||||
COMMENT ON COLUMN sso_users.ou_id IS '所属组织单位ID/部门ID';
|
||||
COMMENT ON COLUMN sso_users.ou_name IS '所属组织单位名称/部门名称';
|
||||
COMMENT ON COLUMN sso_users.status IS '账户状态:0=正常,1=禁用';
|
||||
COMMENT ON COLUMN sso_users.is_leader IS '是否为负责人';
|
||||
COMMENT ON COLUMN sso_users.created_at IS '创建时间';
|
||||
COMMENT ON COLUMN sso_users.updated_at IS '更新时间';
|
||||
COMMENT ON COLUMN sso_users.deleted_at IS '软删除时间';
|
||||
COMMENT ON COLUMN sso_users.password IS '密码字段:当前阶段兼容旧值,后续应迁移为哈希';
|
||||
COMMENT ON COLUMN sso_users.try_count IS '尝试登录次数';
|
||||
COMMENT ON COLUMN sso_users.try_login_time IS '最近一次尝试登录时间';
|
||||
COMMENT ON COLUMN sso_users.area IS '用户主地区,当前系统唯一数据隔离字段';
|
||||
COMMENT ON COLUMN sso_users.mq_person_uuid IS '关联组织/人员同步系统中的人员UUID';
|
||||
COMMENT ON COLUMN sso_users.mq_account_uuid IS '关联组织/账号同步系统中的账号UUID';
|
||||
COMMENT ON COLUMN sso_users.mq_synced_at IS '最近一次组织/账号同步时间';
|
||||
COMMENT ON COLUMN sso_users.tenant_name IS '租户名称/管理单元名称';
|
||||
COMMENT ON COLUMN sso_users.dep_short_name IS '组织简称';
|
||||
COMMENT ON COLUMN sso_users.dep_name IS '组织名称';
|
||||
|
||||
-- --------------------------------------------------------------------------
|
||||
-- 2. roles
|
||||
-- --------------------------------------------------------------------------
|
||||
COMMENT ON TABLE roles IS '角色表:定义系统角色及其默认数据范围,当前主业务角色为 provincial_admin/admin/common';
|
||||
COMMENT ON COLUMN roles.id IS '主键ID';
|
||||
COMMENT ON COLUMN roles.role_key IS '角色机器标识,例如 provincial_admin/admin/common';
|
||||
COMMENT ON COLUMN roles.role_name IS '角色展示名称';
|
||||
COMMENT ON COLUMN roles.data_scope IS '默认数据范围:ALL=全部,DEPT=同地区,SELF=仅自己,GROUP仅保留兼容';
|
||||
COMMENT ON COLUMN roles.description IS '角色描述';
|
||||
COMMENT ON COLUMN roles.created_at IS '创建时间';
|
||||
COMMENT ON COLUMN roles.updated_at IS '更新时间';
|
||||
COMMENT ON COLUMN roles.parent_role_id IS '父角色ID,用于角色继承';
|
||||
COMMENT ON COLUMN roles.priority IS '角色优先级,数值越大优先级越高';
|
||||
COMMENT ON COLUMN roles.is_system_role IS '是否系统内置角色';
|
||||
COMMENT ON COLUMN roles.permissions_cache IS '权限缓存JSON,当前阶段不作为核心依赖';
|
||||
COMMENT ON COLUMN roles.metadata IS '扩展元数据JSON';
|
||||
|
||||
-- --------------------------------------------------------------------------
|
||||
-- 3. user_role
|
||||
-- --------------------------------------------------------------------------
|
||||
COMMENT ON TABLE user_role IS '用户角色关联表:一个用户可挂多个角色,用于聚合功能权限与数据范围';
|
||||
COMMENT ON COLUMN user_role.id IS '主键ID';
|
||||
COMMENT ON COLUMN user_role.user_id IS '用户ID,关联 sso_users.id';
|
||||
COMMENT ON COLUMN user_role.role_id IS '角色ID,关联 roles.id';
|
||||
COMMENT ON COLUMN user_role.created_at IS '创建时间';
|
||||
COMMENT ON COLUMN user_role.updated_at IS '更新时间';
|
||||
|
||||
-- --------------------------------------------------------------------------
|
||||
-- 4. permissions
|
||||
-- --------------------------------------------------------------------------
|
||||
COMMENT ON TABLE permissions IS '权限点定义表:统一定义 API/UI/数据权限点,权限键采用 module:resource:action 风格';
|
||||
COMMENT ON COLUMN permissions.id IS '主键ID';
|
||||
COMMENT ON COLUMN permissions.permission_key IS '权限键,例如 documents:list:read、audit:run:execute';
|
||||
COMMENT ON COLUMN permissions.module IS '所属模块,例如 auth/documents/audit/rules/users/rbac';
|
||||
COMMENT ON COLUMN permissions.resource IS '资源名,例如 list/detail/upload/run';
|
||||
COMMENT ON COLUMN permissions.action IS '动作名,例如 read/write/delete/execute';
|
||||
COMMENT ON COLUMN permissions.description IS '权限描述';
|
||||
COMMENT ON COLUMN permissions.display_name IS '权限展示名称';
|
||||
COMMENT ON COLUMN permissions.permission_type IS '权限类型:API/UI/DATA/RPC';
|
||||
COMMENT ON COLUMN permissions.is_system IS '是否系统内置权限';
|
||||
COMMENT ON COLUMN permissions.metadata IS '扩展元数据JSON';
|
||||
COMMENT ON COLUMN permissions.created_at IS '创建时间';
|
||||
COMMENT ON COLUMN permissions.updated_at IS '更新时间';
|
||||
COMMENT ON COLUMN permissions.created_by IS '创建人用户ID';
|
||||
COMMENT ON COLUMN permissions.updated_by IS '更新人用户ID';
|
||||
COMMENT ON COLUMN permissions.parent_id IS '父权限ID,用于构建权限树';
|
||||
COMMENT ON COLUMN permissions.sort_order IS '排序顺序';
|
||||
COMMENT ON COLUMN permissions.route_id IS '主关联路由ID,关联 sys_routes.id';
|
||||
COMMENT ON COLUMN permissions.api_path IS '对应后端 API 路径';
|
||||
COMMENT ON COLUMN permissions.api_method IS '对应后端 HTTP 方法';
|
||||
COMMENT ON COLUMN permissions.related_routes IS '共享权限可关联的多个路由ID列表';
|
||||
|
||||
-- --------------------------------------------------------------------------
|
||||
-- 5. role_permissions
|
||||
-- --------------------------------------------------------------------------
|
||||
COMMENT ON TABLE role_permissions IS '角色权限关联表:定义某角色拥有哪些权限点,以及该权限点对应的数据范围';
|
||||
COMMENT ON COLUMN role_permissions.id IS '主键ID';
|
||||
COMMENT ON COLUMN role_permissions.role_id IS '角色ID,关联 roles.id';
|
||||
COMMENT ON COLUMN role_permissions.permission_id IS '权限点ID,关联 permissions.id';
|
||||
COMMENT ON COLUMN role_permissions.grant_type IS '授权类型:GRANT=授予,DENY=拒绝';
|
||||
COMMENT ON COLUMN role_permissions.data_scope IS '数据范围:ALL=全部,DEPT=同地区,SELF=仅自己,GROUP仅保留兼容';
|
||||
COMMENT ON COLUMN role_permissions.condition_filter IS '高级条件过滤JSON,当前阶段不作为核心依赖';
|
||||
COMMENT ON COLUMN role_permissions.metadata IS '扩展元数据JSON';
|
||||
COMMENT ON COLUMN role_permissions.created_at IS '创建时间';
|
||||
COMMENT ON COLUMN role_permissions.created_by IS '创建人用户ID';
|
||||
COMMENT ON COLUMN role_permissions.updated_at IS '更新时间';
|
||||
COMMENT ON COLUMN role_permissions.updated_by IS '更新人用户ID';
|
||||
|
||||
-- --------------------------------------------------------------------------
|
||||
-- 6. sys_routes
|
||||
-- --------------------------------------------------------------------------
|
||||
COMMENT ON TABLE sys_routes IS '前端菜单/页面路由表:用于控制角色可见菜单,不替代 API 权限表';
|
||||
COMMENT ON COLUMN sys_routes.id IS '主键ID';
|
||||
COMMENT ON COLUMN sys_routes.route_path IS '前端路由路径';
|
||||
COMMENT ON COLUMN sys_routes.route_name IS '路由名称/内部标识';
|
||||
COMMENT ON COLUMN sys_routes.component IS '前端组件路径';
|
||||
COMMENT ON COLUMN sys_routes.parent_id IS '父路由ID';
|
||||
COMMENT ON COLUMN sys_routes.route_title IS '路由标题/菜单显示名';
|
||||
COMMENT ON COLUMN sys_routes.icon IS '菜单图标标识';
|
||||
COMMENT ON COLUMN sys_routes.sort_order IS '排序顺序';
|
||||
COMMENT ON COLUMN sys_routes.is_hidden IS '是否隐藏路由';
|
||||
COMMENT ON COLUMN sys_routes.is_cache IS '是否启用前端缓存';
|
||||
COMMENT ON COLUMN sys_routes.meta IS '路由扩展元数据JSON';
|
||||
COMMENT ON COLUMN sys_routes.status IS '状态:0=启用,1=禁用';
|
||||
COMMENT ON COLUMN sys_routes.created_at IS '创建时间';
|
||||
COMMENT ON COLUMN sys_routes.updated_at IS '更新时间';
|
||||
COMMENT ON COLUMN sys_routes.deleted_at IS '软删除时间';
|
||||
|
||||
-- --------------------------------------------------------------------------
|
||||
-- 7. role_route
|
||||
-- --------------------------------------------------------------------------
|
||||
COMMENT ON TABLE role_route IS '角色路由关联表:定义某角色可访问哪些菜单/页面路由';
|
||||
COMMENT ON COLUMN role_route.id IS '主键ID';
|
||||
COMMENT ON COLUMN role_route.role_id IS '角色ID,关联 roles.id';
|
||||
COMMENT ON COLUMN role_route.route_id IS '路由ID,关联 sys_routes.id';
|
||||
COMMENT ON COLUMN role_route.permission IS '路由权限类型:R=读,W=写,RW=读写';
|
||||
COMMENT ON COLUMN role_route.created_at IS '创建时间';
|
||||
COMMENT ON COLUMN role_route.updated_at IS '更新时间';
|
||||
COMMENT ON COLUMN role_route.status IS '状态:0=禁用,1=启用';
|
||||
|
||||
COMMIT;
|
||||
@@ -0,0 +1,85 @@
|
||||
-- ==========================================================================
|
||||
-- 老系统用户权限迁移前审计 SQL
|
||||
-- 目标库:docauditai
|
||||
-- 用途:在正式迁移 sso_users / roles / user_role / permissions / role_permissions / sys_routes / role_route 前,
|
||||
-- 先出质量统计,识别脏数据、空地区、无角色用户、历史脏角色。
|
||||
-- ============================================================================
|
||||
|
||||
-- 1. 基础行数
|
||||
SELECT 'sso_users' AS table_name, COUNT(*) AS total FROM sso_users
|
||||
UNION ALL SELECT 'roles', COUNT(*) FROM roles
|
||||
UNION ALL SELECT 'user_role', COUNT(*) FROM user_role
|
||||
UNION ALL SELECT 'permissions', COUNT(*) FROM permissions
|
||||
UNION ALL SELECT 'role_permissions', COUNT(*) FROM role_permissions
|
||||
UNION ALL SELECT 'sys_routes', COUNT(*) FROM sys_routes
|
||||
UNION ALL SELECT 'role_route', COUNT(*) FROM role_route;
|
||||
|
||||
-- 2. 用户地区分布
|
||||
SELECT COALESCE(NULLIF(BTRIM(area), ''), '<EMPTY>') AS area_value, COUNT(*) AS user_count
|
||||
FROM sso_users
|
||||
GROUP BY 1
|
||||
ORDER BY user_count DESC, area_value;
|
||||
|
||||
-- 3. 重复 sub
|
||||
SELECT sub, COUNT(*) AS dup_count
|
||||
FROM sso_users
|
||||
GROUP BY sub
|
||||
HAVING COUNT(*) > 1
|
||||
ORDER BY dup_count DESC, sub;
|
||||
|
||||
-- 4. 重复 username
|
||||
SELECT username, COUNT(*) AS dup_count
|
||||
FROM sso_users
|
||||
GROUP BY username
|
||||
HAVING COUNT(*) > 1
|
||||
ORDER BY dup_count DESC, username;
|
||||
|
||||
-- 5. 空地区 / 禁用 / 软删除 用户统计
|
||||
SELECT
|
||||
COUNT(*) FILTER (WHERE area IS NULL OR BTRIM(area) = '') AS empty_area_count,
|
||||
COUNT(*) FILTER (WHERE status <> 0) AS disabled_count,
|
||||
COUNT(*) FILTER (WHERE deleted_at IS NOT NULL) AS deleted_count
|
||||
FROM sso_users;
|
||||
|
||||
-- 6. 无角色用户
|
||||
SELECT u.id, u.sub, u.username, u.nick_name, u.area
|
||||
FROM sso_users u
|
||||
LEFT JOIN user_role ur ON ur.user_id = u.id
|
||||
WHERE ur.id IS NULL
|
||||
ORDER BY u.id;
|
||||
|
||||
-- 7. 角色分布
|
||||
SELECT r.role_key, r.role_name, COUNT(ur.user_id) AS user_count
|
||||
FROM roles r
|
||||
LEFT JOIN user_role ur ON ur.role_id = r.id
|
||||
GROUP BY r.id, r.role_key, r.role_name
|
||||
ORDER BY user_count DESC, r.role_key;
|
||||
|
||||
-- 8. 历史角色排查(重点看是否还有不应带入新系统的角色)
|
||||
SELECT role_key, role_name, data_scope, description
|
||||
FROM roles
|
||||
ORDER BY role_key;
|
||||
|
||||
-- 9. user_role 脏引用
|
||||
SELECT ur.*
|
||||
FROM user_role ur
|
||||
LEFT JOIN sso_users u ON u.id = ur.user_id
|
||||
LEFT JOIN roles r ON r.id = ur.role_id
|
||||
WHERE u.id IS NULL OR r.id IS NULL
|
||||
ORDER BY ur.id;
|
||||
|
||||
-- 10. role_permissions 脏引用
|
||||
SELECT rp.*
|
||||
FROM role_permissions rp
|
||||
LEFT JOIN roles r ON r.id = rp.role_id
|
||||
LEFT JOIN permissions p ON p.id = rp.permission_id
|
||||
WHERE r.id IS NULL OR p.id IS NULL
|
||||
ORDER BY rp.id;
|
||||
|
||||
-- 11. role_route 脏引用
|
||||
SELECT rr.*
|
||||
FROM role_route rr
|
||||
LEFT JOIN roles r ON r.id = rr.role_id
|
||||
LEFT JOIN sys_routes sr ON sr.id = rr.route_id
|
||||
WHERE r.id IS NULL OR sr.id IS NULL
|
||||
ORDER BY rr.id;
|
||||
@@ -0,0 +1,287 @@
|
||||
-- ==========================================================================
|
||||
-- LeAudit Platform RBAC / User Schema Patch
|
||||
-- 目标:在当前 leaudit_platform 新库中补齐用户、角色、权限、路由核心表
|
||||
-- 设计原则:
|
||||
-- 1. 兼容老系统 docauditai 的核心字段语义
|
||||
-- 2. 新系统只做单地区隔离,地区字段统一使用 sso_users.area
|
||||
-- 3. 采用 bigint 主键,兼容新库现有 leaudit_* bigint 引用字段
|
||||
-- 4. 当前库里尚不存在 sso_users / roles / permissions / role_permissions / sys_routes / role_route / user_role
|
||||
-- ============================================================================
|
||||
|
||||
BEGIN;
|
||||
|
||||
-- --------------------------------------------------------------------------
|
||||
-- 1. 用户主表 sso_users
|
||||
-- --------------------------------------------------------------------------
|
||||
CREATE TABLE IF NOT EXISTS sso_users (
|
||||
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
sub VARCHAR(128) NOT NULL,
|
||||
username VARCHAR(128) NOT NULL,
|
||||
nick_name VARCHAR(128) NOT NULL,
|
||||
phone_number VARCHAR(64),
|
||||
email VARCHAR(256),
|
||||
ou_id VARCHAR(128) NOT NULL,
|
||||
ou_name VARCHAR(255) NOT NULL,
|
||||
status SMALLINT NOT NULL DEFAULT 0,
|
||||
is_leader BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
||||
deleted_at TIMESTAMP WITHOUT TIME ZONE,
|
||||
password VARCHAR(255),
|
||||
try_count INTEGER,
|
||||
try_login_time TIMESTAMP WITHOUT TIME ZONE,
|
||||
area VARCHAR(64),
|
||||
mq_person_uuid VARCHAR(64),
|
||||
mq_account_uuid VARCHAR(64),
|
||||
mq_synced_at TIMESTAMP WITHOUT TIME ZONE,
|
||||
tenant_name VARCHAR(255),
|
||||
dep_short_name VARCHAR(255),
|
||||
dep_name VARCHAR(255),
|
||||
CONSTRAINT sso_users_sub_key UNIQUE (sub)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_sso_users_sub ON sso_users(sub);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_sso_users_sub_unique ON sso_users(sub);
|
||||
CREATE INDEX IF NOT EXISTS idx_sso_users_username ON sso_users(username);
|
||||
CREATE INDEX IF NOT EXISTS idx_sso_users_area ON sso_users(area) WHERE area IS NOT NULL;
|
||||
CREATE INDEX IF NOT EXISTS idx_sso_users_status ON sso_users(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_sso_users_deleted_at ON sso_users(deleted_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_sso_users_ou_id ON sso_users(ou_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_sso_users_is_leader ON sso_users(is_leader);
|
||||
CREATE INDEX IF NOT EXISTS idx_sso_users_mq_person ON sso_users(mq_person_uuid);
|
||||
CREATE INDEX IF NOT EXISTS idx_sso_users_mq_account ON sso_users(mq_account_uuid);
|
||||
|
||||
COMMENT ON TABLE sso_users IS '用户主表:认证身份、组织信息、地区隔离基础字段统一沉淀在这里';
|
||||
COMMENT ON COLUMN sso_users.sub IS '统一身份唯一标识,OAuth / SSO 主键';
|
||||
COMMENT ON COLUMN sso_users.username IS '登录名/工号/展示账号';
|
||||
COMMENT ON COLUMN sso_users.nick_name IS '用户真实姓名';
|
||||
COMMENT ON COLUMN sso_users.area IS '用户主地区,当前系统唯一数据隔离字段';
|
||||
COMMENT ON COLUMN sso_users.password IS '密码字段:当前阶段兼容旧值,后续应迁移为哈希';
|
||||
COMMENT ON COLUMN sso_users.status IS '账户状态:0=正常,1=禁用';
|
||||
COMMENT ON COLUMN sso_users.deleted_at IS '软删除时间';
|
||||
|
||||
-- --------------------------------------------------------------------------
|
||||
-- 2. 角色表 roles
|
||||
-- --------------------------------------------------------------------------
|
||||
CREATE TABLE IF NOT EXISTS roles (
|
||||
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
role_key VARCHAR(64) NOT NULL,
|
||||
role_name VARCHAR(128) NOT NULL,
|
||||
data_scope VARCHAR(16) DEFAULT 'SELF',
|
||||
description VARCHAR(255) DEFAULT '',
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
parent_role_id BIGINT,
|
||||
priority INTEGER DEFAULT 0,
|
||||
is_system_role BOOLEAN DEFAULT FALSE,
|
||||
permissions_cache JSONB,
|
||||
metadata JSONB,
|
||||
CONSTRAINT roles_role_key_key UNIQUE (role_key),
|
||||
CONSTRAINT fk_parent_role FOREIGN KEY (parent_role_id) REFERENCES roles(id) ON DELETE SET NULL,
|
||||
CONSTRAINT chk_roles_data_scope CHECK (data_scope IN ('ALL', 'DEPT', 'SELF', 'GROUP'))
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_roles_parent_role_id ON roles(parent_role_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_roles_priority ON roles(priority DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_roles_permissions_cache ON roles USING GIN (permissions_cache);
|
||||
|
||||
COMMENT ON TABLE roles IS '角色表:当前主业务角色只使用 provincial_admin/admin/common,可选 super_admin';
|
||||
COMMENT ON COLUMN roles.role_key IS '角色机器标识,例如 provincial_admin/admin/common';
|
||||
COMMENT ON COLUMN roles.role_name IS '角色展示名称';
|
||||
COMMENT ON COLUMN roles.data_scope IS '默认数据范围:ALL/DEPT/SELF;GROUP 仅保留兼容';
|
||||
COMMENT ON COLUMN roles.priority IS '角色优先级,数值越大优先级越高';
|
||||
|
||||
-- --------------------------------------------------------------------------
|
||||
-- 3. 菜单/路由表 sys_routes
|
||||
-- --------------------------------------------------------------------------
|
||||
CREATE TABLE IF NOT EXISTS sys_routes (
|
||||
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
route_path VARCHAR(255) NOT NULL,
|
||||
route_name VARCHAR(128) NOT NULL,
|
||||
component VARCHAR(255),
|
||||
parent_id BIGINT,
|
||||
route_title VARCHAR(255),
|
||||
icon VARCHAR(128),
|
||||
sort_order INTEGER DEFAULT 0,
|
||||
is_hidden BOOLEAN DEFAULT FALSE,
|
||||
is_cache BOOLEAN DEFAULT TRUE,
|
||||
meta JSONB,
|
||||
status INTEGER DEFAULT 0,
|
||||
created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
||||
deleted_at TIMESTAMP WITHOUT TIME ZONE,
|
||||
CONSTRAINT fk_parent_route FOREIGN KEY (parent_id) REFERENCES sys_routes(id) ON DELETE SET NULL
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_routes_path ON sys_routes(route_path) WHERE deleted_at IS NULL;
|
||||
CREATE INDEX IF NOT EXISTS idx_routes_parent_id ON sys_routes(parent_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_routes_status ON sys_routes(status);
|
||||
|
||||
COMMENT ON TABLE sys_routes IS '前端菜单/页面路由表:用于控制角色可见菜单,不替代 API 权限表';
|
||||
COMMENT ON COLUMN sys_routes.route_path IS '前端路由路径';
|
||||
COMMENT ON COLUMN sys_routes.status IS '状态:0=启用,1=禁用';
|
||||
|
||||
-- --------------------------------------------------------------------------
|
||||
-- 4. 角色路由关系表 role_route
|
||||
-- --------------------------------------------------------------------------
|
||||
CREATE TABLE IF NOT EXISTS role_route (
|
||||
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
role_id BIGINT NOT NULL,
|
||||
route_id BIGINT NOT NULL,
|
||||
permission VARCHAR(8) DEFAULT 'RW',
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
status SMALLINT NOT NULL DEFAULT 1,
|
||||
CONSTRAINT role_route_role_id_route_id_key UNIQUE (role_id, route_id),
|
||||
CONSTRAINT fk_role_route_role FOREIGN KEY (role_id) REFERENCES roles(id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_role_route_route FOREIGN KEY (route_id) REFERENCES sys_routes(id) ON DELETE CASCADE,
|
||||
CONSTRAINT chk_role_route_status CHECK (status IN (0, 1)),
|
||||
CONSTRAINT chk_role_route_permission CHECK (permission IN ('R', 'W', 'RW'))
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_role_route_status ON role_route(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_role_route_role_status ON role_route(role_id, status);
|
||||
|
||||
COMMENT ON TABLE role_route IS '角色与菜单路由关联表';
|
||||
COMMENT ON COLUMN role_route.permission IS '路由权限类型:R=读,W=写,RW=读写';
|
||||
|
||||
-- --------------------------------------------------------------------------
|
||||
-- 5. 权限点表 permissions
|
||||
-- --------------------------------------------------------------------------
|
||||
CREATE TABLE IF NOT EXISTS permissions (
|
||||
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
permission_key VARCHAR(100) NOT NULL,
|
||||
module VARCHAR(50) NOT NULL,
|
||||
resource VARCHAR(50) NOT NULL,
|
||||
action VARCHAR(50) NOT NULL,
|
||||
description TEXT,
|
||||
display_name VARCHAR(200),
|
||||
permission_type VARCHAR(20) NOT NULL DEFAULT 'API',
|
||||
is_system BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
metadata JSONB,
|
||||
created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
||||
created_by BIGINT,
|
||||
updated_by BIGINT,
|
||||
parent_id BIGINT,
|
||||
sort_order INTEGER DEFAULT 0,
|
||||
route_id BIGINT,
|
||||
api_path VARCHAR(255),
|
||||
api_method VARCHAR(16),
|
||||
related_routes BIGINT[],
|
||||
CONSTRAINT permissions_permission_key_key UNIQUE (permission_key),
|
||||
CONSTRAINT permissions_parent_id_fkey FOREIGN KEY (parent_id) REFERENCES permissions(id) ON DELETE SET NULL,
|
||||
CONSTRAINT permissions_route_id_fkey FOREIGN KEY (route_id) REFERENCES sys_routes(id) ON DELETE SET NULL,
|
||||
CONSTRAINT permissions_key_format_check CHECK (permission_key ~ '^[a-zA-Z0-9_*]+:[a-zA-Z0-9_*]+:[a-zA-Z0-9_*]+$')
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_permissions_module ON permissions(module);
|
||||
CREATE INDEX IF NOT EXISTS idx_permissions_parent_id ON permissions(parent_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_permissions_route_id ON permissions(route_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_permissions_sort_order ON permissions(sort_order);
|
||||
CREATE INDEX IF NOT EXISTS idx_permissions_system ON permissions(is_system);
|
||||
CREATE INDEX IF NOT EXISTS idx_permissions_type ON permissions(permission_type);
|
||||
CREATE INDEX IF NOT EXISTS idx_permissions_metadata ON permissions USING GIN (metadata);
|
||||
|
||||
COMMENT ON TABLE permissions IS '权限点定义表:统一使用 module:resource:action 风格 permission_key';
|
||||
COMMENT ON COLUMN permissions.permission_key IS '权限键,例如 documents:list:read、audit:run:execute';
|
||||
COMMENT ON COLUMN permissions.related_routes IS '共享权限可关联多个路由 ID';
|
||||
|
||||
-- --------------------------------------------------------------------------
|
||||
-- 6. 角色权限关系表 role_permissions
|
||||
-- --------------------------------------------------------------------------
|
||||
CREATE TABLE IF NOT EXISTS role_permissions (
|
||||
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
role_id BIGINT NOT NULL,
|
||||
permission_id BIGINT NOT NULL,
|
||||
grant_type VARCHAR(10) NOT NULL DEFAULT 'GRANT',
|
||||
data_scope VARCHAR(20),
|
||||
condition_filter JSONB,
|
||||
metadata JSONB,
|
||||
created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
||||
created_by BIGINT,
|
||||
updated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_by BIGINT,
|
||||
CONSTRAINT unique_role_permission UNIQUE (role_id, permission_id),
|
||||
CONSTRAINT role_permissions_role_id_fkey FOREIGN KEY (role_id) REFERENCES roles(id) ON DELETE CASCADE,
|
||||
CONSTRAINT role_permissions_permission_id_fkey FOREIGN KEY (permission_id) REFERENCES permissions(id) ON DELETE CASCADE,
|
||||
CONSTRAINT chk_role_permissions_grant_type CHECK (grant_type IN ('GRANT', 'DENY')),
|
||||
CONSTRAINT chk_role_permissions_data_scope CHECK (data_scope IS NULL OR data_scope IN ('ALL', 'DEPT', 'SELF', 'GROUP'))
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_role_permissions_role_id ON role_permissions(role_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_role_permissions_permission_id ON role_permissions(permission_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_role_permissions_grant_type ON role_permissions(grant_type);
|
||||
CREATE INDEX IF NOT EXISTS idx_role_permissions_data_scope ON role_permissions(data_scope);
|
||||
CREATE INDEX IF NOT EXISTS idx_role_permissions_lookup ON role_permissions(role_id, grant_type);
|
||||
CREATE INDEX IF NOT EXISTS idx_role_permissions_condition ON role_permissions USING GIN (condition_filter);
|
||||
|
||||
COMMENT ON TABLE role_permissions IS '角色权限关联表:grant_type 兼容旧系统 DENY 语义,当前新系统默认只配置 GRANT';
|
||||
COMMENT ON COLUMN role_permissions.data_scope IS '数据范围:ALL/DEPT/SELF,GROUP 仅保留兼容';
|
||||
|
||||
-- --------------------------------------------------------------------------
|
||||
-- 7. 用户角色关系表 user_role
|
||||
-- --------------------------------------------------------------------------
|
||||
CREATE TABLE IF NOT EXISTS user_role (
|
||||
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
user_id BIGINT NOT NULL,
|
||||
role_id BIGINT NOT NULL,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
CONSTRAINT user_role_user_id_role_id_key UNIQUE (user_id, role_id),
|
||||
CONSTRAINT fk_user_role_user FOREIGN KEY (user_id) REFERENCES sso_users(id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_user_role_role FOREIGN KEY (role_id) REFERENCES roles(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_user_role_user_role ON user_role(user_id, role_id);
|
||||
|
||||
COMMENT ON TABLE user_role IS '用户与角色关联表;一个用户允许挂多个角色';
|
||||
|
||||
-- --------------------------------------------------------------------------
|
||||
-- 8. 给现有 leaudit_* 业务表补用户外键
|
||||
-- 当前库中这些字段均为 bigint,且当前全为空,可安全补充外键。
|
||||
-- --------------------------------------------------------------------------
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_constraint WHERE conname = 'fk_leaudit_document_files_created_by'
|
||||
) THEN
|
||||
ALTER TABLE leaudit_document_files
|
||||
ADD CONSTRAINT fk_leaudit_document_files_created_by
|
||||
FOREIGN KEY (created_by) REFERENCES sso_users(id) ON DELETE SET NULL;
|
||||
END IF;
|
||||
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_constraint WHERE conname = 'fk_leaudit_audit_runs_trigger_user_id'
|
||||
) THEN
|
||||
ALTER TABLE leaudit_audit_runs
|
||||
ADD CONSTRAINT fk_leaudit_audit_runs_trigger_user_id
|
||||
FOREIGN KEY (trigger_user_id) REFERENCES sso_users(id) ON DELETE SET NULL;
|
||||
END IF;
|
||||
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_constraint WHERE conname = 'fk_leaudit_rule_sets_owner_user_id'
|
||||
) THEN
|
||||
ALTER TABLE leaudit_rule_sets
|
||||
ADD CONSTRAINT fk_leaudit_rule_sets_owner_user_id
|
||||
FOREIGN KEY (owner_user_id) REFERENCES sso_users(id) ON DELETE SET NULL;
|
||||
END IF;
|
||||
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_constraint WHERE conname = 'fk_leaudit_rule_versions_editor_user_id'
|
||||
) THEN
|
||||
ALTER TABLE leaudit_rule_versions
|
||||
ADD CONSTRAINT fk_leaudit_rule_versions_editor_user_id
|
||||
FOREIGN KEY (editor_user_id) REFERENCES sso_users(id) ON DELETE SET NULL;
|
||||
END IF;
|
||||
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_constraint WHERE conname = 'fk_leaudit_rule_versions_publisher_user_id'
|
||||
) THEN
|
||||
ALTER TABLE leaudit_rule_versions
|
||||
ADD CONSTRAINT fk_leaudit_rule_versions_publisher_user_id
|
||||
FOREIGN KEY (publisher_user_id) REFERENCES sso_users(id) ON DELETE SET NULL;
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
COMMIT;
|
||||
@@ -0,0 +1,271 @@
|
||||
-- ==========================================================================
|
||||
-- LeAudit Platform RBAC / User Seed
|
||||
-- 说明:
|
||||
-- 1. 本文件依赖 scripts/user_rbac_schema_patch.sql 已执行
|
||||
-- 2. 权限键统一采用 module:resource:action 风格
|
||||
-- 3. 当前只初始化新系统当前开发真正需要的最小权限集
|
||||
-- ============================================================================
|
||||
|
||||
BEGIN;
|
||||
|
||||
-- --------------------------------------------------------------------------
|
||||
-- 1. 角色初始化
|
||||
-- --------------------------------------------------------------------------
|
||||
INSERT INTO roles (role_key, role_name, data_scope, description, priority, is_system_role, created_at, updated_at)
|
||||
VALUES
|
||||
('super_admin', '系统超级管理员', 'ALL', '可选,仅系统维护/排障使用', 100, TRUE, NOW(), NOW()),
|
||||
('provincial_admin', '省级管理员', 'ALL', '查看全局数据并维护系统配置', 90, TRUE, NOW(), NOW()),
|
||||
('admin', '地区管理员', 'DEPT', '仅管理本地区数据', 50, TRUE, NOW(), NOW()),
|
||||
('common', '普通用户', 'SELF', '仅处理本人数据', 10, TRUE, NOW(), NOW())
|
||||
ON CONFLICT (role_key) DO UPDATE SET
|
||||
role_name = EXCLUDED.role_name,
|
||||
data_scope = EXCLUDED.data_scope,
|
||||
description = EXCLUDED.description,
|
||||
priority = EXCLUDED.priority,
|
||||
is_system_role = EXCLUDED.is_system_role,
|
||||
updated_at = NOW();
|
||||
|
||||
-- --------------------------------------------------------------------------
|
||||
-- 2. 路由初始化
|
||||
-- --------------------------------------------------------------------------
|
||||
INSERT INTO sys_routes (route_path, route_name, component, parent_id, route_title, icon, sort_order, is_hidden, is_cache, meta, status, created_at, updated_at, deleted_at)
|
||||
VALUES
|
||||
('/documents', 'documents', 'Layout', NULL, '文档管理', 'files', 10, FALSE, TRUE, '{"group":"documents"}'::jsonb, 0, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, NULL),
|
||||
('/documents/list', 'documents.list', 'documents/list', NULL, '文档列表', 'table', 11, FALSE, TRUE, '{"group":"documents"}'::jsonb, 0, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, NULL),
|
||||
('/audit', 'audit', 'Layout', NULL, '评查任务', 'audit', 20, FALSE, TRUE, '{"group":"audit"}'::jsonb, 0, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, NULL),
|
||||
('/audit/runs', 'audit.runs', 'audit/runs', NULL, '评查运行', 'history', 21, FALSE, TRUE, '{"group":"audit"}'::jsonb, 0, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, NULL),
|
||||
('/rules', 'rules', 'Layout', NULL, '规则管理', 'rule', 30, FALSE, TRUE, '{"group":"rules"}'::jsonb, 0, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, NULL),
|
||||
('/rules/sets', 'rules.sets', 'rules/sets', NULL, '规则集管理', 'yaml', 31, FALSE, TRUE, '{"group":"rules"}'::jsonb, 0, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, NULL),
|
||||
('/system', 'system', 'Layout', NULL, '系统管理', 'setting', 90, FALSE, TRUE, '{"group":"system"}'::jsonb, 0, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, NULL),
|
||||
('/system/users', 'system.users', 'system/users', NULL, '用户管理', 'user', 91, FALSE, TRUE, '{"group":"system"}'::jsonb, 0, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, NULL),
|
||||
('/system/roles', 'system.roles', 'system/roles', NULL, '角色权限', 'shield', 92, FALSE, TRUE, '{"group":"system"}'::jsonb, 0, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, NULL)
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
-- --------------------------------------------------------------------------
|
||||
-- 3. 权限点初始化
|
||||
-- --------------------------------------------------------------------------
|
||||
INSERT INTO permissions (
|
||||
permission_key, module, resource, action, description, display_name,
|
||||
permission_type, is_system, metadata, created_at, updated_at,
|
||||
created_by, updated_by, parent_id, sort_order, route_id, api_path, api_method, related_routes
|
||||
)
|
||||
VALUES
|
||||
('auth:me:read', 'auth', 'me', 'read', '查看当前登录用户信息', '当前用户信息', 'API', TRUE, NULL, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, NULL, NULL, NULL, 10, NULL, '/api/auth/me', 'GET', NULL),
|
||||
|
||||
('documents:upload:write', 'documents', 'upload', 'write', '上传文档', '上传文档', 'API', TRUE, NULL, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, NULL, NULL, NULL, 20, NULL, '/api/upload', 'POST', NULL),
|
||||
('documents:list:read', 'documents', 'list', 'read', '查看文档列表', '文档列表', 'API', TRUE, NULL, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, NULL, NULL, NULL, 21, NULL, '/api/documents/list', 'GET', NULL),
|
||||
('documents:detail:read', 'documents', 'detail', 'read', '查看文档详情', '文档详情', 'API', TRUE, NULL, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, NULL, NULL, NULL, 22, NULL, '/api/documents/{document_id}', 'GET', NULL),
|
||||
('documents:history:read', 'documents', 'history', 'read', '查看文档历史版本', '文档历史版本', 'API', TRUE, NULL, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, NULL, NULL, NULL, 23, NULL, '/api/documents/{document_id}/versions', 'GET', NULL),
|
||||
('documents:delete:delete', 'documents', 'delete', 'delete', '删除文档', '删除文档', 'API', TRUE, NULL, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, NULL, NULL, NULL, 24, NULL, '/api/documents/{document_id}', 'DELETE', NULL),
|
||||
|
||||
('audit:run:execute', 'audit', 'run', 'execute', '发起评查任务', '发起评查', 'API', TRUE, NULL, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, NULL, NULL, NULL, 30, NULL, '/api/audit/run', 'POST', NULL),
|
||||
('audit:status:read', 'audit', 'status', 'read', '查看评查运行状态', '评查状态', 'API', TRUE, NULL, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, NULL, NULL, NULL, 31, NULL, '/api/audit/run/{run_id}', 'GET', NULL),
|
||||
('audit:result:read', 'audit', 'result', 'read', '查看评查结果', '评查结果', 'API', TRUE, NULL, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, NULL, NULL, NULL, 32, NULL, '/api/audit/result/{run_id}', 'GET', NULL),
|
||||
|
||||
('rules:list:read', 'rules', 'list', 'read', '查看规则集列表', '规则集列表', 'API', TRUE, NULL, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, NULL, NULL, NULL, 40, NULL, '/api/rule-sets', 'GET', NULL),
|
||||
('rules:version_list:read', 'rules', 'version_list', 'read', '查看规则版本列表', '规则版本列表', 'API', TRUE, NULL, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, NULL, NULL, NULL, 41, NULL, '/api/rule-sets/{rule_type}/versions', 'GET', NULL),
|
||||
('rules:content:read', 'rules', 'content', 'read', '查看规则正文', '规则正文', 'API', TRUE, NULL, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, NULL, NULL, NULL, 42, NULL, '/api/rule-sets/versions/{version_id}/content', 'GET', NULL),
|
||||
('rules:validate:execute', 'rules', 'validate', 'execute', '校验规则 YAML', '规则校验', 'API', TRUE, NULL, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, NULL, NULL, NULL, 43, NULL, '/api/rule-sets/{rule_type}/validate', 'POST', NULL),
|
||||
('rules:version_create:write', 'rules', 'version_create', 'write', '创建规则版本', '创建规则版本', 'API', TRUE, NULL, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, NULL, NULL, NULL, 44, NULL, '/api/rule-sets/{rule_type}/versions', 'POST', NULL),
|
||||
('rules:publish:write', 'rules', 'publish', 'write', '发布规则版本', '发布规则', 'API', TRUE, NULL, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, NULL, NULL, NULL, 45, NULL, '/api/rule-sets/{rule_type}/publish', 'POST', NULL),
|
||||
('rules:rollback:write', 'rules', 'rollback', 'write', '回滚规则版本', '回滚规则', 'API', TRUE, NULL, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, NULL, NULL, NULL, 46, NULL, '/api/rule-sets/{rule_type}/rollback', 'POST', NULL),
|
||||
('rules:binding_list:read', 'rules', 'binding_list', 'read', '查看规则绑定列表', '规则绑定列表', 'API', TRUE, NULL, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, NULL, NULL, NULL, 47, NULL, '/api/rule-sets/bindings', 'GET', NULL),
|
||||
('rules:binding_create:write', 'rules', 'binding_create', 'write', '创建规则绑定', '创建规则绑定', 'API', TRUE, NULL, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, NULL, NULL, NULL, 48, NULL, '/api/rule-sets/{rule_type}/bindings', 'POST', NULL),
|
||||
('rules:binding_update:write', 'rules', 'binding_update', 'write', '更新规则绑定', '更新规则绑定', 'API', TRUE, NULL, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, NULL, NULL, NULL, 49, NULL, '/api/rule-sets/bindings/{binding_id}', 'PUT', NULL),
|
||||
('rules:binding_delete:delete', 'rules', 'binding_delete', 'delete', '删除规则绑定', '删除规则绑定', 'API', TRUE, NULL, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, NULL, NULL, NULL, 50, NULL, '/api/rule-sets/bindings/{binding_id}', 'DELETE', NULL),
|
||||
|
||||
('users:list:read', 'users', 'list', 'read', '查看用户列表', '用户列表', 'API', TRUE, NULL, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, NULL, NULL, NULL, 60, NULL, '/api/users/list', 'GET', NULL),
|
||||
('users:create:write', 'users', 'create', 'write', '创建用户', '创建用户', 'API', TRUE, NULL, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, NULL, NULL, NULL, 61, NULL, '/api/users', 'POST', NULL),
|
||||
('users:update:write', 'users', 'update', 'write', '更新用户', '更新用户', 'API', TRUE, NULL, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, NULL, NULL, NULL, 62, NULL, '/api/users/{user_id}', 'PUT', NULL),
|
||||
('users:disable:write', 'users', 'disable', 'write', '禁用/启用用户', '禁用用户', 'API', TRUE, NULL, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, NULL, NULL, NULL, 63, NULL, '/api/users/{user_id}/disable', 'PUT', NULL),
|
||||
('users:roles_assign:write', 'users', 'roles_assign', 'write', '分配用户角色', '分配用户角色', 'API', TRUE, NULL, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, NULL, NULL, NULL, 64, NULL, '/api/users/{user_id}/roles', 'POST', NULL),
|
||||
|
||||
('rbac:roles:read', 'rbac', 'roles', 'read', '查看角色列表', '角色列表', 'API', TRUE, NULL, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, NULL, NULL, NULL, 70, NULL, '/api/rbac/roles', 'GET', NULL),
|
||||
('rbac:roles:update', 'rbac', 'roles', 'update', '维护角色信息', '维护角色', 'API', TRUE, NULL, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, NULL, NULL, NULL, 71, NULL, '/api/rbac/roles/{role_id}', 'PUT', NULL),
|
||||
('rbac:permissions:read', 'rbac', 'permissions', 'read', '查看权限点列表', '权限点列表', 'API', TRUE, NULL, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, NULL, NULL, NULL, 72, NULL, '/api/rbac/permissions', 'GET', NULL),
|
||||
('rbac:role_permissions:write', 'rbac', 'role_permissions', 'write', '分配角色权限', '分配角色权限', 'API', TRUE, NULL, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, NULL, NULL, NULL, 73, NULL, '/api/rbac/roles/{role_id}/permissions', 'POST', NULL),
|
||||
('rbac:role_routes:write', 'rbac', 'role_routes', 'write', '分配角色菜单', '分配角色菜单', 'API', TRUE, NULL, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, NULL, NULL, NULL, 74, NULL, '/api/rbac/roles/{role_id}/routes', 'PUT', NULL)
|
||||
ON CONFLICT (permission_key) DO UPDATE SET
|
||||
module = EXCLUDED.module,
|
||||
resource = EXCLUDED.resource,
|
||||
action = EXCLUDED.action,
|
||||
description = EXCLUDED.description,
|
||||
display_name = EXCLUDED.display_name,
|
||||
permission_type = EXCLUDED.permission_type,
|
||||
is_system = EXCLUDED.is_system,
|
||||
updated_at = CURRENT_TIMESTAMP,
|
||||
api_path = EXCLUDED.api_path,
|
||||
api_method = EXCLUDED.api_method,
|
||||
sort_order = EXCLUDED.sort_order;
|
||||
|
||||
-- --------------------------------------------------------------------------
|
||||
-- 4. 角色菜单授权
|
||||
-- --------------------------------------------------------------------------
|
||||
WITH role_map AS (
|
||||
SELECT id, role_key FROM roles WHERE role_key IN ('super_admin', 'provincial_admin', 'admin', 'common')
|
||||
),
|
||||
route_map AS (
|
||||
SELECT id, route_path FROM sys_routes WHERE deleted_at IS NULL
|
||||
),
|
||||
seed(role_key, route_path, permission, status) AS (
|
||||
VALUES
|
||||
('super_admin', '/documents', 'RW', 1),
|
||||
('super_admin', '/documents/list', 'RW', 1),
|
||||
('super_admin', '/audit', 'RW', 1),
|
||||
('super_admin', '/audit/runs', 'RW', 1),
|
||||
('super_admin', '/rules', 'RW', 1),
|
||||
('super_admin', '/rules/sets', 'RW', 1),
|
||||
('super_admin', '/system', 'RW', 1),
|
||||
('super_admin', '/system/users', 'RW', 1),
|
||||
('super_admin', '/system/roles', 'RW', 1),
|
||||
|
||||
('provincial_admin', '/documents', 'RW', 1),
|
||||
('provincial_admin', '/documents/list', 'RW', 1),
|
||||
('provincial_admin', '/audit', 'RW', 1),
|
||||
('provincial_admin', '/audit/runs', 'RW', 1),
|
||||
('provincial_admin', '/rules', 'RW', 1),
|
||||
('provincial_admin', '/rules/sets', 'RW', 1),
|
||||
('provincial_admin', '/system', 'RW', 1),
|
||||
('provincial_admin', '/system/users', 'RW', 1),
|
||||
('provincial_admin', '/system/roles', 'RW', 1),
|
||||
|
||||
('admin', '/documents', 'RW', 1),
|
||||
('admin', '/documents/list', 'RW', 1),
|
||||
('admin', '/audit', 'RW', 1),
|
||||
('admin', '/audit/runs', 'RW', 1),
|
||||
('admin', '/rules', 'RW', 1),
|
||||
('admin', '/rules/sets', 'RW', 1),
|
||||
('admin', '/system', 'RW', 1),
|
||||
('admin', '/system/users', 'RW', 1),
|
||||
|
||||
('common', '/documents', 'R', 1),
|
||||
('common', '/documents/list', 'R', 1),
|
||||
('common', '/audit', 'R', 1),
|
||||
('common', '/audit/runs', 'R', 1)
|
||||
)
|
||||
INSERT INTO role_route (role_id, route_id, permission, status, created_at, updated_at)
|
||||
SELECT rm.id, tm.id, s.permission, s.status, NOW(), NOW()
|
||||
FROM seed s
|
||||
JOIN role_map rm ON rm.role_key = s.role_key
|
||||
JOIN route_map tm ON tm.route_path = s.route_path
|
||||
ON CONFLICT (role_id, route_id) DO UPDATE SET
|
||||
permission = EXCLUDED.permission,
|
||||
status = EXCLUDED.status,
|
||||
updated_at = NOW();
|
||||
|
||||
-- --------------------------------------------------------------------------
|
||||
-- 5. 角色权限授权
|
||||
-- --------------------------------------------------------------------------
|
||||
WITH role_map AS (
|
||||
SELECT id, role_key FROM roles WHERE role_key IN ('super_admin', 'provincial_admin', 'admin', 'common')
|
||||
),
|
||||
perm_map AS (
|
||||
SELECT id, permission_key FROM permissions
|
||||
),
|
||||
seed(role_key, permission_key, grant_type, data_scope) AS (
|
||||
VALUES
|
||||
('super_admin', 'auth:me:read', 'GRANT', 'ALL'),
|
||||
('super_admin', 'documents:upload:write', 'GRANT', 'ALL'),
|
||||
('super_admin', 'documents:list:read', 'GRANT', 'ALL'),
|
||||
('super_admin', 'documents:detail:read', 'GRANT', 'ALL'),
|
||||
('super_admin', 'documents:history:read', 'GRANT', 'ALL'),
|
||||
('super_admin', 'documents:delete:delete', 'GRANT', 'ALL'),
|
||||
('super_admin', 'audit:run:execute', 'GRANT', 'ALL'),
|
||||
('super_admin', 'audit:status:read', 'GRANT', 'ALL'),
|
||||
('super_admin', 'audit:result:read', 'GRANT', 'ALL'),
|
||||
('super_admin', 'rules:list:read', 'GRANT', 'ALL'),
|
||||
('super_admin', 'rules:version_list:read', 'GRANT', 'ALL'),
|
||||
('super_admin', 'rules:content:read', 'GRANT', 'ALL'),
|
||||
('super_admin', 'rules:validate:execute', 'GRANT', 'ALL'),
|
||||
('super_admin', 'rules:version_create:write', 'GRANT', 'ALL'),
|
||||
('super_admin', 'rules:publish:write', 'GRANT', 'ALL'),
|
||||
('super_admin', 'rules:rollback:write', 'GRANT', 'ALL'),
|
||||
('super_admin', 'rules:binding_list:read', 'GRANT', 'ALL'),
|
||||
('super_admin', 'rules:binding_create:write', 'GRANT', 'ALL'),
|
||||
('super_admin', 'rules:binding_update:write', 'GRANT', 'ALL'),
|
||||
('super_admin', 'rules:binding_delete:delete', 'GRANT', 'ALL'),
|
||||
('super_admin', 'users:list:read', 'GRANT', 'ALL'),
|
||||
('super_admin', 'users:create:write', 'GRANT', 'ALL'),
|
||||
('super_admin', 'users:update:write', 'GRANT', 'ALL'),
|
||||
('super_admin', 'users:disable:write', 'GRANT', 'ALL'),
|
||||
('super_admin', 'users:roles_assign:write', 'GRANT', 'ALL'),
|
||||
('super_admin', 'rbac:roles:read', 'GRANT', 'ALL'),
|
||||
('super_admin', 'rbac:roles:update', 'GRANT', 'ALL'),
|
||||
('super_admin', 'rbac:permissions:read', 'GRANT', 'ALL'),
|
||||
('super_admin', 'rbac:role_permissions:write', 'GRANT', 'ALL'),
|
||||
('super_admin', 'rbac:role_routes:write', 'GRANT', 'ALL'),
|
||||
|
||||
('provincial_admin', 'auth:me:read', 'GRANT', 'ALL'),
|
||||
('provincial_admin', 'documents:upload:write', 'GRANT', 'ALL'),
|
||||
('provincial_admin', 'documents:list:read', 'GRANT', 'ALL'),
|
||||
('provincial_admin', 'documents:detail:read', 'GRANT', 'ALL'),
|
||||
('provincial_admin', 'documents:history:read', 'GRANT', 'ALL'),
|
||||
('provincial_admin', 'documents:delete:delete', 'GRANT', 'ALL'),
|
||||
('provincial_admin', 'audit:run:execute', 'GRANT', 'ALL'),
|
||||
('provincial_admin', 'audit:status:read', 'GRANT', 'ALL'),
|
||||
('provincial_admin', 'audit:result:read', 'GRANT', 'ALL'),
|
||||
('provincial_admin', 'rules:list:read', 'GRANT', 'ALL'),
|
||||
('provincial_admin', 'rules:version_list:read', 'GRANT', 'ALL'),
|
||||
('provincial_admin', 'rules:content:read', 'GRANT', 'ALL'),
|
||||
('provincial_admin', 'rules:validate:execute', 'GRANT', 'ALL'),
|
||||
('provincial_admin', 'rules:version_create:write', 'GRANT', 'ALL'),
|
||||
('provincial_admin', 'rules:publish:write', 'GRANT', 'ALL'),
|
||||
('provincial_admin', 'rules:rollback:write', 'GRANT', 'ALL'),
|
||||
('provincial_admin', 'rules:binding_list:read', 'GRANT', 'ALL'),
|
||||
('provincial_admin', 'rules:binding_create:write', 'GRANT', 'ALL'),
|
||||
('provincial_admin', 'rules:binding_update:write', 'GRANT', 'ALL'),
|
||||
('provincial_admin', 'rules:binding_delete:delete', 'GRANT', 'ALL'),
|
||||
('provincial_admin', 'users:list:read', 'GRANT', 'ALL'),
|
||||
('provincial_admin', 'users:create:write', 'GRANT', 'ALL'),
|
||||
('provincial_admin', 'users:update:write', 'GRANT', 'ALL'),
|
||||
('provincial_admin', 'users:disable:write', 'GRANT', 'ALL'),
|
||||
('provincial_admin', 'users:roles_assign:write', 'GRANT', 'ALL'),
|
||||
('provincial_admin', 'rbac:roles:read', 'GRANT', 'ALL'),
|
||||
('provincial_admin', 'rbac:roles:update', 'GRANT', 'ALL'),
|
||||
('provincial_admin', 'rbac:permissions:read', 'GRANT', 'ALL'),
|
||||
('provincial_admin', 'rbac:role_permissions:write', 'GRANT', 'ALL'),
|
||||
('provincial_admin', 'rbac:role_routes:write', 'GRANT', 'ALL'),
|
||||
|
||||
('admin', 'auth:me:read', 'GRANT', 'DEPT'),
|
||||
('admin', 'documents:upload:write', 'GRANT', 'DEPT'),
|
||||
('admin', 'documents:list:read', 'GRANT', 'DEPT'),
|
||||
('admin', 'documents:detail:read', 'GRANT', 'DEPT'),
|
||||
('admin', 'documents:history:read', 'GRANT', 'DEPT'),
|
||||
('admin', 'documents:delete:delete', 'GRANT', 'DEPT'),
|
||||
('admin', 'audit:run:execute', 'GRANT', 'DEPT'),
|
||||
('admin', 'audit:status:read', 'GRANT', 'DEPT'),
|
||||
('admin', 'audit:result:read', 'GRANT', 'DEPT'),
|
||||
('admin', 'rules:list:read', 'GRANT', 'DEPT'),
|
||||
('admin', 'rules:version_list:read', 'GRANT', 'DEPT'),
|
||||
('admin', 'rules:content:read', 'GRANT', 'DEPT'),
|
||||
('admin', 'rules:validate:execute', 'GRANT', 'DEPT'),
|
||||
('admin', 'rules:binding_list:read', 'GRANT', 'DEPT'),
|
||||
('admin', 'rules:binding_create:write', 'GRANT', 'DEPT'),
|
||||
('admin', 'rules:binding_update:write', 'GRANT', 'DEPT'),
|
||||
('admin', 'users:list:read', 'GRANT', 'DEPT'),
|
||||
('admin', 'users:update:write', 'GRANT', 'DEPT'),
|
||||
|
||||
('common', 'auth:me:read', 'GRANT', 'SELF'),
|
||||
('common', 'documents:upload:write', 'GRANT', 'SELF'),
|
||||
('common', 'documents:list:read', 'GRANT', 'SELF'),
|
||||
('common', 'documents:detail:read', 'GRANT', 'SELF'),
|
||||
('common', 'documents:history:read', 'GRANT', 'SELF'),
|
||||
('common', 'audit:run:execute', 'GRANT', 'SELF'),
|
||||
('common', 'audit:status:read', 'GRANT', 'SELF'),
|
||||
('common', 'audit:result:read', 'GRANT', 'SELF'),
|
||||
('common', 'rules:list:read', 'GRANT', 'DEPT'),
|
||||
('common', 'rules:version_list:read', 'GRANT', 'DEPT'),
|
||||
('common', 'rules:content:read', 'GRANT', 'DEPT'),
|
||||
('common', 'rules:binding_list:read', 'GRANT', 'DEPT')
|
||||
)
|
||||
INSERT INTO role_permissions (role_id, permission_id, grant_type, data_scope, created_at, updated_at)
|
||||
SELECT rm.id, pm.id, s.grant_type, s.data_scope, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP
|
||||
FROM seed s
|
||||
JOIN role_map rm ON rm.role_key = s.role_key
|
||||
JOIN perm_map pm ON pm.permission_key = s.permission_key
|
||||
ON CONFLICT (role_id, permission_id) DO UPDATE SET
|
||||
grant_type = EXCLUDED.grant_type,
|
||||
data_scope = EXCLUDED.data_scope,
|
||||
updated_at = CURRENT_TIMESTAMP;
|
||||
|
||||
COMMIT;
|
||||
Reference in New Issue
Block a user