Compare commits

...
41 Commits
Author SHA1 Message Date
Dylan 7afd8415cd Add support for snowflake ID primary key (#670)
* fix: 修复PostgreSQL SQL语法错误,将反引号替换为双引号

* feat: 新增雪花算法ID实现

* 优化雪花算法和主键类型

* 修复错误引用

* 添加雪花详情链接

* feat: add snowflake ID parser method

* 修复独立执行异常

* 更新系统时间错误类
2025-06-16 13:34:27 +08:00
Dylan 845f2f0ff8 Fix the postgresql sql script syntax error (#669) 2025-06-15 16:38:48 +08:00
Wu Clan 11d7792c0f Bump sqlalchemy crud plus version to 1.10.0 (#668)
* Bump sqlalchemy crud plus version to 1.10.0

* Update filter style

* Update filters style
2025-06-14 12:35:19 +08:00
Wu Clan 6883ec34c6 Fix the sidebar menu type filtering (#667) 2025-06-13 16:50:53 +08:00
Wu Clan 4c45e7ff27 Update the changelog for v1.5.0 (#664) 2025-06-09 21:36:54 +08:00
Wu Clan 5621daea98 Update the version to 1.5.0 (#663) 2025-06-09 21:32:12 +08:00
Wu Clan 71d168dfea Add data permission in init test sql (#662) 2025-06-09 21:00:49 +08:00
Wu Clan 2d4365189d Update menu sort in init test sql (#661) 2025-06-09 20:27:56 +08:00
Wu Clan 0c28a46e60 Update auth failed default status code (#660)
* Update auth failed default status code

* update init sql
2025-06-09 19:58:06 +08:00
Wu Clan a4c7dae99c Update the plugin download interface permission (#659) 2025-06-09 19:10:48 +08:00
Wu Clan bfda0a226c Update code generate interface permission (#658) 2025-06-09 19:10:40 +08:00
Wu Clan c9bbb105a3 Update data scope and rule to m2m (#657) 2025-06-09 19:10:15 +08:00
Wu Clan b415dc8912 Add OAuth2 user to auto bind a role (#656) 2025-06-08 20:18:24 +08:00
Wu Clan 05b74d71a3 Simplify OAuth2 model and optimize auth service (#655) 2025-06-08 16:51:19 +08:00
Wu Clan 10c0c691a7 Update user email and phone operation logic (#654) 2025-06-08 16:43:51 +08:00
Wu Clan 0bc3786fc6 Update OAuth2 callback interface return (#653) 2025-06-06 20:35:25 +08:00
Wu Clan 8e041ccb1b Update the changelog for v1.4.3 (#651) 2025-06-02 20:43:12 +08:00
Wu Clan a82e9b7b9d Fix data permission condition for filter data (#650) 2025-06-01 00:51:31 +08:00
Wu Clan 7bab8cc636 Update default value for role filter scopes (#649) 2025-05-31 23:53:47 +08:00
Wu Clan 7659b63d88 Add data permission condition for filter data (#648) 2025-05-31 23:40:59 +08:00
Wu Clan 33ebc5723f Add get all data scope rules interface (#647) 2025-05-31 23:09:26 +08:00
Wu Clan 86d9efd785 Update the server startup time to string (#646) 2025-05-31 17:26:07 +08:00
Wu Clan 57ae7f03a6 Refactor routes to better align with RESTful (#645) 2025-05-31 17:01:44 +08:00
Wu Clan 6ee3983846 Fix the filter of query all menus (#642) 2025-05-30 22:48:30 +08:00
Wu Clan f05ed5cdda Fix the menu delete interface arg description (#641) 2025-05-30 18:58:33 +08:00
Wu Clan 187dfa5344 Fix the role update business variables (#640) 2025-05-30 18:56:50 +08:00
Wu Clan 918c8a9d33 Update the changelog for v1.4.2 (#639) 2025-05-29 18:12:45 +08:00
Wu Clan ccfe35ea85 Simplify the user info update business (#638) 2025-05-29 18:09:50 +08:00
yzbf 4a118c166a Fix overwriting existing init files in code generator (#637) 2025-05-28 20:17:18 +08:00
Huyuwei 5f3823cb6d Fix auto-increment id for postgres init data (#632)
* fix: Reset auto-increment values for all tables based on max id

* refactor: Remove auto-increment reset for mysql

* remove unused code
2025-05-28 13:01:44 +08:00
Wu Clan 19ef52da9b Optimize install and build of plugin zip (#636)
* Optimiz install and build of plugin zip

* fix import
2025-05-27 11:46:17 +08:00
Wu Clan 39c3339a8d Optimize role-related data processing performance (#635) 2025-05-26 20:04:13 +08:00
Wu Clan 9f52629dcb Update the captcha invalidation error class (#634) 2025-05-26 19:16:56 +08:00
Wu Clan 1cddd6e2d5 Update non-linked sidebar support (#633)
* Update non-linked sidebar support

* fix typing
2025-05-26 15:52:45 +08:00
Wu Clan ed10e6ff47 Update the changelog for v1.4.1 (#630) 2025-05-25 14:19:41 +08:00
Wu Clan 48574fb50a Update the token decode for logout interface (#629) 2025-05-25 14:16:36 +08:00
Wu Clan ee7fe3087d Update the token check for logout interface (#625) 2025-05-23 22:05:26 +08:00
Wu Clan 270ac3d941 Refactor the system token to online users (#624) 2025-05-23 20:18:47 +08:00
Wu Clan 2f76988076 Add the deepwiki badge to README (#623) 2025-05-23 12:09:04 +08:00
Wu Clan b275481742 Update the menu path and type columns (#622)
* Update the menu path and type columns

* Update build vben5 tree

* Update data
2025-05-22 21:22:24 +08:00
Wu Clan f3f1ceb595 Update the changelog for v1.4.0 (#621) 2025-05-22 14:41:03 +08:00
72 changed files with 1442 additions and 1150 deletions
+108
View File
@@ -1,3 +1,106 @@
<a id="v1.5.0"></a>
# [v1.5.0](https://github.com/fastapi-practices/fastapi_best_architecture/releases/tag/v1.5.0) - 2025-06-09
## What's Changed
* Update changelog for v1.4.3 by [@wu-clan](https://github.com/wu-clan) in [#651](https://github.com/fastapi-practices/fastapi_best_architecture/pull/651)
* Update OAuth2 callback interface return by [@wu-clan](https://github.com/wu-clan) in [#653](https://github.com/fastapi-practices/fastapi_best_architecture/pull/653)
* Update user email and phone operation logic by [@wu-clan](https://github.com/wu-clan) in [#654](https://github.com/fastapi-practices/fastapi_best_architecture/pull/654)
* Simplify OAuth2 model and optimize auth service by [@wu-clan](https://github.com/wu-clan) in [#655](https://github.com/fastapi-practices/fastapi_best_architecture/pull/655)
* Add OAuth2 user to auto bind a role by [@wu-clan](https://github.com/wu-clan) in [#656](https://github.com/fastapi-practices/fastapi_best_architecture/pull/656)
* Update data scope and rule to m2m by [@wu-clan](https://github.com/wu-clan) in [#657](https://github.com/fastapi-practices/fastapi_best_architecture/pull/657)
* Update code generate interface permission by [@wu-clan](https://github.com/wu-clan) in [#658](https://github.com/fastapi-practices/fastapi_best_architecture/pull/658)
* Update the plugin download interface permission by [@wu-clan](https://github.com/wu-clan) in [#659](https://github.com/fastapi-practices/fastapi_best_architecture/pull/659)
* Update auth failed default status code by [@wu-clan](https://github.com/wu-clan) in [#660](https://github.com/fastapi-practices/fastapi_best_architecture/pull/660)
* Update menu sort in init test sql by [@wu-clan](https://github.com/wu-clan) in [#661](https://github.com/fastapi-practices/fastapi_best_architecture/pull/661)
* Add data permission in init test sql by [@wu-clan](https://github.com/wu-clan) in [#662](https://github.com/fastapi-practices/fastapi_best_architecture/pull/662)
* Update the version to 1.5.0 by [@wu-clan](https://github.com/wu-clan) in [#663](https://github.com/fastapi-practices/fastapi_best_architecture/pull/663)
**Full Changelog**: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.4.3...v1.5.0
[Changes][v1.5.0]
<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> <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 # [v1.3.0](https://github.com/fastapi-practices/fastapi_best_architecture/releases/tag/v1.3.0) - 2025-05-16
@@ -511,6 +614,11 @@
[Changes][v1.0.0] [Changes][v1.0.0]
[v1.5.0]: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.4.3...v1.5.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.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
+3 -43
View File
@@ -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)
[![GitHub](https://img.shields.io/github/license/fastapi-practices/fastapi_best_architecture)](https://github.com/fastapi-practices/fastapi_best_architecture/blob/master/LICENSE) [![GitHub](https://img.shields.io/github/license/fastapi-practices/fastapi_best_architecture)](https://github.com/fastapi-practices/fastapi_best_architecture/blob/master/LICENSE)
[![Python](https://img.shields.io/badge/python-3.10%2B-blue)](https://www.python.org/downloads/) [![Python](https://img.shields.io/badge/python-3.10%2B-blue)](https://www.python.org/downloads/)
@@ -21,12 +19,10 @@ Enterprise-level backend architecture solution
![Docker](https://img.shields.io/badge/Docker-%232496ED?logo=docker&logoColor=white) ![Docker](https://img.shields.io/badge/Docker-%232496ED?logo=docker&logoColor=white)
[![Discord](https://img.shields.io/badge/Discord-%235865F2.svg?logo=discord&logoColor=white)](https://discord.com/invite/yNN3wTbVAC) [![Discord](https://img.shields.io/badge/Discord-%235865F2.svg?logo=discord&logoColor=white)](https://discord.com/invite/yNN3wTbVAC)
![Discord](https://img.shields.io/discord/1185035164577972344) ![Discord](https://img.shields.io/discord/1185035164577972344)
[![Ask DeepWiki](https://deepwiki.com/badge.svg)](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
View File
@@ -4,11 +4,9 @@
# FastAPI Best Architecture # FastAPI Best Architecture
简体中文 | [English](./README.md)
企业级后端架构解决方案 企业级后端架构解决方案
**🔥持续更新维护中🔥** 简体中文 | [English](./README.md)
[![GitHub](https://img.shields.io/github/license/fastapi-practices/fastapi_best_architecture)](https://github.com/fastapi-practices/fastapi_best_architecture/blob/master/LICENSE) [![GitHub](https://img.shields.io/github/license/fastapi-practices/fastapi_best_architecture)](https://github.com/fastapi-practices/fastapi_best_architecture/blob/master/LICENSE)
[![Python](https://img.shields.io/badge/python-3.10%2B-blue)](https://www.python.org/downloads/) [![Python](https://img.shields.io/badge/python-3.10%2B-blue)](https://www.python.org/downloads/)
@@ -21,12 +19,10 @@
![Docker](https://img.shields.io/badge/Docker-%232496ED?logo=docker&logoColor=white) ![Docker](https://img.shields.io/badge/Docker-%232496ED?logo=docker&logoColor=white)
[![Discord](https://img.shields.io/badge/Discord-%235865F2.svg?logo=discord&logoColor=white)](https://discord.com/invite/yNN3wTbVAC) [![Discord](https://img.shields.io/badge/Discord-%235865F2.svg?logo=discord&logoColor=white)](https://discord.com/invite/yNN3wTbVAC)
![Discord](https://img.shields.io/discord/1185035164577972344) ![Discord](https://img.shields.io/discord/1185035164577972344)
[![Ask DeepWiki](https://deepwiki.com/badge.svg)](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/)
+3 -4
View File
@@ -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 -3
View File
@@ -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=['系统插件'])
+1 -1
View File
@@ -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')],
+3 -3
View File
@@ -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()
+11 -18
View File
@@ -22,18 +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('/changed', summary='插件状态是否变更', dependencies=[DependsJwtAuth]) @router.get('/changes', summary='插件状态是否变更', dependencies=[DependsJwtAuth])
async def plugin_changed() -> ResponseSchemaModel[bool]: async def plugin_changed() -> ResponseSchemaModel[bool]:
plugins = await plugin_service.changed() plugins = await plugin_service.changed()
return response_base.success(data=bool(plugins)) 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,
], ],
) )
@@ -43,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,
], ],
) )
@@ -57,40 +57,33 @@ async def install_git_plugin(repo_url: Annotated[str, Query(description='插件
@router.delete( @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.get( @router.get('/{plugin}', summary='打包并下载插件', dependencies=[DependsJwtAuth])
'/zip/{plugin}',
summary='打包插件',
dependencies=[
Depends(RequestPermission('sys:plugin:zip')),
DependsRBAC,
],
)
async def build_plugin(plugin: Annotated[str, Path(description='插件名称')]) -> StreamingResponse: async def build_plugin(plugin: Annotated[str, Path(description='插件名称')]) -> StreamingResponse:
bio = await plugin_service.build(plugin=plugin) bio = await plugin_service.build(plugin=plugin)
return StreamingResponse( return StreamingResponse(
+3 -11
View File
@@ -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')),
+23 -47
View File
@@ -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,
+4 -9
View File
@@ -2,9 +2,8 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from typing import Sequence from typing import Sequence
from sqlalchemy import Select, and_, desc, select from sqlalchemy import Select
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import noload
from sqlalchemy_crud_plus import CRUDPlus from sqlalchemy_crud_plus import CRUDPlus
from backend.app.admin.model import DataRule from backend.app.admin.model import DataRule
@@ -31,16 +30,12 @@ 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)) filters = {}
filters = []
if name is not None: if name is not None:
filters.append(self.model.name.like(f'%{name}%')) filters['name__like'] = f'%{name}%'
if filters: return await self.select_order('id', load_strategies={'scopes': 'noload'}, **filters)
stmt = stmt.where(and_(*filters))
return stmt
async def get_by_name(self, db: AsyncSession, name: str) -> DataRule | None: async def get_by_name(self, db: AsyncSession, name: str) -> DataRule | None:
""" """
+17 -17
View File
@@ -1,8 +1,9 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from sqlalchemy import Select, and_, desc, select from typing import Sequence
from sqlalchemy import Select, select
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import noload, selectinload
from sqlalchemy_crud_plus import CRUDPlus from sqlalchemy_crud_plus import CRUDPlus
from backend.app.admin.model import DataRule, DataScope from backend.app.admin.model import DataRule, DataScope
@@ -40,9 +41,16 @@ class CRUDDataScope(CRUDPlus[DataScope]):
:param pk: 范围 ID :param pk: 范围 ID
:return: :return:
""" """
stmt = select(self.model).options(selectinload(self.model.rules)).where(self.model.id == pk) return await self.select_model(db, pk, load_strategies=['rules'])
data_scope = await db.execute(stmt)
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:
""" """
@@ -52,22 +60,14 @@ class CRUDDataScope(CRUDPlus[DataScope]):
:param status: 范围状态 :param status: 范围状态
:return: :return:
""" """
stmt = ( filters = {}
select(self.model)
.options(noload(self.model.rules), noload(self.model.roles))
.order_by(desc(self.model.created_time))
)
filters = []
if name is not None: if name is not None:
filters.append(self.model.name.like(f'%{name}%')) filters['name__like'] = f'%{name}%'
if status is not None: if status is not None:
filters.append(self.model.status == status) filters['status'] = status
if filters: return await self.select_order('id', load_strategies={'rules': 'noload', 'roles': 'noload'}, **filters)
stmt = stmt.where(and_(*filters))
return stmt
async def create(self, db: AsyncSession, obj: CreateDataScopeParam) -> None: async def create(self, db: AsyncSession, obj: CreateDataScopeParam) -> None:
""" """
+11 -14
View File
@@ -3,9 +3,7 @@
from typing import Sequence from typing import Sequence
from fastapi import Request from fastapi import Request
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload
from sqlalchemy_crud_plus import CRUDPlus from sqlalchemy_crud_plus import CRUDPlus
from backend.app.admin.model import Dept from backend.app.admin.model import Dept
@@ -56,16 +54,19 @@ class CRUDDept(CRUDPlus[Dept]):
:param status: 部门状态 :param status: 部门状态
:return: :return:
""" """
filters = {'del_flag__eq': 0} filters = {'del_flag': 0}
if name is not None: if name is not None:
filters.update(name__like=f'%{name}%') filters['name__like'] = f'%{name}%'
if leader is not None: if leader is not None:
filters.update(leader__like=f'%{leader}%') filters['leader__like'] = f'%{leader}%'
if phone is not None: if phone is not None:
filters.update(phone__startswith=phone) filters['phone__startswith'] = phone
if status is not None: if status is not None:
filters.update(status=status) filters['status'] = status
return await self.select_models_order(db, 'sort', None, await filter_data_permission(db, request), **filters)
data_filtered = await filter_data_permission(db, request)
return await self.select_models_order(db, 'sort', 'desc', data_filtered, **filters)
async def create(self, db: AsyncSession, obj: CreateDeptParam) -> None: async def create(self, db: AsyncSession, obj: CreateDeptParam) -> None:
""" """
@@ -106,9 +107,7 @@ class CRUDDept(CRUDPlus[Dept]):
:param dept_id: 部门 ID :param dept_id: 部门 ID
:return: :return:
""" """
stmt = select(self.model).options(selectinload(self.model.users)).where(self.model.id == dept_id) return await self.select_model(db, dept_id, load_strategies=['users'])
result = await db.execute(stmt)
return result.scalars().first()
async def get_children(self, db: AsyncSession, dept_id: int) -> Sequence[Dept | None]: async def get_children(self, db: AsyncSession, dept_id: int) -> Sequence[Dept | None]:
""" """
@@ -118,9 +117,7 @@ class CRUDDept(CRUDPlus[Dept]):
:param dept_id: 部门 ID :param dept_id: 部门 ID
:return: :return:
""" """
stmt = select(self.model).where(self.model.parent_id == dept_id, self.model.del_flag == 0) return await self.select_models(db, parent_id=dept_id, del_flag=0)
result = await db.execute(stmt)
return result.scalars().all()
dept_dao: CRUDDept = CRUDDept(Dept) dept_dao: CRUDDept = CRUDDept(Dept)
+5 -3
View File
@@ -21,12 +21,14 @@ class CRUDLoginLog(CRUDPlus[LoginLog]):
:return: :return:
""" """
filters = {} filters = {}
if username is not None: if username is not None:
filters.update(username__like=f'%{username}%') filters['username__like'] = f'%{username}%'
if status is not None: if status is not None:
filters.update(status=status) filters['status'] = status
if ip is not None: if ip is not None:
filters.update(ip__like=f'%{ip}%') filters['ip__like'] = f'%{ip}%'
return await self.select_order('created_time', 'desc', **filters) return await self.select_order('created_time', 'desc', **filters)
async def create(self, db: AsyncSession, obj: CreateLoginLogParam) -> None: async def create(self, db: AsyncSession, obj: CreateLoginLogParam) -> None:
+11 -14
View File
@@ -2,9 +2,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from typing import Sequence from typing import Sequence
from sqlalchemy import and_, asc, select
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload
from sqlalchemy_crud_plus import CRUDPlus from sqlalchemy_crud_plus import CRUDPlus
from backend.app.admin.model import Menu from backend.app.admin.model import Menu
@@ -44,13 +42,15 @@ class CRUDMenu(CRUDPlus[Menu]):
:return: :return:
""" """
filters = {} filters = {}
if title is not None: if title is not None:
filters.update(title=f'%{title}%') filters['title__like'] = f'%{title}%'
if status is not None: if status is not None:
filters.update(status=status) filters['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]:
""" """
获取角色菜单列表 获取角色菜单列表
@@ -59,13 +59,12 @@ class CRUDMenu(CRUDPlus[Menu]):
:param menu_ids: 菜单 ID 列表 :param menu_ids: 菜单 ID 列表
:return: :return:
""" """
stmt = select(self.model).order_by(asc(self.model.sort)) filters = {'type__in': [0, 1, 3, 4]}
filters = [self.model.type.in_([0, 1])]
if not superuser: if not superuser:
filters.append(self.model.id.in_(menu_ids)) filters['id__in'] = menu_ids
stmt = stmt.where(and_(*filters))
menu = await db.execute(stmt) return await self.select_models_order(db, 'sort', 'asc', **filters)
return menu.scalars().all()
async def create(self, db: AsyncSession, obj: CreateMenuParam) -> None: async def create(self, db: AsyncSession, obj: CreateMenuParam) -> None:
""" """
@@ -106,9 +105,7 @@ class CRUDMenu(CRUDPlus[Menu]):
:param menu_id: 菜单 ID :param menu_id: 菜单 ID
:return: :return:
""" """
stmt = select(self.model).options(selectinload(self.model.children)).where(self.model.id == menu_id) menu = await self.select_model(db, menu_id, load_strategies=['children'])
result = await db.execute(stmt)
menu = result.scalars().first()
return menu.children return menu.children
+5 -3
View File
@@ -21,12 +21,14 @@ class CRUDOperaLogDao(CRUDPlus[OperaLog]):
:return: :return:
""" """
filters = {} filters = {}
if username is not None: if username is not None:
filters.update(username__like=f'%{username}%') filters['username__like'] = f'%{username}%'
if status is not None: if status is not None:
filters.update(status=status) filters['status__eq'] = status
if ip is not None: if ip is not None:
filters.update(ip__like=f'%{ip}%') filters['ip__like'] = f'%{ip}%'
return await self.select_order('created_time', 'desc', **filters) return await self.select_order('created_time', 'desc', **filters)
async def create(self, db: AsyncSession, obj: CreateOperaLogParam) -> None: async def create(self, db: AsyncSession, obj: CreateOperaLogParam) -> None:
+16 -34
View File
@@ -2,12 +2,11 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from typing import Sequence from typing import Sequence
from sqlalchemy import Select, and_, desc, select from sqlalchemy import Select, select
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
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,
@@ -37,13 +36,7 @@ class CRUDRole(CRUDPlus[Role]):
:param role_id: 角色 ID :param role_id: 角色 ID
:return: :return:
""" """
stmt = ( return await self.select_model(db, role_id, load_strategies=['menus', 'scopes'])
select(self.model)
.options(selectinload(self.model.menus), selectinload(self.model.scopes))
.where(self.model.id == role_id)
)
role = await db.execute(stmt)
return role.scalars().first()
async def get_all(self, db: AsyncSession) -> Sequence[Role]: async def get_all(self, db: AsyncSession) -> Sequence[Role]:
""" """
@@ -54,18 +47,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:
""" """
获取角色列表 获取角色列表
@@ -74,22 +55,23 @@ class CRUDRole(CRUDPlus[Role]):
:param status: 角色状态 :param status: 角色状态
:return: :return:
""" """
stmt = (
select(self.model)
.options(noload(self.model.users), noload(self.model.menus), noload(self.model.scopes))
.order_by(desc(self.model.created_time))
)
filters = [] filters = {}
if name is not None: if name is not None:
filters.append(self.model.name.like(f'%{name}%')) filters['name__like'] = f'%{name}%'
if status is not None: if status is not None:
filters.append(self.model.status == status) filters['status'] = status
if filters: return await self.select_order(
stmt = stmt.where(and_(*filters)) 'id',
load_strategies={
return stmt 'users': 'noload',
'menus': 'noload',
'scopes': 'noload',
},
**filters,
)
async def get_by_name(self, db: AsyncSession, name: str) -> Role | None: async def get_by_name(self, db: AsyncSession, name: str) -> Role | None:
""" """
+63 -83
View File
@@ -2,7 +2,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import bcrypt import bcrypt
from sqlalchemy import and_, desc, select from sqlalchemy import 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
from sqlalchemy.sql import Select from sqlalchemy.sql import 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: 电子邮箱
@@ -185,30 +171,27 @@ class CRUDUser(CRUDPlus[User]):
:param status: 用户状态 :param status: 用户状态
:return: :return:
""" """
stmt = ( filters = {}
select(self.model)
.options( if dept:
filters['dept_id'] = dept
if username:
filters['username__like'] = f'%{username}%'
if phone:
filters['phone_like'] = f'%{phone}%'
if status is not None:
filters['status'] = status
return await self.select_order(
'id',
'desc',
load_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)),
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.scopes)),
) ],
.order_by(desc(self.model.join_time)) **filters,
) )
filters = []
if dept:
filters.append(self.model.dept_id == dept)
if username:
filters.append(self.model.username.like(f'%{username}%'))
if phone:
filters.append(self.model.phone.like(f'%{phone}%'))
if status is not None:
filters.append(self.model.status == status)
if filters:
stmt = stmt.where(and_(*filters))
return stmt
async def get_super(self, db: AsyncSession, user_id: int) -> bool: async def get_super(self, db: AsyncSession, user_id: int) -> bool:
""" """
获取用户是否为超级管理员 获取用户是否为超级管理员
@@ -308,22 +291,19 @@ class CRUDUser(CRUDPlus[User]):
:param username: 用户名 :param username: 用户名
:return: :return:
""" """
stmt = select(self.model).options( filters = {}
selectinload(self.model.dept),
selectinload(self.model.roles).options(selectinload(Role.menus), selectinload(Role.scopes)),
)
filters = []
if user_id: if user_id:
filters.append(self.model.id == user_id) filters['id'] = user_id
if username: if username:
filters.append(self.model.username == username) filters['username'] = username
if filters: return await self.select_model_by_column(
stmt = stmt.where(and_(*filters)) db,
load_options=[selectinload(self.model.roles).options(selectinload(Role.menus), selectinload(Role.scopes))],
user = await db.execute(stmt) load_strategies=['dept'],
return user.scalars().first() **filters,
)
user_dao: CRUDUser = CRUDUser(User) user_dao: CRUDUser = CRUDUser(User)
+4 -6
View File
@@ -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')
+3 -3
View File
@@ -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')
+2 -2
View File
@@ -4,7 +4,7 @@ from __future__ import annotations
from typing import TYPE_CHECKING, Optional from typing import TYPE_CHECKING, Optional
from sqlalchemy import Boolean, ForeignKey, String from sqlalchemy import BigInteger, Boolean, ForeignKey, String
from sqlalchemy.dialects.postgresql import INTEGER from sqlalchemy.dialects.postgresql import INTEGER
from sqlalchemy.orm import Mapped, mapped_column, relationship from sqlalchemy.orm import Mapped, mapped_column, relationship
@@ -32,7 +32,7 @@ class Dept(Base):
# 父级部门一对多 # 父级部门一对多
parent_id: Mapped[int | None] = mapped_column( parent_id: Mapped[int | None] = mapped_column(
ForeignKey('sys_dept.id', ondelete='SET NULL'), default=None, index=True, comment='父部门ID' BigInteger, ForeignKey('sys_dept.id', ondelete='SET NULL'), default=None, index=True, comment='父部门ID'
) )
parent: Mapped[Optional['Dept']] = relationship(init=False, back_populates='children', remote_side=[id]) parent: Mapped[Optional['Dept']] = relationship(init=False, back_populates='children', remote_side=[id])
children: Mapped[Optional[list['Dept']]] = relationship(init=False, back_populates='parent') children: Mapped[Optional[list['Dept']]] = relationship(init=False, back_populates='parent')
+30 -10
View File
@@ -1,35 +1,55 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from sqlalchemy import INT, Column, ForeignKey, Integer, Table from sqlalchemy import BigInteger, Column, ForeignKey, Table
from backend.common.model import MappedBase from backend.common.model import MappedBase
sys_user_role = Table( sys_user_role = Table(
'sys_user_role', 'sys_user_role',
MappedBase.metadata, MappedBase.metadata,
Column('id', INT, primary_key=True, unique=True, index=True, autoincrement=True, comment='主键ID'), Column('id', BigInteger, primary_key=True, unique=True, index=True, autoincrement=True, comment='主键ID'),
Column('user_id', Integer, ForeignKey('sys_user.id', ondelete='CASCADE'), primary_key=True, comment='用户ID'), Column('user_id', BigInteger, ForeignKey('sys_user.id', ondelete='CASCADE'), primary_key=True, comment='用户ID'),
Column('role_id', Integer, ForeignKey('sys_role.id', ondelete='CASCADE'), primary_key=True, comment='角色ID'), Column('role_id', BigInteger, ForeignKey('sys_role.id', ondelete='CASCADE'), primary_key=True, comment='角色ID'),
) )
sys_role_menu = Table( sys_role_menu = Table(
'sys_role_menu', 'sys_role_menu',
MappedBase.metadata, MappedBase.metadata,
Column('id', INT, primary_key=True, unique=True, index=True, autoincrement=True, comment='主键ID'), Column('id', BigInteger, primary_key=True, unique=True, index=True, autoincrement=True, comment='主键ID'),
Column('role_id', Integer, ForeignKey('sys_role.id', ondelete='CASCADE'), primary_key=True, comment='角色ID'), Column('role_id', BigInteger, ForeignKey('sys_role.id', ondelete='CASCADE'), primary_key=True, comment='角色ID'),
Column('menu_id', Integer, ForeignKey('sys_menu.id', ondelete='CASCADE'), primary_key=True, comment='菜单ID'), Column('menu_id', BigInteger, ForeignKey('sys_menu.id', ondelete='CASCADE'), primary_key=True, comment='菜单ID'),
) )
sys_role_data_scope = Table( sys_role_data_scope = Table(
'sys_role_data_scope', 'sys_role_data_scope',
MappedBase.metadata, MappedBase.metadata,
Column('id', INT, primary_key=True, unique=True, index=True, autoincrement=True, comment='主键 ID'), Column('id', BigInteger, primary_key=True, unique=True, index=True, autoincrement=True, comment='主键 ID'),
Column('role_id', Integer, ForeignKey('sys_role.id', ondelete='CASCADE'), primary_key=True, comment='角色 ID'), Column('role_id', BigInteger, ForeignKey('sys_role.id', ondelete='CASCADE'), primary_key=True, comment='角色 ID'),
Column( Column(
'data_scope_id', 'data_scope_id',
Integer, BigInteger,
ForeignKey('sys_data_scope.id', ondelete='CASCADE'), ForeignKey('sys_data_scope.id', ondelete='CASCADE'),
primary_key=True, primary_key=True,
comment='数据范围 ID', comment='数据范围 ID',
), ),
) )
sys_data_scope_rule = Table(
'sys_data_scope_rule',
MappedBase.metadata,
Column('id', BigInteger, primary_key=True, unique=True, index=True, autoincrement=True, comment='主键ID'),
Column(
'data_scope_id',
BigInteger,
ForeignKey('sys_data_scope.id', ondelete='CASCADE'),
primary_key=True,
comment='数据范围 ID',
),
Column(
'data_rule_id',
BigInteger,
ForeignKey('sys_data_rule.id', ondelete='CASCADE'),
primary_key=True,
comment='数据规则 ID',
),
)
+4 -4
View File
@@ -4,7 +4,7 @@ from __future__ import annotations
from typing import TYPE_CHECKING, Optional from typing import TYPE_CHECKING, Optional
from sqlalchemy import ForeignKey, String from sqlalchemy import BigInteger, ForeignKey, String
from sqlalchemy.dialects.mysql import LONGTEXT from sqlalchemy.dialects.mysql import LONGTEXT
from sqlalchemy.dialects.postgresql import TEXT from sqlalchemy.dialects.postgresql import TEXT
from sqlalchemy.orm import Mapped, mapped_column, relationship from sqlalchemy.orm import Mapped, mapped_column, relationship
@@ -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正常)')
@@ -42,7 +42,7 @@ class Menu(Base):
# 父级菜单一对多 # 父级菜单一对多
parent_id: Mapped[int | None] = mapped_column( parent_id: Mapped[int | None] = mapped_column(
ForeignKey('sys_menu.id', ondelete='SET NULL'), default=None, index=True, comment='父菜单ID' BigInteger, ForeignKey('sys_menu.id', ondelete='SET NULL'), default=None, index=True, comment='父菜单ID'
) )
parent: Mapped[Optional['Menu']] = relationship(init=False, back_populates='children', remote_side=[id]) parent: Mapped[Optional['Menu']] = relationship(init=False, back_populates='children', remote_side=[id])
children: Mapped[Optional[list['Menu']]] = relationship(init=False, back_populates='parent') children: Mapped[Optional[list['Menu']]] = relationship(init=False, back_populates='parent')
+5 -2
View File
@@ -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='备注'
) )
+7 -7
View File
@@ -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='注册时间'
) )
+2 -2
View File
@@ -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='状态')
+1
View File
@@ -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='备注')
-6
View File
@@ -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):
"""令牌详情""" """令牌详情"""
+12 -22
View File
@@ -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='是否管理员')
+14 -8
View File
@@ -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:
""" """
+15 -8
View File
@@ -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
+9 -16
View File
@@ -58,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='插件压缩包内缺少必要文件')
@@ -86,8 +79,8 @@ 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)
@@ -182,7 +175,7 @@ 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) bio.seek(0)
return bio return bio
+4 -19
View File
@@ -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:
+38 -94
View File
@@ -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:
""" """
+9
View File
@@ -64,3 +64,12 @@ class TokenPayload:
@dataclasses.dataclass @dataclasses.dataclass
class UploadUrl: class UploadUrl:
url: str url: str
@dataclasses.dataclass
class SnowflakeInfo:
timestamp: int
datetime: str
cluster_id: int
node_id: int
sequence: int
+2
View File
@@ -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):
+1 -1
View File
@@ -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)
+28 -2
View File
@@ -3,17 +3,43 @@
from datetime import datetime from datetime import datetime
from typing import Annotated from typing import Annotated
from sqlalchemy import DateTime from sqlalchemy import BigInteger, DateTime
from sqlalchemy.ext.asyncio import AsyncAttrs from sqlalchemy.ext.asyncio import AsyncAttrs
from sqlalchemy.orm import DeclarativeBase, Mapped, MappedAsDataclass, declared_attr, mapped_column from sqlalchemy.orm import DeclarativeBase, Mapped, MappedAsDataclass, declared_attr, mapped_column
from backend.utils.snowflake import snowflake
from backend.utils.timezone import timezone from backend.utils.timezone import timezone
# 通用 Mapped 类型主键, 需手动添加,参考以下使用方式 # 通用 Mapped 类型主键, 需手动添加,参考以下使用方式
# MappedBase -> id: Mapped[id_key] # MappedBase -> id: Mapped[id_key]
# DataClassBase && Base -> id: Mapped[id_key] = mapped_column(init=False) # DataClassBase && Base -> id: Mapped[id_key] = mapped_column(init=False)
id_key = Annotated[ id_key = Annotated[
int, mapped_column(primary_key=True, index=True, autoincrement=True, sort_order=-999, comment='主键 ID') int,
mapped_column(
BigInteger,
primary_key=True,
unique=True,
index=True,
autoincrement=True,
sort_order=-999,
comment='主键 ID',
),
]
# 雪花算法 Mapped 类型主键,使用方法与 id_key 相同
# 详情:https://fastapi-practices.github.io/fastapi_best_architecture_docs/backend/reference/pk.html
snowflake_id_key = Annotated[
int,
mapped_column(
BigInteger,
primary_key=True,
unique=True,
index=True,
default=snowflake.generate,
sort_order=-999,
comment='雪花算法主键 ID',
),
] ]
+22 -19
View File
@@ -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:
+9 -4
View File
@@ -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:
+1 -1
View File
@@ -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'
+1 -1
View File
@@ -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')
+6 -6
View File
@@ -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(
@@ -2,7 +2,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from typing import TYPE_CHECKING, Union from typing import TYPE_CHECKING, Union
from sqlalchemy import ForeignKey, String from sqlalchemy import BigInteger, ForeignKey, String
from sqlalchemy.dialects.mysql import LONGTEXT from sqlalchemy.dialects.mysql import LONGTEXT
from sqlalchemy.dialects.postgresql import TEXT from sqlalchemy.dialects.postgresql import TEXT
from sqlalchemy.orm import Mapped, mapped_column, relationship from sqlalchemy.orm import Mapped, mapped_column, relationship
@@ -33,6 +33,6 @@ class GenColumn(DataClassBase):
# 代码生成业务模型一对多 # 代码生成业务模型一对多
gen_business_id: Mapped[int] = mapped_column( gen_business_id: Mapped[int] = mapped_column(
ForeignKey('gen_business.id', ondelete='CASCADE'), default=0, comment='代码生成业务ID' BigInteger, ForeignKey('gen_business.id', ondelete='CASCADE'), default=0, comment='代码生成业务ID'
) )
gen_business: Mapped[Union['GenBusiness', None]] = relationship(init=False, back_populates='gen_column') gen_business: Mapped[Union['GenBusiness', None]] = relationship(init=False, back_populates='gen_column')
@@ -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:
+4 -2
View File
@@ -64,10 +64,12 @@ class CRUDConfig(CRUDPlus[Config]):
:return: :return:
""" """
filters = {'type__not_in': settings.CONFIG_BUILT_IN_TYPES} filters = {'type__not_in': settings.CONFIG_BUILT_IN_TYPES}
if name is not None: if name is not None:
filters.update(name__like=f'%{name}%') filters['name__like'] = f'%{name}%'
if type is not None: if type is not None:
filters.update(type__like=f'%{type}%') filters['type__like'] = f'%{type}%'
return await self.select_order('created_time', 'desc', **filters) return await self.select_order('created_time', 'desc', **filters)
async def create(self, db: AsyncSession, obj: CreateConfigParam) -> None: async def create(self, db: AsyncSession, obj: CreateConfigParam) -> None:
+7 -14
View File
@@ -1,8 +1,7 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from sqlalchemy import Select, and_, desc, select from sqlalchemy import Select
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import noload, selectinload
from sqlalchemy_crud_plus import CRUDPlus from sqlalchemy_crud_plus import CRUDPlus
from backend.plugin.dict.model import DictData from backend.plugin.dict.model import DictData
@@ -31,20 +30,16 @@ class CRUDDictData(CRUDPlus[DictData]):
:param status: 字典状态 :param status: 字典状态
:return: :return:
""" """
stmt = select(self.model).options(noload(self.model.type)).order_by(desc(self.model.sort)) filters = {}
filters = []
if label is not None: if label is not None:
filters.append(self.model.label.like(f'%{label}%')) filters['label__like'] = f'%{label}%'
if value is not None: if value is not None:
filters.append(self.model.value.like(f'%{value}%')) filters['value__like'] = f'%{value}%'
if status is not None: if status is not None:
filters.append(self.model.status == status) filters['status'] = status
if filters: return await self.select_order('id', 'desc', *filters)
stmt = stmt.where(and_(*filters))
return stmt
async def get_by_label(self, db: AsyncSession, label: str) -> DictData | None: async def get_by_label(self, db: AsyncSession, label: str) -> DictData | None:
""" """
@@ -95,9 +90,7 @@ class CRUDDictData(CRUDPlus[DictData]):
:param pk: 字典数据 ID :param pk: 字典数据 ID
:return: :return:
""" """
stmt = select(self.model).options(selectinload(self.model.type)).where(self.model.id == pk) return await self.select_model(db, pk, load_strategies=['type'])
dict_data = await db.execute(stmt)
return dict_data.scalars().first()
dict_data_dao: CRUDDictData = CRUDDictData(DictData) dict_data_dao: CRUDDictData = CRUDDictData(DictData)
+6 -11
View File
@@ -1,8 +1,7 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from sqlalchemy import Select, and_, desc, select from sqlalchemy import Select
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import noload
from sqlalchemy_crud_plus import CRUDPlus from sqlalchemy_crud_plus import CRUDPlus
from backend.plugin.dict.model import DictType from backend.plugin.dict.model import DictType
@@ -31,20 +30,16 @@ class CRUDDictType(CRUDPlus[DictType]):
:param status: 字典状态 :param status: 字典状态
:return: :return:
""" """
stmt = select(self.model).options(noload(self.model.datas)).order_by(desc(self.model.created_time)) filters = {}
filters = []
if name is not None: if name is not None:
filters.append(self.model.name.like(f'%{name}%')) filters['name__like'] = f'%{name}%'
if code is not None: if code is not None:
filters.append(self.model.code.like(f'%{code}%')) filters['code__like'] = f'%{code}%'
if status is not None: if status is not None:
filters.append(self.model.status == status) filters['status'] = status
if filters: return await self.select_order('id', 'desc', **filters)
stmt = stmt.where(and_(*filters))
return stmt
async def get_by_code(self, db: AsyncSession, code: str) -> DictType | None: async def get_by_code(self, db: AsyncSession, code: str) -> DictType | None:
""" """
+3 -1
View File
@@ -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}'
)
+3 -1
View File
@@ -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}'
)
+13 -2
View File
@@ -10,9 +10,9 @@ from backend.plugin.oauth2.schema.user_social import CreateUserSocialParam
class CRUDUserSocial(CRUDPlus[UserSocial]): class CRUDUserSocial(CRUDPlus[UserSocial]):
"""用户社交账号数据库操作类""" """用户社交账号数据库操作类"""
async def get(self, db: AsyncSession, pk: int, source: str) -> 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
@@ -21,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:
""" """
创建用户社交账号绑定 创建用户社交账号绑定
+4 -8
View File
@@ -4,7 +4,7 @@ from __future__ import annotations
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from sqlalchemy import ForeignKey, String from sqlalchemy import BigInteger, ForeignKey, String
from sqlalchemy.orm import Mapped, mapped_column, relationship from sqlalchemy.orm import Mapped, mapped_column, relationship
from backend.common.model import Base, id_key from backend.common.model import Base, id_key
@@ -19,15 +19,11 @@ class UserSocial(Base):
__tablename__ = 'sys_user_social' __tablename__ = 'sys_user_social'
id: Mapped[id_key] = mapped_column(init=False) 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='第三方用户来源') source: Mapped[str] = mapped_column(String(20), comment='第三方用户来源')
open_id: Mapped[str | None] = mapped_column(String(20), default=None, comment='第三方用户 open id')
sid: 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( user_id: Mapped[int] = mapped_column(
ForeignKey('sys_user.id', ondelete='SET NULL'), default=None, comment='用户关联ID' BigInteger, ForeignKey('sys_user.id', ondelete='CASCADE'), comment='用户关联ID'
) )
user: Mapped[User | None] = relationship(init=False, backref='socials') user: Mapped[User | None] = relationship(init=False, backref='socials')
+1 -1
View File
@@ -1,6 +1,6 @@
[plugin] [plugin]
summary = 'OAuth 2.0' summary = 'OAuth 2.0'
version = '0.0.1' version = '0.0.3'
description = '通过 OAuth 2.0 的方式登录系统' description = '通过 OAuth 2.0 的方式登录系统'
author = 'wu-clan' author = 'wu-clan'
+1 -5
View File
@@ -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')
sid: 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):
+44 -35
View File
@@ -7,10 +7,9 @@ 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.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.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
@@ -43,55 +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_nickname = user.get('name') nickname = user.get('nickname')
email = user.get('email')
avatar = user.get('avatar_url')
social_username = user.get('username')
if social == UserSocialType.github: if social == UserSocialType.github:
social_username = user.get('login') sid = user.get('id')
username = user.get('login')
nickname = user.get('name')
social_email = user.get('email')
if social == UserSocialType.linux_do: if social == UserSocialType.linux_do:
social_email = f'{social_username}@linux.do' sid = user.get('id')
if not social_email: nickname = user.get('name')
raise AuthorizationError(msg=f'授权失败,{social.value} 账户未绑定邮箱')
# 创建系统用户 sys_user = None
sys_user = await user_dao.check_email(db, social_email) user_social = await user_social_dao.get_by_sid(db, str(sid), str(social.value))
if not sys_user:
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, sid=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(
@@ -115,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
+1 -1
View File
@@ -22,7 +22,7 @@ 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 RedisCli, 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._await import run_await
from backend.utils.import_parse import import_module_cached from backend.utils.import_parse import import_module_cached
+115 -120
View File
@@ -1,6 +1,6 @@
create table gen_business create table gen_business
( (
id int auto_increment comment '主键 ID' id bigint auto_increment comment '主键 ID'
primary key, primary key,
app_name varchar(50) not null comment '应用名称(英文)', app_name varchar(50) not null comment '应用名称(英文)',
table_name varchar(255) not null comment '表名称(英文)', table_name varchar(255) not null comment '表名称(英文)',
@@ -15,17 +15,16 @@ create table gen_business
remark longtext null comment '备注', remark longtext null comment '备注',
created_time datetime not null comment '创建时间', created_time datetime not null comment '创建时间',
updated_time datetime null comment '更新时间', updated_time datetime null comment '更新时间',
constraint ix_gen_business_id
unique (id),
constraint table_name constraint table_name
unique (table_name) unique (table_name)
) )
comment '代码生成业务表'; comment '代码生成业务表';
create index ix_gen_business_id
on gen_business (id);
create table gen_column create table gen_column
( (
id int auto_increment comment '主键 ID' id bigint auto_increment comment '主键 ID'
primary key, primary key,
name varchar(50) not null comment '列名称', name varchar(50) not null comment '列名称',
comment varchar(255) null comment '列描述', comment varchar(255) null comment '列描述',
@@ -36,7 +35,9 @@ create table gen_column
length int not null comment '列长度', length int not null comment '列长度',
is_pk tinyint(1) not null comment '是否主键', is_pk tinyint(1) not null comment '是否主键',
is_nullable tinyint(1) not null comment '是否可为空', is_nullable tinyint(1) not null comment '是否可为空',
gen_business_id int not null comment '代码生成业务ID', gen_business_id bigint not null comment '代码生成业务ID',
constraint ix_gen_column_id
unique (id),
constraint gen_column_ibfk_1 constraint gen_column_ibfk_1
foreign key (gen_business_id) references gen_business (id) foreign key (gen_business_id) references gen_business (id)
on delete cascade on delete cascade
@@ -46,12 +47,9 @@ create table gen_column
create index gen_business_id create index gen_business_id
on gen_column (gen_business_id); on gen_column (gen_business_id);
create index ix_gen_column_id
on gen_column (id);
create table sys_config create table sys_config
( (
id int auto_increment comment '主键 ID' id bigint auto_increment comment '主键 ID'
primary key, primary key,
name varchar(20) not null comment '名称', name varchar(20) not null comment '名称',
type varchar(20) null comment '类型', type varchar(20) null comment '类型',
@@ -61,30 +59,16 @@ create table sys_config
remark longtext null comment '备注', remark longtext null comment '备注',
created_time datetime not null comment '创建时间', created_time datetime not null comment '创建时间',
updated_time datetime null comment '更新时间', updated_time datetime null comment '更新时间',
constraint ix_sys_config_id
unique (id),
constraint `key` constraint `key`
unique (`key`) unique (`key`)
) )
comment '参数配置表'; comment '参数配置表';
create index ix_sys_config_id
on sys_config (id);
create table sys_data_scope
(
id int auto_increment comment '主键 ID'
primary key,
name varchar(50) not null comment '名称',
status int not null comment '状态(0停用 1正常)',
created_time datetime not null comment '创建时间',
updated_time datetime null comment '更新时间',
constraint name
unique (name)
)
comment '数据范围表';
create table sys_data_rule create table sys_data_rule
( (
id int auto_increment comment '主键 ID' id bigint auto_increment comment '主键 ID'
primary key, primary key,
name varchar(500) not null comment '名称', name varchar(500) not null comment '名称',
model varchar(50) not null comment 'SQLA 模型名,对应 DATA_PERMISSION_MODELS 键名', model varchar(50) not null comment 'SQLA 模型名,对应 DATA_PERMISSION_MODELS 键名',
@@ -92,29 +76,55 @@ create table sys_data_rule
operator int not null comment '运算符(0and、1or', operator int not null comment '运算符(0and、1or',
expression int not null comment '表达式(0==、1!=、2>、3>=、4<、5<=、6in、7not_in', expression int not null comment '表达式(0==、1!=、2>、3>=、4<、5<=、6in、7not_in',
value varchar(255) not null comment '规则值', value varchar(255) not null comment '规则值',
scope_id int null comment '数据范围关联 ID',
created_time datetime not null comment '创建时间', created_time datetime not null comment '创建时间',
updated_time datetime null comment '更新时间', updated_time datetime null comment '更新时间',
constraint ix_sys_data_rule_id
unique (id),
constraint name constraint name
unique (name), unique (name)
constraint sys_data_rule_ibfk_1
foreign key (scope_id) references sys_data_scope (id)
on delete set null
) )
comment '数据规则表'; comment '数据规则表';
create index ix_sys_data_rule_id create table sys_data_scope
on sys_data_rule (id); (
id bigint auto_increment comment '主键 ID'
primary key,
name varchar(50) not null comment '名称',
status int not null comment '状态(0停用 1正常)',
created_time datetime not null comment '创建时间',
updated_time datetime null comment '更新时间',
constraint ix_sys_data_scope_id
unique (id),
constraint name
unique (name)
)
comment '数据范围表';
create index scope_id create table sys_data_scope_rule
on sys_data_rule (scope_id); (
id bigint auto_increment comment '主键ID',
data_scope_id bigint not null comment '数据范围 ID',
data_rule_id bigint not null comment '数据规则 ID',
primary key (id, data_scope_id, data_rule_id),
constraint ix_sys_data_scope_rule_id
unique (id),
constraint sys_data_scope_rule_ibfk_1
foreign key (data_scope_id) references sys_data_scope (id)
on delete cascade,
constraint sys_data_scope_rule_ibfk_2
foreign key (data_rule_id) references sys_data_rule (id)
on delete cascade
);
create index ix_sys_data_scope_id create index data_rule_id
on sys_data_scope (id); on sys_data_scope_rule (data_rule_id);
create index data_scope_id
on sys_data_scope_rule (data_scope_id);
create table sys_dept create table sys_dept
( (
id int auto_increment comment '主键 ID' id bigint auto_increment comment '主键 ID'
primary key, primary key,
name varchar(50) not null comment '部门名称', name varchar(50) not null comment '部门名称',
sort int not null comment '排序', sort int not null comment '排序',
@@ -123,24 +133,23 @@ create table sys_dept
email varchar(50) null comment '邮箱', email varchar(50) null comment '邮箱',
status int not null comment '部门状态(0停用 1正常)', status int not null comment '部门状态(0停用 1正常)',
del_flag tinyint(1) not null comment '删除标志(0删除 1存在)', del_flag tinyint(1) not null comment '删除标志(0删除 1存在)',
parent_id int null comment '父部门ID', parent_id bigint null comment '父部门ID',
created_time datetime not null comment '创建时间', created_time datetime not null comment '创建时间',
updated_time datetime null comment '更新时间', updated_time datetime null comment '更新时间',
constraint ix_sys_dept_id
unique (id),
constraint sys_dept_ibfk_1 constraint sys_dept_ibfk_1
foreign key (parent_id) references sys_dept (id) foreign key (parent_id) references sys_dept (id)
on delete set null on delete set null
) )
comment '部门表'; comment '部门表';
create index ix_sys_dept_id
on sys_dept (id);
create index ix_sys_dept_parent_id create index ix_sys_dept_parent_id
on sys_dept (parent_id); on sys_dept (parent_id);
create table sys_dict_type create table sys_dict_type
( (
id int auto_increment comment '主键 ID' id bigint auto_increment comment '主键 ID'
primary key, primary key,
name varchar(32) not null comment '字典类型名称', name varchar(32) not null comment '字典类型名称',
code varchar(32) not null comment '字典类型编码', code varchar(32) not null comment '字典类型编码',
@@ -149,22 +158,26 @@ create table sys_dict_type
created_time datetime not null comment '创建时间', created_time datetime not null comment '创建时间',
updated_time datetime null comment '更新时间', updated_time datetime null comment '更新时间',
constraint code constraint code
unique (code) unique (code),
constraint ix_sys_dict_type_id
unique (id)
) )
comment '字典类型表'; comment '字典类型表';
create table sys_dict_data create table sys_dict_data
( (
id int auto_increment comment '主键 ID' id bigint auto_increment comment '主键 ID'
primary key, primary key,
label varchar(32) not null comment '字典标签', label varchar(32) not null comment '字典标签',
value varchar(32) not null comment '字典值', value varchar(32) not null comment '字典值',
sort int not null comment '排序', sort int not null comment '排序',
status int not null comment '状态(0停用 1正常)', status int not null comment '状态(0停用 1正常)',
remark longtext null comment '备注', remark longtext null comment '备注',
type_id int not null comment '字典类型关联ID', type_id bigint not null comment '字典类型关联ID',
created_time datetime not null comment '创建时间', created_time datetime not null comment '创建时间',
updated_time datetime null comment '更新时间', updated_time datetime null comment '更新时间',
constraint ix_sys_dict_data_id
unique (id),
constraint label constraint label
unique (label), unique (label),
constraint sys_dict_data_ibfk_1 constraint sys_dict_data_ibfk_1
@@ -173,18 +186,12 @@ create table sys_dict_data
) )
comment '字典数据表'; comment '字典数据表';
create index ix_sys_dict_data_id
on sys_dict_data (id);
create index type_id create index type_id
on sys_dict_data (type_id); on sys_dict_data (type_id);
create index ix_sys_dict_type_id
on sys_dict_type (id);
create table sys_login_log create table sys_login_log
( (
id int auto_increment comment '主键 ID' id bigint not null comment '雪花算法主键 ID'
primary key, primary key,
user_uuid varchar(50) not null comment '用户UUID', user_uuid varchar(50) not null comment '用户UUID',
username varchar(20) not null comment '用户名', username varchar(20) not null comment '用户名',
@@ -199,23 +206,22 @@ create table sys_login_log
device varchar(50) null comment '设备', device varchar(50) null comment '设备',
msg longtext not null comment '提示消息', msg longtext not null comment '提示消息',
login_time datetime not null comment '登录时间', login_time datetime not null comment '登录时间',
created_time datetime not null comment '创建时间' created_time datetime not null comment '创建时间',
constraint ix_sys_login_log_id
unique (id)
) )
comment '登录日志表'; comment '登录日志表';
create index ix_sys_login_log_id
on sys_login_log (id);
create table sys_menu create table sys_menu
( (
id int auto_increment comment '主键 ID' id bigint auto_increment comment '主键 ID'
primary key, primary key,
title varchar(50) not null comment '菜单标题', title varchar(50) not null comment '菜单标题',
name varchar(50) not null comment '菜单名称', name varchar(50) not null comment '菜单名称',
path varchar(200) not null comment '路由地址', path varchar(200) null comment '路由地址',
sort int not null comment '排序', sort int not null comment '排序',
icon varchar(100) null comment '菜单图标', icon varchar(100) null comment '菜单图标',
type int not null comment '菜单类型(0目录 1菜单 2按钮)', type int not null comment '菜单类型(0目录 1菜单 2按钮 3内嵌 4外链',
component varchar(255) null comment '组件路径', component varchar(255) null comment '组件路径',
perms varchar(100) null comment '权限标识', perms varchar(100) null comment '权限标识',
status int not null comment '菜单状态(0停用 1正常)', status int not null comment '菜单状态(0停用 1正常)',
@@ -223,24 +229,23 @@ create table sys_menu
cache int not null comment '是否缓存(0否 1是)', cache int not null comment '是否缓存(0否 1是)',
link longtext null comment '外链地址', link longtext null comment '外链地址',
remark longtext null comment '备注', remark longtext null comment '备注',
parent_id int null comment '父菜单ID', parent_id bigint null comment '父菜单ID',
created_time datetime not null comment '创建时间', created_time datetime not null comment '创建时间',
updated_time datetime null comment '更新时间', updated_time datetime null comment '更新时间',
constraint ix_sys_menu_id
unique (id),
constraint sys_menu_ibfk_1 constraint sys_menu_ibfk_1
foreign key (parent_id) references sys_menu (id) foreign key (parent_id) references sys_menu (id)
on delete set null on delete set null
) )
comment '菜单表'; comment '菜单表';
create index ix_sys_menu_id
on sys_menu (id);
create index ix_sys_menu_parent_id create index ix_sys_menu_parent_id
on sys_menu (parent_id); on sys_menu (parent_id);
create table sys_notice create table sys_notice
( (
id int auto_increment comment '主键 ID' id bigint auto_increment comment '主键 ID'
primary key, primary key,
title varchar(50) not null comment '标题', title varchar(50) not null comment '标题',
type int not null comment '类型(0:通知、1:公告)', type int not null comment '类型(0:通知、1:公告)',
@@ -249,16 +254,15 @@ create table sys_notice
status int not null comment '状态(0:隐藏、1:显示)', status int not null comment '状态(0:隐藏、1:显示)',
content longtext not null comment '内容', content longtext not null comment '内容',
created_time datetime not null comment '创建时间', created_time datetime not null comment '创建时间',
updated_time datetime null comment '更新时间' updated_time datetime null comment '更新时间',
constraint ix_sys_notice_id
unique (id)
) )
comment '系统通知公告表'; comment '系统通知公告表';
create index ix_sys_notice_id
on sys_notice (id);
create table sys_opera_log create table sys_opera_log
( (
id int auto_increment comment '主键 ID' id bigint auto_increment comment '主键 ID'
primary key, primary key,
trace_id varchar(32) not null comment '请求跟踪 ID', trace_id varchar(32) not null comment '请求跟踪 ID',
username varchar(20) null comment '用户名', username varchar(20) null comment '用户名',
@@ -279,35 +283,34 @@ create table sys_opera_log
msg longtext null comment '提示消息', msg longtext null comment '提示消息',
cost_time float not null comment '请求耗时(ms', cost_time float not null comment '请求耗时(ms',
opera_time datetime not null comment '操作时间', opera_time datetime not null comment '操作时间',
created_time datetime not null comment '创建时间' created_time datetime not null comment '创建时间',
constraint ix_sys_opera_log_id
unique (id)
) )
comment '操作日志表'; comment '操作日志表';
create index ix_sys_opera_log_id
on sys_opera_log (id);
create table sys_role create table sys_role
( (
id int auto_increment comment '主键 ID' id bigint 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 ix_sys_role_id
unique (id),
constraint name constraint name
unique (name) unique (name)
) )
comment '角色表'; comment '角色表';
create index ix_sys_role_id
on sys_role (id);
create table sys_role_data_scope create table sys_role_data_scope
( (
id int auto_increment comment '主键 ID', id bigint auto_increment comment '主键 ID',
role_id int not null comment '角色 ID', role_id bigint not null comment '角色 ID',
data_scope_id int not null comment '数据范围 ID', data_scope_id bigint not null comment '数据范围 ID',
primary key (id, role_id, data_scope_id), primary key (id, role_id, data_scope_id),
constraint ix_sys_role_data_scope_id constraint ix_sys_role_data_scope_id
unique (id), unique (id),
@@ -327,9 +330,9 @@ create index role_id
create table sys_role_menu create table sys_role_menu
( (
id int auto_increment comment '主键ID', id bigint auto_increment comment '主键ID',
role_id int not null comment '角色ID', role_id bigint not null comment '角色ID',
menu_id int not null comment '菜单ID', menu_id bigint not null comment '菜单ID',
primary key (id, role_id, menu_id), primary key (id, role_id, menu_id),
constraint ix_sys_role_menu_id constraint ix_sys_role_menu_id
unique (id), unique (id),
@@ -349,31 +352,31 @@ create index role_id
create table sys_user create table sys_user
( (
id int auto_increment comment '主键 ID' id bigint auto_increment comment '主键 ID'
primary key, primary key,
uuid varchar(50) not null, uuid varchar(50) not null,
username varchar(20) not null comment '用户名', username varchar(20) not null comment '用户名',
nickname varchar(20) not null comment '昵称', nickname varchar(20) not null comment '昵称',
password varchar(255) null comment '密码', password varchar(255) not null comment '密码',
salt varbinary(255) null comment '加密盐', salt varbinary(255) not null comment '加密盐',
email varchar(50) not null comment '邮箱', email varchar(50) null comment '邮箱',
phone varchar(11) null comment '手机号',
avatar varchar(255) null comment '头像',
status int not null comment '用户账号状态(0停用 1正常)',
is_superuser tinyint(1) not null comment '超级权限(0否 1是)', is_superuser tinyint(1) not null comment '超级权限(0否 1是)',
is_staff tinyint(1) not null comment '后台管理登陆(0否 1是)', is_staff tinyint(1) not null comment '后台管理登陆(0否 1是)',
status int not null comment '用户账号状态(0停用 1正常)',
is_multi_login tinyint(1) not null comment '是否重复登陆(0否 1是)', is_multi_login tinyint(1) not null comment '是否重复登陆(0否 1是)',
avatar varchar(255) null comment '头像',
phone varchar(11) null comment '手机号',
join_time datetime not null comment '注册时间', join_time datetime not null comment '注册时间',
last_login_time datetime null comment '上次登录', last_login_time datetime null comment '上次登录',
dept_id int null comment '部门关联ID', dept_id bigint null comment '部门关联ID',
created_time datetime not null comment '创建时间', created_time datetime not null comment '创建时间',
updated_time datetime null comment '更新时间', updated_time datetime null comment '更新时间',
constraint ix_sys_user_email constraint ix_sys_user_email
unique (email), unique (email),
constraint ix_sys_user_id
unique (id),
constraint ix_sys_user_username constraint ix_sys_user_username
unique (username), unique (username),
constraint nickname
unique (nickname),
constraint uuid constraint uuid
unique (uuid), unique (uuid),
constraint sys_user_ibfk_1 constraint sys_user_ibfk_1
@@ -385,17 +388,14 @@ create table sys_user
create index dept_id create index dept_id
on sys_user (dept_id); on sys_user (dept_id);
create index ix_sys_user_id
on sys_user (id);
create index ix_sys_user_status create index ix_sys_user_status
on sys_user (status); on sys_user (status);
create table sys_user_role create table sys_user_role
( (
id int auto_increment comment '主键ID', id bigint auto_increment comment '主键ID',
user_id int not null comment '用户ID', user_id bigint not null comment '用户ID',
role_id int not null comment '角色ID', role_id bigint not null comment '角色ID',
primary key (id, user_id, role_id), primary key (id, user_id, role_id),
constraint ix_sys_user_role_id constraint ix_sys_user_role_id
unique (id), unique (id),
@@ -415,26 +415,21 @@ create index user_id
create table sys_user_social create table sys_user_social
( (
id int auto_increment comment '主键 ID' id bigint auto_increment comment '主键 ID'
primary key, primary key,
source varchar(20) not null comment '第三方用户来源', sid varchar(20) not null comment '第三方用户 ID',
open_id varchar(20) null comment '第三方用户 open id', source varchar(20) not null comment '第三方用户来源',
sid varchar(20) null comment '第三方用户 ID', user_id bigint not null comment '用户关联ID',
union_id varchar(20) null comment '第三方用户 union id', created_time datetime not null comment '创建时间',
scope varchar(120) null comment '第三方用户授予的权限', updated_time datetime null comment '更新时间',
code varchar(50) null comment '用户的授权 code', constraint ix_sys_user_social_id
user_id int null comment '用户关联ID', unique (id),
created_time datetime not null comment '创建时间',
updated_time datetime null comment '更新时间',
constraint sys_user_social_ibfk_1 constraint sys_user_social_ibfk_1
foreign key (user_id) references sys_user (id) foreign key (user_id) references sys_user (id)
on delete set null on delete cascade
) )
comment '用户社交表(OAuth2'; comment '用户社交表(OAuth2';
create index ix_sys_user_social_id
on sys_user_social (id);
create index user_id create index user_id
on sys_user_social (user_id); on sys_user_social (user_id);
+103 -27
View File
@@ -1,37 +1,113 @@
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, '系统管理', 'System', 'system', 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, '/system/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, '/system/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, '/system/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, '/system/menu/index.vue', null, 1, 1, 1, null, null, 5, '2023-07-27 19:45:29', null), (9, '文档', 'Document', 'document', 1, 'lucide:book-open-text', 4, '/_core/fallback/iframe.vue', null, 1, 1, 1, 'https://fastapi-practices.github.io/fastapi_best_architecture_docs', null, 6, '2025-06-09 17:59:44', null),
(10, '插件管理', 'SysPlugin', 'sys-plugin', 0, null, 1, '/system/plugin/index.vue', null, 1, 1, 1, null, null, 5, '2023-07-27 19:24:12', null), (10, 'Github', 'Github', 'github', 2, 'ant-design:github-filled', 4, '/_core/fallback/iframe.vue', null, 1, 1, 1, 'https://github.com/fastapi-practices/fastapi_best_architecture', null, 6, '2025-06-09 18:00:50', null),
(11, '数据权限', 'SysDataPermission', 'sys-data-permission', 0, null, 1, '/system/data-permission/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, '/_core/fallback/iframe.vue', 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, '个人中心', 'Profile', 'profile', 6, 'ant-design:profile-outlined', 1, '/_core/profile/index', null, 1, 0, 1, '', null, null, '2025-06-09 18:18:12', null),
(31, '新增', 'AddSysDept', null, 0, null, 2, null, 'sys:dept:add', 1, 0, 1, '', null, 12, '2025-06-09 18:21:17', null),
(32, '修改', 'EditSysDept', null, 0, null, 2, null, 'sys:dept:edit', 1, 0, 1, '', null, 12, '2025-06-09 18:22:01', null),
(33, '删除', 'DeleteSysDept', null, 0, null, 2, null, 'sys:dept:del', 1, 0, 1, '', null, 12, '2025-06-09 18:22:39', null),
(34, '删除', 'DeleteSysUser', null, 0, null, 2, null, 'sys:user:del', 1, 0, 1, '', null, 13, '2025-06-09 18:24:09', null),
(35, '新增', 'AddSysRole', null, 0, null, 2, null, 'sys:role:add', 1, 0, 1, '', null, 14, '2025-06-09 18:25:08', null),
(36, '修改', 'EditSysRole', null, 0, null, 2, null, 'sys:role:edit', 1, 0, 1, '', null, 14, '2025-06-09 18:26:30', null),
(37, '修改角色菜单', 'EditSysRoleMenu', null, 0, null, 2, null, 'sys:role:menu:edit', 1, 0, 1, '', null, 14, '2025-06-09 18:27:24', null),
(38, '修改角色数据范围', 'EditSysRoleScope', null, 0, null, 2, null, 'sys:role:scope:edit', 1, 0, 1, '', null, 14, '2025-06-09 18:28:25', null),
(39, '删除', 'DeleteSysRole', null, 0, null, 2, null, 'sys:role:del', 1, 0, 1, '', null, 14, '2025-06-09 18:28:55', null),
(40, '新增', 'AddSysMenu', null, 0, null, 2, null, 'sys:menu:add', 1, 0, 1, '', null, 15, '2025-06-09 18:29:51', null),
(41, '修改', 'EditSysMenu', null, 0, null, 2, null, 'sys:menu:edit', 1, 0, 1, '', null, 15, '2025-06-09 18:30:13', null),
(42, '删除', 'DeleteSysMenu', null, 0, null, 2, null, 'sys:menu:del', 1, 0, 1, '', null, 15, '2025-06-09 18:30:37', null),
(43, '新增', 'AddSysDataScope', null, 0, null, 2, null, 'data:scope:add', 1, 0, 1, '', null, 17, '2025-06-09 18:31:11', null),
(44, '修改', 'EditSysDataScope', null, 0, null, 2, null, 'data:scope:edit', 1, 0, 1, '', null, 17, '2025-06-09 18:31:42', null),
(45, '修改数据范围规则', 'EditDataScopeRule', null, 0, null, 2, null, 'data:scope:rule:edit', 1, 0, 1, '', null, 17, '2025-06-09 18:32:36', null),
(46, '删除', 'DeleteSysDataScope', null, 0, null, 2, null, 'data:scope:del', 1, 0, 1, '', null, 17, '2025-06-09 18:33:09', null),
(47, '新增', 'AddSysDataRule', null, 0, null, 2, null, 'data:rule:add', 1, 0, 1, '', null, 18, '2025-06-09 18:35:54', null),
(48, '修改', 'EditSysDataRule', null, 0, null, 2, null, 'data:rule:edit', 1, 0, 1, '', null, 18, '2025-06-09 18:36:19', null),
(49, '删除', 'DeleteSysDataRule', null, 0, null, 2, null, 'data:rule:del', 1, 0, 1, '', null, 18, '2025-06-09 18:36:44', null),
(50, '安装zip插件', 'InstallZipSysPlugin', null, 0, null, 2, null, 'sys:plugin:zip', 1, 0, 1, '', null, 19, '2025-06-09 18:38:14', null),
(51, '安装git插件', 'InstallGitSysPlugin', null, 0, null, 2, null, 'sys:plugin:git', 1, 0, 1, '', null, 19, '2025-06-09 18:38:43', null),
(52, '卸载', 'UninstallSysPlugin', null, 0, null, 2, null, 'sys:plugin:del', 1, 0, 1, '', null, 19, '2025-06-09 18:39:08', null),
(53, '修改', 'EditSysPlugin', null, 0, null, 2, null, 'sys:plugin:status', 1, 0, 1, '', null, 19, '2025-06-09 18:39:47', null),
(54, '新增网站参数', 'AddWebsiteSysConfig', null, 0, null, 2, null, 'sys:config:website:add', 1, 0, 1, '', null, 20, '2025-06-09 18:43:30', null),
(55, '新增用户协议', 'AddProtocolSysConfig', null, 0, null, 2, null, 'sys:config:protocol:add', 1, 0, 1, '', null, 20, '2025-06-09 18:44:13', null),
(56, '新增用户政策', 'AddPolicySysConfig', null, 0, null, 2, null, 'sys:config:policy:add', 1, 0, 1, '', null, 20, '2025-06-09 18:45:28', null),
(57, '新增', 'AddSysConfig', null, 0, null, 2, null, 'sys:config:add', 1, 0, 1, '', null, 20, '2025-06-09 18:45:52', null),
(58, '修改', 'EditSysConfig', null, 0, null, 2, null, 'sys:config:edit', 1, 0, 1, '', null, 20, '2025-06-09 18:46:13', null),
(59, '删除', 'DeleteSysConfig', null, 0, null, 2, null, 'sys:config:del', 1, 0, 1, '', null, 20, '2025-06-09 18:46:36', null),
(60, '新增类型', 'AddSysDictType', null, 0, null, 2, null, 'sys:dict:type:add', 1, 0, 1, '', null, 21, '2025-06-09 18:48:17', null),
(61, '修改类型', 'EditSysDictType', null, 0, null, 2, null, 'sys:dict:type:edit', 1, 0, 1, '', null, 21, '2025-06-09 18:48:49', null),
(62, '删除类型', 'DeleteSysDictType', null, 0, null, 2, null, 'sys:dict:type:del', 1, 0, 1, '', null, 21, '2025-06-09 18:49:23', null),
(63, '新增', 'AddSysDictData', null, 0, null, 2, null, 'sys:dict:data:add', 1, 0, 1, '', null, 21, '2025-06-09 18:50:01', null),
(64, '修改', 'EditSysDictData', null, 0, null, 2, null, 'sys:dict:data:edit', 1, 0, 1, '', null, 21, '2025-06-09 18:50:26', null),
(65, '删除', 'DeleteSysDictData', null, 0, null, 2, null, 'sys:dict:data:del', 1, 0, 1, '', null, 21, '2025-06-09 18:50:48', null),
(66, '新增', 'AddSysNotice', null, 0, null, 2, null, 'sys:notice:add', 1, 0, 1, '', null, 22, '2025-06-09 18:51:22', null),
(67, '修改', 'EditSysNotice', null, 0, null, 2, null, 'sys:notice:edit', 1, 0, 1, '', null, 22, '2025-06-09 18:51:45', null),
(68, '删除', 'DeleteSysNotice', null, 0, null, 2, null, 'sys:notice:del', 1, 0, 1, '', null, 22, '2025-06-09 18:52:10', null),
(69, '新增业务', 'AddSysGenCodeBusiness', null, 0, null, 2, null, 'gen:code:business:add', 1, 0, 1, '', null, 23, '2025-06-09 18:53:07', null),
(70, '修改业务', 'EditGenCodeBusiness', null, 0, null, 2, null, 'gen:code:business:edit', 1, 0, 1, '', null, 23, '2025-06-09 18:53:45', null),
(71, '删除业务', 'DeleteGenCodeBusiness', null, 0, null, 2, null, 'gen:code:business:del', 1, 0, 1, '', null, 23, '2025-06-09 18:54:11', null),
(72, '新增模型', 'AddGenCodeModel', null, 0, null, 2, null, 'gen:code:model:add', 1, 0, 1, '', null, 23, '2025-06-09 18:54:45', null),
(73, '修改模型', 'EditGenCodeModel', null, 0, null, 2, null, 'gen:code:model:edit', 1, 0, 1, '', null, 23, '2025-06-09 18:55:08', null),
(74, '删除模型', 'DeleteGenCodeModel', null, 0, null, 2, null, 'gen:code:model:del', 1, 0, 1, '', null, 23, '2025-06-09 18:55:35', null),
(75, '导入', 'ImportGenCode', null, 0, null, 2, null, 'gen:code:import', 1, 0, 1, '', null, 23, '2025-06-09 18:58:16', null),
(76, '写入', 'WriteGenCode', null, 0, null, 2, null, 'gen:code:write', 1, 0, 1, '', null, 23, '2025-06-09 19:01:22', null),
(77, '删除', 'DeleteSysLoginLog', null, 0, null, 2, null, 'log:login:del', 1, 0, 1, '', null, 25, '2025-06-09 19:02:21', null),
(78, '清空', 'EmptyLoginLog', null, 0, null, 2, null, 'log:login:empty', 1, 0, 1, '', null, 25, '2025-06-09 19:02:50', null),
(79, '删除', 'DeleteOperaLog', null, 0, null, 2, null, 'log:opera:del', 1, 0, 1, '', null, 26, '2025-06-09 19:03:13', null),
(80, '清空', 'EmptyOperaLog', null, 0, null, 2, null, 'log:opera:empty', 1, 0, 1, '', null, 26, '2025-06-09 19:03:40', null),
(81, '下线', '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);
+333 -189
View File
@@ -1,12 +1,59 @@
create table sys_data_rule
(
id bigserial,
name varchar(500) not null,
model varchar(50) not null,
"column" varchar(20) not null,
operator integer not null,
expression integer not null,
value varchar(255) not null,
created_time timestamp with time zone not null,
updated_time timestamp with time zone,
primary key (),
unique ()
);
comment on table sys_data_rule is '数据规则表';
comment on column sys_data_rule.id is '主键 ID';
comment on column sys_data_rule.name is '名称';
comment on column sys_data_rule.model is 'SQLA 模型名,对应 DATA_PERMISSION_MODELS 键名';
comment on column sys_data_rule."column" is '模型字段名';
comment on column sys_data_rule.operator is '运算符(0and、1or';
comment on column sys_data_rule.expression is '表达式(0==、1!=、2>、3>=、4<、5<=、6in、7not_in';
comment on column sys_data_rule.value is '规则值';
comment on column sys_data_rule.created_time is '创建时间';
comment on column sys_data_rule.updated_time is '更新时间';
alter table sys_data_rule
owner to postgres;
create unique index sys_data_rule_pkey
on sys_data_rule (id);
create unique index sys_data_rule_name_key
on sys_data_rule (name);
create unique index ix_sys_data_rule_id
on sys_data_rule (id);
create table sys_data_scope create table sys_data_scope
( (
id serial id bigserial,
primary key, name varchar(50) not null,
name varchar(50) not null
unique,
status integer not null, status integer not null,
created_time timestamp with time zone not null, created_time timestamp with time zone not null,
updated_time timestamp with time zone updated_time timestamp with time zone,
primary key (),
unique ()
); );
comment on table sys_data_scope is '数据范围表'; comment on table sys_data_scope is '数据范围表';
@@ -21,13 +68,21 @@ comment on column sys_data_scope.created_time is '创建时间';
comment on column sys_data_scope.updated_time is '更新时间'; comment on column sys_data_scope.updated_time is '更新时间';
create index ix_sys_data_scope_id alter table sys_data_scope
owner to postgres;
create unique index sys_data_scope_pkey
on sys_data_scope (id);
create unique index sys_data_scope_name_key
on sys_data_scope (name);
create unique index ix_sys_data_scope_id
on sys_data_scope (id); on sys_data_scope (id);
create table sys_dept create table sys_dept
( (
id serial id bigserial,
primary key,
name varchar(50) not null, name varchar(50) not null,
sort integer not null, sort integer not null,
leader varchar(20), leader varchar(20),
@@ -35,11 +90,12 @@ create table sys_dept
email varchar(50), email varchar(50),
status integer not null, status integer not null,
del_flag integer not null, del_flag integer not null,
parent_id integer parent_id bigint
references sys_dept references ??? ()
on delete set null, on delete set null,
created_time timestamp with time zone not null, created_time timestamp with time zone not null,
updated_time timestamp with time zone updated_time timestamp with time zone,
primary key ()
); );
comment on table sys_dept is '部门表'; comment on table sys_dept is '部门表';
@@ -66,7 +122,13 @@ comment on column sys_dept.created_time is '创建时间';
comment on column sys_dept.updated_time is '更新时间'; comment on column sys_dept.updated_time is '更新时间';
create index ix_sys_dept_id alter table sys_dept
owner to postgres;
create unique index sys_dept_pkey
on sys_dept (id);
create unique index ix_sys_dept_id
on sys_dept (id); on sys_dept (id);
create index ix_sys_dept_parent_id create index ix_sys_dept_parent_id
@@ -74,8 +136,7 @@ create index ix_sys_dept_parent_id
create table sys_login_log create table sys_login_log
( (
id serial id bigint not null,
primary key,
user_uuid varchar(50) not null, user_uuid varchar(50) not null,
username varchar(20) not null, username varchar(20) not null,
status integer not null, status integer not null,
@@ -89,12 +150,13 @@ create table sys_login_log
device varchar(50), device varchar(50),
msg text not null, msg text not null,
login_time timestamp with time zone not null, login_time timestamp with time zone not null,
created_time timestamp with time zone not null created_time timestamp with time zone not null,
primary key ()
); );
comment on table sys_login_log is '登录日志表'; comment on table sys_login_log is '登录日志表';
comment on column sys_login_log.id is '主键 ID'; comment on column sys_login_log.id is '雪花算法主键 ID';
comment on column sys_login_log.user_uuid is '用户UUID'; comment on column sys_login_log.user_uuid is '用户UUID';
@@ -124,16 +186,21 @@ comment on column sys_login_log.login_time is '登录时间';
comment on column sys_login_log.created_time is '创建时间'; comment on column sys_login_log.created_time is '创建时间';
create index ix_sys_login_log_id alter table sys_login_log
owner to postgres;
create unique index sys_login_log_pkey
on sys_login_log (id);
create unique index ix_sys_login_log_id
on sys_login_log (id); on sys_login_log (id);
create table sys_menu create table sys_menu
( (
id serial id bigserial,
primary key,
title varchar(50) not null, title varchar(50) not null,
name varchar(50) not null, name varchar(50) not null,
path varchar(200) not null, path varchar(200),
sort integer not null, sort integer not null,
icon varchar(100), icon varchar(100),
type integer not null, type integer not null,
@@ -144,11 +211,12 @@ create table sys_menu
cache integer not null, cache integer not null,
link text, link text,
remark text, remark text,
parent_id integer parent_id bigint
references sys_menu references ??? ()
on delete set null, on delete set null,
created_time timestamp with time zone not null, created_time timestamp with time zone not null,
updated_time timestamp with time zone updated_time timestamp with time zone,
primary key ()
); );
comment on table sys_menu is '菜单表'; comment on table sys_menu is '菜单表';
@@ -165,7 +233,7 @@ comment on column sys_menu.sort is '排序';
comment on column sys_menu.icon is '菜单图标'; comment on column sys_menu.icon is '菜单图标';
comment on column sys_menu.type is '菜单类型(0目录 1菜单 2按钮)'; comment on column sys_menu.type is '菜单类型(0目录 1菜单 2按钮 3内嵌 4外链';
comment on column sys_menu.component is '组件路径'; comment on column sys_menu.component is '组件路径';
@@ -187,16 +255,21 @@ comment on column sys_menu.created_time is '创建时间';
comment on column sys_menu.updated_time is '更新时间'; comment on column sys_menu.updated_time is '更新时间';
create index ix_sys_menu_id alter table sys_menu
owner to postgres;
create unique index sys_menu_pkey
on sys_menu (id); on sys_menu (id);
create index ix_sys_menu_parent_id create index ix_sys_menu_parent_id
on sys_menu (parent_id); on sys_menu (parent_id);
create unique index ix_sys_menu_id
on sys_menu (id);
create table sys_opera_log create table sys_opera_log
( (
id serial id bigserial,
primary key,
trace_id varchar(32) not null, trace_id varchar(32) not null,
username varchar(20), username varchar(20),
method varchar(20) not null, method varchar(20) not null,
@@ -216,7 +289,8 @@ create table sys_opera_log
msg text, msg text,
cost_time double precision not null, cost_time double precision not null,
opera_time timestamp with time zone not null, opera_time timestamp with time zone not null,
created_time timestamp with time zone not null created_time timestamp with time zone not null,
primary key ()
); );
comment on table sys_opera_log is '操作日志表'; comment on table sys_opera_log is '操作日志表';
@@ -263,19 +337,26 @@ comment on column sys_opera_log.opera_time is '操作时间';
comment on column sys_opera_log.created_time is '创建时间'; comment on column sys_opera_log.created_time is '创建时间';
create index ix_sys_opera_log_id alter table sys_opera_log
owner to postgres;
create unique index sys_opera_log_pkey
on sys_opera_log (id);
create unique index ix_sys_opera_log_id
on sys_opera_log (id); on sys_opera_log (id);
create table sys_role create table sys_role
( (
id serial id bigserial,
primary key, name varchar(20) not null,
name varchar(20) not null status integer not null,
unique, is_filter_scopes integer not null,
status integer not null, remark text,
remark text, created_time timestamp with time zone not null,
created_time timestamp with time zone not null, updated_time timestamp with time zone,
updated_time timestamp with time zone primary key (),
unique ()
); );
comment on table sys_role is '角色表'; comment on table sys_role is '角色表';
@@ -286,28 +367,39 @@ 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 '创建时间';
comment on column sys_role.updated_time is '更新时间'; comment on column sys_role.updated_time is '更新时间';
create index ix_sys_role_id alter table sys_role
owner to postgres;
create unique index sys_role_pkey
on sys_role (id);
create unique index sys_role_name_key
on sys_role (name);
create unique index ix_sys_role_id
on sys_role (id); on sys_role (id);
create table sys_config create table sys_config
( (
id serial id bigserial,
primary key,
name varchar(20) not null, name varchar(20) not null,
type varchar(20), type varchar(20),
key varchar(50) not null key varchar(50) not null,
unique,
value text not null, value text not null,
is_frontend integer not null, is_frontend integer not null,
remark text, remark text,
created_time timestamp with time zone not null, created_time timestamp with time zone not null,
updated_time timestamp with time zone updated_time timestamp with time zone,
primary key (),
unique ()
); );
comment on table sys_config is '参数配置表'; comment on table sys_config is '参数配置表';
@@ -330,20 +422,29 @@ comment on column sys_config.created_time is '创建时间';
comment on column sys_config.updated_time is '更新时间'; comment on column sys_config.updated_time is '更新时间';
create index ix_sys_config_id alter table sys_config
owner to postgres;
create unique index sys_config_pkey
on sys_config (id);
create unique index sys_config_key_key
on sys_config (key);
create unique index ix_sys_config_id
on sys_config (id); on sys_config (id);
create table sys_dict_type create table sys_dict_type
( (
id serial id bigserial,
primary key,
name varchar(32) not null, name varchar(32) not null,
code varchar(32) not null code varchar(32) not null,
unique,
status integer not null, status integer not null,
remark text, remark text,
created_time timestamp with time zone not null, created_time timestamp with time zone not null,
updated_time timestamp with time zone updated_time timestamp with time zone,
primary key (),
unique ()
); );
comment on table sys_dict_type is '字典类型表'; comment on table sys_dict_type is '字典类型表';
@@ -362,13 +463,21 @@ comment on column sys_dict_type.created_time is '创建时间';
comment on column sys_dict_type.updated_time is '更新时间'; comment on column sys_dict_type.updated_time is '更新时间';
create index ix_sys_dict_type_id alter table sys_dict_type
owner to postgres;
create unique index sys_dict_type_pkey
on sys_dict_type (id);
create unique index sys_dict_type_code_key
on sys_dict_type (code);
create unique index ix_sys_dict_type_id
on sys_dict_type (id); on sys_dict_type (id);
create table sys_notice create table sys_notice
( (
id serial id bigserial,
primary key,
title varchar(50) not null, title varchar(50) not null,
type integer not null, type integer not null,
author varchar(16) not null, author varchar(16) not null,
@@ -376,7 +485,8 @@ create table sys_notice
status integer not null, status integer not null,
content text not null, content text not null,
created_time timestamp with time zone not null, created_time timestamp with time zone not null,
updated_time timestamp with time zone updated_time timestamp with time zone,
primary key ()
); );
comment on table sys_notice is '系统通知公告表'; comment on table sys_notice is '系统通知公告表';
@@ -399,16 +509,20 @@ comment on column sys_notice.created_time is '创建时间';
comment on column sys_notice.updated_time is '更新时间'; comment on column sys_notice.updated_time is '更新时间';
create index ix_sys_notice_id alter table sys_notice
owner to postgres;
create unique index sys_notice_pkey
on sys_notice (id);
create unique index ix_sys_notice_id
on sys_notice (id); on sys_notice (id);
create table gen_business create table gen_business
( (
id serial id bigserial,
primary key,
app_name varchar(50) not null, app_name varchar(50) not null,
table_name varchar(255) not null table_name varchar(255) not null,
unique,
doc_comment varchar(255) not null, doc_comment varchar(255) not null,
table_comment varchar(255), table_comment varchar(255),
class_name varchar(50), class_name varchar(50),
@@ -419,7 +533,9 @@ create table gen_business
gen_path varchar(255), gen_path varchar(255),
remark text, remark text,
created_time timestamp with time zone not null, created_time timestamp with time zone not null,
updated_time timestamp with time zone updated_time timestamp with time zone,
primary key (),
unique ()
); );
comment on table gen_business is '代码生成业务表'; comment on table gen_business is '代码生成业务表';
@@ -452,62 +568,28 @@ comment on column gen_business.created_time is '创建时间';
comment on column gen_business.updated_time is '更新时间'; comment on column gen_business.updated_time is '更新时间';
create index ix_gen_business_id alter table gen_business
owner to postgres;
create unique index gen_business_pkey
on gen_business (id); on gen_business (id);
create table sys_data_rule create unique index gen_business_table_name_key
( on gen_business (table_name);
id serial
primary key,
name varchar(500) not null
unique,
model varchar(50) not null,
"column" varchar(20) not null,
operator integer not null,
expression integer not null,
value varchar(255) not null,
scope_id integer
references sys_data_scope
on delete set null,
created_time timestamp with time zone not null,
updated_time timestamp with time zone
);
comment on table sys_data_rule is '数据规则表'; create unique index ix_gen_business_id
on gen_business (id);
comment on column sys_data_rule.id is '主键 ID';
comment on column sys_data_rule.name is '名称';
comment on column sys_data_rule.model is 'SQLA 模型名,对应 DATA_PERMISSION_MODELS 键名';
comment on column sys_data_rule."column" is '模型字段名';
comment on column sys_data_rule.operator is '运算符(0and、1or';
comment on column sys_data_rule.expression is '表达式(0==、1!=、2>、3>=、4<、5<=、6in、7not_in';
comment on column sys_data_rule.value is '规则值';
comment on column sys_data_rule.scope_id is '数据范围关联 ID';
comment on column sys_data_rule.created_time is '创建时间';
comment on column sys_data_rule.updated_time is '更新时间';
create index ix_sys_data_rule_id
on sys_data_rule (id);
create table sys_role_menu create table sys_role_menu
( (
id serial, id bigserial,
role_id integer not null role_id bigint not null
references sys_role references ??? ()
on delete cascade, on delete cascade,
menu_id integer not null menu_id bigint not null
references sys_menu references ??? ()
on delete cascade, on delete cascade,
primary key (id, role_id, menu_id) primary key ()
); );
comment on column sys_role_menu.id is '主键ID'; comment on column sys_role_menu.id is '主键ID';
@@ -516,19 +598,25 @@ comment on column sys_role_menu.role_id is '角色ID';
comment on column sys_role_menu.menu_id is '菜单ID'; comment on column sys_role_menu.menu_id is '菜单ID';
alter table sys_role_menu
owner to postgres;
create unique index sys_role_menu_pkey
on sys_role_menu (id, role_id, menu_id);
create unique index ix_sys_role_menu_id create unique index ix_sys_role_menu_id
on sys_role_menu (id); on sys_role_menu (id);
create table sys_role_data_scope create table sys_role_data_scope
( (
id serial, id bigserial,
role_id integer not null role_id bigint not null
references sys_role references ??? ()
on delete cascade, on delete cascade,
data_scope_id integer not null data_scope_id bigint not null
references sys_data_scope references ??? ()
on delete cascade, on delete cascade,
primary key (id, role_id, data_scope_id) primary key ()
); );
comment on column sys_role_data_scope.id is '主键 ID'; comment on column sys_role_data_scope.id is '主键 ID';
@@ -537,34 +625,66 @@ comment on column sys_role_data_scope.role_id is '角色 ID';
comment on column sys_role_data_scope.data_scope_id is '数据范围 ID'; comment on column sys_role_data_scope.data_scope_id is '数据范围 ID';
alter table sys_role_data_scope
owner to postgres;
create unique index sys_role_data_scope_pkey
on sys_role_data_scope (id, role_id, data_scope_id);
create unique index ix_sys_role_data_scope_id create unique index ix_sys_role_data_scope_id
on sys_role_data_scope (id); on sys_role_data_scope (id);
create table sys_data_scope_rule
(
id bigserial,
data_scope_id bigint not null
references ??? ()
on delete cascade,
data_rule_id bigint not null
references ??? ()
on delete cascade,
primary key ()
);
comment on column sys_data_scope_rule.id is '主键ID';
comment on column sys_data_scope_rule.data_scope_id is '数据范围 ID';
comment on column sys_data_scope_rule.data_rule_id is '数据规则 ID';
alter table sys_data_scope_rule
owner to postgres;
create unique index sys_data_scope_rule_pkey
on sys_data_scope_rule (id, data_scope_id, data_rule_id);
create unique index ix_sys_data_scope_rule_id
on sys_data_scope_rule (id);
create table sys_user create table sys_user
( (
id serial id bigserial,
primary key, uuid varchar(50) not null,
uuid varchar(50) not null
unique,
username varchar(20) not null, username varchar(20) not null,
nickname varchar(20) not null nickname varchar(20) not null,
unique, password varchar(255) not null,
password varchar(255), salt bytea not null,
salt bytea, email varchar(50),
email varchar(50) not null, phone varchar(11),
avatar varchar(255),
status integer not null,
is_superuser integer not null, is_superuser integer not null,
is_staff integer not null, is_staff integer not null,
status integer not null,
is_multi_login integer not null, is_multi_login integer not null,
avatar varchar(255),
phone varchar(11),
join_time timestamp with time zone not null, join_time timestamp with time zone not null,
last_login_time timestamp with time zone, last_login_time timestamp with time zone,
dept_id integer dept_id bigint
references sys_dept references ??? ()
on delete set null, on delete set null,
created_time timestamp with time zone not null, created_time timestamp with time zone not null,
updated_time timestamp with time zone updated_time timestamp with time zone,
primary key (),
unique ()
); );
comment on table sys_user is '用户表'; comment on table sys_user is '用户表';
@@ -581,18 +701,18 @@ comment on column sys_user.salt is '加密盐';
comment on column sys_user.email is '邮箱'; comment on column sys_user.email is '邮箱';
comment on column sys_user.phone is '手机号';
comment on column sys_user.avatar is '头像';
comment on column sys_user.status is '用户账号状态(0停用 1正常)';
comment on column sys_user.is_superuser is '超级权限(0否 1是)'; comment on column sys_user.is_superuser is '超级权限(0否 1是)';
comment on column sys_user.is_staff is '后台管理登陆(0否 1是)'; comment on column sys_user.is_staff is '后台管理登陆(0否 1是)';
comment on column sys_user.status is '用户账号状态(0停用 1正常)';
comment on column sys_user.is_multi_login is '是否重复登陆(0否 1是)'; comment on column sys_user.is_multi_login is '是否重复登陆(0否 1是)';
comment on column sys_user.avatar is '头像';
comment on column sys_user.phone is '手机号';
comment on column sys_user.join_time is '注册时间'; comment on column sys_user.join_time is '注册时间';
comment on column sys_user.last_login_time is '上次登录'; comment on column sys_user.last_login_time is '上次登录';
@@ -603,33 +723,42 @@ comment on column sys_user.created_time is '创建时间';
comment on column sys_user.updated_time is '更新时间'; comment on column sys_user.updated_time is '更新时间';
alter table sys_user
owner to postgres;
create unique index sys_user_pkey
on sys_user (id);
create unique index sys_user_uuid_key
on sys_user (uuid);
create unique index ix_sys_user_email create unique index ix_sys_user_email
on sys_user (email); on sys_user (email);
create index ix_sys_user_id
on sys_user (id);
create unique index ix_sys_user_username create unique index ix_sys_user_username
on sys_user (username); on sys_user (username);
create unique index ix_sys_user_id
on sys_user (id);
create index ix_sys_user_status create index ix_sys_user_status
on sys_user (status); on sys_user (status);
create table sys_dict_data create table sys_dict_data
( (
id serial id bigserial,
primary key, label varchar(32) not null,
label varchar(32) not null
unique,
value varchar(32) not null, value varchar(32) not null,
sort integer not null, sort integer not null,
status integer not null, status integer not null,
remark text, remark text,
type_id integer not null type_id bigint not null
references sys_dict_type references ??? ()
on delete cascade, on delete cascade,
created_time timestamp with time zone not null, created_time timestamp with time zone not null,
updated_time timestamp with time zone updated_time timestamp with time zone,
primary key (),
unique ()
); );
comment on table sys_dict_data is '字典数据表'; comment on table sys_dict_data is '字典数据表';
@@ -652,13 +781,21 @@ comment on column sys_dict_data.created_time is '创建时间';
comment on column sys_dict_data.updated_time is '更新时间'; comment on column sys_dict_data.updated_time is '更新时间';
create index ix_sys_dict_data_id alter table sys_dict_data
owner to postgres;
create unique index sys_dict_data_pkey
on sys_dict_data (id);
create unique index sys_dict_data_label_key
on sys_dict_data (label);
create unique index ix_sys_dict_data_id
on sys_dict_data (id); on sys_dict_data (id);
create table gen_column create table gen_column
( (
id serial id bigserial,
primary key,
name varchar(50) not null, name varchar(50) not null,
comment varchar(255), comment varchar(255),
type varchar(20) not null, type varchar(20) not null,
@@ -668,9 +805,10 @@ create table gen_column
length integer not null, length integer not null,
is_pk boolean not null, is_pk boolean not null,
is_nullable boolean not null, is_nullable boolean not null,
gen_business_id integer not null gen_business_id bigint not null
references gen_business references ??? ()
on delete cascade on delete cascade,
primary key ()
); );
comment on table gen_column is '代码生成模型列表'; comment on table gen_column is '代码生成模型列表';
@@ -697,19 +835,25 @@ comment on column gen_column.is_nullable is '是否可为空';
comment on column gen_column.gen_business_id is '代码生成业务ID'; comment on column gen_column.gen_business_id is '代码生成业务ID';
create index ix_gen_column_id alter table gen_column
owner to postgres;
create unique index gen_column_pkey
on gen_column (id);
create unique index ix_gen_column_id
on gen_column (id); on gen_column (id);
create table sys_user_role create table sys_user_role
( (
id serial, id bigserial,
user_id integer not null user_id bigint not null
references sys_user references ??? ()
on delete cascade, on delete cascade,
role_id integer not null role_id bigint not null
references sys_role references ??? ()
on delete cascade, on delete cascade,
primary key (id, user_id, role_id) primary key ()
); );
comment on column sys_user_role.id is '主键ID'; comment on column sys_user_role.id is '主键ID';
@@ -718,47 +862,47 @@ comment on column sys_user_role.user_id is '用户ID';
comment on column sys_user_role.role_id is '角色ID'; comment on column sys_user_role.role_id is '角色ID';
alter table sys_user_role
owner to postgres;
create unique index sys_user_role_pkey
on sys_user_role (id, user_id, role_id);
create unique index ix_sys_user_role_id create unique index ix_sys_user_role_id
on sys_user_role (id); on sys_user_role (id);
create table sys_user_social create table sys_user_social
( (
id serial id bigserial,
primary key, sid varchar(20) not null,
source varchar(20) not null, source varchar(20) not null,
open_id varchar(20), user_id bigint not null
sid varchar(20), references ??? ()
union_id varchar(20), on delete cascade,
scope varchar(120),
code varchar(50),
user_id integer
references sys_user
on delete set null,
created_time timestamp with time zone not null, created_time timestamp with time zone not null,
updated_time timestamp with time zone updated_time timestamp with time zone,
primary key ()
); );
comment on table sys_user_social is '用户社交表(OAuth2'; comment on table sys_user_social is '用户社交表(OAuth2';
comment on column sys_user_social.id is '主键 ID'; comment on column sys_user_social.id is '主键 ID';
comment on column sys_user_social.sid 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.uid is '第三方用户 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.code is '用户的授权 code';
comment on column sys_user_social.user_id is '用户关联ID'; comment on column sys_user_social.user_id is '用户关联ID';
comment on column sys_user_social.created_time is '创建时间'; comment on column sys_user_social.created_time is '创建时间';
comment on column sys_user_social.updated_time is '更新时间'; comment on column sys_user_social.updated_time is '更新时间';
create index ix_sys_user_social_id alter table sys_user_social
owner to postgres;
create unique index sys_user_social_pkey
on sys_user_social (id);
create unique index ix_sys_user_social_id
on sys_user_social (id); on sys_user_social (id);
+114 -27
View File
@@ -1,37 +1,124 @@
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, '系统管理', 'System', 'system', 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, '/system/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, '/system/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, '/system/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, '/system/menu/index.vue', null, 1, 1, 1, null, null, 5, '2023-07-27 19:45:29', null), (9, '文档', 'Document', 'document', 1, 'lucide:book-open-text', 4, '/_core/fallback/iframe.vue', null, 1, 1, 1, 'https://fastapi-practices.github.io/fastapi_best_architecture_docs', null, 6, '2025-06-09 17:59:44', null),
(10, '插件管理', 'SysPlugin', 'sys-plugin', 0, null, 1, '/system/plugin/index.vue', null, 1, 1, 1, null, null, 5, '2023-07-27 19:24:12', null), (10, 'Github', 'Github', 'github', 2, 'ant-design:github-filled', 4, '/_core/fallback/iframe.vue', null, 1, 1, 1, 'https://github.com/fastapi-practices/fastapi_best_architecture', null, 6, '2025-06-09 18:00:50', null),
(11, '数据权限', 'SysDataPermission', 'sys-data-permission', 0, null, 1, '/system/data-permission/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, '/_core/fallback/iframe.vue', 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, '个人中心', 'Profile', 'profile', 6, 'ant-design:profile-outlined', 1, '/_core/profile/index', null, 1, 0, 1, '', null, null, '2025-06-09 18:18:12', null),
(31, '新增', 'AddSysDept', null, 0, null, 2, null, 'sys:dept:add', 1, 0, 1, '', null, 12, '2025-06-09 18:21:17', null),
(32, '修改', 'EditSysDept', null, 0, null, 2, null, 'sys:dept:edit', 1, 0, 1, '', null, 12, '2025-06-09 18:22:01', null),
(33, '删除', 'DeleteSysDept', null, 0, null, 2, null, 'sys:dept:del', 1, 0, 1, '', null, 12, '2025-06-09 18:22:39', null),
(34, '删除', 'DeleteSysUser', null, 0, null, 2, null, 'sys:user:del', 1, 0, 1, '', null, 13, '2025-06-09 18:24:09', null),
(35, '新增', 'AddSysRole', null, 0, null, 2, null, 'sys:role:add', 1, 0, 1, '', null, 14, '2025-06-09 18:25:08', null),
(36, '修改', 'EditSysRole', null, 0, null, 2, null, 'sys:role:edit', 1, 0, 1, '', null, 14, '2025-06-09 18:26:30', null),
(37, '修改角色菜单', 'EditSysRoleMenu', null, 0, null, 2, null, 'sys:role:menu:edit', 1, 0, 1, '', null, 14, '2025-06-09 18:27:24', null),
(38, '修改角色数据范围', 'EditSysRoleScope', null, 0, null, 2, null, 'sys:role:scope:edit', 1, 0, 1, '', null, 14, '2025-06-09 18:28:25', null),
(39, '删除', 'DeleteSysRole', null, 0, null, 2, null, 'sys:role:del', 1, 0, 1, '', null, 14, '2025-06-09 18:28:55', null),
(40, '新增', 'AddSysMenu', null, 0, null, 2, null, 'sys:menu:add', 1, 0, 1, '', null, 15, '2025-06-09 18:29:51', null),
(41, '修改', 'EditSysMenu', null, 0, null, 2, null, 'sys:menu:edit', 1, 0, 1, '', null, 15, '2025-06-09 18:30:13', null),
(42, '删除', 'DeleteSysMenu', null, 0, null, 2, null, 'sys:menu:del', 1, 0, 1, '', null, 15, '2025-06-09 18:30:37', null),
(43, '新增', 'AddSysDataScope', null, 0, null, 2, null, 'data:scope:add', 1, 0, 1, '', null, 17, '2025-06-09 18:31:11', null),
(44, '修改', 'EditSysDataScope', null, 0, null, 2, null, 'data:scope:edit', 1, 0, 1, '', null, 17, '2025-06-09 18:31:42', null),
(45, '修改数据范围规则', 'EditDataScopeRule', null, 0, null, 2, null, 'data:scope:rule:edit', 1, 0, 1, '', null, 17, '2025-06-09 18:32:36', null),
(46, '删除', 'DeleteSysDataScope', null, 0, null, 2, null, 'data:scope:del', 1, 0, 1, '', null, 17, '2025-06-09 18:33:09', null),
(47, '新增', 'AddSysDataRule', null, 0, null, 2, null, 'data:rule:add', 1, 0, 1, '', null, 18, '2025-06-09 18:35:54', null),
(48, '修改', 'EditSysDataRule', null, 0, null, 2, null, 'data:rule:edit', 1, 0, 1, '', null, 18, '2025-06-09 18:36:19', null),
(49, '删除', 'DeleteSysDataRule', null, 0, null, 2, null, 'data:rule:del', 1, 0, 1, '', null, 18, '2025-06-09 18:36:44', null),
(50, '安装zip插件', 'InstallZipSysPlugin', null, 0, null, 2, null, 'sys:plugin:zip', 1, 0, 1, '', null, 19, '2025-06-09 18:38:14', null),
(51, '安装git插件', 'InstallGitSysPlugin', null, 0, null, 2, null, 'sys:plugin:git', 1, 0, 1, '', null, 19, '2025-06-09 18:38:43', null),
(52, '卸载', 'UninstallSysPlugin', null, 0, null, 2, null, 'sys:plugin:del', 1, 0, 1, '', null, 19, '2025-06-09 18:39:08', null),
(53, '修改', 'EditSysPlugin', null, 0, null, 2, null, 'sys:plugin:status', 1, 0, 1, '', null, 19, '2025-06-09 18:39:47', null),
(54, '新增网站参数', 'AddWebsiteSysConfig', null, 0, null, 2, null, 'sys:config:website:add', 1, 0, 1, '', null, 20, '2025-06-09 18:43:30', null),
(55, '新增用户协议', 'AddProtocolSysConfig', null, 0, null, 2, null, 'sys:config:protocol:add', 1, 0, 1, '', null, 20, '2025-06-09 18:44:13', null),
(56, '新增用户政策', 'AddPolicySysConfig', null, 0, null, 2, null, 'sys:config:policy:add', 1, 0, 1, '', null, 20, '2025-06-09 18:45:28', null),
(57, '新增', 'AddSysConfig', null, 0, null, 2, null, 'sys:config:add', 1, 0, 1, '', null, 20, '2025-06-09 18:45:52', null),
(58, '修改', 'EditSysConfig', null, 0, null, 2, null, 'sys:config:edit', 1, 0, 1, '', null, 20, '2025-06-09 18:46:13', null),
(59, '删除', 'DeleteSysConfig', null, 0, null, 2, null, 'sys:config:del', 1, 0, 1, '', null, 20, '2025-06-09 18:46:36', null),
(60, '新增类型', 'AddSysDictType', null, 0, null, 2, null, 'sys:dict:type:add', 1, 0, 1, '', null, 21, '2025-06-09 18:48:17', null),
(61, '修改类型', 'EditSysDictType', null, 0, null, 2, null, 'sys:dict:type:edit', 1, 0, 1, '', null, 21, '2025-06-09 18:48:49', null),
(62, '删除类型', 'DeleteSysDictType', null, 0, null, 2, null, 'sys:dict:type:del', 1, 0, 1, '', null, 21, '2025-06-09 18:49:23', null),
(63, '新增', 'AddSysDictData', null, 0, null, 2, null, 'sys:dict:data:add', 1, 0, 1, '', null, 21, '2025-06-09 18:50:01', null),
(64, '修改', 'EditSysDictData', null, 0, null, 2, null, 'sys:dict:data:edit', 1, 0, 1, '', null, 21, '2025-06-09 18:50:26', null),
(65, '删除', 'DeleteSysDictData', null, 0, null, 2, null, 'sys:dict:data:del', 1, 0, 1, '', null, 21, '2025-06-09 18:50:48', null),
(66, '新增', 'AddSysNotice', null, 0, null, 2, null, 'sys:notice:add', 1, 0, 1, '', null, 22, '2025-06-09 18:51:22', null),
(67, '修改', 'EditSysNotice', null, 0, null, 2, null, 'sys:notice:edit', 1, 0, 1, '', null, 22, '2025-06-09 18:51:45', null),
(68, '删除', 'DeleteSysNotice', null, 0, null, 2, null, 'sys:notice:del', 1, 0, 1, '', null, 22, '2025-06-09 18:52:10', null),
(69, '新增业务', 'AddSysGenCodeBusiness', null, 0, null, 2, null, 'gen:code:business:add', 1, 0, 1, '', null, 23, '2025-06-09 18:53:07', null),
(70, '修改业务', 'EditGenCodeBusiness', null, 0, null, 2, null, 'gen:code:business:edit', 1, 0, 1, '', null, 23, '2025-06-09 18:53:45', null),
(71, '删除业务', 'DeleteGenCodeBusiness', null, 0, null, 2, null, 'gen:code:business:del', 1, 0, 1, '', null, 23, '2025-06-09 18:54:11', null),
(72, '新增模型', 'AddGenCodeModel', null, 0, null, 2, null, 'gen:code:model:add', 1, 0, 1, '', null, 23, '2025-06-09 18:54:45', null),
(73, '修改模型', 'EditGenCodeModel', null, 0, null, 2, null, 'gen:code:model:edit', 1, 0, 1, '', null, 23, '2025-06-09 18:55:08', null),
(74, '删除模型', 'DeleteGenCodeModel', null, 0, null, 2, null, 'gen:code:model:del', 1, 0, 1, '', null, 23, '2025-06-09 18:55:35', null),
(75, '导入', 'ImportGenCode', null, 0, null, 2, null, 'gen:code:import', 1, 0, 1, '', null, 23, '2025-06-09 18:58:16', null),
(76, '写入', 'WriteGenCode', null, 0, null, 2, null, 'gen:code:write', 1, 0, 1, '', null, 23, '2025-06-09 19:01:22', null),
(77, '删除', 'DeleteSysLoginLog', null, 0, null, 2, null, 'log:login:del', 1, 0, 1, '', null, 25, '2025-06-09 19:02:21', null),
(78, '清空', 'EmptyLoginLog', null, 0, null, 2, null, 'log:login:empty', 1, 0, 1, '', null, 25, '2025-06-09 19:02:50', null),
(79, '删除', 'DeleteOperaLog', null, 0, null, 2, null, 'log:opera:del', 1, 0, 1, '', null, 26, '2025-06-09 19:03:13', null),
(80, '清空', 'EmptyOperaLog', null, 0, null, 2, null, 'log:opera:empty', 1, 0, 1, '', null, 26, '2025-06-09 19:03:40', null),
(81, '下线', '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;
-92
View File
@@ -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>
+4 -4
View File
@@ -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']),
+1 -1
View File
@@ -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,
} }
+129
View File
@@ -0,0 +1,129 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import time
from dataclasses import dataclass
from backend.common.dataclasses import SnowflakeInfo
from backend.common.exception import errors
from backend.core.conf import settings
@dataclass(frozen=True)
class SnowflakeConfig:
"""雪花算法配置类"""
# 位分配
WORKER_ID_BITS: int = 5
DATACENTER_ID_BITS: int = 5
SEQUENCE_BITS: int = 12
# 最大值
MAX_WORKER_ID: int = (1 << WORKER_ID_BITS) - 1 # 31
MAX_DATACENTER_ID: int = (1 << DATACENTER_ID_BITS) - 1 # 31
SEQUENCE_MASK: int = (1 << SEQUENCE_BITS) - 1 # 4095
# 位移偏移
WORKER_ID_SHIFT: int = SEQUENCE_BITS
DATACENTER_ID_SHIFT: int = SEQUENCE_BITS + WORKER_ID_BITS
TIMESTAMP_LEFT_SHIFT: int = SEQUENCE_BITS + WORKER_ID_BITS + DATACENTER_ID_BITS
# 元年时间戳
EPOCH: int = 1262275200000
# 默认值
DEFAULT_DATACENTER_ID: int = 1
DEFAULT_WORKER_ID: int = 0
DEFAULT_SEQUENCE: int = 0
class Snowflake:
"""雪花算法类"""
def __init__(
self,
cluster_id: int = SnowflakeConfig.DEFAULT_DATACENTER_ID,
node_id: int = SnowflakeConfig.DEFAULT_WORKER_ID,
sequence: int = SnowflakeConfig.DEFAULT_SEQUENCE,
):
"""
初始化雪花算法生成器
:param cluster_id: 集群 ID (0-31)
:param node_id: 节点 ID (0-31)
:param sequence: 起始序列号
"""
if cluster_id < 0 or cluster_id > SnowflakeConfig.MAX_DATACENTER_ID:
raise errors.ForbiddenError(msg=f'集群编号必须在 0-{SnowflakeConfig.MAX_DATACENTER_ID} 之间')
if node_id < 0 or node_id > SnowflakeConfig.MAX_WORKER_ID:
raise errors.ForbiddenError(msg=f'节点编号必须在 0-{SnowflakeConfig.MAX_WORKER_ID} 之间')
self.node_id = node_id
self.cluster_id = cluster_id
self.sequence = sequence
self.last_timestamp = -1
@staticmethod
def _current_millis() -> int:
"""返回当前毫秒时间戳"""
return int(time.time() * 1000)
def _next_millis(self, last_timestamp: int) -> int:
"""
等待至下一毫秒
:param last_timestamp: 上次生成 ID 的时间戳
:return:
"""
timestamp = self._current_millis()
while timestamp <= last_timestamp:
time.sleep((last_timestamp - timestamp + 1) / 1000.0)
timestamp = self._current_millis()
return timestamp
def generate(self) -> int:
"""生成雪花 ID"""
timestamp = self._current_millis()
if timestamp < self.last_timestamp:
raise errors.ServerError(msg=f'系统时间倒退,拒绝生成 ID 直到 {self.last_timestamp}')
if timestamp == self.last_timestamp:
self.sequence = (self.sequence + 1) & SnowflakeConfig.SEQUENCE_MASK
if self.sequence == 0:
timestamp = self._next_millis(self.last_timestamp)
else:
self.sequence = 0
self.last_timestamp = timestamp
return (
((timestamp - SnowflakeConfig.EPOCH) << SnowflakeConfig.TIMESTAMP_LEFT_SHIFT)
| (self.cluster_id << SnowflakeConfig.DATACENTER_ID_SHIFT)
| (self.node_id << SnowflakeConfig.WORKER_ID_SHIFT)
| self.sequence
)
@staticmethod
def parse_id(snowflake_id: int) -> SnowflakeInfo:
"""
解析雪花 ID获取其包含的详细信息
:param snowflake_id: 雪花ID
:return:
"""
timestamp = (snowflake_id >> SnowflakeConfig.TIMESTAMP_LEFT_SHIFT) + SnowflakeConfig.EPOCH
cluster_id = (snowflake_id >> SnowflakeConfig.DATACENTER_ID_SHIFT) & SnowflakeConfig.MAX_DATACENTER_ID
node_id = (snowflake_id >> SnowflakeConfig.WORKER_ID_SHIFT) & SnowflakeConfig.MAX_WORKER_ID
sequence = snowflake_id & SnowflakeConfig.SEQUENCE_MASK
return SnowflakeInfo(
timestamp=timestamp,
datetime=time.strftime(settings.DATETIME_FORMAT, time.localtime(timestamp / 1000)),
cluster_id=cluster_id,
node_id=node_id,
sequence=sequence,
)
snowflake = Snowflake()
+1 -1
View File
@@ -46,7 +46,7 @@ dependencies = [
"python-socketio>=5.12.0", "python-socketio>=5.12.0",
"redis[hiredis]>=5.2.0", "redis[hiredis]>=5.2.0",
"rtoml>=0.12.0", "rtoml>=0.12.0",
"sqlalchemy-crud-plus>=1.8.0", "sqlalchemy-crud-plus>=1.10.0",
"sqlalchemy[asyncio]>=2.0.40", "sqlalchemy[asyncio]>=2.0.40",
"user-agents==2.2.0", "user-agents==2.2.0",
] ]
+1 -1
View File
@@ -252,7 +252,7 @@ sqlalchemy==2.0.40
# alembic # alembic
# fastapi-best-architecture # fastapi-best-architecture
# sqlalchemy-crud-plus # sqlalchemy-crud-plus
sqlalchemy-crud-plus==1.8.0 sqlalchemy-crud-plus==1.10.0
# via fastapi-best-architecture # via fastapi-best-architecture
starlette==0.46.1 starlette==0.46.1
# via # via
Generated
+5 -5
View File
@@ -674,7 +674,7 @@ requires-dist = [
{ name = "redis", extras = ["hiredis"], specifier = ">=5.2.0" }, { name = "redis", extras = ["hiredis"], specifier = ">=5.2.0" },
{ name = "rtoml", specifier = ">=0.12.0" }, { name = "rtoml", specifier = ">=0.12.0" },
{ name = "sqlalchemy", extras = ["asyncio"], specifier = ">=2.0.40" }, { name = "sqlalchemy", extras = ["asyncio"], specifier = ">=2.0.40" },
{ name = "sqlalchemy-crud-plus", specifier = ">=1.8.0" }, { name = "sqlalchemy-crud-plus", specifier = ">=1.10.0" },
{ name = "user-agents", specifier = "==2.2.0" }, { name = "user-agents", specifier = "==2.2.0" },
] ]
@@ -1891,15 +1891,15 @@ asyncio = [
[[package]] [[package]]
name = "sqlalchemy-crud-plus" name = "sqlalchemy-crud-plus"
version = "1.8.0" version = "1.10.0"
source = { registry = "https://mirrors.aliyun.com/pypi/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "pydantic" }, { name = "pydantic" },
{ name = "sqlalchemy" }, { name = "sqlalchemy" },
] ]
sdist = { url = "https://mirrors.aliyun.com/pypi/packages/dc/56/57a19b9a55910f73c80e802c42e9374f83752e6bfb5ad46829cf99e07758/sqlalchemy_crud_plus-1.8.0.tar.gz", hash = "sha256:cda7fc71a07887ac6fbbc423c0e061dfb912063b4ae207eccba31e08dd87aabb" } sdist = { url = "https://files.pythonhosted.org/packages/9b/47/fa0059f2ba24a0e8a6ded068460e23063a0b62b93ce1f3aa23fb7ae002fd/sqlalchemy_crud_plus-1.10.0.tar.gz", hash = "sha256:ce81cb0aac7f1e5cd1d6be794712d8f2a93f127c0936f4c3a2c79f3039eb2f62", size = 52871 }
wheels = [ wheels = [
{ url = "https://mirrors.aliyun.com/pypi/packages/f4/25/7bb0ecc055dee08e18682f67af26671c33974722f477dd74c1cc1400c6f8/sqlalchemy_crud_plus-1.8.0-py3-none-any.whl", hash = "sha256:15ac0c6ce83df3b89585f05df25a8b5e35baf846d3a20fd8305fb1ff58aef65b" }, { url = "https://files.pythonhosted.org/packages/a2/72/a7553fb70a41559d74654d9111644e8326d5d420588b59f39de872a3da8f/sqlalchemy_crud_plus-1.10.0-py3-none-any.whl", hash = "sha256:44db51256b57aa00757e48ade1e8be5ce6b6bd334fdd6d1fed5a22e97c6b0a6b", size = 10517 },
] ]
[[package]] [[package]]