diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e6b8882..00c70b6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,7 +26,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install -r requirements.txt + pip install -r ./backend/requirements.txt - name: pre-commit uses: pre-commit/action@v3.0.0 diff --git a/.gitignore b/.gitignore index 690b435..66f8fb5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,12 +1,2 @@ -__pycache__/ .idea/ -.env -venv/ -.venv/ -.mypy_cache/ -backend/app/log/ -backend/app/alembic/versions/ -backend/app/static/media/ -.ruff_cache/ -.pytest_cache/ -.pdm-python +.vscode/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c7d4a06..b0fd1b3 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -13,18 +13,24 @@ repos: - id: ruff args: - '--config' - - '.ruff.toml' + - 'backend/.ruff.toml' - '--fix' - '--unsafe-fixes' - id: ruff-format - repo: https://github.com/pdm-project/pdm - rev: 2.12.2 + rev: 2.12.4 hooks: - id: pdm-export args: - - -o - - 'requirements.txt' + - '-p' + - 'backend' + - '-o' + - 'backend/requirements.txt' - '--without-hashes' - files: ^pdm.lock$ + files: backend/pdm\.lock$ - id: pdm-lock-check + args: + - '-p' + - 'backend' + files: backend/pdm\.lock$ diff --git a/README.md b/README.md index 59960a3..8c0cc3d 100644 --- a/README.md +++ b/README.md @@ -3,12 +3,15 @@ [![GitHub](https://img.shields.io/github/license/fastapi-practices/fastapi_best_architecture)](https://github.com/fastapi-practices/fastapi_best_architecture/blob/master/LICENSE) [![Static Badge](https://img.shields.io/badge/python-3.10%2B-blue)](https://www.python.org/downloads/) [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff) +[![Pydantic v2](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/pydantic/pydantic/main/docs/badge/v2.json)](https://pydantic.dev) -> [!Tip] -> **2024-3-22 (公告)** +> [!CAUTION] +> **For 2023-12-22 (announcement)** > -> You're looking at the legacy-single-app-pydantic-v2 branch, which has been locked and no longer provides any updates -> and fixes +> The master branch has completed the app architecture refactoring, please pay extra attention to sync fork operations +> to avoid irreparable damage! +> +> We have kept and locked the original branch (legacy-single-app-pydantic-v2), which you can get in the branch selector English | [简体中文](./README.zh-CN.md) @@ -83,117 +86,117 @@ Luckily, we now have a demo site: [FBA UI](https://fba.xwboy.top/) * Redis: The latest stable version is recommended * Nodejs: 14.0+ -### BackEnd +### Backend -1. Install dependencies - - ```shell - pip install -r requirements.txt - ``` - -2. Create a database `fba`, choose utf8mb4 encoding -3. Install and start Redis -4. Create a `.env` file in the `backend/app/` directory - - ```shell - cd backend/app/ - touch .env - ``` - -5. Copy `.env.example` to `.env` +1. Enter the `backend` directory ```shell + cd backend + ``` + +2. Install the dependencies + + ```shell + pip install -r requirements.txt + ``` + +3. Create a database `fba` with utf8mb4 encoding. +4. Install and start Redis +5. Create a `.env` file in the `backend` directory. + + ```shell + touch .env + cp .env.example .env ``` -6. Modify the configuration file as needed -7. Database migration [alembic](https://alembic.sqlalchemy.org/en/latest/tutorial.html) +6. Modify the configuration files `core/conf.py` and `.env` as needed. +7. database migration [alembic](https://alembic.sqlalchemy.org/en/latest/tutorial.html) ```shell - cd backend/app/ - - # Generate migration file + # Generate the migration file alembic revision --autogenerate - + # Execute the migration alembic upgrade head - ``` -8. Start celery worker and beat - - ```shell - celery -A tasks worker --loglevel=INFO - # Optional, if you don't need to use the scheduled task - celery -A tasks beat --loglevel=INFO ``` -9. Execute the `backend/app/main.py` file to start the service -10. Browser access: http://127.0.0.1:8000/api/v1/docs +8. Start celery worker, beat and flower + + ```shell + celery -A app.task.celery worker -l info + + # Scheduled tasks (optional) + celery -A app.task.celery beat -l info + + # Web monitor (optional) + celery -A app.task.celery flower --port=8555 --basic-auth=admin:123456 + ``` + +9. [Initialize test data](#test-data) (Optional) +10. Execute the `main.py` file to start the service +11. Open a browser and visit: http://127.0.0.1:8000/api/v1/docs + +### Front end + +Jump to [fastapi_best_architecture_ui](https://github.com/fastapi-practices/fastapi_best_architecture_ui) View details --- -### Front - -Go to [fastapi_best_architecture_ui](https://github.com/fastapi-practices/fastapi_best_architecture_ui) for details - -### Docker deploy +### Docker Deployment > [!WARNING] -> Default port conflict:8000,3306,6379,5672 > -> As a best practice, shut down on-premises services before deployment:mysql,redis,rabbitmq... +> Default port conflicts: 8000, 3306, 6379, 5672. +> +> It is recommended to shut down local services: mysql, redis, rabbitmq... before deployment. -1. Go to the directory where the ``docker-compose.yml`` file is located and create the environment variable - file ``.env`` +1. Go to the `deploy/backend/docker-compose` directory, and create the environment variable file `.env`. ```shell - cd deploy/docker-compose/ + cd deploy/backend/docker-compose - cp .env.server ../../backend/app/.env + touch .env.server ../../../backend/.env - # This command is optional - cp .env.docker .env + cp .env.server ../../../backend/.env ``` -2. Modify the configuration file as needed -3. Execute the one-click boot command +2. Modify the configuration files `backend/core/conf.py` and `.env` as needed. +3. Execute the one-click startup command ```shell docker-compose up -d --build ``` -4. Wait for the command to complete automatically -5. Visit the browser: http://127.0.0.1:8000/api/v1/docs +4. Wait for the command to complete. +5. Open a browser and visit: http://127.0.0.1:8000/api/v1/docs ## Test data -Initialize the test data using the `backend/sql/init_test_data.sql` file +Initialize the test data using the `backend/sql/init_test_data.sql` file. -## Development process +## Development Process (For reference only) -1. Define the database model (model) and remember to perform database migration for each change -2. Define the data validation model (schema) -3. Define routes (router) and views (api) -4. Define the business logic (service) -5. Write database operations (crud) +1. define the database model (model) +2. define the data validation model (schema) +3. define the view (api) and routing (router) +4. write business (service) +5. write database operations (crud) -## Test +## Testing -Execute unittests via pytest +Execute unit tests through `pytest`. -1. Create the test database `fba_test`, select utf8mb4 encoding -2. Using `backend/sql/create_tables.sql` file to create database tables -3. Initialize the test data using the `backend/sql/init_pytest_data.sql` file -4. Enter the app directory - - ```shell - cd backend/app/ - ``` - -5. Execute the test command +1. create a test database `fba_test` with utf8mb4 encoding +2. create database tables using the `backend/sql/create_tables.sql` file +3. initialize the test data using the `backend/sql/init_pytest_data.sql` file +4. Go to the `backend` directory and execute the test commands. ```shell + cd backend/ + pytest -vs --disable-warnings ``` @@ -203,8 +206,9 @@ Execute unittests via pytest ## Contributors - - + + + ## Special thanks @@ -226,7 +230,7 @@ beans: [:coffee: Sponsor :coffee:](https://wu-clan.github.io/sponsor/) ## License -This project is licensed under the terms of +This project is licensed by the terms of the [MIT](https://github.com/fastapi-practices/fastapi_best_architecture/blob/master/LICENSE) license [![Stargazers over time](https://starchart.cc/fastapi-practices/fastapi_best_architecture.svg?variant=adaptive)](https://starchart.cc/fastapi-practices/fastapi_best_architecture) diff --git a/README.zh-CN.md b/README.zh-CN.md index 645f582..0ee39b0 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -3,11 +3,14 @@ [![GitHub](https://img.shields.io/github/license/fastapi-practices/fastapi_best_architecture)](https://github.com/fastapi-practices/fastapi_best_architecture/blob/master/LICENSE) [![Static Badge](https://img.shields.io/badge/python-3.10%2B-blue)](https://www.python.org/downloads/) [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff) +[![Pydantic v2](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/pydantic/pydantic/main/docs/badge/v2.json)](https://pydantic.dev) -> [!Tip] +> [!CAUTION] > **2024-3-22 (公告)** > -> 你正在查看 legacy-single-app-pydantic-v2 分支,此分支已被锁定,不再提供任何更新和修复 +> 主分支已完成 app 架构重构,请格外注意 sync fork 操作,以免造成不可挽回的损失! +> +> 我们保留并锁定了原始分支(legacy-single-app-pydantic-v2),您可以在分支选择器中找到它 简体中文 | [English](./README.md) @@ -78,84 +81,88 @@ mvc 架构作为常规设计模式,在 python web 中也很常见,但是三 ### 后端 -1. 安装依赖项 - - ```shell - pip install -r requirements.txt - ``` - -2. 创建一个数据库 `fba`,选择 utf8mb4 编码 -3. 安装并启动 Redis -4. 在 `backend/app/` 目录下创建一个 `.env` 文件 - - ```shell - cd backend/app/ - touch .env - ``` - -5. 复制 `.env.example` 到 `.env` +1. 进入 `backend` 目录 ```shell + cd backend + ``` + +2. 安装依赖包 + + ```shell + pip install -r requirements.txt + ``` + +3. 创建一个数据库 `fba`,选择 utf8mb4 编码 +4. 安装并启动 Redis +5. 在 `backend` 目录下创建 `.env` 文件 + + ```shell + touch .env + cp .env.example .env ``` -6. 按需修改配置文件 +6. 按需修改配置文件 `core/conf.py` 和 `.env` 7. 数据库迁移 [alembic](https://alembic.sqlalchemy.org/en/latest/tutorial.html) ```shell - cd backend/app/ - # 生成迁移文件 alembic revision --autogenerate - + # 执行迁移 alembic upgrade head - ``` - -8. 启动 celery worker 和 beat - - ```shell - celery -A tasks worker --loglevel=INFO - # 可选,如果您不需要使用计划任务 - celery -A tasks beat --loglevel=INFO ``` -9. 执行 `backend/app/main.py` 文件启动服务 -10. 浏览器访问:http://127.0.0.1:8000/api/v1/docs +8. 启动 celery worker, beat 和 flower ---- + ```shell + celery -A app.task.celery worker -l info + + # 定时任务(可选) + celery -A app.task.celery beat -l info + + # web 监控(可选) + celery -A app.task.celery flower --port=8555 --basic-auth=admin:123456 + ``` + +9. [初始化测试数据](#测试数据)(可选) +10. 执行 `main.py` 文件启动服务 +11. 打开浏览器访问:http://127.0.0.1:8000/api/v1/docs ### 前端 跳转 [fastapi_best_architecture_ui](https://github.com/fastapi-practices/fastapi_best_architecture_ui) 查看详情 +--- + ### Docker 部署 > [!WARNING] +> > 默认端口冲突:8000,3306,6379,5672 > -> 最佳做法是在部署之前关闭本地服务:mysql,redis,rabbitmq... +> 建议在部署前关闭本地服务:mysql,redis,rabbitmq... -1. 进入 `docker-compose.yml` 文件所在目录,创建环境变量文件`.env` +1. 进入 `deploy/backend/docker-compose` 目录,创建环境变量文件`.env` ```shell - cd deploy/docker-compose/ + cd deploy/backend/docker-compose - cp .env.server ../../backend/app/.env + touch .env.server ../../../backend/.env - # 此命令为可选 - cp .env.docker .env + cp .env.server ../../../backend/.env ``` -2. 按需修改配置文件 +2. 按需修改配置文件 `backend/core/conf.py` 和 `.env` 3. 执行一键启动命令 ```shell docker-compose up -d --build ``` -4. 等待命令自动完成 -5. 浏览器访问:http://127.0.0.1:8000/api/v1/docs +4. 等待命令执行完成 +5. 打开浏览器访问:http://127.0.0.1:8000/api/v1/docs ## 测试数据 @@ -165,28 +172,24 @@ mvc 架构作为常规设计模式,在 python web 中也很常见,但是三 (仅供参考) -1. 定义数据库模型(model),每次变化记得执行数据库迁移 +1. 定义数据库模型(model) 2. 定义数据验证模型(schema) -3. 定义路由(router)和视图(api) -4. 定义业务逻辑(service) +3. 定义视图(api)和路由(router) +4. 编写业务(service) 5. 编写数据库操作(crud) ## 测试 -通过 pytest 执行单元测试 +通过 `pytest` 执行单元测试 1. 创建测试数据库 `fba_test`,选择 utf8mb4 编码 2. 使用 `backend/sql/create_tables.sql` 文件创建数据库表 3. 使用 `backend/sql/init_pytest_data.sql` 文件初始化测试数据 -4. 进入app目录 - - ```shell - cd backend/app/ - ``` - -5. 执行测试命令 +4. 进入 `backend` 目录,执行测试命令 ```shell + cd backend/ + pytest -vs --disable-warnings ``` @@ -196,8 +199,9 @@ mvc 架构作为常规设计模式,在 python web 中也很常见,但是三 ## 贡献者 - - + + + ## 特别鸣谢 @@ -218,6 +222,6 @@ mvc 架构作为常规设计模式,在 python web 中也很常见,但是三 ## 许可证 -本项目根据 [MIT](https://github.com/fastapi-practices/fastapi_best_architecture/blob/master/LICENSE) 许可证的条款进行许可 +本项目由 [MIT](https://github.com/fastapi-practices/fastapi_best_architecture/blob/master/LICENSE) 许可证的条款进行许可 [![Stargazers over time](https://starchart.cc/fastapi-practices/fastapi_best_architecture.svg?variant=adaptive)](https://starchart.cc/fastapi-practices/fastapi_best_architecture) diff --git a/backend/.dockerignore b/backend/.dockerignore new file mode 100644 index 0000000..f48827b --- /dev/null +++ b/backend/.dockerignore @@ -0,0 +1,4 @@ +__pycache__/ +venv/ +.venv/ +.pdm-python diff --git a/backend/app/.env.example b/backend/.env.example similarity index 79% rename from backend/app/.env.example rename to backend/.env.example index 0860edf..75fcf70 100644 --- a/backend/app/.env.example +++ b/backend/.env.example @@ -1,19 +1,25 @@ # Env: dev、pro ENVIRONMENT='dev' # MySQL -DB_HOST='127.0.0.1' -DB_PORT=3306 -DB_USER='root' -DB_PASSWORD='123456' +MYSQL_HOST='127.0.0.1' +MYSQL_PORT=3306 +MYSQL_USER='root' +MYSQL_PASSWORD='123456' # Redis REDIS_HOST='127.0.0.1' REDIS_PORT=6379 REDIS_PASSWORD='' REDIS_DATABASE=0 +# Token +TOKEN_SECRET_KEY='1VkVF75nsNABBjK_7-qz7GtzNy3AMvktc9TCPwKczCk' +# Opera Log +OPERA_LOG_ENCRYPT_SECRET_KEY='d77b25790a804c2b4a339dd0207941e4cefa5751935a33735bc73bb7071a005b' +# Admin +# OAuth2 +OAUTH2_GITHUB_CLIENT_ID='test' +OAUTH2_GITHUB_CLIENT_SECRET='test' +# Task # Celery -CELERY_REDIS_HOST='127.0.0.1' -CELERY_REDIS_PORT=6379 -CELERY_REDIS_PASSWORD='' CELERY_BROKER_REDIS_DATABASE=1 CELERY_BACKEND_REDIS_DATABASE=2 # Rabbitmq @@ -21,10 +27,3 @@ RABBITMQ_HOST='127.0.0.1' RABBITMQ_PORT=5672 RABBITMQ_USERNAME='guest' RABBITMQ_PASSWORD='guest' -# Token -TOKEN_SECRET_KEY='1VkVF75nsNABBjK_7-qz7GtzNy3AMvktc9TCPwKczCk' -# Opera Log -OPERA_LOG_ENCRYPT_SECRET_KEY='d77b25790a804c2b4a339dd0207941e4cefa5751935a33735bc73bb7071a005b' -# OAuth2 -OAUTH2_GITHUB_CLIENT_ID='test' -OAUTH2_GITHUB_CLIENT_SECRET='test' diff --git a/backend/.gitignore b/backend/.gitignore new file mode 100644 index 0000000..5eb36a6 --- /dev/null +++ b/backend/.gitignore @@ -0,0 +1,12 @@ +__pycache__/ +.env +venv/ +.venv/ +.mypy_cache/ +log/ +alembic/versions/ +static/media/ +.ruff_cache/ +.pytest_cache/ +.pdm-python +celerybeat-schedule.* diff --git a/.ruff.toml b/backend/.ruff.toml similarity index 58% rename from .ruff.toml rename to backend/.ruff.toml index 3152a11..4dc4e38 100644 --- a/.ruff.toml +++ b/backend/.ruff.toml @@ -1,29 +1,33 @@ line-length = 120 +unsafe-fixes = true +cache-dir = ".ruff_cache" target-version = "py310" -cache-dir = "./.ruff_cache" - -[per-file-ignores] -"backend/app/api/v1/*.py" = ["TCH"] -"backend/app/models/*.py" = ["TCH003"] -"backend/app/**/__init__.py" = ["F401"] -"backend/app/tests/*.py" = ["E402"] [lint] select = [ "E", "F", "I", + "TCH", + # W "W505", + # PT "PT018", + # SIM "SIM101", "SIM114", + # PGH "PGH004", + # PL "PLE1142", + # RUF "RUF100", - "F404", - "TCH", + # UP "UP007" ] +preview = true +ignore-init-module-imports = true +ignore = ["FURB101"] [lint.flake8-pytest-style] mark-parentheses = false @@ -38,5 +42,13 @@ ignore-variadic-names = true lines-between-types = 1 order-by-type = true +[lint.per-file-ignores] +"**/api/v1/*.py" = ["TCH"] +"**/model/*.py" = ["TCH003"] +"**/model/__init__.py" = ["F401"] +"**/tests/*.py" = ["E402"] + [format] +preview = true quote-style = "single" +docstring-code-format = true diff --git a/backend/README.md b/backend/README.md new file mode 100644 index 0000000..56cc754 --- /dev/null +++ b/backend/README.md @@ -0,0 +1,60 @@ +# FBA Project - Backend + +## Docker + +> [!IMPORTANT] +> Due to Docker context limitations, you cannot successfully build a image using a Dockerfile in the current directory + +1. Make sure you're at the root of the project +2. Run the following Docker command to build a image: + + ```shell + docker build -f backend/backend.dockerfile -t fba_backend_independent . + ``` + +3. Start decker image + + ```shell + docker run -d fba_backend_independent -p 8000:8000 --name fba_app + ``` + +## Contributing + +1. Prerequisites + + You'll need the following prerequisites: + - Any Python version between Python >= 3.10 + - virtualenv or other virtual environment tool + - git + - [PDM](https://pdm-project.org/latest/) + +2. Installation and setup + + ```shell + # Clone your fork and cd into the repo directory + git clone https://github.com//fastapi_best_architecture.git + + cd fastapi_best_architecture/backend + + # Install requirements.txt + pdm install + ``` + +3. Check out a new branch and make your changes + + ```shell + # Checkout a new branch and make your changes + git checkout -b your-new-feature-branch + # Make your changes... + ``` + +4. Run linting + + ```shell + # Run automated code formatting and linting + pdm lint + ``` + +5. Commit and push your changes + + Commit your changes, push your branch to GitHub, and create a pull request. diff --git a/backend/app/api/__init__.py b/backend/__init__.py similarity index 100% rename from backend/app/api/__init__.py rename to backend/__init__.py diff --git a/backend/app/alembic.ini b/backend/alembic.ini similarity index 100% rename from backend/app/alembic.ini rename to backend/alembic.ini diff --git a/backend/app/alembic/README b/backend/alembic/README similarity index 100% rename from backend/app/alembic/README rename to backend/alembic/README diff --git a/backend/app/alembic/env.py b/backend/alembic/env.py similarity index 89% rename from backend/app/alembic/env.py rename to backend/alembic/env.py index e3a5360..7aee02d 100644 --- a/backend/app/alembic/env.py +++ b/backend/alembic/env.py @@ -9,12 +9,12 @@ from alembic import context from sqlalchemy import engine_from_config, pool from sqlalchemy.ext.asyncio import AsyncEngine -sys.path.append('../../') +sys.path.append('../') -from backend.app.core import path_conf # noqa: E402 +from backend.core import path_conf -if not os.path.exists(path_conf.Versions): - os.makedirs(path_conf.Versions) +if not os.path.exists(path_conf.ALEMBIC_Versions_DIR): + os.makedirs(path_conf.ALEMBIC_Versions_DIR) # this is the Alembic Config object, which provides # access to the values within the .ini file in use. @@ -26,12 +26,12 @@ fileConfig(config.config_file_name) # add your model's MetaData object here # for 'autogenerate' support -from backend.app.models import MappedBase # noqa: E402 +from backend.common.msd.model import MappedBase # noqa: E402 target_metadata = MappedBase.metadata # other values from the config, defined by the needs of env.py, -from backend.app.database.db_mysql import SQLALCHEMY_DATABASE_URL # noqa: E402 +from backend.database.db_mysql import SQLALCHEMY_DATABASE_URL # noqa: E402 config.set_main_option('sqlalchemy.url', SQLALCHEMY_DATABASE_URL) diff --git a/backend/app/alembic/script.py.mako b/backend/alembic/script.py.mako similarity index 100% rename from backend/app/alembic/script.py.mako rename to backend/alembic/script.py.mako diff --git a/backend/app/api/v1/__init__.py b/backend/app/admin/__init__.py similarity index 100% rename from backend/app/api/v1/__init__.py rename to backend/app/admin/__init__.py diff --git a/backend/app/common/__init__.py b/backend/app/admin/api/__init__.py similarity index 100% rename from backend/app/common/__init__.py rename to backend/app/admin/api/__init__.py diff --git a/backend/app/admin/api/router.py b/backend/app/admin/api/router.py new file mode 100644 index 0000000..b832a47 --- /dev/null +++ b/backend/app/admin/api/router.py @@ -0,0 +1,15 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +from fastapi import APIRouter + +from backend.app.admin.api.v1.auth import router as auth_router +from backend.app.admin.api.v1.log import router as log_router +from backend.app.admin.api.v1.monitor import router as monitor_router +from backend.app.admin.api.v1.sys import router as sys_router + +v1 = APIRouter() + +v1.include_router(auth_router) +v1.include_router(sys_router) +v1.include_router(log_router) +v1.include_router(monitor_router) diff --git a/backend/app/common/exception/__init__.py b/backend/app/admin/api/v1/__init__.py similarity index 100% rename from backend/app/common/exception/__init__.py rename to backend/app/admin/api/v1/__init__.py diff --git a/backend/app/admin/api/v1/auth/__init__.py b/backend/app/admin/api/v1/auth/__init__.py new file mode 100644 index 0000000..2bd73d8 --- /dev/null +++ b/backend/app/admin/api/v1/auth/__init__.py @@ -0,0 +1,13 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +from fastapi import APIRouter + +from backend.app.admin.api.v1.auth.auth import router as auth_router +from backend.app.admin.api.v1.auth.captcha import router as captcha_router +from backend.app.admin.api.v1.auth.github import router as github_router + +router = APIRouter(prefix='/auth', tags=['授权管理']) + +router.include_router(auth_router) +router.include_router(captcha_router, prefix='/captcha') +router.include_router(github_router, prefix='/github') diff --git a/backend/app/api/v1/auth/auth.py b/backend/app/admin/api/v1/auth/auth.py similarity index 76% rename from backend/app/api/v1/auth/auth.py rename to backend/app/admin/api/v1/auth/auth.py index 02aef04..59f2940 100644 --- a/backend/app/api/v1/auth/auth.py +++ b/backend/app/admin/api/v1/auth/auth.py @@ -7,17 +7,17 @@ from fastapi.security import HTTPBasicCredentials from fastapi_limiter.depends import RateLimiter from starlette.background import BackgroundTasks -from backend.app.common.jwt import DependsJwtAuth -from backend.app.common.response.response_schema import ResponseModel, response_base -from backend.app.schemas.token import GetSwaggerToken -from backend.app.schemas.user import AuthLoginParam -from backend.app.services.auth_service import auth_service +from backend.app.admin.schema.token import GetSwaggerToken +from backend.app.admin.schema.user import AuthLoginParam +from backend.app.admin.service.auth_service import auth_service +from backend.common.response.response_schema import ResponseModel, response_base +from backend.common.security.jwt import DependsJwtAuth router = APIRouter() @router.post('/login/swagger', summary='swagger 调试专用', description='用于快捷获取 token 进行 swagger 认证') -async def swagger_user_login(obj: Annotated[HTTPBasicCredentials, Depends()]) -> GetSwaggerToken: +async def swagger_login(obj: Annotated[HTTPBasicCredentials, Depends()]) -> GetSwaggerToken: token, user = await auth_service.swagger_login(obj=obj) return GetSwaggerToken(access_token=token, user=user) # type: ignore @@ -33,7 +33,7 @@ async def user_login(request: Request, obj: AuthLoginParam, background_tasks: Ba return await response_base.success(data=data) -@router.post('/new_token', summary='创建新 token', dependencies=[DependsJwtAuth]) +@router.post('/token/new', summary='创建新 token', dependencies=[DependsJwtAuth]) async def create_new_token(request: Request, refresh_token: Annotated[str, Query(...)]) -> ResponseModel: data = await auth_service.new_token(request=request, refresh_token=refresh_token) return await response_base.success(data=data) diff --git a/backend/app/api/v1/auth/captcha.py b/backend/app/admin/api/v1/auth/captcha.py similarity index 73% rename from backend/app/api/v1/auth/captcha.py rename to backend/app/admin/api/v1/auth/captcha.py index 267518e..9197a37 100644 --- a/backend/app/api/v1/auth/captcha.py +++ b/backend/app/admin/api/v1/auth/captcha.py @@ -5,15 +5,15 @@ from fastapi import APIRouter, Depends, Request from fastapi_limiter.depends import RateLimiter from starlette.concurrency import run_in_threadpool -from backend.app.common.redis import redis_client -from backend.app.common.response.response_schema import ResponseModel, response_base -from backend.app.core.conf import settings +from backend.app.admin.conf import admin_settings +from backend.common.response.response_schema import ResponseModel, response_base +from backend.database.db_redis import redis_client router = APIRouter() @router.get( - '/captcha', + '', summary='获取登录验证码', dependencies=[Depends(RateLimiter(times=5, seconds=10))], ) @@ -25,6 +25,6 @@ async def get_captcha(request: Request) -> ResponseModel: img, code = await run_in_threadpool(img_captcha, img_byte=img_type) ip = request.state.ip await redis_client.set( - f'{settings.CAPTCHA_LOGIN_REDIS_PREFIX}:{ip}', code, ex=settings.CAPTCHA_LOGIN_EXPIRE_SECONDS + f'{admin_settings.CAPTCHA_LOGIN_REDIS_PREFIX}:{ip}', code, ex=admin_settings.CAPTCHA_LOGIN_EXPIRE_SECONDS ) return await response_base.success(data={'image_type': img_type, 'image': img}) diff --git a/backend/app/api/v1/auth/github.py b/backend/app/admin/api/v1/auth/github.py similarity index 53% rename from backend/app/api/v1/auth/github.py rename to backend/app/admin/api/v1/auth/github.py index 851641e..0e4c856 100644 --- a/backend/app/api/v1/auth/github.py +++ b/backend/app/admin/api/v1/auth/github.py @@ -3,33 +3,33 @@ from fastapi import APIRouter, BackgroundTasks, Depends, Request from fastapi_oauth20 import FastAPIOAuth20, GitHubOAuth20 -from backend.app.common.response.response_schema import ResponseModel, response_base -from backend.app.core.conf import settings -from backend.app.services.github_service import github_service +from backend.app.admin.conf import admin_settings +from backend.app.admin.service.github_service import github_service +from backend.common.response.response_schema import ResponseModel, response_base router = APIRouter() -github_client = GitHubOAuth20(settings.OAUTH2_GITHUB_CLIENT_ID, settings.OAUTH2_GITHUB_CLIENT_SECRET) -github_oauth2 = FastAPIOAuth20(github_client, settings.OAUTH2_GITHUB_REDIRECT_URI) +github_client = GitHubOAuth20(admin_settings.OAUTH2_GITHUB_CLIENT_ID, admin_settings.OAUTH2_GITHUB_CLIENT_SECRET) +github_oauth2 = FastAPIOAuth20(github_client, admin_settings.OAUTH2_GITHUB_REDIRECT_URI) -@router.get('/github', summary='获取 Github 授权链接') -async def auth_github() -> ResponseModel: - auth_url = await github_client.get_authorization_url(redirect_uri=settings.OAUTH2_GITHUB_REDIRECT_URI) +@router.get('', summary='获取 Github 授权链接') +async def github_auth2() -> ResponseModel: + auth_url = await github_client.get_authorization_url(redirect_uri=admin_settings.OAUTH2_GITHUB_REDIRECT_URI) return await response_base.success(data=auth_url) @router.get( - '/github/callback', + '/callback', summary='Github 授权重定向', description='Github 授权后,自动重定向到当前地址并获取用户信息,通过用户信息自动创建系统用户', response_model=None, ) -async def login_github( +async def github_login( request: Request, background_tasks: BackgroundTasks, oauth: FastAPIOAuth20 = Depends(github_oauth2) ) -> ResponseModel: - token, state = oauth + token, _state = oauth access_token = token['access_token'] user = await github_client.get_userinfo(access_token) - data = await github_service.add_with_login(request, background_tasks, user) + data = await github_service.create_with_login(request, background_tasks, user) return await response_base.success(data=data) diff --git a/backend/app/api/v1/log/__init__.py b/backend/app/admin/api/v1/log/__init__.py similarity index 65% rename from backend/app/api/v1/log/__init__.py rename to backend/app/admin/api/v1/log/__init__.py index 315ba11..e3ee952 100644 --- a/backend/app/api/v1/log/__init__.py +++ b/backend/app/admin/api/v1/log/__init__.py @@ -2,8 +2,8 @@ # -*- coding: utf-8 -*- from fastapi import APIRouter -from backend.app.api.v1.log.login_log import router as login_log -from backend.app.api.v1.log.opera_log import router as opera_log +from backend.app.admin.api.v1.log.login_log import router as login_log +from backend.app.admin.api.v1.log.opera_log import router as opera_log router = APIRouter(prefix='/logs') diff --git a/backend/app/api/v1/log/login_log.py b/backend/app/admin/api/v1/log/login_log.py similarity index 74% rename from backend/app/api/v1/log/login_log.py rename to backend/app/admin/api/v1/log/login_log.py index 4ec104f..db5393d 100644 --- a/backend/app/api/v1/log/login_log.py +++ b/backend/app/admin/api/v1/log/login_log.py @@ -4,14 +4,14 @@ from typing import Annotated from fastapi import APIRouter, Depends, Query -from backend.app.common.jwt import DependsJwtAuth -from backend.app.common.pagination import DependsPagination, paging_data -from backend.app.common.permission import RequestPermission -from backend.app.common.rbac import DependsRBAC -from backend.app.common.response.response_schema import ResponseModel, response_base -from backend.app.database.db_mysql import CurrentSession -from backend.app.schemas.login_log import GetLoginLogListDetails -from backend.app.services.login_log_service import login_log_service +from backend.app.admin.schema.login_log import GetLoginLogListDetails +from backend.app.admin.service.login_log_service import login_log_service +from backend.common.pagination import DependsPagination, paging_data +from backend.common.response.response_schema import ResponseModel, response_base +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_mysql import CurrentSession router = APIRouter() diff --git a/backend/app/api/v1/log/opera_log.py b/backend/app/admin/api/v1/log/opera_log.py similarity index 74% rename from backend/app/api/v1/log/opera_log.py rename to backend/app/admin/api/v1/log/opera_log.py index c2866c6..42b8805 100644 --- a/backend/app/api/v1/log/opera_log.py +++ b/backend/app/admin/api/v1/log/opera_log.py @@ -4,14 +4,14 @@ from typing import Annotated from fastapi import APIRouter, Depends, Query -from backend.app.common.jwt import DependsJwtAuth -from backend.app.common.pagination import DependsPagination, paging_data -from backend.app.common.permission import RequestPermission -from backend.app.common.rbac import DependsRBAC -from backend.app.common.response.response_schema import ResponseModel, response_base -from backend.app.database.db_mysql import CurrentSession -from backend.app.schemas.opera_log import GetOperaLogListDetails -from backend.app.services.opera_log_service import opera_log_service +from backend.app.admin.schema.opera_log import GetOperaLogListDetails +from backend.app.admin.service.opera_log_service import opera_log_service +from backend.common.pagination import DependsPagination, paging_data +from backend.common.response.response_schema import ResponseModel, response_base +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_mysql import CurrentSession router = APIRouter() diff --git a/backend/app/admin/api/v1/monitor/__init__.py b/backend/app/admin/api/v1/monitor/__init__.py new file mode 100644 index 0000000..119bdb0 --- /dev/null +++ b/backend/app/admin/api/v1/monitor/__init__.py @@ -0,0 +1,11 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +from fastapi import APIRouter + +from backend.app.admin.api.v1.monitor.redis import router as redis_router +from backend.app.admin.api.v1.monitor.server import router as server_router + +router = APIRouter(prefix='/monitors', tags=['监控管理']) + +router.include_router(redis_router, prefix='/redis') +router.include_router(server_router, prefix='/server') diff --git a/backend/app/api/v1/monitor/redis.py b/backend/app/admin/api/v1/monitor/redis.py similarity index 62% rename from backend/app/api/v1/monitor/redis.py rename to backend/app/admin/api/v1/monitor/redis.py index 6826a6e..5fb861e 100644 --- a/backend/app/api/v1/monitor/redis.py +++ b/backend/app/admin/api/v1/monitor/redis.py @@ -2,16 +2,16 @@ # -*- coding: utf-8 -*- from fastapi import APIRouter, Depends -from backend.app.common.jwt import DependsJwtAuth -from backend.app.common.permission import RequestPermission -from backend.app.common.response.response_schema import ResponseModel, response_base -from backend.app.utils.redis_info import redis_info +from backend.common.response.response_schema import ResponseModel, response_base +from backend.common.security.jwt import DependsJwtAuth +from backend.common.security.permission import RequestPermission +from backend.utils.redis_info import redis_info router = APIRouter() @router.get( - '/redis', + '', summary='redis 监控', dependencies=[ Depends(RequestPermission('sys:monitor:redis')), diff --git a/backend/app/api/v1/monitor/server.py b/backend/app/admin/api/v1/monitor/server.py similarity index 76% rename from backend/app/api/v1/monitor/server.py rename to backend/app/admin/api/v1/monitor/server.py index 8b4c16b..e9f9534 100644 --- a/backend/app/api/v1/monitor/server.py +++ b/backend/app/admin/api/v1/monitor/server.py @@ -3,16 +3,16 @@ from fastapi import APIRouter, Depends from starlette.concurrency import run_in_threadpool -from backend.app.common.jwt import DependsJwtAuth -from backend.app.common.permission import RequestPermission -from backend.app.common.response.response_schema import ResponseModel, response_base -from backend.app.utils.server_info import server_info +from backend.common.response.response_schema import ResponseModel, response_base +from backend.common.security.jwt import DependsJwtAuth +from backend.common.security.permission import RequestPermission +from backend.utils.server_info import server_info router = APIRouter() @router.get( - '/server', + '', summary='server 监控', dependencies=[ Depends(RequestPermission('sys:monitor:server')), diff --git a/backend/app/api/v1/sys/__init__.py b/backend/app/admin/api/v1/sys/__init__.py similarity index 57% rename from backend/app/api/v1/sys/__init__.py rename to backend/app/admin/api/v1/sys/__init__.py index 939f1d0..9ec2112 100644 --- a/backend/app/api/v1/sys/__init__.py +++ b/backend/app/admin/api/v1/sys/__init__.py @@ -2,14 +2,14 @@ # -*- coding: utf-8 -*- from fastapi import APIRouter -from backend.app.api.v1.sys.api import router as api_router -from backend.app.api.v1.sys.casbin import router as casbin_router -from backend.app.api.v1.sys.dept import router as dept_router -from backend.app.api.v1.sys.dict_data import router as dict_data_router -from backend.app.api.v1.sys.dict_type import router as dict_type_router -from backend.app.api.v1.sys.menu import router as menu_router -from backend.app.api.v1.sys.role import router as role_router -from backend.app.api.v1.sys.user import router as user_router +from backend.app.admin.api.v1.sys.api import router as api_router +from backend.app.admin.api.v1.sys.casbin import router as casbin_router +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.role import router as role_router +from backend.app.admin.api.v1.sys.user import router as user_router router = APIRouter(prefix='/sys') diff --git a/backend/app/api/v1/sys/api.py b/backend/app/admin/api/v1/sys/api.py similarity index 79% rename from backend/app/api/v1/sys/api.py rename to backend/app/admin/api/v1/sys/api.py index 968666d..556f49a 100644 --- a/backend/app/api/v1/sys/api.py +++ b/backend/app/admin/api/v1/sys/api.py @@ -4,21 +4,21 @@ from typing import Annotated from fastapi import APIRouter, Depends, Path, Query -from backend.app.common.jwt import DependsJwtAuth -from backend.app.common.pagination import DependsPagination, paging_data -from backend.app.common.permission import RequestPermission -from backend.app.common.rbac import DependsRBAC -from backend.app.common.response.response_schema import ResponseModel, response_base -from backend.app.database.db_mysql import CurrentSession -from backend.app.schemas.api import CreateApiParam, GetApiListDetails, UpdateApiParam -from backend.app.services.api_service import api_service +from backend.app.admin.schema.api import CreateApiParam, GetApiListDetails, UpdateApiParam +from backend.app.admin.service.api_service import api_service +from backend.common.pagination import DependsPagination, paging_data +from backend.common.response.response_schema import ResponseModel, response_base +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_mysql import CurrentSession router = APIRouter() @router.get('/all', summary='获取所有接口', dependencies=[DependsJwtAuth]) async def get_all_apis() -> ResponseModel: - data = await api_service.get_api_list() + data = await api_service.get_all() return await response_base.success(data=data) diff --git a/backend/app/api/v1/sys/casbin.py b/backend/app/admin/api/v1/sys/casbin.py similarity index 77% rename from backend/app/api/v1/sys/casbin.py rename to backend/app/admin/api/v1/sys/casbin.py index 120863b..691335a 100644 --- a/backend/app/api/v1/sys/casbin.py +++ b/backend/app/admin/api/v1/sys/casbin.py @@ -5,13 +5,7 @@ from uuid import UUID from fastapi import APIRouter, Depends, Query -from backend.app.common.jwt import DependsJwtAuth -from backend.app.common.pagination import DependsPagination, paging_data -from backend.app.common.permission import RequestPermission -from backend.app.common.rbac import DependsRBAC -from backend.app.common.response.response_schema import ResponseModel, response_base -from backend.app.database.db_mysql import CurrentSession -from backend.app.schemas.casbin_rule import ( +from backend.app.admin.schema.casbin_rule import ( CreatePolicyParam, CreateUserRoleParam, DeleteAllPoliciesParam, @@ -20,14 +14,20 @@ from backend.app.schemas.casbin_rule import ( GetPolicyListDetails, UpdatePolicyParam, ) -from backend.app.services.casbin_service import casbin_service +from backend.app.admin.service.casbin_service import casbin_service +from backend.common.pagination import DependsPagination, paging_data +from backend.common.response.response_schema import ResponseModel, response_base +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_mysql import CurrentSession router = APIRouter() @router.get( '', - summary='(模糊条件)分页获取所有权限规则', + summary='(模糊条件)分页获取所有权限策略', dependencies=[ DependsJwtAuth, DependsPagination, @@ -35,7 +35,7 @@ router = APIRouter() ) async def get_pagination_casbin( db: CurrentSession, - ptype: Annotated[str | None, Query(description='规则类型, p / g')] = None, + ptype: Annotated[str | None, Query(description='策略类型, p / g')] = None, sub: Annotated[str | None, Query(description='用户 uuid / 角色')] = None, ) -> ResponseModel: casbin_select = await casbin_service.get_casbin_list(ptype=ptype, sub=sub) @@ -43,7 +43,7 @@ async def get_pagination_casbin( return await response_base.success(data=page_data) -@router.get('/policies', summary='获取所有P权限规则', dependencies=[DependsJwtAuth]) +@router.get('/policies', summary='获取所有P权限策略', dependencies=[DependsJwtAuth]) async def get_all_policies(role: Annotated[int | None, Query(description='角色ID')] = None) -> ResponseModel: policies = await casbin_service.get_policy_list(role=role) return await response_base.success(data=policies) @@ -51,7 +51,7 @@ async def get_all_policies(role: Annotated[int | None, Query(description='角色 @router.post( '/policy', - summary='添加P权限规则', + summary='添加P权限策略', dependencies=[ Depends(RequestPermission('casbin:p:add')), DependsRBAC, @@ -59,12 +59,12 @@ async def get_all_policies(role: Annotated[int | None, Query(description='角色 ) async def create_policy(p: CreatePolicyParam) -> ResponseModel: """ - p 规则: + p 策略: - - 推荐添加基于角色的访问权限, 需配合添加 g 规则才能真正拥有访问权限,适合配置全局接口访问策略
+ - 推荐添加基于角色的访问权限, 需配合添加 g 策略才能真正拥有访问权限,适合配置全局接口访问策略
**格式**: 角色 role + 访问路径 path + 访问方法 method - - 如果添加基于用户的访问权限, 不需配合添加 g 规则就能真正拥有权限,适合配置指定用户接口访问策略
+ - 如果添加基于用户的访问权限, 不需配合添加 g 策略就能真正拥有权限,适合配置指定用户接口访问策略
**格式**: 用户 uuid + 访问路径 path + 访问方法 method """ data = await casbin_service.create_policy(p=p) @@ -73,7 +73,7 @@ async def create_policy(p: CreatePolicyParam) -> ResponseModel: @router.post( '/policies', - summary='添加多组P权限规则', + summary='添加多组P权限策略', dependencies=[ Depends(RequestPermission('casbin:p:group:add')), DependsRBAC, @@ -86,7 +86,7 @@ async def create_policies(ps: list[CreatePolicyParam]) -> ResponseModel: @router.put( '/policy', - summary='更新P权限规则', + summary='更新P权限策略', dependencies=[ Depends(RequestPermission('casbin:p:edit')), DependsRBAC, @@ -99,7 +99,7 @@ async def update_policy(old: UpdatePolicyParam, new: UpdatePolicyParam) -> Respo @router.put( '/policies', - summary='更新多组P权限规则', + summary='更新多组P权限策略', dependencies=[ Depends(RequestPermission('casbin:p:group:edit')), DependsRBAC, @@ -112,7 +112,7 @@ async def update_policies(old: list[UpdatePolicyParam], new: list[UpdatePolicyPa @router.delete( '/policy', - summary='删除P权限规则', + summary='删除P权限策略', dependencies=[ Depends(RequestPermission('casbin:p:del')), DependsRBAC, @@ -125,7 +125,7 @@ async def delete_policy(p: DeletePolicyParam) -> ResponseModel: @router.delete( '/policies', - summary='删除多组P权限规则', + summary='删除多组P权限策略', dependencies=[ Depends(RequestPermission('casbin:p:group:del')), DependsRBAC, @@ -138,7 +138,7 @@ async def delete_policies(ps: list[DeletePolicyParam]) -> ResponseModel: @router.delete( '/policies/all', - summary='删除所有P权限规则', + summary='删除所有P权限策略', dependencies=[ Depends(RequestPermission('casbin:p:empty')), DependsRBAC, @@ -151,7 +151,7 @@ async def delete_all_policies(sub: DeleteAllPoliciesParam) -> ResponseModel: return await response_base.fail() -@router.get('/groups', summary='获取所有G权限规则', dependencies=[DependsJwtAuth]) +@router.get('/groups', summary='获取所有G权限策略', dependencies=[DependsJwtAuth]) async def get_all_groups() -> ResponseModel: data = await casbin_service.get_group_list() return await response_base.success(data=data) @@ -159,7 +159,7 @@ async def get_all_groups() -> ResponseModel: @router.post( '/group', - summary='添加G权限规则', + summary='添加G权限策略', dependencies=[ Depends(RequestPermission('casbin:g:add')), DependsRBAC, @@ -167,13 +167,13 @@ async def get_all_groups() -> ResponseModel: ) async def create_group(g: CreateUserRoleParam) -> ResponseModel: """ - g 规则 (**依赖 p 规则**): + g 策略 (**依赖 p 策略**): - - 如果在 p 规则中添加了基于角色的访问权限, 则还需要在 g 规则中添加基于用户组的访问权限, 才能真正拥有访问权限
+ - 如果在 p 策略中添加了基于角色的访问权限, 则还需要在 g 策略中添加基于用户组的访问权限, 才能真正拥有访问权限
**格式**: 用户 uuid + 角色 role - - 如果在 p 策略中添加了基于用户的访问权限, 则不添加相应的 g 规则能直接拥有访问权限
- 但是拥有的不是用户角色的所有权限, 而只是单一的对应的 p 规则所添加的访问权限 + - 如果在 p 策略中添加了基于用户的访问权限, 则不添加相应的 g 策略能直接拥有访问权限
+ 但是拥有的不是用户角色的所有权限, 而只是单一的对应的 p 策略所添加的访问权限 """ data = await casbin_service.create_group(g=g) return await response_base.success(data=data) @@ -181,7 +181,7 @@ async def create_group(g: CreateUserRoleParam) -> ResponseModel: @router.post( '/groups', - summary='添加多组G权限规则', + summary='添加多组G权限策略', dependencies=[ Depends(RequestPermission('casbin:g:group:add')), DependsRBAC, @@ -194,7 +194,7 @@ async def create_groups(gs: list[CreateUserRoleParam]) -> ResponseModel: @router.delete( '/group', - summary='删除G权限规则', + summary='删除G权限策略', dependencies=[ Depends(RequestPermission('casbin:g:del')), DependsRBAC, @@ -207,7 +207,7 @@ async def delete_group(g: DeleteUserRoleParam) -> ResponseModel: @router.delete( '/groups', - summary='删除多组G权限规则', + summary='删除多组G权限策略', dependencies=[ Depends(RequestPermission('casbin:g:group:del')), DependsRBAC, @@ -220,7 +220,7 @@ async def delete_groups(gs: list[DeleteUserRoleParam]) -> ResponseModel: @router.delete( '/groups/all', - summary='删除所有G权限规则', + summary='删除所有G权限策略', dependencies=[ Depends(RequestPermission('casbin:g:empty')), DependsRBAC, diff --git a/backend/app/api/v1/sys/dept.py b/backend/app/admin/api/v1/sys/dept.py similarity index 81% rename from backend/app/api/v1/sys/dept.py rename to backend/app/admin/api/v1/sys/dept.py index d9d1af2..614f615 100644 --- a/backend/app/api/v1/sys/dept.py +++ b/backend/app/admin/api/v1/sys/dept.py @@ -4,13 +4,13 @@ from typing import Annotated from fastapi import APIRouter, Depends, Path, Query -from backend.app.common.jwt import DependsJwtAuth -from backend.app.common.permission import RequestPermission -from backend.app.common.rbac import DependsRBAC -from backend.app.common.response.response_schema import ResponseModel, response_base -from backend.app.schemas.dept import CreateDeptParam, GetDeptListDetails, UpdateDeptParam -from backend.app.services.dept_service import dept_service -from backend.app.utils.serializers import select_as_dict +from backend.app.admin.schema.dept import CreateDeptParam, GetDeptListDetails, UpdateDeptParam +from backend.app.admin.service.dept_service import dept_service +from backend.common.response.response_schema import ResponseModel, response_base +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() diff --git a/backend/app/api/v1/sys/dict_data.py b/backend/app/admin/api/v1/sys/dict_data.py similarity index 78% rename from backend/app/api/v1/sys/dict_data.py rename to backend/app/admin/api/v1/sys/dict_data.py index 09ca0fb..0e00e48 100644 --- a/backend/app/api/v1/sys/dict_data.py +++ b/backend/app/admin/api/v1/sys/dict_data.py @@ -4,15 +4,15 @@ from typing import Annotated from fastapi import APIRouter, Depends, Path, Query -from backend.app.common.jwt import DependsJwtAuth -from backend.app.common.pagination import DependsPagination, paging_data -from backend.app.common.permission import RequestPermission -from backend.app.common.rbac import DependsRBAC -from backend.app.common.response.response_schema import ResponseModel, response_base -from backend.app.database.db_mysql import CurrentSession -from backend.app.schemas.dict_data import CreateDictDataParam, GetDictDataListDetails, UpdateDictDataParam -from backend.app.services.dict_data_service import dict_data_service -from backend.app.utils.serializers import select_as_dict +from backend.app.admin.schema.dict_data import CreateDictDataParam, GetDictDataListDetails, UpdateDictDataParam +from backend.app.admin.service.dict_data_service import dict_data_service +from backend.common.pagination import DependsPagination, paging_data +from backend.common.response.response_schema import ResponseModel, response_base +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_mysql import CurrentSession +from backend.utils.serializers import select_as_dict router = APIRouter() diff --git a/backend/app/api/v1/sys/dict_type.py b/backend/app/admin/api/v1/sys/dict_type.py similarity index 77% rename from backend/app/api/v1/sys/dict_type.py rename to backend/app/admin/api/v1/sys/dict_type.py index ca239a5..5ddc666 100644 --- a/backend/app/api/v1/sys/dict_type.py +++ b/backend/app/admin/api/v1/sys/dict_type.py @@ -4,14 +4,14 @@ from typing import Annotated from fastapi import APIRouter, Depends, Path, Query -from backend.app.common.jwt import DependsJwtAuth -from backend.app.common.pagination import DependsPagination, paging_data -from backend.app.common.permission import RequestPermission -from backend.app.common.rbac import DependsRBAC -from backend.app.common.response.response_schema import ResponseModel, response_base -from backend.app.database.db_mysql import CurrentSession -from backend.app.schemas.dict_type import CreateDictTypeParam, GetDictTypeListDetails, UpdateDictTypeParam -from backend.app.services.dict_type_service import dict_type_service +from backend.app.admin.schema.dict_type import CreateDictTypeParam, GetDictTypeListDetails, UpdateDictTypeParam +from backend.app.admin.service.dict_type_service import dict_type_service +from backend.common.pagination import DependsPagination, paging_data +from backend.common.response.response_schema import ResponseModel, response_base +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_mysql import CurrentSession router = APIRouter() diff --git a/backend/app/api/v1/sys/menu.py b/backend/app/admin/api/v1/sys/menu.py similarity index 79% rename from backend/app/api/v1/sys/menu.py rename to backend/app/admin/api/v1/sys/menu.py index 449076c..dbb7a91 100644 --- a/backend/app/api/v1/sys/menu.py +++ b/backend/app/admin/api/v1/sys/menu.py @@ -4,19 +4,19 @@ from typing import Annotated from fastapi import APIRouter, Depends, Path, Query, Request -from backend.app.common.jwt import DependsJwtAuth -from backend.app.common.permission import RequestPermission -from backend.app.common.rbac import DependsRBAC -from backend.app.common.response.response_schema import ResponseModel, response_base -from backend.app.schemas.menu import CreateMenuParam, GetMenuListDetails, UpdateMenuParam -from backend.app.services.menu_service import menu_service -from backend.app.utils.serializers import select_as_dict +from backend.app.admin.schema.menu import CreateMenuParam, GetMenuListDetails, UpdateMenuParam +from backend.app.admin.service.menu_service import menu_service +from backend.common.response.response_schema import ResponseModel, response_base +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_menus(request: Request) -> ResponseModel: +async def get_user_sidebar_tree(request: Request) -> ResponseModel: menu = await menu_service.get_user_menu_tree(request=request) return await response_base.success(data=menu) diff --git a/backend/app/api/v1/sys/role.py b/backend/app/admin/api/v1/sys/role.py similarity index 83% rename from backend/app/api/v1/sys/role.py rename to backend/app/admin/api/v1/sys/role.py index 9504cdc..f7c7366 100644 --- a/backend/app/api/v1/sys/role.py +++ b/backend/app/admin/api/v1/sys/role.py @@ -4,16 +4,16 @@ from typing import Annotated from fastapi import APIRouter, Depends, Path, Query, Request -from backend.app.common.jwt import DependsJwtAuth -from backend.app.common.pagination import DependsPagination, paging_data -from backend.app.common.permission import RequestPermission -from backend.app.common.rbac import DependsRBAC -from backend.app.common.response.response_schema import ResponseModel, response_base -from backend.app.database.db_mysql import CurrentSession -from backend.app.schemas.role import CreateRoleParam, GetRoleListDetails, UpdateRoleMenuParam, UpdateRoleParam -from backend.app.services.menu_service import menu_service -from backend.app.services.role_service import role_service -from backend.app.utils.serializers import select_as_dict, select_list_serialize +from backend.app.admin.schema.role import CreateRoleParam, GetRoleListDetails, UpdateRoleMenuParam, UpdateRoleParam +from backend.app.admin.service.menu_service import menu_service +from backend.app.admin.service.role_service import role_service +from backend.common.pagination import DependsPagination, paging_data +from backend.common.response.response_schema import ResponseModel, response_base +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_mysql import CurrentSession +from backend.utils.serializers import select_as_dict, select_list_serialize router = APIRouter() diff --git a/backend/app/api/v1/sys/user.py b/backend/app/admin/api/v1/sys/user.py similarity index 86% rename from backend/app/api/v1/sys/user.py rename to backend/app/admin/api/v1/sys/user.py index e9aa754..a8a6d56 100644 --- a/backend/app/api/v1/sys/user.py +++ b/backend/app/admin/api/v1/sys/user.py @@ -4,13 +4,7 @@ from typing import Annotated from fastapi import APIRouter, Depends, Path, Query, Request -from backend.app.common.jwt import DependsJwtAuth -from backend.app.common.pagination import DependsPagination, paging_data -from backend.app.common.permission import RequestPermission -from backend.app.common.rbac import DependsRBAC -from backend.app.common.response.response_schema import ResponseModel, response_base -from backend.app.database.db_mysql import CurrentSession -from backend.app.schemas.user import ( +from backend.app.admin.schema.user import ( AddUserParam, AvatarParam, GetCurrentUserInfoDetail, @@ -20,14 +14,20 @@ from backend.app.schemas.user import ( UpdateUserParam, UpdateUserRoleParam, ) -from backend.app.services.user_service import user_service -from backend.app.utils.serializers import select_as_dict +from backend.app.admin.service.user_service import user_service +from backend.common.pagination import DependsPagination, paging_data +from backend.common.response.response_schema import ResponseModel, response_base +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_mysql import CurrentSession +from backend.utils.serializers import select_as_dict router = APIRouter() -@router.post('/register', summary='用户注册') -async def user_register(obj: RegisterUserParam) -> ResponseModel: +@router.post('/register', summary='注册用户') +async def register_user(obj: RegisterUserParam) -> ResponseModel: await user_service.register(obj=obj) return await response_base.success() @@ -49,7 +49,7 @@ async def password_reset(request: Request, obj: ResetPasswordParam) -> ResponseM @router.get('/me', summary='获取当前用户信息', dependencies=[DependsJwtAuth], response_model_exclude={'password'}) -async def get_current_userinfo(request: Request) -> ResponseModel: +async def get_current_user(request: Request) -> ResponseModel: data = GetCurrentUserInfoDetail(**await select_as_dict(request.user)) return await response_base.success(data=data) @@ -62,7 +62,7 @@ async def get_user(username: Annotated[str, Path(...)]) -> ResponseModel: @router.put('/{username}', summary='更新用户信息', dependencies=[DependsJwtAuth]) -async def update_userinfo(request: Request, username: Annotated[str, Path(...)], obj: UpdateUserParam) -> ResponseModel: +async def update_user(request: Request, username: Annotated[str, Path(...)], obj: UpdateUserParam) -> ResponseModel: count = await user_service.update(request=request, username=username, obj=obj) if count > 0: return await response_base.success() diff --git a/backend/app/admin/conf.py b/backend/app/admin/conf.py new file mode 100644 index 0000000..8314604 --- /dev/null +++ b/backend/app/admin/conf.py @@ -0,0 +1,33 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +from functools import lru_cache + +from pydantic_settings import BaseSettings, SettingsConfigDict + +from backend.core.path_conf import BasePath + + +class AdminSettings(BaseSettings): + """Admin Settings""" + + model_config = SettingsConfigDict(env_file=f'{BasePath}/.env', env_file_encoding='utf-8', extra='ignore') + + # OAuth2:https://github.com/fastapi-practices/fastapi_oauth20 + OAUTH2_GITHUB_CLIENT_ID: str + OAUTH2_GITHUB_CLIENT_SECRET: str + + # OAuth2 + OAUTH2_GITHUB_REDIRECT_URI: str = 'http://127.0.0.1:8000/api/v1/auth/github/callback' + + # Captcha + CAPTCHA_LOGIN_REDIS_PREFIX: str = 'fba_login_captcha' + CAPTCHA_LOGIN_EXPIRE_SECONDS: int = 60 * 5 # 过期时间,单位:秒 + + +@lru_cache +def get_admin_settings() -> AdminSettings: + """获取 admin 配置""" + return AdminSettings() + + +admin_settings = get_admin_settings() diff --git a/backend/app/common/response/__init__.py b/backend/app/admin/crud/__init__.py similarity index 100% rename from backend/app/common/response/__init__.py rename to backend/app/admin/crud/__init__.py diff --git a/backend/app/crud/crud_api.py b/backend/app/admin/crud/crud_api.py similarity index 65% rename from backend/app/crud/crud_api.py rename to backend/app/admin/crud/crud_api.py index e141d6d..723624e 100644 --- a/backend/app/crud/crud_api.py +++ b/backend/app/admin/crud/crud_api.py @@ -5,16 +5,31 @@ from typing import Sequence from sqlalchemy import Select, and_, delete, desc, select from sqlalchemy.ext.asyncio import AsyncSession -from backend.app.crud.base import CRUDBase -from backend.app.models import Api -from backend.app.schemas.api import CreateApiParam, UpdateApiParam +from backend.app.admin.model import Api +from backend.app.admin.schema.api import CreateApiParam, UpdateApiParam +from backend.common.msd.crud import CRUDBase class CRUDApi(CRUDBase[Api, CreateApiParam, UpdateApiParam]): async def get(self, db: AsyncSession, pk: int) -> Api | None: + """ + 获取 API + + :param db: + :param pk: + :return: + """ return await self.get_(db, pk=pk) async def get_list(self, name: str = None, method: str = None, path: str = None) -> Select: + """ + 获取 API 列表 + + :param name: + :param method: + :param path: + :return: + """ se = select(self.model).order_by(desc(self.model.created_time)) where_list = [] if name: @@ -28,20 +43,55 @@ class CRUDApi(CRUDBase[Api, CreateApiParam, UpdateApiParam]): return se async def get_all(self, db: AsyncSession) -> Sequence[Api]: + """ + 获取所有 API + + :param db: + :return: + """ apis = await db.execute(select(self.model)) return apis.scalars().all() async def get_by_name(self, db: AsyncSession, name: str) -> Api | None: + """ + 通过 name 获取 API + + :param db: + :param name: + :return: + """ api = await db.execute(select(self.model).where(self.model.name == name)) return api.scalars().first() async def create(self, db: AsyncSession, obj_in: CreateApiParam) -> None: + """ + 创建 API + + :param db: + :param obj_in: + :return: + """ await self.create_(db, obj_in) async def update(self, db: AsyncSession, pk: int, obj_in: UpdateApiParam) -> int: + """ + 更新 API + + :param db: + :param pk: + :param obj_in: + :return: + """ return await self.update_(db, pk, obj_in) async def delete(self, db: AsyncSession, pk: list[int]) -> int: + """ + 删除 API + + :param db: + :param pk: + :return: + """ apis = await db.execute(delete(self.model).where(self.model.id.in_(pk))) return apis.rowcount diff --git a/backend/app/crud/crud_casbin.py b/backend/app/admin/crud/crud_casbin.py similarity index 66% rename from backend/app/crud/crud_casbin.py rename to backend/app/admin/crud/crud_casbin.py index 9a3308e..bd8d69b 100644 --- a/backend/app/crud/crud_casbin.py +++ b/backend/app/admin/crud/crud_casbin.py @@ -5,13 +5,20 @@ from uuid import UUID from sqlalchemy import Select, and_, delete, or_, select from sqlalchemy.ext.asyncio import AsyncSession -from backend.app.crud.base import CRUDBase -from backend.app.models import CasbinRule -from backend.app.schemas.casbin_rule import CreatePolicyParam, DeleteAllPoliciesParam, UpdatePolicyParam +from backend.app.admin.model import CasbinRule +from backend.app.admin.schema.casbin_rule import CreatePolicyParam, DeleteAllPoliciesParam, UpdatePolicyParam +from backend.common.msd.crud import CRUDBase class CRUDCasbin(CRUDBase[CasbinRule, CreatePolicyParam, UpdatePolicyParam]): - async def get_all_policy(self, ptype: str, sub: str) -> Select: + async def get_list(self, ptype: str, sub: str) -> Select: + """ + 获取策略列表 + + :param ptype: + :param sub: + :return: + """ se = select(self.model).order_by(self.model.id) where_list = [] if ptype: @@ -23,6 +30,13 @@ class CRUDCasbin(CRUDBase[CasbinRule, CreatePolicyParam, UpdatePolicyParam]): return se async def delete_policies_by_sub(self, db: AsyncSession, sub: DeleteAllPoliciesParam) -> int: + """ + 删除角色所有P策略 + + :param db: + :param sub: + :return: + """ where_list = [] if sub.uuid: where_list.append(self.model.v0 == sub.uuid) @@ -31,6 +45,13 @@ class CRUDCasbin(CRUDBase[CasbinRule, CreatePolicyParam, UpdatePolicyParam]): return result.rowcount async def delete_groups_by_uuid(self, db: AsyncSession, uuid: UUID) -> int: + """ + 删除用户所有G策略 + + :param db: + :param uuid: + :return: + """ result = await db.execute(delete(self.model).where(self.model.v0 == str(uuid))) return result.rowcount diff --git a/backend/app/crud/crud_dept.py b/backend/app/admin/crud/crud_dept.py similarity index 69% rename from backend/app/crud/crud_dept.py rename to backend/app/admin/crud/crud_dept.py index 4192553..3cd5fd4 100644 --- a/backend/app/crud/crud_dept.py +++ b/backend/app/admin/crud/crud_dept.py @@ -6,21 +6,45 @@ from sqlalchemy import and_, asc, or_, select from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.orm import selectinload -from backend.app.crud.base import CRUDBase -from backend.app.models import Dept, User -from backend.app.schemas.dept import CreateDeptParam, UpdateDeptParam +from backend.app.admin.model import Dept, User +from backend.app.admin.schema.dept import CreateDeptParam, UpdateDeptParam +from backend.common.msd.crud import CRUDBase class CRUDDept(CRUDBase[Dept, CreateDeptParam, UpdateDeptParam]): async def get(self, db: AsyncSession, dept_id: int) -> Dept | None: + """ + 获取部门 + + :param db: + :param dept_id: + :return: + """ return await self.get_(db, pk=dept_id, del_flag=0) async def get_by_name(self, db: AsyncSession, name: str) -> Dept | None: + """ + 通过 name 获取 API + + :param db: + :param name: + :return: + """ return await self.get_(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 ) -> Sequence[Dept]: + """ + 获取所有部门 + + :param db: + :param name: + :param leader: + :param phone: + :param status: + :return: + """ se = select(self.model).order_by(asc(self.model.sort)) where_list = [self.model.del_flag == 0] conditions = [] @@ -45,15 +69,44 @@ class CRUDDept(CRUDBase[Dept, CreateDeptParam, UpdateDeptParam]): return dept.scalars().all() async def create(self, db: AsyncSession, obj_in: CreateDeptParam) -> None: + """ + 创建部门 + + :param db: + :param obj_in: + :return: + """ await self.create_(db, obj_in) async def update(self, db: AsyncSession, dept_id: int, obj_in: UpdateDeptParam) -> int: + """ + 更新部门 + + :param db: + :param dept_id: + :param obj_in: + :return: + """ return await self.update_(db, dept_id, obj_in) async def delete(self, db: AsyncSession, dept_id: int) -> int: + """ + 删除部门 + + :param db: + :param dept_id: + :return: + """ return await self.delete_(db, dept_id, del_flag=1) - async def get_user_relation(self, db: AsyncSession, dept_id: int) -> list[User]: + async def get_relation(self, db: AsyncSession, dept_id: int) -> list[User]: + """ + 获取关联 + + :param db: + :param dept_id: + :return: + """ result = await db.execute( select(self.model).options(selectinload(self.model.users)).where(self.model.id == dept_id) ) @@ -61,6 +114,13 @@ class CRUDDept(CRUDBase[Dept, CreateDeptParam, UpdateDeptParam]): return user_relation.users async def get_children(self, db: AsyncSession, dept_id: int) -> list[Dept]: + """ + 获取子部门 + + :param db: + :param dept_id: + :return: + """ result = await db.execute( select(self.model).options(selectinload(self.model.children)).where(self.model.id == dept_id) ) diff --git a/backend/app/crud/crud_dict_data.py b/backend/app/admin/crud/crud_dict_data.py similarity index 62% rename from backend/app/crud/crud_dict_data.py rename to backend/app/admin/crud/crud_dict_data.py index 8e5cd50..4199217 100644 --- a/backend/app/crud/crud_dict_data.py +++ b/backend/app/admin/crud/crud_dict_data.py @@ -4,16 +4,31 @@ from sqlalchemy import Select, and_, delete, desc, select from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.orm import selectinload -from backend.app.crud.base import CRUDBase -from backend.app.models.sys_dict_data import DictData -from backend.app.schemas.dict_data import CreateDictDataParam, UpdateDictDataParam +from backend.app.admin.model import DictData +from backend.app.admin.schema.dict_data import CreateDictDataParam, UpdateDictDataParam +from backend.common.msd.crud import CRUDBase class CRUDDictData(CRUDBase[DictData, CreateDictDataParam, UpdateDictDataParam]): async def get(self, db: AsyncSession, pk: int) -> DictData | None: + """ + 获取字典数据 + + :param db: + :param pk: + :return: + """ return await self.get_(db, pk=pk) - async def get_all(self, label: str = None, value: str = None, status: int = None) -> Select: + async def get_list(self, label: str = None, value: str = None, status: int = None) -> Select: + """ + 获取所有字典数据 + + :param label: + :param value: + :param status: + :return: + """ se = select(self.model).options(selectinload(self.model.type)).order_by(desc(self.model.sort)) where_list = [] if label: @@ -27,20 +42,56 @@ class CRUDDictData(CRUDBase[DictData, CreateDictDataParam, UpdateDictDataParam]) return se async def get_by_label(self, db: AsyncSession, label: str) -> DictData | None: + """ + 通过 label 获取字典数据 + + :param db: + :param label: + :return: + """ api = await db.execute(select(self.model).where(self.model.label == label)) return api.scalars().first() async def create(self, db: AsyncSession, obj_in: CreateDictDataParam) -> None: + """ + 创建数据字典 + + :param db: + :param obj_in: + :return: + """ await self.create_(db, obj_in) async def update(self, db: AsyncSession, pk: int, obj_in: UpdateDictDataParam) -> int: + """ + 更新数据字典 + + :param db: + :param pk: + :param obj_in: + :return: + """ return await self.update_(db, pk, obj_in) async def delete(self, db: AsyncSession, pk: list[int]) -> int: + """ + 删除字典数据 + + :param db: + :param pk: + :return: + """ apis = await db.execute(delete(self.model).where(self.model.id.in_(pk))) return apis.rowcount async def get_with_relation(self, db: AsyncSession, pk: int) -> DictData | None: + """ + 获取字典数据和类型 + + :param db: + :param pk: + :return: + """ where = [self.model.id == pk] dict_data = await db.execute(select(self.model).options(selectinload(self.model.type)).where(*where)) return dict_data.scalars().first() diff --git a/backend/app/crud/crud_dict_type.py b/backend/app/admin/crud/crud_dict_type.py similarity index 59% rename from backend/app/crud/crud_dict_type.py rename to backend/app/admin/crud/crud_dict_type.py index 9dbeca5..a34af10 100644 --- a/backend/app/crud/crud_dict_type.py +++ b/backend/app/admin/crud/crud_dict_type.py @@ -3,16 +3,31 @@ from sqlalchemy import Select, delete, desc, select from sqlalchemy.ext.asyncio import AsyncSession -from backend.app.crud.base import CRUDBase -from backend.app.models.sys_dict_type import DictType -from backend.app.schemas.dict_type import CreateDictTypeParam, UpdateDictTypeParam +from backend.app.admin.model import DictType +from backend.app.admin.schema.dict_type import CreateDictTypeParam, UpdateDictTypeParam +from backend.common.msd.crud import CRUDBase class CRUDDictType(CRUDBase[DictType, CreateDictTypeParam, UpdateDictTypeParam]): async def get(self, db: AsyncSession, pk: int) -> DictType | None: + """ + 获取字典类型 + + :param db: + :param pk: + :return: + """ return await self.get_(db, pk=pk) - async def get_all(self, *, name: str = None, code: str = None, status: int = None) -> Select: + async def get_list(self, *, name: str = None, code: str = None, status: int = None) -> Select: + """ + 获取所有字典类型 + + :param name: + :param code: + :param status: + :return: + """ se = select(self.model).order_by(desc(self.model.created_time)) where_list = [] if name: @@ -26,16 +41,45 @@ class CRUDDictType(CRUDBase[DictType, CreateDictTypeParam, UpdateDictTypeParam]) return se async def get_by_code(self, db: AsyncSession, code: str) -> DictType | None: + """ + 通过 code 获取字典类型 + + :param db: + :param code: + :return: + """ dept = await db.execute(select(self.model).where(self.model.code == code)) return dept.scalars().first() async def create(self, db: AsyncSession, obj_in: CreateDictTypeParam) -> None: + """ + 创建字典类型 + + :param db: + :param obj_in: + :return: + """ await self.create_(db, obj_in) async def update(self, db: AsyncSession, pk: int, obj_in: UpdateDictTypeParam) -> int: + """ + 更新字典类型 + + :param db: + :param pk: + :param obj_in: + :return: + """ return await self.update_(db, pk, obj_in) async def delete(self, db: AsyncSession, pk: list[int]) -> int: + """ + 删除字典类型 + + :param db: + :param pk: + :return: + """ apis = await db.execute(delete(self.model).where(self.model.id.in_(pk))) return apis.rowcount diff --git a/backend/app/crud/crud_login_log.py b/backend/app/admin/crud/crud_login_log.py similarity index 60% rename from backend/app/crud/crud_login_log.py rename to backend/app/admin/crud/crud_login_log.py index c960bb0..6e82b2f 100644 --- a/backend/app/crud/crud_login_log.py +++ b/backend/app/admin/crud/crud_login_log.py @@ -3,13 +3,21 @@ from sqlalchemy import Select, and_, delete, desc, select from sqlalchemy.ext.asyncio import AsyncSession -from backend.app.crud.base import CRUDBase -from backend.app.models import LoginLog -from backend.app.schemas.login_log import CreateLoginLogParam, UpdateLoginLogParam +from backend.app.admin.model import LoginLog +from backend.app.admin.schema.login_log import CreateLoginLogParam, UpdateLoginLogParam +from backend.common.msd.crud import CRUDBase class CRUDLoginLog(CRUDBase[LoginLog, CreateLoginLogParam, UpdateLoginLogParam]): - async def get_all(self, username: str | None = None, status: int | None = None, ip: str | None = None) -> Select: + async def get_list(self, username: str | None = None, status: int | None = None, ip: str | None = None) -> Select: + """ + 获取登录日志列表 + + :param username: + :param status: + :param ip: + :return: + """ se = select(self.model).order_by(desc(self.model.created_time)) where_list = [] if username: @@ -22,15 +30,35 @@ class CRUDLoginLog(CRUDBase[LoginLog, CreateLoginLogParam, UpdateLoginLogParam]) se = se.where(and_(*where_list)) return se - async def create(self, db: AsyncSession, obj_in: CreateLoginLogParam): + async def create(self, db: AsyncSession, obj_in: CreateLoginLogParam) -> None: + """ + 创建登录日志 + + :param db: + :param obj_in: + :return: + """ await self.create_(db, obj_in) await db.commit() async def delete(self, db: AsyncSession, pk: list[int]) -> int: + """ + 删除登录日志 + + :param db: + :param pk: + :return: + """ logs = await db.execute(delete(self.model).where(self.model.id.in_(pk))) return logs.rowcount async def delete_all(self, db: AsyncSession) -> int: + """ + 删除所有登录日志 + + :param db: + :return: + """ logs = await db.execute(delete(self.model)) return logs.rowcount diff --git a/backend/app/crud/crud_menu.py b/backend/app/admin/crud/crud_menu.py similarity index 66% rename from backend/app/crud/crud_menu.py rename to backend/app/admin/crud/crud_menu.py index d9c9660..5d59a24 100644 --- a/backend/app/crud/crud_menu.py +++ b/backend/app/admin/crud/crud_menu.py @@ -5,20 +5,42 @@ from typing import Sequence from sqlalchemy import and_, asc, select from sqlalchemy.orm import selectinload -from backend.app.crud.base import CRUDBase -from backend.app.models import Menu -from backend.app.schemas.menu import CreateMenuParam, UpdateMenuParam +from backend.app.admin.model import Menu +from backend.app.admin.schema.menu import CreateMenuParam, UpdateMenuParam +from backend.common.msd.crud import CRUDBase class CRUDMenu(CRUDBase[Menu, CreateMenuParam, UpdateMenuParam]): async def get(self, db, menu_id: int) -> Menu | None: + """ + 获取菜单 + + :param db: + :param menu_id: + :return: + """ return await self.get_(db, pk=menu_id) async def get_by_title(self, db, title: str) -> Menu | None: + """ + 通过 title 获取菜单 + + :param db: + :param title: + :return: + """ result = await db.execute(select(self.model).where(and_(self.model.title == title, self.model.menu_type != 2))) return result.scalars().first() async def get_all(self, db, title: str | None = None, status: int | None = None) -> Sequence[Menu]: + """ + 获取所有菜单 + + :param db: + :param title: + :param status: + :return: + """ se = select(self.model).order_by(asc(self.model.sort)) where_list = [] if title: @@ -31,6 +53,14 @@ class CRUDMenu(CRUDBase[Menu, CreateMenuParam, UpdateMenuParam]): return menu.scalars().all() async def get_role_menus(self, db, superuser: bool, menu_ids: list[int]) -> Sequence[Menu]: + """ + 获取角色菜单 + + :param db: + :param superuser: + :param menu_ids: + :return: + """ se = select(self.model).order_by(asc(self.model.sort)) where_list = [self.model.menu_type.in_([0, 1])] if not superuser: @@ -40,16 +70,45 @@ class CRUDMenu(CRUDBase[Menu, CreateMenuParam, UpdateMenuParam]): return menu.scalars().all() async def create(self, db, obj_in: CreateMenuParam) -> None: + """ + 创建菜单 + + :param db: + :param obj_in: + :return: + """ await self.create_(db, obj_in) async def update(self, db, menu_id: int, obj_in: UpdateMenuParam) -> int: + """ + 更新菜单 + + :param db: + :param menu_id: + :param obj_in: + :return: + """ count = await self.update_(db, menu_id, obj_in) return count async def delete(self, db, menu_id: int) -> int: + """ + 删除菜单 + + :param db: + :param menu_id: + :return: + """ return await self.delete_(db, menu_id) async def get_children(self, db, menu_id: int) -> list[Menu]: + """ + 获取子菜单 + + :param db: + :param menu_id: + :return: + """ result = await db.execute( select(self.model).options(selectinload(self.model.children)).where(self.model.id == menu_id) ) diff --git a/backend/app/crud/crud_opera_log.py b/backend/app/admin/crud/crud_opera_log.py similarity index 61% rename from backend/app/crud/crud_opera_log.py rename to backend/app/admin/crud/crud_opera_log.py index 5c95f1b..45ef61c 100644 --- a/backend/app/crud/crud_opera_log.py +++ b/backend/app/admin/crud/crud_opera_log.py @@ -3,13 +3,21 @@ from sqlalchemy import Select, and_, delete, desc, select from sqlalchemy.ext.asyncio import AsyncSession -from backend.app.crud.base import CRUDBase -from backend.app.models import OperaLog -from backend.app.schemas.opera_log import CreateOperaLogParam, UpdateOperaLogParam +from backend.app.admin.model import OperaLog +from backend.app.admin.schema.opera_log import CreateOperaLogParam, UpdateOperaLogParam +from backend.common.msd.crud import CRUDBase class CRUDOperaLogDao(CRUDBase[OperaLog, CreateOperaLogParam, UpdateOperaLogParam]): - async def get_all(self, username: str | None = None, status: int | None = None, ip: str | None = None) -> Select: + async def get_list(self, username: str | None = None, status: int | None = None, ip: str | None = None) -> Select: + """ + 获取操作日志列表 + + :param username: + :param status: + :param ip: + :return: + """ se = select(self.model).order_by(desc(self.model.created_time)) where_list = [] if username: @@ -23,13 +31,33 @@ class CRUDOperaLogDao(CRUDBase[OperaLog, CreateOperaLogParam, UpdateOperaLogPara return se async def create(self, db: AsyncSession, obj_in: CreateOperaLogParam) -> None: + """ + 创建操作日志 + + :param db: + :param obj_in: + :return: + """ await self.create_(db, obj_in) async def delete(self, db: AsyncSession, pk: list[int]) -> int: + """ + 删除操作日志 + + :param db: + :param pk: + :return: + """ logs = await db.execute(delete(self.model).where(self.model.id.in_(pk))) return logs.rowcount async def delete_all(self, db: AsyncSession) -> int: + """ + 删除所有操作日志 + + :param db: + :return: + """ logs = await db.execute(delete(self.model)) return logs.rowcount diff --git a/backend/app/crud/crud_role.py b/backend/app/admin/crud/crud_role.py similarity index 63% rename from backend/app/crud/crud_role.py rename to backend/app/admin/crud/crud_role.py index 4d06b4b..0557573 100644 --- a/backend/app/crud/crud_role.py +++ b/backend/app/admin/crud/crud_role.py @@ -5,30 +5,65 @@ from typing import Sequence from sqlalchemy import Select, delete, desc, select from sqlalchemy.orm import selectinload -from backend.app.crud.base import CRUDBase -from backend.app.models import Menu, Role, User -from backend.app.schemas.role import CreateRoleParam, UpdateRoleMenuParam, UpdateRoleParam +from backend.app.admin.model import Menu, Role, User +from backend.app.admin.schema.role import CreateRoleParam, UpdateRoleMenuParam, UpdateRoleParam +from backend.common.msd.crud import CRUDBase class CRUDRole(CRUDBase[Role, CreateRoleParam, UpdateRoleParam]): async def get(self, db, role_id: int) -> Role | None: + """ + 获取角色 + + :param db: + :param role_id: + :return: + """ return await self.get_(db, pk=role_id) async def get_with_relation(self, db, role_id: int) -> Role | None: + """ + 获取角色和菜单 + + :param db: + :param role_id: + :return: + """ role = await db.execute( select(self.model).options(selectinload(self.model.menus)).where(self.model.id == role_id) ) return role.scalars().first() async def get_all(self, db) -> Sequence[Role]: + """ + 获取所有角色 + + :param db: + :return: + """ roles = await db.execute(select(self.model)) return roles.scalars().all() - async def get_user_all(self, db, user_id: int) -> Sequence[Role]: + async def get_user_roles(self, db, user_id: int) -> Sequence[Role]: + """ + 获取用户所有角色 + + :param db: + :param user_id: + :return: + """ roles = await db.execute(select(self.model).join(self.model.users).where(User.id == user_id)) return roles.scalars().all() async def get_list(self, name: str = None, data_scope: int = None, status: int = None) -> Select: + """ + 获取角色列表 + + :param name: + :param data_scope: + :param status: + :return: + """ se = select(self.model).options(selectinload(self.model.menus)).order_by(desc(self.model.created_time)) where_list = [] if name: @@ -42,17 +77,47 @@ class CRUDRole(CRUDBase[Role, CreateRoleParam, UpdateRoleParam]): return se async def get_by_name(self, db, name: str) -> Role | None: + """ + 通过 name 获取角色 + + :param db: + :param name: + :return: + """ role = await db.execute(select(self.model).where(self.model.name == name)) return role.scalars().first() async def create(self, db, obj_in: CreateRoleParam) -> None: + """ + 创建角色 + + :param db: + :param obj_in: + :return: + """ await self.create_(db, obj_in) async def update(self, db, role_id: int, obj_in: UpdateRoleParam) -> int: + """ + 更新角色 + + :param db: + :param role_id: + :param obj_in: + :return: + """ rowcount = await self.update_(db, pk=role_id, obj_in=obj_in) return rowcount async def update_menus(self, db, role_id: int, menu_ids: UpdateRoleMenuParam) -> int: + """ + 更新角色菜单 + + :param db: + :param role_id: + :param menu_ids: + :return: + """ current_role = await self.get_with_relation(db, role_id) # 更新菜单 menus = await db.execute(select(Menu).where(Menu.id.in_(menu_ids.menus))) @@ -60,6 +125,13 @@ class CRUDRole(CRUDBase[Role, CreateRoleParam, UpdateRoleParam]): return len(current_role.menus) async def delete(self, db, role_id: list[int]) -> int: + """ + 删除角色 + + :param db: + :param role_id: + :return: + """ roles = await db.execute(delete(self.model).where(self.model.id.in_(role_id))) return roles.rowcount diff --git a/backend/app/crud/crud_user.py b/backend/app/admin/crud/crud_user.py similarity index 65% rename from backend/app/crud/crud_user.py rename to backend/app/admin/crud/crud_user.py index a08679f..37be7ca 100644 --- a/backend/app/crud/crud_user.py +++ b/backend/app/admin/crud/crud_user.py @@ -6,35 +6,77 @@ from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.orm import selectinload from sqlalchemy.sql import Select -from backend.app.common import jwt -from backend.app.crud.base import CRUDBase -from backend.app.models import Role, User -from backend.app.schemas.user import AddUserParam, AvatarParam, RegisterUserParam, UpdateUserParam, UpdateUserRoleParam -from backend.app.utils.timezone import timezone +from backend.app.admin.model import Role, User +from backend.app.admin.schema.user import ( + AddUserParam, + AvatarParam, + RegisterUserParam, + UpdateUserParam, + UpdateUserRoleParam, +) +from backend.common.msd.crud import CRUDBase +from backend.common.security.jwt import get_hash_password +from backend.utils.timezone import timezone class CRUDUser(CRUDBase[User, RegisterUserParam, UpdateUserParam]): async def get(self, db: AsyncSession, user_id: int) -> User | None: + """ + 获取用户 + + :param db: + :param user_id: + :return: + """ return await self.get_(db, pk=user_id) async def get_by_username(self, db: AsyncSession, username: str) -> User | None: + """ + 通过 username 获取用户 + + :param db: + :param username: + :return: + """ user = await db.execute(select(self.model).where(self.model.username == username)) return user.scalars().first() async def get_by_nickname(self, db: AsyncSession, nickname: str) -> User | None: + """ + 通过 nickname 获取用户 + + :param db: + :param nickname: + :return: + """ user = await db.execute(select(self.model).where(self.model.nickname == nickname)) return user.scalars().first() async def update_login_time(self, db: AsyncSession, username: str) -> int: + """ + 更新用户登录时间 + + :param db: + :param username: + :return: + """ user = await db.execute( update(self.model).where(self.model.username == username).values(last_login_time=timezone.now()) ) return user.rowcount async def create(self, db: AsyncSession, obj: RegisterUserParam, *, social: bool = False) -> None: + """ + 创建用户 + + :param db: + :param obj: + :param social: + :return: + """ if not social: salt = text_captcha(5) - obj.password = await jwt.get_hash_password(f'{obj.password}{salt}') + obj.password = await get_hash_password(f'{obj.password}{salt}') dict_obj = obj.model_dump() dict_obj.update({'salt': salt}) else: @@ -44,8 +86,15 @@ class CRUDUser(CRUDBase[User, RegisterUserParam, UpdateUserParam]): db.add(new_user) async def add(self, db: AsyncSession, obj: AddUserParam) -> None: + """ + 后台添加用户 + + :param db: + :param obj: + :return: + """ salt = text_captcha(5) - obj.password = await jwt.get_hash_password(f'{obj.password}{salt}') + obj.password = await get_hash_password(f'{obj.password}{salt}') dict_obj = obj.model_dump(exclude={'roles'}) dict_obj.update({'salt': salt}) new_user = self.model(**dict_obj) @@ -56,11 +105,27 @@ class CRUDUser(CRUDBase[User, RegisterUserParam, UpdateUserParam]): db.add(new_user) async def update_userinfo(self, db: AsyncSession, input_user: User, obj: UpdateUserParam) -> int: + """ + 更新用户信息 + + :param db: + :param input_user: + :param obj: + :return: + """ user = await db.execute(update(self.model).where(self.model.id == input_user.id).values(**obj.model_dump())) return user.rowcount @staticmethod async def update_role(db: AsyncSession, input_user: User, obj: UpdateUserRoleParam) -> None: + """ + 更新用户角色 + + :param db: + :param input_user: + :param obj: + :return: + """ # 删除用户所有角色 for i in list(input_user.roles): input_user.roles.remove(i) @@ -71,23 +136,63 @@ class CRUDUser(CRUDBase[User, RegisterUserParam, UpdateUserParam]): input_user.roles.extend(role_list) async def update_avatar(self, db: AsyncSession, current_user: User, avatar: AvatarParam) -> int: + """ + 更新用户头像 + + :param db: + :param current_user: + :param avatar: + :return: + """ user = await db.execute(update(self.model).where(self.model.id == current_user.id).values(avatar=avatar.url)) return user.rowcount async def delete(self, db: AsyncSession, user_id: int) -> int: + """ + 删除用户 + + :param db: + :param user_id: + :return: + """ return await self.delete_(db, user_id) async def check_email(self, db: AsyncSession, email: str) -> User | None: + """ + 检查邮箱是否存在 + + :param db: + :param email: + :return: + """ mail = await db.execute(select(self.model).where(self.model.email == email)) return mail.scalars().first() async def reset_password(self, db: AsyncSession, pk: int, password: str, salt: str) -> int: + """ + 重置用户密码 + + :param db: + :param pk: + :param password: + :param salt: + :return: + """ user = await db.execute( - update(self.model).where(self.model.id == pk).values(password=await jwt.get_hash_password(password + salt)) + update(self.model).where(self.model.id == pk).values(password=await get_hash_password(password + salt)) ) return user.rowcount - async def get_all(self, dept: int = None, username: str = None, phone: str = None, status: int = None) -> Select: + async def get_list(self, dept: int = None, username: str = None, phone: str = None, status: int = None) -> Select: + """ + 获取用户列表 + + :param dept: + :param username: + :param phone: + :param status: + :return: + """ se = ( select(self.model) .options(selectinload(self.model.dept)) @@ -108,22 +213,57 @@ class CRUDUser(CRUDBase[User, RegisterUserParam, UpdateUserParam]): return se async def get_super(self, db: AsyncSession, user_id: int) -> bool: + """ + 获取用户超级管理员状态 + + :param db: + :param user_id: + :return: + """ user = await self.get(db, user_id) return user.is_superuser async def get_staff(self, db: AsyncSession, user_id: int) -> bool: + """ + 获取用户后台登录状态 + + :param db: + :param user_id: + :return: + """ user = await self.get(db, user_id) return user.is_staff async def get_status(self, db: AsyncSession, user_id: int) -> bool: + """ + 获取用户状态 + + :param db: + :param user_id: + :return: + """ user = await self.get(db, user_id) return user.status async def get_multi_login(self, db: AsyncSession, user_id: int) -> bool: + """ + 获取用户多点登录状态 + + :param db: + :param user_id: + :return: + """ user = await self.get(db, user_id) return user.is_multi_login async def set_super(self, db: AsyncSession, user_id: int) -> int: + """ + 设置用户超级管理员 + + :param db: + :param user_id: + :return: + """ super_status = await self.get_super(db, user_id) user = await db.execute( update(self.model).where(self.model.id == user_id).values(is_superuser=False if super_status else True) @@ -131,6 +271,13 @@ class CRUDUser(CRUDBase[User, RegisterUserParam, UpdateUserParam]): return user.rowcount async def set_staff(self, db: AsyncSession, user_id: int) -> int: + """ + 设置用户后台登录 + + :param db: + :param user_id: + :return: + """ staff_status = await self.get_staff(db, user_id) user = await db.execute( update(self.model).where(self.model.id == user_id).values(is_staff=False if staff_status else True) @@ -138,6 +285,13 @@ class CRUDUser(CRUDBase[User, RegisterUserParam, UpdateUserParam]): return user.rowcount async def set_status(self, db: AsyncSession, user_id: int) -> int: + """ + 设置用户状态 + + :param db: + :param user_id: + :return: + """ status = await self.get_status(db, user_id) user = await db.execute( update(self.model).where(self.model.id == user_id).values(status=False if status else True) @@ -145,6 +299,13 @@ class CRUDUser(CRUDBase[User, RegisterUserParam, UpdateUserParam]): return user.rowcount async def set_multi_login(self, db: AsyncSession, user_id: int) -> int: + """ + 设置用户多点登录 + + :param db: + :param user_id: + :return: + """ multi_login = await self.get_multi_login(db, user_id) user = await db.execute( update(self.model).where(self.model.id == user_id).values(is_multi_login=False if multi_login else True) @@ -152,6 +313,14 @@ class CRUDUser(CRUDBase[User, RegisterUserParam, UpdateUserParam]): return user.rowcount async def get_with_relation(self, db: AsyncSession, *, user_id: int = None, username: str = None) -> User | None: + """ + 获取用户和(部门,角色,菜单) + + :param db: + :param user_id: + :param username: + :return: + """ where = [] if user_id: where.append(self.model.id == user_id) diff --git a/backend/app/crud/crud_user_social.py b/backend/app/admin/crud/crud_user_social.py similarity index 56% rename from backend/app/crud/crud_user_social.py rename to backend/app/admin/crud/crud_user_social.py index 669a4ab..6db1077 100644 --- a/backend/app/crud/crud_user_social.py +++ b/backend/app/admin/crud/crud_user_social.py @@ -3,22 +3,44 @@ from sqlalchemy import and_, select from sqlalchemy.ext.asyncio import AsyncSession -from backend.app.common.enums import UserSocialType -from backend.app.crud.base import CRUDBase -from backend.app.models import UserSocial -from backend.app.schemas.user_social import CreateUserSocialParam, UpdateUserSocialParam +from backend.app.admin.model import UserSocial +from backend.app.admin.schema.user_social import CreateUserSocialParam, UpdateUserSocialParam +from backend.common.enums import UserSocialType +from backend.common.msd.crud import CRUDBase class CRUDOUserSocial(CRUDBase[UserSocial, CreateUserSocialParam, UpdateUserSocialParam]): async def get(self, db: AsyncSession, pk: int, source: UserSocialType) -> UserSocial | None: + """ + 获取用户社交账号绑定 + + :param db: + :param pk: + :param source: + :return: + """ se = select(self.model).where(and_(self.model.id == pk, self.model.source == source)) user_social = await db.execute(se) return user_social.scalars().first() async def create(self, db: AsyncSession, obj_in: CreateUserSocialParam) -> None: + """ + 创建用户社交账号绑定 + + :param db: + :param obj_in: + :return: + """ await self.create_(db, obj_in) async def delete(self, db: AsyncSession, social_id: int) -> int: + """ + 删除用户社交账号绑定 + + :param db: + :param social_id: + :return: + """ return await self.delete_(db, social_id) diff --git a/backend/app/admin/model/__init__.py b/backend/app/admin/model/__init__.py new file mode 100644 index 0000000..cec0611 --- /dev/null +++ b/backend/app/admin/model/__init__.py @@ -0,0 +1,14 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +from backend.common.msd.model import MappedBase # noqa: I001 +from backend.app.admin.model.sys_api import Api +from backend.app.admin.model.sys_casbin_rule import CasbinRule +from backend.app.admin.model.sys_dept import Dept +from backend.app.admin.model.sys_dict_data import DictData +from backend.app.admin.model.sys_dict_type import DictType +from backend.app.admin.model.sys_login_log import LoginLog +from backend.app.admin.model.sys_menu import Menu +from backend.app.admin.model.sys_opera_log import OperaLog +from backend.app.admin.model.sys_role import Role +from backend.app.admin.model.sys_user import User +from backend.app.admin.model.sys_user_social import UserSocial diff --git a/backend/app/models/sys_api.py b/backend/app/admin/model/sys_api.py similarity index 92% rename from backend/app/models/sys_api.py rename to backend/app/admin/model/sys_api.py index 147e0ea..101852c 100644 --- a/backend/app/models/sys_api.py +++ b/backend/app/admin/model/sys_api.py @@ -1,11 +1,10 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- - from sqlalchemy import String from sqlalchemy.dialects.mysql import LONGTEXT from sqlalchemy.orm import Mapped, mapped_column -from backend.app.models.base import Base, id_key +from backend.common.msd.model import Base, id_key class Api(Base): diff --git a/backend/app/models/sys_casbin_rule.py b/backend/app/admin/model/sys_casbin_rule.py similarity index 95% rename from backend/app/models/sys_casbin_rule.py rename to backend/app/admin/model/sys_casbin_rule.py index ec7fb96..f9e9cbe 100644 --- a/backend/app/models/sys_casbin_rule.py +++ b/backend/app/admin/model/sys_casbin_rule.py @@ -4,7 +4,7 @@ from sqlalchemy import String from sqlalchemy.dialects.mysql import LONGTEXT from sqlalchemy.orm import Mapped, mapped_column -from backend.app.models.base import MappedBase, id_key +from backend.common.msd.model import MappedBase, id_key class CasbinRule(MappedBase): diff --git a/backend/app/models/sys_dept.py b/backend/app/admin/model/sys_dept.py similarity index 96% rename from backend/app/models/sys_dept.py rename to backend/app/admin/model/sys_dept.py index 55a101b..3462fc1 100644 --- a/backend/app/models/sys_dept.py +++ b/backend/app/admin/model/sys_dept.py @@ -5,7 +5,7 @@ from typing import Union from sqlalchemy import ForeignKey, String from sqlalchemy.orm import Mapped, mapped_column, relationship -from backend.app.models.base import Base, id_key +from backend.common.msd.model import Base, id_key class Dept(Base): diff --git a/backend/app/models/sys_dict_data.py b/backend/app/admin/model/sys_dict_data.py similarity index 95% rename from backend/app/models/sys_dict_data.py rename to backend/app/admin/model/sys_dict_data.py index cba78ee..a52cee7 100644 --- a/backend/app/models/sys_dict_data.py +++ b/backend/app/admin/model/sys_dict_data.py @@ -4,7 +4,7 @@ from sqlalchemy import ForeignKey, String from sqlalchemy.dialects.mysql import LONGTEXT from sqlalchemy.orm import Mapped, mapped_column, relationship -from backend.app.models.base import Base, id_key +from backend.common.msd.model import Base, id_key class DictData(Base): diff --git a/backend/app/models/sys_dict_type.py b/backend/app/admin/model/sys_dict_type.py similarity index 94% rename from backend/app/models/sys_dict_type.py rename to backend/app/admin/model/sys_dict_type.py index 98b4134..82f756e 100644 --- a/backend/app/models/sys_dict_type.py +++ b/backend/app/admin/model/sys_dict_type.py @@ -4,7 +4,7 @@ from sqlalchemy import String from sqlalchemy.dialects.mysql import LONGTEXT from sqlalchemy.orm import Mapped, mapped_column, relationship -from backend.app.models.base import Base, id_key +from backend.common.msd.model import Base, id_key class DictType(Base): diff --git a/backend/app/models/sys_login_log.py b/backend/app/admin/model/sys_login_log.py similarity index 93% rename from backend/app/models/sys_login_log.py rename to backend/app/admin/model/sys_login_log.py index 29b53db..62fbbd8 100644 --- a/backend/app/models/sys_login_log.py +++ b/backend/app/admin/model/sys_login_log.py @@ -6,8 +6,8 @@ from sqlalchemy import String from sqlalchemy.dialects.mysql import LONGTEXT from sqlalchemy.orm import Mapped, mapped_column -from backend.app.models.base import DataClassBase, id_key -from backend.app.utils.timezone import timezone +from backend.common.msd.model import DataClassBase, id_key +from backend.utils.timezone import timezone class LoginLog(DataClassBase): diff --git a/backend/app/models/sys_menu.py b/backend/app/admin/model/sys_menu.py similarity index 94% rename from backend/app/models/sys_menu.py rename to backend/app/admin/model/sys_menu.py index 8c4aebc..b1d7ef4 100644 --- a/backend/app/models/sys_menu.py +++ b/backend/app/admin/model/sys_menu.py @@ -6,8 +6,8 @@ from sqlalchemy import ForeignKey, String from sqlalchemy.dialects.mysql import LONGTEXT from sqlalchemy.orm import Mapped, mapped_column, relationship -from backend.app.models.base import Base, id_key -from backend.app.models.sys_role_menu import sys_role_menu +from backend.app.admin.model.sys_role_menu import sys_role_menu +from backend.common.msd.model import Base, id_key class Menu(Base): diff --git a/backend/app/models/sys_opera_log.py b/backend/app/admin/model/sys_opera_log.py similarity index 94% rename from backend/app/models/sys_opera_log.py rename to backend/app/admin/model/sys_opera_log.py index b002eb3..dd84bba 100644 --- a/backend/app/models/sys_opera_log.py +++ b/backend/app/admin/model/sys_opera_log.py @@ -6,8 +6,8 @@ from sqlalchemy import String from sqlalchemy.dialects.mysql import JSON, LONGTEXT from sqlalchemy.orm import Mapped, mapped_column -from backend.app.models.base import DataClassBase, id_key -from backend.app.utils.timezone import timezone +from backend.common.msd.model import DataClassBase, id_key +from backend.utils.timezone import timezone class OperaLog(DataClassBase): diff --git a/backend/app/models/sys_role.py b/backend/app/admin/model/sys_role.py similarity index 85% rename from backend/app/models/sys_role.py rename to backend/app/admin/model/sys_role.py index 0addc31..f9d5938 100644 --- a/backend/app/models/sys_role.py +++ b/backend/app/admin/model/sys_role.py @@ -4,9 +4,9 @@ from sqlalchemy import String from sqlalchemy.dialects.mysql import LONGTEXT from sqlalchemy.orm import Mapped, mapped_column, relationship -from backend.app.models.base import Base, id_key -from backend.app.models.sys_role_menu import sys_role_menu -from backend.app.models.sys_user_role import sys_user_role +from backend.app.admin.model.sys_role_menu import sys_role_menu +from backend.app.admin.model.sys_user_role import sys_user_role +from backend.common.msd.model import Base, id_key class Role(Base): diff --git a/backend/app/models/sys_role_menu.py b/backend/app/admin/model/sys_role_menu.py similarity index 91% rename from backend/app/models/sys_role_menu.py rename to backend/app/admin/model/sys_role_menu.py index c447cc9..cde1c77 100644 --- a/backend/app/models/sys_role_menu.py +++ b/backend/app/admin/model/sys_role_menu.py @@ -2,7 +2,7 @@ # -*- coding: utf-8 -*- from sqlalchemy import INT, Column, ForeignKey, Integer, Table -from backend.app.models.base import MappedBase +from backend.common.msd.model import MappedBase sys_role_menu = Table( 'sys_role_menu', diff --git a/backend/app/models/sys_user.py b/backend/app/admin/model/sys_user.py similarity index 91% rename from backend/app/models/sys_user.py rename to backend/app/admin/model/sys_user.py index 9357553..19055d0 100644 --- a/backend/app/models/sys_user.py +++ b/backend/app/admin/model/sys_user.py @@ -6,10 +6,10 @@ from typing import Union from sqlalchemy import ForeignKey, String from sqlalchemy.orm import Mapped, mapped_column, relationship -from backend.app.database.db_mysql import uuid4_str -from backend.app.models.base import Base, id_key -from backend.app.models.sys_user_role import sys_user_role -from backend.app.utils.timezone import timezone +from backend.app.admin.model.sys_user_role import sys_user_role +from backend.common.msd.model import Base, id_key +from backend.database.db_mysql import uuid4_str +from backend.utils.timezone import timezone class User(Base): diff --git a/backend/app/models/sys_user_role.py b/backend/app/admin/model/sys_user_role.py similarity index 91% rename from backend/app/models/sys_user_role.py rename to backend/app/admin/model/sys_user_role.py index 8e37398..88c41de 100644 --- a/backend/app/models/sys_user_role.py +++ b/backend/app/admin/model/sys_user_role.py @@ -2,7 +2,7 @@ # -*- coding: utf-8 -*- from sqlalchemy import INT, Column, ForeignKey, Integer, Table -from backend.app.models.base import MappedBase +from backend.common.msd.model import MappedBase sys_user_role = Table( 'sys_user_role', diff --git a/backend/app/models/sys_user_social.py b/backend/app/admin/model/sys_user_social.py similarity index 96% rename from backend/app/models/sys_user_social.py rename to backend/app/admin/model/sys_user_social.py index af683ab..3389cf7 100644 --- a/backend/app/models/sys_user_social.py +++ b/backend/app/admin/model/sys_user_social.py @@ -5,7 +5,7 @@ from typing import Union from sqlalchemy import ForeignKey, String from sqlalchemy.orm import Mapped, mapped_column, relationship -from backend.app.models.base import Base, id_key +from backend.common.msd.model import Base, id_key class UserSocial(Base): diff --git a/backend/app/core/__init__.py b/backend/app/admin/schema/__init__.py similarity index 100% rename from backend/app/core/__init__.py rename to backend/app/admin/schema/__init__.py diff --git a/backend/app/schemas/api.py b/backend/app/admin/schema/api.py similarity index 86% rename from backend/app/schemas/api.py rename to backend/app/admin/schema/api.py index aaf3b08..979d607 100644 --- a/backend/app/schemas/api.py +++ b/backend/app/admin/schema/api.py @@ -4,8 +4,8 @@ from datetime import datetime from pydantic import ConfigDict, Field -from backend.app.common.enums import MethodType -from backend.app.schemas.base import SchemaBase +from backend.common.enums import MethodType +from backend.common.msd.schema import SchemaBase class ApiSchemaBase(SchemaBase): diff --git a/backend/app/schemas/casbin_rule.py b/backend/app/admin/schema/casbin_rule.py similarity index 92% rename from backend/app/schemas/casbin_rule.py rename to backend/app/admin/schema/casbin_rule.py index 698d92f..5ece8a9 100644 --- a/backend/app/schemas/casbin_rule.py +++ b/backend/app/admin/schema/casbin_rule.py @@ -2,8 +2,8 @@ # -*- coding: utf-8 -*- from pydantic import ConfigDict, Field -from backend.app.common.enums import MethodType -from backend.app.schemas.base import SchemaBase +from backend.common.enums import MethodType +from backend.common.msd.schema import SchemaBase class CreatePolicyParam(SchemaBase): diff --git a/backend/app/schemas/dept.py b/backend/app/admin/schema/dept.py similarity index 85% rename from backend/app/schemas/dept.py rename to backend/app/admin/schema/dept.py index c99b5a7..6389927 100644 --- a/backend/app/schemas/dept.py +++ b/backend/app/admin/schema/dept.py @@ -4,8 +4,8 @@ from datetime import datetime from pydantic import ConfigDict, Field -from backend.app.common.enums import StatusType -from backend.app.schemas.base import CustomEmailStr, CustomPhoneNumber, SchemaBase +from backend.common.enums import StatusType +from backend.common.msd.schema import CustomEmailStr, CustomPhoneNumber, SchemaBase class DeptSchemaBase(SchemaBase): diff --git a/backend/app/schemas/dict_data.py b/backend/app/admin/schema/dict_data.py similarity index 79% rename from backend/app/schemas/dict_data.py rename to backend/app/admin/schema/dict_data.py index 35d19b6..179ba5d 100644 --- a/backend/app/schemas/dict_data.py +++ b/backend/app/admin/schema/dict_data.py @@ -4,9 +4,9 @@ from datetime import datetime from pydantic import ConfigDict, Field -from backend.app.common.enums import StatusType -from backend.app.schemas.base import SchemaBase -from backend.app.schemas.dict_type import GetDictTypeListDetails +from backend.app.admin.schema.dict_type import GetDictTypeListDetails +from backend.common.enums import StatusType +from backend.common.msd.schema import SchemaBase class DictDataSchemaBase(SchemaBase): diff --git a/backend/app/schemas/dict_type.py b/backend/app/admin/schema/dict_type.py similarity index 85% rename from backend/app/schemas/dict_type.py rename to backend/app/admin/schema/dict_type.py index 84a90e8..61edfc9 100644 --- a/backend/app/schemas/dict_type.py +++ b/backend/app/admin/schema/dict_type.py @@ -4,8 +4,8 @@ from datetime import datetime from pydantic import ConfigDict, Field -from backend.app.common.enums import StatusType -from backend.app.schemas.base import SchemaBase +from backend.common.enums import StatusType +from backend.common.msd.schema import SchemaBase class DictTypeSchemaBase(SchemaBase): diff --git a/backend/app/schemas/login_log.py b/backend/app/admin/schema/login_log.py similarity index 93% rename from backend/app/schemas/login_log.py rename to backend/app/admin/schema/login_log.py index d8f44a7..14e59f8 100644 --- a/backend/app/schemas/login_log.py +++ b/backend/app/admin/schema/login_log.py @@ -4,7 +4,7 @@ from datetime import datetime from pydantic import ConfigDict -from backend.app.schemas.base import SchemaBase +from backend.common.msd.schema import SchemaBase class LoginLogSchemaBase(SchemaBase): diff --git a/backend/app/schemas/menu.py b/backend/app/admin/schema/menu.py similarity index 90% rename from backend/app/schemas/menu.py rename to backend/app/admin/schema/menu.py index d36ba51..a69802c 100644 --- a/backend/app/schemas/menu.py +++ b/backend/app/admin/schema/menu.py @@ -4,8 +4,8 @@ from datetime import datetime from pydantic import ConfigDict, Field -from backend.app.common.enums import MenuType, StatusType -from backend.app.schemas.base import SchemaBase +from backend.common.enums import MenuType, StatusType +from backend.common.msd.schema import SchemaBase class MenuSchemaBase(SchemaBase): diff --git a/backend/app/schemas/opera_log.py b/backend/app/admin/schema/opera_log.py similarity index 90% rename from backend/app/schemas/opera_log.py rename to backend/app/admin/schema/opera_log.py index 0e6db5a..50ccc46 100644 --- a/backend/app/schemas/opera_log.py +++ b/backend/app/admin/schema/opera_log.py @@ -4,8 +4,8 @@ from datetime import datetime from pydantic import ConfigDict, Field -from backend.app.common.enums import StatusType -from backend.app.schemas.base import SchemaBase +from backend.common.enums import StatusType +from backend.common.msd.schema import SchemaBase class OperaLogSchemaBase(SchemaBase): diff --git a/backend/app/schemas/role.py b/backend/app/admin/schema/role.py similarity index 82% rename from backend/app/schemas/role.py rename to backend/app/admin/schema/role.py index 5b0cc8f..8901c7f 100644 --- a/backend/app/schemas/role.py +++ b/backend/app/admin/schema/role.py @@ -4,9 +4,9 @@ from datetime import datetime from pydantic import ConfigDict, Field -from backend.app.common.enums import RoleDataScopeType, StatusType -from backend.app.schemas.base import SchemaBase -from backend.app.schemas.menu import GetMenuListDetails +from backend.app.admin.schema.menu import GetMenuListDetails +from backend.common.enums import RoleDataScopeType, StatusType +from backend.common.msd.schema import SchemaBase class RoleSchemaBase(SchemaBase): diff --git a/backend/app/schemas/token.py b/backend/app/admin/schema/token.py similarity index 84% rename from backend/app/schemas/token.py rename to backend/app/admin/schema/token.py index 95c810b..2faf777 100644 --- a/backend/app/schemas/token.py +++ b/backend/app/admin/schema/token.py @@ -2,8 +2,8 @@ # -*- coding: utf-8 -*- from datetime import datetime -from backend.app.schemas.base import SchemaBase -from backend.app.schemas.user import GetUserInfoNoRelationDetail +from backend.app.admin.schema.user import GetUserInfoNoRelationDetail +from backend.common.msd.schema import SchemaBase class GetSwaggerToken(SchemaBase): diff --git a/backend/app/schemas/user.py b/backend/app/admin/schema/user.py similarity index 90% rename from backend/app/schemas/user.py rename to backend/app/admin/schema/user.py index 0985113..ddb03ab 100644 --- a/backend/app/schemas/user.py +++ b/backend/app/admin/schema/user.py @@ -4,10 +4,10 @@ from datetime import datetime from pydantic import ConfigDict, EmailStr, Field, HttpUrl, model_validator -from backend.app.common.enums import StatusType -from backend.app.schemas.base import CustomPhoneNumber, SchemaBase -from backend.app.schemas.dept import GetDeptListDetails -from backend.app.schemas.role import GetRoleListDetails +from backend.app.admin.schema.dept import GetDeptListDetails +from backend.app.admin.schema.role import GetRoleListDetails +from backend.common.enums import StatusType +from backend.common.msd.schema import CustomPhoneNumber, SchemaBase class AuthSchemaBase(SchemaBase): diff --git a/backend/app/schemas/user_social.py b/backend/app/admin/schema/user_social.py similarity index 79% rename from backend/app/schemas/user_social.py rename to backend/app/admin/schema/user_social.py index 232426c..a71b4f1 100644 --- a/backend/app/schemas/user_social.py +++ b/backend/app/admin/schema/user_social.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -from backend.app.common.enums import UserSocialType -from backend.app.schemas.base import SchemaBase +from backend.common.enums import UserSocialType +from backend.common.msd.schema import SchemaBase class UserSocialSchemaBase(SchemaBase): diff --git a/backend/app/crud/__init__.py b/backend/app/admin/service/__init__.py similarity index 100% rename from backend/app/crud/__init__.py rename to backend/app/admin/service/__init__.py diff --git a/backend/app/services/api_service.py b/backend/app/admin/service/api_service.py similarity index 80% rename from backend/app/services/api_service.py rename to backend/app/admin/service/api_service.py index b769db1..655b287 100644 --- a/backend/app/services/api_service.py +++ b/backend/app/admin/service/api_service.py @@ -4,11 +4,11 @@ from typing import Sequence from sqlalchemy import Select -from backend.app.common.exception import errors -from backend.app.crud.crud_api import api_dao -from backend.app.database.db_mysql import async_db_session -from backend.app.models import Api -from backend.app.schemas.api import CreateApiParam, UpdateApiParam +from backend.app.admin.crud.crud_api import api_dao +from backend.app.admin.model import Api +from backend.app.admin.schema.api import CreateApiParam, UpdateApiParam +from backend.common.exception import errors +from backend.database.db_mysql import async_db_session class ApiService: @@ -25,7 +25,7 @@ class ApiService: return await api_dao.get_list(name=name, method=method, path=path) @staticmethod - async def get_api_list() -> Sequence[Api]: + async def get_all() -> Sequence[Api]: async with async_db_session() as db: apis = await api_dao.get_all(db) return apis @@ -51,4 +51,4 @@ class ApiService: return count -api_service: ApiService = ApiService() +api_service = ApiService() diff --git a/backend/app/services/auth_service.py b/backend/app/admin/service/auth_service.py similarity index 74% rename from backend/app/services/auth_service.py rename to backend/app/admin/service/auth_service.py index efad1ed..073f4b0 100644 --- a/backend/app/services/auth_service.py +++ b/backend/app/admin/service/auth_service.py @@ -4,19 +4,27 @@ from fastapi import Request from fastapi.security import HTTPBasicCredentials from starlette.background import BackgroundTask, BackgroundTasks -from backend.app.common import jwt -from backend.app.common.enums import LoginLogStatusType -from backend.app.common.exception import errors -from backend.app.common.redis import redis_client -from backend.app.common.response.response_code import CustomErrorCode -from backend.app.core.conf import settings -from backend.app.crud.crud_user import user_dao -from backend.app.database.db_mysql import async_db_session -from backend.app.models import User -from backend.app.schemas.token import GetLoginToken, GetNewToken -from backend.app.schemas.user import AuthLoginParam -from backend.app.services.login_log_service import LoginLogService -from backend.app.utils.timezone import timezone +from backend.app.admin.conf import admin_settings +from backend.app.admin.crud.crud_user import user_dao +from backend.app.admin.model import User +from backend.app.admin.schema.token import GetLoginToken, GetNewToken +from backend.app.admin.schema.user import AuthLoginParam +from backend.app.admin.service.login_log_service import LoginLogService +from backend.common.enums import LoginLogStatusType +from backend.common.exception import errors +from backend.common.response.response_code import CustomErrorCode +from backend.common.security.jwt import ( + create_access_token, + create_new_token, + create_refresh_token, + get_token, + jwt_decode, + password_verify, +) +from backend.core.conf import settings +from backend.database.db_mysql import async_db_session +from backend.database.db_redis import redis_client +from backend.utils.timezone import timezone class AuthService: @@ -26,13 +34,11 @@ class AuthService: current_user = await user_dao.get_by_username(db, obj.username) if not current_user: raise errors.NotFoundError(msg='用户不存在') - elif not await jwt.password_verify(f'{obj.password}{current_user.salt}', current_user.password): + elif not await password_verify(f'{obj.password}{current_user.salt}', current_user.password): raise errors.AuthorizationError(msg='密码错误') elif not current_user.status: raise errors.AuthorizationError(msg='用户已锁定, 登陆失败') - access_token, _ = await jwt.create_access_token( - str(current_user.id), multi_login=current_user.is_multi_login - ) + access_token, _ = await create_access_token(str(current_user.id), multi_login=current_user.is_multi_login) await user_dao.update_login_time(db, obj.username) return access_token, current_user @@ -43,19 +49,19 @@ class AuthService: current_user = await user_dao.get_by_username(db, obj.username) if not current_user: raise errors.NotFoundError(msg='用户不存在') - elif not await jwt.password_verify(obj.password + current_user.salt, current_user.password): + elif not await password_verify(obj.password + current_user.salt, current_user.password): raise errors.AuthorizationError(msg='密码错误') elif not current_user.status: raise errors.AuthorizationError(msg='用户已锁定, 登陆失败') - captcha_code = await redis_client.get(f'{settings.CAPTCHA_LOGIN_REDIS_PREFIX}:{request.state.ip}') + captcha_code = await redis_client.get(f'{admin_settings.CAPTCHA_LOGIN_REDIS_PREFIX}:{request.state.ip}') if not captcha_code: raise errors.AuthorizationError(msg='验证码失效,请重新获取') if captcha_code.lower() != obj.captcha.lower(): raise errors.CustomError(error=CustomErrorCode.CAPTCHA_ERROR) - access_token, access_token_expire_time = await jwt.create_access_token( + access_token, access_token_expire_time = await create_access_token( str(current_user.id), multi_login=current_user.is_multi_login ) - refresh_token, refresh_token_expire_time = await jwt.create_refresh_token( + refresh_token, refresh_token_expire_time = await create_refresh_token( str(current_user.id), access_token_expire_time, multi_login=current_user.is_multi_login ) await user_dao.update_login_time(db, obj.username) @@ -85,7 +91,7 @@ class AuthService: msg='登录成功', ) background_tasks.add_task(LoginLogService.create, **login_log) - await redis_client.delete(f'{settings.CAPTCHA_LOGIN_REDIS_PREFIX}:{request.state.ip}') + await redis_client.delete(f'{admin_settings.CAPTCHA_LOGIN_REDIS_PREFIX}:{request.state.ip}') data = GetLoginToken( access_token=access_token, refresh_token=refresh_token, @@ -97,7 +103,7 @@ class AuthService: @staticmethod async def new_token(*, request: Request, refresh_token: str) -> GetNewToken: - user_id = await jwt.jwt_decode(refresh_token) + user_id = await jwt_decode(refresh_token) if request.user.id != user_id: raise errors.TokenError(msg='刷新 token 无效') async with async_db_session() as db: @@ -106,13 +112,13 @@ class AuthService: raise errors.NotFoundError(msg='用户不存在') elif not current_user.status: raise errors.AuthorizationError(msg='用户已锁定,操作失败') - current_token = await jwt.get_token(request) + current_token = await get_token(request) ( new_access_token, new_refresh_token, new_access_token_expire_time, new_refresh_token_expire_time, - ) = await jwt.create_new_token( + ) = await create_new_token( str(current_user.id), current_token, refresh_token, multi_login=current_user.is_multi_login ) data = GetNewToken( @@ -125,7 +131,7 @@ class AuthService: @staticmethod async def logout(*, request: Request) -> None: - token = await jwt.get_token(request) + token = await get_token(request) if request.user.is_multi_login: key = f'{settings.TOKEN_REDIS_PREFIX}:{request.user.id}:{token}' await redis_client.delete(key) @@ -134,4 +140,4 @@ class AuthService: await redis_client.delete_prefix(prefix) -auth_service: AuthService = AuthService() +auth_service = AuthService() diff --git a/backend/app/services/casbin_service.py b/backend/app/admin/service/casbin_service.py similarity index 89% rename from backend/app/services/casbin_service.py rename to backend/app/admin/service/casbin_service.py index 0493571..9336f30 100644 --- a/backend/app/services/casbin_service.py +++ b/backend/app/admin/service/casbin_service.py @@ -4,11 +4,8 @@ from uuid import UUID from sqlalchemy import Select -from backend.app.common.exception import errors -from backend.app.common.rbac import rbac -from backend.app.crud.crud_casbin import casbin_dao -from backend.app.database.db_mysql import async_db_session -from backend.app.schemas.casbin_rule import ( +from backend.app.admin.crud.crud_casbin import casbin_dao +from backend.app.admin.schema.casbin_rule import ( CreatePolicyParam, CreateUserRoleParam, DeleteAllPoliciesParam, @@ -16,12 +13,15 @@ from backend.app.schemas.casbin_rule import ( DeleteUserRoleParam, UpdatePolicyParam, ) +from backend.common.exception import errors +from backend.common.security.rbac import rbac +from backend.database.db_mysql import async_db_session class CasbinService: @staticmethod async def get_casbin_list(*, ptype: str, sub: str) -> Select: - return await casbin_dao.get_all_policy(ptype, sub) + return await casbin_dao.get_list(ptype, sub) @staticmethod async def get_policy_list(*, role: int | None = None) -> list: @@ -32,12 +32,6 @@ class CasbinService: data = enforcer.get_policy() return data - @staticmethod - async def get_policy_list_by_role(*, role: str) -> list: - enforcer = await rbac.enforcer() - data = enforcer.get_filtered_named_policy('p', 0, role) - return data - @staticmethod async def create_policy(*, p: CreatePolicyParam) -> bool: enforcer = await rbac.enforcer() @@ -140,4 +134,4 @@ class CasbinService: return count -casbin_service: CasbinService = CasbinService() +casbin_service = CasbinService() diff --git a/backend/app/services/dept_service.py b/backend/app/admin/service/dept_service.py similarity index 86% rename from backend/app/services/dept_service.py rename to backend/app/admin/service/dept_service.py index 4e9636d..97bb150 100644 --- a/backend/app/services/dept_service.py +++ b/backend/app/admin/service/dept_service.py @@ -2,12 +2,12 @@ # -*- coding: utf-8 -*- from typing import Any -from backend.app.common.exception import errors -from backend.app.crud.crud_dept import dept_dao -from backend.app.database.db_mysql import async_db_session -from backend.app.models import Dept -from backend.app.schemas.dept import CreateDeptParam, UpdateDeptParam -from backend.app.utils.build_tree import get_tree_data +from backend.app.admin.crud.crud_dept import dept_dao +from backend.app.admin.model import Dept +from backend.app.admin.schema.dept import CreateDeptParam, UpdateDeptParam +from backend.common.exception import errors +from backend.database.db_mysql import async_db_session +from backend.utils.build_tree import get_tree_data class DeptService: @@ -61,7 +61,7 @@ class DeptService: @staticmethod async def delete(*, pk: int) -> int: async with async_db_session.begin() as db: - dept_user = await dept_dao.get_user_relation(db, pk) + dept_user = await dept_dao.get_relation(db, pk) if dept_user: raise errors.ForbiddenError(msg='部门下存在用户,无法删除') children = await dept_dao.get_children(db, pk) @@ -71,4 +71,4 @@ class DeptService: return count -dept_service: DeptService = DeptService() +dept_service = DeptService() diff --git a/backend/app/services/dict_data_service.py b/backend/app/admin/service/dict_data_service.py similarity index 79% rename from backend/app/services/dict_data_service.py rename to backend/app/admin/service/dict_data_service.py index 9623f41..bc1b54a 100644 --- a/backend/app/services/dict_data_service.py +++ b/backend/app/admin/service/dict_data_service.py @@ -2,12 +2,12 @@ # -*- coding: utf-8 -*- from sqlalchemy import Select -from backend.app.common.exception import errors -from backend.app.crud.crud_dict_data import dict_data_dao -from backend.app.crud.crud_dict_type import dict_type_dao -from backend.app.database.db_mysql import async_db_session -from backend.app.models.sys_dict_data import DictData -from backend.app.schemas.dict_data import CreateDictDataParam, UpdateDictDataParam +from backend.app.admin.crud.crud_dict_data import dict_data_dao +from backend.app.admin.crud.crud_dict_type import dict_type_dao +from backend.app.admin.model import DictData +from backend.app.admin.schema.dict_data import CreateDictDataParam, UpdateDictDataParam +from backend.common.exception import errors +from backend.database.db_mysql import async_db_session class DictDataService: @@ -21,7 +21,7 @@ class DictDataService: @staticmethod async def get_select(*, label: str = None, value: str = None, status: int = None) -> Select: - return await dict_data_dao.get_all(label=label, value=value, status=status) + return await dict_data_dao.get_list(label=label, value=value, status=status) @staticmethod async def create(*, obj: CreateDictDataParam) -> None: @@ -56,4 +56,4 @@ class DictDataService: return count -dict_data_service: DictDataService = DictDataService() +dict_data_service = DictDataService() diff --git a/backend/app/services/dict_type_service.py b/backend/app/admin/service/dict_type_service.py similarity index 77% rename from backend/app/services/dict_type_service.py rename to backend/app/admin/service/dict_type_service.py index 59e208f..ac91537 100644 --- a/backend/app/services/dict_type_service.py +++ b/backend/app/admin/service/dict_type_service.py @@ -2,16 +2,16 @@ # -*- coding: utf-8 -*- from sqlalchemy import Select -from backend.app.common.exception import errors -from backend.app.crud.crud_dict_type import dict_type_dao -from backend.app.database.db_mysql import async_db_session -from backend.app.schemas.dict_type import CreateDictTypeParam, UpdateDictTypeParam +from backend.app.admin.crud.crud_dict_type import dict_type_dao +from backend.app.admin.schema.dict_type import CreateDictTypeParam, UpdateDictTypeParam +from backend.common.exception import errors +from backend.database.db_mysql import async_db_session class DictTypeService: @staticmethod async def get_select(*, name: str = None, code: str = None, status: int = None) -> Select: - return await dict_type_dao.get_all(name=name, code=code, status=status) + return await dict_type_dao.get_list(name=name, code=code, status=status) @staticmethod async def create(*, obj: CreateDictTypeParam) -> None: @@ -40,4 +40,4 @@ class DictTypeService: return count -dict_type_service: DictTypeService = DictTypeService() +dict_type_service = DictTypeService() diff --git a/backend/app/services/github_service.py b/backend/app/admin/service/github_service.py similarity index 74% rename from backend/app/services/github_service.py rename to backend/app/admin/service/github_service.py index 3fdca28..17af43f 100644 --- a/backend/app/services/github_service.py +++ b/backend/app/admin/service/github_service.py @@ -3,24 +3,26 @@ from fast_captcha import text_captcha from fastapi import BackgroundTasks, Request -from backend.app.common import jwt -from backend.app.common.enums import LoginLogStatusType, UserSocialType -from backend.app.common.exception.errors import AuthorizationError -from backend.app.common.redis import redis_client -from backend.app.core.conf import settings -from backend.app.crud.crud_user import user_dao -from backend.app.crud.crud_user_social import user_social_dao -from backend.app.database.db_mysql import async_db_session -from backend.app.schemas.token import GetLoginToken -from backend.app.schemas.user import RegisterUserParam -from backend.app.schemas.user_social import CreateUserSocialParam -from backend.app.services.login_log_service import LoginLogService -from backend.app.utils.timezone import timezone +from backend.app.admin.conf import admin_settings +from backend.app.admin.crud.crud_user import user_dao +from backend.app.admin.crud.crud_user_social import user_social_dao +from backend.app.admin.schema.token import GetLoginToken +from backend.app.admin.schema.user import RegisterUserParam +from backend.app.admin.schema.user_social import CreateUserSocialParam +from backend.app.admin.service.login_log_service import LoginLogService +from backend.common.enums import LoginLogStatusType, UserSocialType +from backend.common.exception.errors import AuthorizationError +from backend.common.security import jwt +from backend.database.db_mysql import async_db_session +from backend.database.db_redis import redis_client +from backend.utils.timezone import timezone class GithubService: @staticmethod - async def add_with_login(request: Request, background_tasks: BackgroundTasks, user: dict) -> GetLoginToken | None: + async def create_with_login( + request: Request, background_tasks: BackgroundTasks, user: dict + ) -> GetLoginToken | None: async with async_db_session.begin() as db: github_email = user['email'] if not github_email: @@ -68,7 +70,7 @@ class GithubService: msg='登录成功(OAuth2)', ) background_tasks.add_task(LoginLogService.create, **login_log) - await redis_client.delete(f'{settings.CAPTCHA_LOGIN_REDIS_PREFIX}:{request.state.ip}') + await redis_client.delete(f'{admin_settings.CAPTCHA_LOGIN_REDIS_PREFIX}:{request.state.ip}') data = GetLoginToken( access_token=access_token, refresh_token=refresh_token, @@ -79,4 +81,4 @@ class GithubService: return data -github_service: GithubService = GithubService() +github_service = GithubService() diff --git a/backend/app/services/login_log_service.py b/backend/app/admin/service/login_log_service.py similarity index 81% rename from backend/app/services/login_log_service.py rename to backend/app/admin/service/login_log_service.py index db21c36..ffc5148 100644 --- a/backend/app/services/login_log_service.py +++ b/backend/app/admin/service/login_log_service.py @@ -6,17 +6,17 @@ from fastapi import Request from sqlalchemy import Select from sqlalchemy.ext.asyncio import AsyncSession -from backend.app.common.log import log -from backend.app.crud.crud_login_log import login_log_dao -from backend.app.database.db_mysql import async_db_session -from backend.app.models import User -from backend.app.schemas.login_log import CreateLoginLogParam +from backend.app.admin.crud.crud_login_log import login_log_dao +from backend.app.admin.model import User +from backend.app.admin.schema.login_log import CreateLoginLogParam +from backend.common.log import log +from backend.database.db_mysql import async_db_session class LoginLogService: @staticmethod async def get_select(*, username: str, status: int, ip: str) -> Select: - return await login_log_dao.get_all(username=username, status=status, ip=ip) + return await login_log_dao.get_list(username=username, status=status, ip=ip) @staticmethod async def create( @@ -56,4 +56,4 @@ class LoginLogService: return count -login_log_service: LoginLogService = LoginLogService() +login_log_service = LoginLogService() diff --git a/backend/app/services/menu_service.py b/backend/app/admin/service/menu_service.py similarity index 87% rename from backend/app/services/menu_service.py rename to backend/app/admin/service/menu_service.py index 8f7013c..01f583d 100644 --- a/backend/app/services/menu_service.py +++ b/backend/app/admin/service/menu_service.py @@ -4,15 +4,15 @@ from typing import Any from fastapi import Request -from backend.app.common.exception import errors -from backend.app.common.redis import redis_client -from backend.app.core.conf import settings -from backend.app.crud.crud_menu import menu_dao -from backend.app.crud.crud_role import role_dao -from backend.app.database.db_mysql import async_db_session -from backend.app.models import Menu -from backend.app.schemas.menu import CreateMenuParam, UpdateMenuParam -from backend.app.utils.build_tree import get_tree_data +from backend.app.admin.crud.crud_menu import menu_dao +from backend.app.admin.crud.crud_role import role_dao +from backend.app.admin.model import Menu +from backend.app.admin.schema.menu import CreateMenuParam, UpdateMenuParam +from backend.common.exception import errors +from backend.core.conf import settings +from backend.database.db_mysql import async_db_session +from backend.database.db_redis import redis_client +from backend.utils.build_tree import get_tree_data class MenuService: @@ -96,4 +96,4 @@ class MenuService: return count -menu_service: MenuService = MenuService() +menu_service = MenuService() diff --git a/backend/app/services/opera_log_service.py b/backend/app/admin/service/opera_log_service.py similarity index 71% rename from backend/app/services/opera_log_service.py rename to backend/app/admin/service/opera_log_service.py index ecc99c5..870b8dc 100644 --- a/backend/app/services/opera_log_service.py +++ b/backend/app/admin/service/opera_log_service.py @@ -2,15 +2,15 @@ # -*- coding: utf-8 -*- from sqlalchemy import Select -from backend.app.crud.crud_opera_log import opera_log_dao -from backend.app.database.db_mysql import async_db_session -from backend.app.schemas.opera_log import CreateOperaLogParam +from backend.app.admin.crud.crud_opera_log import opera_log_dao +from backend.app.admin.schema.opera_log import CreateOperaLogParam +from backend.database.db_mysql import async_db_session class OperaLogService: @staticmethod async def get_select(*, username: str | None = None, status: int | None = None, ip: str | None = None) -> Select: - return await opera_log_dao.get_all(username=username, status=status, ip=ip) + return await opera_log_dao.get_list(username=username, status=status, ip=ip) @staticmethod async def create(*, obj_in: CreateOperaLogParam): @@ -30,4 +30,4 @@ class OperaLogService: return count -opera_log_service: OperaLogService = OperaLogService() +opera_log_service = OperaLogService() diff --git a/backend/app/services/role_service.py b/backend/app/admin/service/role_service.py similarity index 83% rename from backend/app/services/role_service.py rename to backend/app/admin/service/role_service.py index 6bfcd3b..d95e10b 100644 --- a/backend/app/services/role_service.py +++ b/backend/app/admin/service/role_service.py @@ -5,14 +5,14 @@ from typing import Sequence from fastapi import Request from sqlalchemy import Select -from backend.app.common.exception import errors -from backend.app.common.redis import redis_client -from backend.app.core.conf import settings -from backend.app.crud.crud_menu import menu_dao -from backend.app.crud.crud_role import role_dao -from backend.app.database.db_mysql import async_db_session -from backend.app.models import Role -from backend.app.schemas.role import CreateRoleParam, UpdateRoleMenuParam, UpdateRoleParam +from backend.app.admin.crud.crud_menu import menu_dao +from backend.app.admin.crud.crud_role import role_dao +from backend.app.admin.model import Role +from backend.app.admin.schema.role import CreateRoleParam, UpdateRoleMenuParam, UpdateRoleParam +from backend.common.exception import errors +from backend.core.conf import settings +from backend.database.db_mysql import async_db_session +from backend.database.db_redis import redis_client class RoleService: @@ -33,7 +33,7 @@ class RoleService: @staticmethod async def get_user_roles(*, pk: int) -> Sequence[Role]: async with async_db_session() as db: - roles = await role_dao.get_user_all(db, user_id=pk) + roles = await role_dao.get_user_roles(db, user_id=pk) return roles @staticmethod @@ -82,4 +82,4 @@ class RoleService: return count -role_service: RoleService = RoleService() +role_service = RoleService() diff --git a/backend/app/services/user_service.py b/backend/app/admin/service/user_service.py similarity index 93% rename from backend/app/services/user_service.py rename to backend/app/admin/service/user_service.py index 35552c7..e27205b 100644 --- a/backend/app/services/user_service.py +++ b/backend/app/admin/service/user_service.py @@ -5,16 +5,11 @@ import random from fastapi import Request from sqlalchemy import Select -from backend.app.common.exception import errors -from backend.app.common.jwt import get_token, password_verify, superuser_verify -from backend.app.common.redis import redis_client -from backend.app.core.conf import settings -from backend.app.crud.crud_dept import dept_dao -from backend.app.crud.crud_role import role_dao -from backend.app.crud.crud_user import user_dao -from backend.app.database.db_mysql import async_db_session -from backend.app.models import User -from backend.app.schemas.user import ( +from backend.app.admin.crud.crud_dept import dept_dao +from backend.app.admin.crud.crud_role import role_dao +from backend.app.admin.crud.crud_user import user_dao +from backend.app.admin.model import User +from backend.app.admin.schema.user import ( AddUserParam, AvatarParam, RegisterUserParam, @@ -22,6 +17,11 @@ from backend.app.schemas.user import ( UpdateUserParam, UpdateUserRoleParam, ) +from backend.common.exception import errors +from backend.common.security.jwt import get_token, password_verify, superuser_verify +from backend.core.conf import settings +from backend.database.db_mysql import async_db_session +from backend.database.db_redis import redis_client class UserService: @@ -147,7 +147,7 @@ class UserService: @staticmethod async def get_select(*, dept: int, username: str = None, phone: str = None, status: int = None) -> Select: - return await user_dao.get_all(dept=dept, username=username, phone=phone, status=status) + return await user_dao.get_list(dept=dept, username=username, phone=phone, status=status) @staticmethod async def update_permission(*, request: Request, pk: int) -> int: @@ -225,4 +225,4 @@ class UserService: return count -user_service: UserService = UserService() +user_service = UserService() diff --git a/backend/app/database/__init__.py b/backend/app/admin/tests/__init__.py similarity index 100% rename from backend/app/database/__init__.py rename to backend/app/admin/tests/__init__.py diff --git a/backend/app/middleware/__init__.py b/backend/app/admin/tests/api_v1/__init__.py similarity index 100% rename from backend/app/middleware/__init__.py rename to backend/app/admin/tests/api_v1/__init__.py diff --git a/backend/app/admin/tests/api_v1/test_auth.py b/backend/app/admin/tests/api_v1/test_auth.py new file mode 100644 index 0000000..4eb9e52 --- /dev/null +++ b/backend/app/admin/tests/api_v1/test_auth.py @@ -0,0 +1,11 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +from starlette.testclient import TestClient + +from backend.core.conf import settings + + +def test_logout(client: TestClient, token_headers: dict[str, str]) -> None: + response = client.post(f'{settings.API_V1_STR}/auth/logout', headers=token_headers) + assert response.status_code == 200 + assert response.json()['code'] == 200 diff --git a/backend/app/tests/conftest.py b/backend/app/admin/tests/conftest.py similarity index 69% rename from backend/app/tests/conftest.py rename to backend/app/admin/tests/conftest.py index 7bb6a1c..04a2fb5 100644 --- a/backend/app/tests/conftest.py +++ b/backend/app/admin/tests/conftest.py @@ -1,19 +1,15 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -import sys - -sys.path.append('../../') - from typing import Dict, Generator import pytest from starlette.testclient import TestClient -from backend.app.database.db_mysql import get_db -from backend.app.main import app -from backend.app.tests.utils.db_mysql import override_get_db -from backend.app.tests.utils.get_headers import get_token_headers +from backend.app.admin.tests.utils.db_mysql import override_get_db +from backend.app.admin.tests.utils.get_headers import get_token_headers +from backend.database.db_mysql import get_db +from backend.main import app app.dependency_overrides[get_db] = override_get_db diff --git a/backend/app/schemas/__init__.py b/backend/app/admin/tests/utils/__init__.py similarity index 100% rename from backend/app/schemas/__init__.py rename to backend/app/admin/tests/utils/__init__.py diff --git a/backend/app/admin/tests/utils/db_mysql.py b/backend/app/admin/tests/utils/db_mysql.py new file mode 100644 index 0000000..7774031 --- /dev/null +++ b/backend/app/admin/tests/utils/db_mysql.py @@ -0,0 +1,25 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +from sqlalchemy.ext.asyncio import AsyncSession + +from backend.core.conf import settings +from backend.database.db_mysql import create_engine_and_session + +TEST_SQLALCHEMY_DATABASE_URL = ( + f'mysql+asyncmy://{settings.MYSQL_USER}:{settings.MYSQL_PASSWORD}@{settings.MYSQL_HOST}:' + f'{settings.MYSQL_PORT}/{settings.MYSQL_DATABASE}_test?charset={settings.MYSQL_CHARSET}' +) + +_, test_async_db_session = create_engine_and_session(TEST_SQLALCHEMY_DATABASE_URL) + + +async def override_get_db() -> AsyncSession: + """session 生成器""" + session = test_async_db_session() + try: + yield session + except Exception as se: + await session.rollback() + raise se + finally: + await session.close() diff --git a/backend/app/tests/utils/get_headers.py b/backend/app/admin/tests/utils/get_headers.py similarity index 62% rename from backend/app/tests/utils/get_headers.py rename to backend/app/admin/tests/utils/get_headers.py index 5e582cb..8c28102 100644 --- a/backend/app/tests/utils/get_headers.py +++ b/backend/app/admin/tests/utils/get_headers.py @@ -1,18 +1,16 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -from typing import Dict - +#!/usr/bin/env from typing import Dict from starlette.testclient import TestClient -from backend.app.core.conf import settings +from backend.core.conf import settings -def get_token_headers(client: TestClient, username: str, password: str) -> Dict[str, str]: +def get_token_headers(client: TestClient, username: str, password: str) -> dict[str, str]: data = { 'username': username, 'password': password, } - response = client.post(f'{settings.API_V1_STR}/auth/swagger_login', data=data) + response = client.post(f'{settings.API_V1_STR}/auth/login/swagger', params=data) + response.raise_for_status() token_type = response.json()['token_type'] access_token = response.json()['access_token'] headers = {'Authorization': f'{token_type} {access_token}'} diff --git a/backend/app/api/routers.py b/backend/app/api/routers.py deleted file mode 100644 index fcc423b..0000000 --- a/backend/app/api/routers.py +++ /dev/null @@ -1,22 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -from fastapi import APIRouter - -from backend.app.api.v1.auth import router as auth_router -from backend.app.api.v1.log import router as log_router -from backend.app.api.v1.mixed import router as mixed_router -from backend.app.api.v1.monitor import router as monitor_router -from backend.app.api.v1.sys import router as sys_router -from backend.app.api.v1.task import router as task_router -from backend.app.core.conf import settings - -v1 = APIRouter(prefix=settings.API_V1_STR) - -# 集合 -v1.include_router(auth_router) -v1.include_router(sys_router) -v1.include_router(log_router) -v1.include_router(monitor_router) -v1.include_router(mixed_router) -# 独立 -v1.include_router(task_router, prefix='/tasks', tags=['任务管理']) diff --git a/backend/app/api/v1/auth/__init__.py b/backend/app/api/v1/auth/__init__.py deleted file mode 100644 index 06c724c..0000000 --- a/backend/app/api/v1/auth/__init__.py +++ /dev/null @@ -1,13 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -from fastapi import APIRouter - -from backend.app.api.v1.auth.auth import router as auth_router -from backend.app.api.v1.auth.captcha import router as captcha_router -from backend.app.api.v1.auth.github import router as github_router - -router = APIRouter(prefix='/auth', tags=['授权管理']) - -router.include_router(auth_router) -router.include_router(captcha_router) -router.include_router(github_router) diff --git a/backend/app/api/v1/mixed/__init__.py b/backend/app/api/v1/mixed/__init__.py deleted file mode 100644 index f84e40e..0000000 --- a/backend/app/api/v1/mixed/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -from fastapi import APIRouter - -from backend.app.api.v1.mixed.config import router as config_router -from backend.app.api.v1.mixed.tests import router as upload_router - -router = APIRouter(prefix='/mixes', tags=['杂项']) - -router.include_router(config_router) -router.include_router(upload_router) diff --git a/backend/app/api/v1/mixed/config.py b/backend/app/api/v1/mixed/config.py deleted file mode 100644 index 438dca2..0000000 --- a/backend/app/api/v1/mixed/config.py +++ /dev/null @@ -1,33 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -from fastapi import APIRouter, Depends, Request -from fastapi.routing import APIRoute - -from backend.app.common.permission import RequestPermission -from backend.app.common.rbac import DependsRBAC -from backend.app.common.response.response_schema import ResponseModel, response_base - -router = APIRouter() - - -@router.get( - '/routes', - summary='获取所有路由', - dependencies=[ - Depends(RequestPermission('sys:route:list')), - DependsRBAC, - ], -) -async def get_all_route(request: Request) -> ResponseModel: - data = [] - for route in request.app.routes: - if isinstance(route, APIRoute): - data.append( - { - 'path': route.path, - 'name': route.name, - 'summary': route.summary, - 'methods': route.methods, - } - ) - return await response_base.success(data={'route_list': data}) diff --git a/backend/app/api/v1/mixed/tests.py b/backend/app/api/v1/mixed/tests.py deleted file mode 100644 index ccf037f..0000000 --- a/backend/app/api/v1/mixed/tests.py +++ /dev/null @@ -1,31 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -from typing import Annotated - -from fastapi import APIRouter, File, Form, UploadFile - -from backend.app.common.response.response_schema import ResponseModel, response_base -from backend.app.tasks import task_demo_async - -router = APIRouter(prefix='/tests') - - -@router.post('/send', summary='异步任务演示') -async def send_task() -> ResponseModel: - result = task_demo_async.delay() - return await response_base.success(data=result.id) - - -@router.post('/files', summary='上传文件演示') -async def create_file( - file: Annotated[bytes, File()], - fileb: Annotated[UploadFile, File()], - token: Annotated[str, Form()], -) -> ResponseModel: - return ResponseModel( - data={ - 'file_size': len(file), - 'token': token, - 'fileb_content_type': fileb.content_type, - } - ) diff --git a/backend/app/api/v1/monitor/__init__.py b/backend/app/api/v1/monitor/__init__.py deleted file mode 100644 index 8bb3ae6..0000000 --- a/backend/app/api/v1/monitor/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -from fastapi import APIRouter - -from backend.app.api.v1.monitor.redis import router as redis_router -from backend.app.api.v1.monitor.server import router as server_router - -router = APIRouter(prefix='/monitors', tags=['监控管理']) - -router.include_router(redis_router) -router.include_router(server_router) diff --git a/backend/app/api/v1/task.py b/backend/app/api/v1/task.py deleted file mode 100644 index b43968b..0000000 --- a/backend/app/api/v1/task.py +++ /dev/null @@ -1,45 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -from typing import Annotated - -from fastapi import APIRouter, Body, Depends, Path - -from backend.app.common.jwt import DependsJwtAuth -from backend.app.common.permission import RequestPermission -from backend.app.common.rbac import DependsRBAC -from backend.app.common.response.response_code import CustomResponseCode -from backend.app.common.response.response_schema import ResponseModel, response_base -from backend.app.services.task_service import task_service - -router = APIRouter() - - -@router.get('', summary='获取所有可执行任务模块', dependencies=[DependsJwtAuth]) -async def get_all_tasks() -> ResponseModel: - tasks = task_service.get_task_list() - return await response_base.success(data=tasks) - - -@router.get('/{pk}', summary='获取任务结果', dependencies=[DependsJwtAuth]) -async def get_task_result(pk: Annotated[str, Path(description='任务ID')]) -> ResponseModel: - task = task_service.get(pk) - if not task: - return await response_base.fail(res=CustomResponseCode.HTTP_204, data=pk) - return await response_base.success(data=task.result) - - -@router.post( - '/{module}', - summary='执行任务', - dependencies=[ - Depends(RequestPermission('sys:task:run')), - DependsRBAC, - ], -) -async def run_task( - module: Annotated[str, Path(description='任务模块')], - args: Annotated[list | None, Body()] = None, - kwargs: Annotated[dict | None, Body()] = None, -) -> ResponseModel: - task = task_service.run(module=module, args=args, kwargs=kwargs) - return await response_base.success(data=task.result) diff --git a/backend/app/celery-start.sh b/backend/app/celery-start.sh deleted file mode 100644 index a44f2cb..0000000 --- a/backend/app/celery-start.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/usr/bin/env bash - -celery -A tasks worker --loglevel=INFO -B diff --git a/backend/app/common/celery.py b/backend/app/common/celery.py deleted file mode 100644 index 559992d..0000000 --- a/backend/app/common/celery.py +++ /dev/null @@ -1,55 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -from celery import Celery - -from backend.app.core.conf import settings - -__all__ = ['celery_app'] - - -def make_celery(main_name: str) -> Celery: - """ - 创建 celery 应用 - - :param main_name: __main__ module name - :return: - """ - app = Celery(main_name) - app.autodiscover_tasks(packages=['backend.app']) - - # Celery Config - # https://docs.celeryq.dev/en/stable/userguide/configuration.html - app.conf.broker_url = ( - ( - f'redis://:{settings.CELERY_REDIS_PASSWORD}@{settings.CELERY_REDIS_HOST}:' - f'{settings.CELERY_REDIS_PORT}/{settings.CELERY_BROKER_REDIS_DATABASE}' - ) - if settings.CELERY_BROKER == 'redis' - else ( - f'amqp://{settings.RABBITMQ_USERNAME}:{settings.RABBITMQ_PASSWORD}@{settings.RABBITMQ_HOST}:' - f'{settings.RABBITMQ_PORT}' - ) - ) - app.conf.result_backend = ( - f'redis://:{settings.CELERY_REDIS_PASSWORD}@{settings.CELERY_REDIS_HOST}:' - f'{settings.CELERY_REDIS_PORT}/{settings.CELERY_BACKEND_REDIS_DATABASE}' - ) - app.conf.result_backend_transport_options = { - 'global_keyprefix': settings.CELERY_BACKEND_REDIS_PREFIX, - 'retry_policy': { - 'timeout': settings.CELERY_BACKEND_REDIS_TIMEOUT, - }, - 'result_chord_ordered': settings.CELERY_BACKEND_REDIS_ORDERED, - } - app.conf.timezone = settings.DATETIME_TIMEZONE - app.conf.task_track_started = True - - # Celery Schedule Tasks - # https://docs.celeryq.dev/en/stable/userguide/periodic-tasks.html - app.conf.beat_schedule = settings.CELERY_BEAT_SCHEDULE - app.conf.beat_schedule_filename = settings.CELERY_BEAT_SCHEDULE_FILENAME - - return app - - -celery_app = make_celery('celery_app') diff --git a/backend/app/core/path_conf.py b/backend/app/core/path_conf.py deleted file mode 100644 index 97cede4..0000000 --- a/backend/app/core/path_conf.py +++ /dev/null @@ -1,18 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -import os - -from pathlib import Path - -# 获取项目根目录 -# 或使用绝对路径,指到backend目录为止,例如windows:BasePath = D:\git_project\fastapi_mysql\backend -BasePath = Path(__file__).resolve().parent.parent.parent - -# 迁移文件存放路径 -Versions = os.path.join(BasePath, 'app', 'alembic', 'versions') - -# 日志文件路径 -LogPath = os.path.join(BasePath, 'app', 'log') - -# 离线 IP 数据库路径 -IP2REGION_XDB = os.path.join(BasePath, 'app', 'static', 'ip2region.xdb') diff --git a/backend/app/models/__init__.py b/backend/app/models/__init__.py deleted file mode 100644 index 24ecdba..0000000 --- a/backend/app/models/__init__.py +++ /dev/null @@ -1,18 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -# 导入所有模型,并将 Base 放在最前面, 以便 Base 拥有它们 -# imported by Alembic -""" -from backend.app.models.base import MappedBase -from backend.app.models.sys_api import Api -from backend.app.models.sys_casbin_rule import CasbinRule -from backend.app.models.sys_dept import Dept -from backend.app.models.sys_dict_data import DictData -from backend.app.models.sys_dict_type import DictType -from backend.app.models.sys_login_log import LoginLog -from backend.app.models.sys_menu import Menu -from backend.app.models.sys_opera_log import OperaLog -from backend.app.models.sys_role import Role -from backend.app.models.sys_user import User -from backend.app.models.sys_user_social import UserSocial diff --git a/backend/app/router.py b/backend/app/router.py new file mode 100644 index 0000000..26adf14 --- /dev/null +++ b/backend/app/router.py @@ -0,0 +1,12 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +from fastapi import APIRouter + +from backend.app.admin.api.router import v1 as admin_v1 +from backend.app.task.api.router import v1 as task_v1 +from backend.core.conf import settings + +route = APIRouter(prefix=settings.API_V1_STR) + +route.include_router(admin_v1) +route.include_router(task_v1) diff --git a/backend/app/services/task_service.py b/backend/app/services/task_service.py deleted file mode 100644 index def0c59..0000000 --- a/backend/app/services/task_service.py +++ /dev/null @@ -1,36 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -from celery.exceptions import BackendGetMetaError, NotRegistered -from celery.result import AsyncResult - -from backend.app.common.celery import celery_app -from backend.app.common.exception.errors import NotFoundError - - -class TaskService: - @staticmethod - def get(pk: str) -> AsyncResult | None: - try: - result = celery_app.AsyncResult(pk) - except (BackendGetMetaError, NotRegistered): - raise NotFoundError(msg='任务不存在') - if result.failed(): - return None - return result - - @staticmethod - def get_task_list() -> dict: - filtered_tasks = {} - tasks = celery_app.tasks - for key, value in tasks.items(): - if not key.startswith('celery.'): - filtered_tasks[key] = value - return filtered_tasks - - @staticmethod - def run(*, module: str, args: list | None = None, kwargs: dict | None = None) -> AsyncResult: - task = celery_app.send_task(module, args, kwargs) - return task - - -task_service: TaskService = TaskService() diff --git a/backend/app/task/__init__.py b/backend/app/task/__init__.py new file mode 100644 index 0000000..f041604 --- /dev/null +++ b/backend/app/task/__init__.py @@ -0,0 +1,8 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +import sys + +from pathlib import Path + +# 导入项目根目录 +sys.path.append(str(Path(__file__).resolve().parent.parent.parent.parent)) diff --git a/backend/app/services/__init__.py b/backend/app/task/api/__init__.py similarity index 100% rename from backend/app/services/__init__.py rename to backend/app/task/api/__init__.py diff --git a/backend/app/task/api/router.py b/backend/app/task/api/router.py new file mode 100644 index 0000000..b038e51 --- /dev/null +++ b/backend/app/task/api/router.py @@ -0,0 +1,9 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +from fastapi import APIRouter + +from backend.app.task.api.v1.task import router as task_router + +v1 = APIRouter() + +v1.include_router(task_router, prefix='/tasks', tags=['任务管理']) diff --git a/backend/app/tests/__init__.py b/backend/app/task/api/v1/__init__.py similarity index 100% rename from backend/app/tests/__init__.py rename to backend/app/task/api/v1/__init__.py diff --git a/backend/app/task/api/v1/task.py b/backend/app/task/api/v1/task.py new file mode 100644 index 0000000..9187333 --- /dev/null +++ b/backend/app/task/api/v1/task.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +from typing import Annotated + +from fastapi import APIRouter, Body, Depends, Path + +from backend.app.task.service.task_service import task_service +from backend.common.response.response_schema import ResponseModel, response_base +from backend.common.security.jwt import DependsJwtAuth +from backend.common.security.permission import RequestPermission +from backend.common.security.rbac import DependsRBAC + +router = APIRouter() + + +@router.get('', summary='获取所有可执行任务模块', dependencies=[DependsJwtAuth]) +async def get_all_tasks() -> ResponseModel: + tasks = task_service.get_list() + return await response_base.success(data=tasks) + + +@router.get('/current', summary='获取当前正在执行的任务', dependencies=[DependsJwtAuth]) +async def get_current_task() -> ResponseModel: + task = task_service.get() + return await response_base.success(data=task) + + +@router.get('/{uid}/status', summary='获取任务状态', dependencies=[DependsJwtAuth]) +async def get_task_status(uid: Annotated[str, Path(description='任务ID')]) -> ResponseModel: + status = task_service.get_status(uid) + return await response_base.success(data=status) + + +@router.get('/{uid}', summary='获取任务结果', dependencies=[DependsJwtAuth]) +async def get_task_result(uid: Annotated[str, Path(description='任务ID')]) -> ResponseModel: + task = task_service.get_result(uid) + return await response_base.success(data=task) + + +@router.post( + '/{name}', + summary='执行任务', + dependencies=[ + Depends(RequestPermission('sys:task:run')), + DependsRBAC, + ], +) +async def run_task( + name: Annotated[str, Path(description='任务名称')], + args: Annotated[list | None, Body(description='任务函数位置参数')] = None, + kwargs: Annotated[dict | None, Body(description='任务函数关键字参数')] = None, +) -> ResponseModel: + task = task_service.run(name=name, args=args, kwargs=kwargs) + return await response_base.success(data=task) diff --git a/backend/app/task/celery.py b/backend/app/task/celery.py new file mode 100644 index 0000000..f1e199c --- /dev/null +++ b/backend/app/task/celery.py @@ -0,0 +1,59 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +from celery import Celery + +from backend.app.task.conf import task_settings +from backend.core.conf import settings + +__all__ = ['celery_app'] + + +def init_celery() -> Celery: + """创建 celery 应用""" + + app = Celery('fba_celery') + + # Celery Config + # https://docs.celeryq.dev/en/stable/userguide/configuration.html + _redis_broker = ( + f'redis://:{settings.REDIS_PASSWORD}@{settings.REDIS_HOST}:' + f'{settings.REDIS_PORT}/{task_settings.CELERY_BROKER_REDIS_DATABASE}' + ) + _amqp_broker = ( + 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, + }, + } + + # Celery Schedule Tasks + # https://docs.celeryq.dev/en/stable/userguide/periodic-tasks.html + _beat_schedule = task_settings.CELERY_SCHEDULE + + # Update celery settings + app.conf.update( + broker_url=_redis_broker if task_settings.CELERY_BROKER == 'redis' else _amqp_broker, + result_backend=_result_backend, + result_backend_transport_options=_result_backend_transport_options, + timezone=settings.DATETIME_TIMEZONE, + enable_utc=False, + task_track_started=True, + beat_schedule=_beat_schedule, + ) + + # Load task modules + app.autodiscover_tasks(task_settings.CELERY_TASKS_PACKAGES) + + return app + + +# 创建 celery 实例 +celery_app = init_celery() diff --git a/backend/app/tests/api_v1/__init__.py b/backend/app/task/celery_task/__init__.py similarity index 100% rename from backend/app/tests/api_v1/__init__.py rename to backend/app/task/celery_task/__init__.py diff --git a/backend/app/tests/utils/__init__.py b/backend/app/task/celery_task/db_log/__init__.py similarity index 100% rename from backend/app/tests/utils/__init__.py rename to backend/app/task/celery_task/db_log/__init__.py diff --git a/backend/app/task/celery_task/db_log/tasks.py b/backend/app/task/celery_task/db_log/tasks.py new file mode 100644 index 0000000..22395e2 --- /dev/null +++ b/backend/app/task/celery_task/db_log/tasks.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +from sqlalchemy.exc import SQLAlchemyError + +from backend.app.admin.service.login_log_service import login_log_service +from backend.app.admin.service.opera_log_service import opera_log_service +from backend.app.task.celery import celery_app +from backend.app.task.conf import task_settings + + +@celery_app.task( + name='auto_delete_db_opera_log', + bind=True, + retry_backoff=True, + max_retries=task_settings.CELERY_TASK_MAX_RETRIES, +) +async def auto_delete_db_opera_log(self) -> int: + """自动删除数据库操作日志""" + try: + result = await opera_log_service.delete_all() + except SQLAlchemyError as exc: + raise self.retry(exc=exc) + return result + + +@celery_app.task( + name='auto_delete_db_login_log', + bind=True, + retry_backoff=True, + max_retries=task_settings.CELERY_TASK_MAX_RETRIES, +) +async def auto_delete_db_login_log(self) -> int: + """自动删除数据库登录日志""" + + try: + result = await login_log_service.delete_all() + except SQLAlchemyError as exc: + raise self.retry(exc=exc) + return result diff --git a/backend/app/tasks.py b/backend/app/task/celery_task/tasks.py similarity index 60% rename from backend/app/tasks.py rename to backend/app/task/celery_task/tasks.py index 213d344..deb7256 100644 --- a/backend/app/tasks.py +++ b/backend/app/task/celery_task/tasks.py @@ -1,14 +1,11 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -import sys import uuid -sys.path.append('../../') - -from backend.app.common.celery import celery_app # noqa: E402 +from backend.app.task.celery import celery_app -@celery_app.task +@celery_app.task(name='task_demo_async') def task_demo_async() -> str: uid = uuid.uuid4().hex print(f'异步任务 {uid} 执行成功') diff --git a/backend/app/task/conf.py b/backend/app/task/conf.py new file mode 100644 index 0000000..c5df638 --- /dev/null +++ b/backend/app/task/conf.py @@ -0,0 +1,69 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +from functools import lru_cache +from typing import Literal + +from celery.schedules import crontab +from pydantic import model_validator +from pydantic_settings import BaseSettings, SettingsConfigDict + +from backend.core.path_conf import BasePath + + +class TaskSettings(BaseSettings): + """Task Settings""" + + model_config = SettingsConfigDict(env_file=f'{BasePath}/.env', env_file_encoding='utf-8', extra='ignore') + + # Env Config + ENVIRONMENT: Literal['dev', 'pro'] + + # Env Celery + CELERY_BROKER_REDIS_DATABASE: int # 仅当使用 redis 作为 broker 时生效, 更适用于测试环境 + CELERY_BACKEND_REDIS_DATABASE: int + + # 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_BROKER: Literal['rabbitmq', 'redis'] = 'redis' + CELERY_BACKEND_REDIS_PREFIX: str = 'fba_celery' + CELERY_BACKEND_REDIS_TIMEOUT: float = 5.0 + CELERY_TASKS_PACKAGES: list[str] = [ + 'app.task.celery_task', + 'app.task.celery_task.db_log', + ] + CELERY_TASK_MAX_RETRIES: int = 5 + CELERY_SCHEDULE: dict = { + 'exec-every-10-seconds': { + 'task': 'task_demo_async', + 'schedule': 10, + }, + 'exec-every-sunday': { + 'task': 'auto_delete_db_opera_log', + 'schedule': crontab(0, 0, day_of_week='6'), # type: ignore + }, + 'exec-every-15-of-month': { + 'task': 'auto_delete_db_login_log', + 'schedule': crontab(0, 0, day_of_month='15'), # type: ignore + }, + } + + @model_validator(mode='before') + def validate_celery_broker(cls, values): + if values['ENVIRONMENT'] == 'pro': + values['CELERY_BROKER'] = 'rabbitmq' + return values + + +@lru_cache +def get_task_settings() -> TaskSettings: + """获取 task 配置""" + return TaskSettings() + + +task_settings = get_task_settings() diff --git a/backend/app/utils/__init__.py b/backend/app/task/service/__init__.py similarity index 100% rename from backend/app/utils/__init__.py rename to backend/app/task/service/__init__.py diff --git a/backend/app/task/service/task_service.py b/backend/app/task/service/task_service.py new file mode 100644 index 0000000..ef4745e --- /dev/null +++ b/backend/app/task/service/task_service.py @@ -0,0 +1,46 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +from celery.exceptions import NotRegistered +from celery.result import AsyncResult + +from backend.app.task.celery import celery_app +from backend.common.exception.errors import NotFoundError + + +class TaskService: + @staticmethod + def get_list(): + filtered_tasks = [] + tasks = celery_app.tasks + for key, value in tasks.items(): + if not key.startswith('celery.'): + filtered_tasks.append({key, value}) + return filtered_tasks + + @staticmethod + def get(): + return celery_app.current_worker_task + + @staticmethod + def get_status(uid: str): + try: + result = AsyncResult(id=uid, app=celery_app) + except NotRegistered: + raise NotFoundError(msg='任务不存在') + return result.status + + @staticmethod + def get_result(uid: str): + try: + result = AsyncResult(id=uid, app=celery_app) + except NotRegistered: + raise NotFoundError(msg='任务不存在') + return result + + @staticmethod + def run(*, name: str, args: list | None = None, kwargs: dict | None = None): + task = celery_app.send_task(name=name, args=args, kwargs=kwargs) + return task + + +task_service = TaskService() diff --git a/backend/app/tests/api_v1/test_auth.py b/backend/app/tests/api_v1/test_auth.py deleted file mode 100644 index 9484115..0000000 --- a/backend/app/tests/api_v1/test_auth.py +++ /dev/null @@ -1,22 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -from starlette.testclient import TestClient - -from backend.app.core.conf import settings -from backend.app.tests.conftest import PYTEST_PASSWORD, PYTEST_USERNAME - - -def test_login(client: TestClient) -> None: - data = { - 'username': PYTEST_USERNAME, - 'password': PYTEST_PASSWORD, - } - response = client.post(f'{settings.API_V1_STR}/auth/swagger_login', data=data) - assert response.status_code == 200 - assert response.json()['token_type'] == 'Bearer' - - -def test_logout(client: TestClient, token_headers: dict[str, str]) -> None: - response = client.post(f'{settings.API_V1_STR}/auth/logout', headers=token_headers) - assert response.status_code == 200 - assert response.json()['code'] == 200 diff --git a/backend/app/tests/utils/db_mysql.py b/backend/app/tests/utils/db_mysql.py deleted file mode 100644 index 90720ff..0000000 --- a/backend/app/tests/utils/db_mysql.py +++ /dev/null @@ -1,27 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -from sqlalchemy.ext.asyncio import AsyncSession - -from backend.app.core.conf import settings -from backend.app.database.db_mysql import create_engine_and_session - -TEST_DB_DATABASE = settings.DB_DATABASE + '_test' - -TEST_SQLALCHEMY_DATABASE_URL = ( - f'mysql+asyncmy://{settings.DB_USER}:{settings.DB_PASSWORD}@{settings.DB_HOST}:' - f'{settings.DB_PORT}/{TEST_DB_DATABASE}?charset={settings.DB_CHARSET}' -) - -test_async_engine, test_async_db_session = create_engine_and_session(TEST_SQLALCHEMY_DATABASE_URL) - - -async def override_get_db() -> AsyncSession: - """session 生成器""" - session = test_async_db_session() - try: - yield session - except Exception as se: - await session.rollback() - raise se - finally: - await session.close() diff --git a/backend.dockerfile b/backend/backend.dockerfile similarity index 73% rename from backend.dockerfile rename to backend/backend.dockerfile index c5777de..f68d3cc 100644 --- a/backend.dockerfile +++ b/backend/backend.dockerfile @@ -13,14 +13,14 @@ RUN apt-get update \ # 某些包可能存在同步不及时导致安装失败的情况,可更改为官方源:https://pypi.org/simple RUN pip install --upgrade pip -i https://mirrors.aliyun.com/pypi/simple \ - && pip install --no-cache-dir -r requirements.txt -i https://mirrors.aliyun.com/pypi/simple + && pip install -r backend/requirements.txt -i https://mirrors.aliyun.com/pypi/simple ENV TZ = Asia/Shanghai RUN mkdir -p /var/log/fastapi_server -COPY ./deploy/fastapi_server.conf /etc/supervisor/conf.d/ +COPY deploy/backend/fastapi_server.conf /etc/supervisor/conf.d/ EXPOSE 8001 -CMD ["uvicorn", "backend.app.main:app", "--host", "127.0.0.1", "--port", "8000"] +CMD ["uvicorn", "backend.main:app", "--host", "127.0.0.1", "--port", "8000"] diff --git a/backend/celery-start.sh b/backend/celery-start.sh new file mode 100644 index 0000000..e2a021a --- /dev/null +++ b/backend/celery-start.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash + +# work && beat +celery -A app.task.celery worker -l info -B + +# flower +celery -A app.task.celery flower --port=8555 --basic-auth=admin:123456 diff --git a/celery.dockerfile b/backend/celery.dockerfile similarity index 76% rename from celery.dockerfile rename to backend/celery.dockerfile index 33fc188..b8dff8f 100644 --- a/celery.dockerfile +++ b/backend/celery.dockerfile @@ -12,16 +12,18 @@ RUN apt-get update \ && rm -rf /var/lib/apt/lists/* RUN pip install --upgrade pip -i https://mirrors.aliyun.com/pypi/simple \ - && pip install --no-cache-dir -r requirements.txt -i https://mirrors.aliyun.com/pypi/simple + && pip install -r backend/requirements.txt -i https://mirrors.aliyun.com/pypi/simple ENV TZ = Asia/Shanghai RUN mkdir -p /var/log/celery -COPY ./deploy/celery.conf /etc/supervisor/conf.d/ +COPY deploy/backend/celery.conf /etc/supervisor/conf.d/ -WORKDIR /fba/backend/app +WORKDIR /fba/backend/ RUN chmod +x celery-start.sh +EXPOSE 8555 + CMD ["./celery-start.sh"] diff --git a/backend/common/__init__.py b/backend/common/__init__.py new file mode 100644 index 0000000..56fafa5 --- /dev/null +++ b/backend/common/__init__.py @@ -0,0 +1,2 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- diff --git a/backend/app/common/enums.py b/backend/common/enums.py similarity index 100% rename from backend/app/common/enums.py rename to backend/common/enums.py diff --git a/backend/common/exception/__init__.py b/backend/common/exception/__init__.py new file mode 100644 index 0000000..56fafa5 --- /dev/null +++ b/backend/common/exception/__init__.py @@ -0,0 +1,2 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- diff --git a/backend/app/common/exception/errors.py b/backend/common/exception/errors.py similarity index 97% rename from backend/app/common/exception/errors.py rename to backend/common/exception/errors.py index c13f731..c622d60 100644 --- a/backend/app/common/exception/errors.py +++ b/backend/common/exception/errors.py @@ -6,12 +6,13 @@ 业务代码执行异常时,可以使用 raise xxxError 触发内部错误,它尽可能实现带有后台任务的异常,但它不适用于**自定义响应状态码** 如果要求使用**自定义响应状态码**,则可以通过 return await response_base.fail(res=CustomResponseCode.xxx) 直接返回 """ # noqa: E501 + from typing import Any from fastapi import HTTPException from starlette.background import BackgroundTask -from backend.app.common.response.response_code import CustomErrorCode, StandardResponseCode +from backend.common.response.response_code import CustomErrorCode, StandardResponseCode class BaseExceptionMixin(Exception): diff --git a/backend/app/common/exception/exception_handler.py b/backend/common/exception/exception_handler.py similarity index 95% rename from backend/app/common/exception/exception_handler.py rename to backend/common/exception/exception_handler.py index bcc0ac2..75f869a 100644 --- a/backend/app/common/exception/exception_handler.py +++ b/backend/common/exception/exception_handler.py @@ -9,16 +9,16 @@ from starlette.exceptions import HTTPException from starlette.middleware.cors import CORSMiddleware from uvicorn.protocols.http.h11_impl import STATUS_PHRASES -from backend.app.common.exception.errors import BaseExceptionMixin -from backend.app.common.log import log -from backend.app.common.response.response_code import CustomResponseCode, StandardResponseCode -from backend.app.common.response.response_schema import response_base -from backend.app.core.conf import settings -from backend.app.schemas.base import ( +from backend.common.exception.errors import BaseExceptionMixin +from backend.common.log import log +from backend.common.msd.schema import ( CUSTOM_USAGE_ERROR_MESSAGES, CUSTOM_VALIDATION_ERROR_MESSAGES, ) -from backend.app.utils.serializers import MsgSpecJSONResponse +from backend.common.response.response_code import CustomResponseCode, StandardResponseCode +from backend.common.response.response_schema import response_base +from backend.core.conf import settings +from backend.utils.serializers import MsgSpecJSONResponse @sync_to_async @@ -36,7 +36,7 @@ def _get_exception_code(status_code: int): """ try: STATUS_PHRASES[status_code] - except Exception: # noqa: ignore + except Exception: code = StandardResponseCode.HTTP_400 else: code = status_code diff --git a/backend/app/common/log.py b/backend/common/log.py similarity index 91% rename from backend/app/common/log.py rename to backend/common/log.py index 10c3d6d..49a0813 100644 --- a/backend/app/common/log.py +++ b/backend/common/log.py @@ -8,8 +8,8 @@ from typing import TYPE_CHECKING from loguru import logger -from backend.app.core import path_conf -from backend.app.core.conf import settings +from backend.core import path_conf +from backend.core.conf import settings if TYPE_CHECKING: import loguru @@ -17,7 +17,7 @@ if TYPE_CHECKING: class Logger: def __init__(self): - self.log_path = path_conf.LogPath + self.log_path = path_conf.LOG_DIR def log(self) -> loguru.Logger: if not os.path.exists(self.log_path): diff --git a/backend/common/msd/__init__.py b/backend/common/msd/__init__.py new file mode 100644 index 0000000..56fafa5 --- /dev/null +++ b/backend/common/msd/__init__.py @@ -0,0 +1,2 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- diff --git a/backend/app/crud/base.py b/backend/common/msd/crud.py similarity index 98% rename from backend/app/crud/base.py rename to backend/common/msd/crud.py index 05f7288..8135080 100644 --- a/backend/app/crud/base.py +++ b/backend/common/msd/crud.py @@ -6,7 +6,7 @@ from pydantic import BaseModel from sqlalchemy import and_, delete, select, update from sqlalchemy.ext.asyncio import AsyncSession -from backend.app.models.base import MappedBase +from backend.common.msd.model import MappedBase ModelType = TypeVar('ModelType', bound=MappedBase) CreateSchemaType = TypeVar('CreateSchemaType', bound=BaseModel) diff --git a/backend/app/models/base.py b/backend/common/msd/model.py similarity index 98% rename from backend/app/models/base.py rename to backend/common/msd/model.py index 500b130..4822fa9 100644 --- a/backend/app/models/base.py +++ b/backend/common/msd/model.py @@ -5,7 +5,7 @@ from typing import Annotated from sqlalchemy.orm import DeclarativeBase, Mapped, MappedAsDataclass, declared_attr, mapped_column -from backend.app.utils.timezone import timezone +from backend.utils.timezone import timezone # 通用 Mapped 类型主键, 需手动添加,参考以下使用方式 # MappedBase -> id: Mapped[id_key] diff --git a/backend/app/schemas/base.py b/backend/common/msd/schema.py similarity index 99% rename from backend/app/schemas/base.py rename to backend/common/msd/schema.py index 00b5442..464f959 100644 --- a/backend/app/schemas/base.py +++ b/backend/common/msd/schema.py @@ -1,6 +1,5 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- - from pydantic import BaseModel, ConfigDict, EmailStr, validate_email from pydantic_extra_types.phone_numbers import PhoneNumber diff --git a/backend/app/common/pagination.py b/backend/common/pagination.py similarity index 82% rename from backend/app/common/pagination.py rename to backend/common/pagination.py index 11969b9..6e7f0a7 100644 --- a/backend/app/common/pagination.py +++ b/backend/common/pagination.py @@ -53,14 +53,12 @@ class _Page(AbstractPage[T], Generic[T]): page = params.page size = params.size total_pages = math.ceil(total / params.size) - links = create_links( - **{ - 'first': {'page': 1, 'size': f'{size}'}, - 'last': {'page': f'{math.ceil(total / params.size)}', 'size': f'{size}'} if total > 0 else None, - 'next': {'page': f'{page + 1}', 'size': f'{size}'} if (page + 1) <= total_pages else None, - 'prev': {'page': f'{page - 1}', 'size': f'{size}'} if (page - 1) >= 1 else None, - } - ).model_dump() + links = create_links(**{ + 'first': {'page': 1, 'size': f'{size}'}, + 'last': {'page': f'{math.ceil(total / params.size)}', 'size': f'{size}'} if total > 0 else None, + 'next': {'page': f'{page + 1}', 'size': f'{size}'} if (page + 1) <= total_pages else None, + 'prev': {'page': f'{page - 1}', 'size': f'{size}'} if (page - 1) >= 1 else None, + }).model_dump() return cls(items=items, total=total, page=params.page, size=params.size, total_pages=total_pages, links=links) diff --git a/backend/common/response/__init__.py b/backend/common/response/__init__.py new file mode 100644 index 0000000..56fafa5 --- /dev/null +++ b/backend/common/response/__init__.py @@ -0,0 +1,2 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- diff --git a/backend/app/common/response/response_code.py b/backend/common/response/response_code.py similarity index 100% rename from backend/app/common/response/response_code.py rename to backend/common/response/response_code.py diff --git a/backend/app/common/response/response_schema.py b/backend/common/response/response_schema.py similarity index 94% rename from backend/app/common/response/response_schema.py rename to backend/common/response/response_schema.py index 2fd6cdb..1cdf7b3 100644 --- a/backend/app/common/response/response_schema.py +++ b/backend/common/response/response_schema.py @@ -5,8 +5,8 @@ from typing import Any from pydantic import BaseModel, ConfigDict -from backend.app.common.response.response_code import CustomResponse, CustomResponseCode -from backend.app.core.conf import settings +from backend.common.response.response_code import CustomResponse, CustomResponseCode +from backend.core.conf import settings _ExcludeData = set[int | str] | dict[int | str, Any] @@ -23,10 +23,12 @@ class ResponseModel(BaseModel): def test(): return ResponseModel(data={'test': 'test'}) + @router.get('/test') def test() -> ResponseModel: return ResponseModel(data={'test': 'test'}) + @router.get('/test') def test() -> ResponseModel: res = CustomResponseCode.HTTP_200 diff --git a/backend/common/security/__init__.py b/backend/common/security/__init__.py new file mode 100644 index 0000000..56fafa5 --- /dev/null +++ b/backend/common/security/__init__.py @@ -0,0 +1,2 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- diff --git a/backend/app/common/jwt.py b/backend/common/security/jwt.py similarity index 95% rename from backend/app/common/jwt.py rename to backend/common/security/jwt.py index 77bad87..dfb224e 100644 --- a/backend/app/common/jwt.py +++ b/backend/common/security/jwt.py @@ -10,12 +10,11 @@ from jose import jwt from passlib.context import CryptContext from sqlalchemy.ext.asyncio import AsyncSession -from backend.app.common.exception.errors import AuthorizationError, TokenError -from backend.app.common.redis import redis_client -from backend.app.core.conf import settings -from backend.app.crud.crud_user import user_dao -from backend.app.models import User -from backend.app.utils.timezone import timezone +from backend.app.admin.model import User +from backend.common.exception.errors import AuthorizationError, TokenError +from backend.core.conf import settings +from backend.database.db_redis import redis_client +from backend.utils.timezone import timezone pwd_context = CryptContext(schemes=['bcrypt'], deprecated='auto') @@ -180,6 +179,8 @@ async def get_current_user(db: AsyncSession, data: dict) -> User: :return: """ user_id = data.get('sub') + from backend.app.admin.crud.crud_user import user_dao + user = await user_dao.get_with_relation(db, user_id=user_id) if not user: raise TokenError(msg='Token 无效') diff --git a/backend/app/common/permission.py b/backend/common/security/permission.py similarity index 87% rename from backend/app/common/permission.py rename to backend/common/security/permission.py index a6e1d81..3db8b55 100644 --- a/backend/app/common/permission.py +++ b/backend/common/security/permission.py @@ -2,8 +2,8 @@ # -*- coding: utf-8 -*- from fastapi import Request -from backend.app.common.exception.errors import ServerError -from backend.app.core.conf import settings +from backend.common.exception.errors import ServerError +from backend.core.conf import settings class RequestPermission: diff --git a/backend/app/common/rbac.py b/backend/common/security/rbac.py similarity index 78% rename from backend/app/common/rbac.py rename to backend/common/security/rbac.py index 7373fae..fd41044 100644 --- a/backend/app/common/rbac.py +++ b/backend/common/security/rbac.py @@ -5,13 +5,13 @@ import casbin_async_sqlalchemy_adapter from fastapi import Depends, Request -from backend.app.common.enums import MethodType, StatusType -from backend.app.common.exception.errors import AuthorizationError, TokenError -from backend.app.common.jwt import DependsJwtAuth -from backend.app.common.redis import redis_client -from backend.app.core.conf import settings -from backend.app.database.db_mysql import async_engine -from backend.app.models import CasbinRule +from backend.app.admin.model import CasbinRule +from backend.common.enums import MethodType, StatusType +from backend.common.exception.errors import AuthorizationError, TokenError +from backend.common.security.jwt import DependsJwtAuth +from backend.core.conf import settings +from backend.database.db_mysql import async_engine +from backend.database.db_redis import redis_client class RBAC: @@ -89,15 +89,11 @@ class RBAC: user_menu_perms = [] user_forbid_menu_perms = [] for role in user_roles: - user_menus = role.menus - if user_menus: - for menu in user_menus: - perms = menu.perms - if perms: - if menu.status == StatusType.enable: - user_menu_perms.extend(perms.split(',')) - else: - user_forbid_menu_perms.extend(perms.split(',')) + for menu in role.menus: + if menu.status == StatusType.enable: + user_menu_perms.extend(menu.perms.split(',')) + else: + user_forbid_menu_perms.extend(menu.perms.split(',')) await redis_client.set( f'{settings.PERMISSION_REDIS_PREFIX}:{user_uuid}:enable', ','.join(user_menu_perms) ) @@ -116,13 +112,9 @@ class RBAC: if not user_forbid_menu_perms: user_forbid_menu_perms = [] for role in user_roles: - user_menus = role.menus - if user_menus: - for menu in user_menus: - perms = menu.perms - if perms: - if menu.status == StatusType.disable: - user_forbid_menu_perms.extend(perms.split(',')) + for menu in role.menus: + if menu.status == StatusType.disable: + user_forbid_menu_perms.extend(menu.perms.split(',')) await redis_client.set( f'{settings.PERMISSION_REDIS_PREFIX}:{user_uuid}:disable', ','.join(user_forbid_menu_perms) ) diff --git a/backend/core/__init__.py b/backend/core/__init__.py new file mode 100644 index 0000000..56fafa5 --- /dev/null +++ b/backend/core/__init__.py @@ -0,0 +1,2 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- diff --git a/backend/app/core/conf.py b/backend/core/conf.py similarity index 63% rename from backend/app/core/conf.py rename to backend/core/conf.py index a3a66f3..354a42a 100644 --- a/backend/app/core/conf.py +++ b/backend/core/conf.py @@ -6,18 +6,22 @@ from typing import Literal from pydantic import model_validator from pydantic_settings import BaseSettings, SettingsConfigDict +from backend.core.path_conf import BasePath + class Settings(BaseSettings): - model_config = SettingsConfigDict(env_file='.env', env_file_encoding='utf-8') + """Global Settings""" + + model_config = SettingsConfigDict(env_file=f'{BasePath}/.env', env_file_encoding='utf-8', extra='ignore') # Env Config ENVIRONMENT: Literal['dev', 'pro'] # Env MySQL - DB_HOST: str - DB_PORT: int - DB_USER: str - DB_PASSWORD: str + MYSQL_HOST: str + MYSQL_PORT: int + MYSQL_USER: str + MYSQL_PASSWORD: str # Env Redis REDIS_HOST: str @@ -25,30 +29,12 @@ class Settings(BaseSettings): REDIS_PASSWORD: str REDIS_DATABASE: int - # Env Celery - CELERY_REDIS_HOST: str - CELERY_REDIS_PORT: int - CELERY_REDIS_PASSWORD: str - CELERY_BROKER_REDIS_DATABASE: int # 仅当使用 redis 作为 broker 时生效, 更适用于测试环境 - CELERY_BACKEND_REDIS_DATABASE: int - - # 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 - # Env Token TOKEN_SECRET_KEY: str # 密钥 secrets.token_urlsafe(32) # Env Opera Log OPERA_LOG_ENCRYPT_SECRET_KEY: str # 密钥 os.urandom(32), 需使用 bytes.hex() 方法转换为 str - # OAuth2:https://github.com/fastapi-practices/fastapi_oauth20 - OAUTH2_GITHUB_CLIENT_ID: str - OAUTH2_GITHUB_CLIENT_SECRET: str - # FastAPI API_V1_STR: str = '/api/v1' TITLE: str = 'FastAPI' @@ -74,9 +60,6 @@ class Settings(BaseSettings): ('GET', f'{API_V1_STR}/auth/captcha'), } - # OAuth2 - OAUTH2_GITHUB_REDIRECT_URI: str = 'http://127.0.0.1:8000/api/v1/auth/github/callback' - # Uvicorn UVICORN_HOST: str = '127.0.0.1' UVICORN_PORT: int = 8000 @@ -96,9 +79,9 @@ class Settings(BaseSettings): DATETIME_FORMAT: str = '%Y-%m-%d %H:%M:%S' # MySQL - DB_ECHO: bool = False - DB_DATABASE: str = 'fba' - DB_CHARSET: str = 'utf8mb4' + MYSQL_ECHO: bool = False + MYSQL_DATABASE: str = 'fba' + MYSQL_CHARSET: str = 'utf8mb4' # Redis REDIS_TIMEOUT: int = 5 @@ -107,17 +90,12 @@ class Settings(BaseSettings): TOKEN_ALGORITHM: str = 'HS256' # 算法 TOKEN_EXPIRE_SECONDS: int = 60 * 60 * 24 * 1 # 过期时间,单位:秒 TOKEN_REFRESH_EXPIRE_SECONDS: int = 60 * 60 * 24 * 7 # 刷新过期时间,单位:秒 - TOKEN_URL_SWAGGER: str = f'{API_V1_STR}/auth/swagger_login' TOKEN_REDIS_PREFIX: str = 'fba_token' TOKEN_REFRESH_REDIS_PREFIX: str = 'fba_refresh_token' TOKEN_EXCLUDE: list[str] = [ # JWT / RBAC 白名单 f'{API_V1_STR}/auth/login', ] - # Captcha - CAPTCHA_LOGIN_REDIS_PREFIX: str = 'fba_login_captcha' - CAPTCHA_LOGIN_EXPIRE_SECONDS: int = 60 * 5 # 过期时间,单位:秒 - # Log LOG_STDOUT_FILENAME: str = 'fba_access.log' LOG_STDERR_FILENAME: str = 'fba_error.log' @@ -133,11 +111,8 @@ class Settings(BaseSettings): # Casbin Auth CASBIN_EXCLUDE: set[tuple[str, str]] = { - ('POST', f'{API_V1_STR}/auth/swagger_login'), - ('POST', f'{API_V1_STR}/auth/login'), ('POST', f'{API_V1_STR}/auth/logout'), - ('POST', f'{API_V1_STR}/auth/register'), - ('GET', f'{API_V1_STR}/auth/captcha'), + ('POST', f'{API_V1_STR}/auth/token/new'), } # Role Menu Auth @@ -152,7 +127,8 @@ class Settings(BaseSettings): DOCS_URL, REDOCS_URL, OPENAPI_URL, - f'{API_V1_STR}/auth/swagger_login', + f'{API_V1_STR}/auth/login/swagger', + f'{API_V1_STR}/auth/github/callback', ] OPERA_LOG_ENCRYPT: int = 1 # 0: AES (性能损耗); 1: md5; 2: ItsDangerous; 3: 不加密, others: 替换为 ****** OPERA_LOG_ENCRYPT_INCLUDE: list[str] = [ @@ -166,30 +142,12 @@ class Settings(BaseSettings): IP_LOCATION_REDIS_PREFIX: str = 'fba_ip_location' IP_LOCATION_EXPIRE_SECONDS: int = 60 * 60 * 24 * 1 # 过期时间,单位:秒 - # Celery - CELERY_BROKER: Literal['rabbitmq', 'redis'] = 'redis' - CELERY_BACKEND_REDIS_PREFIX: str = 'fba_celery' - CELERY_BACKEND_REDIS_TIMEOUT: float = 5.0 - CELERY_BACKEND_REDIS_ORDERED: bool = True - CELERY_BEAT_SCHEDULE_FILENAME: str = './log/celery_beat-schedule' - CELERY_BEAT_SCHEDULE: dict = { - 'task_demo_async': { - 'task': 'tasks.task_demo_async', - 'schedule': 5.0, - }, - } - - @model_validator(mode='before') - def validate_celery_broker(cls, values): - if values['ENVIRONMENT'] == 'pro': - values['CELERY_BROKER'] = 'rabbitmq' - return values - @lru_cache -def get_settings(): - """读取配置优化""" +def get_settings() -> Settings: + """获取全局配置""" return Settings() +# 创建配置实例 settings = get_settings() diff --git a/backend/core/path_conf.py b/backend/core/path_conf.py new file mode 100644 index 0000000..0514dbb --- /dev/null +++ b/backend/core/path_conf.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +import os + +from pathlib import Path + +# 获取项目根目录 +# 或使用绝对路径,指到backend目录为止,例如windows:BasePath = D:\git_project\fastapi_mysql\backend +BasePath = Path(__file__).resolve().parent.parent + +# alembic 迁移文件存放路径 +ALEMBIC_Versions_DIR = os.path.join(BasePath, 'alembic', 'versions') + +# 日志文件路径 +LOG_DIR = os.path.join(BasePath, 'log') + +# 离线 IP 数据库路径 +IP2REGION_XDB = os.path.join(BasePath, 'static', 'ip2region.xdb') + +# 挂载静态目录 +STATIC_DIR = os.path.join(BasePath, 'static') diff --git a/backend/app/core/registrar.py b/backend/core/registrar.py similarity index 75% rename from backend/app/core/registrar.py rename to backend/core/registrar.py index 39365ef..34f8dcd 100644 --- a/backend/app/core/registrar.py +++ b/backend/core/registrar.py @@ -7,17 +7,18 @@ from fastapi_limiter import FastAPILimiter from fastapi_pagination import add_pagination from starlette.middleware.authentication import AuthenticationMiddleware -from backend.app.api.routers import v1 -from backend.app.common.exception.exception_handler import register_exception -from backend.app.common.redis import redis_client -from backend.app.core.conf import settings -from backend.app.database.db_mysql import create_table -from backend.app.middleware.jwt_auth_middleware import JwtAuthMiddleware -from backend.app.middleware.opera_log_middleware import OperaLogMiddleware -from backend.app.utils.demo_site import demo_site -from backend.app.utils.health_check import ensure_unique_route_names, http_limit_callback -from backend.app.utils.openapi import simplify_operation_ids -from backend.app.utils.serializers import MsgSpecJSONResponse +from backend.app.router import route +from backend.common.exception.exception_handler import register_exception +from backend.core.conf import settings +from backend.core.path_conf import STATIC_DIR +from backend.database.db_mysql import create_table +from backend.database.db_redis import redis_client +from backend.middleware.jwt_auth_middleware import JwtAuthMiddleware +from backend.middleware.opera_log_middleware import OperaLogMiddleware +from backend.utils.demo_site import demo_site +from backend.utils.health_check import ensure_unique_route_names, http_limit_callback +from backend.utils.openapi import simplify_operation_ids +from backend.utils.serializers import MsgSpecJSONResponse @asynccontextmanager @@ -85,9 +86,9 @@ def register_static_file(app: FastAPI): from fastapi.staticfiles import StaticFiles - if not os.path.exists('./static'): - os.mkdir('./static') - app.mount('/static', StaticFiles(directory='static'), name='static') + if not os.path.exists(STATIC_DIR): + os.mkdir(STATIC_DIR) + app.mount('/static', StaticFiles(directory=STATIC_DIR), name='static') def register_middleware(app: FastAPI): @@ -110,7 +111,7 @@ def register_middleware(app: FastAPI): ) # Access log if settings.MIDDLEWARE_ACCESS: - from backend.app.middleware.access_middleware import AccessMiddleware + from backend.middleware.access_middleware import AccessMiddleware app.add_middleware(AccessMiddleware) # CORS: Always at the end @@ -136,7 +137,7 @@ def register_router(app: FastAPI): dependencies = [Depends(demo_site)] if settings.DEMO_MODE else None # API - app.include_router(v1, dependencies=dependencies) + app.include_router(route, dependencies=dependencies) # Extra ensure_unique_route_names(app) diff --git a/backend/database/__init__.py b/backend/database/__init__.py new file mode 100644 index 0000000..56fafa5 --- /dev/null +++ b/backend/database/__init__.py @@ -0,0 +1,2 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- diff --git a/backend/app/database/db_mysql.py b/backend/database/db_mysql.py similarity index 76% rename from backend/app/database/db_mysql.py rename to backend/database/db_mysql.py index f895234..9a7a9cb 100644 --- a/backend/app/database/db_mysql.py +++ b/backend/database/db_mysql.py @@ -9,15 +9,15 @@ from fastapi import Depends from sqlalchemy import URL from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine -from backend.app.common.log import log -from backend.app.core.conf import settings -from backend.app.models.base import MappedBase +from backend.common.log import log +from backend.common.msd.model import MappedBase +from backend.core.conf import settings def create_engine_and_session(url: str | URL): try: # 数据库引擎 - engine = create_async_engine(url, echo=settings.DB_ECHO, future=True, pool_pre_ping=True) + engine = create_async_engine(url, echo=settings.MYSQL_ECHO, future=True, pool_pre_ping=True) # log.success('数据库连接成功') except Exception as e: log.error('❌ 数据库链接失败 {}', e) @@ -28,8 +28,8 @@ def create_engine_and_session(url: str | URL): SQLALCHEMY_DATABASE_URL = ( - f'mysql+asyncmy://{settings.DB_USER}:{settings.DB_PASSWORD}@{settings.DB_HOST}:' - f'{settings.DB_PORT}/{settings.DB_DATABASE}?charset={settings.DB_CHARSET}' + f'mysql+asyncmy://{settings.MYSQL_USER}:{settings.MYSQL_PASSWORD}@{settings.MYSQL_HOST}:' + f'{settings.MYSQL_PORT}/{settings.MYSQL_DATABASE}?charset={settings.MYSQL_CHARSET}' ) async_engine, async_db_session = create_engine_and_session(SQLALCHEMY_DATABASE_URL) diff --git a/backend/app/common/redis.py b/backend/database/db_redis.py similarity index 93% rename from backend/app/common/redis.py rename to backend/database/db_redis.py index 8d3d8ea..ac134ae 100644 --- a/backend/app/common/redis.py +++ b/backend/database/db_redis.py @@ -5,8 +5,8 @@ import sys from redis.asyncio.client import Redis from redis.exceptions import AuthenticationError, TimeoutError -from backend.app.common.log import log -from backend.app.core.conf import settings +from backend.common.log import log +from backend.core.conf import settings class RedisCli(Redis): @@ -60,5 +60,5 @@ class RedisCli(Redis): await self.delete(key) -# 创建redis连接对象 +# 创建 redis 客户端实例 redis_client = RedisCli() diff --git a/backend/app/main.py b/backend/main.py similarity index 88% rename from backend/app/main.py rename to backend/main.py index 2b6a5db..40162e2 100644 --- a/backend/app/main.py +++ b/backend/main.py @@ -4,9 +4,9 @@ import uvicorn from path import Path -from backend.app.common.log import log -from backend.app.core.conf import settings -from backend.app.core.registrar import register_app +from backend.common.log import log +from backend.core.conf import settings +from backend.core.registrar import register_app app = register_app() diff --git a/backend/middleware/__init__.py b/backend/middleware/__init__.py new file mode 100644 index 0000000..56fafa5 --- /dev/null +++ b/backend/middleware/__init__.py @@ -0,0 +1,2 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- diff --git a/backend/app/middleware/access_middleware.py b/backend/middleware/access_middleware.py similarity index 87% rename from backend/app/middleware/access_middleware.py rename to backend/middleware/access_middleware.py index f671a01..eb18fc4 100644 --- a/backend/app/middleware/access_middleware.py +++ b/backend/middleware/access_middleware.py @@ -3,8 +3,8 @@ from fastapi import Request, Response from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint -from backend.app.common.log import log -from backend.app.utils.timezone import timezone +from backend.common.log import log +from backend.utils.timezone import timezone class AccessMiddleware(BaseHTTPMiddleware): diff --git a/backend/app/middleware/jwt_auth_middleware.py b/backend/middleware/jwt_auth_middleware.py similarity index 86% rename from backend/app/middleware/jwt_auth_middleware.py rename to backend/middleware/jwt_auth_middleware.py index 84c26d8..849452f 100644 --- a/backend/app/middleware/jwt_auth_middleware.py +++ b/backend/middleware/jwt_auth_middleware.py @@ -6,12 +6,12 @@ from fastapi import Request, Response from starlette.authentication import AuthCredentials, AuthenticationBackend, AuthenticationError from starlette.requests import HTTPConnection -from backend.app.common import jwt -from backend.app.common.exception.errors import TokenError -from backend.app.common.log import log -from backend.app.core.conf import settings -from backend.app.database.db_mysql import async_db_session -from backend.app.utils.serializers import MsgSpecJSONResponse +from backend.common.exception.errors import TokenError +from backend.common.log import log +from backend.common.security import jwt +from backend.core.conf import settings +from backend.database.db_mysql import async_db_session +from backend.utils.serializers import MsgSpecJSONResponse class _AuthenticationError(AuthenticationError): diff --git a/backend/app/middleware/opera_log_middleware.py b/backend/middleware/opera_log_middleware.py similarity index 93% rename from backend/app/middleware/opera_log_middleware.py rename to backend/middleware/opera_log_middleware.py index 8635ea6..dee4698 100644 --- a/backend/app/middleware/opera_log_middleware.py +++ b/backend/middleware/opera_log_middleware.py @@ -7,14 +7,14 @@ from starlette.datastructures import UploadFile from starlette.middleware.base import BaseHTTPMiddleware from starlette.requests import Request -from backend.app.common.enums import OperaLogCipherType -from backend.app.common.log import log -from backend.app.core.conf import settings -from backend.app.schemas.opera_log import CreateOperaLogParam -from backend.app.services.opera_log_service import OperaLogService -from backend.app.utils.encrypt import AESCipher, ItsDCipher, Md5Cipher -from backend.app.utils.request_parse import parse_ip_info, parse_user_agent_info -from backend.app.utils.timezone import timezone +from backend.app.admin.schema.opera_log import CreateOperaLogParam +from backend.app.admin.service.opera_log_service import OperaLogService +from backend.common.enums import OperaLogCipherType +from backend.common.log import log +from backend.core.conf import settings +from backend.utils.encrypt import AESCipher, ItsDCipher, Md5Cipher +from backend.utils.request_parse import parse_ip_info, parse_user_agent_info +from backend.utils.timezone import timezone class OperaLogMiddleware(BaseHTTPMiddleware): diff --git a/pdm.lock b/backend/pdm.lock similarity index 89% rename from pdm.lock rename to backend/pdm.lock index 01104fa..59e1722 100644 --- a/pdm.lock +++ b/backend/pdm.lock @@ -2,10 +2,10 @@ # It is not intended for manual editing. [metadata] -groups = ["default", "deploy"] +groups = ["default", "lint", "deploy"] strategy = ["cross_platform", "inherit_metadata"] lock_version = "4.4.1" -content_hash = "sha256:06e039c331218934838e6c6865b0c78b8cb14142c3f90f23f7d36844f9a20a09" +content_hash = "sha256:1e8b094727ea68450c56c0182cfb4ca42ca2347cea93fffd883feac54a5fe44f" [[package]] name = "aiofiles" @@ -72,7 +72,7 @@ files = [ [[package]] name = "anyio" -version = "4.2.0" +version = "4.3.0" requires_python = ">=3.8" summary = "High level compatibility layer for multiple asynchronous event loop implementations" groups = ["default"] @@ -83,8 +83,8 @@ dependencies = [ "typing-extensions>=4.1; python_version < \"3.11\"", ] files = [ - {file = "anyio-4.2.0-py3-none-any.whl", hash = "sha256:745843b39e829e108e518c489b31dc757de7d2131d53fac32bd8df268227bfee"}, - {file = "anyio-4.2.0.tar.gz", hash = "sha256:e1875bb4b4e2de1669f4bc7869b6d3f54231cdced71605e6e64c9be77e3be50f"}, + {file = "anyio-4.3.0-py3-none-any.whl", hash = "sha256:048e05d0f6caeed70d731f3db756d35dcc1f35747c8c403364a8332c630441b8"}, + {file = "anyio-4.3.0.tar.gz", hash = "sha256:f75253795a87df48568485fd18cdd2a3fa5c4f7c5be8e5e36637733fce06fed6"}, ] [[package]] @@ -258,13 +258,13 @@ files = [ [[package]] name = "certifi" -version = "2023.11.17" +version = "2024.2.2" requires_python = ">=3.6" summary = "Python package for providing Mozilla's CA Bundle." groups = ["default"] files = [ - {file = "certifi-2023.11.17-py3-none-any.whl", hash = "sha256:e036ab49d5b79556f99cfc2d9320b34cfbe5be05c5871b51de9329f0603b0474"}, - {file = "certifi-2023.11.17.tar.gz", hash = "sha256:9b469f3a900bf28dc19b8cfbf8019bf47f7fdd1a65a1d4ffb98fc14166beb4d1"}, + {file = "certifi-2024.2.2-py3-none-any.whl", hash = "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1"}, + {file = "certifi-2024.2.2.tar.gz", hash = "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f"}, ] [[package]] @@ -438,13 +438,13 @@ files = [ [[package]] name = "dnspython" -version = "2.5.0" +version = "2.6.1" requires_python = ">=3.8" summary = "DNS toolkit" groups = ["default"] files = [ - {file = "dnspython-2.5.0-py3-none-any.whl", hash = "sha256:6facdf76b73c742ccf2d07add296f178e629da60be23ce4b0a9c927b1e02c3a6"}, - {file = "dnspython-2.5.0.tar.gz", hash = "sha256:a0034815a59ba9ae888946be7ccca8f7c157b286f8455b379c692efb51022a15"}, + {file = "dnspython-2.6.1-py3-none-any.whl", hash = "sha256:5ef3b9680161f6fa89daf8ad451b5f1a33b18ae8a1c6778cdf4b43f08c0a6e50"}, + {file = "dnspython-2.6.1.tar.gz", hash = "sha256:e8f0f9c23a7b7cb99ded64e6c3a6f3e701d78f50c55e002b839dea7225cff7cc"}, ] [[package]] @@ -575,6 +575,24 @@ files = [ {file = "filelock-3.13.1.tar.gz", hash = "sha256:521f5f56c50f8426f5e03ad3b281b490a87ef15bc6c526f168290f0c7148d44e"}, ] +[[package]] +name = "flower" +version = "2.0.1" +requires_python = ">=3.7" +summary = "Celery Flower" +groups = ["default"] +dependencies = [ + "celery>=5.0.5", + "humanize", + "prometheus-client>=0.8.0", + "pytz", + "tornado<7.0.0,>=5.0.0", +] +files = [ + {file = "flower-2.0.1-py2.py3-none-any.whl", hash = "sha256:9db2c621eeefbc844c8dd88be64aef61e84e2deb29b271e02ab2b5b9f01068e2"}, + {file = "flower-2.0.1.tar.gz", hash = "sha256:5ab717b979530770c16afb48b50d2a98d23c3e9fe39851dcf6bc4d01845a02a0"}, +] + [[package]] name = "greenlet" version = "3.0.3" @@ -715,7 +733,7 @@ files = [ [[package]] name = "httpcore" -version = "1.0.2" +version = "1.0.4" requires_python = ">=3.8" summary = "A minimal low-level HTTP client." groups = ["default"] @@ -724,8 +742,8 @@ dependencies = [ "h11<0.15,>=0.13", ] files = [ - {file = "httpcore-1.0.2-py3-none-any.whl", hash = "sha256:096cc05bca73b8e459a1fc3dcf585148f63e534eae4339559c9b8a8d6399acc7"}, - {file = "httpcore-1.0.2.tar.gz", hash = "sha256:9fc092e4799b26174648e54b74ed5f683132a464e95643b226e00c2ed2fa6535"}, + {file = "httpcore-1.0.4-py3-none-any.whl", hash = "sha256:ac418c1db41bade2ad53ae2f3834a3a0f5ae76b56cf5aa497d2d033384fc7d73"}, + {file = "httpcore-1.0.4.tar.gz", hash = "sha256:cb2839ccfcba0d2d3c1131d3c3e26dfc327326fbe7a5dc0dbfe9f6c9151bb022"}, ] [[package]] @@ -777,15 +795,26 @@ files = [ {file = "httpx-0.25.2.tar.gz", hash = "sha256:8b8fcaa0c8ea7b05edd69a094e63a2094c4efcb48129fb757361bc423c0ad9e8"}, ] +[[package]] +name = "humanize" +version = "4.9.0" +requires_python = ">=3.8" +summary = "Python humanize utilities" +groups = ["default"] +files = [ + {file = "humanize-4.9.0-py3-none-any.whl", hash = "sha256:ce284a76d5b1377fd8836733b983bfb0b76f1aa1c090de2566fcf008d7f6ab16"}, + {file = "humanize-4.9.0.tar.gz", hash = "sha256:582a265c931c683a7e9b8ed9559089dea7edcf6cc95be39a3cbc2c5d5ac2bcfa"}, +] + [[package]] name = "identify" -version = "2.5.33" +version = "2.5.35" requires_python = ">=3.8" summary = "File identification library for Python" groups = ["default"] files = [ - {file = "identify-2.5.33-py2.py3-none-any.whl", hash = "sha256:d40ce5fcd762817627670da8a7d8d8e65f24342d14539c59488dc603bf662e34"}, - {file = "identify-2.5.33.tar.gz", hash = "sha256:161558f9fe4559e1557e1bff323e8631f6a0e4837f7497767c1782832f16b62d"}, + {file = "identify-2.5.35-py2.py3-none-any.whl", hash = "sha256:c4de0081837b211594f8e877a6b4fad7ca32bbfc1a9307fdd61c28bfe923f13e"}, + {file = "identify-2.5.35.tar.gz", hash = "sha256:10a7ca245cfcd756a554a7288159f72ff105ad233c7c4b9c6f0f4d108f5f6791"}, ] [[package]] @@ -881,42 +910,42 @@ files = [ [[package]] name = "markupsafe" -version = "2.1.4" +version = "2.1.5" requires_python = ">=3.7" summary = "Safely add untrusted strings to HTML/XML markup." groups = ["default"] files = [ - {file = "MarkupSafe-2.1.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:de8153a7aae3835484ac168a9a9bdaa0c5eee4e0bc595503c95d53b942879c84"}, - {file = "MarkupSafe-2.1.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e888ff76ceb39601c59e219f281466c6d7e66bd375b4ec1ce83bcdc68306796b"}, - {file = "MarkupSafe-2.1.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0b838c37ba596fcbfca71651a104a611543077156cb0a26fe0c475e1f152ee8"}, - {file = "MarkupSafe-2.1.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dac1ebf6983148b45b5fa48593950f90ed6d1d26300604f321c74a9ca1609f8e"}, - {file = "MarkupSafe-2.1.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0fbad3d346df8f9d72622ac71b69565e621ada2ce6572f37c2eae8dacd60385d"}, - {file = "MarkupSafe-2.1.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d5291d98cd3ad9a562883468c690a2a238c4a6388ab3bd155b0c75dd55ece858"}, - {file = "MarkupSafe-2.1.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:a7cc49ef48a3c7a0005a949f3c04f8baa5409d3f663a1b36f0eba9bfe2a0396e"}, - {file = "MarkupSafe-2.1.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b83041cda633871572f0d3c41dddd5582ad7d22f65a72eacd8d3d6d00291df26"}, - {file = "MarkupSafe-2.1.4-cp310-cp310-win32.whl", hash = "sha256:0c26f67b3fe27302d3a412b85ef696792c4a2386293c53ba683a89562f9399b0"}, - {file = "MarkupSafe-2.1.4-cp310-cp310-win_amd64.whl", hash = "sha256:a76055d5cb1c23485d7ddae533229039b850db711c554a12ea64a0fd8a0129e2"}, - {file = "MarkupSafe-2.1.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9e9e3c4020aa2dc62d5dd6743a69e399ce3de58320522948af6140ac959ab863"}, - {file = "MarkupSafe-2.1.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0042d6a9880b38e1dd9ff83146cc3c9c18a059b9360ceae207805567aacccc69"}, - {file = "MarkupSafe-2.1.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55d03fea4c4e9fd0ad75dc2e7e2b6757b80c152c032ea1d1de487461d8140efc"}, - {file = "MarkupSafe-2.1.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ab3a886a237f6e9c9f4f7d272067e712cdb4efa774bef494dccad08f39d8ae6"}, - {file = "MarkupSafe-2.1.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:abf5ebbec056817057bfafc0445916bb688a255a5146f900445d081db08cbabb"}, - {file = "MarkupSafe-2.1.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e1a0d1924a5013d4f294087e00024ad25668234569289650929ab871231668e7"}, - {file = "MarkupSafe-2.1.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:e7902211afd0af05fbadcc9a312e4cf10f27b779cf1323e78d52377ae4b72bea"}, - {file = "MarkupSafe-2.1.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c669391319973e49a7c6230c218a1e3044710bc1ce4c8e6eb71f7e6d43a2c131"}, - {file = "MarkupSafe-2.1.4-cp311-cp311-win32.whl", hash = "sha256:31f57d64c336b8ccb1966d156932f3daa4fee74176b0fdc48ef580be774aae74"}, - {file = "MarkupSafe-2.1.4-cp311-cp311-win_amd64.whl", hash = "sha256:54a7e1380dfece8847c71bf7e33da5d084e9b889c75eca19100ef98027bd9f56"}, - {file = "MarkupSafe-2.1.4-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:a76cd37d229fc385738bd1ce4cba2a121cf26b53864c1772694ad0ad348e509e"}, - {file = "MarkupSafe-2.1.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:987d13fe1d23e12a66ca2073b8d2e2a75cec2ecb8eab43ff5624ba0ad42764bc"}, - {file = "MarkupSafe-2.1.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5244324676254697fe5c181fc762284e2c5fceeb1c4e3e7f6aca2b6f107e60dc"}, - {file = "MarkupSafe-2.1.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78bc995e004681246e85e28e068111a4c3f35f34e6c62da1471e844ee1446250"}, - {file = "MarkupSafe-2.1.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a4d176cfdfde84f732c4a53109b293d05883e952bbba68b857ae446fa3119b4f"}, - {file = "MarkupSafe-2.1.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:f9917691f410a2e0897d1ef99619fd3f7dd503647c8ff2475bf90c3cf222ad74"}, - {file = "MarkupSafe-2.1.4-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:f06e5a9e99b7df44640767842f414ed5d7bedaaa78cd817ce04bbd6fd86e2dd6"}, - {file = "MarkupSafe-2.1.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:396549cea79e8ca4ba65525470d534e8a41070e6b3500ce2414921099cb73e8d"}, - {file = "MarkupSafe-2.1.4-cp312-cp312-win32.whl", hash = "sha256:f6be2d708a9d0e9b0054856f07ac7070fbe1754be40ca8525d5adccdbda8f475"}, - {file = "MarkupSafe-2.1.4-cp312-cp312-win_amd64.whl", hash = "sha256:5045e892cfdaecc5b4c01822f353cf2c8feb88a6ec1c0adef2a2e705eef0f656"}, - {file = "MarkupSafe-2.1.4.tar.gz", hash = "sha256:3aae9af4cac263007fd6309c64c6ab4506dd2b79382d9d19a1994f9240b8db4f"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-win32.whl", hash = "sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-win_amd64.whl", hash = "sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-win32.whl", hash = "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-win_amd64.whl", hash = "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-win32.whl", hash = "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb"}, + {file = "MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b"}, ] [[package]] @@ -977,13 +1006,13 @@ files = [ [[package]] name = "packaging" -version = "23.2" +version = "24.0" requires_python = ">=3.7" summary = "Core utilities for Python packages" groups = ["default"] files = [ - {file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"}, - {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"}, + {file = "packaging-24.0-py3-none-any.whl", hash = "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5"}, + {file = "packaging-24.0.tar.gz", hash = "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9"}, ] [[package]] @@ -1102,6 +1131,17 @@ files = [ {file = "pre_commit-3.2.2.tar.gz", hash = "sha256:5b808fcbda4afbccf6d6633a56663fed35b6c2bc08096fd3d47ce197ac351d9d"}, ] +[[package]] +name = "prometheus-client" +version = "0.20.0" +requires_python = ">=3.8" +summary = "Python client for the Prometheus monitoring system." +groups = ["default"] +files = [ + {file = "prometheus_client-0.20.0-py3-none-any.whl", hash = "sha256:cde524a85bce83ca359cc837f28b8c0db5cac7aa653a588fd7e84ba061c329e7"}, + {file = "prometheus_client-0.20.0.tar.gz", hash = "sha256:287629d00b147a32dcb2be0b9df905da599b2d82f80377083ec8463309a4bb89"}, +] + [[package]] name = "prompt-toolkit" version = "3.0.43" @@ -1328,7 +1368,7 @@ files = [ [[package]] name = "python-dateutil" -version = "2.8.2" +version = "2.9.0.post0" requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" summary = "Extensions to the standard Python datetime module" groups = ["default"] @@ -1336,8 +1376,8 @@ dependencies = [ "six>=1.5", ] files = [ - {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, - {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, + {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, + {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, ] [[package]] @@ -1452,7 +1492,7 @@ files = [ [[package]] name = "rich" -version = "13.7.0" +version = "13.7.1" requires_python = ">=3.7.0" summary = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" groups = ["default"] @@ -1461,8 +1501,8 @@ dependencies = [ "pygments<3.0.0,>=2.13.0", ] files = [ - {file = "rich-13.7.0-py3-none-any.whl", hash = "sha256:6da14c108c4866ee9520bbffa71f6fe3962e193b7da68720583850cd4548e235"}, - {file = "rich-13.7.0.tar.gz", hash = "sha256:5cb5123b5cf9ee70584244246816e9114227e0b98ad9176eede6ad54bf5403fa"}, + {file = "rich-13.7.1-py3-none-any.whl", hash = "sha256:4edbae314f59eb482f54e9e30bf00d33350aaa94f4bfcd4e9e3110e64d0d7222"}, + {file = "rich-13.7.1.tar.gz", hash = "sha256:9be308cb1fe2f1f57d67ce99e95af38a1e2bc71ad9813b0e247cf7ffbcc3a432"}, ] [[package]] @@ -1481,39 +1521,39 @@ files = [ [[package]] name = "ruff" -version = "0.1.8" +version = "0.3.3" requires_python = ">=3.7" summary = "An extremely fast Python linter and code formatter, written in Rust." -groups = ["default"] +groups = ["lint"] files = [ - {file = "ruff-0.1.8-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:7de792582f6e490ae6aef36a58d85df9f7a0cfd1b0d4fe6b4fb51803a3ac96fa"}, - {file = "ruff-0.1.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:c8e3255afd186c142eef4ec400d7826134f028a85da2146102a1172ecc7c3696"}, - {file = "ruff-0.1.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ff78a7583020da124dd0deb835ece1d87bb91762d40c514ee9b67a087940528b"}, - {file = "ruff-0.1.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bd8ee69b02e7bdefe1e5da2d5b6eaaddcf4f90859f00281b2333c0e3a0cc9cd6"}, - {file = "ruff-0.1.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a05b0ddd7ea25495e4115a43125e8a7ebed0aa043c3d432de7e7d6e8e8cd6448"}, - {file = "ruff-0.1.8-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:e6f08ca730f4dc1b76b473bdf30b1b37d42da379202a059eae54ec7fc1fbcfed"}, - {file = "ruff-0.1.8-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f35960b02df6b827c1b903091bb14f4b003f6cf102705efc4ce78132a0aa5af3"}, - {file = "ruff-0.1.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7d076717c67b34c162da7c1a5bda16ffc205e0e0072c03745275e7eab888719f"}, - {file = "ruff-0.1.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b6a21ab023124eafb7cef6d038f835cb1155cd5ea798edd8d9eb2f8b84be07d9"}, - {file = "ruff-0.1.8-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:ce697c463458555027dfb194cb96d26608abab920fa85213deb5edf26e026664"}, - {file = "ruff-0.1.8-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:db6cedd9ffed55548ab313ad718bc34582d394e27a7875b4b952c2d29c001b26"}, - {file = "ruff-0.1.8-py3-none-musllinux_1_2_i686.whl", hash = "sha256:05ffe9dbd278965271252704eddb97b4384bf58b971054d517decfbf8c523f05"}, - {file = "ruff-0.1.8-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5daaeaf00ae3c1efec9742ff294b06c3a2a9db8d3db51ee4851c12ad385cda30"}, - {file = "ruff-0.1.8-py3-none-win32.whl", hash = "sha256:e49fbdfe257fa41e5c9e13c79b9e79a23a79bd0e40b9314bc53840f520c2c0b3"}, - {file = "ruff-0.1.8-py3-none-win_amd64.whl", hash = "sha256:f41f692f1691ad87f51708b823af4bb2c5c87c9248ddd3191c8f088e66ce590a"}, - {file = "ruff-0.1.8-py3-none-win_arm64.whl", hash = "sha256:aa8ee4f8440023b0a6c3707f76cadce8657553655dcbb5fc9b2f9bb9bee389f6"}, - {file = "ruff-0.1.8.tar.gz", hash = "sha256:f7ee467677467526cfe135eab86a40a0e8db43117936ac4f9b469ce9cdb3fb62"}, + {file = "ruff-0.3.3-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:973a0e388b7bc2e9148c7f9be8b8c6ae7471b9be37e1cc732f8f44a6f6d7720d"}, + {file = "ruff-0.3.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:cfa60d23269d6e2031129b053fdb4e5a7b0637fc6c9c0586737b962b2f834493"}, + {file = "ruff-0.3.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1eca7ff7a47043cf6ce5c7f45f603b09121a7cc047447744b029d1b719278eb5"}, + {file = "ruff-0.3.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7d3f6762217c1da954de24b4a1a70515630d29f71e268ec5000afe81377642d"}, + {file = "ruff-0.3.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b24c19e8598916d9c6f5a5437671f55ee93c212a2c4c569605dc3842b6820386"}, + {file = "ruff-0.3.3-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:5a6cbf216b69c7090f0fe4669501a27326c34e119068c1494f35aaf4cc683778"}, + {file = "ruff-0.3.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:352e95ead6964974b234e16ba8a66dad102ec7bf8ac064a23f95371d8b198aab"}, + {file = "ruff-0.3.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d6ab88c81c4040a817aa432484e838aaddf8bfd7ca70e4e615482757acb64f8"}, + {file = "ruff-0.3.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:79bca3a03a759cc773fca69e0bdeac8abd1c13c31b798d5bb3c9da4a03144a9f"}, + {file = "ruff-0.3.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:2700a804d5336bcffe063fd789ca2c7b02b552d2e323a336700abb8ae9e6a3f8"}, + {file = "ruff-0.3.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:fd66469f1a18fdb9d32e22b79f486223052ddf057dc56dea0caaf1a47bdfaf4e"}, + {file = "ruff-0.3.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:45817af234605525cdf6317005923bf532514e1ea3d9270acf61ca2440691376"}, + {file = "ruff-0.3.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:0da458989ce0159555ef224d5b7c24d3d2e4bf4c300b85467b08c3261c6bc6a8"}, + {file = "ruff-0.3.3-py3-none-win32.whl", hash = "sha256:f2831ec6a580a97f1ea82ea1eda0401c3cdf512cf2045fa3c85e8ef109e87de0"}, + {file = "ruff-0.3.3-py3-none-win_amd64.whl", hash = "sha256:be90bcae57c24d9f9d023b12d627e958eb55f595428bafcb7fec0791ad25ddfc"}, + {file = "ruff-0.3.3-py3-none-win_arm64.whl", hash = "sha256:0171aab5fecdc54383993389710a3d1227f2da124d76a2784a7098e818f92d61"}, + {file = "ruff-0.3.3.tar.gz", hash = "sha256:38671be06f57a2f8aba957d9f701ea889aa5736be806f18c0cd03d6ff0cbca8d"}, ] [[package]] name = "setuptools" -version = "69.0.3" +version = "69.2.0" requires_python = ">=3.8" summary = "Easily download, build, install, upgrade, and uninstall Python packages" groups = ["default", "deploy"] files = [ - {file = "setuptools-69.0.3-py3-none-any.whl", hash = "sha256:385eb4edd9c9d5c17540511303e39a147ce2fc04bc55289c322b9e5904fe2c05"}, - {file = "setuptools-69.0.3.tar.gz", hash = "sha256:be1af57fc409f93647f2e8e4573a142ed38724b8cdd389706a867bb4efcf1e78"}, + {file = "setuptools-69.2.0-py3-none-any.whl", hash = "sha256:c21c49fb1042386df081cb5d86759792ab89efca84cf114889191cd09aacc80c"}, + {file = "setuptools-69.2.0.tar.gz", hash = "sha256:0ff4183f8f42cd8fa3acea16c45205521a4ef28f73c6391d8a25e92893134f2e"}, ] [[package]] @@ -1539,13 +1579,13 @@ files = [ [[package]] name = "sniffio" -version = "1.3.0" +version = "1.3.1" requires_python = ">=3.7" summary = "Sniff out which async library your code is running under" groups = ["default"] files = [ - {file = "sniffio-1.3.0-py3-none-any.whl", hash = "sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384"}, - {file = "sniffio-1.3.0.tar.gz", hash = "sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101"}, + {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, + {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, ] [[package]] @@ -1626,26 +1666,46 @@ files = [ {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, ] +[[package]] +name = "tornado" +version = "6.4" +requires_python = ">= 3.8" +summary = "Tornado is a Python web framework and asynchronous networking library, originally developed at FriendFeed." +groups = ["default"] +files = [ + {file = "tornado-6.4-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:02ccefc7d8211e5a7f9e8bc3f9e5b0ad6262ba2fbb683a6443ecc804e5224ce0"}, + {file = "tornado-6.4-cp38-abi3-macosx_10_9_x86_64.whl", hash = "sha256:27787de946a9cffd63ce5814c33f734c627a87072ec7eed71f7fc4417bb16263"}, + {file = "tornado-6.4-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f7894c581ecdcf91666a0912f18ce5e757213999e183ebfc2c3fdbf4d5bd764e"}, + {file = "tornado-6.4-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e43bc2e5370a6a8e413e1e1cd0c91bedc5bd62a74a532371042a18ef19e10579"}, + {file = "tornado-6.4-cp38-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f0251554cdd50b4b44362f73ad5ba7126fc5b2c2895cc62b14a1c2d7ea32f212"}, + {file = "tornado-6.4-cp38-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:fd03192e287fbd0899dd8f81c6fb9cbbc69194d2074b38f384cb6fa72b80e9c2"}, + {file = "tornado-6.4-cp38-abi3-musllinux_1_1_i686.whl", hash = "sha256:88b84956273fbd73420e6d4b8d5ccbe913c65d31351b4c004ae362eba06e1f78"}, + {file = "tornado-6.4-cp38-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:71ddfc23a0e03ef2df1c1397d859868d158c8276a0603b96cf86892bff58149f"}, + {file = "tornado-6.4-cp38-abi3-win32.whl", hash = "sha256:6f8a6c77900f5ae93d8b4ae1196472d0ccc2775cc1dfdc9e7727889145c45052"}, + {file = "tornado-6.4-cp38-abi3-win_amd64.whl", hash = "sha256:10aeaa8006333433da48dec9fe417877f8bcc21f48dda8d661ae79da357b2a63"}, + {file = "tornado-6.4.tar.gz", hash = "sha256:72291fa6e6bc84e626589f1c29d90a5a6d593ef5ae68052ee2ef000dfd273dee"}, +] + [[package]] name = "typing-extensions" -version = "4.9.0" +version = "4.10.0" requires_python = ">=3.8" summary = "Backported and Experimental Type Hints for Python 3.8+" groups = ["default"] files = [ - {file = "typing_extensions-4.9.0-py3-none-any.whl", hash = "sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd"}, - {file = "typing_extensions-4.9.0.tar.gz", hash = "sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783"}, + {file = "typing_extensions-4.10.0-py3-none-any.whl", hash = "sha256:69b1a937c3a517342112fb4c6df7e72fc39a38e7891a5730ed4985b5214b5475"}, + {file = "typing_extensions-4.10.0.tar.gz", hash = "sha256:b0abd7c89e8fb96f98db18d86106ff1d90ab692004eb746cf6eda2682f91b3cb"}, ] [[package]] name = "tzdata" -version = "2023.4" +version = "2024.1" requires_python = ">=2" summary = "Provider of IANA time zone data" groups = ["default"] files = [ - {file = "tzdata-2023.4-py2.py3-none-any.whl", hash = "sha256:aa3ace4329eeacda5b7beb7ea08ece826c28d761cda36e747cfbf97996d39bf3"}, - {file = "tzdata-2023.4.tar.gz", hash = "sha256:dd54c94f294765522c77399649b4fefd95522479a664a0cec87f41bebc6148c9"}, + {file = "tzdata-2024.1-py2.py3-none-any.whl", hash = "sha256:9068bc196136463f5245e51efda838afa15aaeca9903f49050dfa2679db4d252"}, + {file = "tzdata-2024.1.tar.gz", hash = "sha256:2674120f8d891909751c38abcdfd386ac0a5a1127954fbc332af6b5ceae07efd"}, ] [[package]] @@ -1751,7 +1811,7 @@ files = [ [[package]] name = "virtualenv" -version = "20.25.0" +version = "20.25.1" requires_python = ">=3.7" summary = "Virtual Python Environment builder" groups = ["default"] @@ -1761,8 +1821,8 @@ dependencies = [ "platformdirs<5,>=3.9.1", ] files = [ - {file = "virtualenv-20.25.0-py3-none-any.whl", hash = "sha256:4238949c5ffe6876362d9c0180fc6c3a824a7b12b80604eeb8085f2ed7460de3"}, - {file = "virtualenv-20.25.0.tar.gz", hash = "sha256:bf51c0d9c7dd63ea8e44086fa1e4fb1093a31e963b86959257378aef020e1f1b"}, + {file = "virtualenv-20.25.1-py3-none-any.whl", hash = "sha256:961c026ac520bac5f69acb8ea063e8a4f071bcc9457b9c1f28f6b085c511583a"}, + {file = "virtualenv-20.25.1.tar.gz", hash = "sha256:e08e13ecdca7a0bd53798f356d5831434afa5b07b93f0abdf0797b7a06ffe197"}, ] [[package]] diff --git a/backend/pre_start.sh b/backend/pre_start.sh new file mode 100644 index 0000000..de4ba74 --- /dev/null +++ b/backend/pre_start.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +alembic upgrade head + +python ./scripts/init_data.py diff --git a/pyproject.toml b/backend/pyproject.toml similarity index 77% rename from pyproject.toml rename to backend/pyproject.toml index a3659dc..c5223b8 100644 --- a/pyproject.toml +++ b/backend/pyproject.toml @@ -1,8 +1,7 @@ [project] name = "fastapi_best_architecture" version = "0.0.1" -description = """FastAPI based on the construction of the front and back of the separation of rights control system, -using a unique pseudo three-tier architecture model design, and as a template library free open source""" +description = "基于 FastAPI 构建的前后端分离 RBAC 权限控制系统,采用独特的伪三层架构模型设计,内置 fastapi-admin 基本实现" authors = [ { name = "Wu Clan", email = "jianhengwu0407@gmail.com" }, ] @@ -41,22 +40,28 @@ dependencies = [ "python-multipart==0.0.6", "pytz==2023.3", "redis[hiredis]==5.0.1", - "ruff==0.1.8", "SQLAlchemy==2.0.23", "user-agents==2.2.0", "uvicorn[standard]==0.24.0", "XdbSearchIP==1.0.2", - "fastapi-oauth20>=0.0.1a1", + "fastapi_oauth20>=0.0.1a1", + "flower==2.0.1", ] requires-python = ">=3.10" readme = "README.md" license = { text = "MIT" } -[project.optional-dependencies] +[tool.pdm.dev-dependencies] +lint = [ + "ruff>=0.3.3", +] deploy = [ - "supervisor==4.2.5", - "wait-for-it==2.2.2", + "supervisor>=4.2.5", + "wait-for-it>=2.2.2", ] [tool.pdm] package-type = "application" + +[tool.pdm.scripts] +lint = "pre-commit run --all-files" diff --git a/requirements.txt b/backend/requirements.txt similarity index 86% rename from requirements.txt rename to backend/requirements.txt index 8922f27..1f13ab1 100644 --- a/requirements.txt +++ b/backend/requirements.txt @@ -6,7 +6,7 @@ aiosmtplib==3.0.1 alembic==1.13.0 amqp==5.2.0 annotated-types==0.6.0 -anyio==4.2.0 +anyio==4.3.0 asgiref==3.7.2 async-timeout==4.0.3; python_full_version <= "3.11.2" asyncmy==0.2.9 @@ -16,7 +16,7 @@ billiard==4.2.0 casbin==1.34.0 casbin-async-sqlalchemy-adapter==1.4.0 celery==5.3.6 -certifi==2023.11.17 +certifi==2024.2.2 cffi==1.16.0 cfgv==3.4.0 click==8.1.7 @@ -26,7 +26,7 @@ click-repl==0.3.0 colorama==0.4.6; sys_platform == "win32" or platform_system == "Windows" cryptography==41.0.7 distlib==0.3.8 -dnspython==2.5.0 +dnspython==2.6.1 ecdsa==0.18.0 email-validator==2.0.0 exceptiongroup==1.2.0; python_version < "3.11" @@ -36,14 +36,16 @@ fastapi-limiter==0.1.6 fastapi-oauth20==0.0.1a1 fastapi-pagination==0.12.13 filelock==3.13.1 +flower==2.0.1 greenlet==3.0.3; platform_machine == "win32" or platform_machine == "WIN32" or platform_machine == "AMD64" or platform_machine == "amd64" or platform_machine == "x86_64" or platform_machine == "ppc64le" or platform_machine == "aarch64" gunicorn==21.2.0 h11==0.14.0 hiredis==2.3.2 -httpcore==1.0.2 +httpcore==1.0.4 httptools==0.6.1 httpx==0.25.2 -identify==2.5.33 +humanize==4.9.0 +identify==2.5.35 idna==3.6 iniconfig==2.0.0 itsdangerous==2.1.2 @@ -51,11 +53,11 @@ kombu==5.3.5 loguru==0.7.2 mako==1.3.2 markdown-it-py==3.0.0 -markupsafe==2.1.4 +markupsafe==2.1.5 mdurl==0.1.2 msgspec==0.18.5 nodeenv==1.8.0 -packaging==23.2 +packaging==24.0 passlib==1.7.4 path==15.1.2 phonenumbers==8.13.27 @@ -63,6 +65,7 @@ pillow==9.5.0 platformdirs==4.2.0 pluggy==1.4.0 pre-commit==3.2.2 +prometheus-client==0.20.0 prompt-toolkit==3.0.43 psutil==5.9.6 pyasn1==0.5.1 @@ -74,32 +77,33 @@ pydantic-settings==2.1.0 pygments==2.17.2 pytest==7.2.2 pytest-pretty==1.2.0 -python-dateutil==2.8.2 +python-dateutil==2.9.0.post0 python-dotenv==1.0.1 python-jose==3.3.0 python-multipart==0.0.6 pytz==2023.3 pyyaml==6.0.1 redis==5.0.1 -rich==13.7.0 +rich==13.7.1 rsa==4.9 -ruff==0.1.8 -setuptools==69.0.3 +ruff==0.3.3 +setuptools==69.2.0 simpleeval==0.9.13 six==1.16.0 -sniffio==1.3.0 +sniffio==1.3.1 sqlalchemy==2.0.23 starlette==0.32.0.post1 supervisor==4.2.5 tomli==2.0.1; python_version < "3.11" -typing-extensions==4.9.0 -tzdata==2023.4 +tornado==6.4 +typing-extensions==4.10.0 +tzdata==2024.1 ua-parser==0.18.0 user-agents==2.2.0 uvicorn==0.24.0 uvloop==0.19.0; (sys_platform != "cygwin" and sys_platform != "win32") and platform_python_implementation != "PyPy" vine==5.1.0 -virtualenv==20.25.0 +virtualenv==20.25.1 wait-for-it==2.2.2 watchfiles==0.21.0 wcwidth==0.2.13 diff --git a/backend/scripts/format.sh b/backend/scripts/format.sh new file mode 100644 index 0000000..816bab2 --- /dev/null +++ b/backend/scripts/format.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env bash + +ruff format --check diff --git a/backend/scripts/init_data.py b/backend/scripts/init_data.py new file mode 100644 index 0000000..a6aa607 --- /dev/null +++ b/backend/scripts/init_data.py @@ -0,0 +1,20 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +import logging + +from anyio import run + +from backend.database.db_mysql import create_table + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + + +async def init() -> None: + logger.info('Creating initial data') + await create_table() + logger.info('Initial data created') + + +if __name__ == '__main__': + run(init) # type: ignore diff --git a/backend/scripts/lint.sh b/backend/scripts/lint.sh new file mode 100644 index 0000000..3adf95c --- /dev/null +++ b/backend/scripts/lint.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env bash + +pdm lint diff --git a/backend/scripts/pdm_export.sh b/backend/scripts/pdm_export.sh new file mode 100644 index 0000000..8c81b06 --- /dev/null +++ b/backend/scripts/pdm_export.sh @@ -0,0 +1,3 @@ +#!/usr/bin/env bash + +pdm export -p . -o requirements.txt --without-hashes diff --git a/backend/sql/create_tables.sql b/backend/sql/create_tables.sql index 572d037..713bfa9 100644 --- a/backend/sql/create_tables.sql +++ b/backend/sql/create_tables.sql @@ -1,239 +1,280 @@ -CREATE TABLE alembic_version +-- sys_api: table +CREATE TABLE `sys_api` ( - version_num VARCHAR(32) NOT NULL, - CONSTRAINT alembic_version_pkc PRIMARY KEY (version_num) -); + `id` int NOT NULL AUTO_INCREMENT COMMENT '主键id', + `name` varchar(50) NOT NULL COMMENT 'api名称', + `method` varchar(16) NOT NULL COMMENT '请求方法', + `path` varchar(500) NOT NULL COMMENT 'api路径', + `remark` longtext COMMENT '备注', + `created_time` datetime NOT NULL COMMENT '创建时间', + `updated_time` datetime DEFAULT NULL COMMENT '更新时间', + PRIMARY KEY (`id`), + UNIQUE KEY `name` (`name`), + KEY `ix_sys_api_id` (`id`) +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 + COLLATE = utf8mb4_0900_ai_ci; -CREATE TABLE sys_api +-- sys_casbin_rule: table +CREATE TABLE `sys_casbin_rule` ( - id INTEGER NOT NULL AUTO_INCREMENT, - name VARCHAR(50) NOT NULL COMMENT 'api名称', - method VARCHAR(16) NOT NULL COMMENT '请求方法', - path VARCHAR(500) NOT NULL COMMENT 'api路径', - remark LONGTEXT COMMENT '备注', - created_time DATETIME NOT NULL COMMENT '创建时间', - updated_time DATETIME COMMENT '更新时间', - PRIMARY KEY (id), - UNIQUE (name) -); + `id` int NOT NULL AUTO_INCREMENT COMMENT '主键id', + `ptype` varchar(255) NOT NULL COMMENT '策略类型: p / g', + `v0` varchar(255) NOT NULL COMMENT '角色ID / 用户uuid', + `v1` longtext NOT NULL COMMENT 'api路径 / 角色名称', + `v2` varchar(255) DEFAULT NULL COMMENT '请求方法', + `v3` varchar(255) DEFAULT NULL, + `v4` varchar(255) DEFAULT NULL, + `v5` varchar(255) DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `ix_sys_casbin_rule_id` (`id`) +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 + COLLATE = utf8mb4_0900_ai_ci; -CREATE INDEX ix_sys_api_id ON sys_api (id); - -CREATE TABLE sys_casbin_rule +-- sys_dept: table +CREATE TABLE `sys_dept` ( - id INTEGER NOT NULL COMMENT '主键id' AUTO_INCREMENT, - ptype VARCHAR(255) NOT NULL COMMENT '策略类型: p 或者 g', - v0 VARCHAR(255) NOT NULL COMMENT '角色 / 用户uuid', - v1 LONGTEXT NOT NULL COMMENT 'api路径 / 角色名称', - v2 VARCHAR(255) COMMENT '请求方法', - v3 VARCHAR(255), - v4 VARCHAR(255), - v5 VARCHAR(255), - PRIMARY KEY (id) -); + `id` int NOT NULL AUTO_INCREMENT COMMENT '主键id', + `name` varchar(50) NOT NULL COMMENT '部门名称', + `level` int NOT NULL COMMENT '部门层级', + `sort` int NOT NULL COMMENT '排序', + `leader` varchar(20) DEFAULT NULL COMMENT '负责人', + `phone` varchar(11) DEFAULT NULL COMMENT '手机', + `email` varchar(50) DEFAULT NULL COMMENT '邮箱', + `status` int NOT NULL COMMENT '部门状态(0停用 1正常)', + `del_flag` tinyint(1) NOT NULL COMMENT '删除标志(0删除 1存在)', + `parent_id` int DEFAULT NULL COMMENT '父部门ID', + `created_time` datetime NOT NULL COMMENT '创建时间', + `updated_time` datetime DEFAULT NULL COMMENT '更新时间', + PRIMARY KEY (`id`), + KEY `ix_sys_dept_parent_id` (`parent_id`), + KEY `ix_sys_dept_id` (`id`), + CONSTRAINT `sys_dept_ibfk_1` FOREIGN KEY (`parent_id`) REFERENCES `sys_dept` (`id`) ON DELETE SET NULL +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 + COLLATE = utf8mb4_0900_ai_ci; -CREATE INDEX ix_sys_casbin_rule_id ON sys_casbin_rule (id); - -CREATE TABLE sys_dept +-- sys_dict_type: table +CREATE TABLE `sys_dict_type` ( - id INTEGER NOT NULL AUTO_INCREMENT, - name VARCHAR(50) NOT NULL COMMENT '部门名称', - level INTEGER NOT NULL COMMENT '部门层级', - sort INTEGER NOT NULL COMMENT '排序', - leader VARCHAR(20) COMMENT '负责人', - phone VARCHAR(11) COMMENT '手机', - email VARCHAR(50) COMMENT '邮箱', - status INTEGER NOT NULL COMMENT '部门状态(0停用 1正常)', - del_flag BOOL NOT NULL COMMENT '删除标志(0删除 1存在)', - parent_id INTEGER COMMENT '父部门ID', - created_time DATETIME NOT NULL COMMENT '创建时间', - updated_time DATETIME COMMENT '更新时间', - PRIMARY KEY (id), - FOREIGN KEY (parent_id) REFERENCES sys_dept (id) ON DELETE SET NULL -); + `id` int NOT NULL AUTO_INCREMENT COMMENT '主键id', + `name` varchar(32) NOT NULL COMMENT '字典类型名称', + `code` varchar(32) NOT NULL COMMENT '字典类型编码', + `status` int NOT NULL COMMENT '状态(0停用 1正常)', + `remark` longtext COMMENT '备注', + `created_time` datetime NOT NULL COMMENT '创建时间', + `updated_time` datetime DEFAULT NULL COMMENT '更新时间', + PRIMARY KEY (`id`), + UNIQUE KEY `name` (`name`), + UNIQUE KEY `code` (`code`), + KEY `ix_sys_dict_type_id` (`id`) +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 + COLLATE = utf8mb4_0900_ai_ci; -CREATE INDEX ix_sys_dept_id ON sys_dept (id); - -CREATE INDEX ix_sys_dept_parent_id ON sys_dept (parent_id); - -CREATE TABLE sys_dict_type +-- sys_dict_data: table +CREATE TABLE `sys_dict_data` ( - id INTEGER NOT NULL AUTO_INCREMENT, - name VARCHAR(32) NOT NULL COMMENT '字典类型名称', - code VARCHAR(32) NOT NULL COMMENT '字典类型编码', - status INTEGER NOT NULL COMMENT '状态(0停用 1正常)', - remark LONGTEXT COMMENT '备注', - created_time DATETIME NOT NULL COMMENT '创建时间', - updated_time DATETIME COMMENT '更新时间', - PRIMARY KEY (id), - UNIQUE (code), - UNIQUE (name) -); + `id` int NOT NULL AUTO_INCREMENT COMMENT '主键id', + `label` varchar(32) NOT NULL COMMENT '字典标签', + `value` varchar(32) NOT NULL COMMENT '字典值', + `sort` int NOT NULL COMMENT '排序', + `status` int NOT NULL COMMENT '状态(0停用 1正常)', + `remark` longtext COMMENT '备注', + `type_id` int NOT NULL COMMENT '字典类型关联ID', + `created_time` datetime NOT NULL COMMENT '创建时间', + `updated_time` datetime DEFAULT NULL COMMENT '更新时间', + PRIMARY KEY (`id`), + UNIQUE KEY `label` (`label`), + UNIQUE KEY `value` (`value`), + KEY `type_id` (`type_id`), + KEY `ix_sys_dict_data_id` (`id`), + CONSTRAINT `sys_dict_data_ibfk_1` FOREIGN KEY (`type_id`) REFERENCES `sys_dict_type` (`id`) +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 + COLLATE = utf8mb4_0900_ai_ci; -CREATE INDEX ix_sys_dict_type_id ON sys_dict_type (id); - -CREATE TABLE sys_login_log +-- sys_login_log: table +CREATE TABLE `sys_login_log` ( - id INTEGER NOT NULL AUTO_INCREMENT, - user_uuid VARCHAR(50) NOT NULL COMMENT '用户UUID', - username VARCHAR(20) NOT NULL COMMENT '用户名', - status INTEGER NOT NULL COMMENT '登录状态(0失败 1成功)', - ip VARCHAR(50) NOT NULL COMMENT '登录IP地址', - country VARCHAR(50) COMMENT '国家', - region VARCHAR(50) COMMENT '地区', - city VARCHAR(50) COMMENT '城市', - user_agent VARCHAR(255) NOT NULL COMMENT '请求头', - os VARCHAR(50) COMMENT '操作系统', - browser VARCHAR(50) COMMENT '浏览器', - device VARCHAR(50) COMMENT '设备', - msg LONGTEXT NOT NULL COMMENT '提示消息', - login_time DATETIME NOT NULL COMMENT '登录时间', - created_time DATETIME NOT NULL COMMENT '创建时间', - PRIMARY KEY (id) -); + `id` int NOT NULL AUTO_INCREMENT COMMENT '主键id', + `user_uuid` varchar(50) NOT NULL COMMENT '用户UUID', + `username` varchar(20) NOT NULL COMMENT '用户名', + `status` int NOT NULL COMMENT '登录状态(0失败 1成功)', + `ip` varchar(50) NOT NULL COMMENT '登录IP地址', + `country` varchar(50) DEFAULT NULL COMMENT '国家', + `region` varchar(50) DEFAULT NULL COMMENT '地区', + `city` varchar(50) DEFAULT NULL COMMENT '城市', + `user_agent` varchar(255) NOT NULL COMMENT '请求头', + `os` varchar(50) DEFAULT NULL COMMENT '操作系统', + `browser` varchar(50) DEFAULT NULL COMMENT '浏览器', + `device` varchar(50) DEFAULT NULL COMMENT '设备', + `msg` longtext NOT NULL COMMENT '提示消息', + `login_time` datetime NOT NULL COMMENT '登录时间', + `created_time` datetime NOT NULL COMMENT '创建时间', + PRIMARY KEY (`id`), + KEY `ix_sys_login_log_id` (`id`) +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 + COLLATE = utf8mb4_0900_ai_ci; -CREATE INDEX ix_sys_login_log_id ON sys_login_log (id); - -CREATE TABLE sys_menu +-- sys_menu: table +CREATE TABLE `sys_menu` ( - id INTEGER NOT NULL AUTO_INCREMENT, - title VARCHAR(50) NOT NULL COMMENT '菜单标题', - name VARCHAR(50) NOT NULL COMMENT '菜单名称', - level INTEGER NOT NULL COMMENT '菜单层级', - sort INTEGER NOT NULL COMMENT '排序', - icon VARCHAR(100) COMMENT '菜单图标', - path VARCHAR(200) COMMENT '路由地址', - menu_type INTEGER NOT NULL COMMENT '菜单类型(0目录 1菜单 2按钮)', - component VARCHAR(255) COMMENT '组件路径', - perms VARCHAR(100) COMMENT '权限标识', - status INTEGER NOT NULL COMMENT '菜单状态(0停用 1正常)', - `show` INTEGER NOT NULL COMMENT '是否显示(0否 1是)', - cache INTEGER NOT NULL COMMENT '是否缓存(0否 1是)', - remark LONGTEXT COMMENT '备注', - parent_id INTEGER COMMENT '父菜单ID', - created_time DATETIME NOT NULL COMMENT '创建时间', - updated_time DATETIME COMMENT '更新时间', - PRIMARY KEY (id), - FOREIGN KEY (parent_id) REFERENCES sys_menu (id) ON DELETE SET NULL -); + `id` int NOT NULL AUTO_INCREMENT COMMENT '主键id', + `title` varchar(50) NOT NULL COMMENT '菜单标题', + `name` varchar(50) NOT NULL COMMENT '菜单名称', + `level` int NOT NULL COMMENT '菜单层级', + `sort` int NOT NULL COMMENT '排序', + `icon` varchar(100) DEFAULT NULL COMMENT '菜单图标', + `path` varchar(200) DEFAULT NULL COMMENT '路由地址', + `menu_type` int NOT NULL COMMENT '菜单类型(0目录 1菜单 2按钮)', + `component` varchar(255) DEFAULT NULL COMMENT '组件路径', + `perms` varchar(100) DEFAULT NULL COMMENT '权限标识', + `status` int NOT NULL COMMENT '菜单状态(0停用 1正常)', + `show` int NOT NULL COMMENT '是否显示(0否 1是)', + `cache` int NOT NULL COMMENT '是否缓存(0否 1是)', + `remark` longtext COMMENT '备注', + `parent_id` int DEFAULT NULL COMMENT '父菜单ID', + `created_time` datetime NOT NULL COMMENT '创建时间', + `updated_time` datetime DEFAULT NULL COMMENT '更新时间', + PRIMARY KEY (`id`), + KEY `ix_sys_menu_id` (`id`), + KEY `ix_sys_menu_parent_id` (`parent_id`), + CONSTRAINT `sys_menu_ibfk_1` FOREIGN KEY (`parent_id`) REFERENCES `sys_menu` (`id`) ON DELETE SET NULL +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 + COLLATE = utf8mb4_0900_ai_ci; -CREATE INDEX ix_sys_menu_id ON sys_menu (id); - -CREATE INDEX ix_sys_menu_parent_id ON sys_menu (parent_id); - -CREATE TABLE sys_opera_log +-- sys_opera_log: table +CREATE TABLE `sys_opera_log` ( - id INTEGER NOT NULL AUTO_INCREMENT, - username VARCHAR(20) COMMENT '用户名', - method VARCHAR(20) NOT NULL COMMENT '请求类型', - title VARCHAR(255) NOT NULL COMMENT '操作模块', - path VARCHAR(500) NOT NULL COMMENT '请求路径', - ip VARCHAR(50) NOT NULL COMMENT 'IP地址', - country VARCHAR(50) COMMENT '国家', - region VARCHAR(50) COMMENT '地区', - city VARCHAR(50) COMMENT '城市', - user_agent VARCHAR(255) NOT NULL COMMENT '请求头', - os VARCHAR(50) COMMENT '操作系统', - browser VARCHAR(50) COMMENT '浏览器', - device VARCHAR(50) COMMENT '设备', - args JSON COMMENT '请求参数', - status INTEGER NOT NULL COMMENT '操作状态(0异常 1正常)', - code VARCHAR(20) NOT NULL COMMENT '操作状态码', - msg LONGTEXT COMMENT '提示消息', - cost_time FLOAT NOT NULL COMMENT '请求耗时ms', - opera_time DATETIME NOT NULL COMMENT '操作时间', - created_time DATETIME NOT NULL COMMENT '创建时间', - PRIMARY KEY (id) -); + `id` int NOT NULL AUTO_INCREMENT COMMENT '主键id', + `username` varchar(20) DEFAULT NULL COMMENT '用户名', + `method` varchar(20) NOT NULL COMMENT '请求类型', + `title` varchar(255) NOT NULL COMMENT '操作模块', + `path` varchar(500) NOT NULL COMMENT '请求路径', + `ip` varchar(50) NOT NULL COMMENT 'IP地址', + `country` varchar(50) DEFAULT NULL COMMENT '国家', + `region` varchar(50) DEFAULT NULL COMMENT '地区', + `city` varchar(50) DEFAULT NULL COMMENT '城市', + `user_agent` varchar(255) NOT NULL COMMENT '请求头', + `os` varchar(50) DEFAULT NULL COMMENT '操作系统', + `browser` varchar(50) DEFAULT NULL COMMENT '浏览器', + `device` varchar(50) DEFAULT NULL COMMENT '设备', + `args` json DEFAULT NULL COMMENT '请求参数', + `status` int NOT NULL COMMENT '操作状态(0异常 1正常)', + `code` varchar(20) NOT NULL COMMENT '操作状态码', + `msg` longtext COMMENT '提示消息', + `cost_time` float NOT NULL COMMENT '请求耗时ms', + `opera_time` datetime NOT NULL COMMENT '操作时间', + `created_time` datetime NOT NULL COMMENT '创建时间', + PRIMARY KEY (`id`), + KEY `ix_sys_opera_log_id` (`id`) +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 + COLLATE = utf8mb4_0900_ai_ci; -CREATE INDEX ix_sys_opera_log_id ON sys_opera_log (id); - -CREATE TABLE sys_role +-- sys_role: table +CREATE TABLE `sys_role` ( - id INTEGER NOT NULL AUTO_INCREMENT, - name VARCHAR(20) NOT NULL COMMENT '角色名称', - data_scope INTEGER COMMENT '权限范围(1:全部数据权限 2:自定义数据权限)', - status INTEGER NOT NULL COMMENT '角色状态(0停用 1正常)', - remark LONGTEXT COMMENT '备注', - created_time DATETIME NOT NULL COMMENT '创建时间', - updated_time DATETIME COMMENT '更新时间', - PRIMARY KEY (id), - UNIQUE (name) -); + `id` int NOT NULL AUTO_INCREMENT COMMENT '主键id', + `name` varchar(20) NOT NULL COMMENT '角色名称', + `data_scope` int DEFAULT NULL COMMENT '权限范围(1:全部数据权限 2:自定义数据权限)', + `status` int NOT NULL COMMENT '角色状态(0停用 1正常)', + `remark` longtext COMMENT '备注', + `created_time` datetime NOT NULL COMMENT '创建时间', + `updated_time` datetime DEFAULT NULL COMMENT '更新时间', + PRIMARY KEY (`id`), + UNIQUE KEY `name` (`name`), + KEY `ix_sys_role_id` (`id`) +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 + COLLATE = utf8mb4_0900_ai_ci; -CREATE INDEX ix_sys_role_id ON sys_role (id); - -CREATE TABLE sys_dict_data +-- sys_role_menu: table +CREATE TABLE `sys_role_menu` ( - id INTEGER NOT NULL AUTO_INCREMENT, - label VARCHAR(32) NOT NULL COMMENT '字典标签', - value VARCHAR(32) NOT NULL COMMENT '字典值', - sort INTEGER NOT NULL COMMENT '排序', - status INTEGER NOT NULL COMMENT '状态(0停用 1正常)', - remark LONGTEXT COMMENT '备注', - type_id INTEGER NOT NULL COMMENT '字典类型关联ID', - created_time DATETIME NOT NULL COMMENT '创建时间', - updated_time DATETIME COMMENT '更新时间', - PRIMARY KEY (id), - FOREIGN KEY (type_id) REFERENCES sys_dict_type (id), - UNIQUE (label), - UNIQUE (value) -); + `id` int NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `role_id` int NOT NULL COMMENT '角色ID', + `menu_id` int NOT NULL COMMENT '菜单ID', + PRIMARY KEY (`id`, `role_id`, `menu_id`), + UNIQUE KEY `ix_sys_role_menu_id` (`id`), + KEY `role_id` (`role_id`), + KEY `menu_id` (`menu_id`), + CONSTRAINT `sys_role_menu_ibfk_1` FOREIGN KEY (`role_id`) REFERENCES `sys_role` (`id`) ON DELETE CASCADE, + CONSTRAINT `sys_role_menu_ibfk_2` FOREIGN KEY (`menu_id`) REFERENCES `sys_menu` (`id`) ON DELETE CASCADE +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 + COLLATE = utf8mb4_0900_ai_ci; -CREATE INDEX ix_sys_dict_data_id ON sys_dict_data (id); - -CREATE TABLE sys_role_menu +-- sys_user: table +CREATE TABLE `sys_user` ( - id INTEGER NOT NULL COMMENT '主键ID' AUTO_INCREMENT, - role_id INTEGER NOT NULL COMMENT '角色ID', - menu_id INTEGER NOT NULL COMMENT '菜单ID', - PRIMARY KEY (id, role_id, menu_id), - FOREIGN KEY (menu_id) REFERENCES sys_menu (id) ON DELETE CASCADE, - FOREIGN KEY (role_id) REFERENCES sys_role (id) ON DELETE CASCADE -); + `id` int NOT NULL AUTO_INCREMENT COMMENT '主键id', + `uuid` varchar(50) NOT NULL, + `username` varchar(20) NOT NULL COMMENT '用户名', + `nickname` varchar(20) NOT NULL COMMENT '昵称', + `password` varchar(255) DEFAULT NULL COMMENT '密码', + `salt` varchar(5) DEFAULT NULL COMMENT '加密盐', + `email` varchar(50) NOT NULL COMMENT '邮箱', + `is_superuser` tinyint(1) NOT NULL COMMENT '超级权限(0否 1是)', + `is_staff` tinyint(1) NOT NULL COMMENT '后台管理登陆(0否 1是)', + `status` int NOT NULL COMMENT '用户账号状态(0停用 1正常)', + `is_multi_login` tinyint(1) NOT NULL COMMENT '是否重复登陆(0否 1是)', + `avatar` varchar(255) DEFAULT NULL COMMENT '头像', + `phone` varchar(11) DEFAULT NULL COMMENT '手机号', + `join_time` datetime NOT NULL COMMENT '注册时间', + `last_login_time` datetime DEFAULT NULL COMMENT '上次登录', + `dept_id` int DEFAULT NULL COMMENT '部门关联ID', + `created_time` datetime NOT NULL COMMENT '创建时间', + `updated_time` datetime DEFAULT NULL COMMENT '更新时间', + PRIMARY KEY (`id`), + UNIQUE KEY `uuid` (`uuid`), + UNIQUE KEY `nickname` (`nickname`), + UNIQUE KEY `ix_sys_user_username` (`username`), + UNIQUE KEY `ix_sys_user_email` (`email`), + KEY `dept_id` (`dept_id`), + KEY `ix_sys_user_id` (`id`), + CONSTRAINT `sys_user_ibfk_1` FOREIGN KEY (`dept_id`) REFERENCES `sys_dept` (`id`) ON DELETE SET NULL +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 + COLLATE = utf8mb4_0900_ai_ci; -CREATE UNIQUE INDEX ix_sys_role_menu_id ON sys_role_menu (id); - -CREATE TABLE sys_user +-- sys_user_role: table +CREATE TABLE `sys_user_role` ( - id INTEGER NOT NULL AUTO_INCREMENT, - uuid VARCHAR(50) NOT NULL, - username VARCHAR(20) NOT NULL COMMENT '用户名', - nickname VARCHAR(20) NOT NULL COMMENT '昵称', - password VARCHAR(255) NOT NULL COMMENT '密码', - salt VARCHAR(5) NOT NULL COMMENT '加密盐', - email VARCHAR(50) NOT NULL COMMENT '邮箱', - is_superuser BOOL NOT NULL COMMENT '超级权限(0否 1是)', - is_staff BOOL NOT NULL COMMENT '后台管理登陆(0否 1是)', - status INTEGER NOT NULL COMMENT '用户账号状态(0停用 1正常)', - is_multi_login BOOL NOT NULL COMMENT '是否重复登陆(0否 1是)', - avatar VARCHAR(255) COMMENT '头像', - phone VARCHAR(11) COMMENT '手机号', - join_time DATETIME NOT NULL COMMENT '注册时间', - last_login_time DATETIME COMMENT '上次登录', - dept_id INTEGER COMMENT '部门关联ID', - created_time DATETIME NOT NULL COMMENT '创建时间', - updated_time DATETIME COMMENT '更新时间', - PRIMARY KEY (id), - FOREIGN KEY (dept_id) REFERENCES sys_dept (id) ON DELETE SET NULL, - UNIQUE (nickname), - UNIQUE (uuid) -); + `id` int NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `user_id` int NOT NULL COMMENT '用户ID', + `role_id` int NOT NULL COMMENT '角色ID', + PRIMARY KEY (`id`, `user_id`, `role_id`), + UNIQUE KEY `ix_sys_user_role_id` (`id`), + KEY `user_id` (`user_id`), + KEY `role_id` (`role_id`), + CONSTRAINT `sys_user_role_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `sys_user` (`id`) ON DELETE CASCADE, + CONSTRAINT `sys_user_role_ibfk_2` FOREIGN KEY (`role_id`) REFERENCES `sys_role` (`id`) ON DELETE CASCADE +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 + COLLATE = utf8mb4_0900_ai_ci; -CREATE UNIQUE INDEX ix_sys_user_email ON sys_user (email); - -CREATE INDEX ix_sys_user_id ON sys_user (id); - -CREATE UNIQUE INDEX ix_sys_user_username ON sys_user (username); - -CREATE TABLE sys_user_role +-- sys_user_social: table +CREATE TABLE `sys_user_social` ( - id INTEGER NOT NULL COMMENT '主键ID' AUTO_INCREMENT, - user_id INTEGER NOT NULL COMMENT '用户ID', - role_id INTEGER NOT NULL COMMENT '角色ID', - PRIMARY KEY (id, user_id, role_id), - FOREIGN KEY (role_id) REFERENCES sys_role (id) ON DELETE CASCADE, - FOREIGN KEY (user_id) REFERENCES sys_user (id) ON DELETE CASCADE -); - -CREATE UNIQUE INDEX ix_sys_user_role_id ON sys_user_role (id); + `id` int NOT NULL AUTO_INCREMENT COMMENT '主键id', + `source` varchar(20) NOT NULL COMMENT '第三方用户来源', + `open_id` varchar(20) DEFAULT NULL COMMENT '第三方用户的 open id', + `uid` varchar(20) DEFAULT NULL COMMENT '第三方用户的 ID', + `union_id` varchar(20) DEFAULT NULL COMMENT '第三方用户的 union id', + `scope` varchar(120) DEFAULT NULL COMMENT '第三方用户授予的权限', + `code` varchar(50) DEFAULT NULL COMMENT '用户的授权 code', + `user_id` int DEFAULT NULL COMMENT '用户关联ID', + `created_time` datetime NOT NULL COMMENT '创建时间', + `updated_time` datetime DEFAULT NULL COMMENT '更新时间', + PRIMARY KEY (`id`), + KEY `user_id` (`user_id`), + KEY `ix_sys_user_social_id` (`id`), + CONSTRAINT `sys_user_social_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `sys_user` (`id`) ON DELETE SET NULL +) ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 + COLLATE = utf8mb4_0900_ai_ci; diff --git a/backend/app/static/ip2region.xdb b/backend/static/ip2region.xdb similarity index 100% rename from backend/app/static/ip2region.xdb rename to backend/static/ip2region.xdb diff --git a/backend/utils/__init__.py b/backend/utils/__init__.py new file mode 100644 index 0000000..56fafa5 --- /dev/null +++ b/backend/utils/__init__.py @@ -0,0 +1,2 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- diff --git a/backend/app/utils/build_tree.py b/backend/utils/build_tree.py similarity index 94% rename from backend/app/utils/build_tree.py rename to backend/utils/build_tree.py index 53917ff..5037bef 100644 --- a/backend/app/utils/build_tree.py +++ b/backend/utils/build_tree.py @@ -4,8 +4,8 @@ from typing import Any, Sequence from asgiref.sync import sync_to_async -from backend.app.common.enums import BuildTreeType -from backend.app.utils.serializers import RowData, select_list_serialize +from backend.common.enums import BuildTreeType +from backend.utils.serializers import RowData, select_list_serialize async def get_tree_nodes(row: Sequence[RowData]) -> list[dict[str, Any]]: diff --git a/backend/app/utils/demo_site.py b/backend/utils/demo_site.py similarity index 82% rename from backend/app/utils/demo_site.py rename to backend/utils/demo_site.py index 648a793..4792b13 100644 --- a/backend/app/utils/demo_site.py +++ b/backend/utils/demo_site.py @@ -2,8 +2,8 @@ # -*- coding: utf-8 -*- from fastapi import Request -from backend.app.common.exception import errors -from backend.app.core.conf import settings +from backend.common.exception import errors +from backend.core.conf import settings async def demo_site(request: Request): diff --git a/backend/app/utils/encrypt.py b/backend/utils/encrypt.py similarity index 98% rename from backend/app/utils/encrypt.py rename to backend/utils/encrypt.py index d620d30..91f6db7 100644 --- a/backend/app/utils/encrypt.py +++ b/backend/utils/encrypt.py @@ -9,7 +9,7 @@ from cryptography.hazmat.primitives import padding from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes from itsdangerous import URLSafeSerializer -from backend.app.common.log import log +from backend.common.log import log class AESCipher: diff --git a/backend/app/utils/health_check.py b/backend/utils/health_check.py similarity index 95% rename from backend/app/utils/health_check.py rename to backend/utils/health_check.py index 0d759cc..2ca4d7c 100644 --- a/backend/app/utils/health_check.py +++ b/backend/utils/health_check.py @@ -5,7 +5,7 @@ from math import ceil from fastapi import FastAPI, Request, Response from fastapi.routing import APIRoute -from backend.app.common.exception import errors +from backend.common.exception import errors def ensure_unique_route_names(app: FastAPI) -> None: diff --git a/backend/app/utils/openapi.py b/backend/utils/openapi.py similarity index 100% rename from backend/app/utils/openapi.py rename to backend/utils/openapi.py diff --git a/backend/app/utils/re_verify.py b/backend/utils/re_verify.py similarity index 100% rename from backend/app/utils/re_verify.py rename to backend/utils/re_verify.py diff --git a/backend/app/utils/redis_info.py b/backend/utils/redis_info.py similarity index 90% rename from backend/app/utils/redis_info.py rename to backend/utils/redis_info.py index 54442a8..9f012ef 100644 --- a/backend/app/utils/redis_info.py +++ b/backend/utils/redis_info.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -from backend.app.common.redis import redis_client -from backend.app.utils.server_info import server_info +from backend.database.db_redis import redis_client +from backend.utils.server_info import server_info class RedisInfo: diff --git a/backend/app/utils/request_parse.py b/backend/utils/request_parse.py similarity index 94% rename from backend/app/utils/request_parse.py rename to backend/utils/request_parse.py index 2718e1c..3edc78b 100644 --- a/backend/app/utils/request_parse.py +++ b/backend/utils/request_parse.py @@ -7,10 +7,10 @@ from fastapi import Request from user_agents import parse from XdbSearchIP.xdbSearcher import XdbSearcher -from backend.app.common.log import log -from backend.app.common.redis import redis_client -from backend.app.core.conf import settings -from backend.app.core.path_conf import IP2REGION_XDB +from backend.common.log import log +from backend.core.conf import settings +from backend.core.path_conf import IP2REGION_XDB +from backend.database.db_redis import redis_client @sync_to_async diff --git a/backend/app/utils/serializers.py b/backend/utils/serializers.py similarity index 100% rename from backend/app/utils/serializers.py rename to backend/utils/serializers.py diff --git a/backend/app/utils/server_info.py b/backend/utils/server_info.py similarity index 88% rename from backend/app/utils/server_info.py rename to backend/utils/server_info.py index a505d51..62dd347 100644 --- a/backend/app/utils/server_info.py +++ b/backend/utils/server_info.py @@ -9,7 +9,7 @@ from typing import List import psutil -from backend.app.utils.timezone import timezone +from backend.utils.timezone import timezone class ServerInfo: @@ -92,17 +92,15 @@ class ServerInfo: disk_info = [] for disk in psutil.disk_partitions(): usage = psutil.disk_usage(disk.mountpoint) - disk_info.append( - { - 'dir': disk.mountpoint, - 'type': disk.fstype, - 'device': disk.device, - 'total': ServerInfo.format_bytes(usage.total), - 'free': ServerInfo.format_bytes(usage.free), - 'used': ServerInfo.format_bytes(usage.used), - 'usage': f'{round(usage.percent, 2)} %', - } - ) + disk_info.append({ + 'dir': disk.mountpoint, + 'type': disk.fstype, + 'device': disk.device, + 'total': ServerInfo.format_bytes(usage.total), + 'free': ServerInfo.format_bytes(usage.free), + 'used': ServerInfo.format_bytes(usage.used), + 'usage': f'{round(usage.percent, 2)} %', + }) return disk_info @staticmethod diff --git a/backend/app/utils/timezone.py b/backend/utils/timezone.py similarity index 95% rename from backend/app/utils/timezone.py rename to backend/utils/timezone.py index 23dc956..e9f37a9 100644 --- a/backend/app/utils/timezone.py +++ b/backend/utils/timezone.py @@ -4,7 +4,7 @@ import zoneinfo from datetime import datetime -from backend.app.core.conf import settings +from backend.core.conf import settings class TimeZone: diff --git a/deploy/backend/celery.conf b/deploy/backend/celery.conf new file mode 100644 index 0000000..7b1039e --- /dev/null +++ b/deploy/backend/celery.conf @@ -0,0 +1,29 @@ +[program:celery_worker] +directory=/fba/backend +command=/usr/local/bin/celery -A app.task.celery worker --loglevel=INFO +user=root +autostart=true +autorestart=true +startretries=5 +redirect_stderr=true +stdout_logfile=/var/log/celery/fba_celery_worker.log + +[program:celery_beat] +directory=/fba/backend +command=/usr/local/bin/celery -A app.task.celery beat --loglevel=INFO +user=root +autostart=true +autorestart=true +startretries=5 +redirect_stderr=true +stdout_logfile=/var/log/celery/fba_celery_beat.log + +[program:celery_flower] +directory=/fba/backend +command=/usr/local/bin/celery -A app.task.celery flower --port=8555 --basic-auth=admin:123456 +user=root +autostart=true +autorestart=true +startretries=5 +redirect_stderr=true +stdout_logfile=/var/log/celery/fba_celery_flower.log diff --git a/deploy/docker-compose/.env.docker b/deploy/backend/docker-compose/.env.docker similarity index 56% rename from deploy/docker-compose/.env.docker rename to deploy/backend/docker-compose/.env.docker index c21edba..f235e50 100644 --- a/deploy/docker-compose/.env.docker +++ b/deploy/backend/docker-compose/.env.docker @@ -1,3 +1,3 @@ # Docker -DOCKER_DB_MAP_PORT=13306 +DOCKER_MYSQL_MAP_PORT=13306 DOCKER_REDIS_MAP_PORT=16379 diff --git a/deploy/docker-compose/.env.server b/deploy/backend/docker-compose/.env.server similarity index 79% rename from deploy/docker-compose/.env.server rename to deploy/backend/docker-compose/.env.server index 7e208c7..2e79ff0 100644 --- a/deploy/docker-compose/.env.server +++ b/deploy/backend/docker-compose/.env.server @@ -1,19 +1,25 @@ # Env: dev、pro ENVIRONMENT='dev' # MySQL -DB_HOST='fba_mysql' -DB_PORT=3306 -DB_USER='root' -DB_PASSWORD='123456' +MYSQL_HOST='fba_mysql' +MYSQL_PORT=3306 +MYSQL_USER='root' +MYSQL_PASSWORD='123456' # Redis REDIS_HOST='fba_redis' REDIS_PORT=6379 REDIS_PASSWORD='' REDIS_DATABASE=0 +# Token +TOKEN_SECRET_KEY='1VkVF75nsNABBjK_7-qz7GtzNy3AMvktc9TCPwKczCk' +# Opera Log +OPERA_LOG_ENCRYPT_SECRET_KEY='d77b25790a804c2b4a339dd0207941e4cefa5751935a33735bc73bb7071a005b' +# Admin +# OAuth2 +OAUTH2_GITHUB_CLIENT_ID='test' +OAUTH2_GITHUB_CLIENT_SECRET='test' +# Task # Celery -CELERY_REDIS_HOST='fba_redis' -CELERY_REDIS_PORT=6379 -CELERY_REDIS_PASSWORD='' CELERY_BROKER_REDIS_DATABASE=1 CELERY_BACKEND_REDIS_DATABASE=2 # Rabbitmq @@ -21,10 +27,3 @@ RABBITMQ_HOST='fba_rabbitmq' RABBITMQ_PORT=5672 RABBITMQ_USERNAME='guest' RABBITMQ_PASSWORD='guest' -# Token -TOKEN_SECRET_KEY='1VkVF75nsNABBjK_7-qz7GtzNy3AMvktc9TCPwKczCk' -# Opera Log -OPERA_LOG_ENCRYPT_SECRET_KEY='d77b25790a804c2b4a339dd0207941e4cefa5751935a33735bc73bb7071a005b' -# OAuth2 -OAUTH2_GITHUB_CLIENT_ID='test' -OAUTH2_GITHUB_CLIENT_SECRET='test' diff --git a/deploy/docker-compose/docker-compose.yml b/deploy/backend/docker-compose/docker-compose.yml similarity index 85% rename from deploy/docker-compose/docker-compose.yml rename to deploy/backend/docker-compose/docker-compose.yml index b3db8a3..623b6b5 100644 --- a/deploy/docker-compose/docker-compose.yml +++ b/deploy/backend/docker-compose/docker-compose.yml @@ -22,8 +22,8 @@ volumes: services: fba_server: build: - context: ../../ - dockerfile: backend.dockerfile + context: ../../../ + dockerfile: backend/backend.dockerfile container_name: fba_server restart: always depends_on: @@ -40,13 +40,13 @@ services: - | wait-for-it -s fba_mysql:3306 -s fba_redis:6379 -t 300 mkdir -p /var/log/supervisor/ - supervisord -c /fba/deploy/supervisor.conf + supervisord -c /fba/deploy/backend/supervisor.conf supervisorctl restart fastapi_server fba_mysql: image: mysql:8.0.29 ports: - - "${DOCKER_DB_MAP_PORT:-3306}:3306" + - "${DOCKER_MYSQL_MAP_PORT:-3306}:3306" container_name: fba_mysql restart: always environment: @@ -88,7 +88,7 @@ services: - fba_server volumes: - ../nginx.conf:/etc/nginx/nginx.conf:ro - - fba_static:/www/fba_server/backend/app/static + - fba_static:/www/fba_server/backend/static networks: - fba_network @@ -110,8 +110,10 @@ services: fba_celery: build: - context: ../../ - dockerfile: celery.dockerfile + context: ../../../ + dockerfile: backend/celery.dockerfile + ports: + - "8555:8555" container_name: fba_celery restart: always depends_on: @@ -124,6 +126,7 @@ services: - | wait-for-it -s fba_rabbitmq:5672 -t 300 mkdir -p /var/log/supervisor/ - supervisord -c /fba/deploy/supervisor.conf + supervisord -c /fba/deploy/backend/supervisor.conf supervisorctl restart celery_worker supervisorctl restart celery_beat + supervisorctl restart celery_flower diff --git a/deploy/fastapi_server.conf b/deploy/backend/fastapi_server.conf similarity index 67% rename from deploy/fastapi_server.conf rename to deploy/backend/fastapi_server.conf index 76bd9ba..3178bef 100644 --- a/deploy/fastapi_server.conf +++ b/deploy/backend/fastapi_server.conf @@ -1,6 +1,6 @@ [program:fastapi_server] directory=/fba -command=/usr/local/bin/gunicorn -c /fba/deploy/gunicorn.conf.py main:app +command=/usr/local/bin/gunicorn -c /fba/deploy/backend/gunicorn.conf.py main:app user=root autostart=true autorestart=true diff --git a/deploy/gunicorn.conf.py b/deploy/backend/gunicorn.conf.py similarity index 70% rename from deploy/gunicorn.conf.py rename to deploy/backend/gunicorn.conf.py index 3cd9ff1..db92135 100644 --- a/deploy/gunicorn.conf.py +++ b/deploy/backend/gunicorn.conf.py @@ -1,8 +1,8 @@ # 监听内网端口 -bind = '0.0.0.0:8001' +bind = "0.0.0.0:8001" # 工作目录 -chdir = '/fba/backend/app' +chdir = "/fba/backend/" # 并行工作进程数 workers = 1 @@ -22,25 +22,25 @@ timeout = 120 daemon = False # 工作模式协程 -worker_class = 'uvicorn.workers.UvicornWorker' +worker_class = "uvicorn.workers.UvicornWorker" # 设置最大并发量 worker_connections = 2000 # 设置进程文件目录 -pidfile = '/fba/gunicorn.pid' +pidfile = "/fba/gunicorn.pid" # 设置访问日志和错误信息日志路径 -accesslog = '/var/log/fastapi_server/gunicorn_access.log' -errorlog = '/var/log/fastapi_server/gunicorn_error.log' +accesslog = "/var/log/fastapi_server/gunicorn_access.log" +errorlog = "/var/log/fastapi_server/gunicorn_error.log" # 设置这个值为true 才会把打印信息记录到错误日志里 capture_output = True # 设置日志记录水平 -loglevel = 'debug' +loglevel = "debug" # python程序 -pythonpath = '/usr/local/lib/python3.10/site-packages' +pythonpath = "/usr/local/lib/python3.10/site-packages" # 启动 gunicorn -c gunicorn.conf.py main:app diff --git a/deploy/nginx.conf b/deploy/backend/nginx.conf similarity index 96% rename from deploy/nginx.conf rename to deploy/backend/nginx.conf index aa8e481..6ca80e0 100644 --- a/deploy/nginx.conf +++ b/deploy/backend/nginx.conf @@ -52,7 +52,7 @@ http { } location /static/ { - alias /www/fba_server/backend/app/static; + alias /www/fba_server/backend/static; } } } diff --git a/deploy/supervisor.conf b/deploy/backend/supervisor.conf similarity index 100% rename from deploy/supervisor.conf rename to deploy/backend/supervisor.conf diff --git a/deploy/celery.conf b/deploy/celery.conf deleted file mode 100644 index ea68c06..0000000 --- a/deploy/celery.conf +++ /dev/null @@ -1,19 +0,0 @@ -[program:celery_worker] -directory=/fba/backend/app -command=/usr/local/bin/celery -A tasks worker --loglevel=INFO -user=root -autostart=true -autorestart=true -startretries=5 -redirect_stderr=true -stdout_logfile=/var/log/celery/fba_celery_worker.log - -[program:celery_beat] -directory=/fba/backend/app -command=/usr/local/bin/celery -A tasks beat --loglevel=INFO -user=root -autostart=true -autorestart=true -startretries=5 -redirect_stderr=true -stdout_logfile=/var/log/celery/fba_celery_beat.log