Compare commits

...
36 Commits
Author SHA1 Message Date
Wu Clan 16a0f9646d Update the SQL script for creat tables (#597) 2025-04-28 19:39:33 +08:00
Wu Clan 032364e48e Refactor the data rule to scope rule (#596)
* WIP

* update codes

* update codes

* update filter_data_permission

* Fix schema

* Fix issues
2025-04-28 18:15:52 +08:00
Wu Clan bc5d142920 Optimize the server information retrieval (#595)
* Optimize the server information retrieval

* Update import

* Fix typing
2025-04-28 15:41:53 +08:00
Wu Clan 9b3425d801 Update the default RBAC solution (#593) 2025-04-27 14:54:39 +08:00
Wu Clan 5842127c2b Update code generator table columns (#590)
* Update code generator table columns

* Update func doc
2025-04-24 20:27:08 +08:00
Wu Clan 193eba3c58 Update the changelog for v1.1.2 (#589) 2025-04-23 23:43:56 +08:00
Wu Clan 338e50e255 Update the routing style of the task app (#588)
* Update the routing style of the task app

* Fix prefix
2025-04-23 19:33:36 +08:00
Wu Clan 65500d7a53 Bump fastapi pagination from 0.12.34 to 0.13.0 (#587) 2025-04-23 13:04:09 +08:00
Wu Clan 0833cd5443 Optimize the userinfo cache cleaning logic (#586) 2025-04-23 12:40:27 +08:00
Wu Clan e8c844ead7 Fix cache cleanup when updating role menu (#585) 2025-04-21 17:58:01 +08:00
Wu Clan 1d5f2dc5f0 Fix the condition to query menu by title (#584) 2025-04-21 17:57:12 +08:00
Wu Clanandwu-clan 9a36154564 Update the changelog for v1.1.1 (#583)
This commit was created by changelog-from-release in 'Release changelog' CI workflow

Co-authored-by: wu-clan <wu-clan@users.noreply.github.com>
2025-04-18 23:19:01 +08:00
Wu Clan 5f8ae74ea5 Fix list query in the dict plugin (#582) 2025-04-18 23:16:03 +08:00
Wu Clan da8e7c1b01 Fix the plugin system route injection (#581) 2025-04-18 23:09:25 +08:00
Wu Clan f513b98f15 Update the changelog for v1.1.0 (#580) 2025-04-17 17:54:52 +08:00
Wu Clan 80cef6dde7 Update code generator file and table naming (#579)
* Update code generator file and table naming

* Update column table comment
2025-04-17 17:47:20 +08:00
Huyuwei 2d504f281e Fix avatar url type of update avatar (#575) 2025-04-17 15:02:19 +08:00
Wu Clan fc34c98f0a Update the code generator to plugin (#578)
* Update the code generator to plugin

* Fix get all tables return type
2025-04-17 11:06:43 +08:00
Wu Clan 1cc020efdd Update unique columns in dict models (#577) 2025-04-17 11:06:29 +08:00
Wu Clan bb33e99e33 Update casbin RBAC verify to dynamic import (#576) 2025-04-17 11:06:15 +08:00
Wu Clan 6c70b34ae6 Optimize the file structure of code generator (#574) 2025-04-15 19:54:18 +08:00
Wu Clan 960d9f6215 Update the default value for some functions (#573)
* Update the default value for some functions

* Fix lint
2025-04-14 14:22:05 +08:00
Wu Clan 085f08ec3b Update the changelog for v1.0.5 (#572) 2025-04-09 11:55:49 +08:00
Wu Clan 2a8722f56e Fix the log rule in gitignore (#571) 2025-04-09 11:53:55 +08:00
Wu Clan 4372b9bbc2 Bump fastapi oauth2 from 0.0.1a2 to 0.0.1 (#570) 2025-04-09 11:49:04 +08:00
Wu Clan db81319dc4 Fix the failure hook of celery task (#569) 2025-04-08 12:09:29 +08:00
Wu Clan 644f7a0413 Fix the login password verification (#568)
* Fix the login password verification

* Update the check criteria
2025-04-07 11:04:39 +08:00
Wu Clan b93ff19ebe Update the number of pagination le (#565) 2025-04-03 19:32:26 +08:00
Wu Clan 4c007cb47d Update custom validation error messages (#566) 2025-04-03 19:32:11 +08:00
Wu Clan c9e9a45288 Update the vben5 tree data structure (#564) 2025-04-02 23:18:55 +08:00
Wu Clan d44a5491f5 Update menu and add vben5 compatibility (#563)
* Update menu and add vben5 compatibility

* Update sidebar api description
2025-04-02 22:20:19 +08:00
Wu Clan 289cf5b974 Update dict data and type to plugin (#562) 2025-04-02 14:04:06 +08:00
Wu Clan e207622a70 Update the system config to plugin (#561) 2025-04-02 13:43:24 +08:00
Wu Clan 38d9d9fda5 Add python 3.13 to GitHub ci (#560) 2025-03-28 14:22:45 +08:00
Wu Clan 996895e775 Bump dependencies and pre-commits (#559) 2025-03-28 14:22:20 +08:00
Wu Clan 0675e53f2a Update the changelog for v1.0.4 (#558) 2025-03-28 10:15:11 +08:00
155 changed files with 2563 additions and 2801 deletions
+2 -2
View File
@@ -12,13 +12,13 @@ jobs:
name: lint ${{ matrix.python-version }} name: lint ${{ matrix.python-version }}
strategy: strategy:
matrix: matrix:
python-version: [ '3.10', '3.11', '3.12' ] python-version: [ '3.10', '3.11', '3.12', '3.13' ]
fail-fast: false fail-fast: false
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Install uv - name: Install uv
uses: astral-sh/setup-uv@v3 uses: astral-sh/setup-uv@v5
- name: Set up Python ${{ matrix.python-version }} - name: Set up Python ${{ matrix.python-version }}
run: uv python install ${{ matrix.python-version }} run: uv python install ${{ matrix.python-version }}
+2 -2
View File
@@ -8,7 +8,7 @@ repos:
- id: check-toml - id: check-toml
- repo: https://github.com/charliermarsh/ruff-pre-commit - repo: https://github.com/charliermarsh/ruff-pre-commit
rev: v0.9.5 rev: v0.11.2
hooks: hooks:
- id: ruff - id: ruff
args: args:
@@ -20,7 +20,7 @@ repos:
- id: ruff-format - id: ruff-format
- repo: https://github.com/astral-sh/uv-pre-commit - repo: https://github.com/astral-sh/uv-pre-commit
rev: 0.5.29 rev: 0.6.10
hooks: hooks:
- id: uv-lock - id: uv-lock
- id: uv-export - id: uv-export
+107
View File
@@ -1,3 +1,105 @@
<a id="v1.1.2"></a>
# [v1.1.2](https://github.com/fastapi-practices/fastapi_best_architecture/releases/tag/v1.1.2) - 2025-04-23
## What's Changed
* Update the changelog for v1.1.1 by [@wu-clan](https://github.com/wu-clan) in [#583](https://github.com/fastapi-practices/fastapi_best_architecture/pull/583)
* Fix the condition to query menu by title by [@wu-clan](https://github.com/wu-clan) in [#584](https://github.com/fastapi-practices/fastapi_best_architecture/pull/584)
* Fix cache cleanup when updating role menu by [@wu-clan](https://github.com/wu-clan) in [#585](https://github.com/fastapi-practices/fastapi_best_architecture/pull/585)
* Optimize the userinfo cache cleaning logic by [@wu-clan](https://github.com/wu-clan) in [#586](https://github.com/fastapi-practices/fastapi_best_architecture/pull/586)
* Bump fastapi pagination from 0.12.34 to 0.13.0 by [@wu-clan](https://github.com/wu-clan) in [#587](https://github.com/fastapi-practices/fastapi_best_architecture/pull/587)
* Update the routing style of the task app by [@wu-clan](https://github.com/wu-clan) in [#588](https://github.com/fastapi-practices/fastapi_best_architecture/pull/588)
**Full Changelog**: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.1.1...v1.1.2
[Changes][v1.1.2]
<a id="v1.1.1"></a>
# [v1.1.1](https://github.com/fastapi-practices/fastapi_best_architecture/releases/tag/v1.1.1) - 2025-04-18
## What's Changed
* Update changelog for v1.1.0 by [@wu-clan](https://github.com/wu-clan) in [#580](https://github.com/fastapi-practices/fastapi_best_architecture/pull/580)
* Fix the plugin system route injection by [@wu-clan](https://github.com/wu-clan) in [#581](https://github.com/fastapi-practices/fastapi_best_architecture/pull/581)
* Fix list query in the dict plugin by [@wu-clan](https://github.com/wu-clan) in [#582](https://github.com/fastapi-practices/fastapi_best_architecture/pull/582)
**Full Changelog**: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.1.0...v1.1.1
[Changes][v1.1.1]
<a id="v1.1.0"></a>
# [v1.1.0](https://github.com/fastapi-practices/fastapi_best_architecture/releases/tag/v1.1.0) - 2025-04-17
## What's Changed
* Update the changelog for v1.0.5 by [@wu-clan](https://github.com/wu-clan) in [#572](https://github.com/fastapi-practices/fastapi_best_architecture/pull/572)
* Update the default value for some functions by [@wu-clan](https://github.com/wu-clan) in [#573](https://github.com/fastapi-practices/fastapi_best_architecture/pull/573)
* Optimize the file structure of code generator by [@wu-clan](https://github.com/wu-clan) in [#574](https://github.com/fastapi-practices/fastapi_best_architecture/pull/574)
* Update casbin RBAC verify to dynamic import by [@wu-clan](https://github.com/wu-clan) in [#576](https://github.com/fastapi-practices/fastapi_best_architecture/pull/576)
* Update unique columns in dict models by [@wu-clan](https://github.com/wu-clan) in [#577](https://github.com/fastapi-practices/fastapi_best_architecture/pull/577)
* Update the code generator to plugin by [@wu-clan](https://github.com/wu-clan) in [#578](https://github.com/fastapi-practices/fastapi_best_architecture/pull/578)
* Fix avatar url type of update avatar by [@huyuwei1996](https://github.com/huyuwei1996) in [#575](https://github.com/fastapi-practices/fastapi_best_architecture/pull/575)
* Update code generator file and table naming by [@wu-clan](https://github.com/wu-clan) in [#579](https://github.com/fastapi-practices/fastapi_best_architecture/pull/579)
**Full Changelog**: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.0.5...v1.1.0
[Changes][v1.1.0]
<a id="v1.0.5"></a>
# [v1.0.5](https://github.com/fastapi-practices/fastapi_best_architecture/releases/tag/v1.0.5) - 2025-04-09
## What's Changed
* Update the changelog for v1.0.4 by [@wu-clan](https://github.com/wu-clan) in [#558](https://github.com/fastapi-practices/fastapi_best_architecture/pull/558)
* Bump dependencies and pre-commits by [@wu-clan](https://github.com/wu-clan) in [#559](https://github.com/fastapi-practices/fastapi_best_architecture/pull/559)
* Add python 3.13 to GitHub ci by [@wu-clan](https://github.com/wu-clan) in [#560](https://github.com/fastapi-practices/fastapi_best_architecture/pull/560)
* Update the system config to plugin by [@wu-clan](https://github.com/wu-clan) in [#561](https://github.com/fastapi-practices/fastapi_best_architecture/pull/561)
* Update dict data and type to plugin by [@wu-clan](https://github.com/wu-clan) in [#562](https://github.com/fastapi-practices/fastapi_best_architecture/pull/562)
* Update menu and add vben5 compatibility by [@wu-clan](https://github.com/wu-clan) in [#563](https://github.com/fastapi-practices/fastapi_best_architecture/pull/563)
* Update the vben5 tree data structure by [@wu-clan](https://github.com/wu-clan) in [#564](https://github.com/fastapi-practices/fastapi_best_architecture/pull/564)
* Update custom validation error messages by [@wu-clan](https://github.com/wu-clan) in [#566](https://github.com/fastapi-practices/fastapi_best_architecture/pull/566)
* Update the number of pagination le by [@wu-clan](https://github.com/wu-clan) in [#565](https://github.com/fastapi-practices/fastapi_best_architecture/pull/565)
* Fix the login password verification by [@wu-clan](https://github.com/wu-clan) in [#568](https://github.com/fastapi-practices/fastapi_best_architecture/pull/568)
* Fix the failure hook of celery task by [@wu-clan](https://github.com/wu-clan) in [#569](https://github.com/fastapi-practices/fastapi_best_architecture/pull/569)
* Bump fastapi oauth2 from 0.0.1a2 to 0.0.1 by [@wu-clan](https://github.com/wu-clan) in [#570](https://github.com/fastapi-practices/fastapi_best_architecture/pull/570)
* Fix the log rule in gitignore by [@wu-clan](https://github.com/wu-clan) in [#571](https://github.com/fastapi-practices/fastapi_best_architecture/pull/571)
**Full Changelog**: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.0.4...v1.0.5
[Changes][v1.0.5]
<a id="v1.0.4"></a>
# [v1.0.4](https://github.com/fastapi-practices/fastapi_best_architecture/releases/tag/v1.0.4) - 2025-03-28
## What's Changed
* Update the changelog for v1.0.3 by [@wu-clan](https://github.com/wu-clan) in [#543](https://github.com/fastapi-practices/fastapi_best_architecture/pull/543)
* Updated the backend contribution guide by [@wu-clan](https://github.com/wu-clan) in [#544](https://github.com/fastapi-practices/fastapi_best_architecture/pull/544)
* Optimize the return of relationship interfaces by [@wu-clan](https://github.com/wu-clan) in [#545](https://github.com/fastapi-practices/fastapi_best_architecture/pull/545)
* Optimize the dynamic import of data models by [@wu-clan](https://github.com/wu-clan) in [#546](https://github.com/fastapi-practices/fastapi_best_architecture/pull/546)
* Update git and docker ignore files by [@wu-clan](https://github.com/wu-clan) in [#547](https://github.com/fastapi-practices/fastapi_best_architecture/pull/547)
* Optimize dependencies to reduce package size by [@wu-clan](https://github.com/wu-clan) in [#548](https://github.com/fastapi-practices/fastapi_best_architecture/pull/548)
* Fix async install plugin dependencies for windows by [@wu-clan](https://github.com/wu-clan) in [#549](https://github.com/fastapi-practices/fastapi_best_architecture/pull/549)
* Fix return schema of the config api by [@wu-clan](https://github.com/wu-clan) in [#551](https://github.com/fastapi-practices/fastapi_best_architecture/pull/551)
* Optimize schemas with model relationships by [@wu-clan](https://github.com/wu-clan) in [#552](https://github.com/fastapi-practices/fastapi_best_architecture/pull/552)
* Fix filters for opera log query list by [@ThankCat](https://github.com/ThankCat) in [#554](https://github.com/fastapi-practices/fastapi_best_architecture/pull/554)
* Fix the celery env in docker compose by [@wu-clan](https://github.com/wu-clan) in [#555](https://github.com/fastapi-practices/fastapi_best_architecture/pull/555)
* Update volumes of redis in docker compose by [@wu-clan](https://github.com/wu-clan) in [#556](https://github.com/fastapi-practices/fastapi_best_architecture/pull/556)
* Fix the query for the sub department by [@PoetryL](https://github.com/PoetryL) in [#557](https://github.com/fastapi-practices/fastapi_best_architecture/pull/557)
* Optimize codes and comments with cursor by [@wu-clan](https://github.com/wu-clan) in [#550](https://github.com/fastapi-practices/fastapi_best_architecture/pull/550)
## New Contributors
* [@ThankCat](https://github.com/ThankCat) made their first contribution in [#554](https://github.com/fastapi-practices/fastapi_best_architecture/pull/554)
* [@PoetryL](https://github.com/PoetryL) made their first contribution in [#557](https://github.com/fastapi-practices/fastapi_best_architecture/pull/557)
**Full Changelog**: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.0.3...v1.0.4
[Changes][v1.0.4]
<a id="v1.0.3"></a> <a id="v1.0.3"></a>
# [v1.0.3](https://github.com/fastapi-practices/fastapi_best_architecture/releases/tag/v1.0.3) - 2025-03-11 # [v1.0.3](https://github.com/fastapi-practices/fastapi_best_architecture/releases/tag/v1.0.3) - 2025-03-11
@@ -375,6 +477,11 @@
[Changes][v1.0.0] [Changes][v1.0.0]
[v1.1.2]: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.1.1...v1.1.2
[v1.1.1]: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.1.0...v1.1.1
[v1.1.0]: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.0.5...v1.1.0
[v1.0.5]: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.0.4...v1.0.5
[v1.0.4]: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.0.3...v1.0.4
[v1.0.3]: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.0.2...v1.0.3 [v1.0.3]: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.0.2...v1.0.3
[v1.0.2]: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.0.1...v1.0.2 [v1.0.2]: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.0.1...v1.0.2
[v1.0.1]: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.0.0...v1.0.1 [v1.0.1]: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.0.0...v1.0.1
+1 -1
View File
@@ -1,6 +1,6 @@
__pycache__/ __pycache__/
.env .env
log/
alembic/versions/ alembic/versions/
static/media/ static/media/
*.log
celerybeat-schedule.* celerybeat-schedule.*
+1 -1
View File
@@ -20,7 +20,7 @@ from backend.plugin.tools import get_plugin_models
# import your new model here # import your new model here
from backend.app.admin.model import * # noqa: F401 from backend.app.admin.model import * # noqa: F401
from backend.app.generator.model import * # noqa: F401 from backend.plugin.code_generator.model import * # noqa: F401
# import plugin model # import plugin model
for cls in get_plugin_models(): for cls in get_plugin_models():
+3 -3
View File
@@ -13,12 +13,12 @@ from backend.common.response.response_schema import ResponseSchemaModel, respons
router = APIRouter() router = APIRouter()
_github_client = GitHubOAuth20(admin_settings.OAUTH2_GITHUB_CLIENT_ID, admin_settings.OAUTH2_GITHUB_CLIENT_SECRET) _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) _github_oauth2 = FastAPIOAuth20(_github_client, redirect_route_name='github_login')
@router.get('', summary='获取 Github 授权链接') @router.get('', summary='获取 Github 授权链接')
async def github_auth2() -> ResponseSchemaModel[str]: async def github_oauth2(request: Request) -> ResponseSchemaModel[str]:
auth_url = await _github_client.get_authorization_url(redirect_uri=admin_settings.OAUTH2_GITHUB_REDIRECT_URI) auth_url = await _github_client.get_authorization_url(redirect_uri=f'{request.url}/callback')
return response_base.success(data=auth_url) return response_base.success(data=auth_url)
+3 -3
View File
@@ -16,12 +16,12 @@ _linux_do_client = LinuxDoOAuth20(
admin_settings.OAUTH2_LINUX_DO_CLIENT_ID, admin_settings.OAUTH2_LINUX_DO_CLIENT_ID,
admin_settings.OAUTH2_LINUX_DO_CLIENT_SECRET, admin_settings.OAUTH2_LINUX_DO_CLIENT_SECRET,
) )
_linux_do_oauth2 = FastAPIOAuth20(_linux_do_client, admin_settings.OAUTH2_LINUX_DO_REDIRECT_URI) _linux_do_oauth2 = FastAPIOAuth20(_linux_do_client, redirect_route_name='linux_do_login')
@router.get('', summary='获取 LinuxDo 授权链接') @router.get('', summary='获取 LinuxDo 授权链接')
async def linux_do_auth2() -> ResponseSchemaModel[str]: async def linux_do_oauth2(request: Request) -> ResponseSchemaModel[str]:
auth_url = await _linux_do_client.get_authorization_url(redirect_uri=admin_settings.OAUTH2_LINUX_DO_REDIRECT_URI) auth_url = await _linux_do_client.get_authorization_url(redirect_uri=f'{request.url}/callback')
return response_base.success(data=auth_url) return response_base.success(data=auth_url)
+3 -7
View File
@@ -2,11 +2,9 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from fastapi import APIRouter from fastapi import APIRouter
from backend.app.admin.api.v1.sys.config import router as config_router
from backend.app.admin.api.v1.sys.data_rule import router as data_rule_router from backend.app.admin.api.v1.sys.data_rule import router as data_rule_router
from backend.app.admin.api.v1.sys.data_scope import router as data_scope_router
from backend.app.admin.api.v1.sys.dept import router as dept_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.menu import router as menu_router
from backend.app.admin.api.v1.sys.plugin import router as plugin_router from backend.app.admin.api.v1.sys.plugin import router as plugin_router
from backend.app.admin.api.v1.sys.role import router as role_router from backend.app.admin.api.v1.sys.role import router as role_router
@@ -16,14 +14,12 @@ from backend.app.admin.api.v1.sys.user import router as user_router
router = APIRouter(prefix='/sys') router = APIRouter(prefix='/sys')
router.include_router(config_router, prefix='/configs', tags=['系统参数配置'])
router.include_router(dept_router, prefix='/depts', tags=['系统部门']) router.include_router(dept_router, prefix='/depts', tags=['系统部门'])
router.include_router(dict_data_router, prefix='/dict-datas', tags=['系统字典数据'])
router.include_router(dict_type_router, prefix='/dict-types', tags=['系统字典类型'])
router.include_router(menu_router, prefix='/menus', tags=['系统菜单']) router.include_router(menu_router, prefix='/menus', tags=['系统菜单'])
router.include_router(role_router, prefix='/roles', tags=['系统角色']) router.include_router(role_router, prefix='/roles', tags=['系统角色'])
router.include_router(user_router, prefix='/users', tags=['系统用户']) router.include_router(user_router, prefix='/users', tags=['系统用户'])
router.include_router(data_rule_router, prefix='/data-rules', tags=['系统数据权限规则']) router.include_router(data_rule_router, prefix='/data-rules', tags=['系统数据规则'])
router.include_router(data_scope_router, prefix='/data-scopes', tags=['系统数据范围'])
router.include_router(token_router, prefix='/tokens', tags=['系统令牌']) router.include_router(token_router, prefix='/tokens', tags=['系统令牌'])
router.include_router(upload_router, prefix='/upload', tags=['系统上传']) router.include_router(upload_router, prefix='/upload', tags=['系统上传'])
router.include_router(plugin_router, prefix='/plugin', tags=['系统插件']) router.include_router(plugin_router, prefix='/plugin', tags=['系统插件'])
+17 -14
View File
@@ -2,9 +2,14 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from typing import Annotated from typing import Annotated
from fastapi import APIRouter, Depends, Path, Query, Request from fastapi import APIRouter, Depends, Path, Query
from backend.app.admin.schema.data_rule import CreateDataRuleParam, GetDataRuleDetail, UpdateDataRuleParam from backend.app.admin.schema.data_rule import (
CreateDataRuleParam,
GetDataRuleColumnDetail,
GetDataRuleDetail,
UpdateDataRuleParam,
)
from backend.app.admin.service.data_rule_service import data_rule_service from backend.app.admin.service.data_rule_service import data_rule_service
from backend.common.pagination import DependsPagination, PageData, paging_data from backend.common.pagination import DependsPagination, PageData, paging_data
from backend.common.response.response_schema import ResponseModel, ResponseSchemaModel, response_base from backend.common.response.response_schema import ResponseModel, ResponseSchemaModel, response_base
@@ -16,16 +21,16 @@ from backend.database.db import CurrentSession
router = APIRouter() router = APIRouter()
@router.get('/models', summary='获取支持过滤的数据库模型', dependencies=[DependsJwtAuth]) @router.get('/models', summary='获取数据规则可用模型', dependencies=[DependsJwtAuth])
async def get_data_rule_models() -> ResponseSchemaModel[list[str]]: async def get_data_rule_models() -> ResponseSchemaModel[list[str]]:
models = await data_rule_service.get_models() models = await data_rule_service.get_models()
return response_base.success(data=models) return response_base.success(data=models)
@router.get('/model/{model}/columns', summary='获取支持过滤的数据库模型列', dependencies=[DependsJwtAuth]) @router.get('/model/{model}/columns', summary='获取数据规则可用模型列', dependencies=[DependsJwtAuth])
async def get_data_rule_model_columns( async def get_data_rule_model_columns(
model: Annotated[str, Path(description='模型名称')], model: Annotated[str, Path(description='模型名称')],
) -> ResponseSchemaModel[list[str]]: ) -> ResponseSchemaModel[list[GetDataRuleColumnDetail]]:
models = await data_rule_service.get_columns(model=model) models = await data_rule_service.get_columns(model=model)
return response_base.success(data=models) return response_base.success(data=models)
@@ -36,7 +41,7 @@ async def get_all_data_rules() -> ResponseSchemaModel[list[GetDataRuleDetail]]:
return response_base.success(data=data) return response_base.success(data=data)
@router.get('/{pk}', summary='获取数据权限规则详情', dependencies=[DependsJwtAuth]) @router.get('/{pk}', summary='获取数据规则详情', dependencies=[DependsJwtAuth])
async def get_data_rule( async def get_data_rule(
pk: Annotated[int, Path(description='数据规则 ID')], pk: Annotated[int, Path(description='数据规则 ID')],
) -> ResponseSchemaModel[GetDataRuleDetail]: ) -> ResponseSchemaModel[GetDataRuleDetail]:
@@ -46,7 +51,7 @@ async def get_data_rule(
@router.get( @router.get(
'', '',
summary='分页获取所有数据权限规则', summary='分页获取所有数据规则',
dependencies=[ dependencies=[
DependsJwtAuth, DependsJwtAuth,
DependsPagination, DependsPagination,
@@ -62,7 +67,7 @@ async def get_pagination_data_rules(
@router.post( @router.post(
'', '',
summary='创建数据权限规则', summary='创建数据规则',
dependencies=[ dependencies=[
Depends(RequestPermission('data:rule:add')), Depends(RequestPermission('data:rule:add')),
DependsRBAC, DependsRBAC,
@@ -75,7 +80,7 @@ async def create_data_rule(obj: CreateDataRuleParam) -> ResponseModel:
@router.put( @router.put(
'/{pk}', '/{pk}',
summary='更新数据权限规则', summary='更新数据规则',
dependencies=[ dependencies=[
Depends(RequestPermission('data:rule:edit')), Depends(RequestPermission('data:rule:edit')),
DependsRBAC, DependsRBAC,
@@ -92,16 +97,14 @@ async def update_data_rule(
@router.delete( @router.delete(
'', '',
summary='批量删除数据权限规则', summary='批量删除数据规则',
dependencies=[ dependencies=[
Depends(RequestPermission('data:rule:del')), Depends(RequestPermission('data:rule:del')),
DependsRBAC, DependsRBAC,
], ],
) )
async def delete_data_rule( async def delete_data_rule(pk: Annotated[list[int], Query(description='数据规则 ID 列表')]) -> ResponseModel:
request: Request, pk: Annotated[list[int], Query(description='数据规则 ID 列表')] count = await data_rule_service.delete(pk=pk)
) -> ResponseModel:
count = await data_rule_service.delete(request=request, pk=pk)
if count > 0: if count > 0:
return response_base.success() return response_base.success()
return response_base.fail() return response_base.fail()
+118
View File
@@ -0,0 +1,118 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from typing import Annotated
from fastapi import APIRouter, Depends, Path, Query
from backend.app.admin.schema.data_scope import (
CreateDataScopeParam,
GetDataScopeDetail,
GetDataScopeWithRelationDetail,
UpdateDataScopeParam,
UpdateDataScopeRuleParam,
)
from backend.app.admin.service.data_scope_service import data_scope_service
from backend.common.pagination import DependsPagination, PageData, paging_data
from backend.common.response.response_schema import ResponseModel, ResponseSchemaModel, 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 import CurrentSession
router = APIRouter()
@router.get('/{pk}', summary='获取数据范围详情', dependencies=[DependsJwtAuth])
async def get_data_scope(
pk: Annotated[int, Path(description='数据范围 ID')],
) -> ResponseSchemaModel[GetDataScopeDetail]:
data = await data_scope_service.get(pk=pk)
return response_base.success(data=data)
@router.get('/{pk}/rules', summary='获取数据范围所有规则', dependencies=[DependsJwtAuth])
async def get_data_scope_rules(
pk: Annotated[int, Path(description='数据范围 ID')],
) -> ResponseSchemaModel[GetDataScopeWithRelationDetail]:
data = await data_scope_service.get_rules(pk=pk)
return response_base.success(data=data)
@router.get(
'',
summary='分页获取所有数据范围',
dependencies=[
DependsJwtAuth,
DependsPagination,
],
)
async def get_pagination_data_scopes(
db: CurrentSession,
name: Annotated[str | None, Query(description='范围名称')] = None,
status: Annotated[int | None, Query(description='状态')] = None,
) -> ResponseSchemaModel[PageData[GetDataScopeDetail]]:
data_scope_select = await data_scope_service.get_select(name=name, status=status)
page_data = await paging_data(db, data_scope_select)
return response_base.success(data=page_data)
@router.post(
'',
summary='创建数据范围',
dependencies=[
Depends(RequestPermission('data:scope:add')),
DependsRBAC,
],
)
async def create_data_scope(obj: CreateDataScopeParam) -> ResponseModel:
await data_scope_service.create(obj=obj)
return response_base.success()
@router.put(
'/{pk}',
summary='更新数据范围',
dependencies=[
Depends(RequestPermission('data:scope:edit')),
DependsRBAC,
],
)
async def update_data_scope(
pk: Annotated[int, Path(description='数据范围 ID')], obj: UpdateDataScopeParam
) -> ResponseModel:
count = await data_scope_service.update(pk=pk, obj=obj)
if count > 0:
return response_base.success()
return response_base.fail()
@router.put(
'/{pk}/rules',
summary='更新数据范围规则',
dependencies=[
Depends(RequestPermission('data:scope:rule:edit')),
DependsRBAC,
],
)
async def update_data_scope_rules(
pk: Annotated[int, Path(description='数据范围 ID')], rule_ids: UpdateDataScopeRuleParam
):
count = await data_scope_service.update_data_scope_rule(pk=pk, rule_ids=rule_ids)
if count > 0:
return response_base.success()
return response_base.fail()
@router.delete(
'',
summary='批量删除数据范围',
dependencies=[
Depends(RequestPermission('data:scope:del')),
DependsRBAC,
],
)
async def delete_data_scope(pk: Annotated[list[int], Query(description='数据范围 ID 列表')]) -> ResponseModel:
count = await data_scope_service.delete(pk=pk)
if count > 0:
return response_base.success()
return response_base.fail()
+4 -3
View File
@@ -22,12 +22,13 @@ async def get_dept(pk: Annotated[int, Path(description='部门 ID')]) -> Respons
@router.get('', summary='获取所有部门展示树', dependencies=[DependsJwtAuth]) @router.get('', summary='获取所有部门展示树', dependencies=[DependsJwtAuth])
async def get_all_depts( async def get_all_depts(
request: Request,
name: Annotated[str | None, Query(description='部门名称')] = None, name: Annotated[str | None, Query(description='部门名称')] = None,
leader: Annotated[str | None, Query(description='部门负责人')] = None, leader: Annotated[str | None, Query(description='部门负责人')] = None,
phone: Annotated[str | None, Query(description='联系电话')] = None, phone: Annotated[str | None, Query(description='联系电话')] = None,
status: Annotated[int | None, Query(description='状态')] = None, status: Annotated[int | None, Query(description='状态')] = None,
) -> ResponseSchemaModel[list[dict[str, Any]]]: ) -> ResponseSchemaModel[list[dict[str, Any]]]:
dept = await dept_service.get_dept_tree(name=name, leader=leader, phone=phone, status=status) dept = await dept_service.get_dept_tree(request=request, name=name, leader=leader, phone=phone, status=status)
return response_base.success(data=dept) return response_base.success(data=dept)
@@ -67,8 +68,8 @@ async def update_dept(pk: Annotated[int, Path(description='部门 ID')], obj: Up
DependsRBAC, DependsRBAC,
], ],
) )
async def delete_dept(request: Request, pk: Annotated[int, Path(description='部门 ID')]) -> ResponseModel: async def delete_dept(pk: Annotated[int, Path(description='部门 ID')]) -> ResponseModel:
count = await dept_service.delete(request=request, pk=pk) count = await dept_service.delete(pk=pk)
if count > 0: if count > 0:
return response_base.success() return response_base.success()
return response_base.fail() return response_base.fail()
+3 -3
View File
@@ -14,7 +14,7 @@ from backend.common.security.rbac import DependsRBAC
router = APIRouter() router = APIRouter()
@router.get('/sidebar', summary='获取用户侧边栏', dependencies=[DependsJwtAuth]) @router.get('/sidebar', summary='获取用户菜单侧边栏', description='适配 vben5', dependencies=[DependsJwtAuth])
async def get_user_sidebar(request: Request) -> ResponseSchemaModel[list[dict[str, Any]]]: async def get_user_sidebar(request: Request) -> ResponseSchemaModel[list[dict[str, Any]]]:
menu = await menu_service.get_user_menu_tree(request=request) menu = await menu_service.get_user_menu_tree(request=request)
return response_base.success(data=menu) return response_base.success(data=menu)
@@ -71,8 +71,8 @@ async def update_menu(pk: Annotated[int, Path(description='菜单 ID')], obj: Up
DependsRBAC, DependsRBAC,
], ],
) )
async def delete_menu(request: Request, pk: Annotated[int, Path(description='菜单 ID 列表')]) -> ResponseModel: async def delete_menu(pk: Annotated[int, Path(description='菜单 ID 列表')]) -> ResponseModel:
count = await menu_service.delete(request=request, pk=pk) count = await menu_service.delete(pk=pk)
if count > 0: if count > 0:
return response_base.success() return response_base.success()
return response_base.fail() return response_base.fail()
+17 -19
View File
@@ -2,7 +2,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from typing import Annotated, Any from typing import Annotated, Any
from fastapi import APIRouter, Depends, Path, Query, Request from fastapi import APIRouter, Depends, Path, Query
from backend.app.admin.schema.role import ( from backend.app.admin.schema.role import (
CreateRoleParam, CreateRoleParam,
@@ -10,10 +10,8 @@ from backend.app.admin.schema.role import (
GetRoleWithRelationDetail, GetRoleWithRelationDetail,
UpdateRoleMenuParam, UpdateRoleMenuParam,
UpdateRoleParam, UpdateRoleParam,
UpdateRoleRuleParam, UpdateRoleScopeParam,
) )
from backend.app.admin.service.data_rule_service import data_rule_service
from backend.app.admin.service.menu_service import menu_service
from backend.app.admin.service.role_service import role_service from backend.app.admin.service.role_service import role_service
from backend.common.pagination import DependsPagination, PageData, paging_data from backend.common.pagination import DependsPagination, PageData, paging_data
from backend.common.response.response_schema import ResponseModel, ResponseSchemaModel, response_base from backend.common.response.response_schema import ResponseModel, ResponseSchemaModel, response_base
@@ -35,7 +33,7 @@ async def get_all_roles() -> ResponseSchemaModel[list[GetRoleDetail]]:
async def get_user_all_roles( async def get_user_all_roles(
pk: Annotated[int, Path(description='用户 ID')], pk: Annotated[int, Path(description='用户 ID')],
) -> ResponseSchemaModel[list[GetRoleDetail]]: ) -> ResponseSchemaModel[list[GetRoleDetail]]:
data = await role_service.get_by_user(pk=pk) data = await role_service.get_users(pk=pk)
return response_base.success(data=data) return response_base.success(data=data)
@@ -43,13 +41,13 @@ async def get_user_all_roles(
async def get_role_all_menus( async def get_role_all_menus(
pk: Annotated[int, Path(description='角色 ID')], pk: Annotated[int, Path(description='角色 ID')],
) -> ResponseSchemaModel[list[dict[str, Any]]]: ) -> ResponseSchemaModel[list[dict[str, Any]]]:
menu = await menu_service.get_role_menu_tree(pk=pk) menu = await role_service.get_menu_tree(pk=pk)
return response_base.success(data=menu) return response_base.success(data=menu)
@router.get('/{pk}/rules', summary='获取角色所有数据规则', dependencies=[DependsJwtAuth]) @router.get('/{pk}/scopes', summary='获取角色所有数据范围', dependencies=[DependsJwtAuth])
async def get_role_all_rules(pk: Annotated[int, Path(description='角色 ID')]) -> ResponseSchemaModel[list[int]]: async def get_role_all_scopes(pk: Annotated[int, Path(description='角色 ID')]) -> ResponseSchemaModel[list[int]]:
rule = await data_rule_service.get_role_rules(pk=pk) rule = await role_service.get_scopes(pk=pk)
return response_base.success(data=rule) return response_base.success(data=rule)
@@ -116,26 +114,26 @@ async def update_role(pk: Annotated[int, Path(description='角色 ID')], obj: Up
], ],
) )
async def update_role_menus( async def update_role_menus(
request: Request, pk: Annotated[int, Path(description='角色 ID')], menu_ids: UpdateRoleMenuParam pk: Annotated[int, Path(description='角色 ID')], menu_ids: UpdateRoleMenuParam
) -> ResponseModel: ) -> ResponseModel:
count = await role_service.update_role_menu(request=request, pk=pk, menu_ids=menu_ids) count = await role_service.update_role_menu(pk=pk, menu_ids=menu_ids)
if count > 0: if count > 0:
return response_base.success() return response_base.success()
return response_base.fail() return response_base.fail()
@router.put( @router.put(
'/{pk}/rule', '/{pk}/scope',
summary='更新角色数据权限规则', summary='更新角色数据范围',
dependencies=[ dependencies=[
Depends(RequestPermission('sys:role:rule:edit')), Depends(RequestPermission('sys:role:scope:edit')),
DependsRBAC, DependsRBAC,
], ],
) )
async def update_role_rules( async def update_role_scopes(
request: Request, pk: Annotated[int, Path(description='角色 ID')], rule_ids: UpdateRoleRuleParam pk: Annotated[int, Path(description='角色 ID')], scope_ids: UpdateRoleScopeParam
) -> ResponseModel: ) -> ResponseModel:
count = await role_service.update_role_rule(request=request, pk=pk, rule_ids=rule_ids) count = await role_service.update_role_scope(pk=pk, scope_ids=scope_ids)
if count > 0: if count > 0:
return response_base.success() return response_base.success()
return response_base.fail() return response_base.fail()
@@ -149,8 +147,8 @@ async def update_role_rules(
DependsRBAC, DependsRBAC,
], ],
) )
async def delete_role(request: Request, pk: Annotated[list[int], Query(description='角色 ID 列表')]) -> ResponseModel: async def delete_role(pk: Annotated[list[int], Query(description='角色 ID 列表')]) -> ResponseModel:
count = await role_service.delete(request=request, pk=pk) count = await role_service.delete(pk=pk)
if count > 0: if count > 0:
return response_base.success() return response_base.success()
return response_base.fail() return response_base.fail()
+2 -1
View File
@@ -61,7 +61,8 @@ async def get_tokens(
extra_info = await redis_client.get(f'{settings.TOKEN_EXTRA_INFO_REDIS_PREFIX}:{session_uuid}') extra_info = await redis_client.get(f'{settings.TOKEN_EXTRA_INFO_REDIS_PREFIX}:{session_uuid}')
if extra_info: if extra_info:
extra_info = json.loads(extra_info) extra_info = json.loads(extra_info)
if extra_info.get('login_type') != 'swagger': # 排除 swagger 登录生成的 token
if extra_info.get('swagger') is None:
if username is not None: if username is not None:
if username == extra_info.get('username'): if username == extra_info.get('username'):
append_token_detail() append_token_detail()
-1
View File
@@ -1,6 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from typing import Annotated from typing import Annotated
from fastapi import APIRouter, File, UploadFile from fastapi import APIRouter, File, UploadFile
+1 -6
View File
@@ -19,21 +19,16 @@ class AdminSettings(BaseSettings):
OAUTH2_LINUX_DO_CLIENT_SECRET: str OAUTH2_LINUX_DO_CLIENT_SECRET: str
# OAuth2 # OAuth2
OAUTH2_GITHUB_REDIRECT_URI: str = 'http://127.0.0.1:8000/api/v1/oauth2/github/callback'
OAUTH2_LINUX_DO_REDIRECT_URI: str = 'http://127.0.0.1:8000/api/v1/oauth2/linux-do/callback'
OAUTH2_FRONTEND_REDIRECT_URI: str = 'http://localhost:5173/oauth2/callback' OAUTH2_FRONTEND_REDIRECT_URI: str = 'http://localhost:5173/oauth2/callback'
# 验证码 # 验证码
CAPTCHA_LOGIN_REDIS_PREFIX: str = 'fba:login:captcha' CAPTCHA_LOGIN_REDIS_PREFIX: str = 'fba:login:captcha'
CAPTCHA_LOGIN_EXPIRE_SECONDS: int = 60 * 5 # 3 分钟 CAPTCHA_LOGIN_EXPIRE_SECONDS: int = 60 * 5 # 3 分钟
# 参数配置
CONFIG_BUILT_IN_TYPES: list[str] = ['website', 'protocol', 'policy']
@lru_cache @lru_cache
def get_admin_settings() -> AdminSettings: def get_admin_settings() -> AdminSettings:
"""获取 admin 参数配置""" """获取 admin 配置"""
return AdminSettings() return AdminSettings()
+3 -3
View File
@@ -12,7 +12,7 @@ from backend.app.admin.schema.data_rule import CreateDataRuleParam, UpdateDataRu
class CRUDDataRule(CRUDPlus[DataRule]): class CRUDDataRule(CRUDPlus[DataRule]):
"""数据权限规则数据库操作类""" """数据规则数据库操作类"""
async def get(self, db: AsyncSession, pk: int) -> DataRule | None: async def get(self, db: AsyncSession, pk: int) -> DataRule | None:
""" """
@@ -24,14 +24,14 @@ class CRUDDataRule(CRUDPlus[DataRule]):
""" """
return await self.select_model(db, pk) return await self.select_model(db, pk)
async def get_list(self, name: str | None = None) -> Select: async def get_list(self, name: str | None) -> Select:
""" """
获取规则列表 获取规则列表
:param name: 规则名称 :param name: 规则名称
:return: :return:
""" """
stmt = select(self.model).options(noload(self.model.roles)).order_by(desc(self.model.created_time)) stmt = select(self.model).options(noload(self.model.scope)).order_by(desc(self.model.created_time))
filters = [] filters = []
if name is not None: if name is not None:
+119
View File
@@ -0,0 +1,119 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from sqlalchemy import Select, and_, desc, select
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import noload, selectinload
from sqlalchemy_crud_plus import CRUDPlus
from backend.app.admin.model import DataRule, DataScope
from backend.app.admin.schema.data_scope import CreateDataScopeParam, UpdateDataScopeParam, UpdateDataScopeRuleParam
class CRUDDataScope(CRUDPlus[DataScope]):
"""数据范围数据库操作类"""
async def get(self, db: AsyncSession, pk: int) -> DataScope | None:
"""
获取数据范围详情
:param db: 数据库会话
:param pk: 范围 ID
:return:
"""
return await self.select_model(db, pk)
async def get_by_name(self, db: AsyncSession, name: str) -> DataScope | None:
"""
通过名称获取数据范围
:param db: 数据库会话
:param name: 范围名称
:return:
"""
return await self.select_model_by_column(db, name=name)
async def get_with_relation(self, db: AsyncSession, pk: int) -> DataScope:
"""
获取数据范围关联数据
:param db: 数据库会话
:param pk: 范围 ID
:return:
"""
stmt = select(self.model).options(selectinload(self.model.rules)).where(self.model.id == pk)
data_scope = await db.execute(stmt)
return data_scope.scalars().first()
async def get_list(self, name: str | None, status: int | None) -> Select:
"""
获取数据范围列表
:param name: 范围名称
:param status: 范围状态
:return:
"""
stmt = (
select(self.model)
.options(noload(self.model.rules), noload(self.model.roles))
.order_by(desc(self.model.created_time))
)
filters = []
if name is not None:
filters.append(self.model.name.like(f'%{name}%'))
if status is not None:
filters.append(self.model.status == status)
if filters:
stmt = stmt.where(and_(*filters))
return stmt
async def create(self, db: AsyncSession, obj: CreateDataScopeParam) -> None:
"""
创建数据范围
:param db: 数据库会话
:param obj: 创建数据范围参数
:return:
"""
await self.create_model(db, obj)
async def update(self, db: AsyncSession, pk: int, obj: UpdateDataScopeParam) -> int:
"""
更新数据范围
:param db: 数据库会话
:param pk: 范围 ID
:param obj: 更新数据范围参数
:return:
"""
return await self.update_model(db, pk, obj)
async def update_rules(self, db: AsyncSession, pk: int, rule_ids: UpdateDataScopeRuleParam) -> int:
"""
更新数据范围规则
:param db: 数据库会话
:param pk: 范围 ID
:param rule_ids: 数据规则 ID 列表
:return:
"""
current_data_scope = await self.get_with_relation(db, pk)
stmt = select(DataRule).where(DataRule.id.in_(rule_ids.rules))
rules = await db.execute(stmt)
current_data_scope.rules = rules.scalars().all()
return len(current_data_scope.rules)
async def delete(self, db: AsyncSession, pk: list[int]) -> int:
"""
删除数据范围
:param db: 数据库会话
:param pk: 范围 ID 列表
:return:
"""
return await self.delete_model_by_column(db, allow_multiple=True, id__in=pk)
data_scope_dao: CRUDDataScope = CRUDDataScope(DataScope)
+9 -5
View File
@@ -2,6 +2,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from typing import Sequence from typing import Sequence
from fastapi import Request
from sqlalchemy import select from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload from sqlalchemy.orm import selectinload
@@ -9,6 +10,7 @@ from sqlalchemy_crud_plus import CRUDPlus
from backend.app.admin.model import Dept from backend.app.admin.model import Dept
from backend.app.admin.schema.dept import CreateDeptParam, UpdateDeptParam from backend.app.admin.schema.dept import CreateDeptParam, UpdateDeptParam
from backend.common.security.permission import filter_data_permission
class CRUDDept(CRUDPlus[Dept]): class CRUDDept(CRUDPlus[Dept]):
@@ -36,15 +38,17 @@ class CRUDDept(CRUDPlus[Dept]):
async def get_all( async def get_all(
self, self,
request: Request,
db: AsyncSession, db: AsyncSession,
name: str | None = None, name: str | None,
leader: str | None = None, leader: str | None,
phone: str | None = None, phone: str | None,
status: int | None = None, status: int | None,
) -> Sequence[Dept]: ) -> Sequence[Dept]:
""" """
获取所有部门 获取所有部门
:param request: FastAPI 请求对象
:param db: 数据库会话 :param db: 数据库会话
:param name: 部门名称 :param name: 部门名称
:param leader: 负责人 :param leader: 负责人
@@ -61,7 +65,7 @@ class CRUDDept(CRUDPlus[Dept]):
filters.update(phone__startswith=phone) filters.update(phone__startswith=phone)
if status is not None: if status is not None:
filters.update(status=status) filters.update(status=status)
return await self.select_models_order(db, sort_columns='sort', **filters) return await self.select_models_order(db, 'sort', None, await filter_data_permission(db, request), **filters)
async def create(self, db: AsyncSession, obj: CreateDeptParam) -> None: async def create(self, db: AsyncSession, obj: CreateDeptParam) -> None:
""" """
+1 -1
View File
@@ -11,7 +11,7 @@ from backend.app.admin.schema.login_log import CreateLoginLogParam
class CRUDLoginLog(CRUDPlus[LoginLog]): class CRUDLoginLog(CRUDPlus[LoginLog]):
"""登录日志数据库操作类""" """登录日志数据库操作类"""
async def get_list(self, username: str | None = None, status: int | None = None, ip: str | None = None) -> Select: async def get_list(self, username: str | None, status: int | None, ip: str | None) -> Select:
""" """
获取登录日志列表 获取登录日志列表
+3 -3
View File
@@ -32,9 +32,9 @@ class CRUDMenu(CRUDPlus[Menu]):
:param title: 菜单标题 :param title: 菜单标题
:return: :return:
""" """
return await self.select_model_by_column(db, title=title, menu_type__ne=2) return await self.select_model_by_column(db, title=title, type__ne=2)
async def get_all(self, db: AsyncSession, title: str | None = None, status: int | None = None) -> Sequence[Menu]: async def get_all(self, db: AsyncSession, title: str | None, status: int | None) -> Sequence[Menu]:
""" """
获取菜单列表 获取菜单列表
@@ -60,7 +60,7 @@ class CRUDMenu(CRUDPlus[Menu]):
:return: :return:
""" """
stmt = select(self.model).order_by(asc(self.model.sort)) stmt = select(self.model).order_by(asc(self.model.sort))
filters = [self.model.menu_type.in_([0, 1])] filters = [self.model.type.in_([0, 1])]
if not superuser: if not superuser:
filters.append(self.model.id.in_(menu_ids)) filters.append(self.model.id.in_(menu_ids))
stmt = stmt.where(and_(*filters)) stmt = stmt.where(and_(*filters))
+1 -1
View File
@@ -11,7 +11,7 @@ from backend.app.admin.schema.opera_log import CreateOperaLogParam
class CRUDOperaLogDao(CRUDPlus[OperaLog]): class CRUDOperaLogDao(CRUDPlus[OperaLog]):
"""操作日志数据库操作类""" """操作日志数据库操作类"""
async def get_list(self, username: str | None = None, status: int | None = None, ip: str | None = None) -> Select: async def get_list(self, username: str | None, status: int | None, ip: str | None) -> Select:
""" """
获取操作日志列表 获取操作日志列表
+13 -13
View File
@@ -7,12 +7,12 @@ from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import noload, selectinload from sqlalchemy.orm import noload, selectinload
from sqlalchemy_crud_plus import CRUDPlus from sqlalchemy_crud_plus import CRUDPlus
from backend.app.admin.model import DataRule, Menu, Role, User from backend.app.admin.model import DataScope, Menu, Role, User
from backend.app.admin.schema.role import ( from backend.app.admin.schema.role import (
CreateRoleParam, CreateRoleParam,
UpdateRoleMenuParam, UpdateRoleMenuParam,
UpdateRoleParam, UpdateRoleParam,
UpdateRoleRuleParam, UpdateRoleScopeParam,
) )
@@ -39,7 +39,7 @@ class CRUDRole(CRUDPlus[Role]):
""" """
stmt = ( stmt = (
select(self.model) select(self.model)
.options(selectinload(self.model.menus), selectinload(self.model.rules)) .options(selectinload(self.model.menus), selectinload(self.model.scopes))
.where(self.model.id == role_id) .where(self.model.id == role_id)
) )
role = await db.execute(stmt) role = await db.execute(stmt)
@@ -54,7 +54,7 @@ class CRUDRole(CRUDPlus[Role]):
""" """
return await self.select_models(db) return await self.select_models(db)
async def get_by_user(self, db: AsyncSession, user_id: int) -> Sequence[Role]: async def get_users(self, db: AsyncSession, user_id: int) -> Sequence[Role]:
""" """
获取用户角色列表 获取用户角色列表
@@ -66,7 +66,7 @@ class CRUDRole(CRUDPlus[Role]):
roles = await db.execute(stmt) roles = await db.execute(stmt)
return roles.scalars().all() return roles.scalars().all()
async def get_list(self, name: str | None = None, status: int | None = None) -> Select: async def get_list(self, name: str | None, status: int | None) -> Select:
""" """
获取角色列表 获取角色列表
@@ -76,7 +76,7 @@ class CRUDRole(CRUDPlus[Role]):
""" """
stmt = ( stmt = (
select(self.model) select(self.model)
.options(noload(self.model.users), noload(self.model.menus), noload(self.model.rules)) .options(noload(self.model.users), noload(self.model.menus), noload(self.model.scopes))
.order_by(desc(self.model.created_time)) .order_by(desc(self.model.created_time))
) )
@@ -137,20 +137,20 @@ class CRUDRole(CRUDPlus[Role]):
current_role.menus = menus.scalars().all() current_role.menus = menus.scalars().all()
return len(current_role.menus) return len(current_role.menus)
async def update_rules(self, db: AsyncSession, role_id: int, rule_ids: UpdateRoleRuleParam) -> int: async def update_scopes(self, db: AsyncSession, role_id: int, scope_ids: UpdateRoleScopeParam) -> int:
""" """
更新角色数据规则 更新角色数据范围
:param db: 数据库会话 :param db: 数据库会话
:param role_id: 角色 ID :param role_id: 角色 ID
:param rule_ids: 权限规则 ID 列表 :param scope_ids: 权限范围 ID 列表
:return: :return:
""" """
current_role = await self.get_with_relation(db, role_id) current_role = await self.get_with_relation(db, role_id)
stmt = select(DataRule).where(DataRule.id.in_(rule_ids.rules)) stmt = select(DataScope).where(DataScope.id.in_(scope_ids.scopes))
rules = await db.execute(stmt) scopes = await db.execute(stmt)
current_role.rules = rules.scalars().all() current_role.scopes = scopes.scalars().all()
return len(current_role.rules) return len(current_role.scopes)
async def delete(self, db: AsyncSession, role_id: list[int]) -> int: async def delete(self, db: AsyncSession, role_id: list[int]) -> int:
""" """
+3 -5
View File
@@ -142,7 +142,7 @@ class CRUDUser(CRUDPlus[User]):
:param avatar: 头像地址 :param avatar: 头像地址
:return: :return:
""" """
return await self.update_model(db, input_user, {'avatar': avatar.url}) return await self.update_model(db, input_user, {'avatar': str(avatar.url)})
async def delete(self, db: AsyncSession, user_id: int) -> int: async def delete(self, db: AsyncSession, user_id: int) -> int:
""" """
@@ -175,9 +175,7 @@ class CRUDUser(CRUDPlus[User]):
""" """
return await self.update_model(db, pk, {'password': new_pwd}) return await self.update_model(db, pk, {'password': new_pwd})
async def get_list( async def get_list(self, dept: int | None, username: str | None, phone: str | None, status: int | None) -> Select:
self, dept: int | None = None, username: str | None = None, phone: str | None = None, status: int | None = None
) -> Select:
""" """
获取用户列表 获取用户列表
@@ -313,7 +311,7 @@ class CRUDUser(CRUDPlus[User]):
""" """
stmt = select(self.model).options( stmt = select(self.model).options(
selectinload(self.model.dept), selectinload(self.model.dept),
selectinload(self.model.roles).options(selectinload(Role.menus), selectinload(Role.rules)), selectinload(self.model.roles).options(selectinload(Role.menus), selectinload(Role.scopes)),
) )
filters = [] filters = []
+1 -3
View File
@@ -1,10 +1,8 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from backend.app.admin.model.config import Config
from backend.app.admin.model.data_rule import DataRule from backend.app.admin.model.data_rule import DataRule
from backend.app.admin.model.data_scope import DataScope
from backend.app.admin.model.dept import Dept from backend.app.admin.model.dept import Dept
from backend.app.admin.model.dict_data import DictData
from backend.app.admin.model.dict_type import DictType
from backend.app.admin.model.login_log import LoginLog from backend.app.admin.model.login_log import LoginLog
from backend.app.admin.model.menu import Menu from backend.app.admin.model.menu import Menu
from backend.app.admin.model.opera_log import OperaLog from backend.app.admin.model.opera_log import OperaLog
+11 -9
View File
@@ -4,30 +4,32 @@ from __future__ import annotations
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from sqlalchemy import String from sqlalchemy import ForeignKey, String
from sqlalchemy.orm import Mapped, mapped_column, relationship from sqlalchemy.orm import Mapped, mapped_column, relationship
from backend.app.admin.model.m2m import sys_role_data_rule
from backend.common.model import Base, id_key from backend.common.model import Base, id_key
if TYPE_CHECKING: if TYPE_CHECKING:
from backend.app.admin.model import Role from backend.app.admin.model import DataScope
class DataRule(Base): class DataRule(Base):
"""数据权限规则表""" """数据规则表"""
__tablename__ = 'sys_data_rule' __tablename__ = 'sys_data_rule'
id: Mapped[id_key] = mapped_column(init=False) id: Mapped[id_key] = mapped_column(init=False)
name: Mapped[str] = mapped_column(String(255), unique=True, comment='规则名称') name: Mapped[str] = mapped_column(String(500), unique=True, comment='名称')
model: Mapped[str] = mapped_column(String(50), comment='SQLA 模型') model: Mapped[str] = mapped_column(String(50), comment='SQLA 模型名,对应 DATA_PERMISSION_MODELS 键名')
column: Mapped[str] = mapped_column(String(20), comment='数据库字段') column: Mapped[str] = mapped_column(String(20), comment='模型字段')
operator: Mapped[int] = mapped_column(comment='运算符(0and、1or') operator: Mapped[int] = mapped_column(comment='运算符(0and、1or')
expression: Mapped[int] = mapped_column( expression: Mapped[int] = mapped_column(
comment='表达式(0==、1!=、2>、3>=、4<、5<=、6in、7not_in' comment='表达式(0==、1!=、2>、3>=、4<、5<=、6in、7not_in'
) )
value: Mapped[str] = mapped_column(String(255), comment='规则值') value: Mapped[str] = mapped_column(String(255), comment='规则值')
# 角色规则对多 # 数据范围规则对多
roles: Mapped[list[Role]] = relationship(init=False, secondary=sys_role_data_rule, back_populates='rules') scope_id: Mapped[int | None] = mapped_column(
ForeignKey('sys_data_scope.id', ondelete='SET NULL'), default=None, comment='数据范围关联 ID'
)
scope: Mapped[DataScope] = relationship(init=False, back_populates='rules')
+30
View File
@@ -0,0 +1,30 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from __future__ import annotations
from typing import TYPE_CHECKING
from sqlalchemy import String
from sqlalchemy.orm import Mapped, mapped_column, relationship
from backend.app.admin.model.m2m import sys_role_data_scope
from backend.common.model import Base, id_key
if TYPE_CHECKING:
from backend.app.admin.model import DataRule, Role
class DataScope(Base):
"""数据范围表"""
__tablename__ = 'sys_data_scope'
id: Mapped[id_key] = mapped_column(init=False)
name: Mapped[str] = mapped_column(String(50), unique=True, comment='名称')
status: Mapped[int] = mapped_column(default=1, comment='状态(0停用 1正常)')
# 数据范围规则一对多
rules: Mapped[list[DataRule]] = relationship(init=False, back_populates='scope')
# 角色数据范围多对多
roles: Mapped[list[Role]] = relationship(init=False, secondary=sys_role_data_scope, back_populates='scopes')
+7 -7
View File
@@ -20,16 +20,16 @@ sys_role_menu = Table(
Column('menu_id', Integer, ForeignKey('sys_menu.id', ondelete='CASCADE'), primary_key=True, comment='菜单ID'), Column('menu_id', Integer, ForeignKey('sys_menu.id', ondelete='CASCADE'), primary_key=True, comment='菜单ID'),
) )
sys_role_data_rule = Table( sys_role_data_scope = Table(
'sys_role_data_rule', 'sys_role_data_scope',
MappedBase.metadata, MappedBase.metadata,
Column('id', INT, primary_key=True, unique=True, index=True, autoincrement=True, comment='主键ID'), Column('id', INT, primary_key=True, unique=True, index=True, autoincrement=True, comment='主键 ID'),
Column('role_id', Integer, ForeignKey('sys_role.id', ondelete='CASCADE'), primary_key=True, comment='角色ID'), Column('role_id', Integer, ForeignKey('sys_role.id', ondelete='CASCADE'), primary_key=True, comment='角色 ID'),
Column( Column(
'data_rule_id', 'data_scope_id',
Integer, Integer,
ForeignKey('sys_data_rule.id', ondelete='CASCADE'), ForeignKey('sys_data_scope.id', ondelete='CASCADE'),
primary_key=True, primary_key=True,
comment='数据权限规则ID', comment='数据范围 ID',
), ),
) )
+5 -2
View File
@@ -24,15 +24,18 @@ class Menu(Base):
id: Mapped[id_key] = mapped_column(init=False) id: Mapped[id_key] = mapped_column(init=False)
title: Mapped[str] = mapped_column(String(50), comment='菜单标题') title: Mapped[str] = mapped_column(String(50), comment='菜单标题')
name: Mapped[str] = mapped_column(String(50), comment='菜单名称') name: Mapped[str] = mapped_column(String(50), comment='菜单名称')
path: Mapped[str] = mapped_column(String(200), comment='路由地址')
sort: Mapped[int] = mapped_column(default=0, comment='排序') sort: Mapped[int] = mapped_column(default=0, comment='排序')
icon: Mapped[str | None] = mapped_column(String(100), default=None, comment='菜单图标') icon: Mapped[str | None] = mapped_column(String(100), default=None, comment='菜单图标')
path: Mapped[str | None] = mapped_column(String(200), default=None, comment='路由地址') type: Mapped[int] = mapped_column(default=0, comment='菜单类型(0目录 1菜单 2按钮)')
menu_type: Mapped[int] = mapped_column(default=0, comment='菜单类型(0目录 1菜单 2按钮)')
component: Mapped[str | None] = mapped_column(String(255), default=None, comment='组件路径') component: Mapped[str | None] = mapped_column(String(255), default=None, comment='组件路径')
perms: Mapped[str | None] = mapped_column(String(100), default=None, comment='权限标识') perms: Mapped[str | None] = mapped_column(String(100), default=None, comment='权限标识')
status: Mapped[int] = mapped_column(default=1, comment='菜单状态(0停用 1正常)') status: Mapped[int] = mapped_column(default=1, comment='菜单状态(0停用 1正常)')
display: Mapped[int] = mapped_column(default=1, comment='是否显示(0否 1是)') display: Mapped[int] = mapped_column(default=1, comment='是否显示(0否 1是)')
cache: Mapped[int] = mapped_column(default=1, comment='是否缓存(0否 1是)') cache: Mapped[int] = mapped_column(default=1, comment='是否缓存(0否 1是)')
link: Mapped[str | None] = mapped_column(
LONGTEXT().with_variant(TEXT, 'postgresql'), default=None, comment='外链地址'
)
remark: Mapped[str | None] = mapped_column( remark: Mapped[str | None] = mapped_column(
LONGTEXT().with_variant(TEXT, 'postgresql'), default=None, comment='备注' LONGTEXT().with_variant(TEXT, 'postgresql'), default=None, comment='备注'
) )
+4 -4
View File
@@ -9,11 +9,11 @@ from sqlalchemy.dialects.mysql import LONGTEXT
from sqlalchemy.dialects.postgresql import TEXT from sqlalchemy.dialects.postgresql import TEXT
from sqlalchemy.orm import Mapped, mapped_column, relationship from sqlalchemy.orm import Mapped, mapped_column, relationship
from backend.app.admin.model.m2m import sys_role_data_rule, sys_role_menu, sys_user_role from backend.app.admin.model.m2m import sys_role_data_scope, sys_role_menu, sys_user_role
from backend.common.model import Base, id_key from backend.common.model import Base, id_key
if TYPE_CHECKING: if TYPE_CHECKING:
from backend.app.admin.model import DataRule, Menu, User from backend.app.admin.model import DataScope, Menu, User
class Role(Base): class Role(Base):
@@ -34,5 +34,5 @@ class Role(Base):
# 角色菜单多对多 # 角色菜单多对多
menus: Mapped[list[Menu]] = relationship(init=False, secondary=sys_role_menu, back_populates='roles') menus: Mapped[list[Menu]] = relationship(init=False, secondary=sys_role_menu, back_populates='roles')
# 角色数据权限规则多对多 # 角色数据范围多对多
rules: Mapped[list[DataRule]] = relationship(init=False, secondary=sys_role_data_rule, back_populates='roles') scopes: Mapped[list[DataScope]] = relationship(init=False, secondary=sys_role_data_scope, back_populates='roles')
+7 -4
View File
@@ -14,7 +14,7 @@ class DataRuleSchemaBase(SchemaBase):
name: str = Field(description='规则名称') name: str = Field(description='规则名称')
model: str = Field(description='模型名称') model: str = Field(description='模型名称')
column: str = Field(description='字段名称') column: str = Field(description='字段名称')
operator: RoleDataRuleOperatorType = Field(RoleDataRuleOperatorType.OR, description='操作符(AND/OR') operator: RoleDataRuleOperatorType = Field(RoleDataRuleOperatorType.AND, description='操作符(AND/OR')
expression: RoleDataRuleExpressionType = Field(RoleDataRuleExpressionType.eq, description='表达式类型') expression: RoleDataRuleExpressionType = Field(RoleDataRuleExpressionType.eq, description='表达式类型')
value: str = Field(description='规则值') value: str = Field(description='规则值')
@@ -36,6 +36,9 @@ class GetDataRuleDetail(DataRuleSchemaBase):
created_time: datetime = Field(description='创建时间') created_time: datetime = Field(description='创建时间')
updated_time: datetime | None = Field(None, description='更新时间') updated_time: datetime | None = Field(None, description='更新时间')
def __hash__(self) -> int:
"""计算哈希值""" class GetDataRuleColumnDetail(SchemaBase):
return hash(self.name) """数据规则可用模型字段详情"""
key: str = Field(description='字段名')
comment: str = Field(description='字段评论')
+46
View File
@@ -0,0 +1,46 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from datetime import datetime
from pydantic import ConfigDict, Field
from backend.app.admin.schema.data_rule import GetDataRuleDetail
from backend.common.enums import StatusType
from backend.common.schema import SchemaBase
class DataScopeBase(SchemaBase):
"""数据范围基础模型"""
name: str = Field(description='名称')
status: StatusType = Field(StatusType.enable, description='状态')
class CreateDataScopeParam(DataScopeBase):
"""创建数据范围参数"""
class UpdateDataScopeParam(DataScopeBase):
"""更新数据范围参数"""
class UpdateDataScopeRuleParam(SchemaBase):
"""更新数据范围规则参数"""
rules: list[int] = Field(description='数据规则 ID 列表')
class GetDataScopeDetail(DataScopeBase):
"""数据范围详情"""
model_config = ConfigDict(from_attributes=True)
id: int = Field(description='数据范围 ID')
created_time: datetime = Field(description='创建时间')
updated_time: datetime | None = Field(None, description='更新时间')
class GetDataScopeWithRelationDetail(GetDataScopeDetail):
"""数据范围关联详情"""
rules: list[GetDataRuleDetail] = Field([], description='数据规则列表')
+3 -2
View File
@@ -13,16 +13,17 @@ class MenuSchemaBase(SchemaBase):
title: str = Field(description='菜单标题') title: str = Field(description='菜单标题')
name: str = Field(description='菜单名称') name: str = Field(description='菜单名称')
path: str = Field(description='路由路径')
parent_id: int | None = Field(None, description='菜单父级 ID') parent_id: int | None = Field(None, description='菜单父级 ID')
sort: int = Field(0, ge=0, description='排序') sort: int = Field(0, ge=0, description='排序')
icon: str | None = Field(None, description='图标') icon: str | None = Field(None, description='图标')
path: str | None = Field(None, description='路由路径') type: MenuType = Field(MenuType.directory, description='菜单类型(0目录 1菜单 2按钮)')
menu_type: MenuType = Field(MenuType.directory, description='菜单类型(0目录 1菜单 2按钮)')
component: str | None = Field(None, description='组件路径') component: str | None = Field(None, description='组件路径')
perms: str | None = Field(None, description='权限标识') perms: str | None = Field(None, description='权限标识')
status: StatusType = Field(StatusType.enable, description='状态') status: StatusType = Field(StatusType.enable, description='状态')
display: StatusType = Field(StatusType.enable, description='是否显示') display: StatusType = Field(StatusType.enable, description='是否显示')
cache: StatusType = Field(StatusType.enable, description='是否缓存') cache: StatusType = Field(StatusType.enable, description='是否缓存')
link: str | None = Field(None, description='外链地址')
remark: str | None = Field(None, description='备注') remark: str | None = Field(None, description='备注')
+5 -5
View File
@@ -4,7 +4,7 @@ from datetime import datetime
from pydantic import ConfigDict, Field from pydantic import ConfigDict, Field
from backend.app.admin.schema.data_rule import GetDataRuleDetail from backend.app.admin.schema.data_scope import GetDataScopeDetail
from backend.app.admin.schema.menu import GetMenuDetail from backend.app.admin.schema.menu import GetMenuDetail
from backend.common.enums import StatusType from backend.common.enums import StatusType
from backend.common.schema import SchemaBase from backend.common.schema import SchemaBase
@@ -32,10 +32,10 @@ class UpdateRoleMenuParam(SchemaBase):
menus: list[int] = Field(description='菜单 ID 列表') menus: list[int] = Field(description='菜单 ID 列表')
class UpdateRoleRuleParam(SchemaBase): class UpdateRoleScopeParam(SchemaBase):
"""更新角色规则参数""" """更新角色数据范围参数"""
rules: list[int] = Field(description='数据规则 ID 列表') scopes: list[int] = Field(description='数据范围 ID 列表')
class GetRoleDetail(RoleSchemaBase): class GetRoleDetail(RoleSchemaBase):
@@ -52,4 +52,4 @@ class GetRoleWithRelationDetail(GetRoleDetail):
"""角色关联详情""" """角色关联详情"""
menus: list[GetMenuDetail | None] = Field([], description='菜单详情列表') menus: list[GetMenuDetail | None] = Field([], description='菜单详情列表')
rules: list[GetDataRuleDetail | None] = Field([], description='数据规则详情列表') scopes: list[GetDataScopeDetail | None] = Field([], description='数据范围列表')
+10 -4
View File
@@ -33,7 +33,7 @@ class AuthService:
"""认证服务类""" """认证服务类"""
@staticmethod @staticmethod
async def user_verify(db: AsyncSession, username: str, password: str) -> User: async def user_verify(db: AsyncSession, username: str, password: str | None) -> User:
""" """
验证用户名和密码 验证用户名和密码
@@ -45,10 +45,16 @@ class AuthService:
user = await user_dao.get_by_username(db, username) user = await user_dao.get_by_username(db, username)
if not user: if not user:
raise errors.NotFoundError(msg='用户名或密码有误') raise errors.NotFoundError(msg='用户名或密码有误')
elif not password_verify(password, user.password):
if user.password is None:
raise errors.AuthorizationError(msg='用户名或密码有误') raise errors.AuthorizationError(msg='用户名或密码有误')
elif not user.status: else:
if not password_verify(password, user.password):
raise errors.AuthorizationError(msg='用户名或密码有误')
if not user.status:
raise errors.AuthorizationError(msg='用户已被锁定, 请联系统管理员') raise errors.AuthorizationError(msg='用户已被锁定, 请联系统管理员')
return user return user
async def swagger_login(self, *, obj: HTTPBasicCredentials) -> tuple[str, User]: async def swagger_login(self, *, obj: HTTPBasicCredentials) -> tuple[str, User]:
@@ -65,7 +71,7 @@ class AuthService:
str(user.id), str(user.id),
user.is_multi_login, user.is_multi_login,
# extra info # extra info
login_type='swagger', swagger=True,
) )
return a_token.access_token, user return a_token.access_token, user
+17 -31
View File
@@ -2,22 +2,19 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from typing import Sequence from typing import Sequence
from fastapi import Request
from sqlalchemy import Select from sqlalchemy import Select
from backend.app.admin.crud.crud_data_rule import data_rule_dao from backend.app.admin.crud.crud_data_rule import data_rule_dao
from backend.app.admin.crud.crud_role import role_dao
from backend.app.admin.model import DataRule from backend.app.admin.model import DataRule
from backend.app.admin.schema.data_rule import CreateDataRuleParam, UpdateDataRuleParam from backend.app.admin.schema.data_rule import CreateDataRuleParam, GetDataRuleColumnDetail, UpdateDataRuleParam
from backend.common.exception import errors from backend.common.exception import errors
from backend.core.conf import settings from backend.core.conf import settings
from backend.database.db import async_db_session from backend.database.db import async_db_session
from backend.database.redis import redis_client
from backend.utils.import_parse import dynamic_import_data_model from backend.utils.import_parse import dynamic_import_data_model
class DataRuleService: class DataRuleService:
"""数据权限规则服务类""" """数据规则服务类"""
@staticmethod @staticmethod
async def get(*, pk: int) -> DataRule: async def get(*, pk: int) -> DataRule:
@@ -33,44 +30,32 @@ class DataRuleService:
raise errors.NotFoundError(msg='数据规则不存在') raise errors.NotFoundError(msg='数据规则不存在')
return data_rule return data_rule
@staticmethod
async def get_role_rules(*, pk: int) -> list[int]:
"""
获取角色的数据规则列表
:param pk: 角色 ID
:return:
"""
async with async_db_session() as db:
role = await role_dao.get_with_relation(db, pk)
if not role:
raise errors.NotFoundError(msg='角色不存在')
rule_ids = [rule.id for rule in role.rules]
return rule_ids
@staticmethod @staticmethod
async def get_models() -> list[str]: async def get_models() -> list[str]:
"""获取所有数据模型""" """获取所有数据规则可用模型"""
return list(settings.DATA_PERMISSION_MODELS.keys()) return list(settings.DATA_PERMISSION_MODELS.keys())
@staticmethod @staticmethod
async def get_columns(model: str) -> list[str]: async def get_columns(model: str) -> list[GetDataRuleColumnDetail]:
""" """
获取数据模型的字段列表 获取数据规则可用模型的字段列表
:param model: 模型名称 :param model: 模型名称
:return: :return:
""" """
if model not in settings.DATA_PERMISSION_MODELS: if model not in settings.DATA_PERMISSION_MODELS:
raise errors.NotFoundError(msg='数据模型不存在') raise errors.NotFoundError(msg='数据规则可用模型不存在')
model_ins = dynamic_import_data_model(settings.DATA_PERMISSION_MODELS[model]) model_ins = dynamic_import_data_model(settings.DATA_PERMISSION_MODELS[model])
model_columns = [ model_columns = [
key for key in model_ins.__table__.columns.keys() if key not in settings.DATA_PERMISSION_COLUMN_EXCLUDE GetDataRuleColumnDetail(key=column.key, comment=column.comment)
for column in model_ins.__table__.columns
if column.key not in settings.DATA_PERMISSION_COLUMN_EXCLUDE
] ]
return model_columns return model_columns
@staticmethod @staticmethod
async def get_select(*, name: str | None = None) -> Select: async def get_select(*, name: str | None) -> Select:
""" """
获取数据规则列表查询条件 获取数据规则列表查询条件
@@ -97,7 +82,7 @@ class DataRuleService:
async with async_db_session.begin() as db: async with async_db_session.begin() as db:
data_rule = await data_rule_dao.get_by_name(db, obj.name) data_rule = await data_rule_dao.get_by_name(db, obj.name)
if data_rule: if data_rule:
raise errors.ForbiddenError(msg='数据权限规则已存在') raise errors.ForbiddenError(msg='数据规则已存在')
await data_rule_dao.create(db, obj) await data_rule_dao.create(db, obj)
@staticmethod @staticmethod
@@ -112,22 +97,23 @@ class DataRuleService:
async with async_db_session.begin() as db: async with async_db_session.begin() as db:
data_rule = await data_rule_dao.get(db, pk) data_rule = await data_rule_dao.get(db, pk)
if not data_rule: if not data_rule:
raise errors.NotFoundError(msg='数据权限规则不存在') raise errors.NotFoundError(msg='数据规则不存在')
if data_rule.name != obj.name:
if await data_rule_dao.get_by_name(db, obj.name):
raise errors.ForbiddenError(msg='数据规则已存在')
count = await data_rule_dao.update(db, pk, obj) count = await data_rule_dao.update(db, pk, obj)
return count return count
@staticmethod @staticmethod
async def delete(*, request: Request, pk: list[int]) -> int: async def delete(*, pk: list[int]) -> int:
""" """
删除数据规则 删除数据规则
:param request: FastAPI 请求对象
:param pk: 规则 ID 列表 :param pk: 规则 ID 列表
:return: :return:
""" """
async with async_db_session.begin() as db: async with async_db_session.begin() as db:
count = await data_rule_dao.delete(db, pk) count = await data_rule_dao.delete(db, pk)
await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{request.user.id}')
return count return count
@@ -0,0 +1,124 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from sqlalchemy import Select
from backend.app.admin.crud.crud_data_scope import data_scope_dao
from backend.app.admin.model import DataScope
from backend.app.admin.schema.data_scope import CreateDataScopeParam, UpdateDataScopeParam, UpdateDataScopeRuleParam
from backend.common.exception import errors
from backend.core.conf import settings
from backend.database.db import async_db_session
from backend.database.redis import redis_client
class DataScopeService:
"""数据范围服务类"""
@staticmethod
async def get(*, pk: int) -> DataScope:
"""
获取数据范围详情
:param pk: 范围 ID
:return:
"""
async with async_db_session() as db:
data_scope = await data_scope_dao.get(db, pk)
if not data_scope:
raise errors.NotFoundError(msg='数据范围不存在')
return data_scope
@staticmethod
async def get_rules(*, pk: int) -> DataScope:
"""
获取数据范围规则
:param pk: 范围 ID
:return:
"""
async with async_db_session() as db:
data_scope = await data_scope_dao.get_with_relation(db, pk)
if not data_scope:
raise errors.NotFoundError(msg='数据范围不存在')
return data_scope
@staticmethod
async def get_select(*, name: str | None, status: int | None) -> Select:
"""
获取数据范围列表查询条件
:param name: 范围名称
:param status: 范围状态
:return:
"""
return await data_scope_dao.get_list(name, status)
@staticmethod
async def create(*, obj: CreateDataScopeParam) -> None:
"""
创建数据范围
:param obj: 数据范围参数
:return:
"""
async with async_db_session.begin() as db:
data_scope = await data_scope_dao.get_by_name(db, obj.name)
if data_scope:
raise errors.ForbiddenError(msg='数据范围已存在')
await data_scope_dao.create(db, obj)
@staticmethod
async def update(*, pk: int, obj: UpdateDataScopeParam) -> int:
"""
更新数据范围
:param pk: 范围 ID
:param obj: 数据范围更新参数
:return:
"""
async with async_db_session.begin() as db:
data_scope = await data_scope_dao.get(db, pk)
if not data_scope:
raise errors.NotFoundError(msg='数据范围不存在')
if data_scope.name != obj.name:
if await data_scope_dao.get_by_name(db, obj.name):
raise errors.ForbiddenError(msg='数据范围已存在')
count = await data_scope_dao.update(db, pk, obj)
for role in await data_scope.awaitable_attrs.roles:
for user in await role.awaitable_attrs.users:
await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{user.id}')
return count
@staticmethod
async def update_data_scope_rule(*, pk: int, rule_ids: UpdateDataScopeRuleParam) -> int:
"""
更新数据范围规则
:param pk: 范围 ID
:param rule_ids: 规则 ID 列表
:return:
"""
async with async_db_session.begin() as db:
count = await data_scope_dao.update_rules(db, pk, rule_ids)
return count
@staticmethod
async def delete(*, pk: list[int]) -> int:
"""
删除数据范围
:param pk: 范围 ID 列表
:return:
"""
async with async_db_session.begin() as db:
count = await data_scope_dao.delete(db, pk)
for _pk in pk:
data_rule = await data_scope_dao.get(db, _pk)
if data_rule:
for role in await data_rule.awaitable_attrs.roles:
for user in await role.awaitable_attrs.users:
await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{user.id}')
return count
data_scope_service: DataScopeService = DataScopeService()
+7 -7
View File
@@ -33,11 +33,12 @@ class DeptService:
@staticmethod @staticmethod
async def get_dept_tree( async def get_dept_tree(
*, name: str | None = None, leader: str | None = None, phone: str | None = None, status: int | None = None *, request: Request, name: str | None, leader: str | None, phone: str | None, status: int | None
) -> list[dict[str, Any]]: ) -> list[dict[str, Any]]:
""" """
获取部门树形结构 获取部门树形结构
:param request: FastAPI 请求对象
:param name: 部门名称 :param name: 部门名称
:param leader: 部门负责人 :param leader: 部门负责人
:param phone: 联系电话 :param phone: 联系电话
@@ -45,7 +46,7 @@ class DeptService:
:return: :return:
""" """
async with async_db_session() as db: async with async_db_session() as db:
dept_select = await dept_dao.get_all(db=db, name=name, leader=leader, phone=phone, status=status) dept_select = await dept_dao.get_all(request, db, name, leader, phone, status)
tree_data = get_tree_data(dept_select) tree_data = get_tree_data(dept_select)
return tree_data return tree_data
@@ -93,24 +94,23 @@ class DeptService:
return count return count
@staticmethod @staticmethod
async def delete(*, request: Request, pk: int) -> int: async def delete(*, pk: int) -> int:
""" """
删除部门 删除部门
:param request: FastAPI 请求对象
:param pk: 部门 ID :param pk: 部门 ID
:return: :return:
""" """
async with async_db_session.begin() as db: async with async_db_session.begin() as db:
dept = await dept_dao.get_with_relation(db, pk) dept = await dept_dao.get_with_relation(db, pk)
dept_user = dept.users if dept.users:
if dept_user:
raise errors.ForbiddenError(msg='部门下存在用户,无法删除') raise errors.ForbiddenError(msg='部门下存在用户,无法删除')
children = await dept_dao.get_children(db, pk) children = await dept_dao.get_children(db, pk)
if children: if children:
raise errors.ForbiddenError(msg='部门下存在子部门,无法删除') raise errors.ForbiddenError(msg='部门下存在子部门,无法删除')
count = await dept_dao.delete(db, pk) count = await dept_dao.delete(db, pk)
await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{request.user.id}') for user in dept.users:
await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{user.id}')
return count return count
@@ -16,7 +16,7 @@ class LoginLogService:
"""登录日志服务类""" """登录日志服务类"""
@staticmethod @staticmethod
async def get_select(*, username: str | None = None, status: int | None = None, ip: str | None = None) -> Select: async def get_select(*, username: str | None, status: int | None, ip: str | None) -> Select:
""" """
获取登录日志列表查询条件 获取登录日志列表查询条件
+12 -24
View File
@@ -5,14 +5,13 @@ from typing import Any
from fastapi import Request from fastapi import Request
from backend.app.admin.crud.crud_menu import menu_dao 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.model import Menu
from backend.app.admin.schema.menu import CreateMenuParam, UpdateMenuParam from backend.app.admin.schema.menu import CreateMenuParam, UpdateMenuParam
from backend.common.exception import errors from backend.common.exception import errors
from backend.core.conf import settings from backend.core.conf import settings
from backend.database.db import async_db_session from backend.database.db import async_db_session
from backend.database.redis import redis_client from backend.database.redis import redis_client
from backend.utils.build_tree import get_tree_data from backend.utils.build_tree import get_tree_data, get_vben5_tree_data
class MenuService: class MenuService:
@@ -33,7 +32,7 @@ class MenuService:
return menu return menu
@staticmethod @staticmethod
async def get_menu_tree(*, title: str | None = None, status: int | None = None) -> list[dict[str, Any]]: async def get_menu_tree(*, title: str | None, status: int | None) -> list[dict[str, Any]]:
""" """
获取菜单树形结构 获取菜单树形结构
@@ -46,23 +45,6 @@ class MenuService:
menu_tree = get_tree_data(menu_select) menu_tree = get_tree_data(menu_select)
return menu_tree return menu_tree
@staticmethod
async def get_role_menu_tree(*, pk: int) -> list[dict[str, Any]]:
"""
获取角色的菜单树形结构
:param pk: 角色 ID
:return:
"""
async with async_db_session() as db:
role = await role_dao.get_with_relation(db, pk)
if not role:
raise errors.NotFoundError(msg='角色不存在')
menu_ids = [menu.id for menu in role.menus]
menu_select = await menu_dao.get_role_menus(db, False, menu_ids)
menu_tree = get_tree_data(menu_select)
return menu_tree
@staticmethod @staticmethod
async def get_user_menu_tree(*, request: Request) -> list[dict[str, Any]]: async def get_user_menu_tree(*, request: Request) -> list[dict[str, Any]]:
""" """
@@ -79,7 +61,7 @@ class MenuService:
for role in roles: for role in roles:
menu_ids.extend([menu.id for menu in role.menus]) menu_ids.extend([menu.id for menu in role.menus])
menu_select = await menu_dao.get_role_menus(db, request.user.is_superuser, menu_ids) menu_select = await menu_dao.get_role_menus(db, request.user.is_superuser, menu_ids)
menu_tree = get_tree_data(menu_select) menu_tree = get_vben5_tree_data(menu_select)
return menu_tree return menu_tree
@staticmethod @staticmethod
@@ -123,14 +105,16 @@ class MenuService:
if obj.parent_id == menu.id: if obj.parent_id == menu.id:
raise errors.ForbiddenError(msg='禁止关联自身为父级') raise errors.ForbiddenError(msg='禁止关联自身为父级')
count = await menu_dao.update(db, pk, obj) count = await menu_dao.update(db, pk, obj)
for role in await menu.awaitable_attrs.roles:
for user in await role.awaitable_attrs.users:
await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{user.id}')
return count return count
@staticmethod @staticmethod
async def delete(*, request: Request, pk: int) -> int: async def delete(*, pk: int) -> int:
""" """
删除菜单 删除菜单
:param request: FastAPI 请求对象
:param pk: 菜单 ID :param pk: 菜单 ID
:return: :return:
""" """
@@ -138,8 +122,12 @@ class MenuService:
children = await menu_dao.get_children(db, pk) children = await menu_dao.get_children(db, pk)
if children: if children:
raise errors.ForbiddenError(msg='菜单下存在子菜单,无法删除') raise errors.ForbiddenError(msg='菜单下存在子菜单,无法删除')
menu = await menu_dao.get(db, pk)
count = await menu_dao.delete(db, pk) count = await menu_dao.delete(db, pk)
await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{request.user.id}') if menu:
for role in await menu.awaitable_attrs.roles:
for user in await role.awaitable_attrs.users:
await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{user.id}')
return count return count
@@ -11,7 +11,7 @@ class OperaLogService:
"""操作日志服务类""" """操作日志服务类"""
@staticmethod @staticmethod
async def get_select(*, username: str | None = None, status: int | None = None, ip: str | None = None) -> Select: async def get_select(*, username: str | None, status: int | None, ip: str | None) -> Select:
""" """
获取操作日志列表查询条件 获取操作日志列表查询条件
+61 -26
View File
@@ -1,11 +1,10 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from typing import Sequence from typing import Any, Sequence
from fastapi import Request
from sqlalchemy import Select from sqlalchemy import Select
from backend.app.admin.crud.crud_data_rule import data_rule_dao from backend.app.admin.crud.crud_data_scope import data_scope_dao
from backend.app.admin.crud.crud_menu import menu_dao from backend.app.admin.crud.crud_menu import menu_dao
from backend.app.admin.crud.crud_role import role_dao from backend.app.admin.crud.crud_role import role_dao
from backend.app.admin.model import Role from backend.app.admin.model import Role
@@ -13,12 +12,13 @@ from backend.app.admin.schema.role import (
CreateRoleParam, CreateRoleParam,
UpdateRoleMenuParam, UpdateRoleMenuParam,
UpdateRoleParam, UpdateRoleParam,
UpdateRoleRuleParam, UpdateRoleScopeParam,
) )
from backend.common.exception import errors from backend.common.exception import errors
from backend.core.conf import settings from backend.core.conf import settings
from backend.database.db import async_db_session from backend.database.db import async_db_session
from backend.database.redis import redis_client from backend.database.redis import redis_client
from backend.utils.build_tree import get_tree_data
class RoleService: class RoleService:
@@ -46,7 +46,7 @@ class RoleService:
return roles return roles
@staticmethod @staticmethod
async def get_by_user(*, pk: int) -> Sequence[Role]: async def get_users(*, pk: int) -> Sequence[Role]:
""" """
获取用户的角色列表 获取用户的角色列表
@@ -54,11 +54,11 @@ class RoleService:
:return: :return:
""" """
async with async_db_session() as db: async with async_db_session() as db:
roles = await role_dao.get_by_user(db, user_id=pk) roles = await role_dao.get_users(db, user_id=pk)
return roles return roles
@staticmethod @staticmethod
async def get_select(*, name: str | None = None, status: int | None = None) -> Select: async def get_select(*, name: str | None, status: int | None) -> Select:
""" """
获取角色列表查询条件 获取角色列表查询条件
@@ -68,6 +68,38 @@ class RoleService:
""" """
return await role_dao.get_list(name=name, status=status) return await role_dao.get_list(name=name, status=status)
@staticmethod
async def get_menu_tree(*, pk: int) -> list[dict[str, Any]]:
"""
获取角色的菜单树形结构
:param pk: 角色 ID
:return:
"""
async with async_db_session() as db:
role = await role_dao.get_with_relation(db, pk)
if not role:
raise errors.NotFoundError(msg='角色不存在')
menu_ids = [menu.id for menu in role.menus]
menu_select = await menu_dao.get_role_menus(db, False, menu_ids)
menu_tree = get_tree_data(menu_select)
return menu_tree
@staticmethod
async def get_scopes(*, pk: int) -> list[int]:
"""
获取角色数据范围列表
:param pk:
:return:
"""
async with async_db_session() as db:
role = await role_dao.get_with_relation(db, pk)
if not role:
raise errors.NotFoundError(msg='角色不存在')
scope_ids = [scope.id for scope in role.scopes]
return scope_ids
@staticmethod @staticmethod
async def create(*, obj: CreateRoleParam) -> None: async def create(*, obj: CreateRoleParam) -> None:
""" """
@@ -100,20 +132,21 @@ class RoleService:
if role: if role:
raise errors.ForbiddenError(msg='角色已存在') raise errors.ForbiddenError(msg='角色已存在')
count = await role_dao.update(db, pk, obj) count = await role_dao.update(db, pk, obj)
for user in await role.awaitable_attrs.users:
await redis_client.delete_prefix(f'{settings.JWT_USER_REDIS_PREFIX}:{user.id}')
return count return count
@staticmethod @staticmethod
async def update_role_menu(*, request: Request, pk: int, menu_ids: UpdateRoleMenuParam) -> int: async def update_role_menu(*, pk: int, menu_ids: UpdateRoleMenuParam) -> int:
""" """
更新角色菜单 更新角色菜单
:param request: FastAPI 请求对象
:param pk: 角色 ID :param pk: 角色 ID
:param menu_ids: 菜单 ID 列表 :param menu_ids: 菜单 ID 列表
:return: :return:
""" """
async with async_db_session.begin() as db: async with async_db_session.begin() as db:
role = await role_dao.get(db, pk) role = await role_dao.get_with_relation(db, pk)
if not role: if not role:
raise errors.NotFoundError(msg='角色不存在') raise errors.NotFoundError(msg='角色不存在')
for menu_id in menu_ids.menus: for menu_id in menu_ids.menus:
@@ -121,45 +154,47 @@ class RoleService:
if not menu: if not menu:
raise errors.NotFoundError(msg='菜单不存在') raise errors.NotFoundError(msg='菜单不存在')
count = await role_dao.update_menus(db, pk, menu_ids) count = await role_dao.update_menus(db, pk, menu_ids)
if pk in [role.id for role in request.user.roles]: for user in await role.awaitable_attrs.users:
await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{request.user.id}') await redis_client.delete_prefix(f'{settings.JWT_USER_REDIS_PREFIX}:{user.id}')
return count return count
@staticmethod @staticmethod
async def update_role_rule(*, request: Request, pk: int, rule_ids: UpdateRoleRuleParam) -> int: async def update_role_scope(*, pk: int, scope_ids: UpdateRoleScopeParam) -> int:
""" """
更新角色数据权限 更新角色数据范围
:param request: FastAPI 请求对象
:param pk: 角色 ID :param pk: 角色 ID
:param rule_ids: 权限规则 ID 列表 :param scope_ids: 权限规则 ID 列表
:return: :return:
""" """
async with async_db_session.begin() as db: async with async_db_session.begin() as db:
role = await role_dao.get(db, pk) role = await role_dao.get(db, pk)
if not role: if not role:
raise errors.NotFoundError(msg='角色不存在') raise errors.NotFoundError(msg='角色不存在')
for rule_id in rule_ids.rules: for scope_id in scope_ids.scopes:
rule = await data_rule_dao.get(db, rule_id) scope = await data_scope_dao.get(db, scope_id)
if not rule: if not scope:
raise errors.NotFoundError(msg='数据权限不存在') raise errors.NotFoundError(msg='数据范围不存在')
count = await role_dao.update_rules(db, pk, rule_ids) count = await role_dao.update_scopes(db, pk, scope_ids)
if pk in [role.id for role in request.user.roles]: for user in await role.awaitable_attrs.users:
await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{request.user.id}') await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{user.id}')
return count return count
@staticmethod @staticmethod
async def delete(*, request: Request, pk: list[int]) -> int: async def delete(*, pk: list[int]) -> int:
""" """
删除角色 删除角色
:param request: FastAPI 请求对象
:param pk: 角色 ID 列表 :param pk: 角色 ID 列表
:return: :return:
""" """
async with async_db_session.begin() as db: async with async_db_session.begin() as db:
count = await role_dao.delete(db, pk) count = await role_dao.delete(db, pk)
await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{request.user.id}') for _pk in pk:
role = await role_dao.get(db, _pk)
if role:
for user in await role.awaitable_attrs.users:
await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{user.id}')
return count return count
+30 -40
View File
@@ -135,25 +135,25 @@ class UserService:
:return: :return:
""" """
async with async_db_session.begin() as db: async with async_db_session.begin() as db:
if not request.user.is_superuser and request.user.username != username: if request.user.username != username:
raise errors.ForbiddenError(msg='你只能修改自己的信息') raise errors.ForbiddenError(msg='你只能修改自己的信息')
input_user = await user_dao.get_with_relation(db, username=username) user = await user_dao.get_with_relation(db, username=username)
if not input_user: if not user:
raise errors.NotFoundError(msg='用户不存在') raise errors.NotFoundError(msg='用户不存在')
if input_user.username != obj.username: if user.username != obj.username:
_username = await user_dao.get_by_username(db, obj.username) _username = await user_dao.get_by_username(db, obj.username)
if _username: if _username:
raise errors.ForbiddenError(msg='用户名已注册') raise errors.ForbiddenError(msg='用户名已注册')
if input_user.nickname != obj.nickname: if user.nickname != obj.nickname:
nickname = await user_dao.get_by_nickname(db, obj.nickname) nickname = await user_dao.get_by_nickname(db, obj.nickname)
if nickname: if nickname:
raise errors.ForbiddenError(msg='昵称已注册') raise errors.ForbiddenError(msg='昵称已注册')
if input_user.email != obj.email: if user.email != obj.email:
email = await user_dao.check_email(db, obj.email) email = await user_dao.check_email(db, obj.email)
if email: if email:
raise errors.ForbiddenError(msg='邮箱已注册') raise errors.ForbiddenError(msg='邮箱已注册')
count = await user_dao.update_userinfo(db, input_user.id, obj) count = await user_dao.update_userinfo(db, user.id, obj)
await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{request.user.id}') await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{user.id}')
return count return count
@staticmethod @staticmethod
@@ -168,7 +168,7 @@ class UserService:
""" """
async with async_db_session.begin() as db: async with async_db_session.begin() as db:
if not request.user.is_superuser and request.user.username != username: if not request.user.is_superuser and request.user.username != username:
raise errors.AuthorizationError raise errors.ForbiddenError(msg='你只能修改自己的信息')
input_user = await user_dao.get_with_relation(db, username=username) input_user = await user_dao.get_with_relation(db, username=username)
if not input_user: if not input_user:
raise errors.NotFoundError(msg='用户不存在') raise errors.NotFoundError(msg='用户不存在')
@@ -190,17 +190,17 @@ class UserService:
:return: :return:
""" """
async with async_db_session.begin() as db: async with async_db_session.begin() as db:
if not request.user.is_superuser and request.user.username != username: if request.user.username != username:
raise errors.AuthorizationError raise errors.AuthorizationError
input_user = await user_dao.get_by_username(db, username) user = await user_dao.get_by_username(db, username)
if not input_user: if not user:
raise errors.NotFoundError(msg='用户不存在') raise errors.NotFoundError(msg='用户不存在')
count = await user_dao.update_avatar(db, input_user.id, avatar) count = await user_dao.update_avatar(db, user.id, avatar)
await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{request.user.id}') await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{user.id}')
return count return count
@staticmethod @staticmethod
async def get_select(*, dept: int, username: str = None, phone: str = None, status: int = None) -> Select: async def get_select(*, dept: int, username: str, phone: str, status: int) -> Select:
""" """
获取用户列表查询条件 获取用户列表查询条件
@@ -230,7 +230,7 @@ class UserService:
raise errors.ForbiddenError(msg='非法操作') raise errors.ForbiddenError(msg='非法操作')
super_status = await user_dao.get_super(db, pk) super_status = await user_dao.get_super(db, pk)
count = await user_dao.set_super(db, pk, not super_status) count = await user_dao.set_super(db, pk, not super_status)
await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{pk}') await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{user.id}')
return count return count
@staticmethod @staticmethod
@@ -251,7 +251,7 @@ class UserService:
raise errors.ForbiddenError(msg='非法操作') raise errors.ForbiddenError(msg='非法操作')
staff_status = await user_dao.get_staff(db, pk) staff_status = await user_dao.get_staff(db, pk)
count = await user_dao.set_staff(db, pk, not staff_status) count = await user_dao.set_staff(db, pk, not staff_status)
await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{pk}') await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{user.id}')
return count return count
@staticmethod @staticmethod
@@ -272,7 +272,7 @@ class UserService:
raise errors.ForbiddenError(msg='非法操作') raise errors.ForbiddenError(msg='非法操作')
status = await user_dao.get_status(db, pk) status = await user_dao.get_status(db, pk)
count = await user_dao.set_status(db, pk, 0 if status == 1 else 1) count = await user_dao.set_status(db, pk, 0 if status == 1 else 1)
await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{pk}') await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{user.id}')
return count return count
@staticmethod @staticmethod
@@ -289,32 +289,22 @@ class UserService:
user = await user_dao.get(db, pk) user = await user_dao.get(db, pk)
if not user: if not user:
raise errors.NotFoundError(msg='用户不存在') raise errors.NotFoundError(msg='用户不存在')
user_id = request.user.id multi_login = await user_dao.get_multi_login(db, pk) if pk != user.id else request.user.is_multi_login
multi_login = await user_dao.get_multi_login(db, pk) if pk != user_id else request.user.is_multi_login new_multi_login = not multi_login
count = await user_dao.set_multi_login(db, pk, not multi_login) count = await user_dao.set_multi_login(db, pk, new_multi_login)
# 删除当前用户缓存 await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{user.id}')
await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{request.user.id}')
token = get_token(request) token = get_token(request)
token_payload = jwt_decode(token) token_payload = jwt_decode(token)
latest_multi_login = await user_dao.get_multi_login(db, pk) if pk == user.id:
# 超级用户修改自身时,除当前 token 外,其他 token 失效 # 系统管理员修改自身时,除当前 token 外,其他 token 失效
if pk == user_id: if not new_multi_login:
if not latest_multi_login: key_prefix = f'{settings.TOKEN_REDIS_PREFIX}:{user.id}'
key_prefix = f'{settings.TOKEN_REDIS_PREFIX}:{pk}'
await redis_client.delete_prefix(key_prefix, exclude=f'{key_prefix}:{token_payload.session_uuid}') await redis_client.delete_prefix(key_prefix, exclude=f'{key_prefix}:{token_payload.session_uuid}')
refresh_token = request.cookies.get(settings.COOKIE_REFRESH_TOKEN_KEY)
if refresh_token:
key_prefix = f'{settings.TOKEN_REFRESH_REDIS_PREFIX}:{pk}'
await redis_client.delete_prefix(key_prefix, exclude=f'{key_prefix}:{refresh_token}')
# 超级用户修改他人时,其他 token 将全部失效
else: else:
if not latest_multi_login: # 系统管理员修改他人时,他人 token 全部失效
key_prefix = [f'{settings.TOKEN_REDIS_PREFIX}:{pk}'] if not new_multi_login:
refresh_token = request.cookies.get(settings.COOKIE_REFRESH_TOKEN_KEY) key_prefix = f'{settings.TOKEN_REDIS_PREFIX}:{user.id}'
if refresh_token: await redis_client.delete_prefix(key_prefix)
key_prefix.append(f'{settings.TOKEN_REFRESH_REDIS_PREFIX}:{pk}')
for prefix in key_prefix:
await redis_client.delete_prefix(prefix)
return count return count
@staticmethod @staticmethod
-14
View File
@@ -1,14 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from fastapi import APIRouter
from backend.app.generator.api.v1.gen import router as gen_router
from backend.app.generator.api.v1.gen_business import router as gen_business_router
from backend.app.generator.api.v1.gen_model import router as gen_model_router
from backend.core.conf import settings
v1 = APIRouter(prefix=f'{settings.FASTAPI_API_V1_PATH}/gen', tags=['代码生成'])
v1.include_router(gen_router)
v1.include_router(gen_business_router, prefix='/businesses')
v1.include_router(gen_model_router, prefix='/models')
-4
View File
@@ -1,4 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from backend.app.generator.model.gen_business import GenBusiness
from backend.app.generator.model.gen_model import GenModel
-2
View File
@@ -3,11 +3,9 @@
from fastapi import APIRouter from fastapi import APIRouter
from backend.app.admin.api.router import v1 as admin_v1 from backend.app.admin.api.router import v1 as admin_v1
from backend.app.generator.api.router import v1 as generator_v1
from backend.app.task.api.router import v1 as task_v1 from backend.app.task.api.router import v1 as task_v1
router = APIRouter() router = APIRouter()
router.include_router(admin_v1) router.include_router(admin_v1)
router.include_router(generator_v1)
router.include_router(task_v1) router.include_router(task_v1)
+2 -2
View File
@@ -5,6 +5,6 @@ from fastapi import APIRouter
from backend.app.task.api.v1.task import router as task_router from backend.app.task.api.v1.task import router as task_router
from backend.core.conf import settings from backend.core.conf import settings
v1 = APIRouter(prefix=settings.FASTAPI_API_V1_PATH, tags=['任务']) v1 = APIRouter(prefix=settings.FASTAPI_API_V1_PATH)
v1.include_router(task_router, prefix='/tasks') v1.include_router(task_router, prefix='/tasks', tags=['任务'])
+5 -2
View File
@@ -1,5 +1,7 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import asyncio
from typing import Any from typing import Any
from celery import Task from celery import Task
@@ -34,7 +36,7 @@ class TaskBase(Task):
""" """
await task_notification(msg=f'任务 {task_id} 执行成功') await task_notification(msg=f'任务 {task_id} 执行成功')
async def on_failure(self, exc: Exception, task_id: str, args, kwargs, einfo) -> None: def on_failure(self, exc: Exception, task_id: str, args, kwargs, einfo) -> None:
""" """
任务失败后执行钩子 任务失败后执行钩子
@@ -43,4 +45,5 @@ class TaskBase(Task):
:param einfo: 异常信息 :param einfo: 异常信息
:return: :return:
""" """
await task_notification(msg=f'任务 {task_id} 执行失败') loop = asyncio.get_event_loop()
loop.create_task(task_notification(msg=f'任务 {task_id} 执行失败'))
+2 -152
View File
@@ -47,14 +47,14 @@ class MenuType(IntEnum):
class RoleDataRuleOperatorType(IntEnum): class RoleDataRuleOperatorType(IntEnum):
"""数据权限规则运算符""" """数据规则运算符"""
AND = 0 AND = 0
OR = 1 OR = 1
class RoleDataRuleExpressionType(IntEnum): class RoleDataRuleExpressionType(IntEnum):
"""数据权限规则表达式""" """数据规则表达式"""
eq = 0 # == eq = 0 # ==
ne = 1 # != ne = 1 # !=
@@ -119,153 +119,3 @@ class FileType(StrEnum):
image = 'image' image = 'image'
video = 'video' video = 'video'
class GenModelMySQLColumnType(StrEnum):
"""代码生成模型列类型(MySQL"""
# Python 类型映射
BIGINT = 'int'
BigInteger = 'int' # BIGINT
BINARY = 'bytes'
BLOB = 'bytes'
BOOLEAN = 'bool' # BOOL
Boolean = 'bool' # BOOL
CHAR = 'str'
CLOB = 'str'
DATE = 'date'
Date = 'date' # DATE
DATETIME = 'datetime'
DateTime = 'datetime' # DATETIME
DECIMAL = 'Decimal'
DOUBLE = 'float'
Double = 'float' # DOUBLE
DOUBLE_PRECISION = 'float'
Enum = 'Enum' # Enum()
FLOAT = 'float'
Float = 'float' # FLOAT
INT = 'int' # INTEGER
INTEGER = 'int'
Integer = 'int' # INTEGER
Interval = 'timedelta' # DATETIME
JSON = 'dict'
LargeBinary = 'bytes' # BLOB
NCHAR = 'str'
NUMERIC = 'Decimal'
Numeric = 'Decimal' # NUMERIC
NVARCHAR = 'str' # String
PickleType = 'bytes' # BLOB
REAL = 'float'
SMALLINT = 'int'
SmallInteger = 'int' # SMALLINT
String = 'str' # String
TEXT = 'str'
Text = 'str' # TEXT
TIME = 'time'
Time = 'time' # TIME
TIMESTAMP = 'datetime'
Unicode = 'str' # String
UnicodeText = 'str' # TEXT
UUID = 'str | UUID'
Uuid = 'str' # CHAR(32)
VARBINARY = 'bytes'
VARCHAR = 'str' # String
# sa.dialects.mysql 导入
BIT = 'bool'
ENUM = 'Enum'
LONGBLOB = 'bytes'
LONGTEXT = 'str'
MEDIUMBLOB = 'bytes'
MEDIUMINT = 'int'
MEDIUMTEXT = 'str'
SET = 'list[str]'
TINYBLOB = 'bytes'
TINYINT = 'int'
TINYTEXT = 'str'
YEAR = 'int'
class GenModelPostgreSQLColumnType(StrEnum):
"""代码生成模型列类型(PostgreSQL)"""
# Python 类型映射
BIGINT = 'int'
BigInteger = 'int' # BIGINT
BINARY = 'bytes'
BLOB = 'bytes'
BOOLEAN = 'bool'
Boolean = 'bool' # BOOLEAN
CHAR = 'str'
CLOB = 'str'
DATE = 'date'
Date = 'date' # DATE
DATETIME = 'datetime'
DateTime = 'datetime' # TIMESTAMP WITHOUT TIME ZONE
DECIMAL = 'Decimal'
DOUBLE = 'float'
Double = 'float' # DOUBLE PRECISION
DOUBLE_PRECISION = 'float' # DOUBLE PRECISION
Enum = 'Enum' # Enum(name='enum')
FLOAT = 'float'
Float = 'float' # FLOAT
INT = 'int' # INTEGER
INTEGER = 'int'
Integer = 'int' # INTEGER
Interval = 'timedelta' # INTERVAL
JSON = 'dict'
LargeBinary = 'bytes' # BYTEA
NCHAR = 'str'
NUMERIC = 'Decimal'
Numeric = 'Decimal' # NUMERIC
NVARCHAR = 'str' # String
PickleType = 'bytes' # BYTEA
REAL = 'float'
SMALLINT = 'int'
SmallInteger = 'int' # SMALLINT
String = 'str' # String
TEXT = 'str'
Text = 'str' # TEXT
TIME = 'time' # TIME WITHOUT TIME ZONE
Time = 'time' # TIME WITHOUT TIME ZONE
TIMESTAMP = 'datetime' # TIMESTAMP WITHOUT TIME ZONE
Unicode = 'str' # String
UnicodeText = 'str' # TEXT
UUID = 'str | UUID'
Uuid = 'str'
VARBINARY = 'bytes'
VARCHAR = 'str' # String
# sa.dialects.postgresql 导入
ARRAY = 'list'
BIT = 'bool'
BYTEA = 'bytes'
CIDR = 'str'
CITEXT = 'str'
DATEMULTIRANGE = 'list[date]'
DATERANGE = 'tuple[date, date]'
DOMAIN = 'str'
ENUM = 'Enum'
HSTORE = 'dict'
INET = 'str'
INT4MULTIRANGE = 'list[int]'
INT4RANGE = 'tuple[int, int]'
INT8MULTIRANGE = 'list[int]'
INT8RANGE = 'tuple[int, int]'
INTERVAL = 'timedelta'
JSONB = 'dict'
JSONPATH = 'str'
MACADDR = 'str'
MACADDR8 = 'str'
MONEY = 'Decimal'
NUMMULTIRANGE = 'list[Decimal]'
NUMRANGE = 'tuple[Decimal, Decimal]'
OID = 'int'
REGCLASS = 'str'
REGCONFIG = 'str'
TSMULTIRANGE = 'list[datetime]'
TSQUERY = 'str'
TSRANGE = 'tuple[datetime, datetime]'
TSTZMULTIRANGE = 'list[datetime]'
TSTZRANGE = 'tuple[datetime, datetime]'
TSVECTOR = 'str'
+4 -4
View File
@@ -8,7 +8,7 @@ from typing import TYPE_CHECKING, Any, Generic, Sequence, TypeVar
from fastapi import Depends, Query from fastapi import Depends, Query
from fastapi_pagination import pagination_ctx from fastapi_pagination import pagination_ctx
from fastapi_pagination.bases import AbstractPage, AbstractParams, RawParams from fastapi_pagination.bases import AbstractPage, AbstractParams, RawParams
from fastapi_pagination.ext.sqlalchemy import paginate from fastapi_pagination.ext.sqlalchemy import apaginate
from fastapi_pagination.links.bases import create_links from fastapi_pagination.links.bases import create_links
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
@@ -24,7 +24,7 @@ class _CustomPageParams(BaseModel, AbstractParams):
"""自定义分页参数""" """自定义分页参数"""
page: int = Query(1, ge=1, description='页码') page: int = Query(1, ge=1, description='页码')
size: int = Query(20, gt=0, le=100, description='每页数量') size: int = Query(20, gt=0, le=200, description='每页数量')
def to_raw_params(self) -> RawParams: def to_raw_params(self) -> RawParams:
return RawParams( return RawParams(
@@ -63,8 +63,8 @@ class _CustomPage(_PageDetails, AbstractPage[T], Generic[T]):
def create( def create(
cls, cls,
items: list, items: list,
total: int,
params: _CustomPageParams, params: _CustomPageParams,
total: int = 0,
) -> _CustomPage[T]: ) -> _CustomPage[T]:
page = params.page page = params.page
size = params.size size = params.size
@@ -119,7 +119,7 @@ async def paging_data(db: AsyncSession, select: Select) -> dict[str, Any]:
:param select: SQL 查询语句 :param select: SQL 查询语句
:return: :return:
""" """
paginated_data: _CustomPage = await paginate(db, select) paginated_data: _CustomPage = await apaginate(db, select)
page_data = paginated_data.model_dump() page_data = paginated_data.model_dump()
return page_data return page_data
+4 -2
View File
@@ -40,7 +40,7 @@ class ResponseModel(BaseModel):
class ResponseSchemaModel(ResponseModel, Generic[SchemaT]): class ResponseSchemaModel(ResponseModel, Generic[SchemaT]):
""" """
包含返回数据 schema 的通用型统一返回模型仅适用于非分页接口 包含返回数据 schema 的通用型统一返回模型
示例:: 示例::
@@ -68,7 +68,9 @@ class ResponseBase:
@staticmethod @staticmethod
def __response( def __response(
*, res: CustomResponseCode | CustomResponse = None, data: Any | None = None *,
res: CustomResponseCode | CustomResponse,
data: Any | None,
) -> ResponseModel | ResponseSchemaModel: ) -> ResponseModel | ResponseSchemaModel:
""" """
请求返回通用方法 请求返回通用方法
+96 -97
View File
@@ -7,106 +7,105 @@ from pydantic import BaseModel, ConfigDict, EmailStr, Field, validate_email
from backend.core.conf import settings from backend.core.conf import settings
# 自定义验证错误信息 # 自定义验证错误信息,参考:
# 不包含验证预期内容(也就是输入内容),受支持的预期内容字段参考以下链接
# https://github.com/pydantic/pydantic-core/blob/a5cb7382643415b716b1a7a5392914e50f726528/tests/test_errors.py#L266 # https://github.com/pydantic/pydantic-core/blob/a5cb7382643415b716b1a7a5392914e50f726528/tests/test_errors.py#L266
# 替换预期内容字段方式,参考以下链接
# https://github.com/pydantic/pydantic/blob/caa78016433ec9b16a973f92f187a7b6bfde6cb5/docs/errors/errors.md?plain=1#L232 # https://github.com/pydantic/pydantic/blob/caa78016433ec9b16a973f92f187a7b6bfde6cb5/docs/errors/errors.md?plain=1#L232
CUSTOM_VALIDATION_ERROR_MESSAGES = { CUSTOM_VALIDATION_ERROR_MESSAGES = {
'arguments_type': '参数类型输入错误', 'no_such_attribute': "对象没有属性 '{attribute}'",
'assertion_error': '断言执行错误', 'json_invalid': '无效的 JSON: {error}',
'bool_parsing': '布尔值输入解析错误', 'json_type': 'JSON 输入应为字符串、字节或字节数组',
'bool_type': '布尔值类型输入错误', 'recursion_loop': '递归错误 - 检测到循环引用',
'bytes_too_long': '字节长度输入过长', 'model_type': '输入应为有效的字典或 {class_name} 的实例',
'bytes_too_short': '字节长度输入过短', 'model_attributes_type': '输入应为有效的字典或可提取字段的对象',
'bytes_type': '字节类型输入错误', 'dataclass_exact_type': '输入应为 {class_name} 的实例',
'callable_type': '可调用对象类型输入错误', 'dataclass_type': '输入应为字典或 {class_name} 的实例',
'dataclass_exact_type': '数据类实例类型输入错误', 'missing': '字段为必填项',
'dataclass_type': '数据类类型输入错误', 'frozen_field': '字段已冻结',
'date_from_datetime_inexact': '日期分量输入非零', 'frozen_instance': '实例已冻结',
'date_from_datetime_parsing': '日期输入解析错误', 'extra_forbidden': '不允许额外的输入',
'date_future': '日期输入非将来时', 'invalid_key': '键应为字符串',
'date_parsing': '日期输入验证错误', 'get_attribute_error': '提取属性时出错: {error}',
'date_past': '日期输入非过去时', 'none_required': '输入应为 None',
'date_type': '日期类型输入错误', 'enum': '输入应为 {expected}',
'datetime_future': '日期时间输入非将来时间', 'greater_than': '输入应大于 {gt}',
'datetime_object_invalid': '日期时间输入对象无效', 'greater_than_equal': '输入应大于或等于 {ge}',
'datetime_parsing': '日期时间输入解析错误', 'less_than': '输入应小于 {lt}',
'datetime_past': '日期时间输入非过去时间', 'less_than_equal': '输入应小于或等于 {le}',
'datetime_type': '日期时间类型输入错误', 'finite_number': '输入应为有限数字',
'decimal_max_digits': '小数位数输入过多', 'too_short': '{field_type} 在验证后应至少有 {min_length} 个项目,而不是 {actual_length}',
'decimal_max_places': '小数位数输入错误', 'too_long': '{field_type} 在验证后最多应有 {max_length} 个项目,而不是 {actual_length}',
'decimal_parsing': '小数输入解析错误', 'string_type': '输入应为有效的字符串',
'decimal_type': '小数类型输入错误', 'string_sub_type': '输入应为字符串,而不是 str 子类的实例',
'decimal_whole_digits': '小数位数输入错误', 'string_unicode': '输入应为有效的字符串,无法将原始数据解析为 Unicode 字符串',
'dict_type': '字典类型输入错误', 'string_pattern_mismatch': "字符串应匹配模式 '{pattern}'",
'enum': '枚举成员输入错误,允许 {expected}', 'string_too_short': '字符串应至少有 {min_length} 个字符',
'extra_forbidden': '禁止额外字段输入', 'string_too_long': '字符串最多应有 {max_length} 个字符',
'finite_number': '有限值输入错误', 'dict_type': '输入应为有效的字典',
'float_parsing': '浮点数输入解析错误', 'mapping_type': '输入应为有效的映射,错误: {error}',
'float_type': '浮点数类型输入错误', 'iterable_type': '输入应为可迭代对象',
'frozen_field': '冻结字段输入错误', 'iteration_error': '迭代对象时出错,错误: {error}',
'frozen_instance': '冻结实例禁止修改', 'list_type': '输入应为有效的列表',
'frozen_set_type': '冻结类型禁止输入', 'tuple_type': '输入应为有效的元组',
'get_attribute_error': '获取属性错误', 'set_type': '输入应为有效的集合',
'greater_than': '输入值过大', 'bool_type': '输入应为有效的布尔',
'greater_than_equal': '输入值过大或相等', 'bool_parsing': '输入应为有效的布尔值,无法解释输入',
'int_from_float': '整数类型输入错误', 'int_type': '输入应为有效的整数',
'int_parsing': '整数输入解析错误', 'int_parsing': '输入应为有效的整数,无法将字符串解析为整数',
'int_parsing_size': '整数输入解析长度错误', 'int_parsing_size': '无法将输入字符串解析为整数,超出最大大小',
'int_type': '整数类型输入错误', 'int_from_float': '输入应为有效的整数,得到一个带有小数部分的数字',
'invalid_key': '输入无效键值', 'multiple_of': '输入应为 {multiple_of} 的倍数',
'is_instance_of': '类型实例输入错误', 'float_type': '输入应为有效的数字',
'is_subclass_of': '类型子类输入错误', 'float_parsing': '输入应为有效的数字,无法将字符串解析为数字',
'iterable_type': '可迭代类型输入错误', 'bytes_type': '输入应为有效的字节',
'iteration_error': '迭代值输入错误', 'bytes_too_short': '数据应至少有 {min_length} 个字节',
'json_invalid': 'JSON 字符串输入错误', 'bytes_too_long': '数据最多应有 {max_length} 个字节',
'json_type': 'JSON 类型输入错误', 'value_error': '值错误,{error}',
'less_than': '输入值过小', 'assertion_error': '断言失败,{error}',
'less_than_equal': '输入值过小或相等', 'literal_error': '输入应为 {expected}',
'list_type': '列表类型输入错误', 'date_type': '输入应为有效的日期',
'literal_error': '字面值输入错误', 'date_parsing': '输入应为 YYYY-MM-DD 格式的有效日期,{error}',
'mapping_type': '映射类型输入错误', 'date_from_datetime_parsing': '输入应为有效的日期或日期时间,{error}',
'missing': '缺少必填字段', 'date_from_datetime_inexact': '提供给日期的日期时间应具有零时间 - 例如为精确日期',
'missing_argument': '缺少参数', 'date_past': '日期应为过去的时间',
'missing_keyword_only_argument': '缺少关键字参数', 'date_future': '日期应为未来的时间',
'missing_positional_only_argument': '缺少位置参数', 'time_type': '输入应为有效的时间',
'model_attributes_type': '模型属性类型输入错误', 'time_parsing': '输入应为有效的时间格式,{error}',
'model_type': '模型实例输入错误', 'datetime_type': '输入应为有效的日期时间',
'multiple_argument_values': '参数值输入过多', 'datetime_parsing': '输入应为有效的日期时间,{error}',
'multiple_of': '输入值非倍数', 'datetime_object_invalid': '无效的日期时间对象,得到 {error}',
'no_such_attribute': '分配无效属性值', 'datetime_past': '输入应为过去的时间',
'none_required': '输入值必须为 None', 'datetime_future': '输入应为未来的时间',
'recursion_loop': '输入循环赋值', 'timezone_naive': '输入不应包含时区信息',
'set_type': '集合类型输入错误', 'timezone_aware': '输入应包含时区信息',
'string_pattern_mismatch': '字符串约束模式输入不匹配', 'timezone_offset': '需要时区偏移为 {tz_expected},实际得到 {tz_actual}',
'string_sub_type': '字符串子类型(非严格实例)输入错误', 'time_delta_type': '输入应为有效的时间差',
'string_too_long': '字符串输入过长', 'time_delta_parsing': '输入应为有效的时间差,{error}',
'string_too_short': '字符串输入过短', 'frozen_set_type': '输入应为有效的冻结集合',
'string_type': '字符串类型输入错误', 'is_instance_of': '输入应为 {class} 的实例',
'string_unicode': '字符串输入非 Unicode', 'is_subclass_of': '输入应为 {class} 的子类',
'time_delta_parsing': '时间差输入解析错误', 'callable_type': '输入应为可调用对象',
'time_delta_type': '时间差类型输入错误', 'union_tag_invalid': "使用 {discriminator} 找到的输入标签 '{tag}' 与任何预期标签不匹配: {expected_tags}",
'time_parsing': '时间输入解析错误', 'union_tag_not_found': '无法使用区分器 {discriminator} 提取标签',
'time_type': '时间类型输入错误', 'arguments_type': '参数必须是元组、列表或字典',
'timezone_aware': '缺少时区输入信息', 'missing_argument': '缺少必需参数',
'timezone_naive': '禁止时区输入信息', 'unexpected_keyword_argument': '意外的关键字参数',
'too_long': '输入过长', 'missing_keyword_only_argument': '缺少必需的关键字专用参数',
'too_short': '输入过短', 'unexpected_positional_argument': '意外的位置参数',
'tuple_type': '元组类型输入错误', 'missing_positional_only_argument': '缺少必需的位置专用参数',
'unexpected_keyword_argument': '输入意外关键字参数', 'multiple_argument_values': '为参数提供了多个值',
'unexpected_positional_argument': '输入意外位置参数', 'url_type': 'URL 输入应为字符串或 URL',
'union_tag_invalid': '联合类型字面值输入错误', 'url_parsing': '输入应为有效的 URL{error}',
'union_tag_not_found': '联合类型参数输入未找到', 'url_syntax_violation': '输入违反了严格的 URL 语法规则,{error}',
'url_parsing': 'URL 输入解析错误', 'url_too_long': 'URL 最多应有 {max_length} 个字符',
'url_scheme': 'URL 输入方案错误', 'url_scheme': 'URL 方案应为 {expected_schemes}',
'url_syntax_violation': 'URL 输入语法错误', 'uuid_type': 'UUID 输入应为字符串、字节或 UUID 对象',
'url_too_long': 'URL 输入过长', 'uuid_parsing': '输入应为有效的 UUID{error}',
'url_type': 'URL 类型输入错误', 'uuid_version': '预期 UUID 版本为 {expected_version}',
'uuid_parsing': 'UUID 输入解析错误', 'decimal_type': '十进制输入应为整数、浮点数、字符串或 Decimal 对象',
'uuid_type': 'UUID 类型输入错误', 'decimal_parsing': '输入应为有效的十进制数',
'uuid_version': 'UUID 版本类型输入错误', 'decimal_max_digits': '十进制输入总共应不超过 {max_digits} 位数字',
'value_error': '值输入错误', 'decimal_max_places': '十进制输入应不超过 {decimal_places} 位小数',
'decimal_whole_digits': '十进制输入在小数点前应不超过 {whole_digits} 位数字',
} }
CustomPhoneNumber = Annotated[str, Field(pattern=r'^1[3-9]\d{9}$')] CustomPhoneNumber = Annotated[str, Field(pattern=r'^1[3-9]\d{9}$')]
+51 -32
View File
@@ -4,7 +4,9 @@ from typing import TYPE_CHECKING
from fastapi import Request from fastapi import Request
from sqlalchemy import ColumnElement, and_, or_ from sqlalchemy import ColumnElement, and_, or_
from sqlalchemy.ext.asyncio import AsyncSession
from backend.app.admin.crud.crud_data_scope import data_scope_dao
from backend.common.enums import RoleDataRuleExpressionType, RoleDataRuleOperatorType from backend.common.enums import RoleDataRuleExpressionType, RoleDataRuleOperatorType
from backend.common.exception import errors from backend.common.exception import errors
from backend.common.exception.errors import ServerError from backend.common.exception.errors import ServerError
@@ -12,7 +14,7 @@ from backend.core.conf import settings
from backend.utils.import_parse import dynamic_import_data_model from backend.utils.import_parse import dynamic_import_data_model
if TYPE_CHECKING: if TYPE_CHECKING:
from backend.app.admin.schema.data_rule import GetDataRuleDetail from backend.app.admin.model import DataRule
class RequestPermission: class RequestPermission:
@@ -47,33 +49,48 @@ class RequestPermission:
request.state.permission = self.value request.state.permission = self.value
def filter_data_permission(request: Request) -> ColumnElement[bool]: async def filter_data_permission(db: AsyncSession, request: Request) -> ColumnElement[bool]:
""" """
过滤数据权限控制用户可见数据范围 过滤数据权限控制用户可见数据范围
使用场景 使用场景
- 用户登录前台后控制其能看到哪些数据 - 控制用户能看到哪些数据
- 根据用户角色和规则过滤数据访问权限
:param db: 数据库会话
:param request: FastAPI 请求对象 :param request: FastAPI 请求对象
:return: :return:
""" """
# 获取用户角色和规则 # 获取用户角色和数据范围
data_rules = [] data_scopes = []
for role in request.user.roles: for role in request.user.roles:
data_rules.extend(role.rules) for scope in role.scopes:
user_data_rules: list[GetDataRuleDetail] = list(dict.fromkeys(data_rules)) if scope.status:
data_scopes.append(scope)
# 超级管理员和无规则用户不做过滤 # 超级管理员和无规则用户不做过滤
if request.user.is_superuser or not user_data_rules: if request.user.is_superuser or not data_scopes:
return or_(1 == 1) return or_(1 == 1)
# 获取数据范围规则
data_rule_list: list[DataRule] = []
for data_scope in data_scopes:
data_scope_with_relation = await data_scope_dao.get_with_relation(db, data_scope.id)
data_rule_list.extend(data_scope_with_relation.rules)
# 去重
seen_data_rule_ids = set()
new_data_rule_list = []
for rule in data_rule_list:
if rule.id not in seen_data_rule_ids:
seen_data_rule_ids.add(rule.id)
new_data_rule_list.append(rule)
where_and_list = [] where_and_list = []
where_or_list = [] where_or_list = []
for rule in user_data_rules: for data_rule in new_data_rule_list:
# 验证规则模型 # 验证规则模型
rule_model = rule.model rule_model = data_rule.model
if rule_model not in settings.DATA_PERMISSION_MODELS: if rule_model not in settings.DATA_PERMISSION_MODELS:
raise errors.NotFoundError(msg='数据规则模型不存在') raise errors.NotFoundError(msg='数据规则模型不存在')
model_ins = dynamic_import_data_model(settings.DATA_PERMISSION_MODELS[rule_model]) model_ins = dynamic_import_data_model(settings.DATA_PERMISSION_MODELS[rule_model])
@@ -82,38 +99,40 @@ def filter_data_permission(request: Request) -> ColumnElement[bool]:
model_columns = [ model_columns = [
key for key in model_ins.__table__.columns.keys() if key not in settings.DATA_PERMISSION_COLUMN_EXCLUDE key for key in model_ins.__table__.columns.keys() if key not in settings.DATA_PERMISSION_COLUMN_EXCLUDE
] ]
column = rule.column column = data_rule.column
if column not in model_columns: if column not in model_columns:
raise errors.NotFoundError(msg='数据规则模型列不存在') raise errors.NotFoundError(msg='数据规则模型列不存在')
# 构建过滤条件 # 构建过滤条件
column_obj = getattr(model_ins, column) column_obj = getattr(model_ins, column)
rule_expression = rule.expression rule_expression = data_rule.expression
condition = None condition = None
if rule_expression == RoleDataRuleExpressionType.eq: match rule_expression:
condition = column_obj == rule.value case RoleDataRuleExpressionType.eq:
elif rule_expression == RoleDataRuleExpressionType.ne: condition = column_obj == data_rule.value
condition = column_obj != rule.value case RoleDataRuleExpressionType.ne:
elif rule_expression == RoleDataRuleExpressionType.gt: condition = column_obj != data_rule.value
condition = column_obj > rule.value case RoleDataRuleExpressionType.gt:
elif rule_expression == RoleDataRuleExpressionType.ge: condition = column_obj > data_rule.value
condition = column_obj >= rule.value case RoleDataRuleExpressionType.ge:
elif rule_expression == RoleDataRuleExpressionType.lt: condition = column_obj >= data_rule.value
condition = column_obj < rule.value case RoleDataRuleExpressionType.lt:
elif rule_expression == RoleDataRuleExpressionType.le: condition = column_obj < data_rule.value
condition = column_obj <= rule.value case RoleDataRuleExpressionType.le:
elif rule_expression == RoleDataRuleExpressionType.in_: condition = column_obj <= data_rule.value
values = rule.value.split(',') if isinstance(rule.value, str) else rule.value case RoleDataRuleExpressionType.in_:
values = data_rule.value.split(',') if isinstance(data_rule.value, str) else data_rule.value
condition = column_obj.in_(values) condition = column_obj.in_(values)
elif rule.expression == RoleDataRuleExpressionType.not_in: case RoleDataRuleExpressionType.not_in:
values = rule.value.split(',') if isinstance(rule.value, str) else rule.value values = data_rule.value.split(',') if isinstance(data_rule.value, str) else data_rule.value
condition = ~column_obj.in_(values) condition = column_obj.not_in(values)
# 根据运算符添加到对应列表 # 根据运算符添加到对应列表
if condition is not None: if condition is not None:
if rule.operator == RoleDataRuleOperatorType.AND: match data_rule.operator:
case RoleDataRuleOperatorType.AND:
where_and_list.append(condition) where_and_list.append(condition)
elif rule.operator == RoleDataRuleOperatorType.OR: case RoleDataRuleOperatorType.OR:
where_or_list.append(condition) where_or_list.append(condition)
# 组合所有条件 # 组合所有条件
+10 -1
View File
@@ -3,10 +3,12 @@
from fastapi import Depends, Request from fastapi import Depends, Request
from backend.common.enums import MethodType, StatusType from backend.common.enums import MethodType, StatusType
from backend.common.exception import errors
from backend.common.exception.errors import AuthorizationError, TokenError from backend.common.exception.errors import AuthorizationError, TokenError
from backend.common.log import log
from backend.common.security.jwt import DependsJwtAuth from backend.common.security.jwt import DependsJwtAuth
from backend.core.conf import settings from backend.core.conf import settings
from backend.plugin.casbin.utils.rbac import casbin_verify from backend.utils.import_parse import import_module_cached
async def rbac_verify(request: Request, _token: str = DependsJwtAuth) -> None: async def rbac_verify(request: Request, _token: str = DependsJwtAuth) -> None:
@@ -67,6 +69,13 @@ async def rbac_verify(request: Request, _token: str = DependsJwtAuth) -> None:
if path_auth_perm not in allow_perms: if path_auth_perm not in allow_perms:
raise AuthorizationError raise AuthorizationError
else: else:
try:
casbin_rbac = import_module_cached('backend.plugin.casbin.utils.rbac')
casbin_verify = getattr(casbin_rbac, 'casbin_verify')
except (ImportError, AttributeError) as e:
log.error(f'正在通过 casbin 执行 RBAC 权限校验,但此插件不存在: {e}')
raise errors.ServerError(msg='权限校验失败,请联系系统管理员')
await casbin_verify(request) await casbin_verify(request)
+3 -2
View File
@@ -77,7 +77,7 @@ class Settings(BaseSettings):
JWT_USER_REDIS_EXPIRE_SECONDS: int = 60 * 60 * 24 * 7 # 7 天 JWT_USER_REDIS_EXPIRE_SECONDS: int = 60 * 60 * 24 * 7 # 7 天
# RBAC # RBAC
RBAC_ROLE_MENU_MODE: bool = False RBAC_ROLE_MENU_MODE: bool = True
RBAC_ROLE_MENU_EXCLUDE: list[str] = [ RBAC_ROLE_MENU_EXCLUDE: list[str] = [
'sys:monitor:redis', 'sys:monitor:redis',
'sys:monitor:server', 'sys:monitor:server',
@@ -89,11 +89,12 @@ class Settings(BaseSettings):
# 数据权限配置 # 数据权限配置
DATA_PERMISSION_MODELS: dict[str, str] = { # 允许进行数据过滤的 SQLA 模型,它必须以模块字符串的方式定义 DATA_PERMISSION_MODELS: dict[str, str] = { # 允许进行数据过滤的 SQLA 模型,它必须以模块字符串的方式定义
'Api': 'backend.plugin.casbin.model.Api', '部门': 'backend.app.admin.model.Dept',
} }
DATA_PERMISSION_COLUMN_EXCLUDE: list[str] = [ # 排除允许进行数据过滤的 SQLA 模型列 DATA_PERMISSION_COLUMN_EXCLUDE: list[str] = [ # 排除允许进行数据过滤的 SQLA 模型列
'id', 'id',
'sort', 'sort',
'del_flag',
'created_time', 'created_time',
'updated_time', 'updated_time',
] ]
-3
View File
@@ -17,9 +17,6 @@ STATIC_DIR = BASE_PATH / 'static'
# 上传文件目录 # 上传文件目录
UPLOAD_DIR = STATIC_DIR / 'upload' UPLOAD_DIR = STATIC_DIR / 'upload'
# jinja2 模版文件路径
JINJA2_TEMPLATE_DIR = BASE_PATH / 'templates'
# 插件目录 # 插件目录
PLUGIN_DIR = BASE_PATH / 'plugin' PLUGIN_DIR = BASE_PATH / 'plugin'
+3 -7
View File
@@ -23,7 +23,7 @@ from backend.database.redis import redis_client
from backend.middleware.jwt_auth_middleware import JwtAuthMiddleware from backend.middleware.jwt_auth_middleware import JwtAuthMiddleware
from backend.middleware.opera_log_middleware import OperaLogMiddleware from backend.middleware.opera_log_middleware import OperaLogMiddleware
from backend.middleware.state_middleware import StateMiddleware from backend.middleware.state_middleware import StateMiddleware
from backend.plugin.tools import plugin_router_inject from backend.plugin.tools import build_final_router
from backend.utils.demo_site import demo_site from backend.utils.demo_site import demo_site
from backend.utils.health_check import ensure_unique_route_names, http_limit_callback from backend.utils.health_check import ensure_unique_route_names, http_limit_callback
from backend.utils.openapi import simplify_operation_ids from backend.utils.openapi import simplify_operation_ids
@@ -157,12 +157,8 @@ def register_router(app: FastAPI) -> None:
""" """
dependencies = [Depends(demo_site)] if settings.DEMO_MODE else None dependencies = [Depends(demo_site)] if settings.DEMO_MODE else None
# 插件路由 # API
plugin_router_inject() router = build_final_router()
# 系统路由(必须在插件路由注入后导入)
from backend.app.router import router
app.include_router(router, dependencies=dependencies) app.include_router(router, dependencies=dependencies)
# Extra # Extra
+1 -1
View File
@@ -71,7 +71,7 @@ class JwtAuthMiddleware(AuthenticationBackend):
except TokenError as exc: except TokenError as exc:
raise _AuthenticationError(code=exc.code, msg=exc.detail, headers=exc.headers) raise _AuthenticationError(code=exc.code, msg=exc.detail, headers=exc.headers)
except Exception as e: except Exception as e:
log.error(f'JWT 授权异常:{e}') log.exception(f'JWT 授权异常:{e}')
raise _AuthenticationError(code=getattr(e, 'code', 500), msg=getattr(e, 'msg', 'Internal Server Error')) raise _AuthenticationError(code=getattr(e, 'code', 500), msg=getattr(e, 'msg', 'Internal Server Error'))
# 请注意,此返回使用非标准模式,所以在认证通过时,将丢失某些标准特性 # 请注意,此返回使用非标准模式,所以在认证通过时,将丢失某些标准特性
-91
View File
@@ -1,91 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from typing import Annotated
from fastapi import APIRouter, Depends, Path, Query, Request
from backend.common.pagination import DependsPagination, PageData, paging_data
from backend.common.response.response_schema import ResponseModel, ResponseSchemaModel, 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 import CurrentSession
from backend.plugin.casbin.schema.api import CreateApiParam, GetApiDetail, UpdateApiParam
from backend.plugin.casbin.service.api_service import api_service
router = APIRouter()
@router.get('/all', summary='获取所有接口', dependencies=[DependsJwtAuth])
async def get_all_apis() -> ResponseSchemaModel[list[GetApiDetail]]:
data = await api_service.get_all()
return response_base.success(data=data)
@router.get('/{pk}', summary='获取接口详情', dependencies=[DependsJwtAuth])
async def get_api(pk: Annotated[int, Path(description='API ID')]) -> ResponseSchemaModel[GetApiDetail]:
api = await api_service.get(pk=pk)
return response_base.success(data=api)
@router.get(
'',
summary='分页获取所有接口',
dependencies=[
DependsJwtAuth,
DependsPagination,
],
)
async def get_pagination_apis(
request: Request,
db: CurrentSession,
name: Annotated[str | None, Query(description='API 名称')] = None,
method: Annotated[str | None, Query(description='请求方法')] = None,
path: Annotated[str | None, Query(description='API 路径')] = None,
) -> ResponseSchemaModel[PageData[GetApiDetail]]:
api_select = await api_service.get_select(request=request, name=name, method=method, path=path)
page_data = await paging_data(db, api_select)
return response_base.success(data=page_data)
@router.post(
'',
summary='创建接口',
dependencies=[
Depends(RequestPermission('sys:api:add')),
DependsRBAC,
],
)
async def create_api(obj: CreateApiParam) -> ResponseModel:
await api_service.create(obj=obj)
return response_base.success()
@router.put(
'/{pk}',
summary='更新接口',
dependencies=[
Depends(RequestPermission('sys:api:edit')),
DependsRBAC,
],
)
async def update_api(pk: Annotated[int, Path(description='API ID')], obj: UpdateApiParam) -> ResponseModel:
count = await api_service.update(pk=pk, obj=obj)
if count > 0:
return response_base.success()
return response_base.fail()
@router.delete(
'',
summary='批量删除接口',
dependencies=[
Depends(RequestPermission('sys:api:del')),
DependsRBAC,
],
)
async def delete_api(pk: Annotated[list[int], Query(description='API ID 列表')]) -> ResponseModel:
count = await api_service.delete(pk=pk)
if count > 0:
return response_base.success()
return response_base.fail()
-218
View File
@@ -1,218 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from typing import Annotated
from uuid import UUID
from fastapi import APIRouter, Depends, Query
from backend.common.pagination import DependsPagination, PageData, paging_data
from backend.common.response.response_schema import ResponseModel, ResponseSchemaModel, 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 import CurrentSession
from backend.plugin.casbin.schema.casbin_rule import (
CreateGroupParam,
CreatePolicyParam,
DeleteAllPoliciesParam,
DeleteGroupParam,
DeletePolicyParam,
GetPolicyDetail,
UpdatePoliciesParam,
UpdatePolicyParam,
)
from backend.plugin.casbin.service.casbin_service import casbin_service
router = APIRouter()
@router.get(
'',
summary='分页获取所有权限策略',
dependencies=[
DependsJwtAuth,
DependsPagination,
],
)
async def get_pagination_casbin(
db: CurrentSession,
ptype: Annotated[str | None, Query(description='策略类型:p / g')] = None,
sub: Annotated[str | None, Query(description='用户 UUID / 角色 ID')] = None,
) -> ResponseSchemaModel[PageData[GetPolicyDetail]]:
casbin_select = await casbin_service.get_casbin_list(ptype=ptype, sub=sub)
page_data = await paging_data(db, casbin_select)
return response_base.success(data=page_data)
@router.get('/policies', summary='获取所有 P 权限策略', dependencies=[DependsJwtAuth])
async def get_all_policies(
role: Annotated[int | None, Query(description='角色 ID')] = None,
) -> ResponseSchemaModel[list[list[str]]]:
policies = await casbin_service.get_policy_list(role=role)
return response_base.success(data=policies)
@router.post(
'/policy',
summary='添加 P 权限策略',
dependencies=[
Depends(RequestPermission('casbin:p:add')),
DependsRBAC,
],
)
async def create_policy(p: CreatePolicyParam) -> ResponseSchemaModel[bool]:
data = await casbin_service.create_policy(p=p)
return response_base.success(data=data)
@router.post(
'/policies',
summary='添加多组 P 权限策略',
dependencies=[
Depends(RequestPermission('casbin:p:group:add')),
DependsRBAC,
],
)
async def create_policies(ps: list[CreatePolicyParam]) -> ResponseSchemaModel[bool]:
data = await casbin_service.create_policies(ps=ps)
return response_base.success(data=data)
@router.put(
'/policy',
summary='更新 P 权限策略',
dependencies=[
Depends(RequestPermission('casbin:p:edit')),
DependsRBAC,
],
)
async def update_policy(obj: UpdatePolicyParam) -> ResponseSchemaModel[bool]:
data = await casbin_service.update_policy(obj=obj)
return response_base.success(data=data)
@router.put(
'/policies',
summary='更新多组 P 权限策略',
dependencies=[
Depends(RequestPermission('casbin:p:group:edit')),
DependsRBAC,
],
)
async def update_policies(obj: UpdatePoliciesParam) -> ResponseSchemaModel[bool]:
data = await casbin_service.update_policies(obj=obj)
return response_base.success(data=data)
@router.delete(
'/policy',
summary='删除 P 权限策略',
dependencies=[
Depends(RequestPermission('casbin:p:del')),
DependsRBAC,
],
)
async def delete_policy(p: DeletePolicyParam) -> ResponseSchemaModel[bool]:
data = await casbin_service.delete_policy(p=p)
return response_base.success(data=data)
@router.delete(
'/policies',
summary='删除多组 P 权限策略',
dependencies=[
Depends(RequestPermission('casbin:p:group:del')),
DependsRBAC,
],
)
async def delete_policies(ps: list[DeletePolicyParam]) -> ResponseSchemaModel[bool]:
data = await casbin_service.delete_policies(ps=ps)
return response_base.success(data=data)
@router.delete(
'/policies/all',
summary='删除所有 P 权限策略',
dependencies=[
Depends(RequestPermission('casbin:p:empty')),
DependsRBAC,
],
)
async def delete_all_policies(sub: DeleteAllPoliciesParam) -> ResponseModel:
count = await casbin_service.delete_all_policies(sub=sub)
if count > 0:
return response_base.success()
return response_base.fail()
@router.get('/groups', summary='获取所有 G 权限策略', dependencies=[DependsJwtAuth])
async def get_all_groups() -> ResponseSchemaModel[list[list[str]]]:
data = await casbin_service.get_group_list()
return response_base.success(data=data)
@router.post(
'/group',
summary='添加 G 权限策略',
dependencies=[
Depends(RequestPermission('casbin:g:add')),
DependsRBAC,
],
)
async def create_group(g: CreateGroupParam) -> ResponseSchemaModel[bool]:
data = await casbin_service.create_group(g=g)
return response_base.success(data=data)
@router.post(
'/groups',
summary='添加多组 G 权限策略',
dependencies=[
Depends(RequestPermission('casbin:g:group:add')),
DependsRBAC,
],
)
async def create_groups(gs: list[CreateGroupParam]) -> ResponseSchemaModel[bool]:
data = await casbin_service.create_groups(gs=gs)
return response_base.success(data=data)
@router.delete(
'/group',
summary='删除 G 权限策略',
dependencies=[
Depends(RequestPermission('casbin:g:del')),
DependsRBAC,
],
)
async def delete_group(g: DeleteGroupParam) -> ResponseSchemaModel[bool]:
data = await casbin_service.delete_group(g=g)
return response_base.success(data=data)
@router.delete(
'/groups',
summary='删除多组 G 权限策略',
dependencies=[
Depends(RequestPermission('casbin:g:group:del')),
DependsRBAC,
],
)
async def delete_groups(gs: list[DeleteGroupParam]) -> ResponseSchemaModel[bool]:
data = await casbin_service.delete_groups(gs=gs)
return response_base.success(data=data)
@router.delete(
'/groups/all',
summary='删除所有 G 权限策略',
dependencies=[
Depends(RequestPermission('casbin:g:empty')),
DependsRBAC,
],
)
async def delete_all_groups(uuid: Annotated[UUID, Query()]) -> ResponseModel:
count = await casbin_service.delete_all_groups(uuid=uuid)
if count > 0:
return response_base.success()
return response_base.fail()
-26
View File
@@ -1,26 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from functools import lru_cache
from pydantic_settings import BaseSettings
from backend.core.conf import settings
class CasbinSettings(BaseSettings):
"""Casbin 配置"""
# RBAC
RBAC_CASBIN_EXCLUDE: set[tuple[str, str]] = {
('POST', f'{settings.FASTAPI_API_V1_PATH}/auth/logout'),
('POST', f'{settings.FASTAPI_API_V1_PATH}/auth/token/new'),
}
@lru_cache
def get_casbin_settings() -> CasbinSettings:
"""获取 Casbin 配置"""
return CasbinSettings()
casbin_settings = get_casbin_settings()
-99
View File
@@ -1,99 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from typing import Sequence
from fastapi import Request
from sqlalchemy import Select
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy_crud_plus import CRUDPlus
from backend.common.security.permission import filter_data_permission
from backend.plugin.casbin.model import Api
from backend.plugin.casbin.schema.api import CreateApiParam, UpdateApiParam
class CRUDApi(CRUDPlus[Api]):
"""API 数据库操作类"""
async def get(self, db: AsyncSession, pk: int) -> Api | None:
"""
获取 API
:param db: 数据库会话
:param pk: API ID
:return:
"""
return await self.select_model(db, pk)
async def get_list(self, request: Request, name: str = None, method: str = None, path: str = None) -> Select:
"""
获取 API 列表
:param request: FastAPI 请求对象
:param name: API 名称
:param method: 请求方法
:param path: API 路径
:return:
"""
filters = {}
if name is not None:
filters.update(name__like=f'%{name}%')
if method is not None:
filters.update(method=method)
if path is not None:
filters.update(path__like=f'%{path}%')
stmt = await self.select_order('created_time', 'desc', **filters)
return stmt.where(filter_data_permission(request))
async def get_all(self, db: AsyncSession) -> Sequence[Api]:
"""
获取所有 API
:param db: 数据库会话
:return:
"""
return await self.select_models(db)
async def get_by_name(self, db: AsyncSession, name: str) -> Api | None:
"""
通过名称获取 API
:param db: 数据库会话
:param name: API 名称
:return:
"""
return await self.select_model_by_column(db, name=name)
async def create(self, db: AsyncSession, obj: CreateApiParam) -> None:
"""
创建 API
:param db: 数据库会话
:param obj: 创建 API 参数
:return:
"""
await self.create_model(db, obj)
async def update(self, db: AsyncSession, pk: int, obj: UpdateApiParam) -> int:
"""
更新 API
:param db: 数据库会话
:param pk: API ID
:param obj: 更新 API 参数
:return:
"""
return await self.update_model(db, pk, obj)
async def delete(self, db: AsyncSession, pk: list[int]) -> int:
"""
删除 API
:param db: 数据库会话
:param pk: API ID 列表
:return:
"""
return await self.delete_model_by_column(db, allow_multiple=True, id__in=pk)
api_dao: CRUDApi = CRUDApi(Api)
-51
View File
@@ -1,51 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from uuid import UUID
from sqlalchemy import Select
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy_crud_plus import CRUDPlus
from backend.plugin.casbin.model import CasbinRule
from backend.plugin.casbin.schema.casbin_rule import DeleteAllPoliciesParam
class CRUDCasbin(CRUDPlus[CasbinRule]):
"""Casbin 规则数据库操作类"""
async def get_list(self, ptype: str, sub: str) -> Select:
"""
获取策略列表
:param ptype: 策略类型
:param sub: 用户 UUID / 角色 ID
:return:
"""
return await self.select_order('id', 'desc', ptype=ptype, v0__like=f'%{sub}%')
async def delete_policies_by_sub(self, db: AsyncSession, sub: DeleteAllPoliciesParam) -> int:
"""
删除角色所有 P 策略
:param db: 数据库会话
:param sub: 删除所有 P 策略参数
:return:
"""
filters = [sub.role]
if sub.uuid:
filters.append(sub.uuid)
return await self.delete_model_by_column(db, allow_multiple=True, v0__mor={'eq': filters})
async def delete_groups_by_uuid(self, db: AsyncSession, uuid: UUID) -> int:
"""
删除用户所有 G 策略
:param db: 数据库会话
:param uuid: 用户 UUID
:return:
"""
return await self.delete_model_by_column(db, allow_multiple=True, v0=str(uuid))
casbin_dao: CRUDCasbin = CRUDCasbin(CasbinRule)
-4
View File
@@ -1,4 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from backend.plugin.casbin.model.api import Api
from backend.plugin.casbin.model.casbin_rule import CasbinRule
-20
View File
@@ -1,20 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from sqlalchemy import String
from sqlalchemy.dialects.mysql import LONGTEXT
from sqlalchemy.dialects.postgresql import TEXT
from sqlalchemy.orm import Mapped, mapped_column
from backend.common.model import Base, id_key
class Api(Base):
"""API 表"""
__tablename__ = 'sys_api'
id: Mapped[id_key] = mapped_column(init=False)
name: Mapped[str] = mapped_column(String(50), unique=True, comment='API 名称')
method: Mapped[str] = mapped_column(String(16), comment='请求方法')
path: Mapped[str] = mapped_column(String(500), comment='API 路径')
remark: Mapped[str | None] = mapped_column(LONGTEXT().with_variant(TEXT, 'postgresql'), comment='备注')
@@ -1,34 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from sqlalchemy import String
from sqlalchemy.dialects.mysql import LONGTEXT
from sqlalchemy.dialects.postgresql import TEXT
from sqlalchemy.orm import Mapped, mapped_column
from backend.common.model import MappedBase, id_key
class CasbinRule(MappedBase):
"""Casbin 规则表"""
__tablename__ = 'sys_casbin_rule'
id: Mapped[id_key]
ptype: Mapped[str] = mapped_column(String(255), comment='策略类型: p / g')
v0: Mapped[str] = mapped_column(String(255), comment='用户 UUID / 角色 ID')
v1: Mapped[str] = mapped_column(LONGTEXT().with_variant(TEXT, 'postgresql'), comment='API 路径 / 角色名称')
v2: Mapped[str | None] = mapped_column(String(255), comment='请求方法')
v3: Mapped[str | None] = mapped_column(String(255), comment='预留字段')
v4: Mapped[str | None] = mapped_column(String(255), comment='预留字段')
v5: Mapped[str | None] = mapped_column(String(255), comment='预留字段')
def __str__(self) -> str:
arr = [self.ptype]
for v in (self.v0, self.v1, self.v2, self.v3, self.v4, self.v5):
if v is None:
break
arr.append(v)
return ', '.join(arr)
def __repr__(self) -> str:
return f'<CasbinRule {self.id}: "{str(self)}">'
-10
View File
@@ -1,10 +0,0 @@
[app]
include = 'admin'
[api.api]
prefix = '/apis'
tags = '系统API'
[api.casbin]
prefix = '/casbin'
tags = '系统Casbin权限'
-2
View File
@@ -1,2 +0,0 @@
casbin>=1.38.0
casbin_async_sqlalchemy_adapter>=1.7.0
-35
View File
@@ -1,35 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from datetime import datetime
from pydantic import ConfigDict, Field
from backend.common.enums import MethodType
from backend.common.schema import SchemaBase
class ApiSchemaBase(SchemaBase):
"""API 基础模型"""
name: str = Field(description='API 名称')
method: MethodType = Field(MethodType.GET, description='请求方法')
path: str = Field(description='API 路径')
remark: str | None = Field(None, description='备注')
class CreateApiParam(ApiSchemaBase):
"""创建 API 参数"""
class UpdateApiParam(ApiSchemaBase):
"""更新 API 参数"""
class GetApiDetail(ApiSchemaBase):
"""API 详情"""
model_config = ConfigDict(from_attributes=True)
id: int = Field(description='API ID')
created_time: datetime = Field(description='创建时间')
updated_time: datetime | None = Field(None, description='更新时间')
@@ -1,65 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from pydantic import ConfigDict, Field
from backend.common.enums import MethodType
from backend.common.schema import SchemaBase
class CreatePolicyParam(SchemaBase):
"""创建 P 策略参数"""
sub: str = Field(description='用户 UUID / 角色 ID')
path: str = Field(description='API 路径')
method: MethodType = Field(MethodType.GET, description='请求方法')
class UpdatePolicyParam(SchemaBase):
"""更新 P 策略参数"""
old: CreatePolicyParam = Field(description='原策略')
new: CreatePolicyParam = Field(description='新策略')
class UpdatePoliciesParam(SchemaBase):
"""批量更新策略参数"""
old: list[CreatePolicyParam] = Field(description='原策略列表')
new: list[CreatePolicyParam] = Field(description='新策略列表')
class DeletePolicyParam(CreatePolicyParam):
"""删除策略参数"""
class DeleteAllPoliciesParam(SchemaBase):
"""删除所有策略参数"""
uuid: str | None = Field(None, description='用户 UUID')
role: str = Field(description='角色')
class CreateGroupParam(SchemaBase):
"""创建 G 策略参数"""
uuid: str = Field(description='用户 UUID')
role: str = Field(description='角色')
class DeleteGroupParam(CreateGroupParam):
"""删除 G 策略参数"""
class GetPolicyDetail(SchemaBase):
"""策略详情"""
model_config = ConfigDict(from_attributes=True)
id: int = Field(description='规则 ID')
ptype: str = Field(description='规则类型, p / g')
v0: str = Field(description='用户 UUID / 角色 ID')
v1: str = Field(description='API 路径 / 角色')
v2: str | None = Field(None, description='请求方法')
v3: str | None = Field(None, description='预留字段')
v4: str | None = Field(None, description='预留字段')
v5: str | None = Field(None, description='预留字段')
@@ -1,95 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from typing import Sequence
from fastapi import Request
from sqlalchemy import Select
from backend.common.exception import errors
from backend.database.db import async_db_session
from backend.plugin.casbin.crud.crud_api import api_dao
from backend.plugin.casbin.model import Api
from backend.plugin.casbin.schema.api import CreateApiParam, UpdateApiParam
class ApiService:
"""API 服务类"""
@staticmethod
async def get(*, pk: int) -> Api:
"""
获取 API
:param pk: API ID
:return:
"""
async with async_db_session() as db:
api = await api_dao.get(db, pk)
if not api:
raise errors.NotFoundError(msg='接口不存在')
return api
@staticmethod
async def get_select(*, request: Request, name: str = None, method: str = None, path: str = None) -> Select:
"""
获取 API 查询对象
:param request: 请求对象
:param name: API 名称
:param method: 请求方法
:param path: API 路径
:return:
"""
return await api_dao.get_list(request=request, name=name, method=method, path=path)
@staticmethod
async def get_all() -> Sequence[Api]:
"""获取所有 API"""
async with async_db_session() as db:
apis = await api_dao.get_all(db)
return apis
@staticmethod
async def create(*, obj: CreateApiParam) -> None:
"""
创建 API
:param obj: 创建 API 参数
:return:
"""
async with async_db_session.begin() as db:
api = await api_dao.get_by_name(db, obj.name)
if api:
raise errors.ForbiddenError(msg='接口已存在')
await api_dao.create(db, obj)
@staticmethod
async def update(*, pk: int, obj: UpdateApiParam) -> int:
"""
更新 API
:param pk: API ID
:param obj: 更新 API 参数
:return:
"""
async with async_db_session.begin() as db:
api = await api_dao.get(db, pk)
if not api:
raise errors.NotFoundError(msg='接口不存在')
count = await api_dao.update(db, pk, obj)
return count
@staticmethod
async def delete(*, pk: list[int]) -> int:
"""
删除 API
:param pk: API ID 列表
:return:
"""
async with async_db_session.begin() as db:
count = await api_dao.delete(db, pk)
return count
api_service: ApiService = ApiService()
@@ -1,232 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from uuid import UUID
from sqlalchemy import Select
from backend.common.exception import errors
from backend.database.db import async_db_session
from backend.plugin.casbin.crud.crud_casbin import casbin_dao
from backend.plugin.casbin.schema.casbin_rule import (
CreateGroupParam,
CreatePolicyParam,
DeleteAllPoliciesParam,
DeleteGroupParam,
DeletePolicyParam,
UpdatePoliciesParam,
UpdatePolicyParam,
)
from backend.plugin.casbin.utils.rbac import casbin_enforcer
class CasbinService:
"""Casbin 权限服务类"""
@staticmethod
async def get_casbin_list(*, ptype: str, sub: str) -> Select:
"""
获取 Casbin 规则列表
:param ptype: 策略类型
:param sub: 用户 UUID / 角色 ID
:return:
"""
return await casbin_dao.get_list(ptype, sub)
@staticmethod
async def get_policy_list(*, role: int | None = None) -> list:
"""
获取 P 策略列表
:param role: 角色ID
:return:
"""
enforcer = await casbin_enforcer()
if role is not None:
data = enforcer.get_filtered_named_policy('p', 0, str(role))
else:
data = enforcer.get_policy()
return data
@staticmethod
async def create_policy(*, p: CreatePolicyParam) -> bool:
"""
创建 P 策略
:param p: 策略参数
:return:
"""
enforcer = await casbin_enforcer()
data = await enforcer.add_policy(p.sub, p.path, p.method)
if not data:
raise errors.ForbiddenError(msg='权限已存在')
return data
@staticmethod
async def create_policies(*, ps: list[CreatePolicyParam]) -> bool:
"""
批量创建 P 策略
:param ps: 策略参数列表
:return:
"""
enforcer = await casbin_enforcer()
data = await enforcer.add_policies([list(p.model_dump().values()) for p in ps])
if not data:
raise errors.ForbiddenError(msg='权限已存在')
return data
@staticmethod
async def update_policy(*, obj: UpdatePolicyParam) -> bool:
"""
更新 P 策略
:param obj: 更新 P 策略参数
:return:
"""
old_obj = obj.old
new_obj = obj.new
enforcer = await casbin_enforcer()
_p = enforcer.has_policy(old_obj.sub, old_obj.path, old_obj.method)
if not _p:
raise errors.NotFoundError(msg='权限不存在')
data = await enforcer.update_policy(
[old_obj.sub, old_obj.path, old_obj.method],
[new_obj.sub, new_obj.path, new_obj.method],
)
return data
@staticmethod
async def update_policies(*, obj: UpdatePoliciesParam) -> bool:
"""
批量更新 P 策略
:param obj: 更新 P 策略参数
:return:
"""
enforcer = await casbin_enforcer()
data = await enforcer.update_policies(
[list(o.model_dump().values()) for o in obj.old],
[list(n.model_dump().values()) for n in obj.new],
)
return data
@staticmethod
async def delete_policy(*, p: DeletePolicyParam) -> bool:
"""
删除 P 策略
:param p: 删除参数
:return:
"""
enforcer = await casbin_enforcer()
_p = enforcer.has_policy(p.sub, p.path, p.method)
if not _p:
raise errors.NotFoundError(msg='权限不存在')
data = await enforcer.remove_policy(p.sub, p.path, p.method)
return data
@staticmethod
async def delete_policies(*, ps: list[DeletePolicyParam]) -> bool:
"""
批量删除 P 策略
:param ps: 删除参数列表
:return:
"""
enforcer = await casbin_enforcer()
data = await enforcer.remove_policies([list(p.model_dump().values()) for p in ps])
if not data:
raise errors.NotFoundError(msg='权限不存在')
return data
@staticmethod
async def delete_all_policies(*, sub: DeleteAllPoliciesParam) -> int:
"""
删除所有 P 策略
:param sub: 删除参数
:return:
"""
async with async_db_session.begin() as db:
count = await casbin_dao.delete_policies_by_sub(db, sub)
return count
@staticmethod
async def get_group_list() -> list:
"""获取 G 策略列表"""
enforcer = await casbin_enforcer()
data = enforcer.get_grouping_policy()
return data
@staticmethod
async def create_group(*, g: CreateGroupParam) -> bool:
"""
创建 G 策略
:param g: 创建 G 策略参数
:return:
"""
enforcer = await casbin_enforcer()
data = await enforcer.add_grouping_policy(g.uuid, g.role)
if not data:
raise errors.ForbiddenError(msg='权限已存在')
return data
@staticmethod
async def create_groups(*, gs: list[CreateGroupParam]) -> bool:
"""
批量创建 G 策略
:param gs: 创建参数列表
:return:
"""
enforcer = await casbin_enforcer()
data = await enforcer.add_grouping_policies([list(g.model_dump().values()) for g in gs])
if not data:
raise errors.ForbiddenError(msg='权限已存在')
return data
@staticmethod
async def delete_group(*, g: DeleteGroupParam) -> bool:
"""
删除 G 策略
:param g: 删除参数
:return:
"""
enforcer = await casbin_enforcer()
_g = enforcer.has_grouping_policy(g.uuid, g.role)
if not _g:
raise errors.NotFoundError(msg='权限不存在')
data = await enforcer.remove_grouping_policy(g.uuid, g.role)
return data
@staticmethod
async def delete_groups(*, gs: list[DeleteGroupParam]) -> bool:
"""
批量删除 G 策略
:param gs: 删除参数列表
:return: 是否成功
"""
enforcer = await casbin_enforcer()
data = await enforcer.remove_grouping_policies([list(g.model_dump().values()) for g in gs])
if not data:
raise errors.NotFoundError(msg='权限不存在')
return data
@staticmethod
async def delete_all_groups(*, uuid: UUID) -> int:
"""
删除所有 G 策略
:param uuid: 用户uuid
:return: 删除数量
"""
async with async_db_session.begin() as db:
count = await casbin_dao.delete_groups_by_uuid(db, uuid)
return count
casbin_service: CasbinService = CasbinService()
-58
View File
@@ -1,58 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import casbin
import casbin_async_sqlalchemy_adapter
from fastapi import Request
from backend.common.exception.errors import AuthorizationError
from backend.database.db import async_engine
from backend.plugin.casbin.conf import casbin_settings
from backend.plugin.casbin.model import CasbinRule
async def casbin_enforcer() -> casbin.AsyncEnforcer:
"""获取 casbin 执行器"""
# 模型定义:https://casbin.org/zh/docs/category/model
_CASBIN_RBAC_MODEL_CONF_TEXT = """
[request_definition]
r = sub, obj, act
[policy_definition]
p = sub, obj, act
[role_definition]
g = _, _
[policy_effect]
e = some(where (p.eft == allow))
[matchers]
m = g(r.sub, p.sub) && (keyMatch(r.obj, p.obj) || keyMatch3(r.obj, p.obj)) && (r.act == p.act || p.act == "*")
"""
adapter = casbin_async_sqlalchemy_adapter.Adapter(async_engine, db_class=CasbinRule)
model = casbin.AsyncEnforcer.new_model(text=_CASBIN_RBAC_MODEL_CONF_TEXT)
enforcer = casbin.AsyncEnforcer(model, adapter)
await enforcer.load_policy()
return enforcer
async def casbin_verify(request: Request) -> None:
"""
Casbin 权限校验
:param request: FastAPI 请求对象
:return:
"""
method = request.method
path = request.url.path
# casbin 鉴权白名单
if (method, path) in casbin_settings.RBAC_CASBIN_EXCLUDE:
return
# casbin 权限校验
user_uuid = request.user.uuid
enforcer = await casbin_enforcer()
if not enforcer.enforce(user_uuid, path, method):
raise AuthorizationError
@@ -0,0 +1,14 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from fastapi import APIRouter
from backend.core.conf import settings
from backend.plugin.code_generator.api.v1.business import router as business_router
from backend.plugin.code_generator.api.v1.column import router as model_router
from backend.plugin.code_generator.api.v1.gen import router as gen_router
v1 = APIRouter(prefix=f'{settings.FASTAPI_API_V1_PATH}/gen', tags=['代码生成'])
v1.include_router(gen_router)
v1.include_router(business_router, prefix='/businesses')
v1.include_router(model_router, prefix='/models')
@@ -4,18 +4,18 @@ from typing import Annotated
from fastapi import APIRouter, Depends, Path from fastapi import APIRouter, Depends, Path
from backend.app.generator.schema.gen_business import (
CreateGenBusinessParam,
GetGenBusinessDetail,
UpdateGenBusinessParam,
)
from backend.app.generator.schema.gen_model import GetGenModelDetail
from backend.app.generator.service.gen_business_service import gen_business_service
from backend.app.generator.service.gen_model_service import gen_model_service
from backend.common.response.response_schema import ResponseModel, ResponseSchemaModel, response_base from backend.common.response.response_schema import ResponseModel, ResponseSchemaModel, response_base
from backend.common.security.jwt import DependsJwtAuth from backend.common.security.jwt import DependsJwtAuth
from backend.common.security.permission import RequestPermission from backend.common.security.permission import RequestPermission
from backend.common.security.rbac import DependsRBAC from backend.common.security.rbac import DependsRBAC
from backend.plugin.code_generator.schema.business import (
CreateGenBusinessParam,
GetGenBusinessDetail,
UpdateGenBusinessParam,
)
from backend.plugin.code_generator.schema.column import GetGenModelDetail
from backend.plugin.code_generator.service.business_service import gen_business_service
from backend.plugin.code_generator.service.column_service import gen_model_service
router = APIRouter() router = APIRouter()
@@ -4,12 +4,12 @@ from typing import Annotated
from fastapi import APIRouter, Depends, Path from fastapi import APIRouter, Depends, Path
from backend.app.generator.schema.gen_model import CreateGenModelParam, GetGenModelDetail, UpdateGenModelParam
from backend.app.generator.service.gen_model_service import gen_model_service
from backend.common.response.response_schema import ResponseModel, ResponseSchemaModel, response_base from backend.common.response.response_schema import ResponseModel, ResponseSchemaModel, response_base
from backend.common.security.jwt import DependsJwtAuth from backend.common.security.jwt import DependsJwtAuth
from backend.common.security.permission import RequestPermission from backend.common.security.permission import RequestPermission
from backend.common.security.rbac import DependsRBAC from backend.common.security.rbac import DependsRBAC
from backend.plugin.code_generator.schema.column import CreateGenModelParam, GetGenModelDetail, UpdateGenModelParam
from backend.plugin.code_generator.service.column_service import gen_model_service
router = APIRouter() router = APIRouter()
@@ -5,13 +5,13 @@ from typing import Annotated
from fastapi import APIRouter, Depends, Path, Query from fastapi import APIRouter, Depends, Path, Query
from fastapi.responses import StreamingResponse from fastapi.responses import StreamingResponse
from backend.app.generator.conf import generator_settings
from backend.app.generator.schema.gen import ImportParam
from backend.app.generator.service.gen_service import gen_service
from backend.common.response.response_schema import ResponseModel, ResponseSchemaModel, response_base from backend.common.response.response_schema import ResponseModel, ResponseSchemaModel, response_base
from backend.common.security.jwt import DependsJwtAuth from backend.common.security.jwt import DependsJwtAuth
from backend.common.security.permission import RequestPermission from backend.common.security.permission import RequestPermission
from backend.common.security.rbac import DependsRBAC from backend.common.security.rbac import DependsRBAC
from backend.plugin.code_generator.conf import generator_settings
from backend.plugin.code_generator.schema.gen import ImportParam
from backend.plugin.code_generator.service.gen_service import gen_service
router = APIRouter() router = APIRouter()
@@ -8,9 +8,6 @@ from pydantic_settings import BaseSettings
class GeneratorSettings(BaseSettings): class GeneratorSettings(BaseSettings):
"""代码生成配置""" """代码生成配置"""
# 模版
TEMPLATE_BACKEND_DIR_NAME: str = 'py'
# 代码下载 # 代码下载
DOWNLOAD_ZIP_FILENAME: str = 'fba_generator' DOWNLOAD_ZIP_FILENAME: str = 'fba_generator'
@@ -5,8 +5,8 @@ from typing import Sequence
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy_crud_plus import CRUDPlus from sqlalchemy_crud_plus import CRUDPlus
from backend.app.generator.model import GenBusiness from backend.plugin.code_generator.model import GenBusiness
from backend.app.generator.schema.gen_business import CreateGenBusinessParam, UpdateGenBusinessParam from backend.plugin.code_generator.schema.business import CreateGenBusinessParam, UpdateGenBusinessParam
class CRUDGenBusiness(CRUDPlus[GenBusiness]): class CRUDGenBusiness(CRUDPlus[GenBusiness]):
@@ -30,7 +30,7 @@ class CRUDGenBusiness(CRUDPlus[GenBusiness]):
:param name: 表名 :param name: 表名
:return: :return:
""" """
return await self.select_model_by_column(db, table_name_en=name) return await self.select_model_by_column(db, table_name=name)
async def get_all(self, db: AsyncSession) -> Sequence[GenBusiness]: async def get_all(self, db: AsyncSession) -> Sequence[GenBusiness]:
""" """
@@ -5,14 +5,14 @@ from typing import Sequence
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy_crud_plus import CRUDPlus from sqlalchemy_crud_plus import CRUDPlus
from backend.app.generator.model import GenModel from backend.plugin.code_generator.model import GenColumn
from backend.app.generator.schema.gen_model import CreateGenModelParam, UpdateGenModelParam from backend.plugin.code_generator.schema.column import CreateGenModelParam, UpdateGenModelParam
class CRUDGenModel(CRUDPlus[GenModel]): class CRUDGenModel(CRUDPlus[GenColumn]):
"""代码生成模型 CRUD 类""" """代码生成模型 CRUD 类"""
async def get(self, db: AsyncSession, pk: int) -> GenModel | None: async def get(self, db: AsyncSession, pk: int) -> GenColumn | None:
""" """
获取代码生成模型列 获取代码生成模型列
@@ -22,7 +22,7 @@ class CRUDGenModel(CRUDPlus[GenModel]):
""" """
return await self.select_model(db, pk) return await self.select_model(db, pk)
async def get_all_by_business(self, db: AsyncSession, business_id: int) -> Sequence[GenModel]: async def get_all_by_business(self, db: AsyncSession, business_id: int) -> Sequence[GenColumn]:
""" """
获取所有代码生成模型列 获取所有代码生成模型列
@@ -32,7 +32,7 @@ class CRUDGenModel(CRUDPlus[GenModel]):
""" """
return await self.select_models_order(db, sort_columns='sort', gen_business_id=business_id) return await self.select_models_order(db, sort_columns='sort', gen_business_id=business_id)
async def create(self, db: AsyncSession, obj: CreateGenModelParam, pd_type: str | None = None) -> None: async def create(self, db: AsyncSession, obj: CreateGenModelParam, pd_type: str | None) -> None:
""" """
创建代码生成模型 创建代码生成模型
@@ -43,7 +43,7 @@ class CRUDGenModel(CRUDPlus[GenModel]):
""" """
await self.create_model(db, obj, pd_type=pd_type) await self.create_model(db, obj, pd_type=pd_type)
async def update(self, db: AsyncSession, pk: int, obj: UpdateGenModelParam, pd_type: str | None = None) -> int: async def update(self, db: AsyncSession, pk: int, obj: UpdateGenModelParam, pd_type: str | None) -> int:
""" """
更新代码生成模型 更新代码生成模型
@@ -66,4 +66,4 @@ class CRUDGenModel(CRUDPlus[GenModel]):
return await self.delete_model(db, pk) return await self.delete_model(db, pk)
gen_model_dao: CRUDGenModel = CRUDGenModel(GenModel) gen_model_dao: CRUDGenModel = CRUDGenModel(GenColumn)
@@ -12,7 +12,7 @@ class CRUDGen:
"""代码生成 CRUD 类""" """代码生成 CRUD 类"""
@staticmethod @staticmethod
async def get_all_tables(db: AsyncSession, table_schema: str) -> list[str]: async def get_all_tables(db: AsyncSession, table_schema: str) -> Sequence[str]:
""" """
获取所有表名 获取所有表名
+153
View File
@@ -0,0 +1,153 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from backend.common.enums import StrEnum
class GenModelMySQLColumnType(StrEnum):
"""代码生成模型列类型(MySQL"""
# Python 类型映射
BIGINT = 'int'
BigInteger = 'int' # BIGINT
BINARY = 'bytes'
BLOB = 'bytes'
BOOLEAN = 'bool' # BOOL
Boolean = 'bool' # BOOL
CHAR = 'str'
CLOB = 'str'
DATE = 'date'
Date = 'date' # DATE
DATETIME = 'datetime'
DateTime = 'datetime' # DATETIME
DECIMAL = 'Decimal'
DOUBLE = 'float'
Double = 'float' # DOUBLE
DOUBLE_PRECISION = 'float'
Enum = 'Enum' # Enum()
FLOAT = 'float'
Float = 'float' # FLOAT
INT = 'int' # INTEGER
INTEGER = 'int'
Integer = 'int' # INTEGER
Interval = 'timedelta' # DATETIME
JSON = 'dict'
LargeBinary = 'bytes' # BLOB
NCHAR = 'str'
NUMERIC = 'Decimal'
Numeric = 'Decimal' # NUMERIC
NVARCHAR = 'str' # String
PickleType = 'bytes' # BLOB
REAL = 'float'
SMALLINT = 'int'
SmallInteger = 'int' # SMALLINT
String = 'str' # String
TEXT = 'str'
Text = 'str' # TEXT
TIME = 'time'
Time = 'time' # TIME
TIMESTAMP = 'datetime'
Unicode = 'str' # String
UnicodeText = 'str' # TEXT
UUID = 'str | UUID'
Uuid = 'str' # CHAR(32)
VARBINARY = 'bytes'
VARCHAR = 'str' # String
# sa.dialects.mysql 导入
BIT = 'bool'
ENUM = 'Enum'
LONGBLOB = 'bytes'
LONGTEXT = 'str'
MEDIUMBLOB = 'bytes'
MEDIUMINT = 'int'
MEDIUMTEXT = 'str'
SET = 'list[str]'
TINYBLOB = 'bytes'
TINYINT = 'int'
TINYTEXT = 'str'
YEAR = 'int'
class GenModelPostgreSQLColumnType(StrEnum):
"""代码生成模型列类型(PostgreSQL)"""
# Python 类型映射
BIGINT = 'int'
BigInteger = 'int' # BIGINT
BINARY = 'bytes'
BLOB = 'bytes'
BOOLEAN = 'bool'
Boolean = 'bool' # BOOLEAN
CHAR = 'str'
CLOB = 'str'
DATE = 'date'
Date = 'date' # DATE
DATETIME = 'datetime'
DateTime = 'datetime' # TIMESTAMP WITHOUT TIME ZONE
DECIMAL = 'Decimal'
DOUBLE = 'float'
Double = 'float' # DOUBLE PRECISION
DOUBLE_PRECISION = 'float' # DOUBLE PRECISION
Enum = 'Enum' # Enum(name='enum')
FLOAT = 'float'
Float = 'float' # FLOAT
INT = 'int' # INTEGER
INTEGER = 'int'
Integer = 'int' # INTEGER
Interval = 'timedelta' # INTERVAL
JSON = 'dict'
LargeBinary = 'bytes' # BYTEA
NCHAR = 'str'
NUMERIC = 'Decimal'
Numeric = 'Decimal' # NUMERIC
NVARCHAR = 'str' # String
PickleType = 'bytes' # BYTEA
REAL = 'float'
SMALLINT = 'int'
SmallInteger = 'int' # SMALLINT
String = 'str' # String
TEXT = 'str'
Text = 'str' # TEXT
TIME = 'time' # TIME WITHOUT TIME ZONE
Time = 'time' # TIME WITHOUT TIME ZONE
TIMESTAMP = 'datetime' # TIMESTAMP WITHOUT TIME ZONE
Unicode = 'str' # String
UnicodeText = 'str' # TEXT
UUID = 'str | UUID'
Uuid = 'str'
VARBINARY = 'bytes'
VARCHAR = 'str' # String
# sa.dialects.postgresql 导入
ARRAY = 'list'
BIT = 'bool'
BYTEA = 'bytes'
CIDR = 'str'
CITEXT = 'str'
DATEMULTIRANGE = 'list[date]'
DATERANGE = 'tuple[date, date]'
DOMAIN = 'str'
ENUM = 'Enum'
HSTORE = 'dict'
INET = 'str'
INT4MULTIRANGE = 'list[int]'
INT4RANGE = 'tuple[int, int]'
INT8MULTIRANGE = 'list[int]'
INT8RANGE = 'tuple[int, int]'
INTERVAL = 'timedelta'
JSONB = 'dict'
JSONPATH = 'str'
MACADDR = 'str'
MACADDR8 = 'str'
MONEY = 'Decimal'
NUMMULTIRANGE = 'list[Decimal]'
NUMRANGE = 'tuple[Decimal, Decimal]'
OID = 'int'
REGCLASS = 'str'
REGCONFIG = 'str'
TSMULTIRANGE = 'list[datetime]'
TSQUERY = 'str'
TSRANGE = 'tuple[datetime, datetime]'
TSTZMULTIRANGE = 'list[datetime]'
TSTZRANGE = 'tuple[datetime, datetime]'
TSVECTOR = 'str'
@@ -0,0 +1,4 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from backend.plugin.code_generator.model.business import GenBusiness
from backend.plugin.code_generator.model.column import GenColumn
@@ -10,22 +10,23 @@ from sqlalchemy.orm import Mapped, mapped_column, relationship
from backend.common.model import Base, id_key from backend.common.model import Base, id_key
if TYPE_CHECKING: if TYPE_CHECKING:
from backend.app.generator.model import GenModel from backend.plugin.code_generator.model import GenColumn
class GenBusiness(Base): class GenBusiness(Base):
"""代码生成业务表""" """代码生成业务表"""
__tablename__ = 'sys_gen_business' __tablename__ = 'gen_business'
id: Mapped[id_key] = mapped_column(init=False) id: Mapped[id_key] = mapped_column(init=False)
app_name: Mapped[str] = mapped_column(String(50), comment='应用名称(英文)') app_name: Mapped[str] = mapped_column(String(50), comment='应用名称(英文)')
table_name_en: Mapped[str] = mapped_column(String(255), unique=True, comment='表名称(英文)') table_name: Mapped[str] = mapped_column(String(255), unique=True, comment='表名称(英文)')
table_name_zh: Mapped[str] = mapped_column(String(255), comment='表名称(中文)') doc_comment: Mapped[str] = mapped_column(String(255), comment='档注释(用于函数/参数文档')
table_simple_name_zh: Mapped[str] = mapped_column(String(255), comment='表名称(中文简称)')
table_comment: Mapped[str | None] = mapped_column(String(255), default=None, comment='表描述') table_comment: Mapped[str | None] = mapped_column(String(255), default=None, comment='表描述')
# relate_model_fk: Mapped[int | None] = mapped_column(default=None, comment='关联表外键') # relate_model_fk: Mapped[int | None] = mapped_column(default=None, comment='关联表外键')
schema_name: Mapped[str | None] = mapped_column(String(255), default=None, comment='Schema 名称 (默认为英文表名称)') class_name: Mapped[str | None] = mapped_column(String(50), default=None, comment='基础类名(默认为英文表名称')
schema_name: Mapped[str | None] = mapped_column(String(50), default=None, comment='Schema 名称 (默认为英文表名称)')
filename: Mapped[str | None] = mapped_column(String(50), default=None, comment='基础文件名(默认为英文表名称)')
default_datetime_column: Mapped[bool] = mapped_column(default=True, comment='是否存在默认时间列') default_datetime_column: Mapped[bool] = mapped_column(default=True, comment='是否存在默认时间列')
api_version: Mapped[str] = mapped_column(String(20), default='v1', comment='代码生成 api 版本,默认为 v1') api_version: Mapped[str] = mapped_column(String(20), default='v1', comment='代码生成 api 版本,默认为 v1')
gen_path: Mapped[str | None] = mapped_column(String(255), default=None, comment='代码生成路径(默认为 app 根路径)') gen_path: Mapped[str | None] = mapped_column(String(255), default=None, comment='代码生成路径(默认为 app 根路径)')
@@ -33,4 +34,4 @@ class GenBusiness(Base):
LONGTEXT().with_variant(TEXT, 'postgresql'), default=None, comment='备注' LONGTEXT().with_variant(TEXT, 'postgresql'), default=None, comment='备注'
) )
# 代码生成业务模型一对多 # 代码生成业务模型一对多
gen_model: Mapped[list['GenModel']] = relationship(init=False, back_populates='gen_business') gen_column: Mapped[list['GenColumn']] = relationship(init=False, back_populates='gen_business')
@@ -10,18 +10,18 @@ from sqlalchemy.orm import Mapped, mapped_column, relationship
from backend.common.model import DataClassBase, id_key from backend.common.model import DataClassBase, id_key
if TYPE_CHECKING: if TYPE_CHECKING:
from backend.app.generator.model import GenBusiness from backend.plugin.code_generator.model import GenBusiness
class GenModel(DataClassBase): class GenColumn(DataClassBase):
"""代码生成模型表""" """代码生成模型"""
__tablename__ = 'sys_gen_model' __tablename__ = 'gen_column'
id: Mapped[id_key] = mapped_column(init=False) id: Mapped[id_key] = mapped_column(init=False)
name: Mapped[str] = mapped_column(String(50), comment='列名称') name: Mapped[str] = mapped_column(String(50), comment='列名称')
comment: Mapped[str | None] = mapped_column(String(255), default=None, comment='列描述') comment: Mapped[str | None] = mapped_column(String(255), default=None, comment='列描述')
type: Mapped[str] = mapped_column(String(20), default='str', comment='SQLA 模型列类型') type: Mapped[str] = mapped_column(String(20), default='String', comment='SQLA 模型列类型')
pd_type: Mapped[str] = mapped_column(String(20), default='str', comment='列类型对应的 pydantic 类型') pd_type: Mapped[str] = mapped_column(String(20), default='str', comment='列类型对应的 pydantic 类型')
default: Mapped[str | None] = mapped_column( default: Mapped[str | None] = mapped_column(
LONGTEXT().with_variant(TEXT, 'postgresql'), default=None, comment='列默认值' LONGTEXT().with_variant(TEXT, 'postgresql'), default=None, comment='列默认值'
@@ -33,6 +33,6 @@ class GenModel(DataClassBase):
# 代码生成业务模型一对多 # 代码生成业务模型一对多
gen_business_id: Mapped[int] = mapped_column( gen_business_id: Mapped[int] = mapped_column(
ForeignKey('sys_gen_business.id', ondelete='CASCADE'), default=0, comment='代码生成业务ID' ForeignKey('gen_business.id', ondelete='CASCADE'), default=0, comment='代码生成业务ID'
) )
gen_business: Mapped[Union['GenBusiness', None]] = relationship(init=False, back_populates='gen_model') gen_business: Mapped[Union['GenBusiness', None]] = relationship(init=False, back_populates='gen_column')
@@ -0,0 +1,6 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from backend.core.path_conf import PLUGIN_DIR
# jinja2 模版文件路径
JINJA2_TEMPLATE_DIR = PLUGIN_DIR / 'code_generator' / 'templates'
@@ -0,0 +1,2 @@
[app]
router = ['v1']
@@ -2,8 +2,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from datetime import datetime from datetime import datetime
from pydantic import ConfigDict, Field, model_validator from pydantic import ConfigDict, Field
from typing_extensions import Self
from backend.common.schema import SchemaBase from backend.common.schema import SchemaBase
@@ -12,23 +11,17 @@ class GenBusinessSchemaBase(SchemaBase):
"""代码生成业务基础模型""" """代码生成业务基础模型"""
app_name: str = Field(description='应用名称(英文)') app_name: str = Field(description='应用名称(英文)')
table_name_en: str = Field(description='表名称(英文)') table_name: str = Field(description='表名称(英文)')
table_name_zh: str = Field(description='表名称(中文)') doc_comment: str = Field(description='档注释(用于函数/参数文档')
table_simple_name_zh: str = Field(description='表名称(中文简称)')
table_comment: str | None = Field(None, description='表描述') table_comment: str | None = Field(None, description='表描述')
class_name: str | None = Field(None, description='基础类名 (默认为英文表名称)')
schema_name: str | None = Field(None, description='Schema 名称 (默认为英文表名称)') schema_name: str | None = Field(None, description='Schema 名称 (默认为英文表名称)')
filename: str | None = Field(None, description='基础文件名(默认为英文表名称)')
default_datetime_column: bool = Field(True, description='是否存在默认时间列') default_datetime_column: bool = Field(True, description='是否存在默认时间列')
api_version: str = Field('v1', description='代码生成 api 版本') api_version: str = Field('v1', description='代码生成 api 版本')
gen_path: str | None = Field(None, description='代码生成路径(默认为 app 根路径)') gen_path: str | None = Field(None, description='代码生成路径(默认为 app 根路径)')
remark: str | None = Field(None, description='备注') remark: str | None = Field(None, description='备注')
@model_validator(mode='after')
def check_schema_name(self) -> Self:
"""检查并设置 schema 名称"""
if self.schema_name is None:
self.schema_name = self.table_name_en
return self
class CreateGenBusinessParam(GenBusinessSchemaBase): class CreateGenBusinessParam(GenBusinessSchemaBase):
"""创建代码生成业务参数""" """创建代码生成业务参数"""
@@ -3,7 +3,7 @@
from pydantic import ConfigDict, Field, field_validator from pydantic import ConfigDict, Field, field_validator
from backend.common.schema import SchemaBase from backend.common.schema import SchemaBase
from backend.utils.type_conversion import sql_type_to_sqlalchemy from backend.plugin.code_generator.utils.type_conversion import sql_type_to_sqlalchemy
class GenModelSchemaBase(SchemaBase): class GenModelSchemaBase(SchemaBase):
@@ -9,5 +9,5 @@ class ImportParam(SchemaBase):
"""导入参数""" """导入参数"""
app: str = Field(description='应用名称,用于代码生成到指定 app') app: str = Field(description='应用名称,用于代码生成到指定 app')
table_name: str = Field(description='数据库表名')
table_schema: str = Field(description='数据库名') table_schema: str = Field(description='数据库名')
table_name: str = Field(description='数据库表名')
@@ -2,11 +2,11 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from typing import Sequence from typing import Sequence
from backend.app.generator.crud.crud_gen_business import gen_business_dao
from backend.app.generator.model import GenBusiness
from backend.app.generator.schema.gen_business import CreateGenBusinessParam, UpdateGenBusinessParam
from backend.common.exception import errors from backend.common.exception import errors
from backend.database.db import async_db_session from backend.database.db import async_db_session
from backend.plugin.code_generator.crud.crud_business import gen_business_dao
from backend.plugin.code_generator.model import GenBusiness
from backend.plugin.code_generator.schema.business import CreateGenBusinessParam, UpdateGenBusinessParam
class GenBusinessService: class GenBusinessService:
@@ -41,7 +41,7 @@ class GenBusinessService:
:return: :return:
""" """
async with async_db_session.begin() as db: async with async_db_session.begin() as db:
business = await gen_business_dao.get_by_name(db, obj.table_name_en) business = await gen_business_dao.get_by_name(db, obj.table_name)
if business: if business:
raise errors.ForbiddenError(msg='代码生成业务已存在') raise errors.ForbiddenError(msg='代码生成业务已存在')
await gen_business_dao.create(db, obj) await gen_business_dao.create(db, obj)
@@ -2,20 +2,20 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from typing import Sequence from typing import Sequence
from backend.app.generator.crud.crud_gen_model import gen_model_dao
from backend.app.generator.model import GenModel
from backend.app.generator.schema.gen_model import CreateGenModelParam, UpdateGenModelParam
from backend.common.enums import GenModelMySQLColumnType
from backend.common.exception import errors from backend.common.exception import errors
from backend.database.db import async_db_session from backend.database.db import async_db_session
from backend.utils.type_conversion import sql_type_to_pydantic from backend.plugin.code_generator.crud.crud_column import gen_model_dao
from backend.plugin.code_generator.enums import GenModelMySQLColumnType
from backend.plugin.code_generator.model import GenColumn
from backend.plugin.code_generator.schema.column import CreateGenModelParam, UpdateGenModelParam
from backend.plugin.code_generator.utils.type_conversion import sql_type_to_pydantic
class GenModelService: class GenModelService:
"""代码生成模型服务类""" """代码生成模型服务类"""
@staticmethod @staticmethod
async def get(*, pk: int) -> GenModel: async def get(*, pk: int) -> GenColumn:
""" """
获取指定 ID 的模型 获取指定 ID 的模型
@@ -36,7 +36,7 @@ class GenModelService:
return types return types
@staticmethod @staticmethod
async def get_by_business(*, business_id: int) -> Sequence[GenModel]: async def get_by_business(*, business_id: int) -> Sequence[GenColumn]:
""" """
获取指定业务的所有模型 获取指定业务的所有模型
@@ -5,31 +5,32 @@ import os.path
import zipfile import zipfile
from pathlib import Path from pathlib import Path
from typing import Sequence
import aiofiles import aiofiles
from pydantic.alias_generators import to_pascal from pydantic.alias_generators import to_pascal
from backend.app.generator.crud.crud_gen import gen_dao
from backend.app.generator.crud.crud_gen_business import gen_business_dao
from backend.app.generator.crud.crud_gen_model import gen_model_dao
from backend.app.generator.model import GenBusiness
from backend.app.generator.schema.gen import ImportParam
from backend.app.generator.schema.gen_business import CreateGenBusinessParam
from backend.app.generator.schema.gen_model import CreateGenModelParam
from backend.app.generator.service.gen_model_service import gen_model_service
from backend.common.exception import errors from backend.common.exception import errors
from backend.core.path_conf import BASE_PATH from backend.core.path_conf import BASE_PATH
from backend.database.db import async_db_session from backend.database.db import async_db_session
from backend.utils.gen_template import gen_template from backend.plugin.code_generator.crud.crud_business import gen_business_dao
from backend.utils.type_conversion import sql_type_to_pydantic from backend.plugin.code_generator.crud.crud_column import gen_model_dao
from backend.plugin.code_generator.crud.crud_gen import gen_dao
from backend.plugin.code_generator.model import GenBusiness
from backend.plugin.code_generator.schema.business import CreateGenBusinessParam
from backend.plugin.code_generator.schema.column import CreateGenModelParam
from backend.plugin.code_generator.schema.gen import ImportParam
from backend.plugin.code_generator.service.column_service import gen_model_service
from backend.plugin.code_generator.utils.gen_template import gen_template
from backend.plugin.code_generator.utils.type_conversion import sql_type_to_pydantic
class GenService: class GenService:
"""代码生成服务类""" """代码生成服务类"""
@staticmethod @staticmethod
async def get_tables(*, table_schema: str) -> list[str]: async def get_tables(*, table_schema: str) -> Sequence[str]:
""" """
获取指定 schema 下的所有表名 获取指定 schema 下的所有表名
@@ -57,14 +58,17 @@ class GenService:
raise errors.ForbiddenError(msg='已存在相同数据库表业务') raise errors.ForbiddenError(msg='已存在相同数据库表业务')
table_name = table_info[0] table_name = table_info[0]
business_data = { new_business = GenBusiness(
'app_name': obj.app, **CreateGenBusinessParam(
'table_name_en': table_name, app_name=obj.app,
'table_name_zh': table_info[1] or ' '.join(table_name.split('_')), table_name=table_name,
'table_simple_name_zh': table_info[1] or table_name.split('_')[-1], doc_comment=table_info[1] or table_name.split('_')[-1],
'table_comment': table_info[1], table_comment=table_info[1],
} class_name=table_name,
new_business = GenBusiness(**CreateGenBusinessParam(**business_data).model_dump()) schema_name=table_name,
filename=table_name,
).model_dump()
)
db.add(new_business) db.add(new_business)
await db.flush() await db.flush()
@@ -72,17 +76,20 @@ class GenService:
for column in column_info: for column in column_info:
column_type = column[-1].split('(')[0].upper() column_type = column[-1].split('(')[0].upper()
pd_type = sql_type_to_pydantic(column_type) pd_type = sql_type_to_pydantic(column_type)
model_data = { await gen_model_dao.create(
'name': column[0], db,
'comment': column[-2], CreateGenModelParam(
'type': column_type, name=column[0],
'sort': column[-3], comment=column[-2],
'length': column[-1].split('(')[1][:-1] if pd_type == 'str' and '(' in column[-1] else 0, type=column_type,
'is_pk': column[1], sort=column[-3],
'is_nullable': column[2], length=column[-1].split('(')[1][:-1] if pd_type == 'str' and '(' in column[-1] else 0,
'gen_business_id': new_business.id, is_pk=column[1],
} is_nullable=column[2],
await gen_model_dao.create(db, CreateGenModelParam(**model_data), pd_type=pd_type) gen_business_id=new_business.id,
),
pd_type=pd_type,
)
@staticmethod @staticmethod
async def render_tpl_code(*, business: GenBusiness) -> dict[str, str]: async def render_tpl_code(*, business: GenBusiness) -> dict[str, str]:
@@ -99,7 +106,7 @@ class GenService:
gen_vars = gen_template.get_vars(business, gen_models) gen_vars = gen_template.get_vars(business, gen_models)
return { return {
tpl_path: await gen_template.get_template(tpl_path).render_async(**gen_vars) tpl_path: await gen_template.get_template(tpl_path).render_async(**gen_vars)
for tpl_path in gen_template.get_template_paths() for tpl_path in gen_template.get_template_files()
} }
async def preview(self, *, pk: int) -> dict[str, bytes]: async def preview(self, *, pk: int) -> dict[str, bytes]:
@@ -115,10 +122,13 @@ class GenService:
raise errors.NotFoundError(msg='业务不存在') raise errors.NotFoundError(msg='业务不存在')
tpl_code_map = await self.render_tpl_code(business=business) tpl_code_map = await self.render_tpl_code(business=business)
return {
tpl.replace('.jinja', '.py') if tpl.startswith('py') else ...: code.encode('utf-8') codes = {}
for tpl, code in tpl_code_map.items() for tpl, code in tpl_code_map.items():
} if tpl.startswith('python'):
codes[tpl.replace('.jinja', '.py').split('/')[-1]] = code.encode('utf-8')
return codes
@staticmethod @staticmethod
async def get_generate_path(*, pk: int) -> list[str]: async def get_generate_path(*, pk: int) -> list[str]:
@@ -133,9 +143,10 @@ class GenService:
if not business: if not business:
raise errors.NotFoundError(msg='业务不存在') raise errors.NotFoundError(msg='业务不存在')
gen_path = business.gen_path or 'fba-backend-app-path' gen_path = business.gen_path or 'fba-backend-app-dir'
target_files = gen_template.get_code_gen_paths(business) target_files = gen_template.get_code_gen_paths(business)
return [os.path.join(gen_path, *target_file.split('/')[1:]) for target_file in target_files]
return [os.path.join(gen_path, *target_file.split('/')) for target_file in target_files]
async def generate(self, *, pk: int) -> None: async def generate(self, *, pk: int) -> None:
""" """
@@ -155,41 +166,42 @@ class GenService:
for tpl_path, code in tpl_code_map.items(): for tpl_path, code in tpl_code_map.items():
code_filepath = os.path.join( code_filepath = os.path.join(
gen_path, gen_path,
*gen_template.get_code_gen_path(tpl_path, business).split('/')[1:], *gen_template.get_code_gen_path(tpl_path, business).split('/'),
) )
code_folder = Path(str(code_filepath)).parent
code_folder.mkdir(parents=True, exist_ok=True)
# 写入 init 文件 # 写入 init 文件
str_code_filepath = str(code_filepath)
code_folder = Path(str_code_filepath).parent
code_folder.mkdir(parents=True, exist_ok=True)
init_filepath = code_folder.joinpath('__init__.py') init_filepath = code_folder.joinpath('__init__.py')
if not init_filepath.exists():
async with aiofiles.open(init_filepath, 'w', encoding='utf-8') as f: async with aiofiles.open(init_filepath, 'w', encoding='utf-8') as f:
await f.write(gen_template.init_content) await f.write(gen_template.init_content)
if 'api' in str(code_folder):
# api __init__.py # api __init__.py
if 'api' in str_code_filepath:
api_init_filepath = code_folder.parent.joinpath('__init__.py') api_init_filepath = code_folder.parent.joinpath('__init__.py')
if not api_init_filepath.exists():
async with aiofiles.open(api_init_filepath, 'w', encoding='utf-8') as f: async with aiofiles.open(api_init_filepath, 'w', encoding='utf-8') as f:
await f.write(gen_template.init_content) await f.write(gen_template.init_content)
# app __init__.py # app __init__.py
app_init_filepath = api_init_filepath.parent.joinpath('__init__.py') if 'service' in str_code_filepath:
if not app_init_filepath.exists(): app_init_filepath = code_folder.parent.joinpath('__init__.py')
async with aiofiles.open(app_init_filepath, 'w', encoding='utf-8') as f: async with aiofiles.open(app_init_filepath, 'w', encoding='utf-8') as f:
await f.write(gen_template.init_content) await f.write(gen_template.init_content)
# 写入代码文件
async with aiofiles.open(code_filepath, 'w', encoding='utf-8') as f:
await f.write(code)
# model init 文件补充 # model init 文件补充
if code_folder.name == 'model': if code_folder.name == 'model':
async with aiofiles.open(init_filepath, 'a', encoding='utf-8') as f: async with aiofiles.open(init_filepath, 'a', encoding='utf-8') as f:
await f.write( await f.write(
f'from backend.app.{business.app_name}.model.{business.table_name_en} ' f'from backend.app.{business.app_name}.model.{business.table_name} '
f'import {to_pascal(business.table_name_en)}\n', f'import {to_pascal(business.table_name)}\n',
) )
# 写入代码文件
async with aiofiles.open(code_filepath, 'w', encoding='utf-8') as f:
await f.write(code)
async def download(self, *, pk: int) -> io.BytesIO: async def download(self, *, pk: int) -> io.BytesIO:
""" """
下载生成的代码 下载生成的代码
@@ -206,27 +218,34 @@ class GenService:
with zipfile.ZipFile(bio, 'w') as zf: with zipfile.ZipFile(bio, 'w') as zf:
tpl_code_map = await self.render_tpl_code(business=business) tpl_code_map = await self.render_tpl_code(business=business)
for tpl_path, code in tpl_code_map.items(): for tpl_path, code in tpl_code_map.items():
# 写入代码文件 code_filepath = gen_template.get_code_gen_path(tpl_path, business)
new_code_path = gen_template.get_code_gen_path(tpl_path, business)
zf.writestr(new_code_path, code)
# 写入 init 文件 # 写入 init 文件
init_filepath = os.path.join(*new_code_path.split('/')[:-1], '__init__.py') code_dir = os.path.dirname(code_filepath)
if 'model' not in new_code_path.split('/'): init_filepath = os.path.join(code_dir, '__init__.py')
if 'model' not in code_filepath.split('/'):
zf.writestr(init_filepath, gen_template.init_content) zf.writestr(init_filepath, gen_template.init_content)
else: else:
zf.writestr( zf.writestr(
init_filepath, init_filepath,
f'{gen_template.init_content}' f'{gen_template.init_content}'
f'from backend.app.{business.app_name}.model.{business.table_name_en} ' f'from backend.app.{business.app_name}.model.{business.table_name} '
f'import {to_pascal(business.table_name_en)}\n', f'import {to_pascal(business.table_name)}\n',
) )
if 'api' in new_code_path:
# api __init__.py # api __init__.py
api_init_filepath = os.path.join(*new_code_path.split('/')[:-2], '__init__.py') if 'api' in code_dir:
api_init_filepath = os.path.join(os.path.dirname(code_dir), '__init__.py')
zf.writestr(api_init_filepath, gen_template.init_content) zf.writestr(api_init_filepath, gen_template.init_content)
# app __init__.py
if 'service' in code_dir:
app_init_filepath = os.path.join(os.path.dirname(code_dir), '__init__.py')
zf.writestr(app_init_filepath, gen_template.init_content)
# 写入代码文件
zf.writestr(code_filepath, code)
bio.seek(0) bio.seek(0)
return bio return bio

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