mirror of
https://github.com/fastapi-practices/fastapi-best-architecture.git
synced 2026-09-21 13:12:24 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7afd8415cd | ||
|
|
845f2f0ff8 | ||
|
|
11d7792c0f | ||
|
|
6883ec34c6 | ||
|
|
4c45e7ff27 | ||
|
|
5621daea98 | ||
|
|
71d168dfea | ||
|
|
2d4365189d | ||
|
|
0c28a46e60 | ||
|
|
a4c7dae99c | ||
|
|
bfda0a226c | ||
|
|
c9bbb105a3 | ||
|
|
b415dc8912 | ||
|
|
05b74d71a3 | ||
|
|
10c0c691a7 | ||
|
|
0bc3786fc6 | ||
|
|
8e041ccb1b | ||
|
|
a82e9b7b9d | ||
|
|
7bab8cc636 | ||
|
|
7659b63d88 | ||
|
|
33ebc5723f | ||
|
|
86d9efd785 | ||
|
|
57ae7f03a6 | ||
|
|
6ee3983846 | ||
|
|
f05ed5cdda | ||
|
|
187dfa5344 | ||
|
|
918c8a9d33 | ||
|
|
ccfe35ea85 | ||
|
|
4a118c166a | ||
|
|
5f3823cb6d | ||
|
|
19ef52da9b | ||
|
|
39c3339a8d | ||
|
|
9f52629dcb | ||
|
|
1cddd6e2d5 | ||
|
|
ed10e6ff47 | ||
|
|
48574fb50a | ||
|
|
ee7fe3087d | ||
|
|
270ac3d941 | ||
|
|
2f76988076 | ||
|
|
b275481742 | ||
|
|
f3f1ceb595 |
+108
@@ -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>
|
||||
# [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]
|
||||
|
||||
|
||||
[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.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
|
||||
|
||||
@@ -4,11 +4,9 @@
|
||||
|
||||
# FastAPI Best Architecture
|
||||
|
||||
English | [简体中文](./README.zh-CN.md)
|
||||
|
||||
Enterprise-level backend architecture solution
|
||||
|
||||
**🔥Continuously updated and maintained🔥**
|
||||
English | [简体中文](./README.zh-CN.md)
|
||||
|
||||
[](https://github.com/fastapi-practices/fastapi_best_architecture/blob/master/LICENSE)
|
||||
[](https://www.python.org/downloads/)
|
||||
@@ -21,12 +19,10 @@ Enterprise-level backend architecture solution
|
||||

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

|
||||
[](https://deepwiki.com/fastapi-practices/fastapi_best_architecture)
|
||||
|
||||
</div>
|
||||
|
||||
> [!NOTE]
|
||||
> This repository as a template library open to any person or enterprise can be used for free!
|
||||
|
||||
## Pseudo 3-tier architecture
|
||||
|
||||
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 |
|
||||
| model | model / entity | model |
|
||||
|
||||
## Features
|
||||
|
||||
- [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
|
||||
## Help
|
||||
|
||||
For more details, please check
|
||||
the [official documentation](https://fastapi-practices.github.io/fastapi_best_architecture_docs/)
|
||||
|
||||
+3
-43
@@ -4,11 +4,9 @@
|
||||
|
||||
# FastAPI Best Architecture
|
||||
|
||||
简体中文 | [English](./README.md)
|
||||
|
||||
企业级后端架构解决方案
|
||||
|
||||
**🔥持续更新维护中🔥**
|
||||
简体中文 | [English](./README.md)
|
||||
|
||||
[](https://github.com/fastapi-practices/fastapi_best_architecture/blob/master/LICENSE)
|
||||
[](https://www.python.org/downloads/)
|
||||
@@ -21,12 +19,10 @@
|
||||

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

|
||||
[](https://deepwiki.com/fastapi-practices/fastapi_best_architecture)
|
||||
|
||||
</div>
|
||||
|
||||
> [!NOTE]
|
||||
> 此仓库作为模板库公开,任何人或企业均可免费使用!
|
||||
|
||||
## 伪三层架构
|
||||
|
||||
mvc 架构作为常规设计模式,在 python web 中也很常见,但是三层架构更令人着迷
|
||||
@@ -43,43 +39,7 @@ mvc 架构作为常规设计模式,在 python web 中也很常见,但是三
|
||||
| 数据访问 | dao / mapper | crud |
|
||||
| 模型 | 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/)
|
||||
|
||||
|
||||
@@ -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.service.auth_service import auth_service
|
||||
from backend.common.response.response_schema import ResponseModel, ResponseSchemaModel, response_base
|
||||
from backend.common.security.jwt import DependsJwtAuth
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@@ -35,13 +34,13 @@ async def user_login(
|
||||
return response_base.success(data=data)
|
||||
|
||||
|
||||
@router.post('/token/new', summary='创建新 token')
|
||||
async def create_new_token(request: Request) -> ResponseSchemaModel[GetNewToken]:
|
||||
@router.post('/tokens/refresh', summary='刷新 token')
|
||||
async def refresh_token(request: Request) -> ResponseSchemaModel[GetNewToken]:
|
||||
data = await auth_service.new_token(request=request)
|
||||
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:
|
||||
await auth_service.logout(request=request, response=response)
|
||||
return response_base.success()
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
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.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(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 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.response.response_schema import ResponseModel, ResponseSchemaModel, response_base
|
||||
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.get('', summary='获取令牌列表', dependencies=[DependsJwtAuth])
|
||||
async def get_tokens(
|
||||
@router.get('', summary='获取在线用户', dependencies=[DependsJwtAuth])
|
||||
async def get_online(
|
||||
username: Annotated[str | None, Query(description='用户名')] = None,
|
||||
) -> ResponseSchemaModel[list[GetTokenDetail]]:
|
||||
token_keys = await redis_client.keys(f'{settings.TOKEN_REDIS_PREFIX}:*')
|
||||
@@ -82,8 +82,10 @@ async def get_tokens(
|
||||
],
|
||||
)
|
||||
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:
|
||||
superuser_verify(request)
|
||||
await revoke_token(str(pk), obj.session_uuid)
|
||||
await revoke_token(str(pk), session_uuid)
|
||||
return response_base.success()
|
||||
@@ -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.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.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.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(data_rule_router, prefix='/data-rules', 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(plugin_router, prefix='/plugin', tags=['系统插件'])
|
||||
router.include_router(plugin_router, prefix='/plugins', tags=['系统插件'])
|
||||
|
||||
@@ -27,7 +27,7 @@ async def get_data_rule_models() -> ResponseSchemaModel[list[str]]:
|
||||
return response_base.success(data=models)
|
||||
|
||||
|
||||
@router.get('/model/{model}/columns', summary='获取数据规则可用模型列', dependencies=[DependsJwtAuth])
|
||||
@router.get('/models/{model}/columns', summary='获取数据规则可用模型列', dependencies=[DependsJwtAuth])
|
||||
async def get_data_rule_model_columns(
|
||||
model: Annotated[str, Path(description='模型名称')],
|
||||
) -> ResponseSchemaModel[list[GetDataRuleColumnDetail]]:
|
||||
|
||||
@@ -22,6 +22,12 @@ from backend.database.db import CurrentSession
|
||||
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])
|
||||
async def get_data_scope(
|
||||
pk: Annotated[int, Path(description='数据范围 ID')],
|
||||
|
||||
@@ -15,8 +15,8 @@ router = APIRouter()
|
||||
|
||||
|
||||
@router.get('/sidebar', summary='获取用户菜单侧边栏', description='适配 vben5', dependencies=[DependsJwtAuth])
|
||||
async def get_user_sidebar(request: Request) -> ResponseSchemaModel[list[dict[str, Any]]]:
|
||||
menu = await menu_service.get_user_menu_tree(request=request)
|
||||
async def get_user_sidebar(request: Request) -> ResponseSchemaModel[list[dict[str, Any] | None]]:
|
||||
menu = await menu_service.get_sidebar(request=request)
|
||||
return response_base.success(data=menu)
|
||||
|
||||
|
||||
@@ -71,7 +71,7 @@ async def update_menu(pk: Annotated[int, Path(description='菜单 ID')], obj: Up
|
||||
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)
|
||||
if count > 0:
|
||||
return response_base.success()
|
||||
|
||||
@@ -22,18 +22,18 @@ async def get_all_plugins() -> ResponseSchemaModel[list[dict[str, Any]]]:
|
||||
return response_base.success(data=plugins)
|
||||
|
||||
|
||||
@router.get('/changed', summary='插件状态是否变更', dependencies=[DependsJwtAuth])
|
||||
@router.get('/changes', summary='插件状态是否变更', dependencies=[DependsJwtAuth])
|
||||
async def plugin_changed() -> ResponseSchemaModel[bool]:
|
||||
plugins = await plugin_service.changed()
|
||||
return response_base.success(data=bool(plugins))
|
||||
|
||||
|
||||
@router.post(
|
||||
'/install/zip',
|
||||
'/zip',
|
||||
summary='安装 zip 插件',
|
||||
description='使用插件 zip 压缩包进行安装',
|
||||
dependencies=[
|
||||
Depends(RequestPermission('sys:plugin:install')),
|
||||
Depends(RequestPermission('sys:plugin:zip')),
|
||||
DependsRBAC,
|
||||
],
|
||||
)
|
||||
@@ -43,11 +43,11 @@ async def install_zip_plugin(file: Annotated[UploadFile, File()]) -> ResponseMod
|
||||
|
||||
|
||||
@router.post(
|
||||
'/install/git',
|
||||
'/git',
|
||||
summary='安装 git 插件',
|
||||
description='使用插件 git 仓库地址进行安装,不限制平台;如果需要凭证,需在 git 仓库地址中添加凭证信息',
|
||||
dependencies=[
|
||||
Depends(RequestPermission('sys:plugin:install')),
|
||||
Depends(RequestPermission('sys:plugin:git')),
|
||||
DependsRBAC,
|
||||
],
|
||||
)
|
||||
@@ -57,40 +57,33 @@ async def install_git_plugin(repo_url: Annotated[str, Query(description='插件
|
||||
|
||||
|
||||
@router.delete(
|
||||
'/uninstall',
|
||||
'/{plugin}',
|
||||
summary='卸载插件',
|
||||
description='此操作会直接删除插件依赖,但不会直接删除插件,而是将插件移动到备份目录',
|
||||
dependencies=[
|
||||
Depends(RequestPermission('sys:plugin:uninstall')),
|
||||
Depends(RequestPermission('sys:plugin:del')),
|
||||
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)
|
||||
return response_base.success(res=CustomResponseCode.PLUGIN_UNINSTALL_SUCCESS)
|
||||
|
||||
|
||||
@router.post(
|
||||
'/status',
|
||||
'/{plugin}/status',
|
||||
summary='更新插件状态',
|
||||
dependencies=[
|
||||
Depends(RequestPermission('sys:plugin:status')),
|
||||
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)
|
||||
return response_base.success()
|
||||
|
||||
|
||||
@router.get(
|
||||
'/zip/{plugin}',
|
||||
summary='打包插件',
|
||||
dependencies=[
|
||||
Depends(RequestPermission('sys:plugin:zip')),
|
||||
DependsRBAC,
|
||||
],
|
||||
)
|
||||
@router.get('/{plugin}', summary='打包并下载插件', dependencies=[DependsJwtAuth])
|
||||
async def build_plugin(plugin: Annotated[str, Path(description='插件名称')]) -> StreamingResponse:
|
||||
bio = await plugin_service.build(plugin=plugin)
|
||||
return StreamingResponse(
|
||||
|
||||
@@ -29,18 +29,10 @@ async def get_all_roles() -> ResponseSchemaModel[list[GetRoleDetail]]:
|
||||
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])
|
||||
async def get_role_all_menus(
|
||||
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)
|
||||
return response_base.success(data=menu)
|
||||
|
||||
@@ -106,7 +98,7 @@ async def update_role(pk: Annotated[int, Path(description='角色 ID')], obj: Up
|
||||
|
||||
|
||||
@router.put(
|
||||
'/{pk}/menu',
|
||||
'/{pk}/menus',
|
||||
summary='更新角色菜单',
|
||||
dependencies=[
|
||||
Depends(RequestPermission('sys:role:menu:edit')),
|
||||
@@ -123,7 +115,7 @@ async def update_role_menus(
|
||||
|
||||
|
||||
@router.put(
|
||||
'/{pk}/scope',
|
||||
'/{pk}/scopes',
|
||||
summary='更新角色数据范围',
|
||||
dependencies=[
|
||||
Depends(RequestPermission('sys:role:scope:edit')),
|
||||
|
||||
@@ -4,15 +4,13 @@ from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, Depends, Path, Query, Request
|
||||
|
||||
from backend.app.admin.schema.role import GetRoleDetail
|
||||
from backend.app.admin.schema.user import (
|
||||
AddUserParam,
|
||||
AvatarParam,
|
||||
GetCurrentUserInfoWithRelationDetail,
|
||||
GetUserInfoWithRelationDetail,
|
||||
RegisterUserParam,
|
||||
ResetPasswordParam,
|
||||
UpdateUserParam,
|
||||
UpdateUserRoleParam,
|
||||
)
|
||||
from backend.app.admin.service.user_service import user_service
|
||||
from backend.common.pagination import DependsPagination, PageData, paging_data
|
||||
@@ -25,12 +23,6 @@ from backend.database.db import CurrentSession
|
||||
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])
|
||||
async def add_user(request: Request, obj: AddUserParam) -> ResponseSchemaModel[GetUserInfoWithRelationDetail]:
|
||||
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)
|
||||
|
||||
|
||||
@router.post('/password/reset', summary='密码重置', dependencies=[DependsJwtAuth])
|
||||
async def password_reset(request: Request, obj: ResetPasswordParam) -> ResponseModel:
|
||||
count = await user_service.pwd_reset(request=request, obj=obj)
|
||||
@router.post('/{username}/password', summary='密码重置', dependencies=[DependsJwtAuth])
|
||||
async def password_reset(
|
||||
username: Annotated[str, Path(description='用户名')], obj: ResetPasswordParam
|
||||
) -> ResponseModel:
|
||||
count = await user_service.pwd_reset(username=username, obj=obj)
|
||||
if count > 0:
|
||||
return response_base.success()
|
||||
return response_base.fail()
|
||||
@@ -60,39 +54,12 @@ async def get_user(
|
||||
return response_base.success(data=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(
|
||||
'/{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('/{username}/roles', summary='获取用户所有角色', dependencies=[DependsJwtAuth])
|
||||
async def get_user_all_roles(
|
||||
username: Annotated[str, Path(description='用户名')],
|
||||
) -> ResponseSchemaModel[list[GetRoleDetail]]:
|
||||
data = await user_service.get_roles(username=username)
|
||||
return response_base.success(data=data)
|
||||
|
||||
|
||||
@router.get(
|
||||
@@ -115,6 +82,16 @@ async def get_pagination_users(
|
||||
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])
|
||||
async def super_set(request: Request, pk: Annotated[int, Path(description='用户 ID')]) -> ResponseModel:
|
||||
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(
|
||||
path='/{username}',
|
||||
summary='用户注销',
|
||||
description='用户注销 != 用户登出,注销之后用户将从数据库删除',
|
||||
summary='删除用户',
|
||||
dependencies=[
|
||||
Depends(RequestPermission('sys:user:del')),
|
||||
DependsRBAC,
|
||||
|
||||
@@ -2,9 +2,8 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from typing import Sequence
|
||||
|
||||
from sqlalchemy import Select, and_, desc, select
|
||||
from sqlalchemy import Select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import noload
|
||||
from sqlalchemy_crud_plus import CRUDPlus
|
||||
|
||||
from backend.app.admin.model import DataRule
|
||||
@@ -31,16 +30,12 @@ class CRUDDataRule(CRUDPlus[DataRule]):
|
||||
:param name: 规则名称
|
||||
:return:
|
||||
"""
|
||||
stmt = select(self.model).options(noload(self.model.scope)).order_by(desc(self.model.created_time))
|
||||
filters = {}
|
||||
|
||||
filters = []
|
||||
if name is not None:
|
||||
filters.append(self.model.name.like(f'%{name}%'))
|
||||
filters['name__like'] = f'%{name}%'
|
||||
|
||||
if filters:
|
||||
stmt = stmt.where(and_(*filters))
|
||||
|
||||
return stmt
|
||||
return await self.select_order('id', load_strategies={'scopes': 'noload'}, **filters)
|
||||
|
||||
async def get_by_name(self, db: AsyncSession, name: str) -> DataRule | None:
|
||||
"""
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- 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.orm import noload, selectinload
|
||||
from sqlalchemy_crud_plus import CRUDPlus
|
||||
|
||||
from backend.app.admin.model import DataRule, DataScope
|
||||
@@ -40,9 +41,16 @@ class CRUDDataScope(CRUDPlus[DataScope]):
|
||||
:param pk: 范围 ID
|
||||
:return:
|
||||
"""
|
||||
stmt = select(self.model).options(selectinload(self.model.rules)).where(self.model.id == pk)
|
||||
data_scope = await db.execute(stmt)
|
||||
return data_scope.scalars().first()
|
||||
return await self.select_model(db, pk, load_strategies=['rules'])
|
||||
|
||||
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:
|
||||
"""
|
||||
@@ -52,22 +60,14 @@ class CRUDDataScope(CRUDPlus[DataScope]):
|
||||
:param status: 范围状态
|
||||
:return:
|
||||
"""
|
||||
stmt = (
|
||||
select(self.model)
|
||||
.options(noload(self.model.rules), noload(self.model.roles))
|
||||
.order_by(desc(self.model.created_time))
|
||||
)
|
||||
filters = {}
|
||||
|
||||
filters = []
|
||||
if name is not None:
|
||||
filters.append(self.model.name.like(f'%{name}%'))
|
||||
filters['name__like'] = f'%{name}%'
|
||||
if status is not None:
|
||||
filters.append(self.model.status == status)
|
||||
filters['status'] = status
|
||||
|
||||
if filters:
|
||||
stmt = stmt.where(and_(*filters))
|
||||
|
||||
return stmt
|
||||
return await self.select_order('id', load_strategies={'rules': 'noload', 'roles': 'noload'}, **filters)
|
||||
|
||||
async def create(self, db: AsyncSession, obj: CreateDataScopeParam) -> None:
|
||||
"""
|
||||
|
||||
@@ -3,9 +3,7 @@
|
||||
from typing import Sequence
|
||||
|
||||
from fastapi import Request
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import selectinload
|
||||
from sqlalchemy_crud_plus import CRUDPlus
|
||||
|
||||
from backend.app.admin.model import Dept
|
||||
@@ -56,16 +54,19 @@ class CRUDDept(CRUDPlus[Dept]):
|
||||
:param status: 部门状态
|
||||
:return:
|
||||
"""
|
||||
filters = {'del_flag__eq': 0}
|
||||
filters = {'del_flag': 0}
|
||||
|
||||
if name is not None:
|
||||
filters.update(name__like=f'%{name}%')
|
||||
filters['name__like'] = f'%{name}%'
|
||||
if leader is not None:
|
||||
filters.update(leader__like=f'%{leader}%')
|
||||
filters['leader__like'] = f'%{leader}%'
|
||||
if phone is not None:
|
||||
filters.update(phone__startswith=phone)
|
||||
filters['phone__startswith'] = phone
|
||||
if status is not None:
|
||||
filters.update(status=status)
|
||||
return await self.select_models_order(db, 'sort', None, await filter_data_permission(db, request), **filters)
|
||||
filters['status'] = status
|
||||
|
||||
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:
|
||||
"""
|
||||
@@ -106,9 +107,7 @@ class CRUDDept(CRUDPlus[Dept]):
|
||||
:param dept_id: 部门 ID
|
||||
:return:
|
||||
"""
|
||||
stmt = select(self.model).options(selectinload(self.model.users)).where(self.model.id == dept_id)
|
||||
result = await db.execute(stmt)
|
||||
return result.scalars().first()
|
||||
return await self.select_model(db, dept_id, load_strategies=['users'])
|
||||
|
||||
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
|
||||
:return:
|
||||
"""
|
||||
stmt = select(self.model).where(self.model.parent_id == dept_id, self.model.del_flag == 0)
|
||||
result = await db.execute(stmt)
|
||||
return result.scalars().all()
|
||||
return await self.select_models(db, parent_id=dept_id, del_flag=0)
|
||||
|
||||
|
||||
dept_dao: CRUDDept = CRUDDept(Dept)
|
||||
|
||||
@@ -21,12 +21,14 @@ class CRUDLoginLog(CRUDPlus[LoginLog]):
|
||||
:return:
|
||||
"""
|
||||
filters = {}
|
||||
|
||||
if username is not None:
|
||||
filters.update(username__like=f'%{username}%')
|
||||
filters['username__like'] = f'%{username}%'
|
||||
if status is not None:
|
||||
filters.update(status=status)
|
||||
filters['status'] = status
|
||||
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)
|
||||
|
||||
async def create(self, db: AsyncSession, obj: CreateLoginLogParam) -> None:
|
||||
|
||||
@@ -2,9 +2,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from typing import Sequence
|
||||
|
||||
from sqlalchemy import and_, asc, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import selectinload
|
||||
from sqlalchemy_crud_plus import CRUDPlus
|
||||
|
||||
from backend.app.admin.model import Menu
|
||||
@@ -44,13 +42,15 @@ class CRUDMenu(CRUDPlus[Menu]):
|
||||
:return:
|
||||
"""
|
||||
filters = {}
|
||||
|
||||
if title is not None:
|
||||
filters.update(title=f'%{title}%')
|
||||
filters['title__like'] = f'%{title}%'
|
||||
if status is not None:
|
||||
filters.update(status=status)
|
||||
filters['status'] = status
|
||||
|
||||
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 列表
|
||||
:return:
|
||||
"""
|
||||
stmt = select(self.model).order_by(asc(self.model.sort))
|
||||
filters = [self.model.type.in_([0, 1])]
|
||||
filters = {'type__in': [0, 1, 3, 4]}
|
||||
|
||||
if not superuser:
|
||||
filters.append(self.model.id.in_(menu_ids))
|
||||
stmt = stmt.where(and_(*filters))
|
||||
menu = await db.execute(stmt)
|
||||
return menu.scalars().all()
|
||||
filters['id__in'] = menu_ids
|
||||
|
||||
return await self.select_models_order(db, 'sort', 'asc', **filters)
|
||||
|
||||
async def create(self, db: AsyncSession, obj: CreateMenuParam) -> None:
|
||||
"""
|
||||
@@ -106,9 +105,7 @@ class CRUDMenu(CRUDPlus[Menu]):
|
||||
:param menu_id: 菜单 ID
|
||||
:return:
|
||||
"""
|
||||
stmt = select(self.model).options(selectinload(self.model.children)).where(self.model.id == menu_id)
|
||||
result = await db.execute(stmt)
|
||||
menu = result.scalars().first()
|
||||
menu = await self.select_model(db, menu_id, load_strategies=['children'])
|
||||
return menu.children
|
||||
|
||||
|
||||
|
||||
@@ -21,12 +21,14 @@ class CRUDOperaLogDao(CRUDPlus[OperaLog]):
|
||||
:return:
|
||||
"""
|
||||
filters = {}
|
||||
|
||||
if username is not None:
|
||||
filters.update(username__like=f'%{username}%')
|
||||
filters['username__like'] = f'%{username}%'
|
||||
if status is not None:
|
||||
filters.update(status=status)
|
||||
filters['status__eq'] = status
|
||||
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)
|
||||
|
||||
async def create(self, db: AsyncSession, obj: CreateOperaLogParam) -> None:
|
||||
|
||||
@@ -2,12 +2,11 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from typing import Sequence
|
||||
|
||||
from sqlalchemy import Select, and_, desc, select
|
||||
from sqlalchemy import Select, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import noload, selectinload
|
||||
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 (
|
||||
CreateRoleParam,
|
||||
UpdateRoleMenuParam,
|
||||
@@ -37,13 +36,7 @@ class CRUDRole(CRUDPlus[Role]):
|
||||
:param role_id: 角色 ID
|
||||
:return:
|
||||
"""
|
||||
stmt = (
|
||||
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()
|
||||
return await self.select_model(db, role_id, load_strategies=['menus', 'scopes'])
|
||||
|
||||
async def get_all(self, db: AsyncSession) -> Sequence[Role]:
|
||||
"""
|
||||
@@ -54,18 +47,6 @@ class CRUDRole(CRUDPlus[Role]):
|
||||
"""
|
||||
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:
|
||||
"""
|
||||
获取角色列表
|
||||
@@ -74,22 +55,23 @@ class CRUDRole(CRUDPlus[Role]):
|
||||
:param status: 角色状态
|
||||
: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:
|
||||
filters.append(self.model.name.like(f'%{name}%'))
|
||||
filters['name__like'] = f'%{name}%'
|
||||
if status is not None:
|
||||
filters.append(self.model.status == status)
|
||||
filters['status'] = status
|
||||
|
||||
if filters:
|
||||
stmt = stmt.where(and_(*filters))
|
||||
|
||||
return stmt
|
||||
return await self.select_order(
|
||||
'id',
|
||||
load_strategies={
|
||||
'users': 'noload',
|
||||
'menus': 'noload',
|
||||
'scopes': 'noload',
|
||||
},
|
||||
**filters,
|
||||
)
|
||||
|
||||
async def get_by_name(self, db: AsyncSession, name: str) -> Role | None:
|
||||
"""
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import bcrypt
|
||||
|
||||
from sqlalchemy import and_, desc, select
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import noload, selectinload
|
||||
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.schema.user import (
|
||||
AddOAuth2UserParam,
|
||||
AddUserParam,
|
||||
AvatarParam,
|
||||
RegisterUserParam,
|
||||
UpdateUserParam,
|
||||
UpdateUserRoleParam,
|
||||
)
|
||||
from backend.common.security.jwt import get_hash_password
|
||||
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)
|
||||
|
||||
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:
|
||||
"""
|
||||
添加用户
|
||||
@@ -97,14 +75,33 @@ class CRUDUser(CRUDPlus[User]):
|
||||
dict_obj.update({'salt': salt})
|
||||
new_user = self.model(**dict_obj)
|
||||
|
||||
role_list = []
|
||||
for role_id in obj.roles:
|
||||
role_list.append(await db.get(Role, role_id))
|
||||
new_user.roles.extend(role_list)
|
||||
stmt = select(Role).where(Role.id.in_(obj.roles))
|
||||
roles = await db.execute(stmt)
|
||||
new_user.roles = roles.scalars().all()
|
||||
|
||||
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: 更新用户参数
|
||||
: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
|
||||
async def update_role(db: AsyncSession, input_user: User, obj: UpdateUserRoleParam) -> None:
|
||||
"""
|
||||
更新用户角色
|
||||
stmt = select(Role).where(Role.id.in_(role_ids))
|
||||
roles = await db.execute(stmt)
|
||||
input_user.roles = roles.scalars().all()
|
||||
return count
|
||||
|
||||
:param db: 数据库会话
|
||||
: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:
|
||||
async def update_avatar(self, db: AsyncSession, user_id: int, avatar: str) -> int:
|
||||
"""
|
||||
更新用户头像
|
||||
|
||||
:param db: 数据库会话
|
||||
:param input_user: 用户 ID
|
||||
:param user_id: 用户 ID
|
||||
:param avatar: 头像地址
|
||||
: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:
|
||||
"""
|
||||
@@ -156,7 +142,7 @@ class CRUDUser(CRUDPlus[User]):
|
||||
|
||||
async def check_email(self, db: AsyncSession, email: str) -> User | None:
|
||||
"""
|
||||
检查邮箱是否已被注册
|
||||
检查邮箱是否已被绑定
|
||||
|
||||
:param db: 数据库会话
|
||||
:param email: 电子邮箱
|
||||
@@ -185,29 +171,26 @@ class CRUDUser(CRUDPlus[User]):
|
||||
:param status: 用户状态
|
||||
:return:
|
||||
"""
|
||||
stmt = (
|
||||
select(self.model)
|
||||
.options(
|
||||
filters = {}
|
||||
|
||||
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.roles).options(noload(Role.users), noload(Role.menus), noload(Role.scopes)),
|
||||
],
|
||||
**filters,
|
||||
)
|
||||
.order_by(desc(self.model.join_time))
|
||||
)
|
||||
|
||||
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:
|
||||
"""
|
||||
@@ -308,22 +291,19 @@ class CRUDUser(CRUDPlus[User]):
|
||||
:param username: 用户名
|
||||
:return:
|
||||
"""
|
||||
stmt = select(self.model).options(
|
||||
selectinload(self.model.dept),
|
||||
selectinload(self.model.roles).options(selectinload(Role.menus), selectinload(Role.scopes)),
|
||||
)
|
||||
filters = {}
|
||||
|
||||
filters = []
|
||||
if user_id:
|
||||
filters.append(self.model.id == user_id)
|
||||
filters['id'] = user_id
|
||||
if username:
|
||||
filters.append(self.model.username == username)
|
||||
filters['username'] = username
|
||||
|
||||
if filters:
|
||||
stmt = stmt.where(and_(*filters))
|
||||
|
||||
user = await db.execute(stmt)
|
||||
return user.scalars().first()
|
||||
return await self.select_model_by_column(
|
||||
db,
|
||||
load_options=[selectinload(self.model.roles).options(selectinload(Role.menus), selectinload(Role.scopes))],
|
||||
load_strategies=['dept'],
|
||||
**filters,
|
||||
)
|
||||
|
||||
|
||||
user_dao: CRUDUser = CRUDUser(User)
|
||||
|
||||
@@ -4,9 +4,10 @@ from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from sqlalchemy import ForeignKey, String
|
||||
from sqlalchemy import String
|
||||
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
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -28,8 +29,5 @@ class DataRule(Base):
|
||||
)
|
||||
value: Mapped[str] = mapped_column(String(255), comment='规则值')
|
||||
|
||||
# 数据范围规则一对多
|
||||
scope_id: Mapped[int | None] = mapped_column(
|
||||
ForeignKey('sys_data_scope.id', ondelete='SET NULL'), default=None, comment='数据范围关联 ID'
|
||||
)
|
||||
scope: Mapped[DataScope] = relationship(init=False, back_populates='rules')
|
||||
# 数据范围规则多对多
|
||||
scopes: Mapped[list[DataScope]] = relationship(init=False, secondary=sys_data_scope_rule, back_populates='rules')
|
||||
|
||||
@@ -7,7 +7,7 @@ from typing import TYPE_CHECKING
|
||||
from sqlalchemy import String
|
||||
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
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -23,8 +23,8 @@ class DataScope(Base):
|
||||
name: Mapped[str] = mapped_column(String(50), unique=True, comment='名称')
|
||||
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')
|
||||
|
||||
@@ -4,7 +4,7 @@ from __future__ import annotations
|
||||
|
||||
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.orm import Mapped, mapped_column, relationship
|
||||
|
||||
@@ -32,7 +32,7 @@ class Dept(Base):
|
||||
|
||||
# 父级部门一对多
|
||||
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])
|
||||
children: Mapped[Optional[list['Dept']]] = relationship(init=False, back_populates='parent')
|
||||
|
||||
@@ -1,35 +1,55 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
from sqlalchemy import INT, Column, ForeignKey, Integer, Table
|
||||
from sqlalchemy import BigInteger, Column, ForeignKey, Table
|
||||
|
||||
from backend.common.model import MappedBase
|
||||
|
||||
sys_user_role = Table(
|
||||
'sys_user_role',
|
||||
MappedBase.metadata,
|
||||
Column('id', INT, 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('role_id', Integer, ForeignKey('sys_role.id', ondelete='CASCADE'), primary_key=True, comment='角色ID'),
|
||||
Column('id', BigInteger, primary_key=True, unique=True, index=True, autoincrement=True, comment='主键ID'),
|
||||
Column('user_id', BigInteger, ForeignKey('sys_user.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',
|
||||
MappedBase.metadata,
|
||||
Column('id', INT, 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('menu_id', Integer, ForeignKey('sys_menu.id', ondelete='CASCADE'), primary_key=True, comment='菜单ID'),
|
||||
Column('id', BigInteger, primary_key=True, unique=True, index=True, autoincrement=True, comment='主键ID'),
|
||||
Column('role_id', BigInteger, ForeignKey('sys_role.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',
|
||||
MappedBase.metadata,
|
||||
Column('id', INT, 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('id', BigInteger, primary_key=True, unique=True, index=True, autoincrement=True, comment='主键 ID'),
|
||||
Column('role_id', BigInteger, ForeignKey('sys_role.id', ondelete='CASCADE'), primary_key=True, comment='角色 ID'),
|
||||
Column(
|
||||
'data_scope_id',
|
||||
Integer,
|
||||
BigInteger,
|
||||
ForeignKey('sys_data_scope.id', ondelete='CASCADE'),
|
||||
primary_key=True,
|
||||
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,7 +4,7 @@ from __future__ import annotations
|
||||
|
||||
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.postgresql import TEXT
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
@@ -24,10 +24,10 @@ class Menu(Base):
|
||||
id: Mapped[id_key] = mapped_column(init=False)
|
||||
title: 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='排序')
|
||||
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='组件路径')
|
||||
perms: Mapped[str | None] = mapped_column(String(100), default=None, comment='权限标识')
|
||||
status: Mapped[int] = mapped_column(default=1, comment='菜单状态(0停用 1正常)')
|
||||
@@ -42,7 +42,7 @@ class Menu(Base):
|
||||
|
||||
# 父级菜单一对多
|
||||
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])
|
||||
children: Mapped[Optional[list['Menu']]] = relationship(init=False, back_populates='parent')
|
||||
|
||||
@@ -4,9 +4,9 @@ from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from sqlalchemy import String
|
||||
from sqlalchemy import Boolean, String
|
||||
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 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)
|
||||
name: Mapped[str] = mapped_column(String(20), unique=True, comment='角色名称')
|
||||
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(
|
||||
LONGTEXT().with_variant(TEXT, 'postgresql'), default=None, comment='备注'
|
||||
)
|
||||
|
||||
@@ -26,22 +26,22 @@ class User(Base):
|
||||
id: Mapped[id_key] = mapped_column(init=False)
|
||||
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='用户名')
|
||||
nickname: Mapped[str] = mapped_column(String(20), unique=True, comment='昵称')
|
||||
password: Mapped[str | None] = mapped_column(String(255), comment='密码')
|
||||
salt: Mapped[bytes | None] = mapped_column(VARBINARY(255).with_variant(BYTEA(255), 'postgresql'), comment='加密盐')
|
||||
email: Mapped[str] = mapped_column(String(50), unique=True, index=True, comment='邮箱')
|
||||
nickname: Mapped[str] = mapped_column(String(20), comment='昵称')
|
||||
password: Mapped[str] = mapped_column(String(255), comment='密码')
|
||||
salt: Mapped[bytes] = mapped_column(VARBINARY(255).with_variant(BYTEA(255), 'postgresql'), 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(
|
||||
Boolean().with_variant(INTEGER, 'postgresql'), default=False, comment='超级权限(0否 1是)'
|
||||
)
|
||||
is_staff: Mapped[bool] = mapped_column(
|
||||
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(
|
||||
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(
|
||||
DateTime(timezone=True), init=False, default_factory=timezone.now, comment='注册时间'
|
||||
)
|
||||
|
||||
@@ -13,11 +13,11 @@ class MenuSchemaBase(SchemaBase):
|
||||
|
||||
title: 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')
|
||||
sort: int = Field(0, ge=0, 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='组件路径')
|
||||
perms: str | None = Field(None, description='权限标识')
|
||||
status: StatusType = Field(StatusType.enable, description='状态')
|
||||
|
||||
@@ -15,6 +15,7 @@ class RoleSchemaBase(SchemaBase):
|
||||
|
||||
name: str = Field(description='角色名称')
|
||||
status: StatusType = Field(StatusType.enable, description='状态')
|
||||
is_filter_scopes: bool = Field(True, description='过滤数据权限')
|
||||
remark: str | None = Field(None, description='备注')
|
||||
|
||||
|
||||
|
||||
@@ -35,12 +35,6 @@ class GetLoginToken(AccessTokenBase):
|
||||
user: GetUserInfoDetail = Field(description='用户信息')
|
||||
|
||||
|
||||
class KickOutToken(SchemaBase):
|
||||
"""踢出令牌"""
|
||||
|
||||
session_uuid: str = Field(description='会话 UUID')
|
||||
|
||||
|
||||
class GetTokenDetail(SchemaBase):
|
||||
"""令牌详情"""
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ from typing_extensions import Self
|
||||
from backend.app.admin.schema.dept import GetDeptDetail
|
||||
from backend.app.admin.schema.role import GetRoleWithRelationDetail
|
||||
from backend.common.enums import StatusType
|
||||
from backend.common.schema import CustomPhoneNumber, SchemaBase
|
||||
from backend.common.schema import CustomEmailStr, CustomPhoneNumber, SchemaBase
|
||||
|
||||
|
||||
class AuthSchemaBase(SchemaBase):
|
||||
@@ -25,20 +25,20 @@ class AuthLoginParam(AuthSchemaBase):
|
||||
captcha: str = Field(description='验证码')
|
||||
|
||||
|
||||
class RegisterUserParam(AuthSchemaBase):
|
||||
"""用户注册参数"""
|
||||
|
||||
nickname: str | None = Field(None, description='昵称')
|
||||
email: EmailStr = Field(examples=['user@example.com'], description='邮箱')
|
||||
|
||||
|
||||
class AddUserParam(AuthSchemaBase):
|
||||
"""添加用户参数"""
|
||||
|
||||
dept_id: int = Field(description='部门 ID')
|
||||
roles: list[int] = Field(description='角色 ID 列表')
|
||||
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):
|
||||
@@ -55,26 +55,15 @@ class UserInfoSchemaBase(SchemaBase):
|
||||
dept_id: int | None = Field(None, description='部门 ID')
|
||||
username: str = Field(description='用户名')
|
||||
nickname: str = Field(description='昵称')
|
||||
email: EmailStr = Field(examples=['user@example.com'], description='邮箱')
|
||||
phone: CustomPhoneNumber | None = Field(None, description='手机号')
|
||||
avatar: HttpUrl | None = Field(None, description='头像地址')
|
||||
|
||||
|
||||
class UpdateUserParam(UserInfoSchemaBase):
|
||||
"""更新用户参数"""
|
||||
|
||||
|
||||
class UpdateUserRoleParam(SchemaBase):
|
||||
"""更新用户角色参数"""
|
||||
|
||||
roles: list[int] = Field(description='角色 ID 列表')
|
||||
|
||||
|
||||
class AvatarParam(SchemaBase):
|
||||
"""更新头像参数"""
|
||||
|
||||
url: HttpUrl = Field(description='头像 http 地址')
|
||||
|
||||
|
||||
class GetUserInfoDetail(UserInfoSchemaBase):
|
||||
"""用户信息详情"""
|
||||
|
||||
@@ -83,7 +72,8 @@ class GetUserInfoDetail(UserInfoSchemaBase):
|
||||
dept_id: int | None = Field(None, description='部门 ID')
|
||||
id: int = Field(description='用户 ID')
|
||||
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='状态')
|
||||
is_superuser: bool = Field(description='是否超级管理员')
|
||||
is_staff: bool = Field(description='是否管理员')
|
||||
|
||||
@@ -92,7 +92,7 @@ class AuthService:
|
||||
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}')
|
||||
if not captcha_code:
|
||||
raise errors.AuthorizationError(msg='验证码失效,请重新获取')
|
||||
raise errors.ForbiddenError(msg='验证码失效,请重新获取')
|
||||
if captcha_code.lower() != obj.captcha.lower():
|
||||
raise errors.CustomError(error=CustomErrorCode.CAPTCHA_ERROR)
|
||||
await redis_client.delete(f'{settings.CAPTCHA_LOGIN_REDIS_PREFIX}:{request.state.ip}')
|
||||
@@ -121,7 +121,7 @@ class AuthService:
|
||||
except errors.NotFoundError as e:
|
||||
log.error('登陆错误: 用户名不存在')
|
||||
raise errors.NotFoundError(msg=e.msg)
|
||||
except (errors.AuthorizationError, errors.CustomError) as e:
|
||||
except (errors.ForbiddenError, errors.CustomError) as e:
|
||||
if not user:
|
||||
log.error('登陆错误: 用户密码有误')
|
||||
task = BackgroundTask(
|
||||
@@ -136,7 +136,7 @@ class AuthService:
|
||||
msg=e.msg,
|
||||
),
|
||||
)
|
||||
raise errors.AuthorizationError(msg=e.msg, background=task)
|
||||
raise errors.RequestError(msg=e.msg, background=task)
|
||||
except Exception as e:
|
||||
log.error(f'登陆错误: {e}')
|
||||
raise e
|
||||
@@ -211,11 +211,17 @@ class AuthService:
|
||||
:param response: FastAPI 响应对象
|
||||
:return:
|
||||
"""
|
||||
try:
|
||||
token = get_token(request)
|
||||
token_payload = jwt_decode(token)
|
||||
user_id = token_payload.id
|
||||
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:
|
||||
await redis_client.delete(f'{settings.TOKEN_REDIS_PREFIX}:{user_id}:{token_payload.session_uuid}')
|
||||
if refresh_token:
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
from typing import Sequence
|
||||
|
||||
from sqlalchemy import Select
|
||||
|
||||
from backend.app.admin.crud.crud_data_scope import data_scope_dao
|
||||
@@ -28,6 +30,13 @@ class DataScopeService:
|
||||
raise errors.NotFoundError(msg='数据范围不存在')
|
||||
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
|
||||
async def get_rules(*, pk: int) -> DataScope:
|
||||
"""
|
||||
|
||||
@@ -41,27 +41,34 @@ class MenuService:
|
||||
:return:
|
||||
"""
|
||||
async with async_db_session() as db:
|
||||
menu_select = await menu_dao.get_all(db, title=title, status=status)
|
||||
menu_tree = get_tree_data(menu_select)
|
||||
menu_data = await menu_dao.get_all(db, title=title, status=status)
|
||||
menu_tree = get_tree_data(menu_data)
|
||||
return menu_tree
|
||||
|
||||
@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 请求对象
|
||||
:return:
|
||||
"""
|
||||
async with async_db_session() as db:
|
||||
roles = request.user.roles
|
||||
menu_ids = []
|
||||
menu_tree = []
|
||||
if roles:
|
||||
unique_menus = {}
|
||||
for role in roles:
|
||||
menu_ids.extend([menu.id for menu in role.menus])
|
||||
menu_select = await menu_dao.get_role_menus(db, request.user.is_superuser, menu_ids)
|
||||
menu_tree = get_vben5_tree_data(menu_select)
|
||||
for menu in role.menus:
|
||||
unique_menus[menu.id] = menu
|
||||
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
|
||||
|
||||
@staticmethod
|
||||
|
||||
@@ -58,21 +58,14 @@ class PluginService:
|
||||
raise errors.ForbiddenError(msg='插件压缩包格式非法')
|
||||
with zipfile.ZipFile(file_bytes) as zf:
|
||||
# 校验压缩包
|
||||
plugin_dir = file.filename[:-4]
|
||||
members_in_plugin_dir = [name for name in zf.namelist() if name.startswith(plugin_dir)]
|
||||
if not members_in_plugin_dir:
|
||||
plugin_namelist = zf.namelist()
|
||||
plugin_name = plugin_namelist[0].split('/')[0]
|
||||
if not plugin_namelist or plugin_name not in file.filename:
|
||||
raise errors.ForbiddenError(msg='插件压缩包内容非法')
|
||||
plugin_name = (
|
||||
members_in_plugin_dir[0]
|
||||
.replace('/', '')
|
||||
.replace('-master', '')
|
||||
.replace('-main', '')
|
||||
.replace('-dev', '')
|
||||
)
|
||||
if (
|
||||
len(members_in_plugin_dir) <= 3
|
||||
or f'{plugin_dir}/plugin.toml' not in members_in_plugin_dir
|
||||
or f'{plugin_dir}/README.md' not in members_in_plugin_dir
|
||||
len(plugin_namelist) <= 3
|
||||
or f'{plugin_name}/plugin.toml' not in plugin_namelist
|
||||
or f'{plugin_name}/README.md' not in plugin_namelist
|
||||
):
|
||||
raise errors.ForbiddenError(msg='插件压缩包内缺少必要文件')
|
||||
|
||||
@@ -86,8 +79,8 @@ class PluginService:
|
||||
# 解压(安装)
|
||||
members = []
|
||||
for member in zf.infolist():
|
||||
if member.filename.startswith(plugin_dir):
|
||||
new_filename = member.filename.replace(plugin_dir, '')
|
||||
if member.filename.startswith(plugin_name):
|
||||
new_filename = member.filename.replace(plugin_name, '')
|
||||
if new_filename:
|
||||
member.filename = new_filename
|
||||
members.append(member)
|
||||
@@ -182,7 +175,7 @@ class PluginService:
|
||||
for file in files:
|
||||
file_path = os.path.join(root, file)
|
||||
arcname = os.path.relpath(file_path, start=plugin_dir)
|
||||
zf.write(file_path, arcname)
|
||||
zf.write(file_path, os.path.join(plugin, arcname))
|
||||
|
||||
bio.seek(0)
|
||||
return bio
|
||||
|
||||
@@ -45,18 +45,6 @@ class RoleService:
|
||||
roles = await role_dao.get_all(db)
|
||||
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
|
||||
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)
|
||||
|
||||
@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)
|
||||
if not role:
|
||||
raise errors.NotFoundError(msg='角色不存在')
|
||||
menu_ids = [menu.id for menu in role.menus]
|
||||
menu_select = await menu_dao.get_role_menus(db, False, menu_ids)
|
||||
menu_tree = get_tree_data(menu_select)
|
||||
menu_tree = get_tree_data(role.menus) if role.menus else []
|
||||
return menu_tree
|
||||
|
||||
@staticmethod
|
||||
@@ -128,8 +114,7 @@ class RoleService:
|
||||
if not role:
|
||||
raise errors.NotFoundError(msg='角色不存在')
|
||||
if role.name != obj.name:
|
||||
role = await role_dao.get_by_name(db, obj.name)
|
||||
if role:
|
||||
if await role_dao.get_by_name(db, obj.name):
|
||||
raise errors.ForbiddenError(msg='角色已存在')
|
||||
count = await role_dao.update(db, pk, obj)
|
||||
for user in await role.awaitable_attrs.users:
|
||||
@@ -146,7 +131,7 @@ class RoleService:
|
||||
:return:
|
||||
"""
|
||||
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:
|
||||
raise errors.NotFoundError(msg='角色不存在')
|
||||
for menu_id in menu_ids.menus:
|
||||
|
||||
@@ -2,20 +2,19 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import random
|
||||
|
||||
from typing import Sequence
|
||||
|
||||
from fastapi import Request
|
||||
from sqlalchemy import Select
|
||||
|
||||
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_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 (
|
||||
AddUserParam,
|
||||
AvatarParam,
|
||||
RegisterUserParam,
|
||||
ResetPasswordParam,
|
||||
UpdateUserParam,
|
||||
UpdateUserRoleParam,
|
||||
)
|
||||
from backend.common.exception import errors
|
||||
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:
|
||||
"""用户服务类"""
|
||||
|
||||
@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
|
||||
async def add(*, request: Request, obj: AddUserParam) -> None:
|
||||
"""
|
||||
@@ -70,9 +46,6 @@ class UserService:
|
||||
raise errors.ForbiddenError(msg='昵称已注册')
|
||||
if not obj.password:
|
||||
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)
|
||||
if not dept:
|
||||
raise errors.NotFoundError(msg='部门不存在')
|
||||
@@ -83,16 +56,16 @@ class UserService:
|
||||
await user_dao.add(db, obj)
|
||||
|
||||
@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: 密码重置参数
|
||||
:return:
|
||||
"""
|
||||
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:
|
||||
raise errors.NotFoundError(msg='用户不存在')
|
||||
if not password_verify(obj.old_password, user.password):
|
||||
@@ -100,11 +73,11 @@ class UserService:
|
||||
if obj.new_password != obj.confirm_password:
|
||||
raise errors.ForbiddenError(msg='密码输入不一致')
|
||||
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 = [
|
||||
f'{settings.TOKEN_REDIS_PREFIX}:{request.user.id}',
|
||||
f'{settings.TOKEN_REFRESH_REDIS_PREFIX}:{request.user.id}',
|
||||
f'{settings.JWT_USER_REDIS_PREFIX}:{request.user.id}',
|
||||
f'{settings.TOKEN_REDIS_PREFIX}:{user.id}',
|
||||
f'{settings.TOKEN_REFRESH_REDIS_PREFIX}:{user.id}',
|
||||
f'{settings.JWT_USER_REDIS_PREFIX}:{user.id}',
|
||||
]
|
||||
for prefix in key_prefix:
|
||||
await redis_client.delete_prefix(prefix)
|
||||
@@ -124,6 +97,33 @@ class UserService:
|
||||
raise errors.NotFoundError(msg='用户不存在')
|
||||
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
|
||||
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)
|
||||
if nickname:
|
||||
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:
|
||||
role = await role_dao.get(db, role_id)
|
||||
if not role:
|
||||
raise errors.NotFoundError(msg='角色不存在')
|
||||
await user_dao.update_role(db, input_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)
|
||||
count = await user_dao.update(db, user, obj)
|
||||
await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{user.id}')
|
||||
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
|
||||
async def update_permission(*, request: Request, pk: int) -> int:
|
||||
"""
|
||||
|
||||
@@ -64,3 +64,12 @@ class TokenPayload:
|
||||
@dataclasses.dataclass
|
||||
class UploadUrl:
|
||||
url: str
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class SnowflakeInfo:
|
||||
timestamp: int
|
||||
datetime: str
|
||||
cluster_id: int
|
||||
node_id: int
|
||||
sequence: int
|
||||
|
||||
@@ -44,6 +44,8 @@ class MenuType(IntEnum):
|
||||
directory = 0
|
||||
menu = 1
|
||||
button = 2
|
||||
embedded = 3
|
||||
link = 4
|
||||
|
||||
|
||||
class RoleDataRuleOperatorType(IntEnum):
|
||||
|
||||
@@ -85,7 +85,7 @@ class GatewayError(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):
|
||||
super().__init__(msg=msg, data=data, background=background)
|
||||
|
||||
+28
-2
@@ -3,17 +3,43 @@
|
||||
from datetime import datetime
|
||||
from typing import Annotated
|
||||
|
||||
from sqlalchemy import DateTime
|
||||
from sqlalchemy import BigInteger, DateTime
|
||||
from sqlalchemy.ext.asyncio import AsyncAttrs
|
||||
from sqlalchemy.orm import DeclarativeBase, Mapped, MappedAsDataclass, declared_attr, mapped_column
|
||||
|
||||
from backend.utils.snowflake import snowflake
|
||||
from backend.utils.timezone import timezone
|
||||
|
||||
# 通用 Mapped 类型主键, 需手动添加,参考以下使用方式
|
||||
# MappedBase -> id: Mapped[id_key]
|
||||
# DataClassBase && Base -> id: Mapped[id_key] = mapped_column(init=False)
|
||||
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',
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from fastapi import Request
|
||||
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.utils.import_parse import dynamic_import_data_model
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from backend.app.admin.model import DataRule
|
||||
|
||||
|
||||
class RequestPermission:
|
||||
"""
|
||||
@@ -60,35 +56,42 @@ async def filter_data_permission(db: AsyncSession, request: Request) -> ColumnEl
|
||||
:param request: FastAPI 请求对象
|
||||
: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 scope in role.scopes:
|
||||
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)
|
||||
|
||||
# 获取数据范围规则
|
||||
data_rule_list: list[DataRule] = []
|
||||
unique_data_rules = {}
|
||||
for data_scope in data_scopes:
|
||||
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()
|
||||
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)
|
||||
# 转换为列表
|
||||
data_rule_list = list(unique_data_rules.values())
|
||||
|
||||
where_and_list = []
|
||||
where_or_list = []
|
||||
|
||||
for data_rule in new_data_rule_list:
|
||||
for data_rule in data_rule_list:
|
||||
# 验证规则模型
|
||||
rule_model = data_rule.model
|
||||
if rule_model not in settings.DATA_PERMISSION_MODELS:
|
||||
|
||||
@@ -60,10 +60,15 @@ async def rbac_verify(request: Request, _token: str = DependsJwtAuth) -> None:
|
||||
if path_auth_perm in settings.RBAC_ROLE_MENU_EXCLUDE:
|
||||
return
|
||||
|
||||
# 已分配菜单权限校验
|
||||
allow_perms = []
|
||||
# 菜单去重
|
||||
unique_menus = {}
|
||||
for role in user_roles:
|
||||
for menu in role.menus:
|
||||
unique_menus[menu.id] = menu
|
||||
|
||||
# 已分配菜单权限校验
|
||||
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:
|
||||
|
||||
@@ -45,7 +45,7 @@ class Settings(BaseSettings):
|
||||
# FastAPI
|
||||
FASTAPI_API_V1_PATH: str = '/api/v1'
|
||||
FASTAPI_TITLE: str = 'FastAPI'
|
||||
FASTAPI_VERSION: str = '0.0.1'
|
||||
FASTAPI_VERSION: str = '1.5.0'
|
||||
FASTAPI_DESCRIPTION: str = 'FastAPI Best Architecture'
|
||||
FASTAPI_DOCS_URL: str = '/docs'
|
||||
FASTAPI_REDOC_URL: str = '/redoc'
|
||||
|
||||
@@ -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.include_router(gen_router)
|
||||
v1.include_router(gen_router, prefix='/tables')
|
||||
v1.include_router(business_router, prefix='/businesses')
|
||||
v1.include_router(model_router, prefix='/models')
|
||||
|
||||
@@ -16,7 +16,7 @@ from backend.plugin.code_generator.service.gen_service import gen_service
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get('/tables', summary='获取数据库表')
|
||||
@router.get('', summary='获取数据库表')
|
||||
async def get_all_tables(
|
||||
table_schema: Annotated[str, Query(description='数据库名')] = 'fba',
|
||||
) -> ResponseSchemaModel[list[str]]:
|
||||
@@ -37,24 +37,24 @@ async def import_table(obj: ImportParam) -> ResponseModel:
|
||||
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]]:
|
||||
data = await gen_service.preview(pk=pk)
|
||||
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]]:
|
||||
data = await gen_service.get_generate_path(pk=pk)
|
||||
return response_base.success(data=data)
|
||||
|
||||
|
||||
@router.post(
|
||||
'/generate/{pk}',
|
||||
'/{pk}/code',
|
||||
summary='代码生成',
|
||||
description='文件磁盘写入,请谨慎操作',
|
||||
dependencies=[
|
||||
Depends(RequestPermission('gen:code:generate')),
|
||||
Depends(RequestPermission('gen:code:write')),
|
||||
DependsRBAC,
|
||||
],
|
||||
)
|
||||
@@ -63,7 +63,7 @@ async def generate_code(pk: Annotated[int, Path(description='业务 ID')]) -> Re
|
||||
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')]):
|
||||
bio = await gen_service.download(pk=pk)
|
||||
return StreamingResponse(
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
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.postgresql import TEXT
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
@@ -33,6 +33,6 @@ class GenColumn(DataClassBase):
|
||||
|
||||
# 代码生成业务模型一对多
|
||||
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')
|
||||
|
||||
@@ -175,6 +175,7 @@ class GenService:
|
||||
code_folder.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
init_filepath = code_folder.joinpath('__init__.py')
|
||||
if not os.path.exists(init_filepath):
|
||||
async with aiofiles.open(init_filepath, 'w', encoding='utf-8') as f:
|
||||
await f.write(gen_template.init_content)
|
||||
|
||||
|
||||
@@ -64,10 +64,12 @@ class CRUDConfig(CRUDPlus[Config]):
|
||||
:return:
|
||||
"""
|
||||
filters = {'type__not_in': settings.CONFIG_BUILT_IN_TYPES}
|
||||
|
||||
if name is not None:
|
||||
filters.update(name__like=f'%{name}%')
|
||||
filters['name__like'] = f'%{name}%'
|
||||
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)
|
||||
|
||||
async def create(self, db: AsyncSession, obj: CreateConfigParam) -> None:
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
from sqlalchemy import Select, and_, desc, select
|
||||
from sqlalchemy import Select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import noload, selectinload
|
||||
from sqlalchemy_crud_plus import CRUDPlus
|
||||
|
||||
from backend.plugin.dict.model import DictData
|
||||
@@ -31,20 +30,16 @@ class CRUDDictData(CRUDPlus[DictData]):
|
||||
:param status: 字典状态
|
||||
:return:
|
||||
"""
|
||||
stmt = select(self.model).options(noload(self.model.type)).order_by(desc(self.model.sort))
|
||||
filters = {}
|
||||
|
||||
filters = []
|
||||
if label is not None:
|
||||
filters.append(self.model.label.like(f'%{label}%'))
|
||||
filters['label__like'] = f'%{label}%'
|
||||
if value is not None:
|
||||
filters.append(self.model.value.like(f'%{value}%'))
|
||||
filters['value__like'] = f'%{value}%'
|
||||
if status is not None:
|
||||
filters.append(self.model.status == status)
|
||||
filters['status'] = status
|
||||
|
||||
if filters:
|
||||
stmt = stmt.where(and_(*filters))
|
||||
|
||||
return stmt
|
||||
return await self.select_order('id', 'desc', *filters)
|
||||
|
||||
async def get_by_label(self, db: AsyncSession, label: str) -> DictData | None:
|
||||
"""
|
||||
@@ -95,9 +90,7 @@ class CRUDDictData(CRUDPlus[DictData]):
|
||||
:param pk: 字典数据 ID
|
||||
:return:
|
||||
"""
|
||||
stmt = select(self.model).options(selectinload(self.model.type)).where(self.model.id == pk)
|
||||
dict_data = await db.execute(stmt)
|
||||
return dict_data.scalars().first()
|
||||
return await self.select_model(db, pk, load_strategies=['type'])
|
||||
|
||||
|
||||
dict_data_dao: CRUDDictData = CRUDDictData(DictData)
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
from sqlalchemy import Select, and_, desc, select
|
||||
from sqlalchemy import Select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import noload
|
||||
from sqlalchemy_crud_plus import CRUDPlus
|
||||
|
||||
from backend.plugin.dict.model import DictType
|
||||
@@ -31,20 +30,16 @@ class CRUDDictType(CRUDPlus[DictType]):
|
||||
:param status: 字典状态
|
||||
:return:
|
||||
"""
|
||||
stmt = select(self.model).options(noload(self.model.datas)).order_by(desc(self.model.created_time))
|
||||
filters = {}
|
||||
|
||||
filters = []
|
||||
if name is not None:
|
||||
filters.append(self.model.name.like(f'%{name}%'))
|
||||
filters['name__like'] = f'%{name}%'
|
||||
if code is not None:
|
||||
filters.append(self.model.code.like(f'%{code}%'))
|
||||
filters['code__like'] = f'%{code}%'
|
||||
if status is not None:
|
||||
filters.append(self.model.status == status)
|
||||
filters['status'] = status
|
||||
|
||||
if filters:
|
||||
stmt = stmt.where(and_(*filters))
|
||||
|
||||
return stmt
|
||||
return await self.select_order('id', 'desc', **filters)
|
||||
|
||||
async def get_by_code(self, db: AsyncSession, code: str) -> DictType | None:
|
||||
"""
|
||||
|
||||
@@ -44,4 +44,6 @@ async def github_login(
|
||||
user=user,
|
||||
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}'
|
||||
)
|
||||
|
||||
@@ -47,4 +47,6 @@ async def linux_do_login(
|
||||
user=user,
|
||||
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}'
|
||||
)
|
||||
|
||||
@@ -10,9 +10,9 @@ from backend.plugin.oauth2.schema.user_social import CreateUserSocialParam
|
||||
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 pk: 用户 ID
|
||||
@@ -21,6 +21,17 @@ class CRUDUserSocial(CRUDPlus[UserSocial]):
|
||||
"""
|
||||
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:
|
||||
"""
|
||||
创建用户社交账号绑定
|
||||
|
||||
@@ -4,7 +4,7 @@ from __future__ import annotations
|
||||
|
||||
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 backend.common.model import Base, id_key
|
||||
@@ -19,15 +19,11 @@ class UserSocial(Base):
|
||||
__tablename__ = 'sys_user_social'
|
||||
|
||||
id: Mapped[id_key] = mapped_column(init=False)
|
||||
sid: Mapped[str] = mapped_column(String(20), comment='第三方用户 ID')
|
||||
source: Mapped[str] = mapped_column(String(20), comment='第三方用户来源')
|
||||
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(
|
||||
ForeignKey('sys_user.id', ondelete='SET NULL'), default=None, comment='用户关联ID'
|
||||
user_id: Mapped[int] = mapped_column(
|
||||
BigInteger, ForeignKey('sys_user.id', ondelete='CASCADE'), comment='用户关联ID'
|
||||
)
|
||||
user: Mapped[User | None] = relationship(init=False, backref='socials')
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[plugin]
|
||||
summary = 'OAuth 2.0'
|
||||
version = '0.0.1'
|
||||
version = '0.0.3'
|
||||
description = '通过 OAuth 2.0 的方式登录系统'
|
||||
author = 'wu-clan'
|
||||
|
||||
|
||||
@@ -9,12 +9,8 @@ from backend.common.schema import SchemaBase
|
||||
class UserSocialSchemaBase(SchemaBase):
|
||||
"""用户社交基础模型"""
|
||||
|
||||
sid: str = Field(description='第三方用户 ID')
|
||||
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):
|
||||
|
||||
@@ -7,10 +7,9 @@ from fastapi import BackgroundTasks, Request, Response
|
||||
|
||||
from backend.app.admin.crud.crud_user import user_dao
|
||||
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.common.enums import LoginLogStatusType, UserSocialType
|
||||
from backend.common.exception.errors import AuthorizationError
|
||||
from backend.common.security import jwt
|
||||
from backend.core.conf import settings
|
||||
from backend.database.db import async_db_session
|
||||
@@ -43,55 +42,65 @@ class OAuth2Service:
|
||||
:return:
|
||||
"""
|
||||
async with async_db_session.begin() as db:
|
||||
# 获取 OAuth2 平台用户信息
|
||||
social_id = user.get('id')
|
||||
social_nickname = user.get('name')
|
||||
sid = user.get('uuid')
|
||||
username = user.get('username')
|
||||
nickname = user.get('nickname')
|
||||
email = user.get('email')
|
||||
avatar = user.get('avatar_url')
|
||||
|
||||
social_username = user.get('username')
|
||||
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:
|
||||
social_email = f'{social_username}@linux.do'
|
||||
if not social_email:
|
||||
raise AuthorizationError(msg=f'授权失败,{social.value} 账户未绑定邮箱')
|
||||
sid = user.get('id')
|
||||
nickname = user.get('name')
|
||||
|
||||
sys_user = None
|
||||
user_social = await user_social_dao.get_by_sid(db, str(sid), str(social.value))
|
||||
if not user_social:
|
||||
if email:
|
||||
sys_user = await user_dao.check_email(db, email)
|
||||
|
||||
# 创建系统用户
|
||||
sys_user = await user_dao.check_email(db, social_email)
|
||||
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
|
||||
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.create(db, new_sys_user, social=True)
|
||||
await user_dao.add_by_oauth2(db, new_sys_user)
|
||||
await db.flush()
|
||||
sys_user = await user_dao.check_email(db, social_email)
|
||||
sys_user = await user_dao.get_by_username(db, username)
|
||||
|
||||
# 绑定社交用户
|
||||
sys_user_id = sys_user.id
|
||||
user_social = await user_social_dao.get(db, sys_user_id, social.value)
|
||||
if not user_social:
|
||||
new_user_social = CreateUserSocialParam(source=social.value, sid=str(social_id), user_id=sys_user_id)
|
||||
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
|
||||
access_token = await jwt.create_access_token(
|
||||
str(sys_user_id),
|
||||
str(sys_user.id),
|
||||
sys_user.is_multi_login,
|
||||
# extra info
|
||||
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()),
|
||||
ip=request.state.ip,
|
||||
os=request.state.os,
|
||||
browser=request.state.browser,
|
||||
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 db.refresh(sys_user)
|
||||
login_log = dict(
|
||||
@@ -115,8 +124,8 @@ class OAuth2Service:
|
||||
data = GetLoginToken(
|
||||
access_token=access_token.access_token,
|
||||
access_token_expire_time=access_token.access_token_expire_time,
|
||||
user=sys_user, # type: ignore
|
||||
session_uuid=access_token.session_uuid,
|
||||
user=sys_user, # type: ignore
|
||||
)
|
||||
return data
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ from backend.core.conf import settings
|
||||
from backend.core.path_conf import PLUGIN_DIR
|
||||
from backend.database.redis import RedisCli, redis_client
|
||||
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
|
||||
|
||||
|
||||
|
||||
+107
-112
@@ -1,6 +1,6 @@
|
||||
create table gen_business
|
||||
(
|
||||
id int auto_increment comment '主键 ID'
|
||||
id bigint auto_increment comment '主键 ID'
|
||||
primary key,
|
||||
app_name varchar(50) not null comment '应用名称(英文)',
|
||||
table_name varchar(255) not null comment '表名称(英文)',
|
||||
@@ -15,17 +15,16 @@ create table gen_business
|
||||
remark longtext null comment '备注',
|
||||
created_time datetime not null comment '创建时间',
|
||||
updated_time datetime null comment '更新时间',
|
||||
constraint ix_gen_business_id
|
||||
unique (id),
|
||||
constraint table_name
|
||||
unique (table_name)
|
||||
)
|
||||
comment '代码生成业务表';
|
||||
|
||||
create index ix_gen_business_id
|
||||
on gen_business (id);
|
||||
|
||||
create table gen_column
|
||||
(
|
||||
id int auto_increment comment '主键 ID'
|
||||
id bigint auto_increment comment '主键 ID'
|
||||
primary key,
|
||||
name varchar(50) not null comment '列名称',
|
||||
comment varchar(255) null comment '列描述',
|
||||
@@ -36,7 +35,9 @@ create table gen_column
|
||||
length int not null comment '列长度',
|
||||
is_pk 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
|
||||
foreign key (gen_business_id) references gen_business (id)
|
||||
on delete cascade
|
||||
@@ -46,12 +47,9 @@ create table gen_column
|
||||
create index gen_business_id
|
||||
on gen_column (gen_business_id);
|
||||
|
||||
create index ix_gen_column_id
|
||||
on gen_column (id);
|
||||
|
||||
create table sys_config
|
||||
(
|
||||
id int auto_increment comment '主键 ID'
|
||||
id bigint auto_increment comment '主键 ID'
|
||||
primary key,
|
||||
name varchar(20) not null comment '名称',
|
||||
type varchar(20) null comment '类型',
|
||||
@@ -61,30 +59,16 @@ create table sys_config
|
||||
remark longtext null comment '备注',
|
||||
created_time datetime not null comment '创建时间',
|
||||
updated_time datetime null comment '更新时间',
|
||||
constraint ix_sys_config_id
|
||||
unique (id),
|
||||
constraint `key`
|
||||
unique (`key`)
|
||||
)
|
||||
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
|
||||
(
|
||||
id int auto_increment comment '主键 ID'
|
||||
id bigint auto_increment comment '主键 ID'
|
||||
primary key,
|
||||
name varchar(500) not null comment '名称',
|
||||
model varchar(50) not null comment 'SQLA 模型名,对应 DATA_PERMISSION_MODELS 键名',
|
||||
@@ -92,29 +76,55 @@ create table sys_data_rule
|
||||
operator int not null comment '运算符(0:and、1:or)',
|
||||
expression int not null comment '表达式(0:==、1:!=、2:>、3:>=、4:<、5:<=、6:in、7:not_in)',
|
||||
value varchar(255) not null comment '规则值',
|
||||
scope_id int null comment '数据范围关联 ID',
|
||||
created_time datetime not null comment '创建时间',
|
||||
updated_time datetime null comment '更新时间',
|
||||
constraint ix_sys_data_rule_id
|
||||
unique (id),
|
||||
constraint name
|
||||
unique (name),
|
||||
constraint sys_data_rule_ibfk_1
|
||||
foreign key (scope_id) references sys_data_scope (id)
|
||||
on delete set null
|
||||
unique (name)
|
||||
)
|
||||
comment '数据规则表';
|
||||
|
||||
create index ix_sys_data_rule_id
|
||||
on sys_data_rule (id);
|
||||
create table sys_data_scope
|
||||
(
|
||||
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
|
||||
on sys_data_rule (scope_id);
|
||||
create table sys_data_scope_rule
|
||||
(
|
||||
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
|
||||
on sys_data_scope (id);
|
||||
create index data_rule_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
|
||||
(
|
||||
id int auto_increment comment '主键 ID'
|
||||
id bigint auto_increment comment '主键 ID'
|
||||
primary key,
|
||||
name varchar(50) not null comment '部门名称',
|
||||
sort int not null comment '排序',
|
||||
@@ -123,24 +133,23 @@ create table sys_dept
|
||||
email varchar(50) null comment '邮箱',
|
||||
status int 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 '创建时间',
|
||||
updated_time datetime null comment '更新时间',
|
||||
constraint ix_sys_dept_id
|
||||
unique (id),
|
||||
constraint sys_dept_ibfk_1
|
||||
foreign key (parent_id) references sys_dept (id)
|
||||
on delete set null
|
||||
)
|
||||
comment '部门表';
|
||||
|
||||
create index ix_sys_dept_id
|
||||
on sys_dept (id);
|
||||
|
||||
create index ix_sys_dept_parent_id
|
||||
on sys_dept (parent_id);
|
||||
|
||||
create table sys_dict_type
|
||||
(
|
||||
id int auto_increment comment '主键 ID'
|
||||
id bigint auto_increment comment '主键 ID'
|
||||
primary key,
|
||||
name 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 '创建时间',
|
||||
updated_time datetime null comment '更新时间',
|
||||
constraint code
|
||||
unique (code)
|
||||
unique (code),
|
||||
constraint ix_sys_dict_type_id
|
||||
unique (id)
|
||||
)
|
||||
comment '字典类型表';
|
||||
|
||||
create table sys_dict_data
|
||||
(
|
||||
id int auto_increment comment '主键 ID'
|
||||
id bigint auto_increment comment '主键 ID'
|
||||
primary key,
|
||||
label varchar(32) not null comment '字典标签',
|
||||
value varchar(32) not null comment '字典值',
|
||||
sort int not null comment '排序',
|
||||
status int not null comment '状态(0停用 1正常)',
|
||||
remark longtext null comment '备注',
|
||||
type_id int not null comment '字典类型关联ID',
|
||||
type_id bigint not null comment '字典类型关联ID',
|
||||
created_time datetime not null comment '创建时间',
|
||||
updated_time datetime null comment '更新时间',
|
||||
constraint ix_sys_dict_data_id
|
||||
unique (id),
|
||||
constraint label
|
||||
unique (label),
|
||||
constraint sys_dict_data_ibfk_1
|
||||
@@ -173,18 +186,12 @@ create table sys_dict_data
|
||||
)
|
||||
comment '字典数据表';
|
||||
|
||||
create index ix_sys_dict_data_id
|
||||
on sys_dict_data (id);
|
||||
|
||||
create index 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
|
||||
(
|
||||
id int auto_increment comment '主键 ID'
|
||||
id bigint not null comment '雪花算法主键 ID'
|
||||
primary key,
|
||||
user_uuid varchar(50) not null comment '用户UUID',
|
||||
username varchar(20) not null comment '用户名',
|
||||
@@ -199,23 +206,22 @@ create table sys_login_log
|
||||
device varchar(50) null comment '设备',
|
||||
msg longtext 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 '登录日志表';
|
||||
|
||||
create index ix_sys_login_log_id
|
||||
on sys_login_log (id);
|
||||
|
||||
create table sys_menu
|
||||
(
|
||||
id int auto_increment comment '主键 ID'
|
||||
id bigint auto_increment comment '主键 ID'
|
||||
primary key,
|
||||
title 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 '排序',
|
||||
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 '组件路径',
|
||||
perms varchar(100) null comment '权限标识',
|
||||
status int not null comment '菜单状态(0停用 1正常)',
|
||||
@@ -223,24 +229,23 @@ create table sys_menu
|
||||
cache int not null comment '是否缓存(0否 1是)',
|
||||
link 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 '创建时间',
|
||||
updated_time datetime null comment '更新时间',
|
||||
constraint ix_sys_menu_id
|
||||
unique (id),
|
||||
constraint sys_menu_ibfk_1
|
||||
foreign key (parent_id) references sys_menu (id)
|
||||
on delete set null
|
||||
)
|
||||
comment '菜单表';
|
||||
|
||||
create index ix_sys_menu_id
|
||||
on sys_menu (id);
|
||||
|
||||
create index ix_sys_menu_parent_id
|
||||
on sys_menu (parent_id);
|
||||
|
||||
create table sys_notice
|
||||
(
|
||||
id int auto_increment comment '主键 ID'
|
||||
id bigint auto_increment comment '主键 ID'
|
||||
primary key,
|
||||
title varchar(50) not null comment '标题',
|
||||
type int not null comment '类型(0:通知、1:公告)',
|
||||
@@ -249,16 +254,15 @@ create table sys_notice
|
||||
status int not null comment '状态(0:隐藏、1:显示)',
|
||||
content longtext 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 '系统通知公告表';
|
||||
|
||||
create index ix_sys_notice_id
|
||||
on sys_notice (id);
|
||||
|
||||
create table sys_opera_log
|
||||
(
|
||||
id int auto_increment comment '主键 ID'
|
||||
id bigint auto_increment comment '主键 ID'
|
||||
primary key,
|
||||
trace_id varchar(32) not null comment '请求跟踪 ID',
|
||||
username varchar(20) null comment '用户名',
|
||||
@@ -279,35 +283,34 @@ create table sys_opera_log
|
||||
msg longtext null comment '提示消息',
|
||||
cost_time float not null comment '请求耗时(ms)',
|
||||
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 '操作日志表';
|
||||
|
||||
create index ix_sys_opera_log_id
|
||||
on sys_opera_log (id);
|
||||
|
||||
create table sys_role
|
||||
(
|
||||
id int auto_increment comment '主键 ID'
|
||||
id bigint auto_increment comment '主键 ID'
|
||||
primary key,
|
||||
name varchar(20) not null comment '角色名称',
|
||||
status int not null comment '角色状态(0停用 1正常)',
|
||||
is_filter_scopes tinyint(1) not null comment '过滤数据权限(0否 1是)',
|
||||
remark longtext null comment '备注',
|
||||
created_time datetime not null comment '创建时间',
|
||||
updated_time datetime null comment '更新时间',
|
||||
constraint ix_sys_role_id
|
||||
unique (id),
|
||||
constraint name
|
||||
unique (name)
|
||||
)
|
||||
comment '角色表';
|
||||
|
||||
create index ix_sys_role_id
|
||||
on sys_role (id);
|
||||
|
||||
create table sys_role_data_scope
|
||||
(
|
||||
id int auto_increment comment '主键 ID',
|
||||
role_id int not null comment '角色 ID',
|
||||
data_scope_id int not null comment '数据范围 ID',
|
||||
id bigint auto_increment comment '主键 ID',
|
||||
role_id bigint not null comment '角色 ID',
|
||||
data_scope_id bigint not null comment '数据范围 ID',
|
||||
primary key (id, role_id, data_scope_id),
|
||||
constraint ix_sys_role_data_scope_id
|
||||
unique (id),
|
||||
@@ -327,9 +330,9 @@ create index role_id
|
||||
|
||||
create table sys_role_menu
|
||||
(
|
||||
id int auto_increment comment '主键ID',
|
||||
role_id int not null comment '角色ID',
|
||||
menu_id int not null comment '菜单ID',
|
||||
id bigint auto_increment comment '主键ID',
|
||||
role_id bigint not null comment '角色ID',
|
||||
menu_id bigint not null comment '菜单ID',
|
||||
primary key (id, role_id, menu_id),
|
||||
constraint ix_sys_role_menu_id
|
||||
unique (id),
|
||||
@@ -349,31 +352,31 @@ create index role_id
|
||||
|
||||
create table sys_user
|
||||
(
|
||||
id int auto_increment comment '主键 ID'
|
||||
id bigint auto_increment comment '主键 ID'
|
||||
primary key,
|
||||
uuid varchar(50) not null,
|
||||
username varchar(20) not null comment '用户名',
|
||||
nickname varchar(20) not null comment '昵称',
|
||||
password varchar(255) null comment '密码',
|
||||
salt varbinary(255) null comment '加密盐',
|
||||
email varchar(50) not null comment '邮箱',
|
||||
password varchar(255) not null comment '密码',
|
||||
salt varbinary(255) 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_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是)',
|
||||
avatar varchar(255) null comment '头像',
|
||||
phone varchar(11) null comment '手机号',
|
||||
join_time datetime not 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 '创建时间',
|
||||
updated_time datetime null comment '更新时间',
|
||||
constraint ix_sys_user_email
|
||||
unique (email),
|
||||
constraint ix_sys_user_id
|
||||
unique (id),
|
||||
constraint ix_sys_user_username
|
||||
unique (username),
|
||||
constraint nickname
|
||||
unique (nickname),
|
||||
constraint uuid
|
||||
unique (uuid),
|
||||
constraint sys_user_ibfk_1
|
||||
@@ -385,17 +388,14 @@ create table sys_user
|
||||
create index dept_id
|
||||
on sys_user (dept_id);
|
||||
|
||||
create index ix_sys_user_id
|
||||
on sys_user (id);
|
||||
|
||||
create index ix_sys_user_status
|
||||
on sys_user (status);
|
||||
|
||||
create table sys_user_role
|
||||
(
|
||||
id int auto_increment comment '主键ID',
|
||||
user_id int not null comment '用户ID',
|
||||
role_id int not null comment '角色ID',
|
||||
id bigint auto_increment comment '主键ID',
|
||||
user_id bigint not null comment '用户ID',
|
||||
role_id bigint not null comment '角色ID',
|
||||
primary key (id, user_id, role_id),
|
||||
constraint ix_sys_user_role_id
|
||||
unique (id),
|
||||
@@ -415,26 +415,21 @@ create index user_id
|
||||
|
||||
create table sys_user_social
|
||||
(
|
||||
id int auto_increment comment '主键 ID'
|
||||
id bigint auto_increment comment '主键 ID'
|
||||
primary key,
|
||||
sid varchar(20) not null comment '第三方用户 ID',
|
||||
source varchar(20) not null comment '第三方用户来源',
|
||||
open_id varchar(20) null comment '第三方用户 open id',
|
||||
sid varchar(20) null comment '第三方用户 ID',
|
||||
union_id varchar(20) null comment '第三方用户 union id',
|
||||
scope varchar(120) null comment '第三方用户授予的权限',
|
||||
code varchar(50) null comment '用户的授权 code',
|
||||
user_id int null comment '用户关联ID',
|
||||
user_id bigint not null comment '用户关联ID',
|
||||
created_time datetime not null comment '创建时间',
|
||||
updated_time datetime null comment '更新时间',
|
||||
constraint ix_sys_user_social_id
|
||||
unique (id),
|
||||
constraint sys_user_social_ibfk_1
|
||||
foreign key (user_id) references sys_user (id)
|
||||
on delete set null
|
||||
on delete cascade
|
||||
)
|
||||
comment '用户社交表(OAuth2)';
|
||||
|
||||
create index ix_sys_user_social_id
|
||||
on sys_user_social (id);
|
||||
|
||||
create index user_id
|
||||
on sys_user_social (user_id);
|
||||
|
||||
|
||||
@@ -1,37 +1,113 @@
|
||||
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)
|
||||
values (1, '测试', 'Test', 'test', 0, null, 0, null, null, 0, 0, 1, null, null, null, '2023-07-27 19:14:10', null),
|
||||
(2, '仪表盘', 'Dashboard', 'dashboard', 0, 'material-symbols:dashboard', 0, null, null, 1, 1, 1, null, null, null, '2023-07-27 19:15:45', 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),
|
||||
(4, '数据分析', 'Analytics', 'analytics', 0, null, 1, '/dashboard/analytics/index.vue', null, 1, 1, 1, null, null, 2, '2023-07-27 19:17:59', null),
|
||||
(5, '系统管理', 'System', 'system', 0, 'eos-icons:admin', 0, null, null, 1, 1, 1, null, null, null, '2023-07-27 19:23:00', 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),
|
||||
(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),
|
||||
(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),
|
||||
(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),
|
||||
(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),
|
||||
(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),
|
||||
(12, '系统自动化', 'Automation', 'automation', 0, 'material-symbols:automation', 0, null, null, 1, 1, 1, null, null, null, '2024-07-27 02:06:20', 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),
|
||||
(14, '系统监控', 'Monitor', 'monitor', 0, 'mdi:monitor-eye', 0, null, null, 1, 1, 1, null, null, null, '2023-07-27 19:27:08', 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),
|
||||
(16, '服务器监控', 'Server', 'server', 0, null, 1, '/monitor/server/index.vue', null, 1, 1, 1, null, null, 14, '2023-07-27 19:28:29', null),
|
||||
(17, '日志', 'Log', 'log', 0, 'carbon:cloud-logging', 0, null, null, 1, 1, 1, null, null, null, '2023-07-27 19:19:59', 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),
|
||||
(19, '操作日志', 'Opera', 'opera', 0, null, 1, '/log/opera/index.vue', null, 1, 1, 1, null, null, 17, '2023-07-27 19:21:28', 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),
|
||||
(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);
|
||||
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, '概览', 'Dashboard', 'dashboard', 0, 'ant-design:dashboard-outlined', 0, null, null, 1, 1, 1, '', null, null, '2025-06-09 17:26:18', null),
|
||||
(2, '系统管理', 'System', 'system', 1, 'eos-icons:admin', 0, null, null, 1, 1, 1, '', null, null, '2025-06-09 17:30:01', null),
|
||||
(3, '系统自动化', 'Automation', 'automation', 2, 'material-symbols:automation', 0, null, null, 1, 1, 1, '', null, null, '2025-06-09 17:31:41', null),
|
||||
(4, '日志管理', 'Log', 'log', 3, 'carbon:cloud-logging', 0, null, null, 1, 1, 1, '', null, null, '2025-06-09 17:32:34', null),
|
||||
(5, '系统监控', 'Monitor', 'monitor', 4, 'mdi:monitor-eye', 0, null, null, 1, 1, 1, '', null, null, '2025-06-09 17:33:29', 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, '分析页', 'Analytics', 'analytics', 0, 'lucide:area-chart', 1, '/dashboard/analytics/index', null, 1, 1, 1, '', null, 1, '2025-06-09 17:54:29', 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, '文档', '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, '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, '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, '部门管理', '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, '用户管理', '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, '角色管理', '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, '菜单管理', '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, '数据权限', '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, '数据范围', '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, '数据规则', '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, '插件管理', '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, '参数管理', '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, '字典管理', '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)
|
||||
values (1, 'test', 1, null, '2023-06-26 17:13:45', null);
|
||||
insert into sys_role (id, name, status, is_filter_scopes, remark, created_time, updated_time)
|
||||
values (1, '测试', 1, 1, null, '2025-05-26 17:13:45', null);
|
||||
|
||||
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)
|
||||
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)
|
||||
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);
|
||||
|
||||
@@ -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 '运算符(0:and、1:or)';
|
||||
|
||||
comment on column sys_data_rule.expression is '表达式(0:==、1:!=、2:>、3:>=、4:<、5:<=、6:in、7:not_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
|
||||
(
|
||||
id serial
|
||||
primary key,
|
||||
name varchar(50) not null
|
||||
unique,
|
||||
id bigserial,
|
||||
name varchar(50) not null,
|
||||
status integer 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 '数据范围表';
|
||||
@@ -21,13 +68,21 @@ comment on column sys_data_scope.created_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);
|
||||
|
||||
create table sys_dept
|
||||
(
|
||||
id serial
|
||||
primary key,
|
||||
id bigserial,
|
||||
name varchar(50) not null,
|
||||
sort integer not null,
|
||||
leader varchar(20),
|
||||
@@ -35,11 +90,12 @@ create table sys_dept
|
||||
email varchar(50),
|
||||
status integer not null,
|
||||
del_flag integer not null,
|
||||
parent_id integer
|
||||
references sys_dept
|
||||
parent_id bigint
|
||||
references ??? ()
|
||||
on delete set 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 '部门表';
|
||||
@@ -66,7 +122,13 @@ comment on column sys_dept.created_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);
|
||||
|
||||
create index ix_sys_dept_parent_id
|
||||
@@ -74,8 +136,7 @@ create index ix_sys_dept_parent_id
|
||||
|
||||
create table sys_login_log
|
||||
(
|
||||
id serial
|
||||
primary key,
|
||||
id bigint not null,
|
||||
user_uuid varchar(50) not null,
|
||||
username varchar(20) not null,
|
||||
status integer not null,
|
||||
@@ -89,12 +150,13 @@ create table sys_login_log
|
||||
device varchar(50),
|
||||
msg text 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 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';
|
||||
|
||||
@@ -124,16 +186,21 @@ comment on column sys_login_log.login_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);
|
||||
|
||||
create table sys_menu
|
||||
(
|
||||
id serial
|
||||
primary key,
|
||||
id bigserial,
|
||||
title varchar(50) not null,
|
||||
name varchar(50) not null,
|
||||
path varchar(200) not null,
|
||||
path varchar(200),
|
||||
sort integer not null,
|
||||
icon varchar(100),
|
||||
type integer not null,
|
||||
@@ -144,11 +211,12 @@ create table sys_menu
|
||||
cache integer not null,
|
||||
link text,
|
||||
remark text,
|
||||
parent_id integer
|
||||
references sys_menu
|
||||
parent_id bigint
|
||||
references ??? ()
|
||||
on delete set 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 '菜单表';
|
||||
@@ -165,7 +233,7 @@ comment on column sys_menu.sort 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 '组件路径';
|
||||
|
||||
@@ -187,16 +255,21 @@ comment on column sys_menu.created_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);
|
||||
|
||||
create index ix_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
|
||||
(
|
||||
id serial
|
||||
primary key,
|
||||
id bigserial,
|
||||
trace_id varchar(32) not null,
|
||||
username varchar(20),
|
||||
method varchar(20) not null,
|
||||
@@ -216,7 +289,8 @@ create table sys_opera_log
|
||||
msg text,
|
||||
cost_time double precision 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 '操作日志表';
|
||||
@@ -263,19 +337,26 @@ comment on column sys_opera_log.opera_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);
|
||||
|
||||
create table sys_role
|
||||
(
|
||||
id serial
|
||||
primary key,
|
||||
name varchar(20) not null
|
||||
unique,
|
||||
id bigserial,
|
||||
name varchar(20) not null,
|
||||
status integer not null,
|
||||
is_filter_scopes integer not null,
|
||||
remark text,
|
||||
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 '角色表';
|
||||
@@ -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.is_filter_scopes is '过滤数据权限(0否 1是)';
|
||||
|
||||
comment on column sys_role.remark is '备注';
|
||||
|
||||
comment on column sys_role.created_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);
|
||||
|
||||
create table sys_config
|
||||
(
|
||||
id serial
|
||||
primary key,
|
||||
id bigserial,
|
||||
name varchar(20) not null,
|
||||
type varchar(20),
|
||||
key varchar(50) not null
|
||||
unique,
|
||||
key varchar(50) not null,
|
||||
value text not null,
|
||||
is_frontend integer not null,
|
||||
remark text,
|
||||
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 '参数配置表';
|
||||
@@ -330,20 +422,29 @@ comment on column sys_config.created_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);
|
||||
|
||||
create table sys_dict_type
|
||||
(
|
||||
id serial
|
||||
primary key,
|
||||
id bigserial,
|
||||
name varchar(32) not null,
|
||||
code varchar(32) not null
|
||||
unique,
|
||||
code varchar(32) not null,
|
||||
status integer not null,
|
||||
remark text,
|
||||
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 '字典类型表';
|
||||
@@ -362,13 +463,21 @@ comment on column sys_dict_type.created_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);
|
||||
|
||||
create table sys_notice
|
||||
(
|
||||
id serial
|
||||
primary key,
|
||||
id bigserial,
|
||||
title varchar(50) not null,
|
||||
type integer not null,
|
||||
author varchar(16) not null,
|
||||
@@ -376,7 +485,8 @@ create table sys_notice
|
||||
status integer not null,
|
||||
content text 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 '系统通知公告表';
|
||||
@@ -399,16 +509,20 @@ comment on column sys_notice.created_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);
|
||||
|
||||
create table gen_business
|
||||
(
|
||||
id serial
|
||||
primary key,
|
||||
id bigserial,
|
||||
app_name varchar(50) not null,
|
||||
table_name varchar(255) not null
|
||||
unique,
|
||||
table_name varchar(255) not null,
|
||||
doc_comment varchar(255) not null,
|
||||
table_comment varchar(255),
|
||||
class_name varchar(50),
|
||||
@@ -419,7 +533,9 @@ create table gen_business
|
||||
gen_path varchar(255),
|
||||
remark text,
|
||||
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 '代码生成业务表';
|
||||
@@ -452,62 +568,28 @@ comment on column gen_business.created_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);
|
||||
|
||||
create table sys_data_rule
|
||||
(
|
||||
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
|
||||
);
|
||||
create unique index gen_business_table_name_key
|
||||
on gen_business (table_name);
|
||||
|
||||
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 '运算符(0:and、1:or)';
|
||||
|
||||
comment on column sys_data_rule.expression is '表达式(0:==、1:!=、2:>、3:>=、4:<、5:<=、6:in、7:not_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 unique index ix_gen_business_id
|
||||
on gen_business (id);
|
||||
|
||||
create table sys_role_menu
|
||||
(
|
||||
id serial,
|
||||
role_id integer not null
|
||||
references sys_role
|
||||
id bigserial,
|
||||
role_id bigint not null
|
||||
references ??? ()
|
||||
on delete cascade,
|
||||
menu_id integer not null
|
||||
references sys_menu
|
||||
menu_id bigint not null
|
||||
references ??? ()
|
||||
on delete cascade,
|
||||
primary key (id, role_id, menu_id)
|
||||
primary key ()
|
||||
);
|
||||
|
||||
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';
|
||||
|
||||
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
|
||||
on sys_role_menu (id);
|
||||
|
||||
create table sys_role_data_scope
|
||||
(
|
||||
id serial,
|
||||
role_id integer not null
|
||||
references sys_role
|
||||
id bigserial,
|
||||
role_id bigint not null
|
||||
references ??? ()
|
||||
on delete cascade,
|
||||
data_scope_id integer not null
|
||||
references sys_data_scope
|
||||
data_scope_id bigint not null
|
||||
references ??? ()
|
||||
on delete cascade,
|
||||
primary key (id, role_id, data_scope_id)
|
||||
primary key ()
|
||||
);
|
||||
|
||||
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';
|
||||
|
||||
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
|
||||
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
|
||||
(
|
||||
id serial
|
||||
primary key,
|
||||
uuid varchar(50) not null
|
||||
unique,
|
||||
id bigserial,
|
||||
uuid varchar(50) not null,
|
||||
username varchar(20) not null,
|
||||
nickname varchar(20) not null
|
||||
unique,
|
||||
password varchar(255),
|
||||
salt bytea,
|
||||
email varchar(50) not null,
|
||||
nickname varchar(20) not null,
|
||||
password varchar(255) not null,
|
||||
salt bytea not null,
|
||||
email varchar(50),
|
||||
phone varchar(11),
|
||||
avatar varchar(255),
|
||||
status integer not null,
|
||||
is_superuser integer not null,
|
||||
is_staff integer not null,
|
||||
status integer not null,
|
||||
is_multi_login integer not null,
|
||||
avatar varchar(255),
|
||||
phone varchar(11),
|
||||
join_time timestamp with time zone not null,
|
||||
last_login_time timestamp with time zone,
|
||||
dept_id integer
|
||||
references sys_dept
|
||||
dept_id bigint
|
||||
references ??? ()
|
||||
on delete set 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 '用户表';
|
||||
@@ -581,18 +701,18 @@ comment on column sys_user.salt 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_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.avatar is '头像';
|
||||
|
||||
comment on column sys_user.phone is '手机号';
|
||||
|
||||
comment on column sys_user.join_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 '更新时间';
|
||||
|
||||
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
|
||||
on sys_user (email);
|
||||
|
||||
create index ix_sys_user_id
|
||||
on sys_user (id);
|
||||
|
||||
create unique index ix_sys_user_username
|
||||
on sys_user (username);
|
||||
|
||||
create unique index ix_sys_user_id
|
||||
on sys_user (id);
|
||||
|
||||
create index ix_sys_user_status
|
||||
on sys_user (status);
|
||||
|
||||
create table sys_dict_data
|
||||
(
|
||||
id serial
|
||||
primary key,
|
||||
label varchar(32) not null
|
||||
unique,
|
||||
id bigserial,
|
||||
label varchar(32) not null,
|
||||
value varchar(32) not null,
|
||||
sort integer not null,
|
||||
status integer not null,
|
||||
remark text,
|
||||
type_id integer not null
|
||||
references sys_dict_type
|
||||
type_id bigint not null
|
||||
references ??? ()
|
||||
on delete cascade,
|
||||
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 '字典数据表';
|
||||
@@ -652,13 +781,21 @@ comment on column sys_dict_data.created_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);
|
||||
|
||||
create table gen_column
|
||||
(
|
||||
id serial
|
||||
primary key,
|
||||
id bigserial,
|
||||
name varchar(50) not null,
|
||||
comment varchar(255),
|
||||
type varchar(20) not null,
|
||||
@@ -668,9 +805,10 @@ create table gen_column
|
||||
length integer not null,
|
||||
is_pk boolean not null,
|
||||
is_nullable boolean not null,
|
||||
gen_business_id integer not null
|
||||
references gen_business
|
||||
on delete cascade
|
||||
gen_business_id bigint not null
|
||||
references ??? ()
|
||||
on delete cascade,
|
||||
primary key ()
|
||||
);
|
||||
|
||||
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';
|
||||
|
||||
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);
|
||||
|
||||
create table sys_user_role
|
||||
(
|
||||
id serial,
|
||||
user_id integer not null
|
||||
references sys_user
|
||||
id bigserial,
|
||||
user_id bigint not null
|
||||
references ??? ()
|
||||
on delete cascade,
|
||||
role_id integer not null
|
||||
references sys_role
|
||||
role_id bigint not null
|
||||
references ??? ()
|
||||
on delete cascade,
|
||||
primary key (id, user_id, role_id)
|
||||
primary key ()
|
||||
);
|
||||
|
||||
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';
|
||||
|
||||
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
|
||||
on sys_user_role (id);
|
||||
|
||||
create table sys_user_social
|
||||
(
|
||||
id serial
|
||||
primary key,
|
||||
id bigserial,
|
||||
sid varchar(20) not null,
|
||||
source varchar(20) not null,
|
||||
open_id varchar(20),
|
||||
sid varchar(20),
|
||||
union_id varchar(20),
|
||||
scope varchar(120),
|
||||
code varchar(50),
|
||||
user_id integer
|
||||
references sys_user
|
||||
on delete set null,
|
||||
user_id bigint not null
|
||||
references ??? ()
|
||||
on delete cascade,
|
||||
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 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.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.created_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);
|
||||
|
||||
@@ -1,37 +1,124 @@
|
||||
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)
|
||||
values (1, '测试', 'Test', 'test', 0, null, 0, null, null, 0, 0, 1, null, null, null, '2023-07-27 19:14:10', null),
|
||||
(2, '仪表盘', 'Dashboard', 'dashboard', 0, 'material-symbols:dashboard', 0, null, null, 1, 1, 1, null, null, null, '2023-07-27 19:15:45', 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),
|
||||
(4, '数据分析', 'Analytics', 'analytics', 0, null, 1, '/dashboard/analytics/index.vue', null, 1, 1, 1, null, null, 2, '2023-07-27 19:17:59', null),
|
||||
(5, '系统管理', 'System', 'system', 0, 'eos-icons:admin', 0, null, null, 1, 1, 1, null, null, null, '2023-07-27 19:23:00', 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),
|
||||
(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),
|
||||
(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),
|
||||
(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),
|
||||
(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),
|
||||
(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),
|
||||
(12, '系统自动化', 'Automation', 'automation', 0, 'material-symbols:automation', 0, null, null, 1, 1, 1, null, null, null, '2024-07-27 02:06:20', 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),
|
||||
(14, '系统监控', 'Monitor', 'monitor', 0, 'mdi:monitor-eye', 0, null, null, 1, 1, 1, null, null, null, '2023-07-27 19:27:08', 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),
|
||||
(16, '服务器监控', 'Server', 'server', 0, null, 1, '/monitor/server/index.vue', null, 1, 1, 1, null, null, 14, '2023-07-27 19:28:29', null),
|
||||
(17, '日志', 'Log', 'log', 0, 'carbon:cloud-logging', 0, null, null, 1, 1, 1, null, null, null, '2023-07-27 19:19:59', 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),
|
||||
(19, '操作日志', 'Opera', 'opera', 0, null, 1, '/log/opera/index.vue', null, 1, 1, 1, null, null, 17, '2023-07-27 19:21:28', 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),
|
||||
(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);
|
||||
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, '概览', 'Dashboard', 'dashboard', 0, 'ant-design:dashboard-outlined', 0, null, null, 1, 1, 1, '', null, null, '2025-06-09 17:26:18', null),
|
||||
(2, '系统管理', 'System', 'system', 1, 'eos-icons:admin', 0, null, null, 1, 1, 1, '', null, null, '2025-06-09 17:30:01', null),
|
||||
(3, '系统自动化', 'Automation', 'automation', 2, 'material-symbols:automation', 0, null, null, 1, 1, 1, '', null, null, '2025-06-09 17:31:41', null),
|
||||
(4, '日志管理', 'Log', 'log', 3, 'carbon:cloud-logging', 0, null, null, 1, 1, 1, '', null, null, '2025-06-09 17:32:34', null),
|
||||
(5, '系统监控', 'Monitor', 'monitor', 4, 'mdi:monitor-eye', 0, null, null, 1, 1, 1, '', null, null, '2025-06-09 17:33:29', 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, '分析页', 'Analytics', 'analytics', 0, 'lucide:area-chart', 1, '/dashboard/analytics/index', null, 1, 1, 1, '', null, 1, '2025-06-09 17:54:29', 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, '文档', '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, '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, '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, '部门管理', '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, '用户管理', '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, '角色管理', '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, '菜单管理', '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, '数据权限', '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, '数据范围', '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, '数据规则', '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, '插件管理', '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, '参数管理', '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, '字典管理', '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)
|
||||
values (1, 'test', 1, null, '2023-06-26 17:13:45', null);
|
||||
insert into sys_role (id, name, status, is_filter_scopes, remark, created_time, updated_time)
|
||||
values (1, '测试', 1, 1, null, '2025-05-26 17:13:45', null);
|
||||
|
||||
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)
|
||||
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)
|
||||
values (1, 1, 1);
|
||||
|
||||
insert into sys_data_scope (id, name, status, created_time, updated_time)
|
||||
values (1, '测试部门数据权限', 1, '2025-06-09 16:53:29', null),
|
||||
(2, '测试部门及以下数据权限', 1, '2025-06-09 16:53:40', null);
|
||||
|
||||
insert into sys_data_rule (id, name, model, "column", operator, expression, "value", created_time, updated_time)
|
||||
values (1, '部门名称等于测试', '部门', 'name', 1, 0, '测试', '2025-06-09 16:56:06', null),
|
||||
(2, '父部门 ID 等于 1', '部门', 'parent_id', 0, 0, '1', '2025-06-09 17:16:14', null);
|
||||
|
||||
insert into sys_data_scope_rule (id, data_scope_id, data_rule_id)
|
||||
values (1, 1, 1),
|
||||
(2, 2, 1),
|
||||
(3, 2, 2);
|
||||
|
||||
-- Reset auto-increment values for each table based on max id
|
||||
SELECT setval(pg_get_serial_sequence('sys_dept', 'id'),COALESCE(MAX(id), 0) + 1, true) FROM sys_dept;
|
||||
SELECT setval(pg_get_serial_sequence('sys_menu', 'id'),COALESCE(MAX(id), 0) + 1, true) FROM sys_menu;
|
||||
SELECT setval(pg_get_serial_sequence('sys_role', 'id'),COALESCE(MAX(id), 0) + 1, true) FROM sys_role;
|
||||
SELECT setval(pg_get_serial_sequence('sys_role_menu', 'id'),COALESCE(MAX(id), 0) + 1, true) FROM sys_role_menu;
|
||||
SELECT setval(pg_get_serial_sequence('sys_user', 'id'),COALESCE(MAX(id), 0) + 1, true) FROM sys_user;
|
||||
SELECT setval(pg_get_serial_sequence('sys_user_role', 'id'),COALESCE(MAX(id), 0) + 1, true) FROM sys_user_role;
|
||||
SELECT setval(pg_get_serial_sequence('sys_data_scope', 'id'),COALESCE(MAX(id), 0) + 1, true) FROM sys_data_scope;
|
||||
SELECT setval(pg_get_serial_sequence('sys_data_rule', 'id'),COALESCE(MAX(id), 0) + 1, true) FROM sys_data_rule;
|
||||
SELECT setval(pg_get_serial_sequence('sys_data_scope_rule', 'id'),COALESCE(MAX(id), 0) + 1, true) FROM sys_data_scope_rule;
|
||||
|
||||
@@ -1,92 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Socket.IO Task Notifications</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
}
|
||||
#notifications {
|
||||
margin-top: 20px;
|
||||
padding: 10px;
|
||||
border: 1px solid #ccc;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.notification {
|
||||
padding: 10px;
|
||||
margin-bottom: 10px;
|
||||
background-color: #f9f9f9;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.notification.new {
|
||||
background-color: #d3f9d8;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<h1>Task Notifications</h1>
|
||||
<p>Waiting for task notifications...</p>
|
||||
|
||||
<div id="notifications"></div>
|
||||
|
||||
<!-- Load Socket.IO client -->
|
||||
<script src="https://cdn.socket.io/4.5.1/socket.io.min.js"></script>
|
||||
<script>
|
||||
// Establish a connection to the server
|
||||
const socket = io('http://127.0.0.1:8000', {
|
||||
autoConnect: true,
|
||||
path: '/ws/socket.io',
|
||||
reconnection: true,
|
||||
reconnectionAttempts: 3,
|
||||
reconnectionDelay: 1000,
|
||||
transports: ['websocket'],
|
||||
auth: {
|
||||
session_uuid: 'da985430-43b3-4860-a20a-97cc8e8e0b76',
|
||||
token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzZXNzaW9uX3V1aWQiOiJkYTk4NTQzMC00M2IzLTQ4NjAtYTIwYS05N2NjOGU4ZTBiNzYiLCJleHAiOjE3Mzc2MzQwODUsInN1YiI6IjEifQ.QeXNtXhHG4cm5GRzOtv2wqZXi6HdqACmW_UjC4JLPsk'
|
||||
}
|
||||
});
|
||||
|
||||
// Handle 'task_notification' event
|
||||
socket.on('task_notification', (data) => {
|
||||
// Get the notifications container
|
||||
const notificationsContainer = document.getElementById('notifications');
|
||||
|
||||
// Create a new div for the notification
|
||||
const notificationDiv = document.createElement('div');
|
||||
notificationDiv.classList.add('notification', 'new');
|
||||
notificationDiv.innerHTML = `
|
||||
<strong>Task Notification:</strong>
|
||||
<p>${data.msg}</p>
|
||||
<small>Received at: ${new Date().toLocaleTimeString()}</small>
|
||||
`;
|
||||
|
||||
// Append the new notification to the container
|
||||
notificationsContainer.appendChild(notificationDiv);
|
||||
|
||||
// Scroll to the bottom to show the latest notification
|
||||
notificationsContainer.scrollTop = notificationsContainer.scrollHeight;
|
||||
});
|
||||
|
||||
// Handle connection errors
|
||||
socket.on('connect_error', (error) => {
|
||||
console.error('Connection error:', error);
|
||||
alert('Unable to connect to the server. Please check your connection.');
|
||||
});
|
||||
|
||||
socket.on('connect', () => {
|
||||
console.log('Connected to server');
|
||||
});
|
||||
|
||||
socket.on('disconnect', () => {
|
||||
console.log('Disconnected from server');
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -93,16 +93,16 @@ def get_vben5_tree_data(row: Sequence[RowData]) -> list[dict[str, Any]]:
|
||||
:param row: 原始数据行序列
|
||||
:return:
|
||||
"""
|
||||
# 需要移除的原始字段
|
||||
remove_keys = {'title', 'icon', 'link', 'cache', 'display', 'status'}
|
||||
meta_keys = {'title', 'icon', 'link', 'cache', 'display', 'status'}
|
||||
|
||||
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': {
|
||||
'title': node['title'],
|
||||
'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'],
|
||||
'hideInMenu': not bool(node['display']),
|
||||
'menuVisibleWithForbidden': not bool(node['status']),
|
||||
|
||||
@@ -164,7 +164,7 @@ class ServerInfo:
|
||||
'mem_vms': ServerInfo.format_bytes(mem_info.vms),
|
||||
'mem_rss': ServerInfo.format_bytes(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,
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -46,7 +46,7 @@ dependencies = [
|
||||
"python-socketio>=5.12.0",
|
||||
"redis[hiredis]>=5.2.0",
|
||||
"rtoml>=0.12.0",
|
||||
"sqlalchemy-crud-plus>=1.8.0",
|
||||
"sqlalchemy-crud-plus>=1.10.0",
|
||||
"sqlalchemy[asyncio]>=2.0.40",
|
||||
"user-agents==2.2.0",
|
||||
]
|
||||
|
||||
+1
-1
@@ -252,7 +252,7 @@ sqlalchemy==2.0.40
|
||||
# alembic
|
||||
# fastapi-best-architecture
|
||||
# sqlalchemy-crud-plus
|
||||
sqlalchemy-crud-plus==1.8.0
|
||||
sqlalchemy-crud-plus==1.10.0
|
||||
# via fastapi-best-architecture
|
||||
starlette==0.46.1
|
||||
# via
|
||||
|
||||
@@ -674,7 +674,7 @@ requires-dist = [
|
||||
{ name = "redis", extras = ["hiredis"], specifier = ">=5.2.0" },
|
||||
{ name = "rtoml", specifier = ">=0.12.0" },
|
||||
{ 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" },
|
||||
]
|
||||
|
||||
@@ -1891,15 +1891,15 @@ asyncio = [
|
||||
|
||||
[[package]]
|
||||
name = "sqlalchemy-crud-plus"
|
||||
version = "1.8.0"
|
||||
source = { registry = "https://mirrors.aliyun.com/pypi/simple" }
|
||||
version = "1.10.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "pydantic" },
|
||||
{ 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 = [
|
||||
{ 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]]
|
||||
|
||||
Reference in New Issue
Block a user