Compare commits

..
25 Commits
Author SHA1 Message Date
Wu Clan e492cec7d7 Optimize codes and comments with cursor (#550) 2025-03-28 10:08:32 +08:00
PoetryL 92fe1e7554 Fix the query for the sub department (#557) 2025-03-27 23:30:12 +08:00
Wu Clan 7b080600f6 Update volumes of redis in docker compose (#556) 2025-03-26 18:58:06 +08:00
Wu Clan 71f9667bc0 Fix the celery env in docker compose (#555) 2025-03-26 18:34:41 +08:00
Thank CAT 2bf548f8b9 Fix filters for opera log query list (#554) 2025-03-25 09:19:28 +08:00
Wu Clan 15fd4545aa Optimize schemas with model relationships (#552) 2025-03-20 18:46:13 +08:00
Wu Clan 7f6c4ad6da Fix return schema of the config api (#551) 2025-03-20 16:27:44 +08:00
Wu Clan 2be12b4ea4 Fix async install plugin dependencies for windows (#549) 2025-03-17 23:39:10 +08:00
Wu Clan baebdd9870 Optimize dependencies to reduce package size (#548) 2025-03-17 18:41:58 +08:00
Wu Clan d1648bb709 Update git and docker ignore files (#547)
* Update git and docker ignore files

* add __pycache__ dir

* add .python-version
2025-03-17 13:20:21 +08:00
Wu Clan a4a394a18d Optimize the dynamic import of data models (#546) 2025-03-16 15:13:09 +08:00
Wu Clan 951fdc902a Optimize the return of relationship interfaces (#545)
* Optimize the return of relationship interfaces

* Fix gen business detail schema
2025-03-13 18:30:05 +08:00
Wu Clan 18ffa06d70 Updated the backend contribution guide (#544) 2025-03-12 18:15:10 +08:00
Wu Clan d28f152d78 Update the changelog for v1.0.3 (#543) 2025-03-12 14:23:38 +08:00
Wu Clan 1e3e9ea84a Update docker scripts and nginx conf (#542) 2025-03-11 23:26:53 +08:00
Huyuwei 5b42e59d65 Add Aliyun mirror to PyPI index in pyproject.toml (#541)
* Add Aliyun mirror to PyPI index in pyproject.toml

* Update uv.lock

* fix end of files

* Remove UV_INDEX environment variable from Dockerfile
2025-03-11 16:48:30 +08:00
Huyuwei 1882467554 Fix Dockerfile mounts for dependency installation (#540) 2025-03-10 15:34:11 +08:00
Huyuwei f1cd48e9d0 Fix typos in Dockerfile comments (#539) 2025-03-10 15:33:42 +08:00
Wu Clan d1fd84617b Refactor toml and dependencies file dir (#538)
* Refactor toml and dependencies file dir

* fix ci
2025-03-09 23:38:20 +08:00
Wu Clan 03fcf82b26 Update docker scripts in backend README (#537) 2025-03-04 18:34:07 +08:00
Wu Clan f75e1bf3df Update the changelog for v1.0.2 (#536) 2025-03-01 16:01:41 +08:00
Wu Clan 554a6f59bd Add plugin related interfaces (#535)
* Add plugin related interfaces

* Update interface auth

* Add build plugin zip service
2025-03-01 15:58:41 +08:00
Wu Clan 68d41cf028 Add log module root and output levels (#534) 2025-02-27 15:38:41 +08:00
Wu Clan 5defeca590 Fix celery async task worker pool (#533)
* Fix celery async task worker pool

* Update celery start scripts

* fix lint
2025-02-27 15:38:10 +08:00
Wu Clan 4d94418124 Update the changelog for v1.0.1 (#532) 2025-02-26 15:53:25 +08:00
168 changed files with 6108 additions and 4649 deletions
+9 -6
View File
@@ -1,6 +1,9 @@
**/__pycache__
**/venv
**/.venv
**/.python-version
**/.git
**/.github
__pycache__/
.git/
.github/
.idea/
.vscode/
venv/
.venv/
.ruff_cache/
.pytest_cache/
+2 -4
View File
@@ -25,12 +25,10 @@ jobs:
- name: Install dependencies
run: |
cd backend
uv sync --only-group lint
- name: Run lint
run: |
cd backend
source .venv/bin/activate
chmod 755 scripts/lint.sh
./scripts/lint.sh
chmod 755 backend/scripts/lint.sh
./backend/scripts/lint.sh
+7
View File
@@ -1,2 +1,9 @@
.idea/
.vscode/
.cursor/
.DS_Store
venv/
.venv/
.python-version
.ruff_cache/
.pytest_cache/
-7
View File
@@ -23,15 +23,8 @@ repos:
rev: 0.5.29
hooks:
- id: uv-lock
args:
- '--directory'
- 'backend'
files: backend/uv\.lock$
- id: uv-export
args:
- '--directory'
- 'backend'
- '-o'
- 'requirements.txt'
- '--no-hashes'
files: backend/uv\.lock$
+56 -1
View File
@@ -1,3 +1,55 @@
<a id="v1.0.3"></a>
# [v1.0.3](https://github.com/fastapi-practices/fastapi_best_architecture/releases/tag/v1.0.3) - 2025-03-11
## What's Changed
* Update the changelog for v1.0.2 by [@wu-clan](https://github.com/wu-clan) in [#536](https://github.com/fastapi-practices/fastapi_best_architecture/pull/536)
* Update docker scripts in backend README by [@wu-clan](https://github.com/wu-clan) in [#537](https://github.com/fastapi-practices/fastapi_best_architecture/pull/537)
* Refactor toml and dependencies file dir by [@wu-clan](https://github.com/wu-clan) in [#538](https://github.com/fastapi-practices/fastapi_best_architecture/pull/538)
* Fix typos in Dockerfile comments by [@huyuwei1996](https://github.com/huyuwei1996) in [#539](https://github.com/fastapi-practices/fastapi_best_architecture/pull/539)
* Fix Dockerfile mounts for dependency installation by [@huyuwei1996](https://github.com/huyuwei1996) in [#540](https://github.com/fastapi-practices/fastapi_best_architecture/pull/540)
* Add Aliyun mirror to PyPI index in pyproject.toml by [@huyuwei1996](https://github.com/huyuwei1996) in [#541](https://github.com/fastapi-practices/fastapi_best_architecture/pull/541)
* Update docker scripts and nginx conf by [@wu-clan](https://github.com/wu-clan) in [#542](https://github.com/fastapi-practices/fastapi_best_architecture/pull/542)
**Full Changelog**: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.0.2...v1.0.3
[Changes][v1.0.3]
<a id="v1.0.2"></a>
# [v1.0.2](https://github.com/fastapi-practices/fastapi_best_architecture/releases/tag/v1.0.2) - 2025-03-01
## What's Changed
* Update the changelog for v1.0.1 by [@wu-clan](https://github.com/wu-clan) in [#532](https://github.com/fastapi-practices/fastapi_best_architecture/pull/532)
* Fix celery async task worker pool by [@wu-clan](https://github.com/wu-clan) in [#533](https://github.com/fastapi-practices/fastapi_best_architecture/pull/533)
* Add log module root and output levels by [@wu-clan](https://github.com/wu-clan) in [#534](https://github.com/fastapi-practices/fastapi_best_architecture/pull/534)
* Add plugin related interfaces by [@wu-clan](https://github.com/wu-clan) in [#535](https://github.com/fastapi-practices/fastapi_best_architecture/pull/535)
**Full Changelog**: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.0.1...v1.0.2
[Changes][v1.0.2]
<a id="v1.0.1"></a>
# [v1.0.1](https://github.com/fastapi-practices/fastapi_best_architecture/releases/tag/v1.0.1) - 2025-02-26
## What's Changed
* Update the changelog for v1.0.0 by [@wu-clan](https://github.com/wu-clan) in [#524](https://github.com/fastapi-practices/fastapi_best_architecture/pull/524)
* Add missing volume config for docker deploy by [@huyuwei1996](https://github.com/huyuwei1996) in [#525](https://github.com/fastapi-practices/fastapi_best_architecture/pull/525)
* Add async attrs for sqla mapped base by [@wu-clan](https://github.com/wu-clan) in [#528](https://github.com/fastapi-practices/fastapi_best_architecture/pull/528)
* Add sqlalchemy connection pool config by [@wu-clan](https://github.com/wu-clan) in [#529](https://github.com/fastapi-practices/fastapi_best_architecture/pull/529)
* Fix the sql script for init data by [@wu-clan](https://github.com/wu-clan) in [#530](https://github.com/fastapi-practices/fastapi_best_architecture/pull/530)
* Optimize Dockerfile for faster builds by [@huyuwei1996](https://github.com/huyuwei1996) in [#526](https://github.com/fastapi-practices/fastapi_best_architecture/pull/526)
## New Contributors
* [@huyuwei1996](https://github.com/huyuwei1996) made their first contribution in [#525](https://github.com/fastapi-practices/fastapi_best_architecture/pull/525)
**Full Changelog**: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.0.0...v1.0.1
[Changes][v1.0.1]
<a id="v1.0.0"></a>
# [v1.0.0](https://github.com/fastapi-practices/fastapi_best_architecture/releases/tag/v1.0.0) - 2025-02-24
@@ -323,6 +375,9 @@
[Changes][v1.0.0]
[v1.0.3]: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.0.2...v1.0.3
[v1.0.2]: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.0.1...v1.0.2
[v1.0.1]: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.0.0...v1.0.1
[v1.0.0]: https://github.com/fastapi-practices/fastapi_best_architecture/tree/v1.0.0
<!-- Generated by https://github.com/rhysd/changelog-from-release v3.8.1 -->
<!-- Generated by https://github.com/rhysd/changelog-from-release v3.9.0 -->
+5 -6
View File
@@ -15,13 +15,12 @@ RUN sed -i 's/deb.debian.org/mirrors.ustc.edu.cn/g' /etc/apt/sources.list.d/debi
ENV UV_COMPILE_BYTECODE=1 \
UV_NO_CACHE=1 \
UV_LINK_MODE=copy \
UV_PROJECT_ENVIRONMENT=/usr/local \
UV_INDEX=https://mirrors.aliyun.com/pypi/simple
UV_PROJECT_ENVIRONMENT=/usr/local
# Install dependencies with cache
RUN --mount=type=cache,target=/root/.cache/uv \
--mount=type=bind,source=backend/uv.lock,target=uv.lock \
--mount=type=bind,source=backend/pyproject.toml,target=pyproject.toml \
--mount=type=bind,source=uv.lock,target=uv.lock \
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \
uv sync --frozen --no-default-groups --group server
# === Runtime base server image ===
@@ -43,7 +42,7 @@ WORKDIR /fba
ENV PYTHONPATH=/fba
RUN python3 backend/scripts/init_plugin.py
# === FastPAI server iamge ===
# === FastAPI server image ===
FROM base_server AS fastapi_server
WORKDIR /fba
@@ -57,7 +56,7 @@ EXPOSE 8001
CMD ["uvicorn", "backend.main:app", "--host", "0.0.0.0", "--port","8000"]
# === Celery server iamge ===
# === Celery server image ===
FROM base_server AS celery
WORKDIR /fba/backend/
+33 -33
View File
@@ -6,9 +6,7 @@
English | [简体中文](./README.zh-CN.md)
A backend and frontend separation solution based on the FastAPI framework, following
the [pseudo 3-tier architecture](#pseudo-3-tier-architecture) design, supporting **Python 3.10** and above
versions
Enterprise-level backend architecture solution
**🔥Continuously updated and maintained🔥**
@@ -49,37 +47,39 @@ pattern, use templates to transform it to your heart's content!
## Features
- [x] Design with FastAPI PEP 593 Annotated Parameters
- [x] Global asynchronous design with async/await + asgiref
- [x] Follows Restful API specification
- [x] Global SQLAlchemy 2.0 syntax
- [x] Pydantic v1 and v2 (different branches)
- [x] Casbin RBAC access control model
- [x] Role menu RBAC access control model
- [x] Celery asynchronous tasks
- [x] JWT middleware whitelist authentication
- [x] Global customizable time zone time
- [x] Docker / Docker-compose deployment
- [x] Pytest Unit Testing
- [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 features
## Built-in Functions
- [x] User management: System User Role Management, Permission Allocation
- [x] Department management: Configure system organization (company, department, team...)
- [x] Menu management: Configure system menu, user menu, button permission tags
- [x] Role management: role menu permission allocation, role route permission allocation
- [x] Dictionary management: Maintain commonly used fixed data or parameters within the system
- [x] Token management: System user online status detection, supports kicking users offline
- [x] Login authentication: backend-based graphical captcha background authentication login
- [x] Multipoint login: One-click modification of multipoint login through user information
- [x] OAuth 2.0: Built-in self-developed OAuth 2.0 login integration
- [x] Code generation: automatic backend code generation, supports preview, writing, and download
- [x] Scheduled task: Automated task, asynchronous task, supports function calls
- [x] Plugin system: Say goodbye to high coupling integration through hot-pluggable plugin mode
- [x] Operation log: Record and query of system normal and abnormal operations
- [x] Login log: Record and query of normal and abnormal user login
- [x] Service monitoring: Server hardware device information and status
- [x] API documentation: Automatically generate online interactive API documentation
- [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
@@ -103,7 +103,7 @@ the [official documentation](https://fastapi-practices.github.io/fastapi_best_ar
## Interactivity
[TG / Discord](https://wu-clan.github.io/homepage/)
[Discord](https://wu-clan.github.io/homepage/)
## Sponsor us
+31 -29
View File
@@ -6,7 +6,7 @@
简体中文 | [English](./README.md)
基于 FastAPI 框架的前后端分离中后台解决方案,遵循[伪三层架构](#伪三层架构)设计, 支持 **python3.10** 及以上版本
企业级后端架构解决方案
**🔥持续更新维护中🔥**
@@ -43,38 +43,40 @@ 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 v1 和 v2 (不同分支)
- [x] Casbin RBAC 访问控制模型
- [x] 角色菜单 RBAC 访问控制模型
- [x] Celery 异步任务
- [x] JWT 中间件白名单认证
- [x] 全局自定义时区时
- [x] Docker / Docker-compose 部署
- [x] Pytest 单元测试
- [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] OAuth20:内置自研 OAuth 2.0 登录集成
- [x] 代码生成:后端代码自动生成,支持预览,写入及下载
- [x] 定时任务:自动化任务,异步任务,支持函数调用
- [x] 插件系统:通过热插拔插件模式告别高耦合集成
- [x] 操作日志:系统正常和异常操作的日志记录与查询
- [x] 登录日志:用户正常和异常登录的日志记录与查询
- [x] 服务监控:服务器硬件设备信息与状态
- [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 文档
## 开发部署
@@ -98,7 +100,7 @@ mvc 架构作为常规设计模式,在 python web 中也很常见,但是三
## 互动
[TG / Discord](https://wu-clan.github.io/homepage/)
[Discord](https://wu-clan.github.io/homepage/)
## 赞助我们
-4
View File
@@ -1,4 +0,0 @@
__pycache__/
venv/
.venv/
.python-version
-6
View File
@@ -1,12 +1,6 @@
__pycache__/
.env
venv/
.venv/
.mypy_cache/
log/
alembic/versions/
static/media/
.ruff_cache/
.pytest_cache/
.python-version
celerybeat-schedule.*
+26 -22
View File
@@ -1,64 +1,68 @@
# FBA Project - Backend
# FastAPI Best Architecture - Backend
## Docker
> [!IMPORTANT]
> Due to Docker context limitations, you can't build an image in this directory
1. Make sure you're at the root of the project
2. Run the following Docker command to build container:
```shell
docker build -f backend/backend.dockerfile -t fba_backend_independent .
docker build -f Dockerfile -t fba_backend_independent .
```
3. Start container
Native boot needs to change `127.0.0.1` in `.env` to `host.docker.internal`
```shell
docker run -d fba_backend_independent -p 8000:8000 --name fba_app
docker run -d -p 8000:8000 --name fba_server fba_backend_independent
```
## Contributing
1. Prerequisites
You'll need the following prerequisites:
- Any python version between Python >= 3.10
- Python >= 3.10
- Git
- [uv](https://docs.astral.sh/uv/getting-started/installation/)
- Fork this repository to your GitHub account
2. Installation and setup
Clone your fork and cd into the repo directory
Clone your forked repository:
```shell
git clone https://github.com/<your username>/fastapi_best_architecture.git
cd fastapi_best_architecture/backend
uv venv
uv pip install -r requirements.txt
git clone https://github.com/<your account>/fastapi_best_architecture.git
```
3. Checkout a new branch and make your changes
Go to the root directory of the project, open the terminal, and run the following command:
```sh
uv sync --frozen
```
3. Checkout
Checkout a new branch and make your changes
```shell
# Checkout a new branch and make your changes
git checkout -b your-new-feature-branch
```
4. Run linting
4. Format and Lint
Auto-formatting and lint via `pre-commit`
```shell
# Run automated code formatting and linting
pre-commit run --all-files
```
5. Commit and push your changes
5. Commit and push
Commit your changes, push your branch to GitHub, and create a pull request.
Commit your changes and push your branch to the GitHub.
6. PR
Create a PR via GitHub
## Scripts
+1 -1
View File
@@ -36,7 +36,7 @@ async def user_login(
@router.post('/token/new', summary='创建新 token')
async def create_new_token(request: Request, response: Response) -> ResponseSchemaModel[GetNewToken]:
async def create_new_token(request: Request) -> ResponseSchemaModel[GetNewToken]:
data = await auth_service.new_token(request=request)
return response_base.success(data=data)
+6 -6
View File
@@ -18,7 +18,7 @@ router = APIRouter()
@router.get(
'',
summary='(模糊条件)分页获取登录日志',
summary='分页获取登录日志',
dependencies=[
DependsJwtAuth,
DependsPagination,
@@ -26,9 +26,9 @@ router = APIRouter()
)
async def get_pagination_login_logs(
db: CurrentSession,
username: Annotated[str | None, Query()] = None,
status: Annotated[int | None, Query()] = None,
ip: Annotated[str | None, Query()] = None,
username: Annotated[str | None, Query(description='用户名')] = None,
status: Annotated[int | None, Query(description='状态')] = None,
ip: Annotated[str | None, Query(description='IP 地址')] = None,
) -> ResponseSchemaModel[PageData[GetLoginLogDetail]]:
log_select = await login_log_service.get_select(username=username, status=status, ip=ip)
page_data = await paging_data(db, log_select)
@@ -37,13 +37,13 @@ async def get_pagination_login_logs(
@router.delete(
'',
summary='批量删除登录日志',
summary='批量删除登录日志',
dependencies=[
Depends(RequestPermission('log:login:del')),
DependsRBAC,
],
)
async def delete_login_log(pk: Annotated[list[int], Query(...)]) -> ResponseModel:
async def delete_login_log(pk: Annotated[list[int], Query(description='登录日志 ID 列表')]) -> ResponseModel:
count = await login_log_service.delete(pk=pk)
if count > 0:
return response_base.success()
+6 -6
View File
@@ -18,7 +18,7 @@ router = APIRouter()
@router.get(
'',
summary='(模糊条件)分页获取操作日志',
summary='分页获取操作日志',
dependencies=[
DependsJwtAuth,
DependsPagination,
@@ -26,9 +26,9 @@ router = APIRouter()
)
async def get_pagination_opera_logs(
db: CurrentSession,
username: Annotated[str | None, Query()] = None,
status: Annotated[int | None, Query()] = None,
ip: Annotated[str | None, Query()] = None,
username: Annotated[str | None, Query(description='用户名')] = None,
status: Annotated[int | None, Query(description='状态')] = None,
ip: Annotated[str | None, Query(description='IP 地址')] = None,
) -> ResponseSchemaModel[PageData[GetOperaLogDetail]]:
log_select = await opera_log_service.get_select(username=username, status=status, ip=ip)
page_data = await paging_data(db, log_select)
@@ -37,13 +37,13 @@ async def get_pagination_opera_logs(
@router.delete(
'',
summary='批量删除操作日志',
summary='批量删除操作日志',
dependencies=[
Depends(RequestPermission('log:opera:del')),
DependsRBAC,
],
)
async def delete_opera_log(pk: Annotated[list[int], Query(...)]) -> ResponseModel:
async def delete_opera_log(pk: Annotated[list[int], Query(description='操作日志 ID 列表')]) -> ResponseModel:
count = await opera_log_service.delete(pk=pk)
if count > 0:
return response_base.success()
+4 -1
View File
@@ -19,5 +19,8 @@ router = APIRouter()
],
)
async def get_redis_info() -> ResponseModel:
data = {'info': await redis_info.get_info(), 'stats': await redis_info.get_stats()}
data = {
'info': await redis_info.get_info(),
'stats': await redis_info.get_stats(),
}
return response_base.success(data=data)
+4 -4
View File
@@ -19,7 +19,7 @@ _linux_do_client = LinuxDoOAuth20(
_linux_do_oauth2 = FastAPIOAuth20(_linux_do_client, admin_settings.OAUTH2_LINUX_DO_REDIRECT_URI)
@router.get('', summary='获取 Linux Do 授权链接')
@router.get('', summary='获取 LinuxDo 授权链接')
async def linux_do_auth2() -> ResponseSchemaModel[str]:
auth_url = await _linux_do_client.get_authorization_url(redirect_uri=admin_settings.OAUTH2_LINUX_DO_REDIRECT_URI)
return response_base.success(data=auth_url)
@@ -27,8 +27,8 @@ async def linux_do_auth2() -> ResponseSchemaModel[str]:
@router.get(
'/callback',
summary='Linux Do 授权自动重定向',
description='Linux Do 授权后,自动重定向到当前地址并获取用户信息,通过用户信息自动创建系统用户',
summary='LinuxDo 授权自动重定向',
description='LinuxDo 授权后,自动重定向到当前地址并获取用户信息,通过用户信息自动创建系统用户',
dependencies=[Depends(RateLimiter(times=5, minutes=1))],
)
async def linux_do_login(
@@ -45,6 +45,6 @@ async def linux_do_login(
response=response,
background_tasks=background_tasks,
user=user,
social=UserSocialType.linuxdo,
social=UserSocialType.linux_do,
)
return RedirectResponse(url=f'{admin_settings.OAUTH2_FRONTEND_REDIRECT_URI}?access_token={data.access_token}')
+3 -1
View File
@@ -8,6 +8,7 @@ from backend.app.admin.api.v1.sys.dept import router as dept_router
from backend.app.admin.api.v1.sys.dict_data import router as dict_data_router
from backend.app.admin.api.v1.sys.dict_type import router as dict_type_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
@@ -15,7 +16,7 @@ from backend.app.admin.api.v1.sys.user import router as user_router
router = APIRouter(prefix='/sys')
router.include_router(config_router, prefix='/configs', tags=['系统配置'])
router.include_router(config_router, prefix='/configs', tags=['系统参数配置'])
router.include_router(dept_router, prefix='/depts', tags=['系统部门'])
router.include_router(dict_data_router, prefix='/dict-datas', tags=['系统字典数据'])
router.include_router(dict_type_router, prefix='/dict-types', tags=['系统字典类型'])
@@ -25,3 +26,4 @@ router.include_router(user_router, prefix='/users', tags=['系统用户'])
router.include_router(data_rule_router, prefix='/data-rules', 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=['系统插件'])
+15 -15
View File
@@ -21,15 +21,15 @@ from backend.database.db import CurrentSession
router = APIRouter()
@router.get('/website', summary='获取网站配置信息', dependencies=[DependsJwtAuth])
async def get_website_config() -> ResponseSchemaModel[GetConfigDetail]:
@router.get('/website', summary='获取网站参数配置', dependencies=[DependsJwtAuth])
async def get_website_config() -> ResponseSchemaModel[list[GetConfigDetail]]:
config = await config_service.get_built_in_config('website')
return response_base.success(data=config)
@router.post(
'/website',
summary='保存网站配置信息',
summary='保存网站参数配置',
dependencies=[
Depends(RequestPermission('sys:config:website:add')),
DependsRBAC,
@@ -41,7 +41,7 @@ async def save_website_config(objs: list[SaveBuiltInConfigParam]) -> ResponseMod
@router.get('/protocol', summary='获取用户协议', dependencies=[DependsJwtAuth])
async def get_protocol_config() -> ResponseSchemaModel[GetConfigDetail]:
async def get_protocol_config() -> ResponseSchemaModel[list[GetConfigDetail]]:
config = await config_service.get_built_in_config('protocol')
return response_base.success(data=config)
@@ -60,7 +60,7 @@ async def save_protocol_config(objs: list[SaveBuiltInConfigParam]) -> ResponseMo
@router.get('/policy', summary='获取用户政策', dependencies=[DependsJwtAuth])
async def get_policy_config() -> ResponseSchemaModel[GetConfigDetail]:
async def get_policy_config() -> ResponseSchemaModel[list[GetConfigDetail]]:
config = await config_service.get_built_in_config('policy')
return response_base.success(data=config)
@@ -78,23 +78,23 @@ async def save_policy_config(objs: list[SaveBuiltInConfigParam]) -> ResponseMode
return response_base.success()
@router.get('/{pk}', summary='获取系统参数配置详情', dependencies=[DependsJwtAuth])
async def get_config(pk: Annotated[int, Path(...)]) -> ResponseSchemaModel[GetConfigDetail]:
@router.get('/{pk}', summary='获取参数配置详情', dependencies=[DependsJwtAuth])
async def get_config(pk: Annotated[int, Path(description='参数配置 ID')]) -> ResponseSchemaModel[GetConfigDetail]:
config = await config_service.get(pk)
return response_base.success(data=config)
@router.get(
'',
summary='(模糊条件)分页获取所有系统参数配置',
summary='分页获取所有参数配置',
dependencies=[
DependsJwtAuth,
DependsPagination,
],
)
async def get_pagination_config(
async def get_pagination_configs(
db: CurrentSession,
name: Annotated[str | None, Query()] = None,
name: Annotated[str | None, Query(description='参数配置名称')] = None,
type: Annotated[str | None, Query()] = None,
) -> ResponseSchemaModel[PageData[GetConfigDetail]]:
config_select = await config_service.get_select(name=name, type=type)
@@ -104,7 +104,7 @@ async def get_pagination_config(
@router.post(
'',
summary='创建系统参数配置',
summary='创建参数配置',
dependencies=[
Depends(RequestPermission('sys:config:add')),
DependsRBAC,
@@ -117,13 +117,13 @@ async def create_config(obj: CreateConfigParam) -> ResponseModel:
@router.put(
'/{pk}',
summary='更新系统参数配置',
summary='更新参数配置',
dependencies=[
Depends(RequestPermission('sys:config:edit')),
DependsRBAC,
],
)
async def update_config(pk: Annotated[int, Path(...)], obj: UpdateConfigParam) -> ResponseModel:
async def update_config(pk: Annotated[int, Path(description='参数配置 ID')], obj: UpdateConfigParam) -> ResponseModel:
count = await config_service.update(pk=pk, obj=obj)
if count > 0:
return response_base.success()
@@ -132,13 +132,13 @@ async def update_config(pk: Annotated[int, Path(...)], obj: UpdateConfigParam) -
@router.delete(
'',
summary='批量删除系统参数配置',
summary='批量删除参数配置',
dependencies=[
Depends(RequestPermission('sys:config:del')),
DependsRBAC,
],
)
async def delete_config(pk: Annotated[list[int], Query(...)]) -> ResponseModel:
async def delete_config(pk: Annotated[list[int], Query(description='参数配置 ID 列表')]) -> ResponseModel:
count = await config_service.delete(pk=pk)
if count > 0:
return response_base.success()
+19 -14
View File
@@ -12,7 +12,6 @@ from backend.common.security.jwt import DependsJwtAuth
from backend.common.security.permission import RequestPermission
from backend.common.security.rbac import DependsRBAC
from backend.database.db import CurrentSession
from backend.utils.serializers import select_as_dict, select_list_serialize
router = APIRouter()
@@ -24,35 +23,37 @@ async def get_data_rule_models() -> ResponseSchemaModel[list[str]]:
@router.get('/model/{model}/columns', summary='获取支持过滤的数据库模型列', dependencies=[DependsJwtAuth])
async def get_data_rule_model_columns(model: Annotated[str, Path()]) -> ResponseSchemaModel[list[str]]:
async def get_data_rule_model_columns(
model: Annotated[str, Path(description='模型名称')],
) -> ResponseSchemaModel[list[str]]:
models = await data_rule_service.get_columns(model=model)
return response_base.success(data=models)
@router.get('/all', summary='获取所有数据规则', dependencies=[DependsJwtAuth])
async def get_all_data_rule() -> ResponseSchemaModel[list[GetDataRuleDetail]]:
data_rules = await data_rule_service.get_all()
data = select_list_serialize(data_rules)
async def get_all_data_rules() -> ResponseSchemaModel[list[GetDataRuleDetail]]:
data = await data_rule_service.get_all()
return response_base.success(data=data)
@router.get('/{pk}', summary='获取数据权限规则详情', dependencies=[DependsJwtAuth])
async def get_data_rule(pk: Annotated[int, Path(...)]) -> ResponseSchemaModel[GetDataRuleDetail]:
data_rule = await data_rule_service.get(pk=pk)
data = GetDataRuleDetail(**select_as_dict(data_rule))
async def get_data_rule(
pk: Annotated[int, Path(description='数据规则 ID')],
) -> ResponseSchemaModel[GetDataRuleDetail]:
data = await data_rule_service.get(pk=pk)
return response_base.success(data=data)
@router.get(
'',
summary='(模糊条件)分页获取所有数据权限规则',
summary='分页获取所有数据权限规则',
dependencies=[
DependsJwtAuth,
DependsPagination,
],
)
async def get_pagination_data_rule(
db: CurrentSession, name: Annotated[str | None, Query()] = None
async def get_pagination_data_rules(
db: CurrentSession, name: Annotated[str | None, Query(description='规则名称')] = None
) -> ResponseSchemaModel[PageData[GetDataRuleDetail]]:
data_rule_select = await data_rule_service.get_select(name=name)
page_data = await paging_data(db, data_rule_select)
@@ -80,7 +81,9 @@ async def create_data_rule(obj: CreateDataRuleParam) -> ResponseModel:
DependsRBAC,
],
)
async def update_data_rule(pk: Annotated[int, Path(...)], obj: UpdateDataRuleParam) -> ResponseModel:
async def update_data_rule(
pk: Annotated[int, Path(description='数据规则 ID')], obj: UpdateDataRuleParam
) -> ResponseModel:
count = await data_rule_service.update(pk=pk, obj=obj)
if count > 0:
return response_base.success()
@@ -89,13 +92,15 @@ async def update_data_rule(pk: Annotated[int, Path(...)], obj: UpdateDataRulePar
@router.delete(
'',
summary='批量删除数据权限规则',
summary='批量删除数据权限规则',
dependencies=[
Depends(RequestPermission('data:rule:del')),
DependsRBAC,
],
)
async def delete_data_rule(request: Request, pk: Annotated[list[int], Query(...)]) -> ResponseModel:
async def delete_data_rule(
request: Request, pk: Annotated[list[int], Query(description='数据规则 ID 列表')]
) -> ResponseModel:
count = await data_rule_service.delete(request=request, pk=pk)
if count > 0:
return response_base.success()
+9 -11
View File
@@ -10,24 +10,22 @@ from backend.common.response.response_schema import ResponseModel, ResponseSchem
from backend.common.security.jwt import DependsJwtAuth
from backend.common.security.permission import RequestPermission
from backend.common.security.rbac import DependsRBAC
from backend.utils.serializers import select_as_dict
router = APIRouter()
@router.get('/{pk}', summary='获取部门详情', dependencies=[DependsJwtAuth])
async def get_dept(pk: Annotated[int, Path(...)]) -> ResponseSchemaModel[GetDeptDetail]:
dept = await dept_service.get(pk=pk)
data = GetDeptDetail(**select_as_dict(dept))
async def get_dept(pk: Annotated[int, Path(description='部门 ID')]) -> ResponseSchemaModel[GetDeptDetail]:
data = await dept_service.get(pk=pk)
return response_base.success(data=data)
@router.get('', summary='获取所有部门展示树', dependencies=[DependsJwtAuth])
async def get_all_depts_tree(
name: Annotated[str | None, Query()] = None,
leader: Annotated[str | None, Query()] = None,
phone: Annotated[str | None, Query()] = None,
status: Annotated[int | None, Query()] = None,
async def get_all_depts(
name: Annotated[str | None, Query(description='部门名称')] = None,
leader: Annotated[str | None, Query(description='部门负责人')] = None,
phone: Annotated[str | None, Query(description='联系电话')] = None,
status: Annotated[int | None, Query(description='状态')] = None,
) -> ResponseSchemaModel[list[dict[str, Any]]]:
dept = await dept_service.get_dept_tree(name=name, leader=leader, phone=phone, status=status)
return response_base.success(data=dept)
@@ -54,7 +52,7 @@ async def create_dept(obj: CreateDeptParam) -> ResponseModel:
DependsRBAC,
],
)
async def update_dept(pk: Annotated[int, Path(...)], obj: UpdateDeptParam) -> ResponseModel:
async def update_dept(pk: Annotated[int, Path(description='部门 ID')], obj: UpdateDeptParam) -> ResponseModel:
count = await dept_service.update(pk=pk, obj=obj)
if count > 0:
return response_base.success()
@@ -69,7 +67,7 @@ async def update_dept(pk: Annotated[int, Path(...)], obj: UpdateDeptParam) -> Re
DependsRBAC,
],
)
async def delete_dept(request: Request, pk: Annotated[int, Path(...)]) -> ResponseModel:
async def delete_dept(request: Request, pk: Annotated[int, Path(description='部门 ID')]) -> ResponseModel:
count = await dept_service.delete(request=request, pk=pk)
if count > 0:
return response_base.success()
+19 -12
View File
@@ -4,7 +4,12 @@ from typing import Annotated
from fastapi import APIRouter, Depends, Path, Query
from backend.app.admin.schema.dict_data import CreateDictDataParam, GetDictDataDetail, UpdateDictDataParam
from backend.app.admin.schema.dict_data import (
CreateDictDataParam,
GetDictDataDetail,
GetDictDataWithRelation,
UpdateDictDataParam,
)
from backend.app.admin.service.dict_data_service import dict_data_service
from backend.common.pagination import DependsPagination, PageData, paging_data
from backend.common.response.response_schema import ResponseModel, ResponseSchemaModel, response_base
@@ -12,21 +17,21 @@ from backend.common.security.jwt import DependsJwtAuth
from backend.common.security.permission import RequestPermission
from backend.common.security.rbac import DependsRBAC
from backend.database.db import CurrentSession
from backend.utils.serializers import select_as_dict
router = APIRouter()
@router.get('/{pk}', summary='获取字典详情', dependencies=[DependsJwtAuth])
async def get_dict_data(pk: Annotated[int, Path(...)]) -> ResponseSchemaModel[GetDictDataDetail]:
dict_data = await dict_data_service.get(pk=pk)
data = GetDictDataDetail(**select_as_dict(dict_data))
async def get_dict_data(
pk: Annotated[int, Path(description='字典数据 ID')],
) -> ResponseSchemaModel[GetDictDataWithRelation]:
data = await dict_data_service.get(pk=pk)
return response_base.success(data=data)
@router.get(
'',
summary='(模糊条件)分页获取所有字典',
summary='分页获取所有字典',
dependencies=[
DependsJwtAuth,
DependsPagination,
@@ -34,9 +39,9 @@ async def get_dict_data(pk: Annotated[int, Path(...)]) -> ResponseSchemaModel[Ge
)
async def get_pagination_dict_datas(
db: CurrentSession,
label: Annotated[str | None, Query()] = None,
value: Annotated[str | None, Query()] = None,
status: Annotated[int | None, Query()] = None,
label: Annotated[str | None, Query(description='字典数据标签')] = None,
value: Annotated[str | None, Query(description='字典数据键值')] = None,
status: Annotated[int | None, Query(description='状态')] = None,
) -> ResponseSchemaModel[PageData[GetDictDataDetail]]:
dict_data_select = await dict_data_service.get_select(label=label, value=value, status=status)
page_data = await paging_data(db, dict_data_select)
@@ -64,7 +69,9 @@ async def create_dict_data(obj: CreateDictDataParam) -> ResponseModel:
DependsRBAC,
],
)
async def update_dict_data(pk: Annotated[int, Path(...)], obj: UpdateDictDataParam) -> ResponseModel:
async def update_dict_data(
pk: Annotated[int, Path(description='字典数据 ID')], obj: UpdateDictDataParam
) -> ResponseModel:
count = await dict_data_service.update(pk=pk, obj=obj)
if count > 0:
return response_base.success()
@@ -73,13 +80,13 @@ async def update_dict_data(pk: Annotated[int, Path(...)], obj: UpdateDictDataPar
@router.delete(
'',
summary='批量删除字典',
summary='批量删除字典',
dependencies=[
Depends(RequestPermission('sys:dict:data:del')),
DependsRBAC,
],
)
async def delete_dict_data(pk: Annotated[list[int], Query(...)]) -> ResponseModel:
async def delete_dict_data(pk: Annotated[list[int], Query(description='字典数据 ID 列表')]) -> ResponseModel:
count = await dict_data_service.delete(pk=pk)
if count > 0:
return response_base.success()
+9 -7
View File
@@ -18,7 +18,7 @@ router = APIRouter()
@router.get(
'',
summary='(模糊条件)分页获取所有字典类型',
summary='分页获取所有字典类型',
dependencies=[
DependsJwtAuth,
DependsPagination,
@@ -26,9 +26,9 @@ router = APIRouter()
)
async def get_pagination_dict_types(
db: CurrentSession,
name: Annotated[str | None, Query()] = None,
code: Annotated[str | None, Query()] = None,
status: Annotated[int | None, Query()] = None,
name: Annotated[str | None, Query(description='字典类型名称')] = None,
code: Annotated[str | None, Query(description='字典类型编码')] = None,
status: Annotated[int | None, Query(description='状态')] = None,
) -> ResponseSchemaModel[PageData[GetDictTypeDetail]]:
dict_type_select = await dict_type_service.get_select(name=name, code=code, status=status)
page_data = await paging_data(db, dict_type_select)
@@ -56,7 +56,9 @@ async def create_dict_type(obj: CreateDictTypeParam) -> ResponseModel:
DependsRBAC,
],
)
async def update_dict_type(pk: Annotated[int, Path(...)], obj: UpdateDictTypeParam) -> ResponseModel:
async def update_dict_type(
pk: Annotated[int, Path(description='字典类型 ID')], obj: UpdateDictTypeParam
) -> ResponseModel:
count = await dict_type_service.update(pk=pk, obj=obj)
if count > 0:
return response_base.success()
@@ -65,13 +67,13 @@ async def update_dict_type(pk: Annotated[int, Path(...)], obj: UpdateDictTypePar
@router.delete(
'',
summary='批量删除字典类型',
summary='批量删除字典类型',
dependencies=[
Depends(RequestPermission('sys:dict:type:del')),
DependsRBAC,
],
)
async def delete_dict_type(pk: Annotated[list[int], Query(...)]) -> ResponseModel:
async def delete_dict_type(pk: Annotated[list[int], Query(description='字典类型 ID 列表')]) -> ResponseModel:
count = await dict_type_service.delete(pk=pk)
if count > 0:
return response_base.success()
+8 -9
View File
@@ -10,27 +10,26 @@ from backend.common.response.response_schema import ResponseModel, ResponseSchem
from backend.common.security.jwt import DependsJwtAuth
from backend.common.security.permission import RequestPermission
from backend.common.security.rbac import DependsRBAC
from backend.utils.serializers import select_as_dict
router = APIRouter()
@router.get('/sidebar', summary='获取用户菜单展示树', dependencies=[DependsJwtAuth])
async def get_user_sidebar_tree(request: Request) -> ResponseSchemaModel[list[dict[str, Any]]]:
@router.get('/sidebar', summary='获取用户侧边栏', dependencies=[DependsJwtAuth])
async def get_user_sidebar(request: Request) -> ResponseSchemaModel[list[dict[str, Any]]]:
menu = await menu_service.get_user_menu_tree(request=request)
return response_base.success(data=menu)
@router.get('/{pk}', summary='获取菜单详情', dependencies=[DependsJwtAuth])
async def get_menu(pk: Annotated[int, Path(...)]) -> ResponseSchemaModel[GetMenuDetail]:
menu = await menu_service.get(pk=pk)
data = GetMenuDetail(**select_as_dict(menu))
async def get_menu(pk: Annotated[int, Path(description='菜单 ID')]) -> ResponseSchemaModel[GetMenuDetail]:
data = await menu_service.get(pk=pk)
return response_base.success(data=data)
@router.get('', summary='获取所有菜单展示树', dependencies=[DependsJwtAuth])
async def get_all_menus(
title: Annotated[str | None, Query()] = None, status: Annotated[int | None, Query()] = None
title: Annotated[str | None, Query(description='菜单标题')] = None,
status: Annotated[int | None, Query(description='状体')] = None,
) -> ResponseSchemaModel[list[dict[str, Any]]]:
menu = await menu_service.get_menu_tree(title=title, status=status)
return response_base.success(data=menu)
@@ -57,7 +56,7 @@ async def create_menu(obj: CreateMenuParam) -> ResponseModel:
DependsRBAC,
],
)
async def update_menu(pk: Annotated[int, Path(...)], obj: UpdateMenuParam) -> ResponseModel:
async def update_menu(pk: Annotated[int, Path(description='菜单 ID')], obj: UpdateMenuParam) -> ResponseModel:
count = await menu_service.update(pk=pk, obj=obj)
if count > 0:
return response_base.success()
@@ -72,7 +71,7 @@ async def update_menu(pk: Annotated[int, Path(...)], obj: UpdateMenuParam) -> Re
DependsRBAC,
],
)
async def delete_menu(request: Request, pk: Annotated[int, Path(...)]) -> ResponseModel:
async def delete_menu(request: Request, pk: Annotated[int, Path(description='菜单 ID 列表')]) -> ResponseModel:
count = await menu_service.delete(request=request, pk=pk)
if count > 0:
return response_base.success()
+100
View File
@@ -0,0 +1,100 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import io
import os.path
import zipfile
from typing import Annotated
from fastapi import APIRouter, Depends, File, UploadFile
from fastapi.params import Query
from starlette.responses import StreamingResponse
from backend.common.exception import errors
from backend.common.response.response_schema import ResponseModel, response_base
from backend.common.security.permission import RequestPermission
from backend.common.security.rbac import DependsRBAC
from backend.core.path_conf import PLUGIN_DIR
from backend.plugin.tools import install_requirements_async
router = APIRouter()
@router.post(
'/install',
summary='安装插件',
description='需使用插件 zip 压缩包进行安装',
dependencies=[
Depends(RequestPermission('sys:plugin:install')),
DependsRBAC,
],
)
async def install_plugin(file: Annotated[UploadFile, File()]) -> ResponseModel:
contents = await file.read()
file_bytes = io.BytesIO(contents)
if not zipfile.is_zipfile(file_bytes):
raise errors.ForbiddenError(msg='插件压缩包格式非法')
with zipfile.ZipFile(file_bytes) as zf:
# 校验压缩包
plugin_dir_in_zip = f'{file.filename[:-4]}/backend/plugin/'
members_in_plugin_dir = [name for name in zf.namelist() if name.startswith(plugin_dir_in_zip)]
if not members_in_plugin_dir:
raise errors.ForbiddenError(msg='插件压缩包内容非法')
plugin_name = members_in_plugin_dir[1].replace(plugin_dir_in_zip, '').replace('/', '')
if (
len(members_in_plugin_dir) <= 3
or f'{plugin_dir_in_zip}{plugin_name}/plugin.toml' not in members_in_plugin_dir
or f'{plugin_dir_in_zip}{plugin_name}/README.md' not in members_in_plugin_dir
):
raise errors.ForbiddenError(msg='插件压缩包内缺少必要文件')
# 插件是否可安装
full_plugin_path = os.path.join(PLUGIN_DIR, plugin_name)
if os.path.exists(full_plugin_path):
raise errors.ForbiddenError(msg='此插件已安装')
else:
os.makedirs(full_plugin_path, exist_ok=True)
# 解压(安装)
members = []
for member in zf.infolist():
if member.filename.startswith(plugin_dir_in_zip):
new_filename = member.filename.replace(plugin_dir_in_zip, '')
if new_filename:
member.filename = new_filename
members.append(member)
zf.extractall(PLUGIN_DIR, members)
if os.path.exists(os.path.join(full_plugin_path, 'requirements.txt')):
await install_requirements_async()
return response_base.success()
@router.post(
'/zip',
summary='打包插件',
dependencies=[
Depends(RequestPermission('sys:plugin:zip')),
DependsRBAC,
],
)
async def build_plugin(plugin: Annotated[str, Query(description='插件名称')]) -> StreamingResponse:
plugin_dir = os.path.join(PLUGIN_DIR, plugin)
if not os.path.exists(plugin_dir):
raise errors.ForbiddenError(msg='插件不存在')
bio = io.BytesIO()
with zipfile.ZipFile(bio, 'w') as zf:
for root, dirs, files in os.walk(plugin_dir):
dirs[:] = [d for d in dirs if d != '__pycache__']
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)
bio.seek(0)
return StreamingResponse(
bio,
media_type='application/x-zip-compressed',
headers={'Content-Disposition': f'attachment; filename={plugin}.zip'},
)
+22 -19
View File
@@ -7,6 +7,7 @@ from fastapi import APIRouter, Depends, Path, Query, Request
from backend.app.admin.schema.role import (
CreateRoleParam,
GetRoleDetail,
GetRoleWithRelationDetail,
UpdateRoleMenuParam,
UpdateRoleParam,
UpdateRoleRuleParam,
@@ -20,47 +21,49 @@ from backend.common.security.jwt import DependsJwtAuth
from backend.common.security.permission import RequestPermission
from backend.common.security.rbac import DependsRBAC
from backend.database.db import CurrentSession
from backend.utils.serializers import select_as_dict, select_list_serialize
router = APIRouter()
@router.get('/all', summary='获取所有角色', dependencies=[DependsJwtAuth])
async def get_all_roles() -> ResponseSchemaModel[list[GetRoleDetail]]:
roles = await role_service.get_all()
data = select_list_serialize(roles)
data = await role_service.get_all()
return response_base.success(data=data)
@router.get('/{pk}/all', summary='获取用户所有角色', dependencies=[DependsJwtAuth])
async def get_user_all_roles(pk: Annotated[int, Path(...)]) -> ResponseSchemaModel[list[GetRoleDetail]]:
roles = await role_service.get_by_user(pk=pk)
data = select_list_serialize(roles)
async def get_user_all_roles(
pk: Annotated[int, Path(description='用户 ID')],
) -> ResponseSchemaModel[list[GetRoleDetail]]:
data = await role_service.get_by_user(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(...)]) -> ResponseSchemaModel[list[dict[str, Any]]]:
async def get_role_all_menus(
pk: Annotated[int, Path(description='角色 ID')],
) -> ResponseSchemaModel[list[dict[str, Any]]]:
menu = await menu_service.get_role_menu_tree(pk=pk)
return response_base.success(data=menu)
@router.get('/{pk}/rules', summary='获取角色所有数据规则', dependencies=[DependsJwtAuth])
async def get_role_all_rules(pk: Annotated[int, Path(...)]) -> ResponseSchemaModel[list[int]]:
async def get_role_all_rules(pk: Annotated[int, Path(description='角色 ID')]) -> ResponseSchemaModel[list[int]]:
rule = await data_rule_service.get_role_rules(pk=pk)
return response_base.success(data=rule)
@router.get('/{pk}', summary='获取角色详情', dependencies=[DependsJwtAuth])
async def get_role(pk: Annotated[int, Path(...)]) -> ResponseSchemaModel[GetRoleDetail]:
role = await role_service.get(pk=pk)
data = GetRoleDetail(**select_as_dict(role))
async def get_role(
pk: Annotated[int, Path(description='角色 ID')],
) -> ResponseSchemaModel[GetRoleWithRelationDetail]:
data = await role_service.get(pk=pk)
return response_base.success(data=data)
@router.get(
'',
summary='(模糊条件)分页获取所有角色',
summary='分页获取所有角色',
dependencies=[
DependsJwtAuth,
DependsPagination,
@@ -68,8 +71,8 @@ async def get_role(pk: Annotated[int, Path(...)]) -> ResponseSchemaModel[GetRole
)
async def get_pagination_roles(
db: CurrentSession,
name: Annotated[str | None, Query()] = None,
status: Annotated[int | None, Query()] = None,
name: Annotated[str | None, Query(description='角色名称')] = None,
status: Annotated[int | None, Query(description='状态')] = None,
) -> ResponseSchemaModel[PageData[GetRoleDetail]]:
role_select = await role_service.get_select(name=name, status=status)
page_data = await paging_data(db, role_select)
@@ -97,7 +100,7 @@ async def create_role(obj: CreateRoleParam) -> ResponseModel:
DependsRBAC,
],
)
async def update_role(pk: Annotated[int, Path(...)], obj: UpdateRoleParam) -> ResponseModel:
async def update_role(pk: Annotated[int, Path(description='角色 ID')], obj: UpdateRoleParam) -> ResponseModel:
count = await role_service.update(pk=pk, obj=obj)
if count > 0:
return response_base.success()
@@ -113,7 +116,7 @@ async def update_role(pk: Annotated[int, Path(...)], obj: UpdateRoleParam) -> Re
],
)
async def update_role_menus(
request: Request, pk: Annotated[int, Path(...)], menu_ids: UpdateRoleMenuParam
request: Request, pk: Annotated[int, Path(description='角色 ID')], menu_ids: UpdateRoleMenuParam
) -> ResponseModel:
count = await role_service.update_role_menu(request=request, pk=pk, menu_ids=menu_ids)
if count > 0:
@@ -130,7 +133,7 @@ async def update_role_menus(
],
)
async def update_role_rules(
request: Request, pk: Annotated[int, Path(...)], rule_ids: UpdateRoleRuleParam
request: Request, pk: Annotated[int, Path(description='角色 ID')], rule_ids: UpdateRoleRuleParam
) -> ResponseModel:
count = await role_service.update_role_rule(request=request, pk=pk, rule_ids=rule_ids)
if count > 0:
@@ -140,13 +143,13 @@ async def update_role_rules(
@router.delete(
'',
summary='批量删除角色',
summary='批量删除角色',
dependencies=[
Depends(RequestPermission('sys:role:del')),
DependsRBAC,
],
)
async def delete_role(request: Request, pk: Annotated[list[int], Query(...)]) -> ResponseModel:
async def delete_role(request: Request, pk: Annotated[list[int], Query(description='角色 ID 列表')]) -> ResponseModel:
count = await role_service.delete(request=request, pk=pk)
if count > 0:
return response_base.success()
+28 -24
View File
@@ -9,7 +9,7 @@ from fastapi import APIRouter, Depends, Path, Query, Request
from backend.app.admin.schema.token import GetTokenDetail, KickOutToken
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, superuser_verify
from backend.common.security.jwt import DependsJwtAuth, jwt_decode, revoke_token, superuser_verify
from backend.common.security.permission import RequestPermission
from backend.common.security.rbac import DependsRBAC
from backend.core.conf import settings
@@ -19,10 +19,28 @@ router = APIRouter()
@router.get('', summary='获取令牌列表', dependencies=[DependsJwtAuth])
async def get_tokens(username: Annotated[str | None, Query()] = None) -> ResponseSchemaModel[list[GetTokenDetail]]:
async def get_tokens(
username: Annotated[str | None, Query(description='用户名')] = None,
) -> ResponseSchemaModel[list[GetTokenDetail]]:
token_keys = await redis_client.keys(f'{settings.TOKEN_REDIS_PREFIX}:*')
token_online = await redis_client.smembers(settings.TOKEN_ONLINE_REDIS_PREFIX)
data = []
online_clients = await redis_client.smembers(settings.TOKEN_ONLINE_REDIS_PREFIX)
data: list[GetTokenDetail] = []
def append_token_detail() -> None:
data.append(
token_detail.model_copy(
update={
'username': extra_info.get('username', '未知'),
'nickname': extra_info.get('nickname', '未知'),
'ip': extra_info.get('ip', '未知'),
'os': extra_info.get('os', '未知'),
'browser': extra_info.get('browser', '未知'),
'device': extra_info.get('device', '未知'),
'last_login_time': extra_info.get('last_login_time', '未知'),
}
)
)
for key in token_keys:
token = await redis_client.get(key)
token_payload = jwt_decode(token)
@@ -36,31 +54,15 @@ async def get_tokens(username: Annotated[str | None, Query()] = None) -> Respons
os='未知',
browser='未知',
device='未知',
status=StatusType.disable if session_uuid not in token_online else StatusType.enable,
status=StatusType.enable if session_uuid in online_clients else StatusType.disable,
last_login_time='未知',
expire_time=token_payload.expire_time,
)
extra_info = await redis_client.get(f'{settings.TOKEN_EXTRA_INFO_REDIS_PREFIX}:{session_uuid}')
if extra_info:
def append_token_detail():
data.append(
token_detail.model_copy(
update={
'username': extra_info.get('username'),
'nickname': extra_info.get('nickname'),
'ip': extra_info.get('ip'),
'os': extra_info.get('os'),
'browser': extra_info.get('browser'),
'device': extra_info.get('device'),
'last_login_time': extra_info.get('last_login_time'),
}
)
)
extra_info = json.loads(extra_info)
if extra_info.get('login_type') != 'swagger':
if username:
if username is not None:
if username == extra_info.get('username'):
append_token_detail()
else:
@@ -78,7 +80,9 @@ async def get_tokens(username: Annotated[str | None, Query()] = None) -> Respons
DependsRBAC,
],
)
async def kick_out(request: Request, pk: Annotated[int, Path(...)], session_uuid: KickOutToken) -> ResponseModel:
async def kick_out(
request: Request, pk: Annotated[int, Path(description='用户 ID')], obj: KickOutToken
) -> ResponseModel:
superuser_verify(request)
await redis_client.delete(f'{settings.TOKEN_REDIS_PREFIX}:{pk}:{session_uuid}')
await revoke_token(str(pk), obj.session_uuid)
return response_base.success()
+30 -27
View File
@@ -7,8 +7,8 @@ from fastapi import APIRouter, Depends, Path, Query, Request
from backend.app.admin.schema.user import (
AddUserParam,
AvatarParam,
GetCurrentUserInfoDetail,
GetUserInfoDetail,
GetCurrentUserInfoWithRelationDetail,
GetUserInfoWithRelationDetail,
RegisterUserParam,
ResetPasswordParam,
UpdateUserParam,
@@ -21,7 +21,6 @@ from backend.common.security.jwt import DependsJwtAuth
from backend.common.security.permission import RequestPermission
from backend.common.security.rbac import DependsRBAC
from backend.database.db import CurrentSession
from backend.utils.serializers import select_as_dict
router = APIRouter()
@@ -33,10 +32,9 @@ async def register_user(obj: RegisterUserParam) -> ResponseModel:
@router.post('/add', summary='添加用户', dependencies=[DependsRBAC])
async def add_user(request: Request, obj: AddUserParam) -> ResponseSchemaModel[GetUserInfoDetail]:
async def add_user(request: Request, obj: AddUserParam) -> ResponseSchemaModel[GetUserInfoWithRelationDetail]:
await user_service.add(request=request, obj=obj)
current_user = await user_service.get_userinfo(username=obj.username)
data = GetUserInfoDetail(**select_as_dict(current_user))
data = await user_service.get_userinfo(username=obj.username)
return response_base.success(data=data)
@@ -48,21 +46,24 @@ async def password_reset(request: Request, obj: ResetPasswordParam) -> ResponseM
return response_base.fail()
@router.get('/me', summary='获取当前用户信息', dependencies=[DependsJwtAuth], response_model_exclude={'password'})
async def get_current_user(request: Request) -> ResponseSchemaModel[GetCurrentUserInfoDetail]:
data = GetCurrentUserInfoDetail(**request.user.model_dump())
@router.get('/me', summary='获取当前用户信息', dependencies=[DependsJwtAuth])
async def get_current_user(request: Request) -> ResponseSchemaModel[GetCurrentUserInfoWithRelationDetail]:
data = request.user.model_dump()
return response_base.success(data=data)
@router.get('/{username}', summary='查看用户信息', dependencies=[DependsJwtAuth])
async def get_user(username: Annotated[str, Path(...)]) -> ResponseSchemaModel[GetUserInfoDetail]:
current_user = await user_service.get_userinfo(username=username)
data = GetUserInfoDetail(**select_as_dict(current_user))
async def get_user(
username: Annotated[str, Path(description='用户名')],
) -> ResponseSchemaModel[GetUserInfoWithRelationDetail]:
data = await user_service.get_userinfo(username=username)
return response_base.success(data=data)
@router.put('/{username}', summary='更新用户信息', dependencies=[DependsJwtAuth])
async def update_user(request: Request, username: Annotated[str, Path(...)], obj: UpdateUserParam) -> ResponseModel:
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()
@@ -78,14 +79,16 @@ async def update_user(request: Request, username: Annotated[str, Path(...)], obj
],
)
async def update_user_role(
request: Request, username: Annotated[str, Path(...)], obj: UpdateUserRoleParam
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(...)], avatar: AvatarParam) -> ResponseModel:
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()
@@ -94,7 +97,7 @@ async def update_avatar(request: Request, username: Annotated[str, Path(...)], a
@router.get(
'',
summary='(模糊条件)分页获取所有用户',
summary='分页获取所有用户',
dependencies=[
DependsJwtAuth,
DependsPagination,
@@ -102,18 +105,18 @@ async def update_avatar(request: Request, username: Annotated[str, Path(...)], a
)
async def get_pagination_users(
db: CurrentSession,
dept: Annotated[int | None, Query()] = None,
username: Annotated[str | None, Query()] = None,
phone: Annotated[str | None, Query()] = None,
status: Annotated[int | None, Query()] = None,
) -> ResponseSchemaModel[PageData[GetUserInfoDetail]]:
dept: Annotated[int | None, Query(description='部门 ID')] = None,
username: Annotated[str | None, Query(description='用户名')] = None,
phone: Annotated[str | None, Query(description='手机号')] = None,
status: Annotated[int | None, Query(description='状态')] = None,
) -> ResponseSchemaModel[PageData[GetUserInfoWithRelationDetail]]:
user_select = await user_service.get_select(dept=dept, username=username, phone=phone, status=status)
page_data = await paging_data(db, user_select)
return response_base.success(data=page_data)
@router.put('/{pk}/super', summary='修改用户超级权限', dependencies=[DependsRBAC])
async def super_set(request: Request, pk: Annotated[int, Path(...)]) -> ResponseModel:
async def super_set(request: Request, pk: Annotated[int, Path(description='用户 ID')]) -> ResponseModel:
count = await user_service.update_permission(request=request, pk=pk)
if count > 0:
return response_base.success()
@@ -121,7 +124,7 @@ async def super_set(request: Request, pk: Annotated[int, Path(...)]) -> Response
@router.put('/{pk}/staff', summary='修改用户后台登录权限', dependencies=[DependsRBAC])
async def staff_set(request: Request, pk: Annotated[int, Path(...)]) -> ResponseModel:
async def staff_set(request: Request, pk: Annotated[int, Path(description='用户 ID')]) -> ResponseModel:
count = await user_service.update_staff(request=request, pk=pk)
if count > 0:
return response_base.success()
@@ -129,15 +132,15 @@ async def staff_set(request: Request, pk: Annotated[int, Path(...)]) -> Response
@router.put('/{pk}/status', summary='修改用户状态', dependencies=[DependsRBAC])
async def status_set(request: Request, pk: Annotated[int, Path(...)]) -> ResponseModel:
async def status_set(request: Request, pk: Annotated[int, Path(description='用户 ID')]) -> ResponseModel:
count = await user_service.update_status(request=request, pk=pk)
if count > 0:
return response_base.success()
return response_base.fail()
@router.put('/{pk}/multi', summary='修改用户多登录状态', dependencies=[DependsRBAC])
async def multi_set(request: Request, pk: Annotated[int, Path(...)]) -> ResponseModel:
@router.put('/{pk}/multi', summary='修改用户多登录状态', dependencies=[DependsRBAC])
async def multi_set(request: Request, pk: Annotated[int, Path(description='用户 ID')]) -> ResponseModel:
count = await user_service.update_multi_login(request=request, pk=pk)
if count > 0:
return response_base.success()
@@ -153,7 +156,7 @@ async def multi_set(request: Request, pk: Annotated[int, Path(...)]) -> Response
DependsRBAC,
],
)
async def delete_user(username: Annotated[str, Path(...)]) -> ResponseModel:
async def delete_user(username: Annotated[str, Path(description='用户名')]) -> ResponseModel:
count = await user_service.delete(username=username)
if count > 0:
return response_base.success()
+12 -15
View File
@@ -4,39 +4,36 @@ from functools import lru_cache
from pydantic_settings import BaseSettings, SettingsConfigDict
from backend.core.path_conf import BasePath
from backend.core.path_conf import BASE_PATH
class AdminSettings(BaseSettings):
"""Admin Settings"""
"""Admin 配置"""
model_config = SettingsConfigDict(env_file=f'{BasePath}/.env', env_file_encoding='utf-8', extra='ignore')
model_config = SettingsConfigDict(env_file=f'{BASE_PATH}/.env', env_file_encoding='utf-8', extra='ignore')
# OAuth2https://github.com/fastapi-practices/fastapi_oauth20
# GitHub
# .env OAuth2
OAUTH2_GITHUB_CLIENT_ID: str
OAUTH2_GITHUB_CLIENT_SECRET: str
OAUTH2_GITHUB_REDIRECT_URI: str = 'http://127.0.0.1:8000/api/v1/oauth2/github/callback'
# Linux Do
OAUTH2_LINUX_DO_CLIENT_ID: str
OAUTH2_LINUX_DO_CLIENT_SECRET: str
OAUTH2_LINUX_DO_REDIRECT_URI: str = 'http://127.0.0.1:8000/api/v1/oauth2/linux-do/callback'
# Front-end redirect address
# OAuth2
OAUTH2_GITHUB_REDIRECT_URI: str = 'http://127.0.0.1:8000/api/v1/oauth2/github/callback'
OAUTH2_LINUX_DO_REDIRECT_URI: str = 'http://127.0.0.1:8000/api/v1/oauth2/linux-do/callback'
OAUTH2_FRONTEND_REDIRECT_URI: str = 'http://localhost:5173/oauth2/callback'
# Captcha
# 验证码
CAPTCHA_LOGIN_REDIS_PREFIX: str = 'fba:login:captcha'
CAPTCHA_LOGIN_EXPIRE_SECONDS: int = 60 * 5 # 过期时间,单位:秒
CAPTCHA_LOGIN_EXPIRE_SECONDS: int = 60 * 5 # 3 分钟
# Config
CONFIG_BUILT_IN_TYPES: list = ['website', 'protocol', 'policy']
# 参数配置
CONFIG_BUILT_IN_TYPES: list[str] = ['website', 'protocol', 'policy']
@lru_cache
def get_admin_settings() -> AdminSettings:
"""获取 admin 配置"""
"""获取 admin 参数配置"""
return AdminSettings()
+35 -37
View File
@@ -12,57 +12,55 @@ from backend.app.admin.schema.config import CreateConfigParam, UpdateConfigParam
class CRUDConfig(CRUDPlus[Config]):
"""系统参数参数配置数据库操作类"""
async def get(self, db: AsyncSession, pk: int) -> Config | None:
"""
获取系统参数配置
获取参数配置详情
:param db:
:param pk:
:param db: 数据库会话
:param pk: 参数配置 ID
:return:
"""
return await self.select_model_by_column(db, id=pk, type__not_in=admin_settings.CONFIG_BUILT_IN_TYPES)
async def get_by_type(self, db: AsyncSession, type: str) -> Sequence[Config]:
"""
通过 type 获取内置系统配置
通过类型获取参数配置
:param db:
:param type:
:param db: 数据库会话
:param type: 参数配置类型
:return:
"""
return await self.select_models(db, type=type)
async def get_by_key_and_type(self, db: AsyncSession, key: str, type: str) -> Config | None:
"""
通过 name 和 type 获取内置系统配置
通过键名和类型获取参数配置
:param db:
:param key:
:param type:
:param db: 数据库会话
:param key: 参数配置键名
:param type: 参数配置类型
:return:
"""
return await self.select_model_by_column(db, key=key, type=type)
async def get_by_key(self, db: AsyncSession, key: str, built_in: bool = False) -> Config | None:
async def get_by_key(self, db: AsyncSession, key: str) -> Config | None:
"""
通过 key 获取系统配置参数
通过键名获取参数配置
:param db:
:param key:
:param built_in:
:param db: 数据库会话
:param key: 参数配置键名
:return:
"""
filters = {'key': key}
if not built_in:
filters.update({'type__not_in': admin_settings.CONFIG_BUILT_IN_TYPES})
return await self.select_model_by_column(db, **filters)
return await self.select_model_by_column(db, key=key)
async def get_list(self, name: str = None, type: str = None) -> Select:
async def get_list(self, name: str | None = None, type: str | None = None) -> Select:
"""
获取系统参数配置列表
获取参数配置列表
:param name:
:param type:
:param name: 参数配置名称
:param type: 参数配置类型
:return:
"""
filters = {'type__not_in': admin_settings.CONFIG_BUILT_IN_TYPES}
@@ -72,33 +70,33 @@ class CRUDConfig(CRUDPlus[Config]):
filters.update(type__like=f'%{type}%')
return await self.select_order('created_time', 'desc', **filters)
async def create(self, db: AsyncSession, obj_in: CreateConfigParam) -> None:
async def create(self, db: AsyncSession, obj: CreateConfigParam) -> None:
"""
创建 Config
创建参数配置
:param db:
:param obj_in:
:param db: 数据库会话
:param obj: 创建参数配置参数
:return:
"""
await self.create_model(db, obj_in)
await self.create_model(db, obj)
async def update(self, db: AsyncSession, pk: int, obj_in: UpdateConfigParam) -> int:
async def update(self, db: AsyncSession, pk: int, obj: UpdateConfigParam) -> int:
"""
更新 Config
更新参数配置
:param db:
:param pk:
:param obj_in:
:param db: 数据库会话
:param pk: 参数配置 ID
:param obj: 更新参数配置参数
:return:
"""
return await self.update_model(db, pk, obj_in)
return await self.update_model(db, pk, obj)
async def delete(self, db: AsyncSession, pk: list[int]) -> int:
"""
删除 Config
删除参数配置
:param db:
:param pk:
:param db: 数据库会话
:param pk: 参数配置 ID 列表
:return:
"""
return await self.delete_model_by_column(
+36 -30
View File
@@ -2,7 +2,7 @@
# -*- coding: utf-8 -*-
from typing import Sequence
from sqlalchemy import Select, desc, select
from sqlalchemy import Select, and_, desc, select
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import noload
from sqlalchemy_crud_plus import CRUDPlus
@@ -12,76 +12,82 @@ from backend.app.admin.schema.data_rule import CreateDataRuleParam, UpdateDataRu
class CRUDDataRule(CRUDPlus[DataRule]):
"""数据权限规则数据库操作类"""
async def get(self, db: AsyncSession, pk: int) -> DataRule | None:
"""
获取数据权限规则
获取规则详情
:param db:
:param pk:
:param db: 数据库会话
:param pk: 规则 ID
:return:
"""
return await self.select_model(db, pk)
async def get_list(self, name: str = None) -> Select:
async def get_list(self, name: str | None = None) -> Select:
"""
获取数据权限规则列表
获取规则列表
:param name: 规则名称
:return:
"""
stmt = select(self.model).options(noload(self.model.roles)).order_by(desc(self.model.created_time))
where_list = []
filters = []
if name is not None:
where_list.append(self.model.name.like(f'%{name}%'))
if where_list:
stmt = stmt.where(*where_list)
filters.append(self.model.name.like(f'%{name}%'))
if filters:
stmt = stmt.where(and_(*filters))
return stmt
async def get_by_name(self, db: AsyncSession, name: str):
async def get_by_name(self, db: AsyncSession, name: str) -> DataRule | None:
"""
通过 name 获取数据权限规则
通过名称获取规则
:param db:
:param name:
:param db: 数据库会话
:param name: 规则名称
:return:
"""
return await self.select_model_by_column(db, name=name)
async def get_all(self, db: AsyncSession) -> Sequence[DataRule]:
"""
获取所有数据权限规则
获取所有规则
:param db:
:param db: 数据库会话
:return:
"""
return await self.select_models(db)
async def create(self, db: AsyncSession, obj_in: CreateDataRuleParam) -> None:
async def create(self, db: AsyncSession, obj: CreateDataRuleParam) -> None:
"""
创建数据权限规则
创建规则
:param db:
:param obj_in:
:param db: 数据库会话
:param obj: 创建规则参数
:return:
"""
await self.create_model(db, obj_in)
await self.create_model(db, obj)
async def update(self, db: AsyncSession, pk: int, obj_in: UpdateDataRuleParam) -> int:
async def update(self, db: AsyncSession, pk: int, obj: UpdateDataRuleParam) -> int:
"""
更新数据权限规则
更新规则
:param db:
:param pk:
:param obj_in:
:param db: 数据库会话
:param pk: 规则 ID
:param obj: 更新规则参数
:return:
"""
return await self.update_model(db, pk, obj_in)
return await self.update_model(db, pk, obj)
async def delete(self, db: AsyncSession, pk: list[int]) -> int:
"""
删除数据权限规则
删除规则
:param db:
:param pk:
:param db: 数据库会话
:param pk: 规则 ID 列表
:return:
"""
return await self.delete_model_by_column(db, allow_multiple=True, id__in=pk)
+42 -37
View File
@@ -7,42 +7,49 @@ from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload
from sqlalchemy_crud_plus import CRUDPlus
from backend.app.admin.model import Dept, User
from backend.app.admin.model import Dept
from backend.app.admin.schema.dept import CreateDeptParam, UpdateDeptParam
class CRUDDept(CRUDPlus[Dept]):
"""部门数据库操作类"""
async def get(self, db: AsyncSession, dept_id: int) -> Dept | None:
"""
获取部门
获取部门详情
:param db:
:param dept_id:
:param db: 数据库会话
:param dept_id: 部门 ID
:return:
"""
return await self.select_model_by_column(db, id=dept_id, del_flag=0)
async def get_by_name(self, db: AsyncSession, name: str) -> Dept | None:
"""
通过 name 获取 API
通过名称获取部门
:param db:
:param name:
:param db: 数据库会话
:param name: 部门名称
:return:
"""
return await self.select_model_by_column(db, name=name, del_flag=0)
async def get_all(
self, db: AsyncSession, name: str = None, leader: str = None, phone: str = None, status: int = None
self,
db: AsyncSession,
name: str | None = None,
leader: str | None = None,
phone: str | None = None,
status: int | None = None,
) -> Sequence[Dept]:
"""
获取所有部门
:param db:
:param name:
:param leader:
:param phone:
:param status:
:param db: 数据库会话
:param name: 部门名称
:param leader: 负责人
:param phone: 联系电话
:param status: 部门状态
:return:
"""
filters = {'del_flag__eq': 0}
@@ -56,62 +63,60 @@ class CRUDDept(CRUDPlus[Dept]):
filters.update(status=status)
return await self.select_models_order(db, sort_columns='sort', **filters)
async def create(self, db: AsyncSession, obj_in: CreateDeptParam) -> None:
async def create(self, db: AsyncSession, obj: CreateDeptParam) -> None:
"""
创建部门
:param db:
:param obj_in:
:param db: 数据库会话
:param obj: 创建部门参数
:return:
"""
await self.create_model(db, obj_in)
await self.create_model(db, obj)
async def update(self, db: AsyncSession, dept_id: int, obj_in: UpdateDeptParam) -> int:
async def update(self, db: AsyncSession, dept_id: int, obj: UpdateDeptParam) -> int:
"""
更新部门
:param db:
:param dept_id:
:param obj_in:
:param db: 数据库会话
:param dept_id: 部门 ID
:param obj: 更新部门参数
:return:
"""
return await self.update_model(db, dept_id, obj_in)
return await self.update_model(db, dept_id, obj)
async def delete(self, db: AsyncSession, dept_id: int) -> int:
"""
删除部门
:param db:
:param dept_id:
:param db: 数据库会话
:param dept_id: 部门 ID
:return:
"""
return await self.delete_model_by_column(db, id=dept_id, logical_deletion=True, deleted_flag_column='del_flag')
async def get_with_relation(self, db: AsyncSession, dept_id: int) -> list[User]:
async def get_with_relation(self, db: AsyncSession, dept_id: int) -> Dept | None:
"""
获取关联
获取部门及关联数据
:param db:
:param dept_id:
:param db: 数据库会话
: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)
user_relation = result.scalars().first()
return user_relation.users
return result.scalars().first()
async def get_children(self, db: AsyncSession, dept_id: int) -> list[Dept]:
async def get_children(self, db: AsyncSession, dept_id: int) -> Sequence[Dept | None]:
"""
获取子部门
获取子部门列表
:param db:
:param dept_id:
:param db: 数据库会话
:param dept_id: 部门 ID
:return:
"""
stmt = select(self.model).options(selectinload(self.model.children)).where(self.model.id == dept_id)
stmt = select(self.model).where(self.model.parent_id == dept_id, self.model.del_flag == 0)
result = await db.execute(stmt)
dept = result.scalars().first()
return dept.children
return result.scalars().all()
dept_dao: CRUDDept = CRUDDept(Dept)
+38 -33
View File
@@ -10,84 +10,89 @@ from backend.app.admin.schema.dict_data import CreateDictDataParam, UpdateDictDa
class CRUDDictData(CRUDPlus[DictData]):
"""字典数据数据库操作类"""
async def get(self, db: AsyncSession, pk: int) -> DictData | None:
"""
获取字典数据
获取字典数据详情
:param db:
:param pk:
:param db: 数据库会话
:param pk: 字典数据 ID
:return:
"""
return await self.select_model(db, pk)
async def get_list(self, label: str = None, value: str = None, status: int = None) -> Select:
async def get_list(self, label: str | None = None, value: str | None = None, status: int | None = None) -> Select:
"""
获取所有字典数据
获取字典数据列表
:param label:
:param value:
:param status:
:param label: 字典数据标签
:param value: 字典数据键值
:param status: 字典状态
:return:
"""
stmt = select(self.model).options(noload(self.model.type)).order_by(desc(self.model.sort))
where_list = []
filters = []
if label is not None:
where_list.append(self.model.label.like(f'%{label}%'))
filters.append(self.model.label.like(f'%{label}%'))
if value is not None:
where_list.append(self.model.value.like(f'%{value}%'))
filters.append(self.model.value.like(f'%{value}%'))
if status is not None:
where_list.append(self.model.status == status)
if where_list:
stmt = stmt.where(and_(*where_list))
filters.append(self.model.status == status)
if filters:
stmt = stmt.where(and_(*filters))
return stmt
async def get_by_label(self, db: AsyncSession, label: str) -> DictData | None:
"""
通过 label 获取字典数据
通过标签获取字典数据
:param db:
:param label:
:param db: 数据库会话
:param label: 字典标签
:return:
"""
return await self.select_model_by_column(db, label=label)
async def create(self, db: AsyncSession, obj_in: CreateDictDataParam) -> None:
async def create(self, db: AsyncSession, obj: CreateDictDataParam) -> None:
"""
创建数据字典
创建字典数据
:param db:
:param obj_in:
:param db: 数据库会话
:param obj: 创建字典数据参数
:return:
"""
await self.create_model(db, obj_in)
await self.create_model(db, obj)
async def update(self, db: AsyncSession, pk: int, obj_in: UpdateDictDataParam) -> int:
async def update(self, db: AsyncSession, pk: int, obj: UpdateDictDataParam) -> int:
"""
更新数据字典
更新字典数据
:param db:
:param pk:
:param obj_in:
:param db: 数据库会话
:param pk: 字典数据 ID
:param obj: 更新字典数据参数
:return:
"""
return await self.update_model(db, pk, obj_in)
return await self.update_model(db, pk, obj)
async def delete(self, db: AsyncSession, pk: list[int]) -> int:
"""
删除字典数据
:param db:
:param pk:
:param db: 数据库会话
:param pk: 字典数据 ID 列表
:return:
"""
return await self.delete_model_by_column(db, allow_multiple=True, id__in=pk)
async def get_with_relation(self, db: AsyncSession, pk: int) -> DictData | None:
"""
获取字典数据和类型
获取字典数据及关联数据
:param db:
:param pk:
:param db: 数据库会话
:param pk: 字典数据 ID
:return:
"""
stmt = select(self.model).options(selectinload(self.model.type)).where(self.model.id == pk)
+24 -22
View File
@@ -9,23 +9,25 @@ from backend.app.admin.schema.dict_type import CreateDictTypeParam, UpdateDictTy
class CRUDDictType(CRUDPlus[DictType]):
"""字典类型数据库操作类"""
async def get(self, db: AsyncSession, pk: int) -> DictType | None:
"""
获取字典类型
获取字典类型详情
:param db:
:param pk:
:param db: 数据库会话
:param pk: 字典类型 ID
:return:
"""
return await self.select_model(db, pk)
async def get_list(self, *, name: str = None, code: str = None, status: int = None) -> Select:
async def get_list(self, *, name: str | None = None, code: str | None = None, status: int | None = None) -> Select:
"""
获取所有字典类型
获取字典类型列表
:param name:
:param code:
:param status:
:param name: 字典类型名称
:param code: 字典类型编码
:param status: 字典状态
:return:
"""
filters = {}
@@ -39,41 +41,41 @@ class CRUDDictType(CRUDPlus[DictType]):
async def get_by_code(self, db: AsyncSession, code: str) -> DictType | None:
"""
通过 code 获取字典类型
通过编码获取字典类型
:param db:
:param code:
:param db: 数据库会话
:param code: 字典编码
:return:
"""
return await self.select_model_by_column(db, code=code)
async def create(self, db: AsyncSession, obj_in: CreateDictTypeParam) -> None:
async def create(self, db: AsyncSession, obj: CreateDictTypeParam) -> None:
"""
创建字典类型
:param db:
:param obj_in:
:param db: 数据库会话
:param obj: 创建字典类型参数
:return:
"""
await self.create_model(db, obj_in)
await self.create_model(db, obj)
async def update(self, db: AsyncSession, pk: int, obj_in: UpdateDictTypeParam) -> int:
async def update(self, db: AsyncSession, pk: int, obj: UpdateDictTypeParam) -> int:
"""
更新字典类型
:param db:
:param pk:
:param obj_in:
:param db: 数据库会话
:param pk: 字典类型 ID
:param obj: 更新字典类型参数
:return:
"""
return await self.update_model(db, pk, obj_in)
return await self.update_model(db, pk, obj)
async def delete(self, db: AsyncSession, pk: list[int]) -> int:
"""
删除字典类型
:param db:
:param pk:
:param db: 数据库会话
:param pk: 字典类型 ID 列表
:return:
"""
return await self.delete_model_by_column(db, allow_multiple=True, id__in=pk)
+13 -11
View File
@@ -9,13 +9,15 @@ from backend.app.admin.schema.login_log import CreateLoginLogParam
class CRUDLoginLog(CRUDPlus[LoginLog]):
"""登录日志数据库操作类"""
async def get_list(self, username: str | None = None, status: int | None = None, ip: str | None = None) -> Select:
"""
获取登录日志列表
:param username:
:param status:
:param ip:
:param username: 用户名
:param status: 登录状态
:param ip: IP 地址
:return:
"""
filters = {}
@@ -27,31 +29,31 @@ class CRUDLoginLog(CRUDPlus[LoginLog]):
filters.update(ip__like=f'%{ip}%')
return await self.select_order('created_time', 'desc', **filters)
async def create(self, db: AsyncSession, obj_in: CreateLoginLogParam) -> None:
async def create(self, db: AsyncSession, obj: CreateLoginLogParam) -> None:
"""
创建登录日志
:param db:
:param obj_in:
:param db: 数据库会话
:param obj: 创建登录日志参数
:return:
"""
await self.create_model(db, obj_in, commit=True)
await self.create_model(db, obj, commit=True)
async def delete(self, db: AsyncSession, pk: list[int]) -> int:
"""
删除登录日志
:param db:
:param pk:
:param db: 数据库会话
:param pk: 登录日志 ID 列表
:return:
"""
return await self.delete_model_by_column(db, allow_multiple=True, id__in=pk)
async def delete_all(self, db: AsyncSession) -> int:
"""
删除所有登录日志
删除所有日志
:param db:
:param db: 数据库会话
:return:
"""
return await self.delete_model_by_column(db, allow_multiple=True)
+41 -38
View File
@@ -3,6 +3,7 @@
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
@@ -11,33 +12,35 @@ from backend.app.admin.schema.menu import CreateMenuParam, UpdateMenuParam
class CRUDMenu(CRUDPlus[Menu]):
async def get(self, db, menu_id: int) -> Menu | None:
"""
获取菜单
"""菜单数据库操作类"""
:param db:
:param menu_id:
async def get(self, db: AsyncSession, menu_id: int) -> Menu | None:
"""
获取菜单详情
:param db: 数据库会话
:param menu_id: 菜单 ID
:return:
"""
return await self.select_model(db, menu_id)
async def get_by_title(self, db, title: str) -> Menu | None:
async def get_by_title(self, db: AsyncSession, title: str) -> Menu | None:
"""
通过 title 获取菜单
通过标题获取菜单
:param db:
:param title:
:param db: 数据库会话
:param title: 菜单标题
:return:
"""
return await self.select_model_by_column(db, title=title, menu_type__ne=2)
async def get_all(self, db, title: str | None = None, status: int | None = None) -> Sequence[Menu]:
async def get_all(self, db: AsyncSession, title: str | None = None, status: int | None = None) -> Sequence[Menu]:
"""
获取所有菜单
获取菜单列表
:param db:
:param title:
:param status:
:param db: 数据库会话
:param title: 菜单标题
:param status: 菜单状态
:return:
"""
filters = {}
@@ -47,60 +50,60 @@ class CRUDMenu(CRUDPlus[Menu]):
filters.update(status=status)
return await self.select_models_order(db, 'sort', **filters)
async def get_role_menus(self, db, superuser: bool, menu_ids: list[int]) -> Sequence[Menu]:
async def get_role_menus(self, db: AsyncSession, superuser: bool, menu_ids: list[int]) -> Sequence[Menu]:
"""
获取角色菜单
获取角色菜单列表
:param db:
:param superuser:
:param menu_ids:
:param db: 数据库会话
:param superuser: 是否超级管理员
:param menu_ids: 菜单 ID 列表
:return:
"""
stmt = select(self.model).order_by(asc(self.model.sort))
where_list = [self.model.menu_type.in_([0, 1])]
filters = [self.model.menu_type.in_([0, 1])]
if not superuser:
where_list.append(self.model.id.in_(menu_ids))
stmt = stmt.where(and_(*where_list))
filters.append(self.model.id.in_(menu_ids))
stmt = stmt.where(and_(*filters))
menu = await db.execute(stmt)
return menu.scalars().all()
async def create(self, db, obj_in: CreateMenuParam) -> None:
async def create(self, db: AsyncSession, obj: CreateMenuParam) -> None:
"""
创建菜单
:param db:
:param obj_in:
:param db: 数据库会话
:param obj: 创建菜单参数
:return:
"""
await self.create_model(db, obj_in)
await self.create_model(db, obj)
async def update(self, db, menu_id: int, obj_in: UpdateMenuParam) -> int:
async def update(self, db: AsyncSession, menu_id: int, obj: UpdateMenuParam) -> int:
"""
更新菜单
:param db:
:param menu_id:
:param obj_in:
:param db: 数据库会话
:param menu_id: 菜单 ID
:param obj: 更新菜单参数
:return:
"""
return await self.update_model(db, menu_id, obj_in)
return await self.update_model(db, menu_id, obj)
async def delete(self, db, menu_id: int) -> int:
async def delete(self, db: AsyncSession, menu_id: int) -> int:
"""
删除菜单
:param db:
:param menu_id:
:param db: 数据库会话
:param menu_id: 菜单 ID
:return:
"""
return await self.delete_model(db, menu_id)
async def get_children(self, db, menu_id: int) -> list[Menu]:
async def get_children(self, db: AsyncSession, menu_id: int) -> list[Menu | None]:
"""
获取子菜单
获取子菜单列表
:param db:
:param menu_id:
:param db: 数据库会话
:param menu_id: 菜单 ID
:return:
"""
stmt = select(self.model).options(selectinload(self.model.children)).where(self.model.id == menu_id)
+15 -13
View File
@@ -9,49 +9,51 @@ from backend.app.admin.schema.opera_log import CreateOperaLogParam
class CRUDOperaLogDao(CRUDPlus[OperaLog]):
"""操作日志数据库操作类"""
async def get_list(self, username: str | None = None, status: int | None = None, ip: str | None = None) -> Select:
"""
获取操作日志列表
:param username:
:param status:
:param ip:
:param username: 用户名
:param status: 操作状态
:param ip: IP 地址
:return:
"""
filters = {}
if username is not None:
filters.update(username=f'%{username}%')
filters.update(username__like=f'%{username}%')
if status is not None:
filters.update(status=status)
if ip is not None:
filters.update(ip=f'%{ip}%')
filters.update(ip__like=f'%{ip}%')
return await self.select_order('created_time', 'desc', **filters)
async def create(self, db: AsyncSession, obj_in: CreateOperaLogParam) -> None:
async def create(self, db: AsyncSession, obj: CreateOperaLogParam) -> None:
"""
创建操作日志
:param db:
:param obj_in:
:param db: 数据库会话
:param obj: 创建操作日志参数
:return:
"""
await self.create_model(db, obj_in)
await self.create_model(db, obj)
async def delete(self, db: AsyncSession, pk: list[int]) -> int:
"""
删除操作日志
:param db:
:param pk:
:param db: 数据库会话
:param pk: 操作日志 ID 列表
:return:
"""
return await self.delete_model_by_column(db, allow_multiple=True, id__in=pk)
async def delete_all(self, db: AsyncSession) -> int:
"""
删除所有操作日志
删除所有日志
:param db:
:param db: 数据库会话
:return:
"""
return await self.delete_model_by_column(db, allow_multiple=True)
+56 -52
View File
@@ -2,7 +2,8 @@
# -*- coding: utf-8 -*-
from typing import Sequence
from sqlalchemy import Select, desc, select
from sqlalchemy import Select, and_, desc, select
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import noload, selectinload
from sqlalchemy_crud_plus import CRUDPlus
@@ -16,22 +17,24 @@ from backend.app.admin.schema.role import (
class CRUDRole(CRUDPlus[Role]):
async def get(self, db, role_id: int) -> Role | None:
"""
获取角色
"""角色数据库操作类"""
:param db:
:param role_id:
async def get(self, db: AsyncSession, role_id: int) -> Role | None:
"""
获取角色详情
:param db: 数据库会话
:param role_id: 角色 ID
:return:
"""
return await self.select_model(db, role_id)
async def get_with_relation(self, db, role_id: int) -> Role | None:
async def get_with_relation(self, db: AsyncSession, role_id: int) -> Role | None:
"""
获取角色和菜单
获取角色及关联数据
:param db:
:param role_id:
:param db: 数据库会话
:param role_id: 角色 ID
:return:
"""
stmt = (
@@ -42,33 +45,33 @@ class CRUDRole(CRUDPlus[Role]):
role = await db.execute(stmt)
return role.scalars().first()
async def get_all(self, db) -> Sequence[Role]:
async def get_all(self, db: AsyncSession) -> Sequence[Role]:
"""
获取所有角色
:param db:
:param db: 数据库会话
:return:
"""
return await self.select_models(db)
async def get_by_user(self, db, user_id: int) -> Sequence[Role]:
async def get_by_user(self, db: AsyncSession, user_id: int) -> Sequence[Role]:
"""
获取用户所有角色
获取用户角色列表
:param db:
:param user_id:
:param db: 数据库会话
:param user_id: 用户 ID
:return:
"""
stmt = select(self.model).join(self.model.users).where(User.id == user_id)
roles = await db.execute(stmt)
return roles.scalars().all()
async def get_list(self, name: str = None, status: int = None) -> Select:
async def get_list(self, name: str | None = None, status: int | None = None) -> Select:
"""
获取角色列表
:param name:
:param status:
:param name: 角色名称
:param status: 角色状态
:return:
"""
stmt = (
@@ -76,84 +79,85 @@ class CRUDRole(CRUDPlus[Role]):
.options(noload(self.model.users), noload(self.model.menus), noload(self.model.rules))
.order_by(desc(self.model.created_time))
)
where_list = []
if name:
where_list.append(self.model.name.like(f'%{name}%'))
filters = []
if name is not None:
filters.append(self.model.name.like(f'%{name}%'))
if status is not None:
where_list.append(self.model.status == status)
if where_list:
stmt = stmt.where(*where_list)
filters.append(self.model.status == status)
if filters:
stmt = stmt.where(and_(*filters))
return stmt
async def get_by_name(self, db, name: str) -> Role | None:
async def get_by_name(self, db: AsyncSession, name: str) -> Role | None:
"""
通过 name 获取角色
通过名称获取角色
:param db:
:param name:
:param db: 数据库会话
:param name: 角色名称
:return:
"""
return await self.select_model_by_column(db, name=name)
async def create(self, db, obj_in: CreateRoleParam) -> None:
async def create(self, db: AsyncSession, obj: CreateRoleParam) -> None:
"""
创建角色
:param db:
:param obj_in:
:param db: 数据库会话
:param obj: 创建角色参数
:return:
"""
await self.create_model(db, obj_in)
await self.create_model(db, obj)
async def update(self, db, role_id: int, obj_in: UpdateRoleParam) -> int:
async def update(self, db: AsyncSession, role_id: int, obj: UpdateRoleParam) -> int:
"""
更新角色
:param db:
:param role_id:
:param obj_in:
:param db: 数据库会话
:param role_id: 角色 ID
:param obj: 更新角色参数
:return:
"""
return await self.update_model(db, role_id, obj_in)
return await self.update_model(db, role_id, obj)
async def update_menus(self, db, role_id: int, menu_ids: UpdateRoleMenuParam) -> int:
async def update_menus(self, db: AsyncSession, role_id: int, menu_ids: UpdateRoleMenuParam) -> int:
"""
更新角色菜单
:param db:
:param role_id:
:param menu_ids:
:param db: 数据库会话
:param role_id: 角色 ID
:param menu_ids: 菜单 ID 列表
:return:
"""
current_role = await self.get_with_relation(db, role_id)
# 更新菜单
stmt = select(Menu).where(Menu.id.in_(menu_ids.menus))
menus = await db.execute(stmt)
current_role.menus = menus.scalars().all()
return len(current_role.menus)
async def update_rules(self, db, role_id: int, rule_ids: UpdateRoleRuleParam) -> int:
async def update_rules(self, db: AsyncSession, role_id: int, rule_ids: UpdateRoleRuleParam) -> int:
"""
更新角色数据权限
更新角色数据规则
:param db:
:param role_id:
:param rule_ids:
:param db: 数据库会话
:param role_id: 角色 ID
:param rule_ids: 权限规则 ID 列表
:return:
"""
current_role = await self.get_with_relation(db, role_id)
# 更新数据权限
stmt = select(DataRule).where(DataRule.id.in_(rule_ids.rules))
rules = await db.execute(stmt)
current_role.rules = rules.scalars().all()
return len(current_role.rules)
async def delete(self, db, role_id: list[int]) -> int:
async def delete(self, db: AsyncSession, role_id: list[int]) -> int:
"""
删除角色
:param db:
:param role_id:
:param db: 数据库会话
:param role_id: 角色 ID 列表
:return:
"""
return await self.delete_model_by_column(db, allow_multiple=True, id__in=role_id)
+102 -90
View File
@@ -21,42 +21,44 @@ from backend.utils.timezone import timezone
class CRUDUser(CRUDPlus[User]):
"""用户数据库操作类"""
async def get(self, db: AsyncSession, user_id: int) -> User | None:
"""
获取用户
获取用户详情
:param db:
:param user_id:
:param db: 数据库会话
:param user_id: 用户 ID
:return:
"""
return await self.select_model(db, user_id)
async def get_by_username(self, db: AsyncSession, username: str) -> User | None:
"""
通过 username 获取用户
通过用户名获取用户
:param db:
:param username:
:param db: 数据库会话
:param username: 用户名
:return:
"""
return await self.select_model_by_column(db, username=username)
async def get_by_nickname(self, db: AsyncSession, nickname: str) -> User | None:
"""
通过 nickname 获取用户
通过昵称获取用户
:param db:
:param nickname:
:param db: 数据库会话
:param nickname: 用户昵称
:return:
"""
return await self.select_model_by_column(db, nickname=nickname)
async def update_login_time(self, db: AsyncSession, username: str) -> int:
"""
更新用户登录时间
更新用户最后登录时间
:param db:
:param username:
:param db: 数据库会话
:param username: 用户名
:return:
"""
return await self.update_model_by_column(db, {'last_login_time': timezone.now()}, username=username)
@@ -65,9 +67,9 @@ class CRUDUser(CRUDPlus[User]):
"""
创建用户
:param db:
:param obj:
:param social: 社交用户适配 oauth 2.0
:param db: 数据库会话
:param obj: 注册用户参数
:param social: 是否社交用户
:return:
"""
if not social:
@@ -83,10 +85,10 @@ class CRUDUser(CRUDPlus[User]):
async def add(self, db: AsyncSession, obj: AddUserParam) -> None:
"""
后台添加用户
添加用户
:param db:
:param obj:
:param db: 数据库会话
:param obj: 添加用户参数
:return:
"""
salt = bcrypt.gensalt()
@@ -94,19 +96,21 @@ class CRUDUser(CRUDPlus[User]):
dict_obj = obj.model_dump(exclude={'roles'})
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)
db.add(new_user)
async def update_userinfo(self, db: AsyncSession, input_user: int, obj: UpdateUserParam) -> int:
"""
更新用户信息
:param db:
:param input_user:
:param obj:
:param db: 数据库会话
:param input_user: 用户 ID
:param obj: 更新用户参数
:return:
"""
return await self.update_model(db, input_user, obj)
@@ -116,15 +120,14 @@ class CRUDUser(CRUDPlus[User]):
"""
更新用户角色
:param db:
:param input_user:
:param obj:
: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))
@@ -134,9 +137,9 @@ class CRUDUser(CRUDPlus[User]):
"""
更新用户头像
:param db:
:param input_user:
:param avatar:
:param db: 数据库会话
:param input_user: 用户 ID
:param avatar: 头像地址
:return:
"""
return await self.update_model(db, input_user, {'avatar': avatar.url})
@@ -145,18 +148,18 @@ class CRUDUser(CRUDPlus[User]):
"""
删除用户
:param db:
:param user_id:
:param db: 数据库会话
:param user_id: 用户 ID
:return:
"""
return await self.delete_model(db, user_id)
async def check_email(self, db: AsyncSession, email: str) -> User | None:
"""
检查邮箱是否存在
检查邮箱是否已被注册
:param db:
:param email:
:param db: 数据库会话
:param email: 电子邮箱
:return:
"""
return await self.select_model_by_column(db, email=email)
@@ -165,21 +168,23 @@ class CRUDUser(CRUDPlus[User]):
"""
重置用户密码
:param db:
:param pk:
:param new_pwd:
:param db: 数据库会话
:param pk: 用户 ID
:param new_pwd: 新密码已加密
:return:
"""
return await self.update_model(db, pk, {'password': new_pwd})
async def get_list(self, dept: int = None, username: str = None, phone: str = None, status: int = None) -> Select:
async def get_list(
self, dept: int | None = None, username: str | None = None, phone: str | None = None, status: int | None = None
) -> Select:
"""
获取用户列表
:param dept:
:param username:
:param phone:
:param status:
:param dept: 部门 ID
:param username: 用户名
:param phone: 电话号码
:param status: 用户状态
:return:
"""
stmt = (
@@ -191,25 +196,28 @@ class CRUDUser(CRUDPlus[User]):
)
.order_by(desc(self.model.join_time))
)
where_list = []
filters = []
if dept:
where_list.append(self.model.dept_id == dept)
filters.append(self.model.dept_id == dept)
if username:
where_list.append(self.model.username.like(f'%{username}%'))
filters.append(self.model.username.like(f'%{username}%'))
if phone:
where_list.append(self.model.phone.like(f'%{phone}%'))
filters.append(self.model.phone.like(f'%{phone}%'))
if status is not None:
where_list.append(self.model.status == status)
if where_list:
stmt = stmt.where(and_(*where_list))
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:
"""
获取用户超级管理员状态
获取用户是否为超级管理员
:param db:
:param user_id:
:param db: 数据库会话
:param user_id: 用户 ID
:return:
"""
user = await self.get(db, user_id)
@@ -217,10 +225,10 @@ class CRUDUser(CRUDPlus[User]):
async def get_staff(self, db: AsyncSession, user_id: int) -> bool:
"""
获取用户后台登录状态
获取用户是否可以登录后台
:param db:
:param user_id:
:param db: 数据库会话
:param user_id: 用户 ID
:return:
"""
user = await self.get(db, user_id)
@@ -230,8 +238,8 @@ class CRUDUser(CRUDPlus[User]):
"""
获取用户状态
:param db:
:param user_id:
:param db: 数据库会话
:param user_id: 用户 ID
:return:
"""
user = await self.get(db, user_id)
@@ -239,81 +247,85 @@ class CRUDUser(CRUDPlus[User]):
async def get_multi_login(self, db: AsyncSession, user_id: int) -> bool:
"""
获取用户多点登录状态
获取用户是否允许多端登录
:param db:
:param user_id:
:param db: 数据库会话
:param user_id: 用户 ID
:return:
"""
user = await self.get(db, user_id)
return user.is_multi_login
async def set_super(self, db: AsyncSession, user_id: int, _super: bool) -> int:
async def set_super(self, db: AsyncSession, user_id: int, is_super: bool) -> int:
"""
设置用户超级管理员
设置用户超级管理员状态
:param db:
:param user_id:
:param _super:
:param db: 数据库会话
:param user_id: 用户 ID
:param is_super: 是否超级管理员
:return:
"""
return await self.update_model(db, user_id, {'is_superuser': _super})
return await self.update_model(db, user_id, {'is_superuser': is_super})
async def set_staff(self, db: AsyncSession, user_id: int, staff: bool) -> int:
async def set_staff(self, db: AsyncSession, user_id: int, is_staff: bool) -> int:
"""
设置用户后台登录
设置用户后台登录状态
:param db:
:param user_id:
:param staff:
:param db: 数据库会话
:param user_id: 用户 ID
:param is_staff: 是否可登录后台
:return:
"""
return await self.update_model(db, user_id, {'is_staff': staff})
return await self.update_model(db, user_id, {'is_staff': is_staff})
async def set_status(self, db: AsyncSession, user_id: int, status: bool) -> int:
async def set_status(self, db: AsyncSession, user_id: int, status: int) -> int:
"""
设置用户状态
:param db:
:param user_id:
:param status:
:param db: 数据库会话
:param user_id: 用户 ID
:param status: 状态
:return:
"""
return await self.update_model(db, user_id, {'status': status})
async def set_multi_login(self, db: AsyncSession, user_id: int, multi_login: bool) -> int:
"""
设置用户多登录
设置用户多登录状态
:param db:
:param user_id:
:param multi_login:
:param db: 数据库会话
:param user_id: 用户 ID
:param multi_login: 是否允许多端登录
:return:
"""
return await self.update_model(db, user_id, {'is_multi_login': multi_login})
async def get_with_relation(self, db: AsyncSession, *, user_id: int = None, username: str = None) -> User | None:
async def get_with_relation(
self, db: AsyncSession, *, user_id: int | None = None, username: str | None = None
) -> User | None:
"""
获取用户部门角色菜单规则
获取用户关联信息
:param db:
:param user_id:
:param username:
:param db: 数据库会话
:param user_id: 用户 ID
:param username: 用户名
:return:
"""
stmt = select(self.model).options(
selectinload(self.model.dept),
selectinload(self.model.roles).options(
selectinload(Role.menus),
selectinload(Role.rules),
),
selectinload(self.model.roles).options(selectinload(Role.menus), selectinload(Role.rules)),
)
filters = []
if user_id:
filters.append(self.model.id == user_id)
if username:
filters.append(self.model.username == username)
user = await db.execute(stmt.where(*filters))
if filters:
stmt = stmt.where(and_(*filters))
user = await db.execute(stmt)
return user.scalars().first()
+14 -12
View File
@@ -8,37 +8,39 @@ from backend.app.admin.schema.user_social import CreateUserSocialParam
from backend.common.enums import UserSocialType
class CRUDOUserSocial(CRUDPlus[UserSocial]):
class CRUDUserSocial(CRUDPlus[UserSocial]):
"""用户社交账号数据库操作类"""
async def get(self, db: AsyncSession, pk: int, source: UserSocialType) -> UserSocial | None:
"""
获取用户社交账号绑定
获取用户社交账号绑定详情
:param db:
:param pk:
:param source:
:param db: 数据库会话
:param pk: 用户 ID
:param source: 社交账号类型
:return:
"""
return await self.select_model_by_column(db, user_id=pk, source=source)
async def create(self, db: AsyncSession, obj_in: CreateUserSocialParam) -> None:
async def create(self, db: AsyncSession, obj: CreateUserSocialParam) -> None:
"""
创建用户社交账号绑定
:param db:
:param obj_in:
:param db: 数据库会话
:param obj: 创建用户社交账号绑定参数
:return:
"""
await self.create_model(db, obj_in)
await self.create_model(db, obj)
async def delete(self, db: AsyncSession, social_id: int) -> int:
"""
删除用户社交账号绑定
:param db:
:param social_id:
:param db: 数据库会话
:param social_id: 社交账号绑定 ID
:return:
"""
return await self.delete_model(db, social_id)
user_social_dao: CRUDOUserSocial = CRUDOUserSocial(UserSocial)
user_social_dao: CRUDUserSocial = CRUDUserSocial(UserSocial)
+1 -1
View File
@@ -9,7 +9,7 @@ from backend.common.model import Base, id_key
class Config(Base):
"""系统配置表"""
"""参数配置表"""
__tablename__ = 'sys_config'
+8 -1
View File
@@ -1,11 +1,18 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from __future__ import annotations
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_rule
from backend.common.model import Base, id_key
if TYPE_CHECKING:
from backend.app.admin.model import Role
class DataRule(Base):
"""数据权限规则表"""
@@ -23,4 +30,4 @@ class DataRule(Base):
value: Mapped[str] = mapped_column(String(255), comment='规则值')
# 角色规则多对多
roles: Mapped[list['Role']] = relationship(init=False, secondary=sys_role_data_rule, back_populates='rules') # noqa: F821
roles: Mapped[list[Role]] = relationship(init=False, secondary=sys_role_data_rule, back_populates='rules')
+9 -4
View File
@@ -1,6 +1,8 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from typing import Union
from __future__ import annotations
from typing import TYPE_CHECKING, Optional
from sqlalchemy import Boolean, ForeignKey, String
from sqlalchemy.dialects.postgresql import INTEGER
@@ -8,6 +10,9 @@ from sqlalchemy.orm import Mapped, mapped_column, relationship
from backend.common.model import Base, id_key
if TYPE_CHECKING:
from backend.app.admin.model import User
class Dept(Base):
"""部门表"""
@@ -29,8 +34,8 @@ class Dept(Base):
parent_id: Mapped[int | None] = mapped_column(
ForeignKey('sys_dept.id', ondelete='SET NULL'), default=None, index=True, comment='父部门ID'
)
parent: Mapped[Union['Dept', None]] = relationship(init=False, back_populates='children', remote_side=[id])
children: Mapped[list['Dept'] | None] = relationship(init=False, back_populates='parent')
parent: Mapped[Optional['Dept']] = relationship(init=False, back_populates='children', remote_side=[id])
children: Mapped[Optional[list['Dept']]] = relationship(init=False, back_populates='parent')
# 部门用户一对多
users: Mapped[list['User']] = relationship(init=False, back_populates='dept') # noqa: F821
users: Mapped[list[User]] = relationship(init=False, back_populates='dept')
+9 -2
View File
@@ -1,5 +1,9 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from __future__ import annotations
from typing import TYPE_CHECKING
from sqlalchemy import ForeignKey, String
from sqlalchemy.dialects.mysql import LONGTEXT
from sqlalchemy.dialects.postgresql import TEXT
@@ -7,9 +11,12 @@ from sqlalchemy.orm import Mapped, mapped_column, relationship
from backend.common.model import Base, id_key
if TYPE_CHECKING:
from backend.app.admin.model import DictType
class DictData(Base):
"""字典数据"""
"""字典数据"""
__tablename__ = 'sys_dict_data'
@@ -26,4 +33,4 @@ class DictData(Base):
type_id: Mapped[int] = mapped_column(
ForeignKey('sys_dict_type.id', ondelete='CASCADE'), default=0, comment='字典类型关联ID'
)
type: Mapped['DictType'] = relationship(init=False, back_populates='datas') # noqa: F821
type: Mapped[DictType] = relationship(init=False, back_populates='datas')
+9 -2
View File
@@ -1,5 +1,9 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from __future__ import annotations
from typing import TYPE_CHECKING
from sqlalchemy import String
from sqlalchemy.dialects.mysql import LONGTEXT
from sqlalchemy.dialects.postgresql import TEXT
@@ -7,9 +11,12 @@ from sqlalchemy.orm import Mapped, mapped_column, relationship
from backend.common.model import Base, id_key
if TYPE_CHECKING:
from backend.app.admin.model import DictData
class DictType(Base):
"""字典类型"""
"""字典类型"""
__tablename__ = 'sys_dict_type'
@@ -22,4 +29,4 @@ class DictType(Base):
)
# 字典类型一对多
datas: Mapped[list['DictData']] = relationship(init=False, back_populates='type') # noqa: F821
datas: Mapped[list[DictData]] = relationship(init=False, back_populates='type')
+9 -4
View File
@@ -1,6 +1,8 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from typing import Union
from __future__ import annotations
from typing import TYPE_CHECKING, Optional
from sqlalchemy import ForeignKey, String
from sqlalchemy.dialects.mysql import LONGTEXT
@@ -10,6 +12,9 @@ from sqlalchemy.orm import Mapped, mapped_column, relationship
from backend.app.admin.model.m2m import sys_role_menu
from backend.common.model import Base, id_key
if TYPE_CHECKING:
from backend.app.admin.model import Role
class Menu(Base):
"""菜单表"""
@@ -36,8 +41,8 @@ class Menu(Base):
parent_id: Mapped[int | None] = mapped_column(
ForeignKey('sys_menu.id', ondelete='SET NULL'), default=None, index=True, comment='父菜单ID'
)
parent: Mapped[Union['Menu', None]] = relationship(init=False, back_populates='children', remote_side=[id])
children: Mapped[list['Menu'] | None] = relationship(init=False, back_populates='parent')
parent: Mapped[Optional['Menu']] = relationship(init=False, back_populates='children', remote_side=[id])
children: Mapped[Optional[list['Menu']]] = relationship(init=False, back_populates='parent')
# 菜单角色多对多
roles: Mapped[list['Role']] = relationship(init=False, secondary=sys_role_menu, back_populates='menus') # noqa: F821
roles: Mapped[list[Role]] = relationship(init=False, secondary=sys_role_menu, back_populates='menus')
+2 -1
View File
@@ -3,7 +3,8 @@
from datetime import datetime
from sqlalchemy import DateTime, String
from sqlalchemy.dialects.mysql import JSON, LONGTEXT, TEXT
from sqlalchemy.dialects.mysql import JSON, LONGTEXT
from sqlalchemy.dialects.postgresql import TEXT
from sqlalchemy.orm import Mapped, mapped_column
from backend.common.model import DataClassBase, id_key
+10 -3
View File
@@ -1,5 +1,9 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from __future__ import annotations
from typing import TYPE_CHECKING
from sqlalchemy import String
from sqlalchemy.dialects.mysql import LONGTEXT
from sqlalchemy.dialects.postgresql import TEXT
@@ -8,6 +12,9 @@ from sqlalchemy.orm import Mapped, mapped_column, relationship
from backend.app.admin.model.m2m import sys_role_data_rule, sys_role_menu, sys_user_role
from backend.common.model import Base, id_key
if TYPE_CHECKING:
from backend.app.admin.model import DataRule, Menu, User
class Role(Base):
"""角色表"""
@@ -22,10 +29,10 @@ class Role(Base):
)
# 角色用户多对多
users: Mapped[list['User']] = relationship(init=False, secondary=sys_user_role, back_populates='roles') # noqa: F821
users: Mapped[list[User]] = relationship(init=False, secondary=sys_user_role, back_populates='roles')
# 角色菜单多对多
menus: Mapped[list['Menu']] = relationship(init=False, secondary=sys_role_menu, back_populates='roles') # noqa: F821
menus: Mapped[list[Menu]] = relationship(init=False, secondary=sys_role_menu, back_populates='roles')
# 角色数据权限规则多对多
rules: Mapped[list['DataRule']] = relationship(init=False, secondary=sys_role_data_rule, back_populates='roles') # noqa: F821
rules: Mapped[list[DataRule]] = relationship(init=False, secondary=sys_role_data_rule, back_populates='roles')
+10 -5
View File
@@ -1,7 +1,9 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from __future__ import annotations
from datetime import datetime
from typing import Union
from typing import TYPE_CHECKING
from sqlalchemy import VARBINARY, Boolean, DateTime, ForeignKey, String
from sqlalchemy.dialects.postgresql import BYTEA, INTEGER
@@ -12,6 +14,9 @@ from backend.common.model import Base, id_key
from backend.database.db import uuid4_str
from backend.utils.timezone import timezone
if TYPE_CHECKING:
from backend.app.admin.model import Dept, Role, UserSocial
class User(Base):
"""用户表"""
@@ -31,7 +36,7 @@ class User(Base):
is_staff: Mapped[bool] = mapped_column(
Boolean().with_variant(INTEGER, 'postgresql'), default=False, comment='后台管理登陆(0否 1是)'
)
status: Mapped[int] = mapped_column(default=1, 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是)'
)
@@ -48,10 +53,10 @@ class User(Base):
dept_id: Mapped[int | None] = mapped_column(
ForeignKey('sys_dept.id', ondelete='SET NULL'), default=None, comment='部门关联ID'
)
dept: Mapped[Union['Dept', None]] = relationship(init=False, back_populates='users') # noqa: F821
dept: Mapped[Dept | None] = relationship(init=False, back_populates='users')
# 用户社交信息一对多
socials: Mapped[list['UserSocial']] = relationship(init=False, back_populates='user') # noqa: F821
socials: Mapped[list[UserSocial]] = relationship(init=False, back_populates='user')
# 用户角色多对多
roles: Mapped[list['Role']] = relationship(init=False, secondary=sys_user_role, back_populates='users') # noqa: F821
roles: Mapped[list[Role]] = relationship(init=False, secondary=sys_user_role, back_populates='users')
+7 -2
View File
@@ -1,12 +1,17 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from typing import Union
from __future__ import annotations
from typing import TYPE_CHECKING
from sqlalchemy import ForeignKey, String
from sqlalchemy.orm import Mapped, mapped_column, relationship
from backend.common.model import Base, id_key
if TYPE_CHECKING:
from backend.app.admin.model import User
class UserSocial(Base):
"""用户社交表(OAuth2"""
@@ -25,4 +30,4 @@ class UserSocial(Base):
user_id: Mapped[int | None] = mapped_column(
ForeignKey('sys_user.id', ondelete='SET NULL'), default=None, comment='用户关联ID'
)
user: Mapped[Union['User', None]] = relationship(init=False, back_populates='socials') # noqa: F821
user: Mapped[User | None] = relationship(init=False, back_populates='socials')
+2
View File
@@ -6,5 +6,7 @@ from backend.common.schema import SchemaBase
class GetCaptchaDetail(SchemaBase):
"""验证码详情"""
image_type: str = Field(description='图片类型')
image: str = Field(description='图片内容')
+21 -15
View File
@@ -2,37 +2,43 @@
# -*- coding: utf-8 -*-
from datetime import datetime
from pydantic import ConfigDict
from pydantic import ConfigDict, Field
from backend.common.schema import SchemaBase
class SaveBuiltInConfigParam(SchemaBase):
name: str
key: str
value: str
"""保存内置参数配置参数"""
name: str = Field(description='参数配置名称')
key: str = Field(description='参数配置键名')
value: str = Field(description='参数配置值')
class ConfigSchemaBase(SchemaBase):
name: str
type: str | None
key: str
value: str
is_frontend: bool
remark: str | None
"""参数配置基础模型"""
name: str = Field(description='参数配置名称')
type: str | None = Field(None, description='参数配置类型')
key: str = Field(description='参数配置键名')
value: str = Field(description='参数配置值')
is_frontend: bool = Field(description='是否前端参数配置')
remark: str | None = Field(None, description='备注')
class CreateConfigParam(ConfigSchemaBase):
pass
"""创建参数配置参数"""
class UpdateConfigParam(ConfigSchemaBase):
pass
"""更新参数配置参数"""
class GetConfigDetail(ConfigSchemaBase):
"""参数配置详情"""
model_config = ConfigDict(from_attributes=True)
id: int
created_time: datetime
updated_time: datetime | None = None
id: int = Field(description='参数配置 ID')
created_time: datetime = Field(description='创建时间')
updated_time: datetime | None = Field(None, description='更新时间')
+17 -12
View File
@@ -9,28 +9,33 @@ from backend.common.schema import SchemaBase
class DataRuleSchemaBase(SchemaBase):
name: str
model: str
column: str
operator: RoleDataRuleOperatorType = Field(RoleDataRuleOperatorType.OR)
expression: RoleDataRuleExpressionType = Field(RoleDataRuleExpressionType.eq)
value: str
"""数据规则基础模型"""
name: str = Field(description='规则名称')
model: str = Field(description='模型名称')
column: str = Field(description='字段名称')
operator: RoleDataRuleOperatorType = Field(RoleDataRuleOperatorType.OR, description='操作符(AND/OR')
expression: RoleDataRuleExpressionType = Field(RoleDataRuleExpressionType.eq, description='表达式类型')
value: str = Field(description='规则值')
class CreateDataRuleParam(DataRuleSchemaBase):
pass
"""创建数据规则参数"""
class UpdateDataRuleParam(DataRuleSchemaBase):
pass
"""更新数据规则参数"""
class GetDataRuleDetail(DataRuleSchemaBase):
"""数据规则详情"""
model_config = ConfigDict(from_attributes=True)
id: int
created_time: datetime
updated_time: datetime | None = None
id: int = Field(description='规则 ID')
created_time: datetime = Field(description='创建时间')
updated_time: datetime | None = Field(None, description='更新时间')
def __hash__(self):
def __hash__(self) -> int:
"""计算哈希值"""
return hash(self.name)
+17 -13
View File
@@ -9,27 +9,31 @@ from backend.common.schema import CustomEmailStr, CustomPhoneNumber, SchemaBase
class DeptSchemaBase(SchemaBase):
name: str
parent_id: int | None = Field(default=None, description='部门父级ID')
sort: int = Field(default=0, ge=0, description='排序')
leader: str | None = None
phone: CustomPhoneNumber | None = None
email: CustomEmailStr | None = None
status: StatusType = Field(default=StatusType.enable)
"""部门基础模型"""
name: str = Field(description='部门名称')
parent_id: int | None = Field(None, description='部门父级 ID')
sort: int = Field(0, ge=0, description='排序')
leader: str | None = Field(None, description='负责人')
phone: CustomPhoneNumber | None = Field(None, description='联系电话')
email: CustomEmailStr | None = Field(None, description='邮箱')
status: StatusType = Field(StatusType.enable, description='状态')
class CreateDeptParam(DeptSchemaBase):
pass
"""创建部门参数"""
class UpdateDeptParam(DeptSchemaBase):
pass
"""更新部门参数"""
class GetDeptDetail(DeptSchemaBase):
"""部门详情"""
model_config = ConfigDict(from_attributes=True)
id: int
del_flag: bool
created_time: datetime
updated_time: datetime | None = None
id: int = Field(description='部门 ID')
del_flag: bool = Field(description='是否删除')
created_time: datetime = Field(description='创建时间')
updated_time: datetime | None = Field(None, description='更新时间')
+21 -12
View File
@@ -10,26 +10,35 @@ from backend.common.schema import SchemaBase
class DictDataSchemaBase(SchemaBase):
type_id: int
label: str
value: str
sort: int
status: StatusType = Field(default=StatusType.enable)
remark: str | None = None
"""字典数据基础模型"""
type_id: int = Field(description='字典类型 ID')
label: str = Field(description='字典标签')
value: str = Field(description='字典值')
sort: int = Field(description='排序')
status: StatusType = Field(StatusType.enable, description='状态')
remark: str | None = Field(None, description='备注')
class CreateDictDataParam(DictDataSchemaBase):
pass
"""创建字典数据参数"""
class UpdateDictDataParam(DictDataSchemaBase):
pass
"""更新字典数据参数"""
class GetDictDataDetail(DictDataSchemaBase):
"""字典数据详情"""
model_config = ConfigDict(from_attributes=True)
id: int
type: GetDictTypeDetail | None = None
created_time: datetime
updated_time: datetime | None = None
id: int = Field(description='字典数据 ID')
created_time: datetime = Field(description='创建时间')
updated_time: datetime | None = Field(None, description='更新时间')
class GetDictDataWithRelation(DictDataSchemaBase):
"""字典数据关联详情"""
type: GetDictTypeDetail | None = Field(None, description='字典类型信息')
+13 -9
View File
@@ -9,23 +9,27 @@ from backend.common.schema import SchemaBase
class DictTypeSchemaBase(SchemaBase):
name: str
code: str
status: StatusType = Field(default=StatusType.enable)
remark: str | None = None
"""字典类型基础模型"""
name: str = Field(description='字典名称')
code: str = Field(description='字典编码')
status: StatusType = Field(StatusType.enable, description='状态')
remark: str | None = Field(None, description='备注')
class CreateDictTypeParam(DictTypeSchemaBase):
pass
"""创建字典类型参数"""
class UpdateDictTypeParam(DictTypeSchemaBase):
pass
"""更新字典类型参数"""
class GetDictTypeDetail(DictTypeSchemaBase):
"""字典类型详情"""
model_config = ConfigDict(from_attributes=True)
id: int
created_time: datetime
updated_time: datetime | None = None
id: int = Field(description='字典类型 ID')
created_time: datetime = Field(description='创建时间')
updated_time: datetime | None = Field(None, description='更新时间')
+22 -18
View File
@@ -2,37 +2,41 @@
# -*- coding: utf-8 -*-
from datetime import datetime
from pydantic import ConfigDict
from pydantic import ConfigDict, Field
from backend.common.schema import SchemaBase
class LoginLogSchemaBase(SchemaBase):
user_uuid: str
username: str
status: int
ip: str
country: str | None
region: str | None
city: str | None
user_agent: str
browser: str | None
os: str | None
device: str | None
msg: str
login_time: datetime
"""登录日志基础模型"""
user_uuid: str = Field(description='用户 UUID')
username: str = Field(description='用户名')
status: int = Field(description='登录状态')
ip: str = Field(description='IP 地址')
country: str | None = Field(None, description='国家')
region: str | None = Field(None, description='地区')
city: str | None = Field(None, description='城市')
user_agent: str = Field(description='用户代理')
browser: str | None = Field(None, description='浏览器')
os: str | None = Field(None, description='操作系统')
device: str | None = Field(None, description='设备')
msg: str = Field(description='消息')
login_time: datetime = Field(description='登录时间')
class CreateLoginLogParam(LoginLogSchemaBase):
pass
"""创建登录日志参数"""
class UpdateLoginLogParam(LoginLogSchemaBase):
pass
"""更新登录日志参数"""
class GetLoginLogDetail(LoginLogSchemaBase):
"""登录日志详情"""
model_config = ConfigDict(from_attributes=True)
id: int
created_time: datetime
id: int = Field(description='日志 ID')
created_time: datetime = Field(description='创建时间')
+22 -18
View File
@@ -9,32 +9,36 @@ from backend.common.schema import SchemaBase
class MenuSchemaBase(SchemaBase):
title: str
name: str
parent_id: int | None = Field(default=None, description='菜单父级ID')
sort: int = Field(default=0, ge=0, description='排序')
icon: str | None = None
path: str | None = None
menu_type: MenuType = Field(default=MenuType.directory, description='菜单类型(0目录 1菜单 2按钮)')
component: str | None = None
perms: str | None = None
status: StatusType = Field(default=StatusType.enable)
display: StatusType = Field(default=StatusType.enable)
cache: StatusType = Field(default=StatusType.enable)
remark: str | None = None
"""菜单基础模型"""
title: str = Field(description='菜单标题')
name: str = Field(description='菜单名称')
parent_id: int | None = Field(None, description='菜单父级 ID')
sort: int = Field(0, ge=0, description='排序')
icon: str | None = Field(None, description='图标')
path: str | None = Field(None, description='路由路径')
menu_type: MenuType = Field(MenuType.directory, description='菜单类型(0目录 1菜单 2按钮)')
component: str | None = Field(None, description='组件路径')
perms: str | None = Field(None, description='权限标识')
status: StatusType = Field(StatusType.enable, description='状态')
display: StatusType = Field(StatusType.enable, description='是否显示')
cache: StatusType = Field(StatusType.enable, description='是否缓存')
remark: str | None = Field(None, description='备注')
class CreateMenuParam(MenuSchemaBase):
pass
"""创建菜单参数"""
class UpdateMenuParam(MenuSchemaBase):
pass
"""更新菜单参数"""
class GetMenuDetail(MenuSchemaBase):
"""菜单详情"""
model_config = ConfigDict(from_attributes=True)
id: int
created_time: datetime
updated_time: datetime | None = None
id: int = Field(description='菜单 ID')
created_time: datetime = Field(description='创建时间')
updated_time: datetime | None = Field(None, description='更新时间')
+28 -23
View File
@@ -1,6 +1,7 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from datetime import datetime
from typing import Any
from pydantic import ConfigDict, Field
@@ -9,37 +10,41 @@ from backend.common.schema import SchemaBase
class OperaLogSchemaBase(SchemaBase):
trace_id: str
username: str | None = None
method: str
title: str
path: str
ip: str
country: str | None = None
region: str | None = None
city: str | None = None
user_agent: str
os: str | None = None
browser: str | None = None
device: str | None = None
args: dict | None = None
status: StatusType = Field(default=StatusType.enable)
code: str
msg: str | None = None
cost_time: float
opera_time: datetime
"""操作日志基础模型"""
trace_id: str = Field(description='追踪 ID')
username: str | None = Field(None, description='用户名')
method: str = Field(description='请求方法')
title: str = Field(description='操作标题')
path: str = Field(description='请求路径')
ip: str = Field(description='IP 地址')
country: str | None = Field(None, description='国家')
region: str | None = Field(None, description='地区')
city: str | None = Field(None, description='城市')
user_agent: str = Field(description='用户代理')
os: str | None = Field(None, description='操作系统')
browser: str | None = Field(None, description='浏览器')
device: str | None = Field(None, description='设备')
args: dict[str, Any] | None = Field(None, description='请求参数')
status: StatusType = Field(StatusType.enable, description='状态')
code: str = Field(description='状态码')
msg: str | None = Field(None, description='消息')
cost_time: float = Field(description='耗时')
opera_time: datetime = Field(description='操作时间')
class CreateOperaLogParam(OperaLogSchemaBase):
pass
"""创建操作日志参数"""
class UpdateOperaLogParam(OperaLogSchemaBase):
pass
"""更新操作日志参数"""
class GetOperaLogDetail(OperaLogSchemaBase):
"""操作日志详情"""
model_config = ConfigDict(from_attributes=True)
id: int
created_time: datetime
id: int = Field(description='日志 ID')
created_time: datetime = Field(description='创建时间')
+25 -12
View File
@@ -11,32 +11,45 @@ from backend.common.schema import SchemaBase
class RoleSchemaBase(SchemaBase):
name: str
status: StatusType = Field(default=StatusType.enable)
remark: str | None = None
"""角色基础模型"""
name: str = Field(description='角色名称')
status: StatusType = Field(StatusType.enable, description='状态')
remark: str | None = Field(None, description='备注')
class CreateRoleParam(RoleSchemaBase):
pass
"""创建角色参数"""
class UpdateRoleParam(RoleSchemaBase):
pass
"""更新角色参数"""
class UpdateRoleMenuParam(SchemaBase):
menus: list[int]
"""更新角色菜单参数"""
menus: list[int] = Field(description='菜单 ID 列表')
class UpdateRoleRuleParam(SchemaBase):
rules: list[int]
"""更新角色规则参数"""
rules: list[int] = Field(description='数据规则 ID 列表')
class GetRoleDetail(RoleSchemaBase):
"""角色详情"""
model_config = ConfigDict(from_attributes=True)
id: int
created_time: datetime
updated_time: datetime | None = None
menus: list[GetMenuDetail | None] = []
rules: list[GetDataRuleDetail | None] = []
id: int = Field(description='角色 ID')
created_time: datetime = Field(description='创建时间')
updated_time: datetime | None = Field(None, description='更新时间')
class GetRoleWithRelationDetail(GetRoleDetail):
"""角色关联详情"""
menus: list[GetMenuDetail | None] = Field([], description='菜单详情列表')
rules: list[GetDataRuleDetail | None] = Field([], description='数据规则详情列表')
+33 -21
View File
@@ -2,44 +2,56 @@
# -*- coding: utf-8 -*-
from datetime import datetime
from backend.app.admin.schema.user import GetUserInfoNoRelationDetail
from pydantic import Field
from backend.app.admin.schema.user import GetUserInfoDetail
from backend.common.enums import StatusType
from backend.common.schema import SchemaBase
class GetSwaggerToken(SchemaBase):
access_token: str
token_type: str = 'Bearer'
user: GetUserInfoNoRelationDetail
"""Swagger 认证令牌"""
access_token: str = Field(description='访问令牌')
token_type: str = Field('Bearer', description='令牌类型')
user: GetUserInfoDetail = Field(description='用户信息')
class AccessTokenBase(SchemaBase):
access_token: str
access_token_expire_time: datetime
session_uuid: str
"""访问令牌基础模型"""
access_token: str = Field(description='访问令牌')
access_token_expire_time: datetime = Field(description='令牌过期时间')
session_uuid: str = Field(description='会话 UUID')
class GetNewToken(AccessTokenBase):
pass
"""获取新令牌"""
class GetLoginToken(AccessTokenBase):
user: GetUserInfoNoRelationDetail
"""获取登录令牌"""
user: GetUserInfoDetail = Field(description='用户信息')
class KickOutToken(SchemaBase):
session_uuid: str
"""踢出令牌"""
session_uuid: str = Field(description='会话 UUID')
class GetTokenDetail(SchemaBase):
id: int
session_uuid: str
username: str
nickname: str
ip: str
os: str
browser: str
device: str
status: StatusType
last_login_time: str
expire_time: datetime
"""令牌详情"""
id: int = Field(description='用户 ID')
session_uuid: str = Field(description='会话 UUID')
username: str = Field(description='用户名')
nickname: str = Field(description='昵称')
ip: str = Field(description='IP 地址')
os: str = Field(description='操作系统')
browser: str = Field(description='浏览器')
device: str = Field(description='设备')
status: StatusType = Field(description='状态')
last_login_time: str = Field(description='最后登录时间')
expire_time: datetime = Field(description='过期时间')
+63 -45
View File
@@ -7,84 +7,112 @@ from pydantic import ConfigDict, EmailStr, Field, HttpUrl, model_validator
from typing_extensions import Self
from backend.app.admin.schema.dept import GetDeptDetail
from backend.app.admin.schema.role import GetRoleDetail
from backend.app.admin.schema.role import GetRoleWithRelationDetail
from backend.common.enums import StatusType
from backend.common.schema import CustomPhoneNumber, SchemaBase
class AuthSchemaBase(SchemaBase):
username: str
password: str | None
"""用户认证基础模型"""
username: str = Field(description='用户名')
password: str | None = Field(description='密码')
class AuthLoginParam(AuthSchemaBase):
captcha: str
"""用户登录参数"""
captcha: str = Field(description='验证码')
class RegisterUserParam(AuthSchemaBase):
nickname: str | None = None
email: EmailStr = Field(examples=['user@example.com'])
"""用户注册参数"""
nickname: str | None = Field(None, description='昵称')
email: EmailStr = Field(examples=['user@example.com'], description='邮箱')
class AddUserParam(AuthSchemaBase):
dept_id: int
roles: list[int]
nickname: str | None = None
email: EmailStr = Field(examples=['user@example.com'])
"""添加用户参数"""
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 ResetPasswordParam(SchemaBase):
"""重置密码参数"""
old_password: str = Field(description='旧密码')
new_password: str = Field(description='新密码')
confirm_password: str = Field(description='确认密码')
class UserInfoSchemaBase(SchemaBase):
dept_id: int | None = None
username: str
nickname: str
email: EmailStr = Field(examples=['user@example.com'])
phone: CustomPhoneNumber | None = None
"""用户信息基础模型"""
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='手机号')
class UpdateUserParam(UserInfoSchemaBase):
pass
"""更新用户参数"""
class UpdateUserRoleParam(SchemaBase):
roles: list[int]
"""更新用户角色参数"""
roles: list[int] = Field(description='角色 ID 列表')
class AvatarParam(SchemaBase):
"""更新头像参数"""
url: HttpUrl = Field(description='头像 http 地址')
class GetUserInfoNoRelationDetail(UserInfoSchemaBase):
class GetUserInfoDetail(UserInfoSchemaBase):
"""用户信息详情"""
model_config = ConfigDict(from_attributes=True)
dept_id: int | None = None
id: int
uuid: str
avatar: str | None = None
status: StatusType = Field(default=StatusType.enable)
is_superuser: bool
is_staff: bool
is_multi_login: bool
join_time: datetime = None
last_login_time: datetime | None = None
dept_id: int | None = Field(None, description='部门 ID')
id: int = Field(description='用户 ID')
uuid: str = Field(description='用户 UUID')
avatar: str | None = Field(None, description='头像')
status: StatusType = Field(StatusType.enable, description='状态')
is_superuser: bool = Field(description='是否超级管理员')
is_staff: bool = Field(description='是否管理员')
is_multi_login: bool = Field(description='是否允许多端登录')
join_time: datetime = Field(description='加入时间')
last_login_time: datetime | None = Field(None, description='最后登录时间')
class GetUserInfoDetail(GetUserInfoNoRelationDetail):
class GetUserInfoWithRelationDetail(GetUserInfoDetail):
"""用户信息关联详情"""
model_config = ConfigDict(from_attributes=True)
dept: GetDeptDetail | None = None
roles: list[GetRoleDetail]
dept: GetDeptDetail | None = Field(None, description='部门信息')
roles: list[GetRoleWithRelationDetail] = Field(description='角色列表')
class GetCurrentUserInfoDetail(GetUserInfoDetail):
class GetCurrentUserInfoWithRelationDetail(GetUserInfoWithRelationDetail):
"""当前用户信息关联详情"""
model_config = ConfigDict(from_attributes=True)
dept: str | None = None
roles: list[str]
dept: str | None = Field(None, description='部门名称')
roles: list[str] = Field(description='角色名称列表')
@model_validator(mode='before')
@classmethod
def handel(cls, data: Any) -> Self:
"""处理部门和角色"""
"""处理部门和角色数据"""
dept = data['dept']
if dept:
data['dept'] = dept['name']
@@ -92,13 +120,3 @@ class GetCurrentUserInfoDetail(GetUserInfoDetail):
if roles:
data['roles'] = [role['name'] for role in roles]
return data
class CurrentUserIns(GetUserInfoDetail):
model_config = ConfigDict(from_attributes=True)
class ResetPasswordParam(SchemaBase):
old_password: str
new_password: str
confirm_password: str
+14 -8
View File
@@ -1,21 +1,27 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from pydantic import Field
from backend.common.enums import UserSocialType
from backend.common.schema import SchemaBase
class UserSocialSchemaBase(SchemaBase):
source: UserSocialType
open_id: str | None = None
uid: str | None = None
union_id: str | None = None
scope: str | None = None
code: str | None = None
"""用户社交基础模型"""
source: UserSocialType = Field(description='社交平台')
open_id: str | None = Field(None, description='开放平台 ID')
uid: str | None = Field(None, description='用户 ID')
union_id: str | None = Field(None, description='开放平台唯一 ID')
scope: str | None = Field(None, description='授权范围')
code: str | None = Field(None, description='授权码')
class CreateUserSocialParam(UserSocialSchemaBase):
user_id: int
"""创建用户社交参数"""
user_id: int = Field(description='用户 ID')
class UpdateUserSocialParam(SchemaBase):
pass
"""更新用户社交参数"""
+38
View File
@@ -30,8 +30,18 @@ from backend.utils.timezone import timezone
class AuthService:
"""认证服务类"""
@staticmethod
async def user_verify(db: AsyncSession, username: str, password: str) -> User:
"""
验证用户名和密码
:param db: 数据库会话
:param username: 用户名
:param password: 密码
:return:
"""
user = await user_dao.get_by_username(db, username)
if not user:
raise errors.NotFoundError(msg='用户名或密码有误')
@@ -42,6 +52,12 @@ class AuthService:
return user
async def swagger_login(self, *, obj: HTTPBasicCredentials) -> tuple[str, User]:
"""
Swagger 文档登录
:param obj: 登录凭证
:return:
"""
async with async_db_session.begin() as db:
user = await self.user_verify(db, obj.username, obj.password)
await user_dao.update_login_time(db, obj.username)
@@ -56,6 +72,15 @@ class AuthService:
async def login(
self, *, request: Request, response: Response, obj: AuthLoginParam, background_tasks: BackgroundTasks
) -> GetLoginToken:
"""
用户登录
:param request: 请求对象
:param response: 响应对象
:param obj: 登录参数
:param background_tasks: 后台任务
:return:
"""
async with async_db_session.begin() as db:
user = None
try:
@@ -133,6 +158,12 @@ class AuthService:
@staticmethod
async def new_token(*, request: Request) -> GetNewToken:
"""
获取新的访问令牌
:param request: FastAPI 请求对象
:return:
"""
refresh_token = request.cookies.get(settings.COOKIE_REFRESH_TOKEN_KEY)
if not refresh_token:
raise errors.TokenError(msg='Refresh Token 已过期,请重新登录')
@@ -168,6 +199,13 @@ class AuthService:
@staticmethod
async def logout(*, request: Request, response: Response) -> None:
"""
用户登出
:param request: FastAPI 请求对象
:param response: FastAPI 响应对象
:return:
"""
token = get_token(request)
token_payload = jwt_decode(token)
user_id = token_payload.id
+54 -3
View File
@@ -17,25 +17,46 @@ from backend.database.db import async_db_session
class ConfigService:
"""参数配置服务类"""
@staticmethod
async def get_built_in_config(type: str) -> Sequence[Config]:
"""
获取内置参数配置
:param type: 参数配置类型
:return:
"""
async with async_db_session() as db:
return await config_dao.get_by_type(db, type)
@staticmethod
async def save_built_in_config(objs: list[SaveBuiltInConfigParam], type: str) -> None:
"""
保存内置参数配置
:param objs: 参数配置参数列表
:param type: 参数配置类型
:return:
"""
async with async_db_session.begin() as db:
for obj in objs:
config = await config_dao.get_by_key_and_type(db, obj.key, type)
if config is None:
if await config_dao.get_by_key(db, obj.key, built_in=True):
if await config_dao.get_by_key(db, obj.key):
raise errors.ForbiddenError(msg=f'参数配置 {obj.key} 已存在')
await config_dao.create_model(db, obj, flush=True, type=type)
else:
await config_dao.update_model(db, config.id, obj, type=type)
@staticmethod
async def get(pk) -> Config | dict:
async def get(pk: int) -> Config:
"""
获取参数配置详情
:param pk: 参数配置 ID
:return:
"""
async with async_db_session() as db:
config = await config_dao.get(db, pk)
if not config:
@@ -43,11 +64,24 @@ class ConfigService:
return config
@staticmethod
async def get_select(*, name: str = None, type: str = None) -> Select:
async def get_select(*, name: str | None = None, type: str | None = None) -> Select:
"""
获取参数配置列表查询条件
:param name: 参数配置名称
:param type: 参数配置类型
:return:
"""
return await config_dao.get_list(name=name, type=type)
@staticmethod
async def create(*, obj: CreateConfigParam) -> None:
"""
创建参数配置
:param obj: 参数配置创建参数
:return:
"""
async with async_db_session.begin() as db:
if obj.type in admin_settings.CONFIG_BUILT_IN_TYPES:
raise errors.ForbiddenError(msg='非法类型参数')
@@ -58,15 +92,32 @@ class ConfigService:
@staticmethod
async def update(*, pk: int, obj: UpdateConfigParam) -> int:
"""
更新参数配置
:param pk: 参数配置 ID
:param obj: 参数配置更新参数
:return:
"""
async with async_db_session.begin() as db:
config = await config_dao.get(db, pk)
if not config:
raise errors.NotFoundError(msg='参数配置不存在')
if config.key != obj.key:
config = await config_dao.get_by_key(db, obj.key)
if config:
raise errors.ForbiddenError(msg=f'参数配置 {obj.key} 已存在')
count = await config_dao.update(db, pk, obj)
return count
@staticmethod
async def delete(*, pk: list[int]) -> int:
"""
删除参数配置
:param pk: 参数配置 ID 列表
:return:
"""
async with async_db_session.begin() as db:
count = await config_dao.delete(db, pk)
return count
+50 -5
View File
@@ -17,8 +17,16 @@ from backend.utils.import_parse import dynamic_import_data_model
class DataRuleService:
"""数据权限规则服务类"""
@staticmethod
async def get(*, pk: int) -> DataRule:
"""
获取数据规则详情
:param pk: 规则 ID
:return:
"""
async with async_db_session() as db:
data_rule = await data_rule_dao.get(db, pk)
if not data_rule:
@@ -27,6 +35,12 @@ class DataRuleService:
@staticmethod
async def get_role_rules(*, pk: int) -> list[int]:
"""
获取角色的数据规则列表
:param pk: 角色 ID
:return:
"""
async with async_db_session() as db:
role = await role_dao.get_with_relation(db, pk)
if not role:
@@ -36,33 +50,50 @@ class DataRuleService:
@staticmethod
async def get_models() -> list[str]:
"""获取所有数据模型"""
return list(settings.DATA_PERMISSION_MODELS.keys())
@staticmethod
async def get_columns(model: str) -> list[str]:
"""
获取数据模型的字段列表
:param model: 模型名称
:return:
"""
if model not in settings.DATA_PERMISSION_MODELS:
raise errors.NotFoundError(msg='数据模型不存在')
try:
model_ins = dynamic_import_data_model(settings.DATA_PERMISSION_MODELS[model])
except (ImportError, AttributeError):
raise errors.ServerError(msg=f'数据模型 {model} 动态导入失败,请联系系统超级管理员')
model_ins = dynamic_import_data_model(settings.DATA_PERMISSION_MODELS[model])
model_columns = [
key for key in model_ins.__table__.columns.keys() if key not in settings.DATA_PERMISSION_COLUMN_EXCLUDE
]
return model_columns
@staticmethod
async def get_select(*, name: str = None) -> Select:
async def get_select(*, name: str | None = None) -> Select:
"""
获取数据规则列表查询条件
:param name: 规则名称
:return:
"""
return await data_rule_dao.get_list(name=name)
@staticmethod
async def get_all() -> Sequence[DataRule]:
"""获取所有数据规则"""
async with async_db_session() as db:
data_rules = await data_rule_dao.get_all(db)
return data_rules
@staticmethod
async def create(*, obj: CreateDataRuleParam) -> None:
"""
创建数据规则
:param obj: 规则创建参数
:return:
"""
async with async_db_session.begin() as db:
data_rule = await data_rule_dao.get_by_name(db, obj.name)
if data_rule:
@@ -71,6 +102,13 @@ class DataRuleService:
@staticmethod
async def update(*, pk: int, obj: UpdateDataRuleParam) -> int:
"""
更新数据规则
:param pk: 规则 ID
:param obj: 规则更新参数
:return:
"""
async with async_db_session.begin() as db:
data_rule = await data_rule_dao.get(db, pk)
if not data_rule:
@@ -80,6 +118,13 @@ class DataRuleService:
@staticmethod
async def delete(*, request: Request, pk: list[int]) -> int:
"""
删除数据规则
:param request: FastAPI 请求对象
:param pk: 规则 ID 列表
:return:
"""
async with async_db_session.begin() as db:
count = await data_rule_dao.delete(db, pk)
await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{request.user.id}')
+39 -1
View File
@@ -15,8 +15,16 @@ from backend.utils.build_tree import get_tree_data
class DeptService:
"""部门服务类"""
@staticmethod
async def get(*, pk: int) -> Dept:
"""
获取部门详情
:param pk: 部门 ID
:return:
"""
async with async_db_session() as db:
dept = await dept_dao.get(db, pk)
if not dept:
@@ -27,6 +35,15 @@ class DeptService:
async def get_dept_tree(
*, name: str | None = None, leader: str | None = None, phone: str | None = None, status: int | None = None
) -> list[dict[str, Any]]:
"""
获取部门树形结构
:param name: 部门名称
:param leader: 部门负责人
:param phone: 联系电话
:param status: 状态
:return:
"""
async with async_db_session() as db:
dept_select = await dept_dao.get_all(db=db, name=name, leader=leader, phone=phone, status=status)
tree_data = get_tree_data(dept_select)
@@ -34,6 +51,12 @@ class DeptService:
@staticmethod
async def create(*, obj: CreateDeptParam) -> None:
"""
创建部门
:param obj: 部门创建参数
:return:
"""
async with async_db_session.begin() as db:
dept = await dept_dao.get_by_name(db, obj.name)
if dept:
@@ -46,6 +69,13 @@ class DeptService:
@staticmethod
async def update(*, pk: int, obj: UpdateDeptParam) -> int:
"""
更新部门
:param pk: 部门 ID
:param obj: 部门更新参数
:return:
"""
async with async_db_session.begin() as db:
dept = await dept_dao.get(db, pk)
if not dept:
@@ -64,8 +94,16 @@ class DeptService:
@staticmethod
async def delete(*, request: Request, pk: int) -> int:
"""
删除部门
:param request: FastAPI 请求对象
:param pk: 部门 ID
:return:
"""
async with async_db_session.begin() as db:
dept_user = await dept_dao.get_with_relation(db, pk)
dept = await dept_dao.get_with_relation(db, pk)
dept_user = dept.users
if dept_user:
raise errors.ForbiddenError(msg='部门下存在用户,无法删除')
children = await dept_dao.get_children(db, pk)
+36 -1
View File
@@ -11,8 +11,16 @@ from backend.database.db import async_db_session
class DictDataService:
"""字典数据服务类"""
@staticmethod
async def get(*, pk: int) -> DictData:
"""
获取字典数据详情
:param pk: 字典数据 ID
:return:
"""
async with async_db_session() as db:
dict_data = await dict_data_dao.get_with_relation(db, pk)
if not dict_data:
@@ -20,11 +28,25 @@ class DictDataService:
return dict_data
@staticmethod
async def get_select(*, label: str = None, value: str = None, status: int = None) -> Select:
async def get_select(*, label: str | None = None, value: str | None = None, status: int | None = None) -> Select:
"""
获取字典数据列表查询条件
:param label: 字典数据标签
:param value: 字典数据键值
:param status: 状态
:return:
"""
return await dict_data_dao.get_list(label=label, value=value, status=status)
@staticmethod
async def create(*, obj: CreateDictDataParam) -> None:
"""
创建字典数据
:param obj: 字典数据创建参数
:return:
"""
async with async_db_session.begin() as db:
dict_data = await dict_data_dao.get_by_label(db, obj.label)
if dict_data:
@@ -36,6 +58,13 @@ class DictDataService:
@staticmethod
async def update(*, pk: int, obj: UpdateDictDataParam) -> int:
"""
更新字典数据
:param pk: 字典数据 ID
:param obj: 字典数据更新参数
:return:
"""
async with async_db_session.begin() as db:
dict_data = await dict_data_dao.get(db, pk)
if not dict_data:
@@ -51,6 +80,12 @@ class DictDataService:
@staticmethod
async def delete(*, pk: list[int]) -> int:
"""
删除字典数据
:param pk: 字典数据 ID 列表
:return:
"""
async with async_db_session.begin() as db:
count = await dict_data_dao.delete(db, pk)
return count
+30 -1
View File
@@ -9,12 +9,28 @@ from backend.database.db import async_db_session
class DictTypeService:
"""字典类型服务类"""
@staticmethod
async def get_select(*, name: str = None, code: str = None, status: int = None) -> Select:
async def get_select(*, name: str | None = None, code: str | None = None, status: int | None = None) -> Select:
"""
获取字典类型列表查询条件
:param name: 字典类型名称
:param code: 字典类型编码
:param status: 状态
:return:
"""
return await dict_type_dao.get_list(name=name, code=code, status=status)
@staticmethod
async def create(*, obj: CreateDictTypeParam) -> None:
"""
创建字典类型
:param obj: 字典类型创建参数
:return:
"""
async with async_db_session.begin() as db:
dict_type = await dict_type_dao.get_by_code(db, obj.code)
if dict_type:
@@ -23,6 +39,13 @@ class DictTypeService:
@staticmethod
async def update(*, pk: int, obj: UpdateDictTypeParam) -> int:
"""
更新字典类型
:param pk: 字典类型 ID
:param obj: 字典类型更新参数
:return:
"""
async with async_db_session.begin() as db:
dict_type = await dict_type_dao.get(db, pk)
if not dict_type:
@@ -35,6 +58,12 @@ class DictTypeService:
@staticmethod
async def delete(*, pk: list[int]) -> int:
"""
删除字典类型
:param pk: 字典类型 ID 列表
:return:
"""
async with async_db_session.begin() as db:
count = await dict_type_dao.delete(db, pk)
return count
+32 -3
View File
@@ -13,8 +13,18 @@ from backend.database.db import async_db_session
class LoginLogService:
"""登录日志服务类"""
@staticmethod
async def get_select(*, username: str, status: int, ip: str) -> Select:
async def get_select(*, username: str | None = None, status: int | None = None, ip: str | None = None) -> Select:
"""
获取登录日志列表查询条件
:param username: 用户名
:param status: 状态
:param ip: IP 地址
:return:
"""
return await login_log_dao.get_list(username=username, status=status, ip=ip)
@staticmethod
@@ -28,8 +38,20 @@ class LoginLogService:
status: int,
msg: str,
) -> None:
"""
创建登录日志
:param db: 数据库会话
:param request: FastAPI 请求对象
:param user_uuid: 用户 UUID
:param username: 用户名
:param login_time: 登录时间
:param status: 状态
:param msg: 消息
:return:
"""
try:
obj_in = CreateLoginLogParam(
obj = CreateLoginLogParam(
user_uuid=user_uuid,
username=username,
status=status,
@@ -44,18 +66,25 @@ class LoginLogService:
msg=msg,
login_time=login_time,
)
await login_log_dao.create(db, obj_in)
await login_log_dao.create(db, obj)
except Exception as e:
log.error(f'登录日志创建失败: {e}')
@staticmethod
async def delete(*, pk: list[int]) -> int:
"""
删除登录日志
:param pk: 日志 ID 列表
:return:
"""
async with async_db_session.begin() as db:
count = await login_log_dao.delete(db, pk)
return count
@staticmethod
async def delete_all() -> int:
"""清空所有登录日志"""
async with async_db_session.begin() as db:
count = await login_log_dao.delete_all(db)
return count
+47
View File
@@ -16,8 +16,16 @@ from backend.utils.build_tree import get_tree_data
class MenuService:
"""菜单服务类"""
@staticmethod
async def get(*, pk: int) -> Menu:
"""
获取菜单详情
:param pk: 菜单 ID
:return:
"""
async with async_db_session() as db:
menu = await menu_dao.get(db, menu_id=pk)
if not menu:
@@ -26,6 +34,13 @@ class MenuService:
@staticmethod
async def get_menu_tree(*, title: str | None = None, status: int | None = None) -> list[dict[str, Any]]:
"""
获取菜单树形结构
:param title: 菜单标题
:param status: 状态
: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)
@@ -33,6 +48,12 @@ class MenuService:
@staticmethod
async def get_role_menu_tree(*, pk: int) -> list[dict[str, Any]]:
"""
获取角色的菜单树形结构
:param pk: 角色 ID
:return:
"""
async with async_db_session() as db:
role = await role_dao.get_with_relation(db, pk)
if not role:
@@ -44,6 +65,12 @@ class MenuService:
@staticmethod
async def get_user_menu_tree(*, request: Request) -> list[dict[str, Any]]:
"""
获取用户的菜单树形结构
:param request: FastAPI 请求对象
:return:
"""
async with async_db_session() as db:
roles = request.user.roles
menu_ids = []
@@ -57,6 +84,12 @@ class MenuService:
@staticmethod
async def create(*, obj: CreateMenuParam) -> None:
"""
创建菜单
:param obj: 菜单创建参数
:return:
"""
async with async_db_session.begin() as db:
title = await menu_dao.get_by_title(db, obj.title)
if title:
@@ -69,6 +102,13 @@ class MenuService:
@staticmethod
async def update(*, pk: int, obj: UpdateMenuParam) -> int:
"""
更新菜单
:param pk: 菜单 ID
:param obj: 菜单更新参数
:return:
"""
async with async_db_session.begin() as db:
menu = await menu_dao.get(db, pk)
if not menu:
@@ -87,6 +127,13 @@ class MenuService:
@staticmethod
async def delete(*, request: Request, pk: int) -> int:
"""
删除菜单
:param request: FastAPI 请求对象
:param pk: 菜单 ID
:return:
"""
async with async_db_session.begin() as db:
children = await menu_dao.get_children(db, pk)
if children:
+16 -2
View File
@@ -1,5 +1,7 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from typing import Any
from fast_captcha import text_captcha
from fastapi import BackgroundTasks, Request, Response
@@ -20,15 +22,27 @@ from backend.utils.timezone import timezone
class OAuth2Service:
"""OAuth2 认证服务类"""
@staticmethod
async def create_with_login(
*,
request: Request,
response: Response,
background_tasks: BackgroundTasks,
user: dict,
user: dict[str, Any],
social: UserSocialType,
) -> GetLoginToken | None:
"""
创建 OAuth2 用户并登录
:param request: FastAPI 请求对象
:param response: FastAPI 响应对象
:param background_tasks: FastAPI 后台任务
:param user: OAuth2 用户信息
:param social: 社交平台类型
:return:
"""
async with async_db_session.begin() as db:
# 获取 OAuth2 平台用户信息
social_id = user.get('id')
@@ -37,7 +51,7 @@ class OAuth2Service:
social_username = user.get('login')
social_nickname = user.get('name')
social_email = user.get('email')
if social == UserSocialType.linuxdo: # 不提供明文邮箱的平台
if social == UserSocialType.linux_do: # 不提供明文邮箱的平台
social_email = f'{social_username}@linux.do'
if not social_email:
raise AuthorizationError(msg=f'授权失败,{social.value} 账户未绑定邮箱')
+25 -2
View File
@@ -8,23 +8,46 @@ from backend.database.db import async_db_session
class OperaLogService:
"""操作日志服务类"""
@staticmethod
async def get_select(*, username: str | None = None, status: int | None = None, ip: str | None = None) -> Select:
"""
获取操作日志列表查询条件
:param username: 用户名
:param status: 状态
:param ip: IP 地址
:return:
"""
return await opera_log_dao.get_list(username=username, status=status, ip=ip)
@staticmethod
async def create(*, obj_in: CreateOperaLogParam):
async def create(*, obj: CreateOperaLogParam) -> None:
"""
创建操作日志
:param obj: 操作日志创建参数
:return:
"""
async with async_db_session.begin() as db:
await opera_log_dao.create(db, obj_in)
await opera_log_dao.create(db, obj)
@staticmethod
async def delete(*, pk: list[int]) -> int:
"""
删除操作日志
:param pk: 日志 ID 列表
:return:
"""
async with async_db_session.begin() as db:
count = await opera_log_dao.delete(db, pk)
return count
@staticmethod
async def delete_all() -> int:
"""清空所有操作日志"""
async with async_db_session.begin() as db:
count = await opera_log_dao.delete_all(db)
return count
+59 -1
View File
@@ -22,8 +22,16 @@ from backend.database.redis import redis_client
class RoleService:
"""角色服务类"""
@staticmethod
async def get(*, pk: int) -> Role:
"""
获取角色详情
:param pk: 角色 ID
:return:
"""
async with async_db_session() as db:
role = await role_dao.get_with_relation(db, pk)
if not role:
@@ -32,22 +40,42 @@ class RoleService:
@staticmethod
async def get_all() -> Sequence[Role]:
"""获取所有角色"""
async with async_db_session() as db:
roles = await role_dao.get_all(db)
return roles
@staticmethod
async def get_by_user(*, pk: int) -> Sequence[Role]:
"""
获取用户的角色列表
:param pk: 用户 ID
:return:
"""
async with async_db_session() as db:
roles = await role_dao.get_by_user(db, user_id=pk)
return roles
@staticmethod
async def get_select(*, name: str = None, status: int = None) -> Select:
async def get_select(*, name: str | None = None, status: int | None = None) -> Select:
"""
获取角色列表查询条件
:param name: 角色名称
:param status: 状态
:return:
"""
return await role_dao.get_list(name=name, status=status)
@staticmethod
async def create(*, obj: CreateRoleParam) -> None:
"""
创建角色
:param obj: 角色创建参数
:return:
"""
async with async_db_session.begin() as db:
role = await role_dao.get_by_name(db, obj.name)
if role:
@@ -56,6 +84,13 @@ class RoleService:
@staticmethod
async def update(*, pk: int, obj: UpdateRoleParam) -> int:
"""
更新角色
:param pk: 角色 ID
:param obj: 角色更新参数
:return:
"""
async with async_db_session.begin() as db:
role = await role_dao.get(db, pk)
if not role:
@@ -69,6 +104,14 @@ class RoleService:
@staticmethod
async def update_role_menu(*, request: Request, pk: int, menu_ids: UpdateRoleMenuParam) -> int:
"""
更新角色菜单
:param request: FastAPI 请求对象
:param pk: 角色 ID
:param menu_ids: 菜单 ID 列表
:return:
"""
async with async_db_session.begin() as db:
role = await role_dao.get(db, pk)
if not role:
@@ -84,6 +127,14 @@ class RoleService:
@staticmethod
async def update_role_rule(*, request: Request, pk: int, rule_ids: UpdateRoleRuleParam) -> int:
"""
更新角色数据权限
:param request: FastAPI 请求对象
:param pk: 角色 ID
:param rule_ids: 权限规则 ID 列表
:return:
"""
async with async_db_session.begin() as db:
role = await role_dao.get(db, pk)
if not role:
@@ -99,6 +150,13 @@ class RoleService:
@staticmethod
async def delete(*, request: Request, pk: list[int]) -> int:
"""
删除角色
:param request: FastAPI 请求对象
:param pk: 角色 ID 列表
:return:
"""
async with async_db_session.begin() as db:
count = await role_dao.delete(db, pk)
await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{request.user.id}')
+161 -70
View File
@@ -25,8 +25,16 @@ 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='密码为空')
@@ -44,6 +52,13 @@ class UserService:
@staticmethod
async def add(*, request: Request, obj: AddUserParam) -> None:
"""
添加新用户
:param request: FastAPI 请求对象
:param obj: 用户添加参数
:return:
"""
async with async_db_session.begin() as db:
superuser_verify(request)
username = await user_dao.get_by_username(db, obj.username)
@@ -69,13 +84,20 @@ class UserService:
@staticmethod
async def pwd_reset(*, request: Request, obj: ResetPasswordParam) -> int:
"""
重置用户密码
:param request: FastAPI 请求对象
:param obj: 密码重置参数
:return:
"""
async with async_db_session.begin() as db:
user = await user_dao.get(db, request.user.id)
if not user:
raise errors.NotFoundError(msg='用户不存在')
if not password_verify(obj.old_password, user.password):
raise errors.ForbiddenError(msg='原密码错误')
np1 = obj.new_password
np2 = obj.confirm_password
if np1 != np2:
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)
@@ -90,6 +112,12 @@ class UserService:
@staticmethod
async def get_userinfo(*, username: str) -> User:
"""
获取用户信息
:param username: 用户名
:return:
"""
async with async_db_session() as db:
user = await user_dao.get_with_relation(db, username=username)
if not user:
@@ -98,10 +126,17 @@ class UserService:
@staticmethod
async def update(*, request: Request, username: str, obj: UpdateUserParam) -> int:
"""
更新用户信息
:param request: FastAPI 请求对象
:param username: 用户名
:param obj: 用户更新参数
:return:
"""
async with async_db_session.begin() as db:
if not request.user.is_superuser:
if request.user.username != username:
raise errors.ForbiddenError(msg='你只能修改自己的信息')
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='用户不存在')
@@ -123,10 +158,17 @@ class UserService:
@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:
if request.user.username != username:
raise errors.AuthorizationError
if not request.user.is_superuser and request.user.username != username:
raise errors.AuthorizationError
input_user = await user_dao.get_with_relation(db, username=username)
if not input_user:
raise errors.NotFoundError(msg='用户不存在')
@@ -139,10 +181,17 @@ class UserService:
@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 not request.user.is_superuser:
if request.user.username != username:
raise errors.AuthorizationError
if not request.user.is_superuser and request.user.username != username:
raise errors.AuthorizationError
input_user = await user_dao.get_by_username(db, username)
if not input_user:
raise errors.NotFoundError(msg='用户不存在')
@@ -152,96 +201,138 @@ class UserService:
@staticmethod
async def get_select(*, dept: int, username: str = None, phone: str = None, status: int = None) -> 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:
"""
更新用户权限
:param request: FastAPI 请求对象
:param pk: 用户 ID
:return:
"""
async with async_db_session.begin() as db:
superuser_verify(request)
if not await user_dao.get(db, pk):
user = await user_dao.get(db, pk)
if not user:
raise errors.NotFoundError(msg='用户不存在')
else:
if pk == request.user.id:
raise errors.ForbiddenError(msg='非法操作')
super_status = await user_dao.get_super(db, pk)
count = await user_dao.set_super(db, pk, False if super_status else True)
await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{pk}')
return count
if pk == request.user.id:
raise errors.ForbiddenError(msg='非法操作')
super_status = await user_dao.get_super(db, pk)
count = await user_dao.set_super(db, pk, not super_status)
await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{pk}')
return count
@staticmethod
async def update_staff(*, request: Request, pk: int) -> int:
"""
更新用户职员状态
:param request: FastAPI 请求对象
:param pk: 用户 ID
:return:
"""
async with async_db_session.begin() as db:
superuser_verify(request)
if not await user_dao.get(db, pk):
user = await user_dao.get(db, pk)
if not user:
raise errors.NotFoundError(msg='用户不存在')
else:
if pk == request.user.id:
raise errors.ForbiddenError(msg='非法操作')
staff_status = await user_dao.get_staff(db, pk)
count = await user_dao.set_staff(db, pk, False if staff_status else True)
await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{pk}')
return count
if pk == request.user.id:
raise errors.ForbiddenError(msg='非法操作')
staff_status = await user_dao.get_staff(db, pk)
count = await user_dao.set_staff(db, pk, not staff_status)
await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{pk}')
return count
@staticmethod
async def update_status(*, request: Request, pk: int) -> int:
"""
更新用户状态
:param request: FastAPI 请求对象
:param pk: 用户 ID
:return:
"""
async with async_db_session.begin() as db:
superuser_verify(request)
if not await user_dao.get(db, pk):
user = await user_dao.get(db, pk)
if not user:
raise errors.NotFoundError(msg='用户不存在')
else:
if pk == request.user.id:
raise errors.ForbiddenError(msg='非法操作')
status = await user_dao.get_status(db, pk)
count = await user_dao.set_status(db, pk, False if status else True)
await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{pk}')
return count
if pk == request.user.id:
raise errors.ForbiddenError(msg='非法操作')
status = await user_dao.get_status(db, pk)
count = await user_dao.set_status(db, pk, 0 if status == 1 else 1)
await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{pk}')
return count
@staticmethod
async def update_multi_login(*, request: Request, pk: int) -> int:
"""
更新用户多端登录状态
:param request: FastAPI 请求对象
:param pk: 用户 ID
:return:
"""
async with async_db_session.begin() as db:
superuser_verify(request)
if not await user_dao.get(db, pk):
user = await user_dao.get(db, pk)
if not user:
raise errors.NotFoundError(msg='用户不存在')
user_id = request.user.id
multi_login = await user_dao.get_multi_login(db, pk) if pk != user_id else request.user.is_multi_login
count = await user_dao.set_multi_login(db, pk, not multi_login)
# 删除当前用户缓存
await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{request.user.id}')
token = get_token(request)
token_payload = jwt_decode(token)
latest_multi_login = await user_dao.get_multi_login(db, pk)
# 超级用户修改自身时,除当前 token 外,其他 token 失效
if pk == user_id:
if not latest_multi_login:
key_prefix = f'{settings.TOKEN_REDIS_PREFIX}:{pk}'
await redis_client.delete_prefix(key_prefix, exclude=f'{key_prefix}:{token_payload.session_uuid}')
refresh_token = request.cookies.get(settings.COOKIE_REFRESH_TOKEN_KEY)
if refresh_token:
key_prefix = f'{settings.TOKEN_REFRESH_REDIS_PREFIX}:{pk}'
await redis_client.delete_prefix(key_prefix, exclude=f'{key_prefix}:{refresh_token}')
# 超级用户修改他人时,其他 token 将全部失效
else:
user_id = request.user.id
multi_login = await user_dao.get_multi_login(db, pk) if pk != user_id else request.user.is_multi_login
count = await user_dao.set_multi_login(db, pk, False if multi_login else True)
await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{request.user.id}')
token = get_token(request)
token_payload = jwt_decode(token)
latest_multi_login = await user_dao.get_multi_login(db, pk)
# 超级用户修改自身时,除当前token外,其他token失效
if pk == user_id:
if not latest_multi_login:
key_prefix = f'{settings.TOKEN_REDIS_PREFIX}:{pk}'
await redis_client.delete_prefix(
key_prefix, exclude=f'{key_prefix}:{token_payload.session_uuid}'
)
refresh_token = request.cookies.get(settings.COOKIE_REFRESH_TOKEN_KEY)
if refresh_token:
key_prefix = f'{settings.TOKEN_REFRESH_REDIS_PREFIX}:{pk}'
await redis_client.delete_prefix(key_prefix, exclude=f'{key_prefix}:{refresh_token}')
# 超级用户修改他人时,其他token将全部失效
else:
if not latest_multi_login:
key_prefix = [f'{settings.TOKEN_REDIS_PREFIX}:{pk}']
refresh_token = request.cookies.get(settings.COOKIE_REFRESH_TOKEN_KEY)
if refresh_token:
key_prefix.append(f'{settings.TOKEN_REFRESH_REDIS_PREFIX}:{pk}')
for prefix in key_prefix:
await redis_client.delete_prefix(prefix)
return count
if not latest_multi_login:
key_prefix = [f'{settings.TOKEN_REDIS_PREFIX}:{pk}']
refresh_token = request.cookies.get(settings.COOKIE_REFRESH_TOKEN_KEY)
if refresh_token:
key_prefix.append(f'{settings.TOKEN_REFRESH_REDIS_PREFIX}:{pk}')
for prefix in key_prefix:
await redis_client.delete_prefix(prefix)
return count
@staticmethod
async def delete(*, username: str) -> int:
"""
删除用户
:param username: 用户名
:return:
"""
async with async_db_session.begin() as db:
input_user = await user_dao.get_by_username(db, username)
if not input_user:
user = await user_dao.get_by_username(db, username)
if not user:
raise errors.NotFoundError(msg='用户不存在')
count = await user_dao.delete(db, input_user.id)
count = await user_dao.delete(db, user.id)
key_prefix = [
f'{settings.TOKEN_REDIS_PREFIX}:{input_user.id}',
f'{settings.TOKEN_REFRESH_REDIS_PREFIX}:{input_user.id}',
f'{settings.TOKEN_REDIS_PREFIX}:{user.id}',
f'{settings.TOKEN_REFRESH_REDIS_PREFIX}:{user.id}',
]
for key in key_prefix:
await redis_client.delete_prefix(key)
+4 -1
View File
@@ -1,5 +1,8 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from typing import AsyncGenerator
from sqlalchemy.ext.asyncio.session import AsyncSession
from backend.database.db import create_async_engine_and_session, create_database_url
@@ -8,7 +11,7 @@ TEST_SQLALCHEMY_DATABASE_URL = create_database_url(unittest=True)
_, async_test_db_session = create_async_engine_and_session(TEST_SQLALCHEMY_DATABASE_URL)
async def override_get_db():
async def override_get_db() -> AsyncGenerator[AsyncSession, None]:
"""session 生成器"""
async with async_test_db_session() as session:
yield session
+5 -5
View File
@@ -18,7 +18,7 @@ router = APIRouter()
@router.get('/tables', summary='获取数据库表')
async def get_all_tables(
table_schema: Annotated[str, Query(..., description='数据库名')] = 'fba',
table_schema: Annotated[str, Query(description='数据库名')] = 'fba',
) -> ResponseSchemaModel[list[str]]:
data = await gen_service.get_tables(table_schema=table_schema)
return response_base.success(data=data)
@@ -38,13 +38,13 @@ async def import_table(obj: ImportParam) -> ResponseModel:
@router.get('/preview/{pk}', summary='生成代码预览', dependencies=[DependsJwtAuth])
async def preview_code(pk: Annotated[int, Path(..., description='业务ID')]) -> ResponseSchemaModel[dict[str, bytes]]:
async def preview_code(pk: Annotated[int, Path(description='业务 ID')]) -> ResponseSchemaModel[dict[str, bytes]]:
data = await gen_service.preview(pk=pk)
return response_base.success(data=data)
@router.get('/generate/{pk}/path', summary='获取代码生成路径', dependencies=[DependsJwtAuth])
async def generate_path(pk: Annotated[int, Path(..., description='业务ID')]) -> ResponseSchemaModel[list[str]]:
async def generate_path(pk: Annotated[int, Path(description='业务 ID')]) -> ResponseSchemaModel[list[str]]:
data = await gen_service.get_generate_path(pk=pk)
return response_base.success(data=data)
@@ -58,13 +58,13 @@ async def generate_path(pk: Annotated[int, Path(..., description='业务ID')]) -
DependsRBAC,
],
)
async def generate_code(pk: Annotated[int, Path(..., description='业务ID')]) -> ResponseModel:
async def generate_code(pk: Annotated[int, Path(description='业务 ID')]) -> ResponseModel:
await gen_service.generate(pk=pk)
return response_base.success()
@router.get('/download/{pk}', summary='下载代码', dependencies=[DependsJwtAuth])
async def download_code(pk: Annotated[int, Path(..., description='业务ID')]):
async def download_code(pk: Annotated[int, Path(description='业务 ID')]):
bio = await gen_service.download(pk=pk)
return StreamingResponse(
bio,
+13 -11
View File
@@ -16,29 +16,29 @@ from backend.common.response.response_schema import ResponseModel, ResponseSchem
from backend.common.security.jwt import DependsJwtAuth
from backend.common.security.permission import RequestPermission
from backend.common.security.rbac import DependsRBAC
from backend.utils.serializers import select_as_dict, select_list_serialize
router = APIRouter()
@router.get('/all', summary='获取所有代码生成业务', dependencies=[DependsJwtAuth])
async def get_all_businesses() -> ResponseSchemaModel[list[GetGenBusinessDetail]]:
businesses = await gen_business_service.get_all()
data = select_list_serialize(businesses)
data = await gen_business_service.get_all()
return response_base.success(data=data)
@router.get('/{pk}', summary='获取代码生成业务详情', dependencies=[DependsJwtAuth])
async def get_business(pk: Annotated[int, Path(...)]) -> ResponseSchemaModel[GetGenBusinessDetail]:
business = await gen_business_service.get(pk=pk)
data = GetGenBusinessDetail(**select_as_dict(business))
async def get_business(
pk: Annotated[int, Path(description='业务 ID')],
) -> ResponseSchemaModel[GetGenBusinessDetail]:
data = await gen_business_service.get(pk=pk)
return response_base.success(data=data)
@router.get('/{pk}/models', summary='获取代码生成业务所有模型', dependencies=[DependsJwtAuth])
async def get_business_all_models(pk: Annotated[int, Path(...)]) -> ResponseSchemaModel[list[GetGenModelDetail]]:
models = await gen_model_service.get_by_business(business_id=pk)
data = select_list_serialize(models)
async def get_business_all_models(
pk: Annotated[int, Path(description='业务 ID')],
) -> ResponseSchemaModel[list[GetGenModelDetail]]:
data = await gen_model_service.get_by_business(business_id=pk)
return response_base.success(data=data)
@@ -64,7 +64,9 @@ async def create_business(obj: CreateGenBusinessParam) -> ResponseModel:
DependsRBAC,
],
)
async def update_business(pk: Annotated[int, Path(...)], obj: UpdateGenBusinessParam) -> ResponseModel:
async def update_business(
pk: Annotated[int, Path(description='业务 ID')], obj: UpdateGenBusinessParam
) -> ResponseModel:
count = await gen_business_service.update(pk=pk, obj=obj)
if count > 0:
return response_base.success()
@@ -79,7 +81,7 @@ async def update_business(pk: Annotated[int, Path(...)], obj: UpdateGenBusinessP
DependsRBAC,
],
)
async def delete_business(pk: Annotated[int, Path(...)]) -> ResponseModel:
async def delete_business(pk: Annotated[int, Path(description='业务 ID')]) -> ResponseModel:
count = await gen_business_service.delete(pk=pk)
if count > 0:
return response_base.success()
+4 -6
View File
@@ -10,7 +10,6 @@ from backend.common.response.response_schema import ResponseModel, ResponseSchem
from backend.common.security.jwt import DependsJwtAuth
from backend.common.security.permission import RequestPermission
from backend.common.security.rbac import DependsRBAC
from backend.utils.serializers import select_as_dict
router = APIRouter()
@@ -22,9 +21,8 @@ async def get_model_types() -> ResponseSchemaModel[list[str]]:
@router.get('/{pk}', summary='获取代码生成模型详情', dependencies=[DependsJwtAuth])
async def get_model(pk: Annotated[int, Path(...)]) -> ResponseSchemaModel[GetGenModelDetail]:
model = await gen_model_service.get(pk=pk)
data = GetGenModelDetail(**select_as_dict(model))
async def get_model(pk: Annotated[int, Path(description='模型 ID')]) -> ResponseSchemaModel[GetGenModelDetail]:
data = await gen_model_service.get(pk=pk)
return response_base.success(data=data)
@@ -49,7 +47,7 @@ async def create_model(obj: CreateGenModelParam) -> ResponseModel:
DependsRBAC,
],
)
async def update_model(pk: Annotated[int, Path(...)], obj: UpdateGenModelParam) -> ResponseModel:
async def update_model(pk: Annotated[int, Path(description='模型 ID')], obj: UpdateGenModelParam) -> ResponseModel:
count = await gen_model_service.update(pk=pk, obj=obj)
if count > 0:
return response_base.success()
@@ -64,7 +62,7 @@ async def update_model(pk: Annotated[int, Path(...)], obj: UpdateGenModelParam)
DependsRBAC,
],
)
async def delete_model(pk: Annotated[int, Path(...)]) -> ResponseModel:
async def delete_model(pk: Annotated[int, Path(description='模型 ID')]) -> ResponseModel:
count = await gen_model_service.delete(pk=pk)
if count > 0:
return response_base.success()
+3 -3
View File
@@ -6,9 +6,9 @@ from pydantic_settings import BaseSettings
class GeneratorSettings(BaseSettings):
"""Admin Settings"""
"""代码生成配置"""
# 模版目录
# 模版
TEMPLATE_BACKEND_DIR_NAME: str = 'py'
# 代码下载
@@ -17,7 +17,7 @@ class GeneratorSettings(BaseSettings):
@lru_cache
def get_generator_settings() -> GeneratorSettings:
"""获取 generator 配置"""
"""获取代码生成配置"""
return GeneratorSettings()
+25 -1
View File
@@ -9,8 +9,17 @@ from backend.core.conf import settings
class CRUDGen:
"""代码生成 CRUD 类"""
@staticmethod
async def get_all_tables(db: AsyncSession, table_schema: str) -> Sequence[str]:
async def get_all_tables(db: AsyncSession, table_schema: str) -> list[str]:
"""
获取所有表名
:param db: 数据库会话
:param table_schema: 数据库 schema 名称
:return:
"""
if settings.DATABASE_TYPE == 'mysql':
sql = """
SELECT table_name AS table_name FROM information_schema.tables
@@ -30,6 +39,13 @@ class CRUDGen:
@staticmethod
async def get_table(db: AsyncSession, table_name: str) -> Row[tuple]:
"""
获取表信息
:param db: 数据库会话
:param table_name: 表名
:return:
"""
if settings.DATABASE_TYPE == 'mysql':
sql = """
SELECT table_name AS table_name, table_comment AS table_comment FROM information_schema.tables
@@ -51,6 +67,14 @@ class CRUDGen:
@staticmethod
async def get_all_columns(db: AsyncSession, table_schema: str, table_name: str) -> Sequence[Row[tuple]]:
"""
获取所有列信息
:param db: 数据库会话
:param table_schema: 数据库 schema 名称
:param table_name: 表名
:return:
"""
if settings.DATABASE_TYPE == 'mysql':
sql = """
SELECT column_name AS column_name,
+24 -21
View File
@@ -10,61 +10,64 @@ from backend.app.generator.schema.gen_business import CreateGenBusinessParam, Up
class CRUDGenBusiness(CRUDPlus[GenBusiness]):
"""代码生成业务 CRUD 类"""
async def get(self, db: AsyncSession, pk: int) -> GenBusiness | None:
"""
获取代码生成业务
获取代码生成业务
:param db:
:param pk:
:param db: 数据库会话
:param pk: 代码生成业务 ID
:return:
"""
return await self.select_model(db, pk)
async def get_by_name(self, db: AsyncSession, name: str) -> GenBusiness | None:
"""
通过 name 获取代码生成业务
通过 name 获取代码生成业务
:param db:
:param name:
:param db: 数据库会话
:param name: 表名
:return:
"""
return await self.select_model_by_column(db, table_name_en=name)
async def get_all(self, db: AsyncSession) -> Sequence[GenBusiness]:
"""
获取所有代码生成业务
获取所有代码生成业务
:param db: 数据库会话
:return:
"""
return await self.select_models(db)
async def create(self, db: AsyncSession, obj_in: CreateGenBusinessParam) -> None:
async def create(self, db: AsyncSession, obj: CreateGenBusinessParam) -> None:
"""
创建代码生成业务
创建代码生成业务
:param db:
:param obj_in:
:param db: 数据库会话
:param obj: 创建代码生成业务参数
:return:
"""
await self.create_model(db, obj_in)
await self.create_model(db, obj)
async def update(self, db: AsyncSession, pk: int, obj_in: UpdateGenBusinessParam) -> int:
async def update(self, db: AsyncSession, pk: int, obj: UpdateGenBusinessParam) -> int:
"""
更新代码生成业务
更新代码生成业务
:param db:
:param pk:
:param obj_in:
:param db: 数据库会话
:param pk: 代码生成业务 ID
:param obj: 更新代码生成业务参数
:return:
"""
return await self.update_model(db, pk, obj_in)
return await self.update_model(db, pk, obj)
async def delete(self, db: AsyncSession, pk: int) -> int:
"""
删除代码生成业务
删除代码生成业务
:param db:
:param pk:
:param db: 数据库会话
:param pk: 代码生成业务 ID
:return:
"""
return await self.delete_model(db, pk)
+23 -19
View File
@@ -10,53 +10,57 @@ from backend.app.generator.schema.gen_model import CreateGenModelParam, UpdateGe
class CRUDGenModel(CRUDPlus[GenModel]):
"""代码生成模型 CRUD 类"""
async def get(self, db: AsyncSession, pk: int) -> GenModel | None:
"""
获取代码生成模型列
:param db: 数据库会话
:param pk: 代码生成模型 ID
:return:
"""
return await self.select_model(db, pk)
async def get_all_by_business_id(self, db: AsyncSession, business_id: int) -> Sequence[GenModel]:
async def get_all_by_business(self, db: AsyncSession, business_id: int) -> Sequence[GenModel]:
"""
获取所有代码生成模型列
:param db:
:param business_id:
:param db: 数据库会话
:param business_id: 业务 ID
:return:
"""
return await self.select_models_order(db, sort_columns='sort', gen_business_id=business_id)
async def create(self, db: AsyncSession, obj_in: CreateGenModelParam, pd_type: str | None = None) -> None:
async def create(self, db: AsyncSession, obj: CreateGenModelParam, pd_type: str | None = None) -> None:
"""
创建代码生成模型
创建代码生成模型
:param db:
:param obj_in:
:param pd_type:
:param db: 数据库会话
:param obj: 创建代码生成模型参数
:param pd_type: Pydantic 类型
:return:
"""
await self.create_model(db, obj_in, pd_type=pd_type)
await self.create_model(db, obj, pd_type=pd_type)
async def update(self, db: AsyncSession, pk: int, obj_in: UpdateGenModelParam, pd_type: str | None = None) -> int:
async def update(self, db: AsyncSession, pk: int, obj: UpdateGenModelParam, pd_type: str | None = None) -> int:
"""
代码生成模型
代码生成模型
:param db:
:param pk:
:param obj_in:
:param pd_type:
:param db: 数据库会话
:param pk: 代码生成模型 ID
:param obj: 更新代码生成模型参数
:param pd_type: Pydantic 类型
:return:
"""
return await self.update_model(db, pk, obj_in, pd_type=pd_type)
return await self.update_model(db, pk, obj, pd_type=pd_type)
async def delete(self, db: AsyncSession, pk: int) -> int:
"""
删除代码生成模型
删除代码生成模型
:param db:
:param pk:
:param db: 数据库会话
:param pk: 代码生成模型 ID
:return:
"""
return await self.delete_model(db, pk)
+6 -1
View File
@@ -1,5 +1,7 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from typing import TYPE_CHECKING
from sqlalchemy import String
from sqlalchemy.dialects.mysql import LONGTEXT
from sqlalchemy.dialects.postgresql import TEXT
@@ -7,6 +9,9 @@ from sqlalchemy.orm import Mapped, mapped_column, relationship
from backend.common.model import Base, id_key
if TYPE_CHECKING:
from backend.app.generator.model import GenModel
class GenBusiness(Base):
"""代码生成业务表"""
@@ -28,4 +33,4 @@ class GenBusiness(Base):
LONGTEXT().with_variant(TEXT, 'postgresql'), default=None, comment='备注'
)
# 代码生成业务模型一对多
gen_model: Mapped[list['GenModel']] = relationship(init=False, back_populates='gen_business') # noqa: F821
gen_model: Mapped[list['GenModel']] = relationship(init=False, back_populates='gen_business')
+5 -2
View File
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from typing import Union
from typing import TYPE_CHECKING, Union
from sqlalchemy import ForeignKey, String
from sqlalchemy.dialects.mysql import LONGTEXT
@@ -9,6 +9,9 @@ from sqlalchemy.orm import Mapped, mapped_column, relationship
from backend.common.model import DataClassBase, id_key
if TYPE_CHECKING:
from backend.app.generator.model import GenBusiness
class GenModel(DataClassBase):
"""代码生成模型表"""
@@ -32,4 +35,4 @@ class GenModel(DataClassBase):
gen_business_id: Mapped[int] = mapped_column(
ForeignKey('sys_gen_business.id', ondelete='CASCADE'), default=0, comment='代码生成业务ID'
)
gen_business: Mapped[Union['GenBusiness', None]] = relationship(init=False, back_populates='gen_model') # noqa: F821
gen_business: Mapped[Union['GenBusiness', None]] = relationship(init=False, back_populates='gen_model')
+2
View File
@@ -6,6 +6,8 @@ from backend.common.schema import SchemaBase
class ImportParam(SchemaBase):
"""导入参数"""
app: str = Field(description='应用名称,用于代码生成到指定 app')
table_name: str = Field(description='数据库表名')
table_schema: str = Field(description='数据库名')
+20 -17
View File
@@ -5,41 +5,44 @@ from datetime import datetime
from pydantic import ConfigDict, Field, model_validator
from typing_extensions import Self
from backend.app.generator.schema.gen_model import GetGenModelDetail
from backend.common.schema import SchemaBase
class GenBusinessSchemaBase(SchemaBase):
app_name: str
table_name_en: str
table_name_zh: str
table_simple_name_zh: str
table_comment: str | None = None
schema_name: str | None = None
default_datetime_column: bool = Field(default=True)
api_version: str = Field(default='v1')
gen_path: str | None = None
remark: str | None = None
"""代码生成业务基础模型"""
app_name: str = Field(description='应用名称(英文)')
table_name_en: str = Field(description='表名称(英文)')
table_name_zh: str = Field(description='表名称(中文)')
table_simple_name_zh: str = Field(description='表名称(中文简称)')
table_comment: str | None = Field(None, description='表描述')
schema_name: str | None = Field(None, description='Schema 名称 (默认为英文表名称)')
default_datetime_column: bool = Field(True, description='是否存在默认时间列')
api_version: str = Field('v1', description='代码生成 api 版本')
gen_path: str | None = Field(None, description='代码生成路径(默认为 app 根路径)')
remark: str | None = Field(None, description='备注')
@model_validator(mode='after')
def check_schema_name(self) -> Self:
"""检查并设置 schema 名称"""
if self.schema_name is None:
self.schema_name = self.table_name_en
return self
class CreateGenBusinessParam(GenBusinessSchemaBase):
pass
"""创建代码生成业务参数"""
class UpdateGenBusinessParam(GenBusinessSchemaBase):
pass
"""更新代码生成业务参数"""
class GetGenBusinessDetail(GenBusinessSchemaBase):
"""获取代码生成业务详情"""
model_config = ConfigDict(from_attributes=True)
id: int
created_time: datetime
updated_time: datetime | None = None
gen_model: list[GetGenModelDetail] | None = None
id: int = Field(description='主键 ID')
created_time: datetime = Field(description='创建时间')
updated_time: datetime | None = Field(None, description='更新时间')
+19 -14
View File
@@ -7,32 +7,37 @@ from backend.utils.type_conversion import sql_type_to_sqlalchemy
class GenModelSchemaBase(SchemaBase):
name: str
comment: str | None = None
type: str
default: str | None = None
sort: int
length: int
is_pk: bool = Field(default=False)
is_nullable: bool = Field(default=False)
gen_business_id: int | None = Field(ge=1)
"""代码生成模型基础模型"""
name: str = Field(description='列名称')
comment: str | None = Field(None, description='列描述')
type: str = Field(description='SQLA 模型列类型')
default: str | None = Field(None, description='列默认值')
sort: int = Field(description='列排序')
length: int = Field(description='列长度')
is_pk: bool = Field(False, description='是否主键')
is_nullable: bool = Field(False, description='是否可为空')
gen_business_id: int = Field(description='代码生成业务ID')
@field_validator('type')
@classmethod
def type_update(cls, v):
def type_update(cls, v: str) -> str:
"""更新列类型"""
return sql_type_to_sqlalchemy(v)
class CreateGenModelParam(GenModelSchemaBase):
pass
"""创建代码生成模型参数"""
class UpdateGenModelParam(GenModelSchemaBase):
pass
"""更新代码生成模型参数"""
class GetGenModelDetail(GenModelSchemaBase):
"""获取代码生成模型详情"""
model_config = ConfigDict(from_attributes=True)
id: int
pd_type: str
id: int = Field(description='主键 ID')
pd_type: str = Field(description='列类型对应的 pydantic 类型')
@@ -10,8 +10,16 @@ from backend.database.db import async_db_session
class GenBusinessService:
"""代码生成业务服务类"""
@staticmethod
async def get(*, pk: int) -> GenBusiness:
"""
获取指定 ID 的业务
:param pk: 业务 ID
:return:
"""
async with async_db_session() as db:
business = await gen_business_dao.get(db, pk)
if not business:
@@ -20,12 +28,18 @@ class GenBusinessService:
@staticmethod
async def get_all() -> Sequence[GenBusiness]:
"""获取所有业务"""
async with async_db_session() as db:
businesses = await gen_business_dao.get_all(db)
return businesses
return await gen_business_dao.get_all(db)
@staticmethod
async def create(*, obj: CreateGenBusinessParam) -> None:
"""
创建业务
:param obj: 创建业务参数
:return:
"""
async with async_db_session.begin() as db:
business = await gen_business_dao.get_by_name(db, obj.table_name_en)
if business:
@@ -34,15 +48,26 @@ class GenBusinessService:
@staticmethod
async def update(*, pk: int, obj: UpdateGenBusinessParam) -> int:
"""
更新业务
:param pk: 业务 ID
:param obj: 更新业务参数
:return:
"""
async with async_db_session.begin() as db:
count = await gen_business_dao.update(db, pk, obj)
return count
return await gen_business_dao.update(db, pk, obj)
@staticmethod
async def delete(*, pk: int) -> int:
"""
删除业务
:param pk: 业务 ID
:return:
"""
async with async_db_session.begin() as db:
count = await gen_business_dao.delete(db, pk)
return count
return await gen_business_dao.delete(db, pk)
gen_business_service: GenBusinessService = GenBusinessService()
@@ -12,50 +12,85 @@ from backend.utils.type_conversion import sql_type_to_pydantic
class GenModelService:
"""代码生成模型服务类"""
@staticmethod
async def get(*, pk: int) -> GenModel:
"""
获取指定 ID 的模型
:param pk: 模型 ID
:return:
"""
async with async_db_session() as db:
gen_model = await gen_model_dao.get(db, pk)
return gen_model
model = await gen_model_dao.get(db, pk)
if not model:
raise errors.NotFoundError(msg='代码生成模型不存在')
return model
@staticmethod
async def get_types() -> list[str]:
"""获取所有 MySQL 列类型"""
types = GenModelMySQLColumnType.get_member_keys()
types.sort()
return types
@staticmethod
async def get_by_business(*, business_id: int) -> Sequence[GenModel]:
"""
获取指定业务的所有模型
:param business_id: 业务 ID
:return:
"""
async with async_db_session() as db:
gen_models = await gen_model_dao.get_all_by_business_id(db, business_id)
return gen_models
return await gen_model_dao.get_all_by_business(db, business_id)
@staticmethod
async def create(*, obj: CreateGenModelParam) -> None:
"""
创建模型
:param obj: 创建模型参数
:return:
"""
async with async_db_session.begin() as db:
gen_models = await gen_model_dao.get_all_by_business_id(db, obj.gen_business_id)
gen_models = await gen_model_dao.get_all_by_business(db, obj.gen_business_id)
if obj.name in [gen_model.name for gen_model in gen_models]:
raise errors.ForbiddenError(msg='禁止添加相同列到同一模型表')
pd_type = sql_type_to_pydantic(obj.type)
await gen_model_dao.create(db, obj, pd_type=pd_type)
@staticmethod
async def update(*, pk: int, obj: UpdateGenModelParam) -> int:
"""
更新模型
:param pk: 模型 ID
:param obj: 更新模型参数
:return:
"""
async with async_db_session.begin() as db:
model = await gen_model_dao.get(db, pk)
if obj.name != model.name:
gen_models = await gen_model_dao.get_all_by_business_id(db, obj.gen_business_id)
gen_models = await gen_model_dao.get_all_by_business(db, obj.gen_business_id)
if obj.name in [gen_model.name for gen_model in gen_models]:
raise errors.ForbiddenError(msg='模型列名已存在')
pd_type = sql_type_to_pydantic(obj.type)
count = await gen_model_dao.update(db, pk, obj, pd_type=pd_type)
return count
return await gen_model_dao.update(db, pk, obj, pd_type=pd_type)
@staticmethod
async def delete(*, pk: int) -> int:
"""
删除模型
:param pk: 模型 ID
:return:
"""
async with async_db_session.begin() as db:
count = await gen_model_dao.delete(db, pk)
return count
return await gen_model_dao.delete(db, pk)
gen_model_service: GenModelService = GenModelService()
+93 -44
View File
@@ -5,7 +5,6 @@ import os.path
import zipfile
from pathlib import Path
from typing import Sequence
import aiofiles
@@ -20,27 +19,43 @@ from backend.app.generator.schema.gen_business import CreateGenBusinessParam
from backend.app.generator.schema.gen_model import CreateGenModelParam
from backend.app.generator.service.gen_model_service import gen_model_service
from backend.common.exception import errors
from backend.core.path_conf import BasePath
from backend.core.path_conf import BASE_PATH
from backend.database.db import async_db_session
from backend.utils.gen_template import gen_template
from backend.utils.type_conversion import sql_type_to_pydantic
class GenService:
"""代码生成服务类"""
@staticmethod
async def get_tables(*, table_schema: str) -> Sequence[str]:
async def get_tables(*, table_schema: str) -> list[str]:
"""
获取指定 schema 下的所有表名
:param table_schema: 数据库 schema 名称
:return:
"""
async with async_db_session() as db:
return await gen_dao.get_all_tables(db, table_schema)
@staticmethod
async def import_business_and_model(*, obj: ImportParam) -> None:
"""
导入业务和模型数据
:param obj: 导入参数对象
:return:
"""
async with async_db_session.begin() as db:
table_info = await gen_dao.get_table(db, obj.table_name)
if not table_info:
raise errors.NotFoundError(msg='数据库表不存在')
business_info = await gen_business_dao.get_by_name(db, obj.table_name)
if business_info:
raise errors.ForbiddenError(msg='已存在相同数据库表业务')
table_name = table_info[0]
business_data = {
'app_name': obj.app,
@@ -52,6 +67,7 @@ class GenService:
new_business = GenBusiness(**CreateGenBusinessParam(**business_data).model_dump())
db.add(new_business)
await db.flush()
column_info = await gen_dao.get_all_columns(db, obj.table_schema, table_name)
for column in column_info:
column_type = column[-1].split('(')[0].upper()
@@ -70,20 +86,34 @@ class GenService:
@staticmethod
async def render_tpl_code(*, business: GenBusiness) -> dict[str, str]:
"""
渲染模板代码
:param business: 业务对象
:return:
"""
gen_models = await gen_model_service.get_by_business(business_id=business.id)
if not gen_models:
raise errors.NotFoundError(msg='代码生成模型表为空')
gen_vars = gen_template.get_vars(business, gen_models)
tpl_code_map = {}
for tpl_path in gen_template.get_template_paths():
tpl_code_map[tpl_path] = await gen_template.get_template(tpl_path).render_async(**gen_vars)
return tpl_code_map
return {
tpl_path: await gen_template.get_template(tpl_path).render_async(**gen_vars)
for tpl_path in gen_template.get_template_paths()
}
async def preview(self, *, pk: int) -> dict[str, bytes]:
"""
预览生成的代码
:param pk: 业务 ID
:return:
"""
async with async_db_session() as db:
business = await gen_business_dao.get(db, pk)
if not business:
raise errors.NotFoundError(msg='业务不存在')
tpl_code_map = await self.render_tpl_code(business=business)
return {
tpl.replace('.jinja', '.py') if tpl.startswith('py') else ...: code.encode('utf-8')
@@ -92,42 +122,50 @@ class GenService:
@staticmethod
async def get_generate_path(*, pk: int) -> list[str]:
"""
获取代码生成路径
:param pk: 业务 ID
:return:
"""
async with async_db_session() as db:
business = await gen_business_dao.get(db, pk)
if not business:
raise errors.NotFoundError(msg='业务不存在')
gen_path = business.gen_path
if not gen_path:
# 伪加密路径
gen_path = 'current-backend-app-path'
gen_path = business.gen_path or 'fba-backend-app-path'
target_files = gen_template.get_code_gen_paths(business)
code_gen_paths = []
for target_file in target_files:
code_gen_paths.append(os.path.join(gen_path, *target_file.split('/')[1:]))
return code_gen_paths
return [os.path.join(gen_path, *target_file.split('/')[1:]) for target_file in target_files]
async def generate(self, *, pk: int) -> None:
"""
生成代码文件
:param pk: 业务 ID
:return:
"""
async with async_db_session() as db:
business = await gen_business_dao.get(db, pk)
if not business:
raise errors.NotFoundError(msg='业务不存在')
tpl_code_map = await self.render_tpl_code(business=business)
gen_path = business.gen_path
if not gen_path:
gen_path = os.path.join(BasePath, 'app')
gen_path = business.gen_path or os.path.join(BASE_PATH, 'app')
for tpl_path, code in tpl_code_map.items():
code_filepath = os.path.join(
gen_path,
*gen_template.get_code_gen_path(tpl_path, business).split('/')[1:],
)
code_folder = Path(str(code_filepath)).parent
if not code_folder.exists():
code_folder.mkdir(parents=True, exist_ok=True)
code_folder.mkdir(parents=True, exist_ok=True)
# 写入 init 文件
init_filepath = code_folder.joinpath('__init__.py')
if not init_filepath.exists():
async with aiofiles.open(init_filepath, 'w', encoding='utf-8') as f:
await f.write(gen_template.init_content)
if 'api' in str(code_folder):
# api __init__.py
api_init_filepath = code_folder.parent.joinpath('__init__.py')
@@ -136,12 +174,14 @@ class GenService:
await f.write(gen_template.init_content)
# app __init__.py
app_init_filepath = api_init_filepath.parent.joinpath('__init__.py')
if not app_init_filepath:
if not app_init_filepath.exists():
async with aiofiles.open(app_init_filepath, 'w', encoding='utf-8') as f:
await f.write(gen_template.init_content)
# 写入代码文件呢
# 写入代码文件
async with aiofiles.open(code_filepath, 'w', encoding='utf-8') as f:
await f.write(code)
# model init 文件补充
if code_folder.name == 'model':
async with aiofiles.open(init_filepath, 'a', encoding='utf-8') as f:
@@ -151,33 +191,42 @@ class GenService:
)
async def download(self, *, pk: int) -> io.BytesIO:
"""
下载生成的代码
:param pk: 业务 ID
:return:
"""
async with async_db_session() as db:
business = await gen_business_dao.get(db, pk)
if not business:
raise errors.NotFoundError(msg='业务不存在')
bio = io.BytesIO()
zf = zipfile.ZipFile(bio, 'w')
tpl_code_map = await self.render_tpl_code(business=business)
for tpl_path, code in tpl_code_map.items():
# 写入代码文件
new_code_path = gen_template.get_code_gen_path(tpl_path, business)
zf.writestr(new_code_path, code)
# 写入 init 文件
init_filepath = os.path.join(*new_code_path.split('/')[:-1], '__init__.py')
if 'model' not in new_code_path.split('/'):
zf.writestr(init_filepath, gen_template.init_content)
else:
zf.writestr(
init_filepath,
f'{gen_template.init_content}'
f'from backend.app.{business.app_name}.model.{business.table_name_en} '
f'import {to_pascal(business.table_name_en)}\n',
)
if 'api' in new_code_path:
# api __init__.py
api_init_filepath = os.path.join(*new_code_path.split('/')[:-2], '__init__.py')
zf.writestr(api_init_filepath, gen_template.init_content)
zf.close()
with zipfile.ZipFile(bio, 'w') as zf:
tpl_code_map = await self.render_tpl_code(business=business)
for tpl_path, code in tpl_code_map.items():
# 写入代码文件
new_code_path = gen_template.get_code_gen_path(tpl_path, business)
zf.writestr(new_code_path, code)
# 写入 init 文件
init_filepath = os.path.join(*new_code_path.split('/')[:-1], '__init__.py')
if 'model' not in new_code_path.split('/'):
zf.writestr(init_filepath, gen_template.init_content)
else:
zf.writestr(
init_filepath,
f'{gen_template.init_content}'
f'from backend.app.{business.app_name}.model.{business.table_name_en} '
f'import {to_pascal(business.table_name_en)}\n',
)
if 'api' in new_code_path:
# api __init__.py
api_init_filepath = os.path.join(*new_code_path.split('/')[:-2], '__init__.py')
zf.writestr(api_init_filepath, gen_template.init_content)
bio.seek(0)
return bio
+2 -2
View File
@@ -5,6 +5,6 @@ from fastapi import APIRouter
from backend.app.task.api.v1.task import router as task_router
from backend.core.conf import settings
v1 = APIRouter(prefix=settings.FASTAPI_API_V1_PATH)
v1 = APIRouter(prefix=settings.FASTAPI_API_V1_PATH, tags=['任务'])
v1.include_router(task_router, prefix='/tasks', tags=['任务'])
v1.include_router(task_router, prefix='/tasks')
+2 -2
View File
@@ -27,7 +27,7 @@ async def get_all_tasks() -> ResponseSchemaModel[list[str]]:
description='此接口被视为作废,建议使用 flower 查看任务详情',
dependencies=[DependsJwtAuth],
)
async def get_task_detail(tid: Annotated[str, Path(description='任务ID')]) -> ResponseSchemaModel[TaskResult]:
async def get_task_detail(tid: Annotated[str, Path(description='任务 UUID')]) -> ResponseSchemaModel[TaskResult]:
status = task_service.get_detail(tid=tid)
return response_base.success(data=status)
@@ -40,7 +40,7 @@ async def get_task_detail(tid: Annotated[str, Path(description='任务ID')]) ->
DependsRBAC,
],
)
async def revoke_task(tid: Annotated[str, Path(description='任务ID')]) -> ResponseModel:
async def revoke_task(tid: Annotated[str, Path(description='任务 UUID')]) -> ResponseModel:
task_service.revoke(tid=tid)
return response_base.success()
+40 -37
View File
@@ -1,5 +1,7 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from typing import Any
import celery
import celery_aio_pool
@@ -9,8 +11,39 @@ from backend.core.conf import settings
__all__ = ['celery_app']
def get_broker_url() -> str:
"""获取消息代理 URL"""
if task_settings.CELERY_BROKER == 'redis':
return (
f'redis://:{settings.REDIS_PASSWORD}@{settings.REDIS_HOST}:'
f'{settings.REDIS_PORT}/{task_settings.CELERY_BROKER_REDIS_DATABASE}'
)
return (
f'amqp://{task_settings.RABBITMQ_USERNAME}:{task_settings.RABBITMQ_PASSWORD}@'
f'{task_settings.RABBITMQ_HOST}:{task_settings.RABBITMQ_PORT}'
)
def get_result_backend() -> str:
"""获取结果后端 URL"""
return (
f'redis://:{settings.REDIS_PASSWORD}@{settings.REDIS_HOST}:'
f'{settings.REDIS_PORT}/{task_settings.CELERY_BACKEND_REDIS_DATABASE}'
)
def get_result_backend_transport_options() -> dict[str, Any]:
"""获取结果后端传输选项"""
return {
'global_keyprefix': task_settings.CELERY_BACKEND_REDIS_PREFIX,
'retry_policy': {
'timeout': task_settings.CELERY_BACKEND_REDIS_TIMEOUT,
},
}
def init_celery() -> celery.Celery:
"""初始化 celery 应用"""
"""初始化 Celery 应用"""
# TODO: Update this work if celery version >= 6.0.0
# https://github.com/fastapi-practices/fastapi_best_architecture/issues/321
@@ -18,54 +51,24 @@ def init_celery() -> celery.Celery:
celery.app.trace.build_tracer = celery_aio_pool.build_async_tracer
celery.app.trace.reset_worker_optimizations()
# Celery Schedule Tasks
# https://docs.celeryq.dev/en/stable/userguide/periodic-tasks.html
beat_schedule = task_settings.CELERY_SCHEDULE
# Celery Config
# https://docs.celeryq.dev/en/stable/userguide/configuration.html
broker_url = (
(
f'redis://:{settings.REDIS_PASSWORD}@{settings.REDIS_HOST}:'
f'{settings.REDIS_PORT}/{task_settings.CELERY_BROKER_REDIS_DATABASE}'
)
if task_settings.CELERY_BROKER == 'redis'
else (
f'amqp://{task_settings.RABBITMQ_USERNAME}:{task_settings.RABBITMQ_PASSWORD}@'
f'{task_settings.RABBITMQ_HOST}:{task_settings.RABBITMQ_PORT}'
)
)
result_backend = (
f'redis://:{settings.REDIS_PASSWORD}@{settings.REDIS_HOST}:'
f'{settings.REDIS_PORT}/{task_settings.CELERY_BACKEND_REDIS_DATABASE}'
)
result_backend_transport_options = {
'global_keyprefix': f'{task_settings.CELERY_BACKEND_REDIS_PREFIX}',
'retry_policy': {
'timeout': task_settings.CELERY_BACKEND_REDIS_TIMEOUT,
},
}
app = celery.Celery(
'fba_celery',
enable_utc=False,
timezone=settings.DATETIME_TIMEZONE,
beat_schedule=beat_schedule,
broker_url=broker_url,
beat_schedule=task_settings.CELERY_SCHEDULE,
broker_url=get_broker_url(),
broker_connection_retry_on_startup=True,
result_backend=result_backend,
result_backend_transport_options=result_backend_transport_options,
result_backend=get_result_backend(),
result_backend_transport_options=get_result_backend_transport_options(),
task_cls='app.task.celery_task.base:TaskBase',
task_track_started=True,
# TODO: Update this work if celery version >= 6.0.0
worker_pool=celery_aio_pool.pool.AsyncIOPool,
)
# Load task modules
# 自动发现任务
app.autodiscover_tasks(task_settings.CELERY_TASK_PACKAGES)
return app
# 创建 celery 实例
# 创建 Celery 实例
celery_app: celery.Celery = init_celery()
+26 -4
View File
@@ -1,5 +1,6 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from typing import Any
from celery import Task
from sqlalchemy.exc import SQLAlchemyError
@@ -9,16 +10,37 @@ from backend.common.socketio.actions import task_notification
class TaskBase(Task):
"""任务基类"""
"""Celery 任务基类"""
autoretry_for = (SQLAlchemyError,)
max_retries = task_settings.CELERY_TASK_MAX_RETRIES
async def before_start(self, task_id, args, kwargs):
async def before_start(self, task_id: str, args, kwargs) -> None:
"""
任务开始前执行钩子
:param task_id: 任务 ID
:return:
"""
await task_notification(msg=f'任务 {task_id} 开始执行')
async def on_success(self, retval, task_id, args, kwargs):
async def on_success(self, retval: Any, task_id: str, args, kwargs) -> None:
"""
任务成功后执行钩子
:param retval: 任务返回值
:param task_id: 任务 ID
:return:
"""
await task_notification(msg=f'任务 {task_id} 执行成功')
async def on_failure(self, exc, task_id, args, kwargs, einfo):
async def on_failure(self, exc: Exception, task_id: str, args, kwargs, einfo) -> None:
"""
任务失败后执行钩子
:param exc: 异常对象
:param task_id: 任务 ID
:param einfo: 异常信息
:return:
"""
await task_notification(msg=f'任务 {task_id} 执行失败')
+1
View File
@@ -7,5 +7,6 @@ from backend.app.task.celery import celery_app
@celery_app.task(name='task_demo_async')
async def task_demo_async() -> str:
"""异步示例任务,模拟耗时操作"""
await sleep(20)
return 'test async'
+15 -12
View File
@@ -1,35 +1,35 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from functools import lru_cache
from typing import Literal
from typing import Any, Literal
from celery.schedules import crontab
from pydantic import model_validator
from pydantic_settings import BaseSettings, SettingsConfigDict
from backend.core.path_conf import BasePath
from backend.core.path_conf import BASE_PATH
class TaskSettings(BaseSettings):
"""Task Settings"""
"""Celery 任务配置"""
model_config = SettingsConfigDict(env_file=f'{BasePath}/.env', env_file_encoding='utf-8', extra='ignore')
model_config = SettingsConfigDict(env_file=f'{BASE_PATH}/.env', env_file_encoding='utf-8', extra='ignore')
# Env Config
# .env 环境
ENVIRONMENT: Literal['dev', 'pro']
# Env Celery
CELERY_BROKER_REDIS_DATABASE: int # 仅在 dev 模式时生效
# .env Redis 配置
CELERY_BROKER_REDIS_DATABASE: int
CELERY_BACKEND_REDIS_DATABASE: int
# Env Rabbitmq
# .env RabbitMQ 配置
# docker run -d --hostname fba-mq --name fba-mq -p 5672:5672 -p 15672:15672 rabbitmq:latest
RABBITMQ_HOST: str
RABBITMQ_PORT: int
RABBITMQ_USERNAME: str
RABBITMQ_PASSWORD: str
# Celery
# Celery 基础配置
CELERY_BROKER: Literal['rabbitmq', 'redis'] = 'redis'
CELERY_BACKEND_REDIS_PREFIX: str = 'fba:celery:'
CELERY_BACKEND_REDIS_TIMEOUT: int = 5
@@ -38,7 +38,9 @@ class TaskSettings(BaseSettings):
'app.task.celery_task.db_log',
]
CELERY_TASK_MAX_RETRIES: int = 5
CELERY_SCHEDULE: dict = {
# Celery 定时任务配置
CELERY_SCHEDULE: dict[str, dict[str, Any]] = {
'exec-every-10-seconds': {
'task': 'task_demo_async',
'schedule': 10,
@@ -55,7 +57,8 @@ class TaskSettings(BaseSettings):
@model_validator(mode='before')
@classmethod
def validate_celery_broker(cls, values):
def validate_celery_broker(cls, values: Any) -> Any:
"""生产环境强制使用 RabbitMQ 作为消息代理"""
if values['ENVIRONMENT'] == 'pro':
values['CELERY_BROKER'] = 'rabbitmq'
return values
@@ -63,7 +66,7 @@ class TaskSettings(BaseSettings):
@lru_cache
def get_task_settings() -> TaskSettings:
"""获取 task 配置"""
"""获取 Celery 任务配置"""
return TaskSettings()
+17 -11
View File
@@ -1,23 +1,29 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from typing import Any
from pydantic import Field
from backend.common.schema import SchemaBase
class RunParam(SchemaBase):
"""任务运行参数"""
name: str = Field(description='任务名称')
args: list | None = Field(default=None, description='任务函数位置参数')
kwargs: dict | None = Field(default=None, description='任务函数关键字参数')
args: list[Any] | None = Field(None, description='任务函数位置参数')
kwargs: dict[str, Any] | None = Field(None, description='任务函数关键字参数')
class TaskResult(SchemaBase):
result: str
traceback: str
status: str
name: str
args: list | None
kwargs: dict | None
worker: str
retries: int | None
queue: str | None
"""任务执行结果"""
result: str = Field(description='任务执行结果')
traceback: str = Field(description='错误堆栈信息')
status: str = Field(description='任务状态')
name: str = Field(description='任务名称')
args: list[Any] | None = Field(None, description='任务函数位置参数')
kwargs: dict[str, Any] | None = Field(None, description='任务函数关键字参数')
worker: str = Field(description='执行任务的 worker')
retries: int | None = Field(None, description='重试次数')
queue: str | None = Field(None, description='任务队列')
+21 -2
View File
@@ -13,14 +13,21 @@ from backend.common.exception.errors import NotFoundError
class TaskService:
@staticmethod
async def get_list() -> list[str]:
"""获取所有已注册的 Celery 任务列表"""
registered_tasks = await run_in_threadpool(celery_app.control.inspect().registered)
if not registered_tasks:
raise errors.ForbiddenError(msg='celery 服务未启动')
raise errors.ForbiddenError(msg='Celery 服务未启动')
tasks = list(registered_tasks.values())[0]
return tasks
@staticmethod
def get_detail(*, tid: str) -> TaskResult:
"""
获取指定任务的详细信息
:param tid: 任务 UUID
:return:
"""
try:
result = AsyncResult(id=tid, app=celery_app)
except NotRegistered:
@@ -38,7 +45,13 @@ class TaskService:
)
@staticmethod
def revoke(*, tid: str):
def revoke(*, tid: str) -> None:
"""
撤销指定的任务
:param tid: 任务 UUID
:return:
"""
try:
result = AsyncResult(id=tid, app=celery_app)
except NotRegistered:
@@ -47,6 +60,12 @@ class TaskService:
@staticmethod
def run(*, obj: RunParam) -> str:
"""
运行指定的任务
:param obj: 任务运行参数
:return:
"""
task: AsyncResult = celery_app.send_task(name=obj.name, args=obj.args, kwargs=obj.kwargs)
return task.task_id
+4 -1
View File
@@ -1,7 +1,10 @@
#!/usr/bin/env bash
# work && beat
celery -A app.task.celery worker -l info -B
celery -A app.task.celery worker -l info -P gevent -c 100 &
# beat
celery -A app.task.celery beat -l info &
# flower
celery -A app.task.celery flower --port=8555 --basic-auth=admin:123456
+20 -9
View File
@@ -2,27 +2,38 @@
# -*- coding: utf-8 -*-
from enum import Enum
from enum import IntEnum as SourceIntEnum
from typing import Type
from typing import Any, Type, TypeVar
T = TypeVar('T', bound=Enum)
class _EnumBase:
"""枚举基类,提供通用方法"""
@classmethod
def get_member_keys(cls: Type[Enum]) -> list[str]:
def get_member_keys(cls: Type[T]) -> list[str]:
"""获取枚举成员名称列表"""
return [name for name in cls.__members__.keys()]
@classmethod
def get_member_values(cls: Type[Enum]) -> list:
def get_member_values(cls: Type[T]) -> list:
"""获取枚举成员值列表"""
return [item.value for item in cls.__members__.values()]
@classmethod
def get_member_dict(cls: Type[T]) -> dict[str, Any]:
"""获取枚举成员字典"""
return {name: item.value for name, item in cls.__members__.items()}
class IntEnum(_EnumBase, SourceIntEnum):
"""整型枚举"""
"""整型枚举基类"""
pass
class StrEnum(_EnumBase, str, Enum):
"""字符串枚举"""
"""字符串枚举基类"""
pass
@@ -56,7 +67,7 @@ class RoleDataRuleExpressionType(IntEnum):
class MethodType(StrEnum):
"""请求方法"""
"""HTTP 请求方法"""
GET = 'GET'
POST = 'POST'
@@ -67,7 +78,7 @@ class MethodType(StrEnum):
class LoginLogStatusType(IntEnum):
"""日志状态"""
"""日志状态"""
fail = 0
success = 1
@@ -100,7 +111,7 @@ class UserSocialType(StrEnum):
"""用户社交类型"""
github = 'GitHub'
linuxdo = 'LinuxDo'
linux_do = 'LinuxDo'
class FileType(StrEnum):
@@ -176,7 +187,7 @@ class GenModelMySQLColumnType(StrEnum):
class GenModelPostgreSQLColumnType(StrEnum):
"""代码生成模型列类型(PostgreSQL),仅作为数据保留,并未实施"""
"""代码生成模型列类型(PostgreSQL)"""
# Python 类型映射
BIGINT = 'int'

Some files were not shown because too many files have changed in this diff Show More