mirror of
https://github.com/fastapi-practices/fastapi-best-architecture.git
synced 2026-09-21 21:15:13 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5621daea98 | ||
|
|
71d168dfea | ||
|
|
2d4365189d | ||
|
|
0c28a46e60 | ||
|
|
a4c7dae99c | ||
|
|
bfda0a226c | ||
|
|
c9bbb105a3 | ||
|
|
b415dc8912 | ||
|
|
05b74d71a3 | ||
|
|
10c0c691a7 | ||
|
|
0bc3786fc6 | ||
|
|
8e041ccb1b | ||
|
|
a82e9b7b9d | ||
|
|
7bab8cc636 | ||
|
|
7659b63d88 | ||
|
|
33ebc5723f | ||
|
|
86d9efd785 | ||
|
|
57ae7f03a6 | ||
|
|
6ee3983846 | ||
|
|
f05ed5cdda | ||
|
|
187dfa5344 | ||
|
|
918c8a9d33 | ||
|
|
ccfe35ea85 | ||
|
|
4a118c166a | ||
|
|
5f3823cb6d | ||
|
|
19ef52da9b | ||
|
|
39c3339a8d | ||
|
|
9f52629dcb | ||
|
|
1cddd6e2d5 | ||
|
|
ed10e6ff47 | ||
|
|
48574fb50a | ||
|
|
ee7fe3087d | ||
|
|
270ac3d941 | ||
|
|
2f76988076 | ||
|
|
b275481742 | ||
|
|
f3f1ceb595 | ||
|
|
bcc764142b | ||
|
|
9b0d9f8296 | ||
|
|
63ba7646c1 | ||
|
|
1a7734185f | ||
|
|
8541059d23 | ||
|
|
61edc81333 | ||
|
|
8998687adb | ||
|
|
85b0294da9 | ||
|
|
a8ecb4ae3b | ||
|
|
a87ff74b56 |
+102
@@ -1,3 +1,100 @@
|
|||||||
|
<a id="v1.4.3"></a>
|
||||||
|
# [v1.4.3](https://github.com/fastapi-practices/fastapi_best_architecture/releases/tag/v1.4.3) - 2025-06-02
|
||||||
|
|
||||||
|
## What's Changed
|
||||||
|
* Update changelog for v1.4.2 by [@wu-clan](https://github.com/wu-clan) in [#639](https://github.com/fastapi-practices/fastapi_best_architecture/pull/639)
|
||||||
|
* Fix the role update business variables by [@wu-clan](https://github.com/wu-clan) in [#640](https://github.com/fastapi-practices/fastapi_best_architecture/pull/640)
|
||||||
|
* Fix the menu delete interface arg description by [@wu-clan](https://github.com/wu-clan) in [#641](https://github.com/fastapi-practices/fastapi_best_architecture/pull/641)
|
||||||
|
* Fix the filter of query all menus by [@wu-clan](https://github.com/wu-clan) in [#642](https://github.com/fastapi-practices/fastapi_best_architecture/pull/642)
|
||||||
|
* Refactor routes to better align with RESTful by [@wu-clan](https://github.com/wu-clan) in [#645](https://github.com/fastapi-practices/fastapi_best_architecture/pull/645)
|
||||||
|
* Update the server startup time to string by [@wu-clan](https://github.com/wu-clan) in [#646](https://github.com/fastapi-practices/fastapi_best_architecture/pull/646)
|
||||||
|
* Add get all data scope rules interface by [@wu-clan](https://github.com/wu-clan) in [#647](https://github.com/fastapi-practices/fastapi_best_architecture/pull/647)
|
||||||
|
* Add data permission condition for filter data by [@wu-clan](https://github.com/wu-clan) in [#648](https://github.com/fastapi-practices/fastapi_best_architecture/pull/648)
|
||||||
|
* Update default value for role filter scopes by [@wu-clan](https://github.com/wu-clan) in [#649](https://github.com/fastapi-practices/fastapi_best_architecture/pull/649)
|
||||||
|
* Fix data permission condition for filter data by [@wu-clan](https://github.com/wu-clan) in [#650](https://github.com/fastapi-practices/fastapi_best_architecture/pull/650)
|
||||||
|
|
||||||
|
|
||||||
|
**Full Changelog**: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.4.2...v1.4.3
|
||||||
|
|
||||||
|
[Changes][v1.4.3]
|
||||||
|
|
||||||
|
|
||||||
|
<a id="v1.4.2"></a>
|
||||||
|
# [v1.4.2](https://github.com/fastapi-practices/fastapi_best_architecture/releases/tag/v1.4.2) - 2025-05-29
|
||||||
|
|
||||||
|
## What's Changed
|
||||||
|
* Update changelog for v1.4.1 by [@wu-clan](https://github.com/wu-clan) in [#630](https://github.com/fastapi-practices/fastapi_best_architecture/pull/630)
|
||||||
|
* Update non-linked sidebar support by [@wu-clan](https://github.com/wu-clan) in [#633](https://github.com/fastapi-practices/fastapi_best_architecture/pull/633)
|
||||||
|
* Update the captcha invalidation error class by [@wu-clan](https://github.com/wu-clan) in [#634](https://github.com/fastapi-practices/fastapi_best_architecture/pull/634)
|
||||||
|
* Optimize role-related data processing performance by [@wu-clan](https://github.com/wu-clan) in [#635](https://github.com/fastapi-practices/fastapi_best_architecture/pull/635)
|
||||||
|
* Optimize install and build of plugin zip by [@wu-clan](https://github.com/wu-clan) in [#636](https://github.com/fastapi-practices/fastapi_best_architecture/pull/636)
|
||||||
|
* Fix auto-increment id for postgres init data by [@huyuwei1996](https://github.com/huyuwei1996) in [#632](https://github.com/fastapi-practices/fastapi_best_architecture/pull/632)
|
||||||
|
* Fix: prevent overwriting existing init files in code generator by [@lin-wu-1990](https://github.com/lin-wu-1990) in [#637](https://github.com/fastapi-practices/fastapi_best_architecture/pull/637)
|
||||||
|
* Simplify the user info update business by [@wu-clan](https://github.com/wu-clan) in [#638](https://github.com/fastapi-practices/fastapi_best_architecture/pull/638)
|
||||||
|
|
||||||
|
## New Contributors
|
||||||
|
* [@lin-wu-1990](https://github.com/lin-wu-1990) made their first contribution in [#637](https://github.com/fastapi-practices/fastapi_best_architecture/pull/637)
|
||||||
|
|
||||||
|
**Full Changelog**: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.4.1...v1.4.2
|
||||||
|
|
||||||
|
[Changes][v1.4.2]
|
||||||
|
|
||||||
|
|
||||||
|
<a id="v1.4.1"></a>
|
||||||
|
# [v1.4.1](https://github.com/fastapi-practices/fastapi_best_architecture/releases/tag/v1.4.1) - 2025-05-25
|
||||||
|
|
||||||
|
## What's Changed
|
||||||
|
* Update changelog for v1.4.0 by [@wu-clan](https://github.com/wu-clan) in [#621](https://github.com/fastapi-practices/fastapi_best_architecture/pull/621)
|
||||||
|
* Update the menu path and type columns by [@wu-clan](https://github.com/wu-clan) in [#622](https://github.com/fastapi-practices/fastapi_best_architecture/pull/622)
|
||||||
|
* Add the deepwiki badge to README by [@wu-clan](https://github.com/wu-clan) in [#623](https://github.com/fastapi-practices/fastapi_best_architecture/pull/623)
|
||||||
|
* Refactor the system token to online users by [@wu-clan](https://github.com/wu-clan) in [#624](https://github.com/fastapi-practices/fastapi_best_architecture/pull/624)
|
||||||
|
* Update the token check for logout interface by [@wu-clan](https://github.com/wu-clan) in [#625](https://github.com/fastapi-practices/fastapi_best_architecture/pull/625)
|
||||||
|
* Update the token decode for logout interface by [@wu-clan](https://github.com/wu-clan) in [#629](https://github.com/fastapi-practices/fastapi_best_architecture/pull/629)
|
||||||
|
|
||||||
|
|
||||||
|
**Full Changelog**: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.4.0...v1.4.1
|
||||||
|
|
||||||
|
[Changes][v1.4.1]
|
||||||
|
|
||||||
|
|
||||||
|
<a id="v1.4.0"></a>
|
||||||
|
# [v1.4.0](https://github.com/fastapi-practices/fastapi_best_architecture/releases/tag/v1.4.0) - 2025-05-22
|
||||||
|
|
||||||
|
## What's Changed
|
||||||
|
* Update changelog for v1.3.0 by [@wu-clan](https://github.com/wu-clan) in [#605](https://github.com/fastapi-practices/fastapi_best_architecture/pull/605)
|
||||||
|
* Add new plugin status check interface by [@wu-clan](https://github.com/wu-clan) in [#606](https://github.com/fastapi-practices/fastapi_best_architecture/pull/606)
|
||||||
|
* Update the new plugin status to changed by [@wu-clan](https://github.com/wu-clan) in [#607](https://github.com/fastapi-practices/fastapi_best_architecture/pull/607)
|
||||||
|
* Fix the task result schema param type by [@wu-clan](https://github.com/wu-clan) in [#611](https://github.com/fastapi-practices/fastapi_best_architecture/pull/611)
|
||||||
|
* Fix the plugin status update logic by [@wu-clan](https://github.com/wu-clan) in [#613](https://github.com/fastapi-practices/fastapi_best_architecture/pull/613)
|
||||||
|
* Update uninstall and build plugin api method by [@wu-clan](https://github.com/wu-clan) in [#614](https://github.com/fastapi-practices/fastapi_best_architecture/pull/614)
|
||||||
|
* Fix non-asyncio nested async IO by [@wu-clan](https://github.com/wu-clan) in [#610](https://github.com/fastapi-practices/fastapi_best_architecture/pull/610)
|
||||||
|
* Update the build plugin api params by [@wu-clan](https://github.com/wu-clan) in [#615](https://github.com/fastapi-practices/fastapi_best_architecture/pull/615)
|
||||||
|
* Update uv installation in docker deploy by [@wu-clan](https://github.com/wu-clan) in [#619](https://github.com/fastapi-practices/fastapi_best_architecture/pull/619)
|
||||||
|
* Update the OAuth2 module to plugin by [@wu-clan](https://github.com/wu-clan) in [#620](https://github.com/fastapi-practices/fastapi_best_architecture/pull/620)
|
||||||
|
|
||||||
|
|
||||||
|
**Full Changelog**: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.3.0...v1.4.0
|
||||||
|
|
||||||
|
[Changes][v1.4.0]
|
||||||
|
|
||||||
|
|
||||||
|
<a id="v1.3.0"></a>
|
||||||
|
# [v1.3.0](https://github.com/fastapi-practices/fastapi_best_architecture/releases/tag/v1.3.0) - 2025-05-16
|
||||||
|
|
||||||
|
## What's Changed
|
||||||
|
* Update changelog for v1.2.0 by [@wu-clan](https://github.com/wu-clan) in [#598](https://github.com/fastapi-practices/fastapi_best_architecture/pull/598)
|
||||||
|
* Simplify apps and plugins config method by [@wu-clan](https://github.com/wu-clan) in [#600](https://github.com/fastapi-practices/fastapi_best_architecture/pull/600)
|
||||||
|
* Add plugin info config and interfaces by [@wu-clan](https://github.com/wu-clan) in [#601](https://github.com/fastapi-practices/fastapi_best_architecture/pull/601)
|
||||||
|
* Fix the fastapi cli startup event loop by [@wu-clan](https://github.com/wu-clan) in [#602](https://github.com/fastapi-practices/fastapi_best_architecture/pull/602)
|
||||||
|
* Optimize the zip plugin install logic by [@wu-clan](https://github.com/wu-clan) in [#603](https://github.com/fastapi-practices/fastapi_best_architecture/pull/603)
|
||||||
|
* Update the casbin RBAC module path by [@wu-clan](https://github.com/wu-clan) in [#604](https://github.com/fastapi-practices/fastapi_best_architecture/pull/604)
|
||||||
|
|
||||||
|
|
||||||
|
**Full Changelog**: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.2.0...v1.3.0
|
||||||
|
|
||||||
|
[Changes][v1.3.0]
|
||||||
|
|
||||||
|
|
||||||
<a id="v1.2.0"></a>
|
<a id="v1.2.0"></a>
|
||||||
# [v1.2.0](https://github.com/fastapi-practices/fastapi_best_architecture/releases/tag/v1.2.0) - 2025-05-01
|
# [v1.2.0](https://github.com/fastapi-practices/fastapi_best_architecture/releases/tag/v1.2.0) - 2025-05-01
|
||||||
|
|
||||||
@@ -494,6 +591,11 @@
|
|||||||
[Changes][v1.0.0]
|
[Changes][v1.0.0]
|
||||||
|
|
||||||
|
|
||||||
|
[v1.4.3]: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.4.2...v1.4.3
|
||||||
|
[v1.4.2]: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.4.1...v1.4.2
|
||||||
|
[v1.4.1]: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.4.0...v1.4.1
|
||||||
|
[v1.4.0]: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.3.0...v1.4.0
|
||||||
|
[v1.3.0]: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.2.0...v1.3.0
|
||||||
[v1.2.0]: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.1.2...v1.2.0
|
[v1.2.0]: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.1.2...v1.2.0
|
||||||
[v1.1.2]: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.1.1...v1.1.2
|
[v1.1.2]: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.1.1...v1.1.2
|
||||||
[v1.1.1]: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.1.0...v1.1.1
|
[v1.1.1]: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.1.0...v1.1.1
|
||||||
|
|||||||
+1
-2
@@ -2,8 +2,7 @@
|
|||||||
ARG SERVER_TYPE=fastapi_server
|
ARG SERVER_TYPE=fastapi_server
|
||||||
|
|
||||||
# === Python environment from uv ===
|
# === Python environment from uv ===
|
||||||
FROM python:3.10-slim AS builder
|
FROM ghcr.io/astral-sh/uv:python3.10-bookworm-slim AS builder
|
||||||
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
|
|
||||||
|
|
||||||
# Used for build Python packages
|
# Used for build Python packages
|
||||||
RUN sed -i 's/deb.debian.org/mirrors.ustc.edu.cn/g' /etc/apt/sources.list.d/debian.sources \
|
RUN sed -i 's/deb.debian.org/mirrors.ustc.edu.cn/g' /etc/apt/sources.list.d/debian.sources \
|
||||||
|
|||||||
@@ -4,11 +4,9 @@
|
|||||||
|
|
||||||
# FastAPI Best Architecture
|
# FastAPI Best Architecture
|
||||||
|
|
||||||
English | [简体中文](./README.zh-CN.md)
|
|
||||||
|
|
||||||
Enterprise-level backend architecture solution
|
Enterprise-level backend architecture solution
|
||||||
|
|
||||||
**🔥Continuously updated and maintained🔥**
|
English | [简体中文](./README.zh-CN.md)
|
||||||
|
|
||||||
[](https://github.com/fastapi-practices/fastapi_best_architecture/blob/master/LICENSE)
|
[](https://github.com/fastapi-practices/fastapi_best_architecture/blob/master/LICENSE)
|
||||||
[](https://www.python.org/downloads/)
|
[](https://www.python.org/downloads/)
|
||||||
@@ -21,12 +19,10 @@ Enterprise-level backend architecture solution
|
|||||||

|

|
||||||
[](https://discord.com/invite/yNN3wTbVAC)
|
[](https://discord.com/invite/yNN3wTbVAC)
|
||||||

|

|
||||||
|
[](https://deepwiki.com/fastapi-practices/fastapi_best_architecture)
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
> [!NOTE]
|
|
||||||
> This repository as a template library open to any person or enterprise can be used for free!
|
|
||||||
|
|
||||||
## Pseudo 3-tier architecture
|
## Pseudo 3-tier architecture
|
||||||
|
|
||||||
The mvc architecture is a common design pattern in python web, but the 3-tier architecture is even more fascinating
|
The mvc architecture is a common design pattern in python web, but the 3-tier architecture is even more fascinating
|
||||||
@@ -45,43 +41,7 @@ pattern, use templates to transform it to your heart's content!
|
|||||||
| data access | dao / mapper | crud |
|
| data access | dao / mapper | crud |
|
||||||
| model | model / entity | model |
|
| model | model / entity | model |
|
||||||
|
|
||||||
## Features
|
## Help
|
||||||
|
|
||||||
- [x] Global FastAPI PEP 593 Annotated parameter style
|
|
||||||
- [x] Comprehensive async/await + asgiref asynchronous design
|
|
||||||
- [x] Adheres to RESTful API specifications
|
|
||||||
- [x] Uses SQLAlchemy 2.0 with new syntax
|
|
||||||
- [x] Uses Pydantic v2 version
|
|
||||||
- [x] Implements role-menu RBAC access control
|
|
||||||
- [x] Integrates Casbin RBAC access control
|
|
||||||
- [x] Supports Celery asynchronous tasks
|
|
||||||
- [x] Custom-developed JWT authentication middleware
|
|
||||||
- [x] Supports global custom time zones
|
|
||||||
- [x] Supports Docker / Docker-compose deployment
|
|
||||||
- [x] Integrates Pytest unit testing
|
|
||||||
|
|
||||||
## Built-in Functions
|
|
||||||
|
|
||||||
- [x] User Management: Assign roles and permissions
|
|
||||||
- [x] Department Management: Configure organizational structure (company, department, team, etc.)
|
|
||||||
- [x] Menu Management: Set up menus and button-level permissions
|
|
||||||
- [x] Role Management: Configure roles, assign menus and permissions
|
|
||||||
- [x] Dictionary Management: Maintain common parameters and configurations
|
|
||||||
- [x] Parameter Management: Dynamically configure commonly used system parameters
|
|
||||||
- [x] Notification Announcements: Publish and maintain system notification and announcement information
|
|
||||||
- [x] Token Management: Detect online status, support forced logout
|
|
||||||
- [x] Multi-device Login: Support one-click switching between multi-device login modes
|
|
||||||
- [x] OAuth 2.0: Built-in custom-developed OAuth 2.0 authorization login
|
|
||||||
- [x] Plugin System: Hot-swappable plugin design to reduce coupling
|
|
||||||
- [x] Scheduled Tasks: Support scheduled, asynchronous tasks, and function calls
|
|
||||||
- [x] Code Generation: Automatically generate code with preview, write, and download support
|
|
||||||
- [x] Operation Logs: Record and query normal and abnormal operations
|
|
||||||
- [x] Login Logs: Record and query normal and abnormal logins
|
|
||||||
- [x] Cache Monitoring: Query system cache information and command statistics
|
|
||||||
- [x] Service Monitoring: View server hardware information and status
|
|
||||||
- [x] API Documentation: Automatically generate online interactive API documentation
|
|
||||||
|
|
||||||
## Development and deployment
|
|
||||||
|
|
||||||
For more details, please check
|
For more details, please check
|
||||||
the [official documentation](https://fastapi-practices.github.io/fastapi_best_architecture_docs/)
|
the [official documentation](https://fastapi-practices.github.io/fastapi_best_architecture_docs/)
|
||||||
|
|||||||
+3
-43
@@ -4,11 +4,9 @@
|
|||||||
|
|
||||||
# FastAPI Best Architecture
|
# FastAPI Best Architecture
|
||||||
|
|
||||||
简体中文 | [English](./README.md)
|
|
||||||
|
|
||||||
企业级后端架构解决方案
|
企业级后端架构解决方案
|
||||||
|
|
||||||
**🔥持续更新维护中🔥**
|
简体中文 | [English](./README.md)
|
||||||
|
|
||||||
[](https://github.com/fastapi-practices/fastapi_best_architecture/blob/master/LICENSE)
|
[](https://github.com/fastapi-practices/fastapi_best_architecture/blob/master/LICENSE)
|
||||||
[](https://www.python.org/downloads/)
|
[](https://www.python.org/downloads/)
|
||||||
@@ -21,12 +19,10 @@
|
|||||||

|

|
||||||
[](https://discord.com/invite/yNN3wTbVAC)
|
[](https://discord.com/invite/yNN3wTbVAC)
|
||||||

|

|
||||||
|
[](https://deepwiki.com/fastapi-practices/fastapi_best_architecture)
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
> [!NOTE]
|
|
||||||
> 此仓库作为模板库公开,任何人或企业均可免费使用!
|
|
||||||
|
|
||||||
## 伪三层架构
|
## 伪三层架构
|
||||||
|
|
||||||
mvc 架构作为常规设计模式,在 python web 中也很常见,但是三层架构更令人着迷
|
mvc 架构作为常规设计模式,在 python web 中也很常见,但是三层架构更令人着迷
|
||||||
@@ -43,43 +39,7 @@ mvc 架构作为常规设计模式,在 python web 中也很常见,但是三
|
|||||||
| 数据访问 | dao / mapper | crud |
|
| 数据访问 | dao / mapper | crud |
|
||||||
| 模型 | model / entity | model |
|
| 模型 | model / entity | model |
|
||||||
|
|
||||||
## 特性
|
## 帮助
|
||||||
|
|
||||||
- [x] 全局 FastAPI PEP 593 Annotated 参数风格
|
|
||||||
- [x] 全面 async/await + asgiref 异步设计
|
|
||||||
- [x] 遵循 RESTful API 规范
|
|
||||||
- [x] 使用 SQLAlchemy 2.0 全新语法
|
|
||||||
- [x] 使用 Pydantic v2 版本
|
|
||||||
- [x] 实现角色菜单 RBAC 访问控制
|
|
||||||
- [x] 集成 Casbin RBAC 访问控制
|
|
||||||
- [x] 支持 Celery 异步任务
|
|
||||||
- [x] 自研 JWT 认证中间件
|
|
||||||
- [x] 支持全局自定义时间时区
|
|
||||||
- [x] 支持 Docker / Docker-compose 部署
|
|
||||||
- [x] 集成 Pytest 单元测试
|
|
||||||
|
|
||||||
## 内置功能
|
|
||||||
|
|
||||||
- [x] 用户管理:分配角色和权限
|
|
||||||
- [x] 部门管理:配置组织架构(公司、部门、小组等)
|
|
||||||
- [x] 菜单管理:设置菜单及按钮级权限
|
|
||||||
- [x] 角色管理:配置角色、分配菜单和权限
|
|
||||||
- [x] 字典管理:维护常用参数和配置
|
|
||||||
- [x] 参数管理:系统常用参数动态配置
|
|
||||||
- [x] 通知公告:发布和维护系统通知公告信息
|
|
||||||
- [x] 令牌管理:检测在线状态,支持强制下线
|
|
||||||
- [x] 多端登录:支持一键切换多端登录模式
|
|
||||||
- [x] OAuth 2.0:内置自研 OAuth 2.0 授权登录
|
|
||||||
- [x] 插件系统:热插拔插件设计,降低耦合
|
|
||||||
- [x] 定时任务:支持定时,异步任务及函数调用
|
|
||||||
- [x] 代码生成:自动生成代码,支持预览、写入和下载
|
|
||||||
- [x] 操作日志:记录和查询正常和异常操作
|
|
||||||
- [x] 登录日志:记录和查询正常和异常登录
|
|
||||||
- [x] 缓存监控:查询系统缓存信息和命令统计
|
|
||||||
- [x] 服务监控:查看服务器硬件信息和状态
|
|
||||||
- [x] 接口文档:自动生成在线交互式 API 文档
|
|
||||||
|
|
||||||
## 开发部署
|
|
||||||
|
|
||||||
更多详情请查看 [官方文档](https://fastapi-practices.github.io/fastapi_best_architecture_docs/)
|
更多详情请查看 [官方文档](https://fastapi-practices.github.io/fastapi_best_architecture_docs/)
|
||||||
|
|
||||||
|
|||||||
@@ -5,14 +5,12 @@ from fastapi import APIRouter
|
|||||||
from backend.app.admin.api.v1.auth import router as auth_router
|
from backend.app.admin.api.v1.auth import router as auth_router
|
||||||
from backend.app.admin.api.v1.log import router as log_router
|
from backend.app.admin.api.v1.log import router as log_router
|
||||||
from backend.app.admin.api.v1.monitor import router as monitor_router
|
from backend.app.admin.api.v1.monitor import router as monitor_router
|
||||||
from backend.app.admin.api.v1.oauth2 import router as oauth2_router
|
|
||||||
from backend.app.admin.api.v1.sys import router as sys_router
|
from backend.app.admin.api.v1.sys import router as sys_router
|
||||||
from backend.core.conf import settings
|
from backend.core.conf import settings
|
||||||
|
|
||||||
v1 = APIRouter(prefix=settings.FASTAPI_API_V1_PATH)
|
v1 = APIRouter(prefix=settings.FASTAPI_API_V1_PATH)
|
||||||
|
|
||||||
v1.include_router(auth_router)
|
v1.include_router(auth_router)
|
||||||
v1.include_router(oauth2_router)
|
|
||||||
v1.include_router(sys_router)
|
v1.include_router(sys_router)
|
||||||
v1.include_router(log_router)
|
v1.include_router(log_router)
|
||||||
v1.include_router(monitor_router)
|
v1.include_router(monitor_router)
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ from backend.app.admin.schema.token import GetLoginToken, GetNewToken, GetSwagge
|
|||||||
from backend.app.admin.schema.user import AuthLoginParam
|
from backend.app.admin.schema.user import AuthLoginParam
|
||||||
from backend.app.admin.service.auth_service import auth_service
|
from backend.app.admin.service.auth_service import auth_service
|
||||||
from backend.common.response.response_schema import ResponseModel, ResponseSchemaModel, response_base
|
from backend.common.response.response_schema import ResponseModel, ResponseSchemaModel, response_base
|
||||||
from backend.common.security.jwt import DependsJwtAuth
|
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
@@ -35,13 +34,13 @@ async def user_login(
|
|||||||
return response_base.success(data=data)
|
return response_base.success(data=data)
|
||||||
|
|
||||||
|
|
||||||
@router.post('/token/new', summary='创建新 token')
|
@router.post('/tokens/refresh', summary='刷新 token')
|
||||||
async def create_new_token(request: Request) -> ResponseSchemaModel[GetNewToken]:
|
async def refresh_token(request: Request) -> ResponseSchemaModel[GetNewToken]:
|
||||||
data = await auth_service.new_token(request=request)
|
data = await auth_service.new_token(request=request)
|
||||||
return response_base.success(data=data)
|
return response_base.success(data=data)
|
||||||
|
|
||||||
|
|
||||||
@router.post('/logout', summary='用户登出', dependencies=[DependsJwtAuth])
|
@router.post('/logout', summary='用户登出')
|
||||||
async def user_logout(request: Request, response: Response) -> ResponseModel:
|
async def user_logout(request: Request, response: Response) -> ResponseModel:
|
||||||
await auth_service.logout(request=request, response=response)
|
await auth_service.logout(request=request, response=response)
|
||||||
return response_base.success()
|
return response_base.success()
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
from fastapi import APIRouter
|
from fastapi import APIRouter
|
||||||
|
|
||||||
|
from backend.app.admin.api.v1.monitor.online import router as token_router
|
||||||
from backend.app.admin.api.v1.monitor.redis import router as redis_router
|
from backend.app.admin.api.v1.monitor.redis import router as redis_router
|
||||||
from backend.app.admin.api.v1.monitor.server import router as server_router
|
from backend.app.admin.api.v1.monitor.server import router as server_router
|
||||||
|
|
||||||
@@ -9,3 +10,4 @@ router = APIRouter(prefix='/monitors')
|
|||||||
|
|
||||||
router.include_router(redis_router, prefix='/redis', tags=['redis监控'])
|
router.include_router(redis_router, prefix='/redis', tags=['redis监控'])
|
||||||
router.include_router(server_router, prefix='/server', tags=['服务器监控'])
|
router.include_router(server_router, prefix='/server', tags=['服务器监控'])
|
||||||
|
router.include_router(token_router, prefix='/online', tags=['在线用户'])
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ from typing import Annotated
|
|||||||
|
|
||||||
from fastapi import APIRouter, Depends, Path, Query, Request
|
from fastapi import APIRouter, Depends, Path, Query, Request
|
||||||
|
|
||||||
from backend.app.admin.schema.token import GetTokenDetail, KickOutToken
|
from backend.app.admin.schema.token import GetTokenDetail
|
||||||
from backend.common.enums import StatusType
|
from backend.common.enums import StatusType
|
||||||
from backend.common.response.response_schema import ResponseModel, ResponseSchemaModel, response_base
|
from backend.common.response.response_schema import ResponseModel, ResponseSchemaModel, response_base
|
||||||
from backend.common.security.jwt import DependsJwtAuth, jwt_decode, revoke_token, superuser_verify
|
from backend.common.security.jwt import DependsJwtAuth, jwt_decode, revoke_token, superuser_verify
|
||||||
@@ -18,8 +18,8 @@ from backend.database.redis import redis_client
|
|||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
@router.get('', summary='获取令牌列表', dependencies=[DependsJwtAuth])
|
@router.get('', summary='获取在线用户', dependencies=[DependsJwtAuth])
|
||||||
async def get_tokens(
|
async def get_online(
|
||||||
username: Annotated[str | None, Query(description='用户名')] = None,
|
username: Annotated[str | None, Query(description='用户名')] = None,
|
||||||
) -> ResponseSchemaModel[list[GetTokenDetail]]:
|
) -> ResponseSchemaModel[list[GetTokenDetail]]:
|
||||||
token_keys = await redis_client.keys(f'{settings.TOKEN_REDIS_PREFIX}:*')
|
token_keys = await redis_client.keys(f'{settings.TOKEN_REDIS_PREFIX}:*')
|
||||||
@@ -82,8 +82,10 @@ async def get_tokens(
|
|||||||
],
|
],
|
||||||
)
|
)
|
||||||
async def kick_out(
|
async def kick_out(
|
||||||
request: Request, pk: Annotated[int, Path(description='用户 ID')], obj: KickOutToken
|
request: Request,
|
||||||
|
pk: Annotated[int, Path(description='用户 ID')],
|
||||||
|
session_uuid: Annotated[str, Query(description='会话 UUID')],
|
||||||
) -> ResponseModel:
|
) -> ResponseModel:
|
||||||
superuser_verify(request)
|
superuser_verify(request)
|
||||||
await revoke_token(str(pk), obj.session_uuid)
|
await revoke_token(str(pk), session_uuid)
|
||||||
return response_base.success()
|
return response_base.success()
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from fastapi import APIRouter
|
|
||||||
|
|
||||||
from backend.app.admin.api.v1.oauth2.github import router as github_router
|
|
||||||
from backend.app.admin.api.v1.oauth2.linux_do import router as linux_do_router
|
|
||||||
|
|
||||||
router = APIRouter(prefix='/oauth2')
|
|
||||||
|
|
||||||
router.include_router(github_router, prefix='/github', tags=['GitHub OAuth2'])
|
|
||||||
router.include_router(linux_do_router, prefix='/linux-do', tags=['LinuxDo OAuth2'])
|
|
||||||
@@ -8,7 +8,6 @@ from backend.app.admin.api.v1.sys.dept import router as dept_router
|
|||||||
from backend.app.admin.api.v1.sys.menu import router as menu_router
|
from backend.app.admin.api.v1.sys.menu import router as menu_router
|
||||||
from backend.app.admin.api.v1.sys.plugin import router as plugin_router
|
from backend.app.admin.api.v1.sys.plugin import router as plugin_router
|
||||||
from backend.app.admin.api.v1.sys.role import router as role_router
|
from backend.app.admin.api.v1.sys.role import router as role_router
|
||||||
from backend.app.admin.api.v1.sys.token import router as token_router
|
|
||||||
from backend.app.admin.api.v1.sys.upload import router as upload_router
|
from backend.app.admin.api.v1.sys.upload import router as upload_router
|
||||||
from backend.app.admin.api.v1.sys.user import router as user_router
|
from backend.app.admin.api.v1.sys.user import router as user_router
|
||||||
|
|
||||||
@@ -20,6 +19,5 @@ router.include_router(role_router, prefix='/roles', tags=['系统角色'])
|
|||||||
router.include_router(user_router, prefix='/users', tags=['系统用户'])
|
router.include_router(user_router, prefix='/users', tags=['系统用户'])
|
||||||
router.include_router(data_rule_router, prefix='/data-rules', tags=['系统数据规则'])
|
router.include_router(data_rule_router, prefix='/data-rules', tags=['系统数据规则'])
|
||||||
router.include_router(data_scope_router, prefix='/data-scopes', tags=['系统数据范围'])
|
router.include_router(data_scope_router, prefix='/data-scopes', tags=['系统数据范围'])
|
||||||
router.include_router(token_router, prefix='/tokens', tags=['系统令牌'])
|
|
||||||
router.include_router(upload_router, prefix='/upload', tags=['系统上传'])
|
router.include_router(upload_router, prefix='/upload', tags=['系统上传'])
|
||||||
router.include_router(plugin_router, prefix='/plugin', tags=['系统插件'])
|
router.include_router(plugin_router, prefix='/plugins', tags=['系统插件'])
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ async def get_data_rule_models() -> ResponseSchemaModel[list[str]]:
|
|||||||
return response_base.success(data=models)
|
return response_base.success(data=models)
|
||||||
|
|
||||||
|
|
||||||
@router.get('/model/{model}/columns', summary='获取数据规则可用模型列', dependencies=[DependsJwtAuth])
|
@router.get('/models/{model}/columns', summary='获取数据规则可用模型列', dependencies=[DependsJwtAuth])
|
||||||
async def get_data_rule_model_columns(
|
async def get_data_rule_model_columns(
|
||||||
model: Annotated[str, Path(description='模型名称')],
|
model: Annotated[str, Path(description='模型名称')],
|
||||||
) -> ResponseSchemaModel[list[GetDataRuleColumnDetail]]:
|
) -> ResponseSchemaModel[list[GetDataRuleColumnDetail]]:
|
||||||
|
|||||||
@@ -22,6 +22,12 @@ from backend.database.db import CurrentSession
|
|||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
@router.get('/all', summary='获取所有数据范围', dependencies=[DependsJwtAuth])
|
||||||
|
async def get_all_data_scope() -> ResponseSchemaModel[list[GetDataScopeDetail]]:
|
||||||
|
data = await data_scope_service.get_all()
|
||||||
|
return response_base.success(data=data)
|
||||||
|
|
||||||
|
|
||||||
@router.get('/{pk}', summary='获取数据范围详情', dependencies=[DependsJwtAuth])
|
@router.get('/{pk}', summary='获取数据范围详情', dependencies=[DependsJwtAuth])
|
||||||
async def get_data_scope(
|
async def get_data_scope(
|
||||||
pk: Annotated[int, Path(description='数据范围 ID')],
|
pk: Annotated[int, Path(description='数据范围 ID')],
|
||||||
|
|||||||
@@ -15,8 +15,8 @@ router = APIRouter()
|
|||||||
|
|
||||||
|
|
||||||
@router.get('/sidebar', summary='获取用户菜单侧边栏', description='适配 vben5', dependencies=[DependsJwtAuth])
|
@router.get('/sidebar', summary='获取用户菜单侧边栏', description='适配 vben5', dependencies=[DependsJwtAuth])
|
||||||
async def get_user_sidebar(request: Request) -> ResponseSchemaModel[list[dict[str, Any]]]:
|
async def get_user_sidebar(request: Request) -> ResponseSchemaModel[list[dict[str, Any] | None]]:
|
||||||
menu = await menu_service.get_user_menu_tree(request=request)
|
menu = await menu_service.get_sidebar(request=request)
|
||||||
return response_base.success(data=menu)
|
return response_base.success(data=menu)
|
||||||
|
|
||||||
|
|
||||||
@@ -71,7 +71,7 @@ async def update_menu(pk: Annotated[int, Path(description='菜单 ID')], obj: Up
|
|||||||
DependsRBAC,
|
DependsRBAC,
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
async def delete_menu(pk: Annotated[int, Path(description='菜单 ID 列表')]) -> ResponseModel:
|
async def delete_menu(pk: Annotated[int, Path(description='菜单 ID')]) -> ResponseModel:
|
||||||
count = await menu_service.delete(pk=pk)
|
count = await menu_service.delete(pk=pk)
|
||||||
if count > 0:
|
if count > 0:
|
||||||
return response_base.success()
|
return response_base.success()
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
from typing import Annotated, Any
|
from typing import Annotated, Any
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, File, UploadFile
|
from fastapi import APIRouter, Depends, File, Path, UploadFile
|
||||||
from fastapi.params import Query
|
from fastapi.params import Query
|
||||||
from starlette.responses import StreamingResponse
|
from starlette.responses import StreamingResponse
|
||||||
|
|
||||||
@@ -22,12 +22,18 @@ async def get_all_plugins() -> ResponseSchemaModel[list[dict[str, Any]]]:
|
|||||||
return response_base.success(data=plugins)
|
return response_base.success(data=plugins)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get('/changes', summary='插件状态是否变更', dependencies=[DependsJwtAuth])
|
||||||
|
async def plugin_changed() -> ResponseSchemaModel[bool]:
|
||||||
|
plugins = await plugin_service.changed()
|
||||||
|
return response_base.success(data=bool(plugins))
|
||||||
|
|
||||||
|
|
||||||
@router.post(
|
@router.post(
|
||||||
'/install/zip',
|
'/zip',
|
||||||
summary='安装 zip 插件',
|
summary='安装 zip 插件',
|
||||||
description='使用插件 zip 压缩包进行安装',
|
description='使用插件 zip 压缩包进行安装',
|
||||||
dependencies=[
|
dependencies=[
|
||||||
Depends(RequestPermission('sys:plugin:install')),
|
Depends(RequestPermission('sys:plugin:zip')),
|
||||||
DependsRBAC,
|
DependsRBAC,
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
@@ -37,11 +43,11 @@ async def install_zip_plugin(file: Annotated[UploadFile, File()]) -> ResponseMod
|
|||||||
|
|
||||||
|
|
||||||
@router.post(
|
@router.post(
|
||||||
'/install/git',
|
'/git',
|
||||||
summary='安装 git 插件',
|
summary='安装 git 插件',
|
||||||
description='使用插件 git 仓库地址进行安装,不限制平台;如果需要凭证,需在 git 仓库地址中添加凭证信息',
|
description='使用插件 git 仓库地址进行安装,不限制平台;如果需要凭证,需在 git 仓库地址中添加凭证信息',
|
||||||
dependencies=[
|
dependencies=[
|
||||||
Depends(RequestPermission('sys:plugin:install')),
|
Depends(RequestPermission('sys:plugin:git')),
|
||||||
DependsRBAC,
|
DependsRBAC,
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
@@ -50,44 +56,36 @@ async def install_git_plugin(repo_url: Annotated[str, Query(description='插件
|
|||||||
return response_base.success(res=CustomResponseCode.PLUGIN_INSTALL_SUCCESS)
|
return response_base.success(res=CustomResponseCode.PLUGIN_INSTALL_SUCCESS)
|
||||||
|
|
||||||
|
|
||||||
@router.post(
|
@router.delete(
|
||||||
'/uninstall',
|
'/{plugin}',
|
||||||
summary='卸载插件',
|
summary='卸载插件',
|
||||||
description='此操作会直接删除插件依赖,但不会直接删除插件,而是将插件移动到备份目录',
|
description='此操作会直接删除插件依赖,但不会直接删除插件,而是将插件移动到备份目录',
|
||||||
dependencies=[
|
dependencies=[
|
||||||
Depends(RequestPermission('sys:plugin:uninstall')),
|
Depends(RequestPermission('sys:plugin:del')),
|
||||||
DependsRBAC,
|
DependsRBAC,
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
async def uninstall_plugin(plugin: Annotated[str, Query(description='插件名称')]) -> ResponseModel:
|
async def uninstall_plugin(plugin: Annotated[str, Path(description='插件名称')]) -> ResponseModel:
|
||||||
await plugin_service.uninstall(plugin=plugin)
|
await plugin_service.uninstall(plugin=plugin)
|
||||||
return response_base.success(res=CustomResponseCode.PLUGIN_UNINSTALL_SUCCESS)
|
return response_base.success(res=CustomResponseCode.PLUGIN_UNINSTALL_SUCCESS)
|
||||||
|
|
||||||
|
|
||||||
@router.post(
|
@router.post(
|
||||||
'/status',
|
'/{plugin}/status',
|
||||||
summary='更新插件状态',
|
summary='更新插件状态',
|
||||||
dependencies=[
|
dependencies=[
|
||||||
Depends(RequestPermission('sys:plugin:status')),
|
Depends(RequestPermission('sys:plugin:status')),
|
||||||
DependsRBAC,
|
DependsRBAC,
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
async def update_plugin_status(plugin: Annotated[str, Query(description='插件名称')]) -> ResponseModel:
|
async def update_plugin_status(plugin: Annotated[str, Path(description='插件名称')]) -> ResponseModel:
|
||||||
await plugin_service.update_status(plugin=plugin)
|
await plugin_service.update_status(plugin=plugin)
|
||||||
return response_base.success()
|
return response_base.success()
|
||||||
|
|
||||||
|
|
||||||
@router.post(
|
@router.get('/{plugin}', summary='打包并下载插件', dependencies=[DependsJwtAuth])
|
||||||
'/zip',
|
async def build_plugin(plugin: Annotated[str, Path(description='插件名称')]) -> StreamingResponse:
|
||||||
summary='打包插件',
|
|
||||||
dependencies=[
|
|
||||||
Depends(RequestPermission('sys:plugin:zip')),
|
|
||||||
DependsRBAC,
|
|
||||||
],
|
|
||||||
)
|
|
||||||
async def build_plugin(plugin: Annotated[str, Query(description='插件名称')]) -> StreamingResponse:
|
|
||||||
bio = await plugin_service.build(plugin=plugin)
|
bio = await plugin_service.build(plugin=plugin)
|
||||||
bio.seek(0)
|
|
||||||
return StreamingResponse(
|
return StreamingResponse(
|
||||||
bio,
|
bio,
|
||||||
media_type='application/x-zip-compressed',
|
media_type='application/x-zip-compressed',
|
||||||
|
|||||||
@@ -29,18 +29,10 @@ async def get_all_roles() -> ResponseSchemaModel[list[GetRoleDetail]]:
|
|||||||
return response_base.success(data=data)
|
return response_base.success(data=data)
|
||||||
|
|
||||||
|
|
||||||
@router.get('/{pk}/all', summary='获取用户所有角色', dependencies=[DependsJwtAuth])
|
|
||||||
async def get_user_all_roles(
|
|
||||||
pk: Annotated[int, Path(description='用户 ID')],
|
|
||||||
) -> ResponseSchemaModel[list[GetRoleDetail]]:
|
|
||||||
data = await role_service.get_users(pk=pk)
|
|
||||||
return response_base.success(data=data)
|
|
||||||
|
|
||||||
|
|
||||||
@router.get('/{pk}/menus', summary='获取角色所有菜单', dependencies=[DependsJwtAuth])
|
@router.get('/{pk}/menus', summary='获取角色所有菜单', dependencies=[DependsJwtAuth])
|
||||||
async def get_role_all_menus(
|
async def get_role_all_menus(
|
||||||
pk: Annotated[int, Path(description='角色 ID')],
|
pk: Annotated[int, Path(description='角色 ID')],
|
||||||
) -> ResponseSchemaModel[list[dict[str, Any]]]:
|
) -> ResponseSchemaModel[list[dict[str, Any] | None]]:
|
||||||
menu = await role_service.get_menu_tree(pk=pk)
|
menu = await role_service.get_menu_tree(pk=pk)
|
||||||
return response_base.success(data=menu)
|
return response_base.success(data=menu)
|
||||||
|
|
||||||
@@ -106,7 +98,7 @@ async def update_role(pk: Annotated[int, Path(description='角色 ID')], obj: Up
|
|||||||
|
|
||||||
|
|
||||||
@router.put(
|
@router.put(
|
||||||
'/{pk}/menu',
|
'/{pk}/menus',
|
||||||
summary='更新角色菜单',
|
summary='更新角色菜单',
|
||||||
dependencies=[
|
dependencies=[
|
||||||
Depends(RequestPermission('sys:role:menu:edit')),
|
Depends(RequestPermission('sys:role:menu:edit')),
|
||||||
@@ -123,7 +115,7 @@ async def update_role_menus(
|
|||||||
|
|
||||||
|
|
||||||
@router.put(
|
@router.put(
|
||||||
'/{pk}/scope',
|
'/{pk}/scopes',
|
||||||
summary='更新角色数据范围',
|
summary='更新角色数据范围',
|
||||||
dependencies=[
|
dependencies=[
|
||||||
Depends(RequestPermission('sys:role:scope:edit')),
|
Depends(RequestPermission('sys:role:scope:edit')),
|
||||||
|
|||||||
@@ -4,15 +4,13 @@ from typing import Annotated
|
|||||||
|
|
||||||
from fastapi import APIRouter, Depends, Path, Query, Request
|
from fastapi import APIRouter, Depends, Path, Query, Request
|
||||||
|
|
||||||
|
from backend.app.admin.schema.role import GetRoleDetail
|
||||||
from backend.app.admin.schema.user import (
|
from backend.app.admin.schema.user import (
|
||||||
AddUserParam,
|
AddUserParam,
|
||||||
AvatarParam,
|
|
||||||
GetCurrentUserInfoWithRelationDetail,
|
GetCurrentUserInfoWithRelationDetail,
|
||||||
GetUserInfoWithRelationDetail,
|
GetUserInfoWithRelationDetail,
|
||||||
RegisterUserParam,
|
|
||||||
ResetPasswordParam,
|
ResetPasswordParam,
|
||||||
UpdateUserParam,
|
UpdateUserParam,
|
||||||
UpdateUserRoleParam,
|
|
||||||
)
|
)
|
||||||
from backend.app.admin.service.user_service import user_service
|
from backend.app.admin.service.user_service import user_service
|
||||||
from backend.common.pagination import DependsPagination, PageData, paging_data
|
from backend.common.pagination import DependsPagination, PageData, paging_data
|
||||||
@@ -25,12 +23,6 @@ from backend.database.db import CurrentSession
|
|||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
@router.post('/register', summary='注册用户')
|
|
||||||
async def register_user(obj: RegisterUserParam) -> ResponseModel:
|
|
||||||
await user_service.register(obj=obj)
|
|
||||||
return response_base.success()
|
|
||||||
|
|
||||||
|
|
||||||
@router.post('/add', summary='添加用户', dependencies=[DependsRBAC])
|
@router.post('/add', summary='添加用户', dependencies=[DependsRBAC])
|
||||||
async def add_user(request: Request, obj: AddUserParam) -> ResponseSchemaModel[GetUserInfoWithRelationDetail]:
|
async def add_user(request: Request, obj: AddUserParam) -> ResponseSchemaModel[GetUserInfoWithRelationDetail]:
|
||||||
await user_service.add(request=request, obj=obj)
|
await user_service.add(request=request, obj=obj)
|
||||||
@@ -38,9 +30,11 @@ async def add_user(request: Request, obj: AddUserParam) -> ResponseSchemaModel[G
|
|||||||
return response_base.success(data=data)
|
return response_base.success(data=data)
|
||||||
|
|
||||||
|
|
||||||
@router.post('/password/reset', summary='密码重置', dependencies=[DependsJwtAuth])
|
@router.post('/{username}/password', summary='密码重置', dependencies=[DependsJwtAuth])
|
||||||
async def password_reset(request: Request, obj: ResetPasswordParam) -> ResponseModel:
|
async def password_reset(
|
||||||
count = await user_service.pwd_reset(request=request, obj=obj)
|
username: Annotated[str, Path(description='用户名')], obj: ResetPasswordParam
|
||||||
|
) -> ResponseModel:
|
||||||
|
count = await user_service.pwd_reset(username=username, obj=obj)
|
||||||
if count > 0:
|
if count > 0:
|
||||||
return response_base.success()
|
return response_base.success()
|
||||||
return response_base.fail()
|
return response_base.fail()
|
||||||
@@ -60,39 +54,12 @@ async def get_user(
|
|||||||
return response_base.success(data=data)
|
return response_base.success(data=data)
|
||||||
|
|
||||||
|
|
||||||
@router.put('/{username}', summary='更新用户信息', dependencies=[DependsJwtAuth])
|
@router.get('/{username}/roles', summary='获取用户所有角色', dependencies=[DependsJwtAuth])
|
||||||
async def update_user(
|
async def get_user_all_roles(
|
||||||
request: Request, username: Annotated[str, Path(description='用户名')], obj: UpdateUserParam
|
username: Annotated[str, Path(description='用户名')],
|
||||||
) -> ResponseModel:
|
) -> ResponseSchemaModel[list[GetRoleDetail]]:
|
||||||
count = await user_service.update(request=request, username=username, obj=obj)
|
data = await user_service.get_roles(username=username)
|
||||||
if count > 0:
|
return response_base.success(data=data)
|
||||||
return response_base.success()
|
|
||||||
return response_base.fail()
|
|
||||||
|
|
||||||
|
|
||||||
@router.put(
|
|
||||||
'/{username}/role',
|
|
||||||
summary='更新用户角色',
|
|
||||||
dependencies=[
|
|
||||||
Depends(RequestPermission('sys:user:role:edit')),
|
|
||||||
DependsRBAC,
|
|
||||||
],
|
|
||||||
)
|
|
||||||
async def update_user_role(
|
|
||||||
request: Request, username: Annotated[str, Path(description='用户名')], obj: UpdateUserRoleParam
|
|
||||||
) -> ResponseModel:
|
|
||||||
await user_service.update_roles(request=request, username=username, obj=obj)
|
|
||||||
return response_base.success()
|
|
||||||
|
|
||||||
|
|
||||||
@router.put('/{username}/avatar', summary='更新头像', dependencies=[DependsJwtAuth])
|
|
||||||
async def update_avatar(
|
|
||||||
request: Request, username: Annotated[str, Path(description='用户名')], avatar: AvatarParam
|
|
||||||
) -> ResponseModel:
|
|
||||||
count = await user_service.update_avatar(request=request, username=username, avatar=avatar)
|
|
||||||
if count > 0:
|
|
||||||
return response_base.success()
|
|
||||||
return response_base.fail()
|
|
||||||
|
|
||||||
|
|
||||||
@router.get(
|
@router.get(
|
||||||
@@ -115,6 +82,16 @@ async def get_pagination_users(
|
|||||||
return response_base.success(data=page_data)
|
return response_base.success(data=page_data)
|
||||||
|
|
||||||
|
|
||||||
|
@router.put('/{username}', summary='更新用户信息', dependencies=[DependsJwtAuth])
|
||||||
|
async def update_user(
|
||||||
|
request: Request, username: Annotated[str, Path(description='用户名')], obj: UpdateUserParam
|
||||||
|
) -> ResponseModel:
|
||||||
|
count = await user_service.update(request=request, username=username, obj=obj)
|
||||||
|
if count > 0:
|
||||||
|
return response_base.success()
|
||||||
|
return response_base.fail()
|
||||||
|
|
||||||
|
|
||||||
@router.put('/{pk}/super', summary='修改用户超级权限', dependencies=[DependsRBAC])
|
@router.put('/{pk}/super', summary='修改用户超级权限', dependencies=[DependsRBAC])
|
||||||
async def super_set(request: Request, pk: Annotated[int, Path(description='用户 ID')]) -> ResponseModel:
|
async def super_set(request: Request, pk: Annotated[int, Path(description='用户 ID')]) -> ResponseModel:
|
||||||
count = await user_service.update_permission(request=request, pk=pk)
|
count = await user_service.update_permission(request=request, pk=pk)
|
||||||
@@ -149,8 +126,7 @@ async def multi_set(request: Request, pk: Annotated[int, Path(description='用
|
|||||||
|
|
||||||
@router.delete(
|
@router.delete(
|
||||||
path='/{username}',
|
path='/{username}',
|
||||||
summary='用户注销',
|
summary='删除用户',
|
||||||
description='用户注销 != 用户登出,注销之后用户将从数据库删除',
|
|
||||||
dependencies=[
|
dependencies=[
|
||||||
Depends(RequestPermission('sys:user:del')),
|
Depends(RequestPermission('sys:user:del')),
|
||||||
DependsRBAC,
|
DependsRBAC,
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ class CRUDDataRule(CRUDPlus[DataRule]):
|
|||||||
:param name: 规则名称
|
:param name: 规则名称
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
stmt = select(self.model).options(noload(self.model.scope)).order_by(desc(self.model.created_time))
|
stmt = select(self.model).options(noload(self.model.scopes)).order_by(desc(self.model.created_time))
|
||||||
|
|
||||||
filters = []
|
filters = []
|
||||||
if name is not None:
|
if name is not None:
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
from typing import Sequence
|
||||||
|
|
||||||
from sqlalchemy import Select, and_, desc, select
|
from sqlalchemy import Select, and_, desc, select
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
from sqlalchemy.orm import noload, selectinload
|
from sqlalchemy.orm import noload, selectinload
|
||||||
@@ -44,6 +46,15 @@ class CRUDDataScope(CRUDPlus[DataScope]):
|
|||||||
data_scope = await db.execute(stmt)
|
data_scope = await db.execute(stmt)
|
||||||
return data_scope.scalars().first()
|
return data_scope.scalars().first()
|
||||||
|
|
||||||
|
async def get_all(self, db: AsyncSession) -> Sequence[DataScope]:
|
||||||
|
"""
|
||||||
|
获取所有数据范围
|
||||||
|
|
||||||
|
:param db: 数据库会话
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
return await self.select_models(db)
|
||||||
|
|
||||||
async def get_list(self, name: str | None, status: int | None) -> Select:
|
async def get_list(self, name: str | None, status: int | None) -> Select:
|
||||||
"""
|
"""
|
||||||
获取数据范围列表
|
获取数据范围列表
|
||||||
|
|||||||
@@ -45,12 +45,12 @@ class CRUDMenu(CRUDPlus[Menu]):
|
|||||||
"""
|
"""
|
||||||
filters = {}
|
filters = {}
|
||||||
if title is not None:
|
if title is not None:
|
||||||
filters.update(title=f'%{title}%')
|
filters.update(title__like=f'%{title}%')
|
||||||
if status is not None:
|
if status is not None:
|
||||||
filters.update(status=status)
|
filters.update(status=status)
|
||||||
return await self.select_models_order(db, 'sort', **filters)
|
return await self.select_models_order(db, 'sort', **filters)
|
||||||
|
|
||||||
async def get_role_menus(self, db: AsyncSession, superuser: bool, menu_ids: list[int]) -> Sequence[Menu]:
|
async def get_sidebar(self, db: AsyncSession, superuser: bool, menu_ids: list[int | None]) -> Sequence[Menu]:
|
||||||
"""
|
"""
|
||||||
获取角色菜单列表
|
获取角色菜单列表
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
|||||||
from sqlalchemy.orm import noload, selectinload
|
from sqlalchemy.orm import noload, selectinload
|
||||||
from sqlalchemy_crud_plus import CRUDPlus
|
from sqlalchemy_crud_plus import CRUDPlus
|
||||||
|
|
||||||
from backend.app.admin.model import DataScope, Menu, Role, User
|
from backend.app.admin.model import DataScope, Menu, Role
|
||||||
from backend.app.admin.schema.role import (
|
from backend.app.admin.schema.role import (
|
||||||
CreateRoleParam,
|
CreateRoleParam,
|
||||||
UpdateRoleMenuParam,
|
UpdateRoleMenuParam,
|
||||||
@@ -54,18 +54,6 @@ class CRUDRole(CRUDPlus[Role]):
|
|||||||
"""
|
"""
|
||||||
return await self.select_models(db)
|
return await self.select_models(db)
|
||||||
|
|
||||||
async def get_users(self, db: AsyncSession, user_id: int) -> Sequence[Role]:
|
|
||||||
"""
|
|
||||||
获取用户角色列表
|
|
||||||
|
|
||||||
:param db: 数据库会话
|
|
||||||
:param user_id: 用户 ID
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
stmt = select(self.model).join(self.model.users).where(User.id == user_id)
|
|
||||||
roles = await db.execute(stmt)
|
|
||||||
return roles.scalars().all()
|
|
||||||
|
|
||||||
async def get_list(self, name: str | None, status: int | None) -> Select:
|
async def get_list(self, name: str | None, status: int | None) -> Select:
|
||||||
"""
|
"""
|
||||||
获取角色列表
|
获取角色列表
|
||||||
|
|||||||
@@ -10,11 +10,9 @@ from sqlalchemy_crud_plus import CRUDPlus
|
|||||||
|
|
||||||
from backend.app.admin.model import Dept, Role, User
|
from backend.app.admin.model import Dept, Role, User
|
||||||
from backend.app.admin.schema.user import (
|
from backend.app.admin.schema.user import (
|
||||||
|
AddOAuth2UserParam,
|
||||||
AddUserParam,
|
AddUserParam,
|
||||||
AvatarParam,
|
|
||||||
RegisterUserParam,
|
|
||||||
UpdateUserParam,
|
UpdateUserParam,
|
||||||
UpdateUserRoleParam,
|
|
||||||
)
|
)
|
||||||
from backend.common.security.jwt import get_hash_password
|
from backend.common.security.jwt import get_hash_password
|
||||||
from backend.utils.timezone import timezone
|
from backend.utils.timezone import timezone
|
||||||
@@ -63,26 +61,6 @@ class CRUDUser(CRUDPlus[User]):
|
|||||||
"""
|
"""
|
||||||
return await self.update_model_by_column(db, {'last_login_time': timezone.now()}, username=username)
|
return await self.update_model_by_column(db, {'last_login_time': timezone.now()}, username=username)
|
||||||
|
|
||||||
async def create(self, db: AsyncSession, obj: RegisterUserParam, *, social: bool = False) -> None:
|
|
||||||
"""
|
|
||||||
创建用户
|
|
||||||
|
|
||||||
:param db: 数据库会话
|
|
||||||
:param obj: 注册用户参数
|
|
||||||
:param social: 是否社交用户
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
if not social:
|
|
||||||
salt = bcrypt.gensalt()
|
|
||||||
obj.password = get_hash_password(obj.password, salt)
|
|
||||||
dict_obj = obj.model_dump()
|
|
||||||
dict_obj.update({'is_staff': True, 'salt': salt})
|
|
||||||
else:
|
|
||||||
dict_obj = obj.model_dump()
|
|
||||||
dict_obj.update({'is_staff': True, 'salt': None})
|
|
||||||
new_user = self.model(**dict_obj)
|
|
||||||
db.add(new_user)
|
|
||||||
|
|
||||||
async def add(self, db: AsyncSession, obj: AddUserParam) -> None:
|
async def add(self, db: AsyncSession, obj: AddUserParam) -> None:
|
||||||
"""
|
"""
|
||||||
添加用户
|
添加用户
|
||||||
@@ -97,14 +75,33 @@ class CRUDUser(CRUDPlus[User]):
|
|||||||
dict_obj.update({'salt': salt})
|
dict_obj.update({'salt': salt})
|
||||||
new_user = self.model(**dict_obj)
|
new_user = self.model(**dict_obj)
|
||||||
|
|
||||||
role_list = []
|
stmt = select(Role).where(Role.id.in_(obj.roles))
|
||||||
for role_id in obj.roles:
|
roles = await db.execute(stmt)
|
||||||
role_list.append(await db.get(Role, role_id))
|
new_user.roles = roles.scalars().all()
|
||||||
new_user.roles.extend(role_list)
|
|
||||||
|
|
||||||
db.add(new_user)
|
db.add(new_user)
|
||||||
|
|
||||||
async def update_userinfo(self, db: AsyncSession, input_user: int, obj: UpdateUserParam) -> int:
|
async def add_by_oauth2(self, db: AsyncSession, obj: AddOAuth2UserParam) -> None:
|
||||||
|
"""
|
||||||
|
通过 OAuth2 添加用户
|
||||||
|
|
||||||
|
:param db: 数据库会话
|
||||||
|
:param obj: 注册用户参数
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
salt = bcrypt.gensalt()
|
||||||
|
obj.password = get_hash_password(obj.password, salt)
|
||||||
|
dict_obj = obj.model_dump()
|
||||||
|
dict_obj.update({'is_staff': True, 'salt': salt})
|
||||||
|
new_user = self.model(**dict_obj)
|
||||||
|
|
||||||
|
stmt = select(Role)
|
||||||
|
role = await db.execute(stmt)
|
||||||
|
new_user.roles = [role.scalars().first()] # 默认绑定第一个角色
|
||||||
|
|
||||||
|
db.add(new_user)
|
||||||
|
|
||||||
|
async def update(self, db: AsyncSession, input_user: User, obj: UpdateUserParam) -> int:
|
||||||
"""
|
"""
|
||||||
更新用户信息
|
更新用户信息
|
||||||
|
|
||||||
@@ -113,36 +110,25 @@ class CRUDUser(CRUDPlus[User]):
|
|||||||
:param obj: 更新用户参数
|
:param obj: 更新用户参数
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
return await self.update_model(db, input_user, obj)
|
role_ids = obj.roles
|
||||||
|
del obj.roles
|
||||||
|
count = await self.update_model(db, input_user.id, obj)
|
||||||
|
|
||||||
@staticmethod
|
stmt = select(Role).where(Role.id.in_(role_ids))
|
||||||
async def update_role(db: AsyncSession, input_user: User, obj: UpdateUserRoleParam) -> None:
|
roles = await db.execute(stmt)
|
||||||
"""
|
input_user.roles = roles.scalars().all()
|
||||||
更新用户角色
|
return count
|
||||||
|
|
||||||
:param db: 数据库会话
|
async def update_avatar(self, db: AsyncSession, user_id: int, avatar: str) -> int:
|
||||||
:param input_user: 用户对象
|
|
||||||
:param obj: 更新角色参数
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
for i in list(input_user.roles):
|
|
||||||
input_user.roles.remove(i)
|
|
||||||
|
|
||||||
role_list = []
|
|
||||||
for role_id in obj.roles:
|
|
||||||
role_list.append(await db.get(Role, role_id))
|
|
||||||
input_user.roles.extend(role_list)
|
|
||||||
|
|
||||||
async def update_avatar(self, db: AsyncSession, input_user: int, avatar: AvatarParam) -> int:
|
|
||||||
"""
|
"""
|
||||||
更新用户头像
|
更新用户头像
|
||||||
|
|
||||||
:param db: 数据库会话
|
:param db: 数据库会话
|
||||||
:param input_user: 用户 ID
|
:param user_id: 用户 ID
|
||||||
:param avatar: 头像地址
|
:param avatar: 头像地址
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
return await self.update_model(db, input_user, {'avatar': str(avatar.url)})
|
return await self.update_model(db, user_id, {'avatar': avatar})
|
||||||
|
|
||||||
async def delete(self, db: AsyncSession, user_id: int) -> int:
|
async def delete(self, db: AsyncSession, user_id: int) -> int:
|
||||||
"""
|
"""
|
||||||
@@ -156,7 +142,7 @@ class CRUDUser(CRUDPlus[User]):
|
|||||||
|
|
||||||
async def check_email(self, db: AsyncSession, email: str) -> User | None:
|
async def check_email(self, db: AsyncSession, email: str) -> User | None:
|
||||||
"""
|
"""
|
||||||
检查邮箱是否已被注册
|
检查邮箱是否已被绑定
|
||||||
|
|
||||||
:param db: 数据库会话
|
:param db: 数据库会话
|
||||||
:param email: 电子邮箱
|
:param email: 电子邮箱
|
||||||
@@ -189,8 +175,7 @@ class CRUDUser(CRUDPlus[User]):
|
|||||||
select(self.model)
|
select(self.model)
|
||||||
.options(
|
.options(
|
||||||
selectinload(self.model.dept).options(noload(Dept.parent), noload(Dept.children), noload(Dept.users)),
|
selectinload(self.model.dept).options(noload(Dept.parent), noload(Dept.children), noload(Dept.users)),
|
||||||
noload(self.model.socials),
|
selectinload(self.model.roles).options(noload(Role.users), noload(Role.menus), noload(Role.scopes)),
|
||||||
selectinload(self.model.roles).options(noload(Role.users), noload(Role.menus), noload(Role.rules)),
|
|
||||||
)
|
)
|
||||||
.order_by(desc(self.model.join_time))
|
.order_by(desc(self.model.join_time))
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -8,4 +8,3 @@ from backend.app.admin.model.menu import Menu
|
|||||||
from backend.app.admin.model.opera_log import OperaLog
|
from backend.app.admin.model.opera_log import OperaLog
|
||||||
from backend.app.admin.model.role import Role
|
from backend.app.admin.model.role import Role
|
||||||
from backend.app.admin.model.user import User
|
from backend.app.admin.model.user import User
|
||||||
from backend.app.admin.model.user_social import UserSocial
|
|
||||||
|
|||||||
@@ -4,9 +4,10 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from sqlalchemy import ForeignKey, String
|
from sqlalchemy import String
|
||||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||||
|
|
||||||
|
from backend.app.admin.model.m2m import sys_data_scope_rule
|
||||||
from backend.common.model import Base, id_key
|
from backend.common.model import Base, id_key
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
@@ -28,8 +29,5 @@ class DataRule(Base):
|
|||||||
)
|
)
|
||||||
value: Mapped[str] = mapped_column(String(255), comment='规则值')
|
value: Mapped[str] = mapped_column(String(255), comment='规则值')
|
||||||
|
|
||||||
# 数据范围规则一对多
|
# 数据范围规则多对多
|
||||||
scope_id: Mapped[int | None] = mapped_column(
|
scopes: Mapped[list[DataScope]] = relationship(init=False, secondary=sys_data_scope_rule, back_populates='rules')
|
||||||
ForeignKey('sys_data_scope.id', ondelete='SET NULL'), default=None, comment='数据范围关联 ID'
|
|
||||||
)
|
|
||||||
scope: Mapped[DataScope] = relationship(init=False, back_populates='rules')
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ from typing import TYPE_CHECKING
|
|||||||
from sqlalchemy import String
|
from sqlalchemy import String
|
||||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||||
|
|
||||||
from backend.app.admin.model.m2m import sys_role_data_scope
|
from backend.app.admin.model.m2m import sys_data_scope_rule, sys_role_data_scope
|
||||||
from backend.common.model import Base, id_key
|
from backend.common.model import Base, id_key
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
@@ -23,8 +23,8 @@ class DataScope(Base):
|
|||||||
name: Mapped[str] = mapped_column(String(50), unique=True, comment='名称')
|
name: Mapped[str] = mapped_column(String(50), unique=True, comment='名称')
|
||||||
status: Mapped[int] = mapped_column(default=1, comment='状态(0停用 1正常)')
|
status: Mapped[int] = mapped_column(default=1, comment='状态(0停用 1正常)')
|
||||||
|
|
||||||
# 数据范围规则一对多
|
# 数据范围规则多对多
|
||||||
rules: Mapped[list[DataRule]] = relationship(init=False, back_populates='scope')
|
rules: Mapped[list[DataRule]] = relationship(init=False, secondary=sys_data_scope_rule, back_populates='scopes')
|
||||||
|
|
||||||
# 角色数据范围多对多
|
# 角色数据范围多对多
|
||||||
roles: Mapped[list[Role]] = relationship(init=False, secondary=sys_role_data_scope, back_populates='scopes')
|
roles: Mapped[list[Role]] = relationship(init=False, secondary=sys_role_data_scope, back_populates='scopes')
|
||||||
|
|||||||
@@ -33,3 +33,23 @@ sys_role_data_scope = Table(
|
|||||||
comment='数据范围 ID',
|
comment='数据范围 ID',
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
sys_data_scope_rule = Table(
|
||||||
|
'sys_data_scope_rule',
|
||||||
|
MappedBase.metadata,
|
||||||
|
Column('id', INT, primary_key=True, unique=True, index=True, autoincrement=True, comment='主键ID'),
|
||||||
|
Column(
|
||||||
|
'data_scope_id',
|
||||||
|
Integer,
|
||||||
|
ForeignKey('sys_data_scope.id', ondelete='CASCADE'),
|
||||||
|
primary_key=True,
|
||||||
|
comment='数据范围 ID',
|
||||||
|
),
|
||||||
|
Column(
|
||||||
|
'data_rule_id',
|
||||||
|
Integer,
|
||||||
|
ForeignKey('sys_data_rule.id', ondelete='CASCADE'),
|
||||||
|
primary_key=True,
|
||||||
|
comment='数据规则 ID',
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|||||||
@@ -24,10 +24,10 @@ class Menu(Base):
|
|||||||
id: Mapped[id_key] = mapped_column(init=False)
|
id: Mapped[id_key] = mapped_column(init=False)
|
||||||
title: Mapped[str] = mapped_column(String(50), comment='菜单标题')
|
title: Mapped[str] = mapped_column(String(50), comment='菜单标题')
|
||||||
name: Mapped[str] = mapped_column(String(50), comment='菜单名称')
|
name: Mapped[str] = mapped_column(String(50), comment='菜单名称')
|
||||||
path: Mapped[str] = mapped_column(String(200), comment='路由地址')
|
path: Mapped[str | None] = mapped_column(String(200), comment='路由地址')
|
||||||
sort: Mapped[int] = mapped_column(default=0, comment='排序')
|
sort: Mapped[int] = mapped_column(default=0, comment='排序')
|
||||||
icon: Mapped[str | None] = mapped_column(String(100), default=None, comment='菜单图标')
|
icon: Mapped[str | None] = mapped_column(String(100), default=None, comment='菜单图标')
|
||||||
type: Mapped[int] = mapped_column(default=0, comment='菜单类型(0目录 1菜单 2按钮)')
|
type: Mapped[int] = mapped_column(default=0, comment='菜单类型(0目录 1菜单 2按钮 3内嵌 4外链)')
|
||||||
component: Mapped[str | None] = mapped_column(String(255), default=None, comment='组件路径')
|
component: Mapped[str | None] = mapped_column(String(255), default=None, comment='组件路径')
|
||||||
perms: Mapped[str | None] = mapped_column(String(100), default=None, comment='权限标识')
|
perms: Mapped[str | None] = mapped_column(String(100), default=None, comment='权限标识')
|
||||||
status: Mapped[int] = mapped_column(default=1, comment='菜单状态(0停用 1正常)')
|
status: Mapped[int] = mapped_column(default=1, comment='菜单状态(0停用 1正常)')
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from sqlalchemy import String
|
from sqlalchemy import Boolean, String
|
||||||
from sqlalchemy.dialects.mysql import LONGTEXT
|
from sqlalchemy.dialects.mysql import LONGTEXT
|
||||||
from sqlalchemy.dialects.postgresql import TEXT
|
from sqlalchemy.dialects.postgresql import INTEGER, TEXT
|
||||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||||
|
|
||||||
from backend.app.admin.model.m2m import sys_role_data_scope, sys_role_menu, sys_user_role
|
from backend.app.admin.model.m2m import sys_role_data_scope, sys_role_menu, sys_user_role
|
||||||
@@ -24,6 +24,9 @@ class Role(Base):
|
|||||||
id: Mapped[id_key] = mapped_column(init=False)
|
id: Mapped[id_key] = mapped_column(init=False)
|
||||||
name: Mapped[str] = mapped_column(String(20), unique=True, comment='角色名称')
|
name: Mapped[str] = mapped_column(String(20), unique=True, comment='角色名称')
|
||||||
status: Mapped[int] = mapped_column(default=1, comment='角色状态(0停用 1正常)')
|
status: Mapped[int] = mapped_column(default=1, comment='角色状态(0停用 1正常)')
|
||||||
|
is_filter_scopes: Mapped[bool] = mapped_column(
|
||||||
|
Boolean().with_variant(INTEGER, 'postgresql'), default=True, comment='过滤数据权限(0否 1是)'
|
||||||
|
)
|
||||||
remark: Mapped[str | None] = mapped_column(
|
remark: Mapped[str | None] = mapped_column(
|
||||||
LONGTEXT().with_variant(TEXT, 'postgresql'), default=None, comment='备注'
|
LONGTEXT().with_variant(TEXT, 'postgresql'), default=None, comment='备注'
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ from backend.database.db import uuid4_str
|
|||||||
from backend.utils.timezone import timezone
|
from backend.utils.timezone import timezone
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from backend.app.admin.model import Dept, Role, UserSocial
|
from backend.app.admin.model import Dept, Role
|
||||||
|
|
||||||
|
|
||||||
class User(Base):
|
class User(Base):
|
||||||
@@ -26,22 +26,22 @@ class User(Base):
|
|||||||
id: Mapped[id_key] = mapped_column(init=False)
|
id: Mapped[id_key] = mapped_column(init=False)
|
||||||
uuid: Mapped[str] = mapped_column(String(50), init=False, default_factory=uuid4_str, unique=True)
|
uuid: Mapped[str] = mapped_column(String(50), init=False, default_factory=uuid4_str, unique=True)
|
||||||
username: Mapped[str] = mapped_column(String(20), unique=True, index=True, comment='用户名')
|
username: Mapped[str] = mapped_column(String(20), unique=True, index=True, comment='用户名')
|
||||||
nickname: Mapped[str] = mapped_column(String(20), unique=True, comment='昵称')
|
nickname: Mapped[str] = mapped_column(String(20), comment='昵称')
|
||||||
password: Mapped[str | None] = mapped_column(String(255), comment='密码')
|
password: Mapped[str] = mapped_column(String(255), comment='密码')
|
||||||
salt: Mapped[bytes | None] = mapped_column(VARBINARY(255).with_variant(BYTEA(255), 'postgresql'), comment='加密盐')
|
salt: Mapped[bytes] = mapped_column(VARBINARY(255).with_variant(BYTEA(255), 'postgresql'), comment='加密盐')
|
||||||
email: Mapped[str] = mapped_column(String(50), unique=True, index=True, comment='邮箱')
|
email: Mapped[str | None] = mapped_column(String(50), default=None, unique=True, index=True, comment='邮箱')
|
||||||
|
phone: Mapped[str | None] = mapped_column(String(11), default=None, comment='手机号')
|
||||||
|
avatar: Mapped[str | None] = mapped_column(String(255), default=None, comment='头像')
|
||||||
|
status: Mapped[int] = mapped_column(default=1, index=True, comment='用户账号状态(0停用 1正常)')
|
||||||
is_superuser: Mapped[bool] = mapped_column(
|
is_superuser: Mapped[bool] = mapped_column(
|
||||||
Boolean().with_variant(INTEGER, 'postgresql'), default=False, comment='超级权限(0否 1是)'
|
Boolean().with_variant(INTEGER, 'postgresql'), default=False, comment='超级权限(0否 1是)'
|
||||||
)
|
)
|
||||||
is_staff: Mapped[bool] = mapped_column(
|
is_staff: Mapped[bool] = mapped_column(
|
||||||
Boolean().with_variant(INTEGER, 'postgresql'), default=False, comment='后台管理登陆(0否 1是)'
|
Boolean().with_variant(INTEGER, 'postgresql'), default=False, comment='后台管理登陆(0否 1是)'
|
||||||
)
|
)
|
||||||
status: Mapped[int] = mapped_column(default=1, index=True, comment='用户账号状态(0停用 1正常)')
|
|
||||||
is_multi_login: Mapped[bool] = mapped_column(
|
is_multi_login: Mapped[bool] = mapped_column(
|
||||||
Boolean().with_variant(INTEGER, 'postgresql'), default=False, comment='是否重复登陆(0否 1是)'
|
Boolean().with_variant(INTEGER, 'postgresql'), default=False, comment='是否重复登陆(0否 1是)'
|
||||||
)
|
)
|
||||||
avatar: Mapped[str | None] = mapped_column(String(255), default=None, comment='头像')
|
|
||||||
phone: Mapped[str | None] = mapped_column(String(11), default=None, comment='手机号')
|
|
||||||
join_time: Mapped[datetime] = mapped_column(
|
join_time: Mapped[datetime] = mapped_column(
|
||||||
DateTime(timezone=True), init=False, default_factory=timezone.now, comment='注册时间'
|
DateTime(timezone=True), init=False, default_factory=timezone.now, comment='注册时间'
|
||||||
)
|
)
|
||||||
@@ -55,8 +55,5 @@ class User(Base):
|
|||||||
)
|
)
|
||||||
dept: Mapped[Dept | None] = relationship(init=False, back_populates='users')
|
dept: Mapped[Dept | None] = relationship(init=False, back_populates='users')
|
||||||
|
|
||||||
# 用户社交信息一对多
|
|
||||||
socials: Mapped[list[UserSocial]] = relationship(init=False, back_populates='user')
|
|
||||||
|
|
||||||
# 用户角色多对多
|
# 用户角色多对多
|
||||||
roles: Mapped[list[Role]] = relationship(init=False, secondary=sys_user_role, back_populates='users')
|
roles: Mapped[list[Role]] = relationship(init=False, secondary=sys_user_role, back_populates='users')
|
||||||
|
|||||||
@@ -1,33 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from typing import TYPE_CHECKING
|
|
||||||
|
|
||||||
from sqlalchemy import ForeignKey, String
|
|
||||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
|
||||||
|
|
||||||
from backend.common.model import Base, id_key
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from backend.app.admin.model import User
|
|
||||||
|
|
||||||
|
|
||||||
class UserSocial(Base):
|
|
||||||
"""用户社交表(OAuth2)"""
|
|
||||||
|
|
||||||
__tablename__ = 'sys_user_social'
|
|
||||||
|
|
||||||
id: Mapped[id_key] = mapped_column(init=False)
|
|
||||||
source: Mapped[str] = mapped_column(String(20), comment='第三方用户来源')
|
|
||||||
open_id: Mapped[str | None] = mapped_column(String(20), default=None, comment='第三方用户的 open id')
|
|
||||||
uid: Mapped[str | None] = mapped_column(String(20), default=None, comment='第三方用户的 ID')
|
|
||||||
union_id: Mapped[str | None] = mapped_column(String(20), default=None, comment='第三方用户的 union id')
|
|
||||||
scope: Mapped[str | None] = mapped_column(String(120), default=None, comment='第三方用户授予的权限')
|
|
||||||
code: Mapped[str | None] = mapped_column(String(50), default=None, comment='用户的授权 code')
|
|
||||||
|
|
||||||
# 用户社交信息一对多
|
|
||||||
user_id: Mapped[int | None] = mapped_column(
|
|
||||||
ForeignKey('sys_user.id', ondelete='SET NULL'), default=None, comment='用户关联ID'
|
|
||||||
)
|
|
||||||
user: Mapped[User | None] = relationship(init=False, back_populates='socials')
|
|
||||||
@@ -13,11 +13,11 @@ class MenuSchemaBase(SchemaBase):
|
|||||||
|
|
||||||
title: str = Field(description='菜单标题')
|
title: str = Field(description='菜单标题')
|
||||||
name: str = Field(description='菜单名称')
|
name: str = Field(description='菜单名称')
|
||||||
path: str = Field(description='路由路径')
|
path: str | None = Field(None, description='路由地址')
|
||||||
parent_id: int | None = Field(None, description='菜单父级 ID')
|
parent_id: int | None = Field(None, description='菜单父级 ID')
|
||||||
sort: int = Field(0, ge=0, description='排序')
|
sort: int = Field(0, ge=0, description='排序')
|
||||||
icon: str | None = Field(None, description='图标')
|
icon: str | None = Field(None, description='图标')
|
||||||
type: MenuType = Field(MenuType.directory, description='菜单类型(0目录 1菜单 2按钮)')
|
type: MenuType = Field(MenuType.directory, description='菜单类型(0目录 1菜单 2按钮 3内嵌 4外链)')
|
||||||
component: str | None = Field(None, description='组件路径')
|
component: str | None = Field(None, description='组件路径')
|
||||||
perms: str | None = Field(None, description='权限标识')
|
perms: str | None = Field(None, description='权限标识')
|
||||||
status: StatusType = Field(StatusType.enable, description='状态')
|
status: StatusType = Field(StatusType.enable, description='状态')
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ class RoleSchemaBase(SchemaBase):
|
|||||||
|
|
||||||
name: str = Field(description='角色名称')
|
name: str = Field(description='角色名称')
|
||||||
status: StatusType = Field(StatusType.enable, description='状态')
|
status: StatusType = Field(StatusType.enable, description='状态')
|
||||||
|
is_filter_scopes: bool = Field(True, description='过滤数据权限')
|
||||||
remark: str | None = Field(None, description='备注')
|
remark: str | None = Field(None, description='备注')
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -35,12 +35,6 @@ class GetLoginToken(AccessTokenBase):
|
|||||||
user: GetUserInfoDetail = Field(description='用户信息')
|
user: GetUserInfoDetail = Field(description='用户信息')
|
||||||
|
|
||||||
|
|
||||||
class KickOutToken(SchemaBase):
|
|
||||||
"""踢出令牌"""
|
|
||||||
|
|
||||||
session_uuid: str = Field(description='会话 UUID')
|
|
||||||
|
|
||||||
|
|
||||||
class GetTokenDetail(SchemaBase):
|
class GetTokenDetail(SchemaBase):
|
||||||
"""令牌详情"""
|
"""令牌详情"""
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ from typing_extensions import Self
|
|||||||
from backend.app.admin.schema.dept import GetDeptDetail
|
from backend.app.admin.schema.dept import GetDeptDetail
|
||||||
from backend.app.admin.schema.role import GetRoleWithRelationDetail
|
from backend.app.admin.schema.role import GetRoleWithRelationDetail
|
||||||
from backend.common.enums import StatusType
|
from backend.common.enums import StatusType
|
||||||
from backend.common.schema import CustomPhoneNumber, SchemaBase
|
from backend.common.schema import CustomEmailStr, CustomPhoneNumber, SchemaBase
|
||||||
|
|
||||||
|
|
||||||
class AuthSchemaBase(SchemaBase):
|
class AuthSchemaBase(SchemaBase):
|
||||||
@@ -25,20 +25,20 @@ class AuthLoginParam(AuthSchemaBase):
|
|||||||
captcha: str = Field(description='验证码')
|
captcha: str = Field(description='验证码')
|
||||||
|
|
||||||
|
|
||||||
class RegisterUserParam(AuthSchemaBase):
|
|
||||||
"""用户注册参数"""
|
|
||||||
|
|
||||||
nickname: str | None = Field(None, description='昵称')
|
|
||||||
email: EmailStr = Field(examples=['user@example.com'], description='邮箱')
|
|
||||||
|
|
||||||
|
|
||||||
class AddUserParam(AuthSchemaBase):
|
class AddUserParam(AuthSchemaBase):
|
||||||
"""添加用户参数"""
|
"""添加用户参数"""
|
||||||
|
|
||||||
dept_id: int = Field(description='部门 ID')
|
dept_id: int = Field(description='部门 ID')
|
||||||
roles: list[int] = Field(description='角色 ID 列表')
|
roles: list[int] = Field(description='角色 ID 列表')
|
||||||
nickname: str | None = Field(None, description='昵称')
|
nickname: str | None = Field(None, description='昵称')
|
||||||
email: EmailStr = Field(examples=['user@example.com'], description='邮箱')
|
|
||||||
|
|
||||||
|
class AddOAuth2UserParam(AuthSchemaBase):
|
||||||
|
"""添加 OAuth2 用户参数"""
|
||||||
|
|
||||||
|
nickname: str | None = Field(None, description='昵称')
|
||||||
|
email: EmailStr = Field(description='邮箱')
|
||||||
|
avatar: HttpUrl | None = Field(None, description='头像地址')
|
||||||
|
|
||||||
|
|
||||||
class ResetPasswordParam(SchemaBase):
|
class ResetPasswordParam(SchemaBase):
|
||||||
@@ -55,26 +55,15 @@ class UserInfoSchemaBase(SchemaBase):
|
|||||||
dept_id: int | None = Field(None, description='部门 ID')
|
dept_id: int | None = Field(None, description='部门 ID')
|
||||||
username: str = Field(description='用户名')
|
username: str = Field(description='用户名')
|
||||||
nickname: str = Field(description='昵称')
|
nickname: str = Field(description='昵称')
|
||||||
email: EmailStr = Field(examples=['user@example.com'], description='邮箱')
|
avatar: HttpUrl | None = Field(None, description='头像地址')
|
||||||
phone: CustomPhoneNumber | None = Field(None, description='手机号')
|
|
||||||
|
|
||||||
|
|
||||||
class UpdateUserParam(UserInfoSchemaBase):
|
class UpdateUserParam(UserInfoSchemaBase):
|
||||||
"""更新用户参数"""
|
"""更新用户参数"""
|
||||||
|
|
||||||
|
|
||||||
class UpdateUserRoleParam(SchemaBase):
|
|
||||||
"""更新用户角色参数"""
|
|
||||||
|
|
||||||
roles: list[int] = Field(description='角色 ID 列表')
|
roles: list[int] = Field(description='角色 ID 列表')
|
||||||
|
|
||||||
|
|
||||||
class AvatarParam(SchemaBase):
|
|
||||||
"""更新头像参数"""
|
|
||||||
|
|
||||||
url: HttpUrl = Field(description='头像 http 地址')
|
|
||||||
|
|
||||||
|
|
||||||
class GetUserInfoDetail(UserInfoSchemaBase):
|
class GetUserInfoDetail(UserInfoSchemaBase):
|
||||||
"""用户信息详情"""
|
"""用户信息详情"""
|
||||||
|
|
||||||
@@ -83,7 +72,8 @@ class GetUserInfoDetail(UserInfoSchemaBase):
|
|||||||
dept_id: int | None = Field(None, description='部门 ID')
|
dept_id: int | None = Field(None, description='部门 ID')
|
||||||
id: int = Field(description='用户 ID')
|
id: int = Field(description='用户 ID')
|
||||||
uuid: str = Field(description='用户 UUID')
|
uuid: str = Field(description='用户 UUID')
|
||||||
avatar: str | None = Field(None, description='头像')
|
email: CustomEmailStr | None = Field(None, description='邮箱')
|
||||||
|
phone: CustomPhoneNumber | None = Field(None, description='手机号')
|
||||||
status: StatusType = Field(StatusType.enable, description='状态')
|
status: StatusType = Field(StatusType.enable, description='状态')
|
||||||
is_superuser: bool = Field(description='是否超级管理员')
|
is_superuser: bool = Field(description='是否超级管理员')
|
||||||
is_staff: bool = Field(description='是否管理员')
|
is_staff: bool = Field(description='是否管理员')
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ class AuthService:
|
|||||||
user = await self.user_verify(db, obj.username, obj.password)
|
user = await self.user_verify(db, obj.username, obj.password)
|
||||||
captcha_code = await redis_client.get(f'{settings.CAPTCHA_LOGIN_REDIS_PREFIX}:{request.state.ip}')
|
captcha_code = await redis_client.get(f'{settings.CAPTCHA_LOGIN_REDIS_PREFIX}:{request.state.ip}')
|
||||||
if not captcha_code:
|
if not captcha_code:
|
||||||
raise errors.AuthorizationError(msg='验证码失效,请重新获取')
|
raise errors.ForbiddenError(msg='验证码失效,请重新获取')
|
||||||
if captcha_code.lower() != obj.captcha.lower():
|
if captcha_code.lower() != obj.captcha.lower():
|
||||||
raise errors.CustomError(error=CustomErrorCode.CAPTCHA_ERROR)
|
raise errors.CustomError(error=CustomErrorCode.CAPTCHA_ERROR)
|
||||||
await redis_client.delete(f'{settings.CAPTCHA_LOGIN_REDIS_PREFIX}:{request.state.ip}')
|
await redis_client.delete(f'{settings.CAPTCHA_LOGIN_REDIS_PREFIX}:{request.state.ip}')
|
||||||
@@ -121,7 +121,7 @@ class AuthService:
|
|||||||
except errors.NotFoundError as e:
|
except errors.NotFoundError as e:
|
||||||
log.error('登陆错误: 用户名不存在')
|
log.error('登陆错误: 用户名不存在')
|
||||||
raise errors.NotFoundError(msg=e.msg)
|
raise errors.NotFoundError(msg=e.msg)
|
||||||
except (errors.AuthorizationError, errors.CustomError) as e:
|
except (errors.ForbiddenError, errors.CustomError) as e:
|
||||||
if not user:
|
if not user:
|
||||||
log.error('登陆错误: 用户密码有误')
|
log.error('登陆错误: 用户密码有误')
|
||||||
task = BackgroundTask(
|
task = BackgroundTask(
|
||||||
@@ -136,7 +136,7 @@ class AuthService:
|
|||||||
msg=e.msg,
|
msg=e.msg,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
raise errors.AuthorizationError(msg=e.msg, background=task)
|
raise errors.RequestError(msg=e.msg, background=task)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.error(f'登陆错误: {e}')
|
log.error(f'登陆错误: {e}')
|
||||||
raise e
|
raise e
|
||||||
@@ -211,11 +211,17 @@ class AuthService:
|
|||||||
:param response: FastAPI 响应对象
|
:param response: FastAPI 响应对象
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
token = get_token(request)
|
try:
|
||||||
token_payload = jwt_decode(token)
|
token = get_token(request)
|
||||||
user_id = token_payload.id
|
token_payload = jwt_decode(token)
|
||||||
refresh_token = request.cookies.get(settings.COOKIE_REFRESH_TOKEN_KEY)
|
user_id = token_payload.id
|
||||||
response.delete_cookie(settings.COOKIE_REFRESH_TOKEN_KEY)
|
refresh_token = request.cookies.get(settings.COOKIE_REFRESH_TOKEN_KEY)
|
||||||
|
except errors.TokenError:
|
||||||
|
return
|
||||||
|
finally:
|
||||||
|
response.delete_cookie(settings.COOKIE_REFRESH_TOKEN_KEY)
|
||||||
|
|
||||||
|
# 清理缓存
|
||||||
if request.user.is_multi_login:
|
if request.user.is_multi_login:
|
||||||
await redis_client.delete(f'{settings.TOKEN_REDIS_PREFIX}:{user_id}:{token_payload.session_uuid}')
|
await redis_client.delete(f'{settings.TOKEN_REDIS_PREFIX}:{user_id}:{token_payload.session_uuid}')
|
||||||
if refresh_token:
|
if refresh_token:
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
from typing import Sequence
|
||||||
|
|
||||||
from sqlalchemy import Select
|
from sqlalchemy import Select
|
||||||
|
|
||||||
from backend.app.admin.crud.crud_data_scope import data_scope_dao
|
from backend.app.admin.crud.crud_data_scope import data_scope_dao
|
||||||
@@ -28,6 +30,13 @@ class DataScopeService:
|
|||||||
raise errors.NotFoundError(msg='数据范围不存在')
|
raise errors.NotFoundError(msg='数据范围不存在')
|
||||||
return data_scope
|
return data_scope
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def get_all() -> Sequence[DataScope]:
|
||||||
|
"""获取所有数据范围"""
|
||||||
|
async with async_db_session() as db:
|
||||||
|
data_scopes = await data_scope_dao.get_all(db)
|
||||||
|
return data_scopes
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def get_rules(*, pk: int) -> DataScope:
|
async def get_rules(*, pk: int) -> DataScope:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -41,27 +41,34 @@ class MenuService:
|
|||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
async with async_db_session() as db:
|
async with async_db_session() as db:
|
||||||
menu_select = await menu_dao.get_all(db, title=title, status=status)
|
menu_data = await menu_dao.get_all(db, title=title, status=status)
|
||||||
menu_tree = get_tree_data(menu_select)
|
menu_tree = get_tree_data(menu_data)
|
||||||
return menu_tree
|
return menu_tree
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def get_user_menu_tree(*, request: Request) -> list[dict[str, Any]]:
|
async def get_sidebar(*, request: Request) -> list[dict[str, Any] | None]:
|
||||||
"""
|
"""
|
||||||
获取用户的菜单树形结构
|
获取用户的菜单侧边栏
|
||||||
|
|
||||||
:param request: FastAPI 请求对象
|
:param request: FastAPI 请求对象
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
async with async_db_session() as db:
|
async with async_db_session() as db:
|
||||||
roles = request.user.roles
|
roles = request.user.roles
|
||||||
menu_ids = []
|
|
||||||
menu_tree = []
|
menu_tree = []
|
||||||
if roles:
|
if roles:
|
||||||
|
unique_menus = {}
|
||||||
for role in roles:
|
for role in roles:
|
||||||
menu_ids.extend([menu.id for menu in role.menus])
|
for menu in role.menus:
|
||||||
menu_select = await menu_dao.get_role_menus(db, request.user.is_superuser, menu_ids)
|
unique_menus[menu.id] = menu
|
||||||
menu_tree = get_vben5_tree_data(menu_select)
|
all_ids = set(unique_menus.keys())
|
||||||
|
valid_menu_ids = [
|
||||||
|
menu_id
|
||||||
|
for menu_id, menu in unique_menus.items()
|
||||||
|
if menu.parent_id is None or menu.parent_id in all_ids
|
||||||
|
]
|
||||||
|
menu_data = await menu_dao.get_sidebar(db, request.user.is_superuser, valid_menu_ids)
|
||||||
|
menu_tree = get_vben5_tree_data(menu_data)
|
||||||
return menu_tree
|
return menu_tree
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
|||||||
@@ -39,6 +39,11 @@ class PluginService:
|
|||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def changed() -> str | None:
|
||||||
|
"""插件状态是否变更"""
|
||||||
|
return await redis_client.get(f'{settings.PLUGIN_REDIS_PREFIX}:changed')
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def install_zip(*, file: UploadFile) -> None:
|
async def install_zip(*, file: UploadFile) -> None:
|
||||||
"""
|
"""
|
||||||
@@ -53,21 +58,14 @@ class PluginService:
|
|||||||
raise errors.ForbiddenError(msg='插件压缩包格式非法')
|
raise errors.ForbiddenError(msg='插件压缩包格式非法')
|
||||||
with zipfile.ZipFile(file_bytes) as zf:
|
with zipfile.ZipFile(file_bytes) as zf:
|
||||||
# 校验压缩包
|
# 校验压缩包
|
||||||
plugin_dir = file.filename[:-4]
|
plugin_namelist = zf.namelist()
|
||||||
members_in_plugin_dir = [name for name in zf.namelist() if name.startswith(plugin_dir)]
|
plugin_name = plugin_namelist[0].split('/')[0]
|
||||||
if not members_in_plugin_dir:
|
if not plugin_namelist or plugin_name not in file.filename:
|
||||||
raise errors.ForbiddenError(msg='插件压缩包内容非法')
|
raise errors.ForbiddenError(msg='插件压缩包内容非法')
|
||||||
plugin_name = (
|
|
||||||
members_in_plugin_dir[0]
|
|
||||||
.replace('/', '')
|
|
||||||
.replace('-master', '')
|
|
||||||
.replace('-main', '')
|
|
||||||
.replace('-dev', '')
|
|
||||||
)
|
|
||||||
if (
|
if (
|
||||||
len(members_in_plugin_dir) <= 3
|
len(plugin_namelist) <= 3
|
||||||
or f'{plugin_dir}/plugin.toml' not in members_in_plugin_dir
|
or f'{plugin_name}/plugin.toml' not in plugin_namelist
|
||||||
or f'{plugin_dir}/README.md' not in members_in_plugin_dir
|
or f'{plugin_name}/README.md' not in plugin_namelist
|
||||||
):
|
):
|
||||||
raise errors.ForbiddenError(msg='插件压缩包内缺少必要文件')
|
raise errors.ForbiddenError(msg='插件压缩包内缺少必要文件')
|
||||||
|
|
||||||
@@ -81,14 +79,15 @@ class PluginService:
|
|||||||
# 解压(安装)
|
# 解压(安装)
|
||||||
members = []
|
members = []
|
||||||
for member in zf.infolist():
|
for member in zf.infolist():
|
||||||
if member.filename.startswith(plugin_dir):
|
if member.filename.startswith(plugin_name):
|
||||||
new_filename = member.filename.replace(plugin_dir, '')
|
new_filename = member.filename.replace(plugin_name, '')
|
||||||
if new_filename:
|
if new_filename:
|
||||||
member.filename = new_filename
|
member.filename = new_filename
|
||||||
members.append(member)
|
members.append(member)
|
||||||
zf.extractall(os.path.join(PLUGIN_DIR, plugin_name), members)
|
zf.extractall(os.path.join(PLUGIN_DIR, plugin_name), members)
|
||||||
|
|
||||||
await install_requirements_async(plugin_name)
|
await install_requirements_async(plugin_name)
|
||||||
|
await redis_client.set(f'{settings.PLUGIN_REDIS_PREFIX}:changed', 'ture')
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def install_git(*, repo_url: str):
|
async def install_git(*, repo_url: str):
|
||||||
@@ -112,6 +111,7 @@ class PluginService:
|
|||||||
raise errors.ServerError(msg='插件安装失败,请稍后重试') from e
|
raise errors.ServerError(msg='插件安装失败,请稍后重试') from e
|
||||||
else:
|
else:
|
||||||
await install_requirements_async(repo_name)
|
await install_requirements_async(repo_name)
|
||||||
|
await redis_client.set(f'{settings.PLUGIN_REDIS_PREFIX}:changed', 'ture')
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def uninstall(*, plugin: str):
|
async def uninstall(*, plugin: str):
|
||||||
@@ -127,6 +127,9 @@ class PluginService:
|
|||||||
await uninstall_requirements_async(plugin)
|
await uninstall_requirements_async(plugin)
|
||||||
bacup_dir = os.path.join(PLUGIN_DIR, f'{plugin}.{timezone.now().strftime("%Y%m%d%H%M%S")}.backup')
|
bacup_dir = os.path.join(PLUGIN_DIR, f'{plugin}.{timezone.now().strftime("%Y%m%d%H%M%S")}.backup')
|
||||||
shutil.move(plugin_dir, bacup_dir)
|
shutil.move(plugin_dir, bacup_dir)
|
||||||
|
await redis_client.delete(f'{settings.PLUGIN_REDIS_PREFIX}:info:{plugin}')
|
||||||
|
await redis_client.hdel(f'{settings.PLUGIN_REDIS_PREFIX}:status', plugin)
|
||||||
|
await redis_client.set(f'{settings.PLUGIN_REDIS_PREFIX}:changed', 'ture')
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def update_status(*, plugin: str):
|
async def update_status(*, plugin: str):
|
||||||
@@ -140,16 +143,18 @@ class PluginService:
|
|||||||
if not plugin_info:
|
if not plugin_info:
|
||||||
raise errors.ForbiddenError(msg='插件不存在')
|
raise errors.ForbiddenError(msg='插件不存在')
|
||||||
plugin_info = json.loads(plugin_info)
|
plugin_info = json.loads(plugin_info)
|
||||||
|
|
||||||
|
# 更新持久缓存状态
|
||||||
new_status = (
|
new_status = (
|
||||||
StatusType.enable.value
|
str(StatusType.enable.value)
|
||||||
if plugin_info.get('plugin', {}).get('enable') == StatusType.disable.value
|
if plugin_info['plugin']['enable'] == str(StatusType.disable.value)
|
||||||
else StatusType.disable.value
|
else str(StatusType.disable.value)
|
||||||
)
|
)
|
||||||
plugin_info['plugin']['enable'] = new_status
|
plugin_info['plugin']['enable'] = new_status
|
||||||
await redis_client.set(
|
await redis_client.set(
|
||||||
f'{settings.PLUGIN_REDIS_PREFIX}:info:{plugin}', json.dumps(plugin_info, ensure_ascii=False)
|
f'{settings.PLUGIN_REDIS_PREFIX}:info:{plugin}', json.dumps(plugin_info, ensure_ascii=False)
|
||||||
)
|
)
|
||||||
await redis_client.hset(f'{settings.PLUGIN_REDIS_PREFIX}:status', plugin, str(new_status))
|
await redis_client.hset(f'{settings.PLUGIN_REDIS_PREFIX}:status', plugin, new_status)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def build(*, plugin: str) -> io.BytesIO:
|
async def build(*, plugin: str) -> io.BytesIO:
|
||||||
@@ -170,8 +175,9 @@ class PluginService:
|
|||||||
for file in files:
|
for file in files:
|
||||||
file_path = os.path.join(root, file)
|
file_path = os.path.join(root, file)
|
||||||
arcname = os.path.relpath(file_path, start=plugin_dir)
|
arcname = os.path.relpath(file_path, start=plugin_dir)
|
||||||
zf.write(file_path, arcname)
|
zf.write(file_path, os.path.join(plugin, arcname))
|
||||||
|
|
||||||
|
bio.seek(0)
|
||||||
return bio
|
return bio
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -45,18 +45,6 @@ class RoleService:
|
|||||||
roles = await role_dao.get_all(db)
|
roles = await role_dao.get_all(db)
|
||||||
return roles
|
return roles
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
async def get_users(*, pk: int) -> Sequence[Role]:
|
|
||||||
"""
|
|
||||||
获取用户的角色列表
|
|
||||||
|
|
||||||
:param pk: 用户 ID
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
async with async_db_session() as db:
|
|
||||||
roles = await role_dao.get_users(db, user_id=pk)
|
|
||||||
return roles
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def get_select(*, name: str | None, status: int | None) -> Select:
|
async def get_select(*, name: str | None, status: int | None) -> Select:
|
||||||
"""
|
"""
|
||||||
@@ -69,7 +57,7 @@ class RoleService:
|
|||||||
return await role_dao.get_list(name=name, status=status)
|
return await role_dao.get_list(name=name, status=status)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def get_menu_tree(*, pk: int) -> list[dict[str, Any]]:
|
async def get_menu_tree(*, pk: int) -> list[dict[str, Any] | None]:
|
||||||
"""
|
"""
|
||||||
获取角色的菜单树形结构
|
获取角色的菜单树形结构
|
||||||
|
|
||||||
@@ -80,9 +68,7 @@ class RoleService:
|
|||||||
role = await role_dao.get_with_relation(db, pk)
|
role = await role_dao.get_with_relation(db, pk)
|
||||||
if not role:
|
if not role:
|
||||||
raise errors.NotFoundError(msg='角色不存在')
|
raise errors.NotFoundError(msg='角色不存在')
|
||||||
menu_ids = [menu.id for menu in role.menus]
|
menu_tree = get_tree_data(role.menus) if role.menus else []
|
||||||
menu_select = await menu_dao.get_role_menus(db, False, menu_ids)
|
|
||||||
menu_tree = get_tree_data(menu_select)
|
|
||||||
return menu_tree
|
return menu_tree
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@@ -128,8 +114,7 @@ class RoleService:
|
|||||||
if not role:
|
if not role:
|
||||||
raise errors.NotFoundError(msg='角色不存在')
|
raise errors.NotFoundError(msg='角色不存在')
|
||||||
if role.name != obj.name:
|
if role.name != obj.name:
|
||||||
role = await role_dao.get_by_name(db, obj.name)
|
if await role_dao.get_by_name(db, obj.name):
|
||||||
if role:
|
|
||||||
raise errors.ForbiddenError(msg='角色已存在')
|
raise errors.ForbiddenError(msg='角色已存在')
|
||||||
count = await role_dao.update(db, pk, obj)
|
count = await role_dao.update(db, pk, obj)
|
||||||
for user in await role.awaitable_attrs.users:
|
for user in await role.awaitable_attrs.users:
|
||||||
@@ -146,7 +131,7 @@ class RoleService:
|
|||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
async with async_db_session.begin() as db:
|
async with async_db_session.begin() as db:
|
||||||
role = await role_dao.get_with_relation(db, pk)
|
role = await role_dao.get(db, pk)
|
||||||
if not role:
|
if not role:
|
||||||
raise errors.NotFoundError(msg='角色不存在')
|
raise errors.NotFoundError(msg='角色不存在')
|
||||||
for menu_id in menu_ids.menus:
|
for menu_id in menu_ids.menus:
|
||||||
|
|||||||
@@ -2,20 +2,19 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
import random
|
import random
|
||||||
|
|
||||||
|
from typing import Sequence
|
||||||
|
|
||||||
from fastapi import Request
|
from fastapi import Request
|
||||||
from sqlalchemy import Select
|
from sqlalchemy import Select
|
||||||
|
|
||||||
from backend.app.admin.crud.crud_dept import dept_dao
|
from backend.app.admin.crud.crud_dept import dept_dao
|
||||||
from backend.app.admin.crud.crud_role import role_dao
|
from backend.app.admin.crud.crud_role import role_dao
|
||||||
from backend.app.admin.crud.crud_user import user_dao
|
from backend.app.admin.crud.crud_user import user_dao
|
||||||
from backend.app.admin.model import User
|
from backend.app.admin.model import Role, User
|
||||||
from backend.app.admin.schema.user import (
|
from backend.app.admin.schema.user import (
|
||||||
AddUserParam,
|
AddUserParam,
|
||||||
AvatarParam,
|
|
||||||
RegisterUserParam,
|
|
||||||
ResetPasswordParam,
|
ResetPasswordParam,
|
||||||
UpdateUserParam,
|
UpdateUserParam,
|
||||||
UpdateUserRoleParam,
|
|
||||||
)
|
)
|
||||||
from backend.common.exception import errors
|
from backend.common.exception import errors
|
||||||
from backend.common.security.jwt import get_hash_password, get_token, jwt_decode, password_verify, superuser_verify
|
from backend.common.security.jwt import get_hash_password, get_token, jwt_decode, password_verify, superuser_verify
|
||||||
@@ -27,29 +26,6 @@ from backend.database.redis import redis_client
|
|||||||
class UserService:
|
class UserService:
|
||||||
"""用户服务类"""
|
"""用户服务类"""
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
async def register(*, obj: RegisterUserParam) -> None:
|
|
||||||
"""
|
|
||||||
注册新用户
|
|
||||||
|
|
||||||
:param obj: 用户注册参数
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
async with async_db_session.begin() as db:
|
|
||||||
if not obj.password:
|
|
||||||
raise errors.ForbiddenError(msg='密码为空')
|
|
||||||
username = await user_dao.get_by_username(db, obj.username)
|
|
||||||
if username:
|
|
||||||
raise errors.ForbiddenError(msg='用户已注册')
|
|
||||||
obj.nickname = obj.nickname if obj.nickname else f'#{random.randrange(10000, 88888)}'
|
|
||||||
nickname = await user_dao.get_by_nickname(db, obj.nickname)
|
|
||||||
if nickname:
|
|
||||||
raise errors.ForbiddenError(msg='昵称已注册')
|
|
||||||
email = await user_dao.check_email(db, obj.email)
|
|
||||||
if email:
|
|
||||||
raise errors.ForbiddenError(msg='邮箱已注册')
|
|
||||||
await user_dao.create(db, obj)
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def add(*, request: Request, obj: AddUserParam) -> None:
|
async def add(*, request: Request, obj: AddUserParam) -> None:
|
||||||
"""
|
"""
|
||||||
@@ -70,9 +46,6 @@ class UserService:
|
|||||||
raise errors.ForbiddenError(msg='昵称已注册')
|
raise errors.ForbiddenError(msg='昵称已注册')
|
||||||
if not obj.password:
|
if not obj.password:
|
||||||
raise errors.ForbiddenError(msg='密码为空')
|
raise errors.ForbiddenError(msg='密码为空')
|
||||||
email = await user_dao.check_email(db, obj.email)
|
|
||||||
if email:
|
|
||||||
raise errors.ForbiddenError(msg='邮箱已注册')
|
|
||||||
dept = await dept_dao.get(db, obj.dept_id)
|
dept = await dept_dao.get(db, obj.dept_id)
|
||||||
if not dept:
|
if not dept:
|
||||||
raise errors.NotFoundError(msg='部门不存在')
|
raise errors.NotFoundError(msg='部门不存在')
|
||||||
@@ -83,16 +56,16 @@ class UserService:
|
|||||||
await user_dao.add(db, obj)
|
await user_dao.add(db, obj)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def pwd_reset(*, request: Request, obj: ResetPasswordParam) -> int:
|
async def pwd_reset(*, username: str, obj: ResetPasswordParam) -> int:
|
||||||
"""
|
"""
|
||||||
重置用户密码
|
重置用户密码
|
||||||
|
|
||||||
:param request: FastAPI 请求对象
|
:param username: 用户名
|
||||||
:param obj: 密码重置参数
|
:param obj: 密码重置参数
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
async with async_db_session.begin() as db:
|
async with async_db_session.begin() as db:
|
||||||
user = await user_dao.get(db, request.user.id)
|
user = await user_dao.get_by_username(db, username)
|
||||||
if not user:
|
if not user:
|
||||||
raise errors.NotFoundError(msg='用户不存在')
|
raise errors.NotFoundError(msg='用户不存在')
|
||||||
if not password_verify(obj.old_password, user.password):
|
if not password_verify(obj.old_password, user.password):
|
||||||
@@ -100,11 +73,11 @@ class UserService:
|
|||||||
if obj.new_password != obj.confirm_password:
|
if obj.new_password != obj.confirm_password:
|
||||||
raise errors.ForbiddenError(msg='密码输入不一致')
|
raise errors.ForbiddenError(msg='密码输入不一致')
|
||||||
new_pwd = get_hash_password(obj.new_password, user.salt)
|
new_pwd = get_hash_password(obj.new_password, user.salt)
|
||||||
count = await user_dao.reset_password(db, request.user.id, new_pwd)
|
count = await user_dao.reset_password(db, user.id, new_pwd)
|
||||||
key_prefix = [
|
key_prefix = [
|
||||||
f'{settings.TOKEN_REDIS_PREFIX}:{request.user.id}',
|
f'{settings.TOKEN_REDIS_PREFIX}:{user.id}',
|
||||||
f'{settings.TOKEN_REFRESH_REDIS_PREFIX}:{request.user.id}',
|
f'{settings.TOKEN_REFRESH_REDIS_PREFIX}:{user.id}',
|
||||||
f'{settings.JWT_USER_REDIS_PREFIX}:{request.user.id}',
|
f'{settings.JWT_USER_REDIS_PREFIX}:{user.id}',
|
||||||
]
|
]
|
||||||
for prefix in key_prefix:
|
for prefix in key_prefix:
|
||||||
await redis_client.delete_prefix(prefix)
|
await redis_client.delete_prefix(prefix)
|
||||||
@@ -124,6 +97,33 @@ class UserService:
|
|||||||
raise errors.NotFoundError(msg='用户不存在')
|
raise errors.NotFoundError(msg='用户不存在')
|
||||||
return user
|
return user
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def get_roles(*, username: str) -> Sequence[Role]:
|
||||||
|
"""
|
||||||
|
获取用户所有角色
|
||||||
|
|
||||||
|
:param username: 用户名
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
async with async_db_session() as db:
|
||||||
|
user = await user_dao.get_with_relation(db, username=username)
|
||||||
|
if not user:
|
||||||
|
raise errors.NotFoundError(msg='用户不存在')
|
||||||
|
return user.roles
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def get_select(*, dept: int, username: str, phone: str, status: int) -> Select:
|
||||||
|
"""
|
||||||
|
获取用户列表查询条件
|
||||||
|
|
||||||
|
:param dept: 部门 ID
|
||||||
|
:param username: 用户名
|
||||||
|
:param phone: 手机号
|
||||||
|
:param status: 状态
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
return await user_dao.get_list(dept=dept, username=username, phone=phone, status=status)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def update(*, request: Request, username: str, obj: UpdateUserParam) -> int:
|
async def update(*, request: Request, username: str, obj: UpdateUserParam) -> int:
|
||||||
"""
|
"""
|
||||||
@@ -148,70 +148,14 @@ class UserService:
|
|||||||
nickname = await user_dao.get_by_nickname(db, obj.nickname)
|
nickname = await user_dao.get_by_nickname(db, obj.nickname)
|
||||||
if nickname:
|
if nickname:
|
||||||
raise errors.ForbiddenError(msg='昵称已注册')
|
raise errors.ForbiddenError(msg='昵称已注册')
|
||||||
if user.email != obj.email:
|
|
||||||
email = await user_dao.check_email(db, obj.email)
|
|
||||||
if email:
|
|
||||||
raise errors.ForbiddenError(msg='邮箱已注册')
|
|
||||||
count = await user_dao.update_userinfo(db, user.id, obj)
|
|
||||||
await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{user.id}')
|
|
||||||
return count
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
async def update_roles(*, request: Request, username: str, obj: UpdateUserRoleParam) -> None:
|
|
||||||
"""
|
|
||||||
更新用户角色
|
|
||||||
|
|
||||||
:param request: FastAPI 请求对象
|
|
||||||
:param username: 用户名
|
|
||||||
:param obj: 角色更新参数
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
async with async_db_session.begin() as db:
|
|
||||||
if not request.user.is_superuser and request.user.username != username:
|
|
||||||
raise errors.ForbiddenError(msg='你只能修改自己的信息')
|
|
||||||
input_user = await user_dao.get_with_relation(db, username=username)
|
|
||||||
if not input_user:
|
|
||||||
raise errors.NotFoundError(msg='用户不存在')
|
|
||||||
for role_id in obj.roles:
|
for role_id in obj.roles:
|
||||||
role = await role_dao.get(db, role_id)
|
role = await role_dao.get(db, role_id)
|
||||||
if not role:
|
if not role:
|
||||||
raise errors.NotFoundError(msg='角色不存在')
|
raise errors.NotFoundError(msg='角色不存在')
|
||||||
await user_dao.update_role(db, input_user, obj)
|
count = await user_dao.update(db, user, obj)
|
||||||
await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{input_user.id}')
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
async def update_avatar(*, request: Request, username: str, avatar: AvatarParam) -> int:
|
|
||||||
"""
|
|
||||||
更新用户头像
|
|
||||||
|
|
||||||
:param request: FastAPI 请求对象
|
|
||||||
:param username: 用户名
|
|
||||||
:param avatar: 头像参数
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
async with async_db_session.begin() as db:
|
|
||||||
if request.user.username != username:
|
|
||||||
raise errors.AuthorizationError(msg='你只能修改自己的信息')
|
|
||||||
user = await user_dao.get_by_username(db, username)
|
|
||||||
if not user:
|
|
||||||
raise errors.NotFoundError(msg='用户不存在')
|
|
||||||
count = await user_dao.update_avatar(db, user.id, avatar)
|
|
||||||
await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{user.id}')
|
await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{user.id}')
|
||||||
return count
|
return count
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
async def get_select(*, dept: int, username: str, phone: str, status: int) -> Select:
|
|
||||||
"""
|
|
||||||
获取用户列表查询条件
|
|
||||||
|
|
||||||
:param dept: 部门 ID
|
|
||||||
:param username: 用户名
|
|
||||||
:param phone: 手机号
|
|
||||||
:param status: 状态
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
return await user_dao.get_list(dept=dept, username=username, phone=phone, status=status)
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def update_permission(*, request: Request, pk: int) -> int:
|
async def update_permission(*, request: Request, pk: int) -> int:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -19,11 +19,11 @@ class TaskResult(SchemaBase):
|
|||||||
"""任务执行结果"""
|
"""任务执行结果"""
|
||||||
|
|
||||||
result: str = Field(description='任务执行结果')
|
result: str = Field(description='任务执行结果')
|
||||||
traceback: str = Field(description='错误堆栈信息')
|
traceback: str | None = Field(None, description='错误堆栈信息')
|
||||||
status: str = Field(description='任务状态')
|
status: str = Field(description='任务状态')
|
||||||
name: str = Field(description='任务名称')
|
name: str | None = Field(None, description='任务名称')
|
||||||
args: list[Any] | None = Field(None, description='任务函数位置参数')
|
args: list[Any] | None = Field(None, description='任务函数位置参数')
|
||||||
kwargs: dict[str, Any] | None = Field(None, description='任务函数关键字参数')
|
kwargs: dict[str, Any] | None = Field(None, description='任务函数关键字参数')
|
||||||
worker: str = Field(description='执行任务的 worker')
|
worker: str | None = Field(None, description='执行任务的 worker')
|
||||||
retries: int | None = Field(None, description='重试次数')
|
retries: int | None = Field(None, description='重试次数')
|
||||||
queue: str | None = Field(None, description='任务队列')
|
queue: str | None = Field(None, description='任务队列')
|
||||||
|
|||||||
@@ -44,6 +44,8 @@ class MenuType(IntEnum):
|
|||||||
directory = 0
|
directory = 0
|
||||||
menu = 1
|
menu = 1
|
||||||
button = 2
|
button = 2
|
||||||
|
embedded = 3
|
||||||
|
link = 4
|
||||||
|
|
||||||
|
|
||||||
class RoleDataRuleOperatorType(IntEnum):
|
class RoleDataRuleOperatorType(IntEnum):
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ class GatewayError(BaseExceptionMixin):
|
|||||||
class AuthorizationError(BaseExceptionMixin):
|
class AuthorizationError(BaseExceptionMixin):
|
||||||
"""授权异常"""
|
"""授权异常"""
|
||||||
|
|
||||||
code = StandardResponseCode.HTTP_401
|
code = StandardResponseCode.HTTP_403
|
||||||
|
|
||||||
def __init__(self, *, msg: str = 'Permission Denied', data: Any = None, background: BackgroundTask | None = None):
|
def __init__(self, *, msg: str = 'Permission Denied', data: Any = None, background: BackgroundTask | None = None):
|
||||||
super().__init__(msg=msg, data=data, background=background)
|
super().__init__(msg=msg, data=data, background=background)
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
from typing import TYPE_CHECKING
|
|
||||||
|
|
||||||
from fastapi import Request
|
from fastapi import Request
|
||||||
from sqlalchemy import ColumnElement, and_, or_
|
from sqlalchemy import ColumnElement, and_, or_
|
||||||
@@ -13,9 +12,6 @@ from backend.common.exception.errors import ServerError
|
|||||||
from backend.core.conf import settings
|
from backend.core.conf import settings
|
||||||
from backend.utils.import_parse import dynamic_import_data_model
|
from backend.utils.import_parse import dynamic_import_data_model
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from backend.app.admin.model import DataRule
|
|
||||||
|
|
||||||
|
|
||||||
class RequestPermission:
|
class RequestPermission:
|
||||||
"""
|
"""
|
||||||
@@ -60,35 +56,42 @@ async def filter_data_permission(db: AsyncSession, request: Request) -> ColumnEl
|
|||||||
:param request: FastAPI 请求对象
|
:param request: FastAPI 请求对象
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
# 获取用户角色和数据范围
|
# 是否过滤数据权限
|
||||||
data_scopes = []
|
if request.user.is_superuser:
|
||||||
|
return or_(1 == 1)
|
||||||
|
|
||||||
|
for role in request.user.roles:
|
||||||
|
if not role.is_filter_scopes:
|
||||||
|
return or_(1 == 1)
|
||||||
|
|
||||||
|
# 获取数据范围
|
||||||
|
unique_data_scopes = {}
|
||||||
for role in request.user.roles:
|
for role in request.user.roles:
|
||||||
for scope in role.scopes:
|
for scope in role.scopes:
|
||||||
if scope.status:
|
if scope.status:
|
||||||
data_scopes.append(scope)
|
unique_data_scopes[scope.id] = scope
|
||||||
|
|
||||||
# 超级管理员和无规则用户不做过滤
|
# 转换为列表
|
||||||
if request.user.is_superuser or not data_scopes:
|
data_scopes = list(unique_data_scopes.values())
|
||||||
|
|
||||||
|
# 无规则用户不做过滤
|
||||||
|
if not data_scopes:
|
||||||
return or_(1 == 1)
|
return or_(1 == 1)
|
||||||
|
|
||||||
# 获取数据范围规则
|
# 获取数据范围规则
|
||||||
data_rule_list: list[DataRule] = []
|
unique_data_rules = {}
|
||||||
for data_scope in data_scopes:
|
for data_scope in data_scopes:
|
||||||
data_scope_with_relation = await data_scope_dao.get_with_relation(db, data_scope.id)
|
data_scope_with_relation = await data_scope_dao.get_with_relation(db, data_scope.id)
|
||||||
data_rule_list.extend(data_scope_with_relation.rules)
|
for rule in data_scope_with_relation.rules:
|
||||||
|
unique_data_rules[rule.id] = rule
|
||||||
|
|
||||||
# 去重
|
# 转换为列表
|
||||||
seen_data_rule_ids = set()
|
data_rule_list = list(unique_data_rules.values())
|
||||||
new_data_rule_list = []
|
|
||||||
for rule in data_rule_list:
|
|
||||||
if rule.id not in seen_data_rule_ids:
|
|
||||||
seen_data_rule_ids.add(rule.id)
|
|
||||||
new_data_rule_list.append(rule)
|
|
||||||
|
|
||||||
where_and_list = []
|
where_and_list = []
|
||||||
where_or_list = []
|
where_or_list = []
|
||||||
|
|
||||||
for data_rule in new_data_rule_list:
|
for data_rule in data_rule_list:
|
||||||
# 验证规则模型
|
# 验证规则模型
|
||||||
rule_model = data_rule.model
|
rule_model = data_rule.model
|
||||||
if rule_model not in settings.DATA_PERMISSION_MODELS:
|
if rule_model not in settings.DATA_PERMISSION_MODELS:
|
||||||
|
|||||||
@@ -60,12 +60,17 @@ async def rbac_verify(request: Request, _token: str = DependsJwtAuth) -> None:
|
|||||||
if path_auth_perm in settings.RBAC_ROLE_MENU_EXCLUDE:
|
if path_auth_perm in settings.RBAC_ROLE_MENU_EXCLUDE:
|
||||||
return
|
return
|
||||||
|
|
||||||
# 已分配菜单权限校验
|
# 菜单去重
|
||||||
allow_perms = []
|
unique_menus = {}
|
||||||
for role in user_roles:
|
for role in user_roles:
|
||||||
for menu in role.menus:
|
for menu in role.menus:
|
||||||
if menu.perms and menu.status == StatusType.enable:
|
unique_menus[menu.id] = menu
|
||||||
allow_perms.extend(menu.perms.split(','))
|
|
||||||
|
# 已分配菜单权限校验
|
||||||
|
allow_perms = []
|
||||||
|
for menu in list(unique_menus.values()):
|
||||||
|
if menu.perms and menu.status == StatusType.enable:
|
||||||
|
allow_perms.extend(menu.perms.split(','))
|
||||||
if path_auth_perm not in allow_perms:
|
if path_auth_perm not in allow_perms:
|
||||||
raise AuthorizationError
|
raise AuthorizationError
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ class Settings(BaseSettings):
|
|||||||
# FastAPI
|
# FastAPI
|
||||||
FASTAPI_API_V1_PATH: str = '/api/v1'
|
FASTAPI_API_V1_PATH: str = '/api/v1'
|
||||||
FASTAPI_TITLE: str = 'FastAPI'
|
FASTAPI_TITLE: str = 'FastAPI'
|
||||||
FASTAPI_VERSION: str = '0.0.1'
|
FASTAPI_VERSION: str = '1.5.0'
|
||||||
FASTAPI_DESCRIPTION: str = 'FastAPI Best Architecture'
|
FASTAPI_DESCRIPTION: str = 'FastAPI Best Architecture'
|
||||||
FASTAPI_DOCS_URL: str = '/docs'
|
FASTAPI_DOCS_URL: str = '/docs'
|
||||||
FASTAPI_REDOC_URL: str = '/redoc'
|
FASTAPI_REDOC_URL: str = '/redoc'
|
||||||
|
|||||||
@@ -40,8 +40,6 @@ async def register_init(app: FastAPI) -> AsyncGenerator[None, None]:
|
|||||||
"""
|
"""
|
||||||
# 创建数据库表
|
# 创建数据库表
|
||||||
await create_table()
|
await create_table()
|
||||||
# 连接 redis
|
|
||||||
await redis_client.open()
|
|
||||||
# 初始化 limiter
|
# 初始化 limiter
|
||||||
await FastAPILimiter.init(
|
await FastAPILimiter.init(
|
||||||
redis=redis_client,
|
redis=redis_client,
|
||||||
|
|||||||
@@ -9,6 +9,6 @@ from backend.plugin.code_generator.api.v1.gen import router as gen_router
|
|||||||
|
|
||||||
v1 = APIRouter(prefix=f'{settings.FASTAPI_API_V1_PATH}/gen', tags=['代码生成'])
|
v1 = APIRouter(prefix=f'{settings.FASTAPI_API_V1_PATH}/gen', tags=['代码生成'])
|
||||||
|
|
||||||
v1.include_router(gen_router)
|
v1.include_router(gen_router, prefix='/tables')
|
||||||
v1.include_router(business_router, prefix='/businesses')
|
v1.include_router(business_router, prefix='/businesses')
|
||||||
v1.include_router(model_router, prefix='/models')
|
v1.include_router(model_router, prefix='/models')
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ from backend.plugin.code_generator.service.gen_service import gen_service
|
|||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
@router.get('/tables', summary='获取数据库表')
|
@router.get('', summary='获取数据库表')
|
||||||
async def get_all_tables(
|
async def get_all_tables(
|
||||||
table_schema: Annotated[str, Query(description='数据库名')] = 'fba',
|
table_schema: Annotated[str, Query(description='数据库名')] = 'fba',
|
||||||
) -> ResponseSchemaModel[list[str]]:
|
) -> ResponseSchemaModel[list[str]]:
|
||||||
@@ -37,24 +37,24 @@ async def import_table(obj: ImportParam) -> ResponseModel:
|
|||||||
return response_base.success()
|
return response_base.success()
|
||||||
|
|
||||||
|
|
||||||
@router.get('/preview/{pk}', summary='生成代码预览', dependencies=[DependsJwtAuth])
|
@router.get('/{pk}/preview', summary='生成代码预览', dependencies=[DependsJwtAuth])
|
||||||
async def preview_code(pk: Annotated[int, Path(description='业务 ID')]) -> ResponseSchemaModel[dict[str, bytes]]:
|
async def preview_code(pk: Annotated[int, Path(description='业务 ID')]) -> ResponseSchemaModel[dict[str, bytes]]:
|
||||||
data = await gen_service.preview(pk=pk)
|
data = await gen_service.preview(pk=pk)
|
||||||
return response_base.success(data=data)
|
return response_base.success(data=data)
|
||||||
|
|
||||||
|
|
||||||
@router.get('/generate/{pk}/path', summary='获取代码生成路径', dependencies=[DependsJwtAuth])
|
@router.get('/{pk}/code/path', summary='获取代码生成路径', dependencies=[DependsJwtAuth])
|
||||||
async def generate_path(pk: Annotated[int, Path(description='业务 ID')]) -> ResponseSchemaModel[list[str]]:
|
async def generate_path(pk: Annotated[int, Path(description='业务 ID')]) -> ResponseSchemaModel[list[str]]:
|
||||||
data = await gen_service.get_generate_path(pk=pk)
|
data = await gen_service.get_generate_path(pk=pk)
|
||||||
return response_base.success(data=data)
|
return response_base.success(data=data)
|
||||||
|
|
||||||
|
|
||||||
@router.post(
|
@router.post(
|
||||||
'/generate/{pk}',
|
'/{pk}/code',
|
||||||
summary='代码生成',
|
summary='代码生成',
|
||||||
description='文件磁盘写入,请谨慎操作',
|
description='文件磁盘写入,请谨慎操作',
|
||||||
dependencies=[
|
dependencies=[
|
||||||
Depends(RequestPermission('gen:code:generate')),
|
Depends(RequestPermission('gen:code:write')),
|
||||||
DependsRBAC,
|
DependsRBAC,
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
@@ -63,7 +63,7 @@ async def generate_code(pk: Annotated[int, Path(description='业务 ID')]) -> Re
|
|||||||
return response_base.success()
|
return response_base.success()
|
||||||
|
|
||||||
|
|
||||||
@router.get('/download/{pk}', summary='下载代码', dependencies=[DependsJwtAuth])
|
@router.get('/{pk}', summary='下载代码', dependencies=[DependsJwtAuth])
|
||||||
async def download_code(pk: Annotated[int, Path(description='业务 ID')]):
|
async def download_code(pk: Annotated[int, Path(description='业务 ID')]):
|
||||||
bio = await gen_service.download(pk=pk)
|
bio = await gen_service.download(pk=pk)
|
||||||
return StreamingResponse(
|
return StreamingResponse(
|
||||||
|
|||||||
@@ -175,8 +175,9 @@ class GenService:
|
|||||||
code_folder.mkdir(parents=True, exist_ok=True)
|
code_folder.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
init_filepath = code_folder.joinpath('__init__.py')
|
init_filepath = code_folder.joinpath('__init__.py')
|
||||||
async with aiofiles.open(init_filepath, 'w', encoding='utf-8') as f:
|
if not os.path.exists(init_filepath):
|
||||||
await f.write(gen_template.init_content)
|
async with aiofiles.open(init_filepath, 'w', encoding='utf-8') as f:
|
||||||
|
await f.write(gen_template.init_content)
|
||||||
|
|
||||||
# api __init__.py
|
# api __init__.py
|
||||||
if 'api' in str_code_filepath:
|
if 'api' in str_code_filepath:
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from fastapi import APIRouter
|
||||||
|
|
||||||
|
from backend.core.conf import settings
|
||||||
|
from backend.plugin.oauth2.api.v1.github import router as github_router
|
||||||
|
from backend.plugin.oauth2.api.v1.linux_do import router as linux_do_router
|
||||||
|
|
||||||
|
v1 = APIRouter(prefix=f'{settings.FASTAPI_API_V1_PATH}/oauth2')
|
||||||
|
|
||||||
|
v1.include_router(github_router, prefix='/github', tags=['Github OAuth2'])
|
||||||
|
v1.include_router(linux_do_router, prefix='/linux-do', tags=['LinuxDo OAuth2'])
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
@@ -5,10 +5,10 @@ from fastapi_limiter.depends import RateLimiter
|
|||||||
from fastapi_oauth20 import FastAPIOAuth20, GitHubOAuth20
|
from fastapi_oauth20 import FastAPIOAuth20, GitHubOAuth20
|
||||||
from starlette.responses import RedirectResponse
|
from starlette.responses import RedirectResponse
|
||||||
|
|
||||||
from backend.app.admin.service.oauth2_service import oauth2_service
|
|
||||||
from backend.common.enums import UserSocialType
|
from backend.common.enums import UserSocialType
|
||||||
from backend.common.response.response_schema import ResponseSchemaModel, response_base
|
from backend.common.response.response_schema import ResponseSchemaModel, response_base
|
||||||
from backend.core.conf import settings
|
from backend.core.conf import settings
|
||||||
|
from backend.plugin.oauth2.service.oauth2_service import oauth2_service
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
@@ -44,4 +44,6 @@ async def github_login(
|
|||||||
user=user,
|
user=user,
|
||||||
social=UserSocialType.github,
|
social=UserSocialType.github,
|
||||||
)
|
)
|
||||||
return RedirectResponse(url=f'{settings.OAUTH2_FRONTEND_REDIRECT_URI}?access_token={data.access_token}')
|
return RedirectResponse(
|
||||||
|
url=f'{settings.OAUTH2_FRONTEND_REDIRECT_URI}?access_token={data.access_token}&session_uuid={data.session_uuid}'
|
||||||
|
)
|
||||||
+4
-2
@@ -5,10 +5,10 @@ from fastapi_limiter.depends import RateLimiter
|
|||||||
from fastapi_oauth20 import FastAPIOAuth20, LinuxDoOAuth20
|
from fastapi_oauth20 import FastAPIOAuth20, LinuxDoOAuth20
|
||||||
from starlette.responses import RedirectResponse
|
from starlette.responses import RedirectResponse
|
||||||
|
|
||||||
from backend.app.admin.service.oauth2_service import oauth2_service
|
|
||||||
from backend.common.enums import UserSocialType
|
from backend.common.enums import UserSocialType
|
||||||
from backend.common.response.response_schema import ResponseSchemaModel, response_base
|
from backend.common.response.response_schema import ResponseSchemaModel, response_base
|
||||||
from backend.core.conf import settings
|
from backend.core.conf import settings
|
||||||
|
from backend.plugin.oauth2.service.oauth2_service import oauth2_service
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
@@ -47,4 +47,6 @@ async def linux_do_login(
|
|||||||
user=user,
|
user=user,
|
||||||
social=UserSocialType.linux_do,
|
social=UserSocialType.linux_do,
|
||||||
)
|
)
|
||||||
return RedirectResponse(url=f'{settings.OAUTH2_FRONTEND_REDIRECT_URI}?access_token={data.access_token}')
|
return RedirectResponse(
|
||||||
|
url=f'{settings.OAUTH2_FRONTEND_REDIRECT_URI}?access_token={data.access_token}&session_uuid={data.session_uuid}'
|
||||||
|
)
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
+15
-5
@@ -3,17 +3,16 @@
|
|||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
from sqlalchemy_crud_plus import CRUDPlus
|
from sqlalchemy_crud_plus import CRUDPlus
|
||||||
|
|
||||||
from backend.app.admin.model import UserSocial
|
from backend.plugin.oauth2.model import UserSocial
|
||||||
from backend.app.admin.schema.user_social import CreateUserSocialParam
|
from backend.plugin.oauth2.schema.user_social import CreateUserSocialParam
|
||||||
from backend.common.enums import UserSocialType
|
|
||||||
|
|
||||||
|
|
||||||
class CRUDUserSocial(CRUDPlus[UserSocial]):
|
class CRUDUserSocial(CRUDPlus[UserSocial]):
|
||||||
"""用户社交账号数据库操作类"""
|
"""用户社交账号数据库操作类"""
|
||||||
|
|
||||||
async def get(self, db: AsyncSession, pk: int, source: UserSocialType) -> UserSocial | None:
|
async def check_binding(self, db: AsyncSession, pk: int, source: str) -> UserSocial | None:
|
||||||
"""
|
"""
|
||||||
获取用户社交账号绑定详情
|
检查系统用户社交账号绑定
|
||||||
|
|
||||||
:param db: 数据库会话
|
:param db: 数据库会话
|
||||||
:param pk: 用户 ID
|
:param pk: 用户 ID
|
||||||
@@ -22,6 +21,17 @@ class CRUDUserSocial(CRUDPlus[UserSocial]):
|
|||||||
"""
|
"""
|
||||||
return await self.select_model_by_column(db, user_id=pk, source=source)
|
return await self.select_model_by_column(db, user_id=pk, source=source)
|
||||||
|
|
||||||
|
async def get_by_sid(self, db: AsyncSession, sid: str, source: str) -> UserSocial | None:
|
||||||
|
"""
|
||||||
|
通过 UUID 获取社交用户
|
||||||
|
|
||||||
|
:param db: 数据库会话
|
||||||
|
:param sid: 第三方 UUID
|
||||||
|
:param source: 社交账号类型
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
return await self.select_model_by_column(db, sid=sid, source=source)
|
||||||
|
|
||||||
async def create(self, db: AsyncSession, obj: CreateUserSocialParam) -> None:
|
async def create(self, db: AsyncSession, obj: CreateUserSocialParam) -> None:
|
||||||
"""
|
"""
|
||||||
创建用户社交账号绑定
|
创建用户社交账号绑定
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from backend.plugin.oauth2.model.user_social import UserSocial
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
from sqlalchemy import ForeignKey, String
|
||||||
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||||
|
|
||||||
|
from backend.common.model import Base, id_key
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from backend.app.admin.model import User
|
||||||
|
|
||||||
|
|
||||||
|
class UserSocial(Base):
|
||||||
|
"""用户社交表(OAuth2)"""
|
||||||
|
|
||||||
|
__tablename__ = 'sys_user_social'
|
||||||
|
|
||||||
|
id: Mapped[id_key] = mapped_column(init=False)
|
||||||
|
sid: Mapped[str] = mapped_column(String(20), comment='第三方用户 ID')
|
||||||
|
source: Mapped[str] = mapped_column(String(20), comment='第三方用户来源')
|
||||||
|
|
||||||
|
# 用户社交信息一对多
|
||||||
|
user_id: Mapped[int] = mapped_column(ForeignKey('sys_user.id', ondelete='CASCADE'), comment='用户关联ID')
|
||||||
|
user: Mapped[User | None] = relationship(init=False, backref='socials')
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
[plugin]
|
||||||
|
summary = 'OAuth 2.0'
|
||||||
|
version = '0.0.3'
|
||||||
|
description = '通过 OAuth 2.0 的方式登录系统'
|
||||||
|
author = 'wu-clan'
|
||||||
|
|
||||||
|
[app]
|
||||||
|
router = ['v1']
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
fastapi-oauth20>=0.0.1
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
+1
-5
@@ -9,12 +9,8 @@ from backend.common.schema import SchemaBase
|
|||||||
class UserSocialSchemaBase(SchemaBase):
|
class UserSocialSchemaBase(SchemaBase):
|
||||||
"""用户社交基础模型"""
|
"""用户社交基础模型"""
|
||||||
|
|
||||||
|
sid: str = Field(description='第三方用户 ID')
|
||||||
source: UserSocialType = Field(description='社交平台')
|
source: UserSocialType = Field(description='社交平台')
|
||||||
open_id: str | None = Field(None, description='开放平台 ID')
|
|
||||||
uid: str | None = Field(None, description='用户 ID')
|
|
||||||
union_id: str | None = Field(None, description='开放平台唯一 ID')
|
|
||||||
scope: str | None = Field(None, description='授权范围')
|
|
||||||
code: str | None = Field(None, description='授权码')
|
|
||||||
|
|
||||||
|
|
||||||
class CreateUserSocialParam(UserSocialSchemaBase):
|
class CreateUserSocialParam(UserSocialSchemaBase):
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
+50
-38
@@ -6,17 +6,16 @@ from fast_captcha import text_captcha
|
|||||||
from fastapi import BackgroundTasks, Request, Response
|
from fastapi import BackgroundTasks, Request, Response
|
||||||
|
|
||||||
from backend.app.admin.crud.crud_user import user_dao
|
from backend.app.admin.crud.crud_user import user_dao
|
||||||
from backend.app.admin.crud.crud_user_social import user_social_dao
|
|
||||||
from backend.app.admin.schema.token import GetLoginToken
|
from backend.app.admin.schema.token import GetLoginToken
|
||||||
from backend.app.admin.schema.user import RegisterUserParam
|
from backend.app.admin.schema.user import AddOAuth2UserParam
|
||||||
from backend.app.admin.schema.user_social import CreateUserSocialParam
|
|
||||||
from backend.app.admin.service.login_log_service import login_log_service
|
from backend.app.admin.service.login_log_service import login_log_service
|
||||||
from backend.common.enums import LoginLogStatusType, UserSocialType
|
from backend.common.enums import LoginLogStatusType, UserSocialType
|
||||||
from backend.common.exception.errors import AuthorizationError
|
|
||||||
from backend.common.security import jwt
|
from backend.common.security import jwt
|
||||||
from backend.core.conf import settings
|
from backend.core.conf import settings
|
||||||
from backend.database.db import async_db_session
|
from backend.database.db import async_db_session
|
||||||
from backend.database.redis import redis_client
|
from backend.database.redis import redis_client
|
||||||
|
from backend.plugin.oauth2.crud.crud_user_social import user_social_dao
|
||||||
|
from backend.plugin.oauth2.schema.user_social import CreateUserSocialParam
|
||||||
from backend.utils.timezone import timezone
|
from backend.utils.timezone import timezone
|
||||||
|
|
||||||
|
|
||||||
@@ -43,52 +42,65 @@ class OAuth2Service:
|
|||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
async with async_db_session.begin() as db:
|
async with async_db_session.begin() as db:
|
||||||
# 获取 OAuth2 平台用户信息
|
sid = user.get('uuid')
|
||||||
social_id = user.get('id')
|
username = user.get('username')
|
||||||
social_username = user.get('username')
|
nickname = user.get('nickname')
|
||||||
|
email = user.get('email')
|
||||||
|
avatar = user.get('avatar_url')
|
||||||
|
|
||||||
if social == UserSocialType.github:
|
if social == UserSocialType.github:
|
||||||
social_username = user.get('login')
|
sid = user.get('id')
|
||||||
social_nickname = user.get('name')
|
username = user.get('login')
|
||||||
social_email = user.get('email')
|
nickname = user.get('name')
|
||||||
if social == UserSocialType.linux_do: # 不提供明文邮箱的平台
|
|
||||||
social_email = f'{social_username}@linux.do'
|
if social == UserSocialType.linux_do:
|
||||||
if not social_email:
|
sid = user.get('id')
|
||||||
raise AuthorizationError(msg=f'授权失败,{social.value} 账户未绑定邮箱')
|
nickname = user.get('name')
|
||||||
# 创建系统用户
|
|
||||||
sys_user = await user_dao.check_email(db, social_email)
|
sys_user = None
|
||||||
if not sys_user:
|
user_social = await user_social_dao.get_by_sid(db, str(sid), str(social.value))
|
||||||
sys_user = await user_dao.get_by_username(db, social_username)
|
|
||||||
if sys_user:
|
|
||||||
social_username = f'{social_username}#{text_captcha(5)}'
|
|
||||||
sys_user = await user_dao.get_by_nickname(db, social_nickname)
|
|
||||||
if sys_user:
|
|
||||||
social_username = f'{social_nickname}#{text_captcha(5)}'
|
|
||||||
new_sys_user = RegisterUserParam(
|
|
||||||
username=social_username, password=None, nickname=social_username, email=social_email
|
|
||||||
)
|
|
||||||
await user_dao.create(db, new_sys_user, social=True)
|
|
||||||
await db.flush()
|
|
||||||
sys_user = await user_dao.check_email(db, social_email)
|
|
||||||
# 绑定社交用户
|
|
||||||
sys_user_id = sys_user.id
|
|
||||||
user_social = await user_social_dao.get(db, sys_user_id, social.value)
|
|
||||||
if not user_social:
|
if not user_social:
|
||||||
new_user_social = CreateUserSocialParam(source=social.value, uid=str(social_id), user_id=sys_user_id)
|
if email:
|
||||||
await user_social_dao.create(db, new_user_social)
|
sys_user = await user_dao.check_email(db, email)
|
||||||
|
|
||||||
|
# 创建系统用户
|
||||||
|
if not sys_user:
|
||||||
|
while await user_dao.get_by_username(db, username):
|
||||||
|
username = f'{username}_{text_captcha(5)}'
|
||||||
|
new_sys_user = AddOAuth2UserParam(
|
||||||
|
username=username,
|
||||||
|
password='123456', # 默认密码,可修改系统用户表进行默认密码检测并配合前端进行修改密码提示
|
||||||
|
nickname=nickname,
|
||||||
|
email=email,
|
||||||
|
avatar=avatar,
|
||||||
|
)
|
||||||
|
await user_dao.add_by_oauth2(db, new_sys_user)
|
||||||
|
await db.flush()
|
||||||
|
sys_user = await user_dao.get_by_username(db, username)
|
||||||
|
|
||||||
|
# 绑定社交用户
|
||||||
|
new_user_social = CreateUserSocialParam(sid=str(sid), source=social.value, user_id=sys_user.id)
|
||||||
|
await user_social_dao.create(db, new_user_social)
|
||||||
|
|
||||||
|
if not sys_user:
|
||||||
|
sys_user = await user_dao.get(db, user_social.user_id)
|
||||||
|
if avatar:
|
||||||
|
await user_dao.update_avatar(db, sys_user.id, avatar)
|
||||||
|
|
||||||
# 创建 token
|
# 创建 token
|
||||||
access_token = await jwt.create_access_token(
|
access_token = await jwt.create_access_token(
|
||||||
str(sys_user_id),
|
str(sys_user.id),
|
||||||
sys_user.is_multi_login,
|
sys_user.is_multi_login,
|
||||||
# extra info
|
# extra info
|
||||||
username=sys_user.username,
|
username=sys_user.username,
|
||||||
nickname=sys_user.nickname,
|
nickname=sys_user.nickname or f'#{text_captcha(5)}',
|
||||||
last_login_time=timezone.t_str(timezone.now()),
|
last_login_time=timezone.t_str(timezone.now()),
|
||||||
ip=request.state.ip,
|
ip=request.state.ip,
|
||||||
os=request.state.os,
|
os=request.state.os,
|
||||||
browser=request.state.browser,
|
browser=request.state.browser,
|
||||||
device=request.state.device,
|
device=request.state.device,
|
||||||
)
|
)
|
||||||
refresh_token = await jwt.create_refresh_token(str(sys_user_id), multi_login=sys_user.is_multi_login)
|
refresh_token = await jwt.create_refresh_token(str(sys_user.id), multi_login=sys_user.is_multi_login)
|
||||||
await user_dao.update_login_time(db, sys_user.username)
|
await user_dao.update_login_time(db, sys_user.username)
|
||||||
await db.refresh(sys_user)
|
await db.refresh(sys_user)
|
||||||
login_log = dict(
|
login_log = dict(
|
||||||
@@ -112,8 +124,8 @@ class OAuth2Service:
|
|||||||
data = GetLoginToken(
|
data = GetLoginToken(
|
||||||
access_token=access_token.access_token,
|
access_token=access_token.access_token,
|
||||||
access_token_expire_time=access_token.access_token_expire_time,
|
access_token_expire_time=access_token.access_token_expire_time,
|
||||||
user=sys_user, # type: ignore
|
|
||||||
session_uuid=access_token.session_uuid,
|
session_uuid=access_token.session_uuid,
|
||||||
|
user=sys_user, # type: ignore
|
||||||
)
|
)
|
||||||
return data
|
return data
|
||||||
|
|
||||||
+19
-20
@@ -1,6 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
import asyncio
|
|
||||||
import inspect
|
import inspect
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
@@ -11,7 +10,6 @@ import warnings
|
|||||||
from functools import lru_cache
|
from functools import lru_cache
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
import nest_asyncio
|
|
||||||
import rtoml
|
import rtoml
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, Request
|
from fastapi import APIRouter, Depends, Request
|
||||||
@@ -22,8 +20,9 @@ from backend.common.exception.errors import ForbiddenError
|
|||||||
from backend.common.log import log
|
from backend.common.log import log
|
||||||
from backend.core.conf import settings
|
from backend.core.conf import settings
|
||||||
from backend.core.path_conf import PLUGIN_DIR
|
from backend.core.path_conf import PLUGIN_DIR
|
||||||
from backend.database.redis import redis_client
|
from backend.database.redis import RedisCli, redis_client
|
||||||
from backend.plugin.errors import PluginConfigError, PluginInjectError
|
from backend.plugin.errors import PluginConfigError, PluginInjectError
|
||||||
|
from backend.utils._asyncio import run_await
|
||||||
from backend.utils.import_parse import import_module_cached
|
from backend.utils.import_parse import import_module_cached
|
||||||
|
|
||||||
|
|
||||||
@@ -84,19 +83,18 @@ def parse_plugin_config() -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
|
|||||||
extra_plugins = []
|
extra_plugins = []
|
||||||
app_plugins = []
|
app_plugins = []
|
||||||
|
|
||||||
# 事件循环嵌套: https://pypi.org/project/nest-asyncio/
|
plugins = get_plugins()
|
||||||
try:
|
|
||||||
loop = asyncio.get_running_loop()
|
|
||||||
except RuntimeError:
|
|
||||||
loop = asyncio.get_event_loop()
|
|
||||||
else:
|
|
||||||
nest_asyncio.apply()
|
|
||||||
|
|
||||||
plugin_status = loop.run_until_complete(redis_client.hgetall(f'{settings.PLUGIN_REDIS_PREFIX}:status')) # type: ignore
|
# 使用独立单例,避免与主线程冲突
|
||||||
|
current_redis_client = RedisCli()
|
||||||
|
run_await(current_redis_client.open)()
|
||||||
|
|
||||||
|
run_await(current_redis_client.delete_prefix)(f'{settings.PLUGIN_REDIS_PREFIX}:info', exclude=plugins)
|
||||||
|
plugin_status = run_await(current_redis_client.hgetall)(f'{settings.PLUGIN_REDIS_PREFIX}:status')
|
||||||
if not plugin_status:
|
if not plugin_status:
|
||||||
plugin_status = {}
|
plugin_status = {}
|
||||||
|
|
||||||
for plugin in get_plugins():
|
for plugin in plugins:
|
||||||
data = load_plugin_config(plugin)
|
data = load_plugin_config(plugin)
|
||||||
|
|
||||||
plugin_info = data.get('plugin')
|
plugin_info = data.get('plugin')
|
||||||
@@ -118,16 +116,17 @@ def parse_plugin_config() -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
|
|||||||
app_plugins.append(data)
|
app_plugins.append(data)
|
||||||
|
|
||||||
# 补充插件信息
|
# 补充插件信息
|
||||||
data['plugin']['enable'] = plugin_status.setdefault(plugin, StatusType.enable.value)
|
data['plugin']['enable'] = plugin_status.setdefault(plugin, str(StatusType.enable.value))
|
||||||
data['plugin']['name'] = plugin
|
data['plugin']['name'] = plugin
|
||||||
|
|
||||||
# 缓存插件信息
|
# 缓存插件信息
|
||||||
loop.create_task(
|
run_await(current_redis_client.set)(
|
||||||
redis_client.set(f'{settings.PLUGIN_REDIS_PREFIX}:info:{plugin}', json.dumps(data, ensure_ascii=False))
|
f'{settings.PLUGIN_REDIS_PREFIX}:info:{plugin}', json.dumps(data, ensure_ascii=False)
|
||||||
)
|
)
|
||||||
|
|
||||||
# 缓存插件状态
|
# 缓存插件状态
|
||||||
loop.create_task(redis_client.hset(f'{settings.PLUGIN_REDIS_PREFIX}:status', mapping=plugin_status))
|
run_await(current_redis_client.hset)(f'{settings.PLUGIN_REDIS_PREFIX}:status', mapping=plugin_status)
|
||||||
|
run_await(current_redis_client.delete)(f'{settings.PLUGIN_REDIS_PREFIX}:changed')
|
||||||
|
|
||||||
return extra_plugins, app_plugins
|
return extra_plugins, app_plugins
|
||||||
|
|
||||||
@@ -150,9 +149,9 @@ def inject_extra_router(plugin: dict[str, Any]) -> None:
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
# 解析插件路由配置
|
# 解析插件路由配置
|
||||||
file_config = plugin.get('api', {}).get(f'{file[:-3]}', {})
|
file_config = plugin['api'][file[:-3]]
|
||||||
prefix = file_config.get('prefix', '')
|
prefix = file_config['prefix']
|
||||||
tags = file_config.get('tags', [])
|
tags = file_config['tags']
|
||||||
|
|
||||||
# 获取插件路由模块
|
# 获取插件路由模块
|
||||||
file_path = os.path.join(root, file)
|
file_path = os.path.join(root, file)
|
||||||
@@ -205,7 +204,7 @@ def inject_app_router(plugin: dict[str, Any], target_router: APIRouter) -> None:
|
|||||||
module_path = f'backend.plugin.{plugin_name}.api.router'
|
module_path = f'backend.plugin.{plugin_name}.api.router'
|
||||||
try:
|
try:
|
||||||
module = import_module_cached(module_path)
|
module = import_module_cached(module_path)
|
||||||
routers = plugin.get('app', {}).get('router')
|
routers = plugin['app']['router']
|
||||||
if not routers or not isinstance(routers, list):
|
if not routers or not isinstance(routers, list):
|
||||||
raise PluginConfigError(f'应用级插件 {plugin_name} 配置文件存在错误,请检查')
|
raise PluginConfigError(f'应用级插件 {plugin_name} 配置文件存在错误,请检查')
|
||||||
|
|
||||||
|
|||||||
@@ -288,13 +288,14 @@ create index ix_sys_opera_log_id
|
|||||||
|
|
||||||
create table sys_role
|
create table sys_role
|
||||||
(
|
(
|
||||||
id int auto_increment comment '主键 ID'
|
id int auto_increment comment '主键 ID'
|
||||||
primary key,
|
primary key,
|
||||||
name varchar(20) not null comment '角色名称',
|
name varchar(20) not null comment '角色名称',
|
||||||
status int not null comment '角色状态(0停用 1正常)',
|
status int not null comment '角色状态(0停用 1正常)',
|
||||||
remark longtext null comment '备注',
|
is_filter_scopes tinyint(1) not null comment '过滤数据权限(0否 1是)',
|
||||||
created_time datetime not null comment '创建时间',
|
remark longtext null comment '备注',
|
||||||
updated_time datetime null comment '更新时间',
|
created_time datetime not null comment '创建时间',
|
||||||
|
updated_time datetime null comment '更新时间',
|
||||||
constraint name
|
constraint name
|
||||||
unique (name)
|
unique (name)
|
||||||
)
|
)
|
||||||
@@ -418,9 +419,9 @@ create table sys_user_social
|
|||||||
id int auto_increment comment '主键 ID'
|
id int auto_increment comment '主键 ID'
|
||||||
primary key,
|
primary key,
|
||||||
source varchar(20) not null comment '第三方用户来源',
|
source varchar(20) not null comment '第三方用户来源',
|
||||||
open_id varchar(20) null comment '第三方用户的 open id',
|
open_id varchar(20) null comment '第三方用户 open id',
|
||||||
uid varchar(20) null comment '第三方用户的 ID',
|
sid varchar(20) null comment '第三方用户 ID',
|
||||||
union_id varchar(20) null comment '第三方用户的 union id',
|
union_id varchar(20) null comment '第三方用户 union id',
|
||||||
scope varchar(120) null comment '第三方用户授予的权限',
|
scope varchar(120) null comment '第三方用户授予的权限',
|
||||||
code varchar(50) null comment '用户的授权 code',
|
code varchar(50) null comment '用户的授权 code',
|
||||||
user_id int null comment '用户关联ID',
|
user_id int null comment '用户关联ID',
|
||||||
@@ -437,3 +438,4 @@ create index ix_sys_user_social_id
|
|||||||
|
|
||||||
create index user_id
|
create index user_id
|
||||||
on sys_user_social (user_id);
|
on sys_user_social (user_id);
|
||||||
|
|
||||||
|
|||||||
@@ -1,37 +1,112 @@
|
|||||||
insert into sys_dept (id, name, sort, leader, phone, email, status, del_flag, parent_id, created_time, updated_time)
|
insert into sys_dept (id, name, sort, leader, phone, email, status, del_flag, parent_id, created_time, updated_time)
|
||||||
values (1, 'test', 0, null, null, null, 1, 0, null, '2023-06-26 17:13:45', null);
|
values (1, '测试', 0, null, null, null, 1, 0, null, '2025-05-26 17:13:45', null);
|
||||||
|
|
||||||
insert into fba.sys_menu (id, title, name, path, sort, icon, type, component, perms, status, display, cache, link, remark, parent_id, created_time, updated_time)
|
insert into sys_menu (id, title, name, path, sort, icon, type, component, perms, status, display, cache, link, remark, parent_id, created_time, updated_time)
|
||||||
values (1, '测试', 'Test', 'test', 0, null, 0, null, null, 0, 0, 1, null, null, null, '2023-07-27 19:14:10', null),
|
values (1, '概览', 'Dashboard', 'dashboard', 0, 'ant-design:dashboard-outlined', 0, null, null, 1, 1, 1, '', null, null, '2025-06-09 17:26:18', null),
|
||||||
(2, '仪表盘', 'Dashboard', 'dashboard', 0, 'material-symbols:dashboard', 0, null, null, 1, 1, 1, null, null, null, '2023-07-27 19:15:45', null),
|
(2, '系统管理', 'System', 'system', 1, 'eos-icons:admin', 0, null, null, 1, 1, 1, '', null, null, '2025-06-09 17:30:01', null),
|
||||||
(3, '工作台', 'Workspace', 'workspace', 0, null, 1, '/dashboard/workspace/index.vue', null, 1, 1, 1, null, null, 2, '2023-07-27 19:17:59', null),
|
(3, '系统自动化', 'Automation', 'automation', 2, 'material-symbols:automation', 0, null, null, 1, 1, 1, '', null, null, '2025-06-09 17:31:41', null),
|
||||||
(4, '数据分析', 'Analytics', 'analytics', 0, null, 1, '/dashboard/analytics/index.vue', null, 1, 1, 1, null, null, 2, '2023-07-27 19:17:59', null),
|
(4, '日志管理', 'Log', 'log', 3, 'carbon:cloud-logging', 0, null, null, 1, 1, 1, '', null, null, '2025-06-09 17:32:34', null),
|
||||||
(5, '系统管理', 'Admin', 'admin', 0, 'eos-icons:admin', 0, null, null, 1, 1, 1, null, null, null, '2023-07-27 19:23:00', null),
|
(5, '系统监控', 'Monitor', 'monitor', 4, 'mdi:monitor-eye', 0, null, null, 1, 1, 1, '', null, null, '2025-06-09 17:33:29', null),
|
||||||
(6, '部门管理', 'SysDept', 'sys-dept', 0, null, 1, '/admin/dept/index.vue', null, 1, 1, 1, null, null, 5, '2023-07-27 19:23:42', null),
|
(6, '项目', 'Project', 'fba', 5, 'https://wu-clan.github.io/picx-images-hosting/logo/fba.png', 0, null, null, 1, 1, 1, '', null, null, '2025-06-09 17:35:41', null),
|
||||||
(7, '用户管理', 'SysUser', 'sys-user', 0, null, 1, '/admin/user/index.vue', null, 1, 1, 1, null, null, 5, '2023-07-27 19:25:13', null),
|
(7, '分析页', 'Analytics', 'analytics', 0, 'lucide:area-chart', 1, '/dashboard/analytics/index', null, 1, 1, 1, '', null, 1, '2025-06-09 17:54:29', null),
|
||||||
(8, '角色管理', 'SysRole', 'sys-role', 0, null, 1, '/admin/role/index.vue', null, 1, 1, 1, null, null, 5, '2023-07-27 19:25:45', null),
|
(8, '工作台', 'Workspace', 'workspace', 1, 'carbon:workspace', 1, '/dashboard/workspace/index', null, 1, 1, 1, '', null, 1, '2025-06-09 17:57:09', null),
|
||||||
(9, '菜单管理', 'SysMenu', 'sys-menu', 0, null, 1, '/admin/menu/index.vue', null, 1, 1, 1, null, null, 5, '2023-07-27 19:45:29', null),
|
(9, '文档', 'Document', null, 1, 'lucide:book-open-text', 4, null, null, 1, 1, 1, 'https://fastapi-practices.github.io/fastapi_best_architecture_docs', null, 6, '2025-06-09 17:59:44', null),
|
||||||
(10, 'API 管理', 'SysApi', 'sys-api', 0, null, 1, '/admin/api/index.vue', null, 1, 1, 1, null, null, 5, '2023-07-27 19:24:12', null),
|
(10, 'Github', 'Github', null, 2, 'ant-design:github-filled', 4, null, null, 1, 1, 1, 'https://github.com/fastapi-practices/fastapi_best_architecture', null, 6, '2025-06-09 18:00:50', null),
|
||||||
(11, '数据规则管理', 'SysDataRule', 'sys-data-rule', 0, null, 1, '/admin/data-rule/index.vue', null, 1, 1, 1, null, null, 5, '2023-07-27 19:24:12', null),
|
(11, 'Apifox', 'Apifox', 'apifox', 3, 'simple-icons:apifox', 3, null, null, 1, 1, 1, 'https://apifox.com/apidoc/shared-28a93f02-730b-4f33-bb5e-4dad92058cc0', null, 6, '2025-06-09 18:01:39', null),
|
||||||
(12, '系统自动化', 'Automation', 'automation', 0, 'material-symbols:automation', 0, null, null, 1, 1, 1, null, null, null, '2024-07-27 02:06:20', null),
|
(12, '部门管理', 'SysDept', 'sys-dept', 1, 'mingcute:department-line', 1, '/system/dept/index', null, 1, 1, 1, '', null, 2, '2025-06-09 18:03:17', null),
|
||||||
(13, '代码生成', 'CodeGenerator', 'code-generator', 0, null, 1, '/automation/code-generator/index.vue', null, 1, 1, 1, null, null, 12, '2024-07-27 12:24:54', null),
|
(13, '用户管理', 'SysUser', 'sys-user', 2, 'ant-design:user-outlined', 1, '/system/user/index', null, 1, 1, 1, '', null, 2, '2025-06-09 18:03:54', null),
|
||||||
(14, '系统监控', 'Monitor', 'monitor', 0, 'mdi:monitor-eye', 0, null, null, 1, 1, 1, null, null, null, '2023-07-27 19:27:08', null),
|
(14, '角色管理', 'SysRole', 'sys-role', 3, 'carbon:user-role', 1, '/system/role/index', null, 1, 1, 1, '', null, 2, '2025-06-09 18:04:47', null),
|
||||||
(15, 'Redis 监控', 'Redis', 'redis', 0, null, 1, '/monitor/redis/index.vue', null, 1, 1, 1, null, null, 14, '2023-07-27 19:28:03', null),
|
(15, '菜单管理', 'SysMenu', 'sys-menu', 4, 'ant-design:menu-outlined', 1, '/system/menu/index', null, 1, 1, 1, '', null, 2, '2025-06-09 18:05:31', null),
|
||||||
(16, '服务器监控', 'Server', 'server', 0, null, 1, '/monitor/server/index.vue', null, 1, 1, 1, null, null, 14, '2023-07-27 19:28:29', null),
|
(16, '数据权限', 'SysDataPermission', 'sys-data-permission', 5, 'icon-park-outline:permissions', 0, null, null, 1, 1, 1, '', null, 2, '2025-06-09 18:07:04', null),
|
||||||
(17, '日志', 'Log', 'log', 0, 'carbon:cloud-logging', 0, null, null, 1, 1, 1, null, null, null, '2023-07-27 19:19:59', null),
|
(17, '数据范围', 'SysDataScope', 'sys-data-scope', 6, 'cuida:scope-outline', 1, '/system/data-permission/scope/index', null, 1, 1, 1, '', null, 16, '2025-06-09 18:07:46', null),
|
||||||
(18, '登录日志', 'Login', 'login', 0, null, 1, '/log/login/index.vue', null, 1, 1, 1, null, null, 17, '2023-07-27 19:20:56', null),
|
(18, '数据规则', 'SysDataRule', 'sys-data-rule', 7, 'material-symbols:rule', 1, '/system/data-permission/rule/index', null, 1, 1, 1, '', null, 16, '2025-06-09 18:08:22', null),
|
||||||
(19, '操作日志', 'Opera', 'opera', 0, null, 1, '/log/opera/index.vue', null, 1, 1, 1, null, null, 17, '2023-07-27 19:21:28', null),
|
(19, '插件管理', 'SysPlugin', 'sys-plugin', 8, 'clarity:plugin-line', 1, '/system/plugin/index', null, 1, 1, 1, '', null, 2, '2025-06-09 18:09:12', null),
|
||||||
(20, '官网', 'Site', '', 998, 'dashicons:admin-site', 1, null, null, 1, 1, 1, 'https://fastapi-practices.github.io/fastapi_best_architecture_docs/', null, null, '2023-07-27 19:22:24', null),
|
(20, '参数管理', 'SysConfig', 'sys-config', 9, 'codicon:symbol-parameter', 1, '/system/config/index', null, 1, 1, 1, '', null, 2, '2025-06-09 18:10:20', null),
|
||||||
(21, '赞助', 'Sponsor', '', 999, 'material-icon-theme:github-sponsors', 1, null, null, 1, 1, 1, 'https://wu-clan.github.io/sponsor/', null, null, '2024-07-27 12:39:57', null);
|
(21, '字典管理', 'SysDict', 'sys-dict', 10, 'fluent-mdl2:dictionary', 1, '/system/dict/index', null, 1, 1, 1, '', null, 2, '2025-06-09 18:11:10', null),
|
||||||
|
(22, '通知公告', 'SysNotice', 'sys-notice', 11, 'fe:notice-push', 1, '/system/notice/index', null, 1, 1, 1, '', null, 2, '2025-06-09 18:11:38', null),
|
||||||
|
(23, '代码生成', 'CodeGenerator', 'code-generator', 1, 'tabler:code', 1, '/automation/code-generator/index', null, 1, 1, 1, '', null, 3, '2025-06-09 18:12:38', null),
|
||||||
|
(24, '任务调度', 'Scheduler', 'scheduler', 2, 'ix:scheduler', 1, '/automation/scheduler/index', null, 1, 1, 1, '', null, 3, '2025-06-09 18:13:19', null),
|
||||||
|
(25, '登录日志', 'LoginLog', 'login', 1, 'mdi:login', 1, '/log/login/index', null, 1, 1, 1, '', null, 4, '2025-06-09 18:14:35', null),
|
||||||
|
(26, '操作日志', 'OperaLog', 'opera', 2, 'carbon:operations-record', 1, '/log/opera/index', null, 1, 1, 1, '', null, 4, '2025-06-09 18:15:26', null),
|
||||||
|
(27, '在线用户', 'Online', 'online', 1, 'wpf:online', 1, '/monitor/online/index', null, 1, 1, 1, '', null, 5, '2025-06-09 18:17:12', null),
|
||||||
|
(28, 'Redis', 'Redis', 'redis', 2, 'devicon:redis', 1, '/monitor/redis/index', null, 1, 1, 1, '', null, 5, '2025-06-09 18:17:42', null),
|
||||||
|
(29, 'Server', 'Server', 'server', 3, 'mdi:server-outline', 1, '/monitor/server/index', null, 1, 1, 1, '', null, 5, '2025-06-09 18:18:12', null),
|
||||||
|
(30, '新增', 'AddSysDept', null, 0, null, 2, null, 'sys:dept:add', 1, 0, 1, '', null, 12, '2025-06-09 18:21:17', null),
|
||||||
|
(31, '修改', 'EditSysDept', null, 0, null, 2, null, 'sys:dept:edit', 1, 0, 1, '', null, 12, '2025-06-09 18:22:01', null),
|
||||||
|
(32, '删除', 'DeleteSysDept', null, 0, null, 2, null, 'sys:dept:del', 1, 0, 1, '', null, 12, '2025-06-09 18:22:39', null),
|
||||||
|
(33, '删除', 'DeleteSysUser', null, 0, null, 2, null, 'sys:user:del', 1, 0, 1, '', null, 13, '2025-06-09 18:24:09', null),
|
||||||
|
(34, '新增', 'AddSysRole', null, 0, null, 2, null, 'sys:role:add', 1, 0, 1, '', null, 14, '2025-06-09 18:25:08', null),
|
||||||
|
(35, '修改', 'EditSysRole', null, 0, null, 2, null, 'sys:role:edit', 1, 0, 1, '', null, 14, '2025-06-09 18:26:30', null),
|
||||||
|
(36, '修改角色菜单', 'EditSysRoleMenu', null, 0, null, 2, null, 'sys:role:menu:edit', 1, 0, 1, '', null, 14, '2025-06-09 18:27:24', null),
|
||||||
|
(37, '修改角色数据范围', 'EditSysRoleScope', null, 0, null, 2, null, 'sys:role:scope:edit', 1, 0, 1, '', null, 14, '2025-06-09 18:28:25', null),
|
||||||
|
(38, '删除', 'DeleteSysRole', null, 0, null, 2, null, 'sys:role:del', 1, 0, 1, '', null, 14, '2025-06-09 18:28:55', null),
|
||||||
|
(39, '新增', 'AddSysMenu', null, 0, null, 2, null, 'sys:menu:add', 1, 0, 1, '', null, 15, '2025-06-09 18:29:51', null),
|
||||||
|
(40, '修改', 'EditSysMenu', null, 0, null, 2, null, 'sys:menu:edit', 1, 0, 1, '', null, 15, '2025-06-09 18:30:13', null),
|
||||||
|
(41, '删除', 'DeleteSysMenu', null, 0, null, 2, null, 'sys:menu:del', 1, 0, 1, '', null, 15, '2025-06-09 18:30:37', null),
|
||||||
|
(42, '新增', 'AddSysDataScope', null, 0, null, 2, null, 'data:scope:add', 1, 0, 1, '', null, 17, '2025-06-09 18:31:11', null),
|
||||||
|
(43, '修改', 'EditSysDataScope', null, 0, null, 2, null, 'data:scope:edit', 1, 0, 1, '', null, 17, '2025-06-09 18:31:42', null),
|
||||||
|
(44, '修改数据范围规则', 'EditDataScopeRule', null, 0, null, 2, null, 'data:scope:rule:edit', 1, 0, 1, '', null, 17, '2025-06-09 18:32:36', null),
|
||||||
|
(45, '删除', 'DeleteSysDataScope', null, 0, null, 2, null, 'data:scope:del', 1, 0, 1, '', null, 17, '2025-06-09 18:33:09', null),
|
||||||
|
(46, '新增', 'AddSysDataRule', null, 0, null, 2, null, 'data:rule:add', 1, 0, 1, '', null, 18, '2025-06-09 18:35:54', null),
|
||||||
|
(47, '修改', 'EditSysDataRule', null, 0, null, 2, null, 'data:rule:edit', 1, 0, 1, '', null, 18, '2025-06-09 18:36:19', null),
|
||||||
|
(48, '删除', 'DeleteSysDataRule', null, 0, null, 2, null, 'data:rule:del', 1, 0, 1, '', null, 18, '2025-06-09 18:36:44', null),
|
||||||
|
(49, '安装zip插件', 'InstallZipSysPlugin', null, 0, null, 2, null, 'sys:plugin:zip', 1, 0, 1, '', null, 19, '2025-06-09 18:38:14', null),
|
||||||
|
(50, '安装git插件', 'InstallGitSysPlugin', null, 0, null, 2, null, 'sys:plugin:git', 1, 0, 1, '', null, 19, '2025-06-09 18:38:43', null),
|
||||||
|
(51, '卸载', 'UninstallSysPlugin', null, 0, null, 2, null, 'sys:plugin:del', 1, 0, 1, '', null, 19, '2025-06-09 18:39:08', null),
|
||||||
|
(52, '修改', 'EditSysPlugin', null, 0, null, 2, null, 'sys:plugin:status', 1, 0, 1, '', null, 19, '2025-06-09 18:39:47', null),
|
||||||
|
(53, '新增网站参数', 'AddWebsiteSysConfig', null, 0, null, 2, null, 'sys:config:website:add', 1, 0, 1, '', null, 20, '2025-06-09 18:43:30', null),
|
||||||
|
(54, '新增用户协议', 'AddProtocolSysConfig', null, 0, null, 2, null, 'sys:config:protocol:add', 1, 0, 1, '', null, 20, '2025-06-09 18:44:13', null),
|
||||||
|
(55, '新增用户政策', 'AddPolicySysConfig', null, 0, null, 2, null, 'sys:config:policy:add', 1, 0, 1, '', null, 20, '2025-06-09 18:45:28', null),
|
||||||
|
(56, '新增', 'AddSysConfig', null, 0, null, 2, null, 'sys:config:add', 1, 0, 1, '', null, 20, '2025-06-09 18:45:52', null),
|
||||||
|
(57, '修改', 'EditSysConfig', null, 0, null, 2, null, 'sys:config:edit', 1, 0, 1, '', null, 20, '2025-06-09 18:46:13', null),
|
||||||
|
(58, '删除', 'DeleteSysConfig', null, 0, null, 2, null, 'sys:config:del', 1, 0, 1, '', null, 20, '2025-06-09 18:46:36', null),
|
||||||
|
(59, '新增类型', 'AddSysDictType', null, 0, null, 2, null, 'sys:dict:type:add', 1, 0, 1, '', null, 21, '2025-06-09 18:48:17', null),
|
||||||
|
(60, '修改类型', 'EditSysDictType', null, 0, null, 2, null, 'sys:dict:type:edit', 1, 0, 1, '', null, 21, '2025-06-09 18:48:49', null),
|
||||||
|
(61, '删除类型', 'DeleteSysDictType', null, 0, null, 2, null, 'sys:dict:type:del', 1, 0, 1, '', null, 21, '2025-06-09 18:49:23', null),
|
||||||
|
(62, '新增', 'AddSysDictData', null, 0, null, 2, null, 'sys:dict:data:add', 1, 0, 1, '', null, 21, '2025-06-09 18:50:01', null),
|
||||||
|
(63, '修改', 'EditSysDictData', null, 0, null, 2, null, 'sys:dict:data:edit', 1, 0, 1, '', null, 21, '2025-06-09 18:50:26', null),
|
||||||
|
(64, '删除', 'DeleteSysDictData', null, 0, null, 2, null, 'sys:dict:data:del', 1, 0, 1, '', null, 21, '2025-06-09 18:50:48', null),
|
||||||
|
(65, '新增', 'AddSysNotice', null, 0, null, 2, null, 'sys:notice:add', 1, 0, 1, '', null, 22, '2025-06-09 18:51:22', null),
|
||||||
|
(66, '修改', 'EditSysNotice', null, 0, null, 2, null, 'sys:notice:edit', 1, 0, 1, '', null, 22, '2025-06-09 18:51:45', null),
|
||||||
|
(67, '删除', 'DeleteSysNotice', null, 0, null, 2, null, 'sys:notice:del', 1, 0, 1, '', null, 22, '2025-06-09 18:52:10', null),
|
||||||
|
(68, '新增业务', 'AddSysGenCodeBusiness', null, 0, null, 2, null, 'gen:code:business:add', 1, 0, 1, '', null, 23, '2025-06-09 18:53:07', null),
|
||||||
|
(69, '修改业务', 'EditGenCodeBusiness', null, 0, null, 2, null, 'gen:code:business:edit', 1, 0, 1, '', null, 23, '2025-06-09 18:53:45', null),
|
||||||
|
(70, '删除业务', 'DeleteGenCodeBusiness', null, 0, null, 2, null, 'gen:code:business:del', 1, 0, 1, '', null, 23, '2025-06-09 18:54:11', null),
|
||||||
|
(71, '新增模型', 'AddGenCodeModel', null, 0, null, 2, null, 'gen:code:model:add', 1, 0, 1, '', null, 23, '2025-06-09 18:54:45', null),
|
||||||
|
(72, '修改模型', 'EditGenCodeModel', null, 0, null, 2, null, 'gen:code:model:edit', 1, 0, 1, '', null, 23, '2025-06-09 18:55:08', null),
|
||||||
|
(73, '删除模型', 'DeleteGenCodeModel', null, 0, null, 2, null, 'gen:code:model:del', 1, 0, 1, '', null, 23, '2025-06-09 18:55:35', null),
|
||||||
|
(74, '导入', 'ImportGenCode', null, 0, null, 2, null, 'gen:code:import', 1, 0, 1, '', null, 23, '2025-06-09 18:58:16', null),
|
||||||
|
(75, '写入', 'WriteGenCode', null, 0, null, 2, null, 'gen:code:write', 1, 0, 1, '', null, 23, '2025-06-09 19:01:22', null),
|
||||||
|
(76, '删除', 'DeleteSysLoginLog', null, 0, null, 2, null, 'log:login:del', 1, 0, 1, '', null, 25, '2025-06-09 19:02:21', null),
|
||||||
|
(77, '清空', 'EmptyLoginLog', null, 0, null, 2, null, 'log:login:empty', 1, 0, 1, '', null, 25, '2025-06-09 19:02:50', null),
|
||||||
|
(78, '删除', 'DeleteOperaLog', null, 0, null, 2, null, 'log:opera:del', 1, 0, 1, '', null, 26, '2025-06-09 19:03:13', null),
|
||||||
|
(79, '清空', 'EmptyOperaLog', null, 0, null, 2, null, 'log:opera:empty', 1, 0, 1, '', null, 26, '2025-06-09 19:03:40', null),
|
||||||
|
(80, '下线', 'KickSysToken', null, 0, null, 2, null, 'sys:token:kick', 1, 0, 1, '', null, 27, '2025-06-09 19:04:52', null);
|
||||||
|
|
||||||
insert into sys_role (id, name, status, remark, created_time, updated_time)
|
insert into sys_role (id, name, status, is_filter_scopes, remark, created_time, updated_time)
|
||||||
values (1, 'test', 1, null, '2023-06-26 17:13:45', null);
|
values (1, '测试', 1, 1, null, '2025-05-26 17:13:45', null);
|
||||||
|
|
||||||
insert into sys_role_menu (id, role_id, menu_id)
|
insert into sys_role_menu (id, role_id, menu_id)
|
||||||
values (1, 1, 1);
|
values (1, 1, 1),
|
||||||
|
(2, 1, 2),
|
||||||
|
(3, 1, 3),
|
||||||
|
(4, 1, 4);
|
||||||
|
|
||||||
insert into sys_user (id, uuid, username, nickname, password, salt, email, is_superuser, is_staff, status, is_multi_login, avatar, phone, join_time, last_login_time, dept_id, created_time, updated_time)
|
insert into sys_user (id, uuid, username, nickname, password, salt, email, is_superuser, is_staff, status, is_multi_login, avatar, phone, join_time, last_login_time, dept_id, created_time, updated_time)
|
||||||
values (1, 'af4c804f-3966-4949-ace2-3bb7416ea926', 'admin', '用户88888', '$2b$12$8y2eNucX19VjmZ3tYhBLcOsBwy9w1IjBQE4SSqwMDL5bGQVp2wqS.', 0x24326224313224387932654E7563583139566A6D5A33745968424C634F, 'admin@example.com', 1, 1, 1, 0, null, null, '2023-06-26 17:13:45', '2024-11-18 13:53:57', 1, '2023-06-26 17:13:45', '2024-11-18 13:53:57');
|
values (1, 'af4c804f-3966-4949-ace2-3bb7416ea926', 'admin', '用户88888', '$2b$12$8y2eNucX19VjmZ3tYhBLcOsBwy9w1IjBQE4SSqwMDL5bGQVp2wqS.', 0x24326224313224387932654E7563583139566A6D5A33745968424C634F, 'admin@example.com', 1, 1, 1, 1, null, null, '2023-06-26 17:13:45', '2024-11-18 13:53:57', 1, '2023-06-26 17:13:45', '2024-11-18 13:53:57');
|
||||||
|
|
||||||
insert into sys_user_role (id, user_id, role_id)
|
insert into sys_user_role (id, user_id, role_id)
|
||||||
values (1, 1, 1);
|
values (1, 1, 1);
|
||||||
|
|
||||||
|
insert into sys_data_scope (id, name, status, created_time, updated_time)
|
||||||
|
values (1, '测试部门数据权限', 1, '2025-06-09 16:53:29', null),
|
||||||
|
(2, '测试部门及以下数据权限', 1, '2025-06-09 16:53:40', null);
|
||||||
|
|
||||||
|
insert into sys_data_rule (id, name, model, `column`, operator, expression, `value`, created_time, updated_time)
|
||||||
|
values (1, '部门名称等于测试', '部门', 'name', 1, 0, '测试', '2025-06-09 16:56:06', null),
|
||||||
|
(2, '父部门 ID 等于 1', '部门', 'parent_id', 0, 0, '1', '2025-06-09 17:16:14', null);
|
||||||
|
|
||||||
|
insert into sys_data_scope_rule (id, data_scope_id, data_rule_id)
|
||||||
|
values (1, 1, 1),
|
||||||
|
(2, 2, 1),
|
||||||
|
(3, 2, 2);
|
||||||
|
|||||||
@@ -268,14 +268,15 @@ create index ix_sys_opera_log_id
|
|||||||
|
|
||||||
create table sys_role
|
create table sys_role
|
||||||
(
|
(
|
||||||
id serial
|
id serial
|
||||||
primary key,
|
primary key,
|
||||||
name varchar(20) not null
|
name varchar(20) not null
|
||||||
unique,
|
unique,
|
||||||
status integer not null,
|
status integer not null,
|
||||||
remark text,
|
is_filter_scopes integer not null,
|
||||||
created_time timestamp with time zone not null,
|
remark text,
|
||||||
updated_time timestamp with time zone
|
created_time timestamp with time zone not null,
|
||||||
|
updated_time timestamp with time zone
|
||||||
);
|
);
|
||||||
|
|
||||||
comment on table sys_role is '角色表';
|
comment on table sys_role is '角色表';
|
||||||
@@ -286,6 +287,8 @@ comment on column sys_role.name is '角色名称';
|
|||||||
|
|
||||||
comment on column sys_role.status is '角色状态(0停用 1正常)';
|
comment on column sys_role.status is '角色状态(0停用 1正常)';
|
||||||
|
|
||||||
|
comment on column sys_role.is_filter_scopes is '过滤数据权限(0否 1是)';
|
||||||
|
|
||||||
comment on column sys_role.remark is '备注';
|
comment on column sys_role.remark is '备注';
|
||||||
|
|
||||||
comment on column sys_role.created_time is '创建时间';
|
comment on column sys_role.created_time is '创建时间';
|
||||||
@@ -727,7 +730,7 @@ create table sys_user_social
|
|||||||
primary key,
|
primary key,
|
||||||
source varchar(20) not null,
|
source varchar(20) not null,
|
||||||
open_id varchar(20),
|
open_id varchar(20),
|
||||||
uid varchar(20),
|
sid varchar(20),
|
||||||
union_id varchar(20),
|
union_id varchar(20),
|
||||||
scope varchar(120),
|
scope varchar(120),
|
||||||
code varchar(50),
|
code varchar(50),
|
||||||
@@ -744,11 +747,11 @@ comment on column sys_user_social.id is '主键 ID';
|
|||||||
|
|
||||||
comment on column sys_user_social.source is '第三方用户来源';
|
comment on column sys_user_social.source is '第三方用户来源';
|
||||||
|
|
||||||
comment on column sys_user_social.open_id is '第三方用户的 open id';
|
comment on column sys_user_social.open_id is '第三方用户 open id';
|
||||||
|
|
||||||
comment on column sys_user_social.uid is '第三方用户的 ID';
|
comment on column sys_user_social.uid is '第三方用户 ID';
|
||||||
|
|
||||||
comment on column sys_user_social.union_id is '第三方用户的 union id';
|
comment on column sys_user_social.union_id is '第三方用户 union id';
|
||||||
|
|
||||||
comment on column sys_user_social.scope is '第三方用户授予的权限';
|
comment on column sys_user_social.scope is '第三方用户授予的权限';
|
||||||
|
|
||||||
|
|||||||
@@ -1,37 +1,123 @@
|
|||||||
insert into sys_dept (id, name, sort, leader, phone, email, status, del_flag, parent_id, created_time, updated_time)
|
insert into sys_dept (id, name, sort, leader, phone, email, status, del_flag, parent_id, created_time, updated_time)
|
||||||
values (1, 'test', 0, null, null, null, 1, 0, null, '2023-06-26 17:13:45', null);
|
values (1, '测试', 0, null, null, null, 1, 0, null, '2025-05-26 17:13:45', null);
|
||||||
|
|
||||||
insert into fba.sys_menu (id, title, name, path, sort, icon, type, component, perms, status, display, cache, link, remark, parent_id, created_time, updated_time)
|
insert into fba.sys_menu (id, title, name, path, sort, icon, type, component, perms, status, display, cache, link, remark, parent_id, created_time, updated_time)
|
||||||
values (1, '测试', 'Test', 'test', 0, null, 0, null, null, 0, 0, 1, null, null, null, '2023-07-27 19:14:10', null),
|
values (1, '概览', 'Dashboard', 'dashboard', 0, 'ant-design:dashboard-outlined', 0, null, null, 1, 1, 1, '', null, null, '2025-06-09 17:26:18', null),
|
||||||
(2, '仪表盘', 'Dashboard', 'dashboard', 0, 'material-symbols:dashboard', 0, null, null, 1, 1, 1, null, null, null, '2023-07-27 19:15:45', null),
|
(2, '系统管理', 'System', 'system', 1, 'eos-icons:admin', 0, null, null, 1, 1, 1, '', null, null, '2025-06-09 17:30:01', null),
|
||||||
(3, '工作台', 'Workspace', 'workspace', 0, null, 1, '/dashboard/workspace/index.vue', null, 1, 1, 1, null, null, 2, '2023-07-27 19:17:59', null),
|
(3, '系统自动化', 'Automation', 'automation', 2, 'material-symbols:automation', 0, null, null, 1, 1, 1, '', null, null, '2025-06-09 17:31:41', null),
|
||||||
(4, '数据分析', 'Analytics', 'analytics', 0, null, 1, '/dashboard/analytics/index.vue', null, 1, 1, 1, null, null, 2, '2023-07-27 19:17:59', null),
|
(4, '日志管理', 'Log', 'log', 3, 'carbon:cloud-logging', 0, null, null, 1, 1, 1, '', null, null, '2025-06-09 17:32:34', null),
|
||||||
(5, '系统管理', 'Admin', 'admin', 0, 'eos-icons:admin', 0, null, null, 1, 1, 1, null, null, null, '2023-07-27 19:23:00', null),
|
(5, '系统监控', 'Monitor', 'monitor', 4, 'mdi:monitor-eye', 0, null, null, 1, 1, 1, '', null, null, '2025-06-09 17:33:29', null),
|
||||||
(6, '部门管理', 'SysDept', 'sys-dept', 0, null, 1, '/admin/dept/index.vue', null, 1, 1, 1, null, null, 5, '2023-07-27 19:23:42', null),
|
(6, '项目', 'Project', 'fba', 5, 'https://wu-clan.github.io/picx-images-hosting/logo/fba.png', 0, null, null, 1, 1, 1, '', null, null, '2025-06-09 17:35:41', null),
|
||||||
(7, '用户管理', 'SysUser', 'sys-user', 0, null, 1, '/admin/user/index.vue', null, 1, 1, 1, null, null, 5, '2023-07-27 19:25:13', null),
|
(7, '分析页', 'Analytics', 'analytics', 0, 'lucide:area-chart', 1, '/dashboard/analytics/index', null, 1, 1, 1, '', null, 1, '2025-06-09 17:54:29', null),
|
||||||
(8, '角色管理', 'SysRole', 'sys-role', 0, null, 1, '/admin/role/index.vue', null, 1, 1, 1, null, null, 5, '2023-07-27 19:25:45', null),
|
(8, '工作台', 'Workspace', 'workspace', 1, 'carbon:workspace', 1, '/dashboard/workspace/index', null, 1, 1, 1, '', null, 1, '2025-06-09 17:57:09', null),
|
||||||
(9, '菜单管理', 'SysMenu', 'sys-menu', 0, null, 1, '/admin/menu/index.vue', null, 1, 1, 1, null, null, 5, '2023-07-27 19:45:29', null),
|
(9, '文档', 'Document', null, 1, 'lucide:book-open-text', 4, null, null, 1, 1, 1, 'https://fastapi-practices.github.io/fastapi_best_architecture_docs', null, 6, '2025-06-09 17:59:44', null),
|
||||||
(10, 'API 管理', 'SysApi', 'sys-api', 0, null, 1, '/admin/api/index.vue', null, 1, 1, 1, null, null, 5, '2023-07-27 19:24:12', null),
|
(10, 'Github', 'Github', null, 2, 'ant-design:github-filled', 4, null, null, 1, 1, 1, 'https://github.com/fastapi-practices/fastapi_best_architecture', null, 6, '2025-06-09 18:00:50', null),
|
||||||
(11, '数据规则管理', 'SysDataRule', 'sys-data-rule', 0, null, 1, '/admin/data-rule/index.vue', null, 1, 1, 1, null, null, 5, '2023-07-27 19:24:12', null),
|
(11, 'Apifox', 'Apifox', 'apifox', 3, 'simple-icons:apifox', 3, null, null, 1, 1, 1, 'https://apifox.com/apidoc/shared-28a93f02-730b-4f33-bb5e-4dad92058cc0', null, 6, '2025-06-09 18:01:39', null),
|
||||||
(12, '系统自动化', 'Automation', 'automation', 0, 'material-symbols:automation', 0, null, null, 1, 1, 1, null, null, null, '2024-07-27 02:06:20', null),
|
(12, '部门管理', 'SysDept', 'sys-dept', 1, 'mingcute:department-line', 1, '/system/dept/index', null, 1, 1, 1, '', null, 2, '2025-06-09 18:03:17', null),
|
||||||
(13, '代码生成', 'CodeGenerator', 'code-generator', 0, null, 1, '/automation/code-generator/index.vue', null, 1, 1, 1, null, null, 12, '2024-07-27 12:24:54', null),
|
(13, '用户管理', 'SysUser', 'sys-user', 2, 'ant-design:user-outlined', 1, '/system/user/index', null, 1, 1, 1, '', null, 2, '2025-06-09 18:03:54', null),
|
||||||
(14, '系统监控', 'Monitor', 'monitor', 0, 'mdi:monitor-eye', 0, null, null, 1, 1, 1, null, null, null, '2023-07-27 19:27:08', null),
|
(14, '角色管理', 'SysRole', 'sys-role', 3, 'carbon:user-role', 1, '/system/role/index', null, 1, 1, 1, '', null, 2, '2025-06-09 18:04:47', null),
|
||||||
(15, 'Redis 监控', 'Redis', 'redis', 0, null, 1, '/monitor/redis/index.vue', null, 1, 1, 1, null, null, 14, '2023-07-27 19:28:03', null),
|
(15, '菜单管理', 'SysMenu', 'sys-menu', 4, 'ant-design:menu-outlined', 1, '/system/menu/index', null, 1, 1, 1, '', null, 2, '2025-06-09 18:05:31', null),
|
||||||
(16, '服务器监控', 'Server', 'server', 0, null, 1, '/monitor/server/index.vue', null, 1, 1, 1, null, null, 14, '2023-07-27 19:28:29', null),
|
(16, '数据权限', 'SysDataPermission', 'sys-data-permission', 5, 'icon-park-outline:permissions', 0, null, null, 1, 1, 1, '', null, 2, '2025-06-09 18:07:04', null),
|
||||||
(17, '日志', 'Log', 'log', 0, 'carbon:cloud-logging', 0, null, null, 1, 1, 1, null, null, null, '2023-07-27 19:19:59', null),
|
(17, '数据范围', 'SysDataScope', 'sys-data-scope', 6, 'cuida:scope-outline', 1, '/system/data-permission/scope/index', null, 1, 1, 1, '', null, 16, '2025-06-09 18:07:46', null),
|
||||||
(18, '登录日志', 'Login', 'login', 0, null, 1, '/log/login/index.vue', null, 1, 1, 1, null, null, 17, '2023-07-27 19:20:56', null),
|
(18, '数据规则', 'SysDataRule', 'sys-data-rule', 7, 'material-symbols:rule', 1, '/system/data-permission/rule/index', null, 1, 1, 1, '', null, 16, '2025-06-09 18:08:22', null),
|
||||||
(19, '操作日志', 'Opera', 'opera', 0, null, 1, '/log/opera/index.vue', null, 1, 1, 1, null, null, 17, '2023-07-27 19:21:28', null),
|
(19, '插件管理', 'SysPlugin', 'sys-plugin', 8, 'clarity:plugin-line', 1, '/system/plugin/index', null, 1, 1, 1, '', null, 2, '2025-06-09 18:09:12', null),
|
||||||
(20, '官网', 'Site', '', 998, 'dashicons:admin-site', 1, null, null, 1, 1, 1, 'https://fastapi-practices.github.io/fastapi_best_architecture_docs/', null, null, '2023-07-27 19:22:24', null),
|
(20, '参数管理', 'SysConfig', 'sys-config', 9, 'codicon:symbol-parameter', 1, '/system/config/index', null, 1, 1, 1, '', null, 2, '2025-06-09 18:10:20', null),
|
||||||
(21, '赞助', 'Sponsor', '', 999, 'material-icon-theme:github-sponsors', 1, null, null, 1, 1, 1, 'https://wu-clan.github.io/sponsor/', null, null, '2024-07-27 12:39:57', null);
|
(21, '字典管理', 'SysDict', 'sys-dict', 10, 'fluent-mdl2:dictionary', 1, '/system/dict/index', null, 1, 1, 1, '', null, 2, '2025-06-09 18:11:10', null),
|
||||||
|
(22, '通知公告', 'SysNotice', 'sys-notice', 11, 'fe:notice-push', 1, '/system/notice/index', null, 1, 1, 1, '', null, 2, '2025-06-09 18:11:38', null),
|
||||||
|
(23, '代码生成', 'CodeGenerator', 'code-generator', 1, 'tabler:code', 1, '/automation/code-generator/index', null, 1, 1, 1, '', null, 3, '2025-06-09 18:12:38', null),
|
||||||
|
(24, '任务调度', 'Scheduler', 'scheduler', 2, 'ix:scheduler', 1, '/automation/scheduler/index', null, 1, 1, 1, '', null, 3, '2025-06-09 18:13:19', null),
|
||||||
|
(25, '登录日志', 'LoginLog', 'login', 1, 'mdi:login', 1, '/log/login/index', null, 1, 1, 1, '', null, 4, '2025-06-09 18:14:35', null),
|
||||||
|
(26, '操作日志', 'OperaLog', 'opera', 2, 'carbon:operations-record', 1, '/log/opera/index', null, 1, 1, 1, '', null, 4, '2025-06-09 18:15:26', null),
|
||||||
|
(27, '在线用户', 'Online', 'online', 1, 'wpf:online', 1, '/monitor/online/index', null, 1, 1, 1, '', null, 5, '2025-06-09 18:17:12', null),
|
||||||
|
(28, 'Redis', 'Redis', 'redis', 2, 'devicon:redis', 1, '/monitor/redis/index', null, 1, 1, 1, '', null, 5, '2025-06-09 18:17:42', null),
|
||||||
|
(29, 'Server', 'Server', 'server', 3, 'mdi:server-outline', 1, '/monitor/server/index', null, 1, 1, 1, '', null, 5, '2025-06-09 18:18:12', null),
|
||||||
|
(30, '新增', 'AddSysDept', null, 0, null, 2, null, 'sys:dept:add', 1, 0, 1, '', null, 12, '2025-06-09 18:21:17', null),
|
||||||
|
(31, '修改', 'EditSysDept', null, 0, null, 2, null, 'sys:dept:edit', 1, 0, 1, '', null, 12, '2025-06-09 18:22:01', null),
|
||||||
|
(32, '删除', 'DeleteSysDept', null, 0, null, 2, null, 'sys:dept:del', 1, 0, 1, '', null, 12, '2025-06-09 18:22:39', null),
|
||||||
|
(33, '删除', 'DeleteSysUser', null, 0, null, 2, null, 'sys:user:del', 1, 0, 1, '', null, 13, '2025-06-09 18:24:09', null),
|
||||||
|
(34, '新增', 'AddSysRole', null, 0, null, 2, null, 'sys:role:add', 1, 0, 1, '', null, 14, '2025-06-09 18:25:08', null),
|
||||||
|
(35, '修改', 'EditSysRole', null, 0, null, 2, null, 'sys:role:edit', 1, 0, 1, '', null, 14, '2025-06-09 18:26:30', null),
|
||||||
|
(36, '修改角色菜单', 'EditSysRoleMenu', null, 0, null, 2, null, 'sys:role:menu:edit', 1, 0, 1, '', null, 14, '2025-06-09 18:27:24', null),
|
||||||
|
(37, '修改角色数据范围', 'EditSysRoleScope', null, 0, null, 2, null, 'sys:role:scope:edit', 1, 0, 1, '', null, 14, '2025-06-09 18:28:25', null),
|
||||||
|
(38, '删除', 'DeleteSysRole', null, 0, null, 2, null, 'sys:role:del', 1, 0, 1, '', null, 14, '2025-06-09 18:28:55', null),
|
||||||
|
(39, '新增', 'AddSysMenu', null, 0, null, 2, null, 'sys:menu:add', 1, 0, 1, '', null, 15, '2025-06-09 18:29:51', null),
|
||||||
|
(40, '修改', 'EditSysMenu', null, 0, null, 2, null, 'sys:menu:edit', 1, 0, 1, '', null, 15, '2025-06-09 18:30:13', null),
|
||||||
|
(41, '删除', 'DeleteSysMenu', null, 0, null, 2, null, 'sys:menu:del', 1, 0, 1, '', null, 15, '2025-06-09 18:30:37', null),
|
||||||
|
(42, '新增', 'AddSysDataScope', null, 0, null, 2, null, 'data:scope:add', 1, 0, 1, '', null, 17, '2025-06-09 18:31:11', null),
|
||||||
|
(43, '修改', 'EditSysDataScope', null, 0, null, 2, null, 'data:scope:edit', 1, 0, 1, '', null, 17, '2025-06-09 18:31:42', null),
|
||||||
|
(44, '修改数据范围规则', 'EditDataScopeRule', null, 0, null, 2, null, 'data:scope:rule:edit', 1, 0, 1, '', null, 17, '2025-06-09 18:32:36', null),
|
||||||
|
(45, '删除', 'DeleteSysDataScope', null, 0, null, 2, null, 'data:scope:del', 1, 0, 1, '', null, 17, '2025-06-09 18:33:09', null),
|
||||||
|
(46, '新增', 'AddSysDataRule', null, 0, null, 2, null, 'data:rule:add', 1, 0, 1, '', null, 18, '2025-06-09 18:35:54', null),
|
||||||
|
(47, '修改', 'EditSysDataRule', null, 0, null, 2, null, 'data:rule:edit', 1, 0, 1, '', null, 18, '2025-06-09 18:36:19', null),
|
||||||
|
(48, '删除', 'DeleteSysDataRule', null, 0, null, 2, null, 'data:rule:del', 1, 0, 1, '', null, 18, '2025-06-09 18:36:44', null),
|
||||||
|
(49, '安装zip插件', 'InstallZipSysPlugin', null, 0, null, 2, null, 'sys:plugin:zip', 1, 0, 1, '', null, 19, '2025-06-09 18:38:14', null),
|
||||||
|
(50, '安装git插件', 'InstallGitSysPlugin', null, 0, null, 2, null, 'sys:plugin:git', 1, 0, 1, '', null, 19, '2025-06-09 18:38:43', null),
|
||||||
|
(51, '卸载', 'UninstallSysPlugin', null, 0, null, 2, null, 'sys:plugin:del', 1, 0, 1, '', null, 19, '2025-06-09 18:39:08', null),
|
||||||
|
(52, '修改', 'EditSysPlugin', null, 0, null, 2, null, 'sys:plugin:status', 1, 0, 1, '', null, 19, '2025-06-09 18:39:47', null),
|
||||||
|
(53, '新增网站参数', 'AddWebsiteSysConfig', null, 0, null, 2, null, 'sys:config:website:add', 1, 0, 1, '', null, 20, '2025-06-09 18:43:30', null),
|
||||||
|
(54, '新增用户协议', 'AddProtocolSysConfig', null, 0, null, 2, null, 'sys:config:protocol:add', 1, 0, 1, '', null, 20, '2025-06-09 18:44:13', null),
|
||||||
|
(55, '新增用户政策', 'AddPolicySysConfig', null, 0, null, 2, null, 'sys:config:policy:add', 1, 0, 1, '', null, 20, '2025-06-09 18:45:28', null),
|
||||||
|
(56, '新增', 'AddSysConfig', null, 0, null, 2, null, 'sys:config:add', 1, 0, 1, '', null, 20, '2025-06-09 18:45:52', null),
|
||||||
|
(57, '修改', 'EditSysConfig', null, 0, null, 2, null, 'sys:config:edit', 1, 0, 1, '', null, 20, '2025-06-09 18:46:13', null),
|
||||||
|
(58, '删除', 'DeleteSysConfig', null, 0, null, 2, null, 'sys:config:del', 1, 0, 1, '', null, 20, '2025-06-09 18:46:36', null),
|
||||||
|
(59, '新增类型', 'AddSysDictType', null, 0, null, 2, null, 'sys:dict:type:add', 1, 0, 1, '', null, 21, '2025-06-09 18:48:17', null),
|
||||||
|
(60, '修改类型', 'EditSysDictType', null, 0, null, 2, null, 'sys:dict:type:edit', 1, 0, 1, '', null, 21, '2025-06-09 18:48:49', null),
|
||||||
|
(61, '删除类型', 'DeleteSysDictType', null, 0, null, 2, null, 'sys:dict:type:del', 1, 0, 1, '', null, 21, '2025-06-09 18:49:23', null),
|
||||||
|
(62, '新增', 'AddSysDictData', null, 0, null, 2, null, 'sys:dict:data:add', 1, 0, 1, '', null, 21, '2025-06-09 18:50:01', null),
|
||||||
|
(63, '修改', 'EditSysDictData', null, 0, null, 2, null, 'sys:dict:data:edit', 1, 0, 1, '', null, 21, '2025-06-09 18:50:26', null),
|
||||||
|
(64, '删除', 'DeleteSysDictData', null, 0, null, 2, null, 'sys:dict:data:del', 1, 0, 1, '', null, 21, '2025-06-09 18:50:48', null),
|
||||||
|
(65, '新增', 'AddSysNotice', null, 0, null, 2, null, 'sys:notice:add', 1, 0, 1, '', null, 22, '2025-06-09 18:51:22', null),
|
||||||
|
(66, '修改', 'EditSysNotice', null, 0, null, 2, null, 'sys:notice:edit', 1, 0, 1, '', null, 22, '2025-06-09 18:51:45', null),
|
||||||
|
(67, '删除', 'DeleteSysNotice', null, 0, null, 2, null, 'sys:notice:del', 1, 0, 1, '', null, 22, '2025-06-09 18:52:10', null),
|
||||||
|
(68, '新增业务', 'AddSysGenCodeBusiness', null, 0, null, 2, null, 'gen:code:business:add', 1, 0, 1, '', null, 23, '2025-06-09 18:53:07', null),
|
||||||
|
(69, '修改业务', 'EditGenCodeBusiness', null, 0, null, 2, null, 'gen:code:business:edit', 1, 0, 1, '', null, 23, '2025-06-09 18:53:45', null),
|
||||||
|
(70, '删除业务', 'DeleteGenCodeBusiness', null, 0, null, 2, null, 'gen:code:business:del', 1, 0, 1, '', null, 23, '2025-06-09 18:54:11', null),
|
||||||
|
(71, '新增模型', 'AddGenCodeModel', null, 0, null, 2, null, 'gen:code:model:add', 1, 0, 1, '', null, 23, '2025-06-09 18:54:45', null),
|
||||||
|
(72, '修改模型', 'EditGenCodeModel', null, 0, null, 2, null, 'gen:code:model:edit', 1, 0, 1, '', null, 23, '2025-06-09 18:55:08', null),
|
||||||
|
(73, '删除模型', 'DeleteGenCodeModel', null, 0, null, 2, null, 'gen:code:model:del', 1, 0, 1, '', null, 23, '2025-06-09 18:55:35', null),
|
||||||
|
(74, '导入', 'ImportGenCode', null, 0, null, 2, null, 'gen:code:import', 1, 0, 1, '', null, 23, '2025-06-09 18:58:16', null),
|
||||||
|
(75, '写入', 'WriteGenCode', null, 0, null, 2, null, 'gen:code:write', 1, 0, 1, '', null, 23, '2025-06-09 19:01:22', null),
|
||||||
|
(76, '删除', 'DeleteSysLoginLog', null, 0, null, 2, null, 'log:login:del', 1, 0, 1, '', null, 25, '2025-06-09 19:02:21', null),
|
||||||
|
(77, '清空', 'EmptyLoginLog', null, 0, null, 2, null, 'log:login:empty', 1, 0, 1, '', null, 25, '2025-06-09 19:02:50', null),
|
||||||
|
(78, '删除', 'DeleteOperaLog', null, 0, null, 2, null, 'log:opera:del', 1, 0, 1, '', null, 26, '2025-06-09 19:03:13', null),
|
||||||
|
(79, '清空', 'EmptyOperaLog', null, 0, null, 2, null, 'log:opera:empty', 1, 0, 1, '', null, 26, '2025-06-09 19:03:40', null),
|
||||||
|
(80, '下线', 'KickSysToken', null, 0, null, 2, null, 'sys:token:kick', 1, 0, 1, '', null, 27, '2025-06-09 19:04:52', null);
|
||||||
|
|
||||||
insert into sys_role (id, name, status, remark, created_time, updated_time)
|
insert into sys_role (id, name, status, is_filter_scopes, remark, created_time, updated_time)
|
||||||
values (1, 'test', 1, null, '2023-06-26 17:13:45', null);
|
values (1, '测试', 1, 1, null, '2025-05-26 17:13:45', null);
|
||||||
|
|
||||||
insert into sys_role_menu (id, role_id, menu_id)
|
insert into sys_role_menu (id, role_id, menu_id)
|
||||||
values (1, 1, 1);
|
values (1, 1, 1),
|
||||||
|
(2, 1, 2),
|
||||||
|
(3, 1, 3),
|
||||||
|
(4, 1, 4);
|
||||||
|
|
||||||
insert into sys_user (id, uuid, username, nickname, password, salt, email, is_superuser, is_staff, status, is_multi_login, avatar, phone, join_time, last_login_time, dept_id, created_time, updated_time)
|
insert into sys_user (id, uuid, username, nickname, password, salt, email, is_superuser, is_staff, status, is_multi_login, avatar, phone, join_time, last_login_time, dept_id, created_time, updated_time)
|
||||||
values (1, 'af4c804f-3966-4949-ace2-3bb7416ea926', 'admin', '用户88888', '$2b$12$8y2eNucX19VjmZ3tYhBLcOsBwy9w1IjBQE4SSqwMDL5bGQVp2wqS.', '0x24326224313224387932654E7563583139566A6D5A33745968424C634F', 'admin@example.com', 1, 1, 1, 0, null, null, '2023-06-26 17:13:45', '2024-11-18 13:53:57', 1, '2023-06-26 17:13:45', '2024-11-18 13:53:57');
|
values (1, 'af4c804f-3966-4949-ace2-3bb7416ea926', 'admin', '用户88888', '$2b$12$8y2eNucX19VjmZ3tYhBLcOsBwy9w1IjBQE4SSqwMDL5bGQVp2wqS.', '0x24326224313224387932654E7563583139566A6D5A33745968424C634F', 'admin@example.com', 1, 1, 1, 1, null, null, '2023-06-26 17:13:45', '2024-11-18 13:53:57', 1, '2023-06-26 17:13:45', '2024-11-18 13:53:57');
|
||||||
|
|
||||||
insert into sys_user_role (id, user_id, role_id)
|
insert into sys_user_role (id, user_id, role_id)
|
||||||
values (1, 1, 1);
|
values (1, 1, 1);
|
||||||
|
|
||||||
|
insert into sys_data_scope (id, name, status, created_time, updated_time)
|
||||||
|
values (1, '测试部门数据权限', 1, '2025-06-09 16:53:29', null),
|
||||||
|
(2, '测试部门及以下数据权限', 1, '2025-06-09 16:53:40', null);
|
||||||
|
|
||||||
|
insert into sys_data_rule (id, name, model, `column`, operator, expression, `value`, created_time, updated_time)
|
||||||
|
values (1, '部门名称等于测试', '部门', 'name', 1, 0, '测试', '2025-06-09 16:56:06', null),
|
||||||
|
(2, '父部门 ID 等于 1', '部门', 'parent_id', 0, 0, '1', '2025-06-09 17:16:14', null);
|
||||||
|
|
||||||
|
insert into sys_data_scope_rule (id, data_scope_id, data_rule_id)
|
||||||
|
values (1, 1, 1),
|
||||||
|
(2, 2, 1),
|
||||||
|
(3, 2, 2);
|
||||||
|
|
||||||
|
-- Reset auto-increment values for each table based on max id
|
||||||
|
SELECT setval(pg_get_serial_sequence('sys_dept', 'id'),COALESCE(MAX(id), 0) + 1, true) FROM sys_dept;
|
||||||
|
SELECT setval(pg_get_serial_sequence('sys_menu', 'id'),COALESCE(MAX(id), 0) + 1, true) FROM sys_menu;
|
||||||
|
SELECT setval(pg_get_serial_sequence('sys_role', 'id'),COALESCE(MAX(id), 0) + 1, true) FROM sys_role;
|
||||||
|
SELECT setval(pg_get_serial_sequence('sys_role_menu', 'id'),COALESCE(MAX(id), 0) + 1, true) FROM sys_role_menu;
|
||||||
|
SELECT setval(pg_get_serial_sequence('sys_user', 'id'),COALESCE(MAX(id), 0) + 1, true) FROM sys_user;
|
||||||
|
SELECT setval(pg_get_serial_sequence('sys_user_role', 'id'),COALESCE(MAX(id), 0) + 1, true) FROM sys_user_role;
|
||||||
|
SELECT setval(pg_get_serial_sequence('sys_data_scope', 'id'),COALESCE(MAX(id), 0) + 1, true) FROM sys_data_scope;
|
||||||
|
SELECT setval(pg_get_serial_sequence('sys_data_rule', 'id'),COALESCE(MAX(id), 0) + 1, true) FROM sys_data_rule;
|
||||||
|
SELECT setval(pg_get_serial_sequence('sys_data_scope_rule', 'id'),COALESCE(MAX(id), 0) + 1, true) FROM sys_data_scope_rule;
|
||||||
|
|||||||
@@ -1,92 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>Socket.IO Task Notifications</title>
|
|
||||||
<style>
|
|
||||||
body {
|
|
||||||
font-family: Arial, sans-serif;
|
|
||||||
margin: 0;
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
#notifications {
|
|
||||||
margin-top: 20px;
|
|
||||||
padding: 10px;
|
|
||||||
border: 1px solid #ccc;
|
|
||||||
max-height: 300px;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
.notification {
|
|
||||||
padding: 10px;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
background-color: #f9f9f9;
|
|
||||||
border: 1px solid #ddd;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
.notification.new {
|
|
||||||
background-color: #d3f9d8;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
|
|
||||||
<h1>Task Notifications</h1>
|
|
||||||
<p>Waiting for task notifications...</p>
|
|
||||||
|
|
||||||
<div id="notifications"></div>
|
|
||||||
|
|
||||||
<!-- Load Socket.IO client -->
|
|
||||||
<script src="https://cdn.socket.io/4.5.1/socket.io.min.js"></script>
|
|
||||||
<script>
|
|
||||||
// Establish a connection to the server
|
|
||||||
const socket = io('http://127.0.0.1:8000', {
|
|
||||||
autoConnect: true,
|
|
||||||
path: '/ws/socket.io',
|
|
||||||
reconnection: true,
|
|
||||||
reconnectionAttempts: 3,
|
|
||||||
reconnectionDelay: 1000,
|
|
||||||
transports: ['websocket'],
|
|
||||||
auth: {
|
|
||||||
session_uuid: 'da985430-43b3-4860-a20a-97cc8e8e0b76',
|
|
||||||
token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzZXNzaW9uX3V1aWQiOiJkYTk4NTQzMC00M2IzLTQ4NjAtYTIwYS05N2NjOGU4ZTBiNzYiLCJleHAiOjE3Mzc2MzQwODUsInN1YiI6IjEifQ.QeXNtXhHG4cm5GRzOtv2wqZXi6HdqACmW_UjC4JLPsk'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Handle 'task_notification' event
|
|
||||||
socket.on('task_notification', (data) => {
|
|
||||||
// Get the notifications container
|
|
||||||
const notificationsContainer = document.getElementById('notifications');
|
|
||||||
|
|
||||||
// Create a new div for the notification
|
|
||||||
const notificationDiv = document.createElement('div');
|
|
||||||
notificationDiv.classList.add('notification', 'new');
|
|
||||||
notificationDiv.innerHTML = `
|
|
||||||
<strong>Task Notification:</strong>
|
|
||||||
<p>${data.msg}</p>
|
|
||||||
<small>Received at: ${new Date().toLocaleTimeString()}</small>
|
|
||||||
`;
|
|
||||||
|
|
||||||
// Append the new notification to the container
|
|
||||||
notificationsContainer.appendChild(notificationDiv);
|
|
||||||
|
|
||||||
// Scroll to the bottom to show the latest notification
|
|
||||||
notificationsContainer.scrollTop = notificationsContainer.scrollHeight;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Handle connection errors
|
|
||||||
socket.on('connect_error', (error) => {
|
|
||||||
console.error('Connection error:', error);
|
|
||||||
alert('Unable to connect to the server. Please check your connection.');
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on('connect', () => {
|
|
||||||
console.log('Connected to server');
|
|
||||||
});
|
|
||||||
|
|
||||||
socket.on('disconnect', () => {
|
|
||||||
console.log('Disconnected from server');
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import asyncio
|
||||||
|
import atexit
|
||||||
|
import threading
|
||||||
|
import weakref
|
||||||
|
|
||||||
|
from typing import Awaitable, Callable, TypeVar
|
||||||
|
|
||||||
|
T = TypeVar('T')
|
||||||
|
|
||||||
|
|
||||||
|
class _TaskRunner:
|
||||||
|
"""A task runner that runs an asyncio event loop on a background thread."""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.__loop: asyncio.AbstractEventLoop | None = None
|
||||||
|
self.__thread: threading.Thread | None = None
|
||||||
|
self.__lock = threading.Lock()
|
||||||
|
atexit.register(self.close)
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
"""关闭事件循环"""
|
||||||
|
if self.__loop:
|
||||||
|
self.__loop.stop()
|
||||||
|
|
||||||
|
def _target(self):
|
||||||
|
"""后台线程目标"""
|
||||||
|
loop = self.__loop
|
||||||
|
try:
|
||||||
|
loop.run_forever()
|
||||||
|
finally:
|
||||||
|
loop.close()
|
||||||
|
|
||||||
|
def run(self, coro):
|
||||||
|
"""在后台线程上同步运行协程"""
|
||||||
|
with self.__lock:
|
||||||
|
name = f'{threading.current_thread().name} - runner'
|
||||||
|
if self.__loop is None:
|
||||||
|
self.__loop = asyncio.new_event_loop()
|
||||||
|
self.__thread = threading.Thread(target=self._target, daemon=True, name=name)
|
||||||
|
self.__thread.start()
|
||||||
|
fut = asyncio.run_coroutine_threadsafe(coro, self.__loop)
|
||||||
|
return fut.result(None)
|
||||||
|
|
||||||
|
|
||||||
|
_runner_map = weakref.WeakValueDictionary()
|
||||||
|
|
||||||
|
|
||||||
|
def run_await(coro: Callable[..., Awaitable[T]]) -> Callable[..., T]:
|
||||||
|
"""将协程包装在一个函数中,该函数会阻塞,直到它执行完为止"""
|
||||||
|
|
||||||
|
def wrapped(*args, **kwargs):
|
||||||
|
name = threading.current_thread().name
|
||||||
|
inner = coro(*args, **kwargs)
|
||||||
|
try:
|
||||||
|
# 如果当前此线程中正在运行循环
|
||||||
|
# 使用任务运行程序
|
||||||
|
asyncio.get_running_loop()
|
||||||
|
if name not in _runner_map:
|
||||||
|
_runner_map[name] = _TaskRunner()
|
||||||
|
return _runner_map[name].run(inner)
|
||||||
|
except RuntimeError:
|
||||||
|
# 如果没有,请创建一个新的事件循环
|
||||||
|
loop = asyncio.get_event_loop()
|
||||||
|
return loop.run_until_complete(inner)
|
||||||
|
|
||||||
|
wrapped.__doc__ = coro.__doc__
|
||||||
|
return wrapped
|
||||||
@@ -93,16 +93,16 @@ def get_vben5_tree_data(row: Sequence[RowData]) -> list[dict[str, Any]]:
|
|||||||
:param row: 原始数据行序列
|
:param row: 原始数据行序列
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
# 需要移除的原始字段
|
meta_keys = {'title', 'icon', 'link', 'cache', 'display', 'status'}
|
||||||
remove_keys = {'title', 'icon', 'link', 'cache', 'display', 'status'}
|
|
||||||
|
|
||||||
vben5_nodes = [
|
vben5_nodes = [
|
||||||
{
|
{
|
||||||
**{k: v for k, v in node.items() if k not in remove_keys},
|
**{k: v for k, v in node.items() if k not in meta_keys},
|
||||||
'meta': {
|
'meta': {
|
||||||
'title': node['title'],
|
'title': node['title'],
|
||||||
'icon': node['icon'],
|
'icon': node['icon'],
|
||||||
'link': node['link'],
|
'iframeSrc': node['link'] if node['type'] == 3 else '',
|
||||||
|
'link': node['link'] if node['type'] == 4 else '',
|
||||||
'keepAlive': node['cache'],
|
'keepAlive': node['cache'],
|
||||||
'hideInMenu': not bool(node['display']),
|
'hideInMenu': not bool(node['display']),
|
||||||
'menuVisibleWithForbidden': not bool(node['status']),
|
'menuVisibleWithForbidden': not bool(node['status']),
|
||||||
|
|||||||
@@ -164,7 +164,7 @@ class ServerInfo:
|
|||||||
'mem_vms': ServerInfo.format_bytes(mem_info.vms),
|
'mem_vms': ServerInfo.format_bytes(mem_info.vms),
|
||||||
'mem_rss': ServerInfo.format_bytes(mem_info.rss),
|
'mem_rss': ServerInfo.format_bytes(mem_info.rss),
|
||||||
'mem_free': ServerInfo.format_bytes(mem_info.vms - mem_info.rss),
|
'mem_free': ServerInfo.format_bytes(mem_info.vms - mem_info.rss),
|
||||||
'startup': start_time,
|
'startup': timezone.t_str(start_time),
|
||||||
'elapsed': elapsed,
|
'elapsed': elapsed,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ dependencies = [
|
|||||||
"fast-captcha>=0.3.2",
|
"fast-captcha>=0.3.2",
|
||||||
"fastapi-cli==0.0.5",
|
"fastapi-cli==0.0.5",
|
||||||
"fastapi-limiter>=0.1.6",
|
"fastapi-limiter>=0.1.6",
|
||||||
"fastapi-oauth20>=0.0.1",
|
|
||||||
"fastapi-pagination>=0.13.0",
|
"fastapi-pagination>=0.13.0",
|
||||||
"fastapi[standard]==0.115.11",
|
"fastapi[standard]==0.115.11",
|
||||||
"flower>=2.0.0",
|
"flower>=2.0.0",
|
||||||
@@ -38,7 +37,6 @@ dependencies = [
|
|||||||
"jinja2>=3.1.4",
|
"jinja2>=3.1.4",
|
||||||
"loguru>=0.7.3",
|
"loguru>=0.7.3",
|
||||||
"msgspec>=0.19.0",
|
"msgspec>=0.19.0",
|
||||||
"nest-asyncio>=1.6.0",
|
|
||||||
"path==17.0.0",
|
"path==17.0.0",
|
||||||
"psutil>=6.0.0",
|
"psutil>=6.0.0",
|
||||||
"pwdlib>=0.2.1",
|
"pwdlib>=0.2.1",
|
||||||
|
|||||||
+1
-7
@@ -97,8 +97,6 @@ fastapi-cli==0.0.5
|
|||||||
# fastapi-best-architecture
|
# fastapi-best-architecture
|
||||||
fastapi-limiter==0.1.6
|
fastapi-limiter==0.1.6
|
||||||
# via fastapi-best-architecture
|
# via fastapi-best-architecture
|
||||||
fastapi-oauth20==0.0.1
|
|
||||||
# via fastapi-best-architecture
|
|
||||||
fastapi-pagination==0.13.0
|
fastapi-pagination==0.13.0
|
||||||
# via fastapi-best-architecture
|
# via fastapi-best-architecture
|
||||||
filelock==3.18.0
|
filelock==3.18.0
|
||||||
@@ -123,9 +121,7 @@ httpcore==1.0.7
|
|||||||
httptools==0.6.4
|
httptools==0.6.4
|
||||||
# via uvicorn
|
# via uvicorn
|
||||||
httpx==0.28.1
|
httpx==0.28.1
|
||||||
# via
|
# via fastapi
|
||||||
# fastapi
|
|
||||||
# fastapi-oauth20
|
|
||||||
humanize==4.12.2
|
humanize==4.12.2
|
||||||
# via flower
|
# via flower
|
||||||
identify==2.6.9
|
identify==2.6.9
|
||||||
@@ -161,8 +157,6 @@ mdurl==0.1.2
|
|||||||
# via markdown-it-py
|
# via markdown-it-py
|
||||||
msgspec==0.19.0
|
msgspec==0.19.0
|
||||||
# via fastapi-best-architecture
|
# via fastapi-best-architecture
|
||||||
nest-asyncio==1.6.0
|
|
||||||
# via fastapi-best-architecture
|
|
||||||
nodeenv==1.9.1
|
nodeenv==1.9.1
|
||||||
# via pre-commit
|
# via pre-commit
|
||||||
packaging==24.2
|
packaging==24.2
|
||||||
|
|||||||
@@ -603,7 +603,6 @@ dependencies = [
|
|||||||
{ name = "fastapi", extra = ["standard"] },
|
{ name = "fastapi", extra = ["standard"] },
|
||||||
{ name = "fastapi-cli" },
|
{ name = "fastapi-cli" },
|
||||||
{ name = "fastapi-limiter" },
|
{ name = "fastapi-limiter" },
|
||||||
{ name = "fastapi-oauth20" },
|
|
||||||
{ name = "fastapi-pagination" },
|
{ name = "fastapi-pagination" },
|
||||||
{ name = "flower" },
|
{ name = "flower" },
|
||||||
{ name = "gevent" },
|
{ name = "gevent" },
|
||||||
@@ -612,7 +611,6 @@ dependencies = [
|
|||||||
{ name = "jinja2" },
|
{ name = "jinja2" },
|
||||||
{ name = "loguru" },
|
{ name = "loguru" },
|
||||||
{ name = "msgspec" },
|
{ name = "msgspec" },
|
||||||
{ name = "nest-asyncio" },
|
|
||||||
{ name = "path" },
|
{ name = "path" },
|
||||||
{ name = "psutil" },
|
{ name = "psutil" },
|
||||||
{ name = "pwdlib" },
|
{ name = "pwdlib" },
|
||||||
@@ -658,7 +656,6 @@ requires-dist = [
|
|||||||
{ name = "fastapi", extras = ["standard"], specifier = "==0.115.11" },
|
{ name = "fastapi", extras = ["standard"], specifier = "==0.115.11" },
|
||||||
{ name = "fastapi-cli", specifier = "==0.0.5" },
|
{ name = "fastapi-cli", specifier = "==0.0.5" },
|
||||||
{ name = "fastapi-limiter", specifier = ">=0.1.6" },
|
{ name = "fastapi-limiter", specifier = ">=0.1.6" },
|
||||||
{ name = "fastapi-oauth20", specifier = ">=0.0.1" },
|
|
||||||
{ name = "fastapi-pagination", specifier = ">=0.13.0" },
|
{ name = "fastapi-pagination", specifier = ">=0.13.0" },
|
||||||
{ name = "flower", specifier = ">=2.0.0" },
|
{ name = "flower", specifier = ">=2.0.0" },
|
||||||
{ name = "gevent", specifier = ">=24.11.1" },
|
{ name = "gevent", specifier = ">=24.11.1" },
|
||||||
@@ -667,7 +664,6 @@ requires-dist = [
|
|||||||
{ name = "jinja2", specifier = ">=3.1.4" },
|
{ name = "jinja2", specifier = ">=3.1.4" },
|
||||||
{ name = "loguru", specifier = ">=0.7.3" },
|
{ name = "loguru", specifier = ">=0.7.3" },
|
||||||
{ name = "msgspec", specifier = ">=0.19.0" },
|
{ name = "msgspec", specifier = ">=0.19.0" },
|
||||||
{ name = "nest-asyncio", specifier = ">=1.6.0" },
|
|
||||||
{ name = "path", specifier = "==17.0.0" },
|
{ name = "path", specifier = "==17.0.0" },
|
||||||
{ name = "psutil", specifier = ">=6.0.0" },
|
{ name = "psutil", specifier = ">=6.0.0" },
|
||||||
{ name = "pwdlib", specifier = ">=0.2.1" },
|
{ name = "pwdlib", specifier = ">=0.2.1" },
|
||||||
@@ -725,18 +721,6 @@ wheels = [
|
|||||||
{ url = "https://mirrors.aliyun.com/pypi/packages/cd/b5/6f6b4d18bee1cafc857eae12738b3a03b7d1102b833668be868938c57b9d/fastapi_limiter-0.1.6-py3-none-any.whl", hash = "sha256:2e53179a4208b8f2c8795e38bb001324d3dc37d2800ff49fd28ec5caabf7a240" },
|
{ url = "https://mirrors.aliyun.com/pypi/packages/cd/b5/6f6b4d18bee1cafc857eae12738b3a03b7d1102b833668be868938c57b9d/fastapi_limiter-0.1.6-py3-none-any.whl", hash = "sha256:2e53179a4208b8f2c8795e38bb001324d3dc37d2800ff49fd28ec5caabf7a240" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "fastapi-oauth20"
|
|
||||||
version = "0.0.1"
|
|
||||||
source = { registry = "https://mirrors.aliyun.com/pypi/simple" }
|
|
||||||
dependencies = [
|
|
||||||
{ name = "httpx" },
|
|
||||||
]
|
|
||||||
sdist = { url = "https://mirrors.aliyun.com/pypi/packages/8e/66/25d4ece161689184f47130bc7ea5bf9c73113c2e628cf586546aa574d8d1/fastapi_oauth20-0.0.1.tar.gz", hash = "sha256:22bf6564f3af42a94cb597f8e20af1366e89e19af49291d15784a4c14059d4c9" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://mirrors.aliyun.com/pypi/packages/cf/87/7e0423109ba2c0f8a0794eedff5bb1f79792e5d8d0941ba0b26f962e3e5c/fastapi_oauth20-0.0.1-py3-none-any.whl", hash = "sha256:921fe0e874cb4186998f74b1d951c360400aceb5ca3889a85c806d865a991f88" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fastapi-pagination"
|
name = "fastapi-pagination"
|
||||||
version = "0.13.0"
|
version = "0.13.0"
|
||||||
@@ -1237,15 +1221,6 @@ wheels = [
|
|||||||
{ url = "https://mirrors.aliyun.com/pypi/packages/9c/fd/b247aec6add5601956d440488b7f23151d8343747e82c038af37b28d6098/multidict-6.2.0-py3-none-any.whl", hash = "sha256:5d26547423e5e71dcc562c4acdc134b900640a39abd9066d7326a7cc2324c530" },
|
{ url = "https://mirrors.aliyun.com/pypi/packages/9c/fd/b247aec6add5601956d440488b7f23151d8343747e82c038af37b28d6098/multidict-6.2.0-py3-none-any.whl", hash = "sha256:5d26547423e5e71dcc562c4acdc134b900640a39abd9066d7326a7cc2324c530" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "nest-asyncio"
|
|
||||||
version = "1.6.0"
|
|
||||||
source = { registry = "https://mirrors.aliyun.com/pypi/simple" }
|
|
||||||
sdist = { url = "https://mirrors.aliyun.com/pypi/packages/83/f8/51569ac65d696c8ecbee95938f89d4abf00f47d58d48f6fbabfe8f0baefe/nest_asyncio-1.6.0.tar.gz", hash = "sha256:6f172d5449aca15afd6c646851f4e31e02c598d553a667e38cafa997cfec55fe" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://mirrors.aliyun.com/pypi/packages/a0/c4/c2971a3ba4c6103a3d10c4b0f24f461ddc027f0f09763220cf35ca1401b3/nest_asyncio-1.6.0-py3-none-any.whl", hash = "sha256:87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nodeenv"
|
name = "nodeenv"
|
||||||
version = "1.9.1"
|
version = "1.9.1"
|
||||||
|
|||||||
Reference in New Issue
Block a user