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 }}
strategy:
matrix:
python-version: [ '3.10', '3.11', '3.12' ]
python-version: [ '3.10', '3.11', '3.12', '3.13' ]
fail-fast: false
steps:
- uses: actions/checkout@v4
- name: Install uv
uses: astral-sh/setup-uv@v3
uses: astral-sh/setup-uv@v5
- name: Set up Python ${{ matrix.python-version }}
run: uv python install ${{ matrix.python-version }}
+2 -2
View File
@@ -8,7 +8,7 @@ repos:
- id: check-toml
- repo: https://github.com/charliermarsh/ruff-pre-commit
rev: v0.9.5
rev: v0.11.2
hooks:
- id: ruff
args:
@@ -20,7 +20,7 @@ repos:
- id: ruff-format
- repo: https://github.com/astral-sh/uv-pre-commit
rev: 0.5.29
rev: 0.6.10
hooks:
- id: uv-lock
- 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>
# [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]
[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.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
+1 -1
View File
@@ -1,6 +1,6 @@
__pycache__/
.env
log/
alembic/versions/
static/media/
*.log
celerybeat-schedule.*
+1 -1
View File
@@ -20,7 +20,7 @@ from backend.plugin.tools import get_plugin_models
# import your new model here
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
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()
_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 授权链接')
async def github_auth2() -> ResponseSchemaModel[str]:
auth_url = await _github_client.get_authorization_url(redirect_uri=admin_settings.OAUTH2_GITHUB_REDIRECT_URI)
async def github_oauth2(request: Request) -> ResponseSchemaModel[str]:
auth_url = await _github_client.get_authorization_url(redirect_uri=f'{request.url}/callback')
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_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 授权链接')
async def linux_do_auth2() -> ResponseSchemaModel[str]:
auth_url = await _linux_do_client.get_authorization_url(redirect_uri=admin_settings.OAUTH2_LINUX_DO_REDIRECT_URI)
async def linux_do_oauth2(request: Request) -> ResponseSchemaModel[str]:
auth_url = await _linux_do_client.get_authorization_url(redirect_uri=f'{request.url}/callback')
return response_base.success(data=auth_url)
+3 -7
View File
@@ -2,11 +2,9 @@
# -*- coding: utf-8 -*-
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_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.dict_data import router as dict_data_router
from backend.app.admin.api.v1.sys.dict_type import router as dict_type_router
from backend.app.admin.api.v1.sys.menu import router as menu_router
from backend.app.admin.api.v1.sys.plugin import router as plugin_router
from backend.app.admin.api.v1.sys.role import router as role_router
@@ -16,14 +14,12 @@ from backend.app.admin.api.v1.sys.user import router as user_router
router = APIRouter(prefix='/sys')
router.include_router(config_router, prefix='/configs', tags=['系统参数配置'])
router.include_router(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(role_router, prefix='/roles', tags=['系统角色'])
router.include_router(user_router, prefix='/users', tags=['系统用户'])
router.include_router(data_rule_router, prefix='/data-rules', tags=['系统数据权限规则'])
router.include_router(data_rule_router, prefix='/data-rules', tags=['系统数据规则'])
router.include_router(data_scope_router, prefix='/data-scopes', tags=['系统数据范围'])
router.include_router(token_router, prefix='/tokens', tags=['系统令牌'])
router.include_router(upload_router, prefix='/upload', tags=['系统上传'])
router.include_router(plugin_router, prefix='/plugin', tags=['系统插件'])
+17 -14
View File
@@ -2,9 +2,14 @@
# -*- coding: utf-8 -*-
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.common.pagination import DependsPagination, PageData, paging_data
from backend.common.response.response_schema import ResponseModel, ResponseSchemaModel, response_base
@@ -16,16 +21,16 @@ from backend.database.db import CurrentSession
router = APIRouter()
@router.get('/models', summary='获取支持过滤的数据库模型', dependencies=[DependsJwtAuth])
@router.get('/models', summary='获取数据规则可用模型', dependencies=[DependsJwtAuth])
async def get_data_rule_models() -> ResponseSchemaModel[list[str]]:
models = await data_rule_service.get_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(
model: Annotated[str, Path(description='模型名称')],
) -> ResponseSchemaModel[list[str]]:
) -> ResponseSchemaModel[list[GetDataRuleColumnDetail]]:
models = await data_rule_service.get_columns(model=model)
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)
@router.get('/{pk}', summary='获取数据权限规则详情', dependencies=[DependsJwtAuth])
@router.get('/{pk}', summary='获取数据规则详情', dependencies=[DependsJwtAuth])
async def get_data_rule(
pk: Annotated[int, Path(description='数据规则 ID')],
) -> ResponseSchemaModel[GetDataRuleDetail]:
@@ -46,7 +51,7 @@ async def get_data_rule(
@router.get(
'',
summary='分页获取所有数据权限规则',
summary='分页获取所有数据规则',
dependencies=[
DependsJwtAuth,
DependsPagination,
@@ -62,7 +67,7 @@ async def get_pagination_data_rules(
@router.post(
'',
summary='创建数据权限规则',
summary='创建数据规则',
dependencies=[
Depends(RequestPermission('data:rule:add')),
DependsRBAC,
@@ -75,7 +80,7 @@ async def create_data_rule(obj: CreateDataRuleParam) -> ResponseModel:
@router.put(
'/{pk}',
summary='更新数据权限规则',
summary='更新数据规则',
dependencies=[
Depends(RequestPermission('data:rule:edit')),
DependsRBAC,
@@ -92,16 +97,14 @@ async def update_data_rule(
@router.delete(
'',
summary='批量删除数据权限规则',
summary='批量删除数据规则',
dependencies=[
Depends(RequestPermission('data:rule:del')),
DependsRBAC,
],
)
async def delete_data_rule(
request: Request, pk: Annotated[list[int], Query(description='数据规则 ID 列表')]
) -> ResponseModel:
count = await data_rule_service.delete(request=request, pk=pk)
async def delete_data_rule(pk: Annotated[list[int], Query(description='数据规则 ID 列表')]) -> ResponseModel:
count = await data_rule_service.delete(pk=pk)
if count > 0:
return response_base.success()
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])
async def get_all_depts(
request: Request,
name: Annotated[str | None, Query(description='部门名称')] = None,
leader: Annotated[str | None, Query(description='部门负责人')] = None,
phone: Annotated[str | None, Query(description='联系电话')] = None,
status: Annotated[int | None, Query(description='状态')] = None,
) -> ResponseSchemaModel[list[dict[str, Any]]]:
dept = await dept_service.get_dept_tree(name=name, leader=leader, phone=phone, status=status)
dept = await dept_service.get_dept_tree(request=request, name=name, leader=leader, phone=phone, status=status)
return response_base.success(data=dept)
@@ -67,8 +68,8 @@ async def update_dept(pk: Annotated[int, Path(description='部门 ID')], obj: Up
DependsRBAC,
],
)
async def delete_dept(request: Request, pk: Annotated[int, Path(description='部门 ID')]) -> ResponseModel:
count = await dept_service.delete(request=request, pk=pk)
async def delete_dept(pk: Annotated[int, Path(description='部门 ID')]) -> ResponseModel:
count = await dept_service.delete(pk=pk)
if count > 0:
return response_base.success()
return response_base.fail()
+3 -3
View File
@@ -14,7 +14,7 @@ from backend.common.security.rbac import DependsRBAC
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]]]:
menu = await menu_service.get_user_menu_tree(request=request)
return response_base.success(data=menu)
@@ -71,8 +71,8 @@ async def update_menu(pk: Annotated[int, Path(description='菜单 ID')], obj: Up
DependsRBAC,
],
)
async def delete_menu(request: Request, pk: Annotated[int, Path(description='菜单 ID 列表')]) -> ResponseModel:
count = await menu_service.delete(request=request, pk=pk)
async def delete_menu(pk: Annotated[int, Path(description='菜单 ID 列表')]) -> ResponseModel:
count = await menu_service.delete(pk=pk)
if count > 0:
return response_base.success()
return response_base.fail()
+17 -19
View File
@@ -2,7 +2,7 @@
# -*- coding: utf-8 -*-
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 (
CreateRoleParam,
@@ -10,10 +10,8 @@ from backend.app.admin.schema.role import (
GetRoleWithRelationDetail,
UpdateRoleMenuParam,
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.common.pagination import DependsPagination, PageData, paging_data
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(
pk: Annotated[int, Path(description='用户 ID')],
) -> 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)
@@ -43,13 +41,13 @@ async def get_user_all_roles(
async def get_role_all_menus(
pk: Annotated[int, Path(description='角色 ID')],
) -> ResponseSchemaModel[list[dict[str, Any]]]:
menu = await menu_service.get_role_menu_tree(pk=pk)
menu = await role_service.get_menu_tree(pk=pk)
return response_base.success(data=menu)
@router.get('/{pk}/rules', summary='获取角色所有数据规则', dependencies=[DependsJwtAuth])
async def get_role_all_rules(pk: Annotated[int, Path(description='角色 ID')]) -> ResponseSchemaModel[list[int]]:
rule = await data_rule_service.get_role_rules(pk=pk)
@router.get('/{pk}/scopes', summary='获取角色所有数据范围', dependencies=[DependsJwtAuth])
async def get_role_all_scopes(pk: Annotated[int, Path(description='角色 ID')]) -> ResponseSchemaModel[list[int]]:
rule = await role_service.get_scopes(pk=pk)
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(
request: Request, pk: Annotated[int, Path(description='角色 ID')], menu_ids: UpdateRoleMenuParam
pk: Annotated[int, Path(description='角色 ID')], menu_ids: UpdateRoleMenuParam
) -> ResponseModel:
count = await role_service.update_role_menu(request=request, pk=pk, menu_ids=menu_ids)
count = await role_service.update_role_menu(pk=pk, menu_ids=menu_ids)
if count > 0:
return response_base.success()
return response_base.fail()
@router.put(
'/{pk}/rule',
summary='更新角色数据权限规则',
'/{pk}/scope',
summary='更新角色数据范围',
dependencies=[
Depends(RequestPermission('sys:role:rule:edit')),
Depends(RequestPermission('sys:role:scope:edit')),
DependsRBAC,
],
)
async def update_role_rules(
request: Request, pk: Annotated[int, Path(description='角色 ID')], rule_ids: UpdateRoleRuleParam
async def update_role_scopes(
pk: Annotated[int, Path(description='角色 ID')], scope_ids: UpdateRoleScopeParam
) -> 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:
return response_base.success()
return response_base.fail()
@@ -149,8 +147,8 @@ async def update_role_rules(
DependsRBAC,
],
)
async def delete_role(request: Request, pk: Annotated[list[int], Query(description='角色 ID 列表')]) -> ResponseModel:
count = await role_service.delete(request=request, pk=pk)
async def delete_role(pk: Annotated[list[int], Query(description='角色 ID 列表')]) -> ResponseModel:
count = await role_service.delete(pk=pk)
if count > 0:
return response_base.success()
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}')
if 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 == extra_info.get('username'):
append_token_detail()
-1
View File
@@ -1,6 +1,5 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from typing import Annotated
from fastapi import APIRouter, File, UploadFile
+1 -6
View File
@@ -19,21 +19,16 @@ class AdminSettings(BaseSettings):
OAUTH2_LINUX_DO_CLIENT_SECRET: str
# OAuth2
OAUTH2_GITHUB_REDIRECT_URI: str = 'http://127.0.0.1:8000/api/v1/oauth2/github/callback'
OAUTH2_LINUX_DO_REDIRECT_URI: str = 'http://127.0.0.1:8000/api/v1/oauth2/linux-do/callback'
OAUTH2_FRONTEND_REDIRECT_URI: str = 'http://localhost:5173/oauth2/callback'
# 验证码
CAPTCHA_LOGIN_REDIS_PREFIX: str = 'fba:login:captcha'
CAPTCHA_LOGIN_EXPIRE_SECONDS: int = 60 * 5 # 3 分钟
# 参数配置
CONFIG_BUILT_IN_TYPES: list[str] = ['website', 'protocol', 'policy']
@lru_cache
def get_admin_settings() -> AdminSettings:
"""获取 admin 参数配置"""
"""获取 admin 配置"""
return AdminSettings()
+3 -3
View File
@@ -12,7 +12,7 @@ from backend.app.admin.schema.data_rule import CreateDataRuleParam, UpdateDataRu
class CRUDDataRule(CRUDPlus[DataRule]):
"""数据权限规则数据库操作类"""
"""数据规则数据库操作类"""
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)
async def get_list(self, name: str | None = None) -> Select:
async def get_list(self, name: str | None) -> Select:
"""
获取规则列表
:param name: 规则名称
: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 = []
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 -*-
from typing import Sequence
from fastapi import Request
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
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.schema.dept import CreateDeptParam, UpdateDeptParam
from backend.common.security.permission import filter_data_permission
class CRUDDept(CRUDPlus[Dept]):
@@ -36,15 +38,17 @@ class CRUDDept(CRUDPlus[Dept]):
async def get_all(
self,
request: Request,
db: AsyncSession,
name: str | None = None,
leader: str | None = None,
phone: str | None = None,
status: int | None = None,
name: str | None,
leader: str | None,
phone: str | None,
status: int | None,
) -> Sequence[Dept]:
"""
获取所有部门
:param request: FastAPI 请求对象
:param db: 数据库会话
:param name: 部门名称
:param leader: 负责人
@@ -61,7 +65,7 @@ class CRUDDept(CRUDPlus[Dept]):
filters.update(phone__startswith=phone)
if status is not None:
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:
"""
+1 -1
View File
@@ -11,7 +11,7 @@ from backend.app.admin.schema.login_log import CreateLoginLogParam
class CRUDLoginLog(CRUDPlus[LoginLog]):
"""登录日志数据库操作类"""
async def get_list(self, username: str | None = None, status: int | None = None, ip: str | None = None) -> Select:
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: 菜单标题
: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:
"""
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:
filters.append(self.model.id.in_(menu_ids))
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]):
"""操作日志数据库操作类"""
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_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 (
CreateRoleParam,
UpdateRoleMenuParam,
UpdateRoleParam,
UpdateRoleRuleParam,
UpdateRoleScopeParam,
)
@@ -39,7 +39,7 @@ class CRUDRole(CRUDPlus[Role]):
"""
stmt = (
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)
)
role = await db.execute(stmt)
@@ -54,7 +54,7 @@ class CRUDRole(CRUDPlus[Role]):
"""
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)
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 = (
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))
)
@@ -137,20 +137,20 @@ class CRUDRole(CRUDPlus[Role]):
current_role.menus = menus.scalars().all()
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 role_id: 角色 ID
:param rule_ids: 权限规则 ID 列表
:param scope_ids: 权限范围 ID 列表
:return:
"""
current_role = await self.get_with_relation(db, role_id)
stmt = select(DataRule).where(DataRule.id.in_(rule_ids.rules))
rules = await db.execute(stmt)
current_role.rules = rules.scalars().all()
return len(current_role.rules)
stmt = select(DataScope).where(DataScope.id.in_(scope_ids.scopes))
scopes = await db.execute(stmt)
current_role.scopes = scopes.scalars().all()
return len(current_role.scopes)
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: 头像地址
: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:
"""
@@ -175,9 +175,7 @@ class CRUDUser(CRUDPlus[User]):
"""
return await self.update_model(db, pk, {'password': new_pwd})
async def get_list(
self, dept: int | None = None, username: str | None = None, phone: str | None = None, status: int | None = None
) -> Select:
async def get_list(self, dept: int | None, username: str | None, phone: str | None, status: int | None) -> Select:
"""
获取用户列表
@@ -313,7 +311,7 @@ class CRUDUser(CRUDPlus[User]):
"""
stmt = select(self.model).options(
selectinload(self.model.dept),
selectinload(self.model.roles).options(selectinload(Role.menus), selectinload(Role.rules)),
selectinload(self.model.roles).options(selectinload(Role.menus), selectinload(Role.scopes)),
)
filters = []
+1 -3
View File
@@ -1,10 +1,8 @@
#!/usr/bin/env python3
# -*- 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_scope import DataScope
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.menu import Menu
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 sqlalchemy import String
from sqlalchemy import ForeignKey, String
from sqlalchemy.orm import Mapped, mapped_column, relationship
from backend.app.admin.model.m2m import sys_role_data_rule
from backend.common.model import Base, id_key
if TYPE_CHECKING:
from backend.app.admin.model import Role
from backend.app.admin.model import DataScope
class DataRule(Base):
"""数据权限规则表"""
"""数据规则表"""
__tablename__ = 'sys_data_rule'
id: Mapped[id_key] = mapped_column(init=False)
name: Mapped[str] = mapped_column(String(255), unique=True, comment='规则名称')
model: Mapped[str] = mapped_column(String(50), comment='SQLA 模型')
column: Mapped[str] = mapped_column(String(20), comment='数据库字段')
name: Mapped[str] = mapped_column(String(500), unique=True, comment='名称')
model: Mapped[str] = mapped_column(String(50), comment='SQLA 模型名,对应 DATA_PERMISSION_MODELS 键名')
column: Mapped[str] = mapped_column(String(20), comment='模型字段')
operator: Mapped[int] = mapped_column(comment='运算符(0and、1or')
expression: Mapped[int] = mapped_column(
comment='表达式(0==、1!=、2>、3>=、4<、5<=、6in、7not_in'
)
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'),
)
sys_role_data_rule = Table(
'sys_role_data_rule',
sys_role_data_scope = Table(
'sys_role_data_scope',
MappedBase.metadata,
Column('id', INT, primary_key=True, unique=True, index=True, autoincrement=True, comment='主键ID'),
Column('role_id', Integer, ForeignKey('sys_role.id', ondelete='CASCADE'), primary_key=True, comment='角色ID'),
Column('id', 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(
'data_rule_id',
'data_scope_id',
Integer,
ForeignKey('sys_data_rule.id', ondelete='CASCADE'),
ForeignKey('sys_data_scope.id', ondelete='CASCADE'),
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)
title: 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='排序')
icon: Mapped[str | None] = mapped_column(String(100), default=None, comment='菜单图标')
path: Mapped[str | None] = mapped_column(String(200), default=None, comment='路由地址')
menu_type: Mapped[int] = mapped_column(default=0, comment='菜单类型(0目录 1菜单 2按钮)')
type: Mapped[int] = mapped_column(default=0, comment='菜单类型(0目录 1菜单 2按钮)')
component: Mapped[str | None] = mapped_column(String(255), default=None, comment='组件路径')
perms: Mapped[str | None] = mapped_column(String(100), default=None, comment='权限标识')
status: Mapped[int] = mapped_column(default=1, comment='菜单状态(0停用 1正常)')
display: 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(
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.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
if TYPE_CHECKING:
from backend.app.admin.model import DataRule, Menu, User
from backend.app.admin.model import DataScope, Menu, User
class Role(Base):
@@ -34,5 +34,5 @@ class Role(Base):
# 角色菜单多对多
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='规则名称')
model: 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='表达式类型')
value: str = Field(description='规则值')
@@ -36,6 +36,9 @@ class GetDataRuleDetail(DataRuleSchemaBase):
created_time: datetime = Field(description='创建时间')
updated_time: datetime | None = Field(None, description='更新时间')
def __hash__(self) -> int:
"""计算哈希值"""
return hash(self.name)
class GetDataRuleColumnDetail(SchemaBase):
"""数据规则可用模型字段详情"""
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='菜单标题')
name: str = Field(description='菜单名称')
path: str = Field(description='路由路径')
parent_id: int | None = Field(None, description='菜单父级 ID')
sort: int = Field(0, ge=0, description='排序')
icon: str | None = Field(None, description='图标')
path: str | None = Field(None, description='路由路径')
menu_type: MenuType = Field(MenuType.directory, description='菜单类型(0目录 1菜单 2按钮)')
type: MenuType = Field(MenuType.directory, description='菜单类型(0目录 1菜单 2按钮)')
component: str | None = Field(None, description='组件路径')
perms: str | None = Field(None, description='权限标识')
status: StatusType = Field(StatusType.enable, description='状态')
display: StatusType = Field(StatusType.enable, description='是否显示')
cache: StatusType = Field(StatusType.enable, description='是否缓存')
link: 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 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.common.enums import StatusType
from backend.common.schema import SchemaBase
@@ -32,10 +32,10 @@ class UpdateRoleMenuParam(SchemaBase):
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):
@@ -52,4 +52,4 @@ class GetRoleWithRelationDetail(GetRoleDetail):
"""角色关联详情"""
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
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)
if not user:
raise errors.NotFoundError(msg='用户名或密码有误')
elif not password_verify(password, user.password):
if user.password is None:
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='用户已被锁定, 请联系统管理员')
return user
async def swagger_login(self, *, obj: HTTPBasicCredentials) -> tuple[str, User]:
@@ -65,7 +71,7 @@ class AuthService:
str(user.id),
user.is_multi_login,
# extra info
login_type='swagger',
swagger=True,
)
return a_token.access_token, user
+17 -31
View File
@@ -2,22 +2,19 @@
# -*- coding: utf-8 -*-
from typing import Sequence
from fastapi import Request
from sqlalchemy import Select
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.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.core.conf import settings
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
class DataRuleService:
"""数据权限规则服务类"""
"""数据规则服务类"""
@staticmethod
async def get(*, pk: int) -> DataRule:
@@ -33,44 +30,32 @@ class DataRuleService:
raise errors.NotFoundError(msg='数据规则不存在')
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
async def get_models() -> list[str]:
"""获取所有数据模型"""
"""获取所有数据规则可用模型"""
return list(settings.DATA_PERMISSION_MODELS.keys())
@staticmethod
async def get_columns(model: str) -> list[str]:
async def get_columns(model: str) -> list[GetDataRuleColumnDetail]:
"""
获取数据模型的字段列表
获取数据规则可用模型的字段列表
:param model: 模型名称
:return:
"""
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_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
@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:
data_rule = await data_rule_dao.get_by_name(db, obj.name)
if data_rule:
raise errors.ForbiddenError(msg='数据权限规则已存在')
raise errors.ForbiddenError(msg='数据规则已存在')
await data_rule_dao.create(db, obj)
@staticmethod
@@ -112,22 +97,23 @@ class DataRuleService:
async with async_db_session.begin() as db:
data_rule = await data_rule_dao.get(db, pk)
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)
return count
@staticmethod
async def delete(*, request: Request, pk: list[int]) -> int:
async def delete(*, pk: list[int]) -> int:
"""
删除数据规则
:param request: FastAPI 请求对象
:param pk: 规则 ID 列表
:return:
"""
async with async_db_session.begin() as db:
count = await data_rule_dao.delete(db, pk)
await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{request.user.id}')
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
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]]:
"""
获取部门树形结构
:param request: FastAPI 请求对象
:param name: 部门名称
:param leader: 部门负责人
:param phone: 联系电话
@@ -45,7 +46,7 @@ class DeptService:
:return:
"""
async with async_db_session() as db:
dept_select = await dept_dao.get_all(db=db, name=name, leader=leader, phone=phone, status=status)
dept_select = await dept_dao.get_all(request, db, name, leader, phone, status)
tree_data = get_tree_data(dept_select)
return tree_data
@@ -93,24 +94,23 @@ class DeptService:
return count
@staticmethod
async def delete(*, request: Request, pk: int) -> int:
async def delete(*, pk: int) -> int:
"""
删除部门
:param request: FastAPI 请求对象
:param pk: 部门 ID
:return:
"""
async with async_db_session.begin() as db:
dept = await dept_dao.get_with_relation(db, pk)
dept_user = dept.users
if dept_user:
if dept.users:
raise errors.ForbiddenError(msg='部门下存在用户,无法删除')
children = await dept_dao.get_children(db, pk)
if children:
raise errors.ForbiddenError(msg='部门下存在子部门,无法删除')
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
@@ -16,7 +16,7 @@ class LoginLogService:
"""登录日志服务类"""
@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 backend.app.admin.crud.crud_menu import menu_dao
from backend.app.admin.crud.crud_role import role_dao
from backend.app.admin.model import Menu
from backend.app.admin.schema.menu import CreateMenuParam, UpdateMenuParam
from backend.common.exception import errors
from backend.core.conf import settings
from backend.database.db import async_db_session
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:
@@ -33,7 +32,7 @@ class MenuService:
return menu
@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)
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
async def get_user_menu_tree(*, request: Request) -> list[dict[str, Any]]:
"""
@@ -79,7 +61,7 @@ class MenuService:
for role in roles:
menu_ids.extend([menu.id for menu in role.menus])
menu_select = await menu_dao.get_role_menus(db, request.user.is_superuser, menu_ids)
menu_tree = get_tree_data(menu_select)
menu_tree = get_vben5_tree_data(menu_select)
return menu_tree
@staticmethod
@@ -123,14 +105,16 @@ class MenuService:
if obj.parent_id == menu.id:
raise errors.ForbiddenError(msg='禁止关联自身为父级')
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
@staticmethod
async def delete(*, request: Request, pk: int) -> int:
async def delete(*, pk: int) -> int:
"""
删除菜单
:param request: FastAPI 请求对象
:param pk: 菜单 ID
:return:
"""
@@ -138,8 +122,12 @@ class MenuService:
children = await menu_dao.get_children(db, pk)
if children:
raise errors.ForbiddenError(msg='菜单下存在子菜单,无法删除')
menu = await menu_dao.get(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
@@ -11,7 +11,7 @@ class OperaLogService:
"""操作日志服务类"""
@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
# -*- coding: utf-8 -*-
from typing import Sequence
from typing import Any, Sequence
from fastapi import Request
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_role import role_dao
from backend.app.admin.model import Role
@@ -13,12 +12,13 @@ from backend.app.admin.schema.role import (
CreateRoleParam,
UpdateRoleMenuParam,
UpdateRoleParam,
UpdateRoleRuleParam,
UpdateRoleScopeParam,
)
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
from backend.utils.build_tree import get_tree_data
class RoleService:
@@ -46,7 +46,7 @@ class RoleService:
return roles
@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:
"""
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
@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)
@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
async def create(*, obj: CreateRoleParam) -> None:
"""
@@ -100,20 +132,21 @@ class RoleService:
if role:
raise errors.ForbiddenError(msg='角色已存在')
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
@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 menu_ids: 菜单 ID 列表
:return:
"""
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:
raise errors.NotFoundError(msg='角色不存在')
for menu_id in menu_ids.menus:
@@ -121,45 +154,47 @@ class RoleService:
if not menu:
raise errors.NotFoundError(msg='菜单不存在')
count = await role_dao.update_menus(db, pk, menu_ids)
if pk in [role.id for role in request.user.roles]:
await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{request.user.id}')
for user in await role.awaitable_attrs.users:
await redis_client.delete_prefix(f'{settings.JWT_USER_REDIS_PREFIX}:{user.id}')
return count
@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 rule_ids: 权限规则 ID 列表
:param scope_ids: 权限规则 ID 列表
:return:
"""
async with async_db_session.begin() as db:
role = await role_dao.get(db, pk)
if not role:
raise errors.NotFoundError(msg='角色不存在')
for rule_id in rule_ids.rules:
rule = await data_rule_dao.get(db, rule_id)
if not rule:
raise errors.NotFoundError(msg='数据权限不存在')
count = await role_dao.update_rules(db, pk, rule_ids)
if pk in [role.id for role in request.user.roles]:
await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{request.user.id}')
for scope_id in scope_ids.scopes:
scope = await data_scope_dao.get(db, scope_id)
if not scope:
raise errors.NotFoundError(msg='数据范围不存在')
count = await role_dao.update_scopes(db, pk, scope_ids)
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 delete(*, request: Request, pk: list[int]) -> int:
async def delete(*, pk: list[int]) -> int:
"""
删除角色
:param request: FastAPI 请求对象
:param pk: 角色 ID 列表
:return:
"""
async with async_db_session.begin() as db:
count = await role_dao.delete(db, pk)
await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{request.user.id}')
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
+30 -40
View File
@@ -135,25 +135,25 @@ class UserService:
:return:
"""
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='你只能修改自己的信息')
input_user = await user_dao.get_with_relation(db, username=username)
if not input_user:
user = await user_dao.get_with_relation(db, username=username)
if not user:
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)
if _username:
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)
if nickname:
raise errors.ForbiddenError(msg='昵称已注册')
if input_user.email != obj.email:
if user.email != obj.email:
email = await user_dao.check_email(db, obj.email)
if email:
raise errors.ForbiddenError(msg='邮箱已注册')
count = await user_dao.update_userinfo(db, input_user.id, obj)
await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{request.user.id}')
count = await user_dao.update_userinfo(db, user.id, obj)
await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{user.id}')
return count
@staticmethod
@@ -168,7 +168,7 @@ class UserService:
"""
async with async_db_session.begin() as db:
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)
if not input_user:
raise errors.NotFoundError(msg='用户不存在')
@@ -190,17 +190,17 @@ class UserService:
:return:
"""
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
input_user = await user_dao.get_by_username(db, username)
if not input_user:
user = await user_dao.get_by_username(db, username)
if not user:
raise errors.NotFoundError(msg='用户不存在')
count = await user_dao.update_avatar(db, input_user.id, avatar)
await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{request.user.id}')
count = await user_dao.update_avatar(db, user.id, avatar)
await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{user.id}')
return count
@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='非法操作')
super_status = await user_dao.get_super(db, pk)
count = await user_dao.set_super(db, pk, not super_status)
await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{pk}')
await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{user.id}')
return count
@staticmethod
@@ -251,7 +251,7 @@ class UserService:
raise errors.ForbiddenError(msg='非法操作')
staff_status = await user_dao.get_staff(db, pk)
count = await user_dao.set_staff(db, pk, not staff_status)
await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{pk}')
await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{user.id}')
return count
@staticmethod
@@ -272,7 +272,7 @@ class UserService:
raise errors.ForbiddenError(msg='非法操作')
status = await user_dao.get_status(db, pk)
count = await user_dao.set_status(db, pk, 0 if status == 1 else 1)
await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{pk}')
await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{user.id}')
return count
@staticmethod
@@ -289,32 +289,22 @@ class UserService:
user = await user_dao.get(db, pk)
if not user:
raise errors.NotFoundError(msg='用户不存在')
user_id = request.user.id
multi_login = await user_dao.get_multi_login(db, pk) if pk != user_id else request.user.is_multi_login
count = await user_dao.set_multi_login(db, pk, not multi_login)
# 删除当前用户缓存
await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{request.user.id}')
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, new_multi_login)
await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{user.id}')
token = get_token(request)
token_payload = jwt_decode(token)
latest_multi_login = await user_dao.get_multi_login(db, pk)
# 超级用户修改自身时,除当前 token 外,其他 token 失效
if pk == user_id:
if not latest_multi_login:
key_prefix = f'{settings.TOKEN_REDIS_PREFIX}:{pk}'
if pk == user.id:
# 系统管理员修改自身时,除当前 token 外,其他 token 失效
if not new_multi_login:
key_prefix = f'{settings.TOKEN_REDIS_PREFIX}:{user.id}'
await redis_client.delete_prefix(key_prefix, exclude=f'{key_prefix}:{token_payload.session_uuid}')
refresh_token = request.cookies.get(settings.COOKIE_REFRESH_TOKEN_KEY)
if refresh_token:
key_prefix = f'{settings.TOKEN_REFRESH_REDIS_PREFIX}:{pk}'
await redis_client.delete_prefix(key_prefix, exclude=f'{key_prefix}:{refresh_token}')
# 超级用户修改他人时,其他 token 将全部失效
else:
if not latest_multi_login:
key_prefix = [f'{settings.TOKEN_REDIS_PREFIX}:{pk}']
refresh_token = request.cookies.get(settings.COOKIE_REFRESH_TOKEN_KEY)
if refresh_token:
key_prefix.append(f'{settings.TOKEN_REFRESH_REDIS_PREFIX}:{pk}')
for prefix in key_prefix:
await redis_client.delete_prefix(prefix)
# 系统管理员修改他人时,他人 token 全部失效
if not new_multi_login:
key_prefix = f'{settings.TOKEN_REDIS_PREFIX}:{user.id}'
await redis_client.delete_prefix(key_prefix)
return count
@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 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
router = APIRouter()
router.include_router(admin_v1)
router.include_router(generator_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.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
# -*- coding: utf-8 -*-
import asyncio
from typing import Any
from celery import Task
@@ -34,7 +36,7 @@ class TaskBase(Task):
"""
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: 异常信息
: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):
"""数据权限规则运算符"""
"""数据规则运算符"""
AND = 0
OR = 1
class RoleDataRuleExpressionType(IntEnum):
"""数据权限规则表达式"""
"""数据规则表达式"""
eq = 0 # ==
ne = 1 # !=
@@ -119,153 +119,3 @@ class FileType(StrEnum):
image = 'image'
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_pagination import pagination_ctx
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 pydantic import BaseModel, Field
@@ -24,7 +24,7 @@ class _CustomPageParams(BaseModel, AbstractParams):
"""自定义分页参数"""
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:
return RawParams(
@@ -63,8 +63,8 @@ class _CustomPage(_PageDetails, AbstractPage[T], Generic[T]):
def create(
cls,
items: list,
total: int,
params: _CustomPageParams,
total: int = 0,
) -> _CustomPage[T]:
page = params.page
size = params.size
@@ -119,7 +119,7 @@ async def paging_data(db: AsyncSession, select: Select) -> dict[str, Any]:
:param select: SQL 查询语句
:return:
"""
paginated_data: _CustomPage = await paginate(db, select)
paginated_data: _CustomPage = await apaginate(db, select)
page_data = paginated_data.model_dump()
return page_data
+4 -2
View File
@@ -40,7 +40,7 @@ class ResponseModel(BaseModel):
class ResponseSchemaModel(ResponseModel, Generic[SchemaT]):
"""
包含返回数据 schema 的通用型统一返回模型仅适用于非分页接口
包含返回数据 schema 的通用型统一返回模型
示例::
@@ -68,7 +68,9 @@ class ResponseBase:
@staticmethod
def __response(
*, res: CustomResponseCode | CustomResponse = None, data: Any | None = None
*,
res: CustomResponseCode | CustomResponse,
data: Any | None,
) -> 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
# 自定义验证错误信息
# 不包含验证预期内容(也就是输入内容),受支持的预期内容字段参考以下链接
# 自定义验证错误信息,参考:
# 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
CUSTOM_VALIDATION_ERROR_MESSAGES = {
'arguments_type': '参数类型输入错误',
'assertion_error': '断言执行错误',
'bool_parsing': '布尔值输入解析错误',
'bool_type': '布尔值类型输入错误',
'bytes_too_long': '字节长度输入过长',
'bytes_too_short': '字节长度输入过短',
'bytes_type': '字节类型输入错误',
'callable_type': '可调用对象类型输入错误',
'dataclass_exact_type': '数据类实例类型输入错误',
'dataclass_type': '数据类类型输入错误',
'date_from_datetime_inexact': '日期分量输入非零',
'date_from_datetime_parsing': '日期输入解析错误',
'date_future': '日期输入非将来时',
'date_parsing': '日期输入验证错误',
'date_past': '日期输入非过去时',
'date_type': '日期类型输入错误',
'datetime_future': '日期时间输入非将来时间',
'datetime_object_invalid': '日期时间输入对象无效',
'datetime_parsing': '日期时间输入解析错误',
'datetime_past': '日期时间输入非过去时间',
'datetime_type': '日期时间类型输入错误',
'decimal_max_digits': '小数位数输入过多',
'decimal_max_places': '小数位数输入错误',
'decimal_parsing': '小数输入解析错误',
'decimal_type': '小数类型输入错误',
'decimal_whole_digits': '小数位数输入错误',
'dict_type': '字典类型输入错误',
'enum': '枚举成员输入错误,允许 {expected}',
'extra_forbidden': '禁止额外字段输入',
'finite_number': '有限值输入错误',
'float_parsing': '浮点数输入解析错误',
'float_type': '浮点数类型输入错误',
'frozen_field': '冻结字段输入错误',
'frozen_instance': '冻结实例禁止修改',
'frozen_set_type': '冻结类型禁止输入',
'get_attribute_error': '获取属性错误',
'greater_than': '输入值过大',
'greater_than_equal': '输入值过大或相等',
'int_from_float': '整数类型输入错误',
'int_parsing': '整数输入解析错误',
'int_parsing_size': '整数输入解析长度错误',
'int_type': '整数类型输入错误',
'invalid_key': '输入无效键值',
'is_instance_of': '类型实例输入错误',
'is_subclass_of': '类型子类输入错误',
'iterable_type': '可迭代类型输入错误',
'iteration_error': '迭代值输入错误',
'json_invalid': 'JSON 字符串输入错误',
'json_type': 'JSON 类型输入错误',
'less_than': '输入值过小',
'less_than_equal': '输入值过小或相等',
'list_type': '列表类型输入错误',
'literal_error': '字面值输入错误',
'mapping_type': '映射类型输入错误',
'missing': '缺少必填字段',
'missing_argument': '缺少参数',
'missing_keyword_only_argument': '缺少关键字参数',
'missing_positional_only_argument': '缺少位置参数',
'model_attributes_type': '模型属性类型输入错误',
'model_type': '模型实例输入错误',
'multiple_argument_values': '参数值输入过多',
'multiple_of': '输入值非倍数',
'no_such_attribute': '分配无效属性值',
'none_required': '输入值必须为 None',
'recursion_loop': '输入循环赋值',
'set_type': '集合类型输入错误',
'string_pattern_mismatch': '字符串约束模式输入不匹配',
'string_sub_type': '字符串子类型(非严格实例)输入错误',
'string_too_long': '字符串输入过长',
'string_too_short': '字符串输入过短',
'string_type': '字符串类型输入错误',
'string_unicode': '字符串输入非 Unicode',
'time_delta_parsing': '时间差输入解析错误',
'time_delta_type': '时间差类型输入错误',
'time_parsing': '时间输入解析错误',
'time_type': '时间类型输入错误',
'timezone_aware': '缺少时区输入信息',
'timezone_naive': '禁止时区输入信息',
'too_long': '输入过长',
'too_short': '输入过短',
'tuple_type': '元组类型输入错误',
'unexpected_keyword_argument': '输入意外关键字参数',
'unexpected_positional_argument': '输入意外位置参数',
'union_tag_invalid': '联合类型字面值输入错误',
'union_tag_not_found': '联合类型参数输入未找到',
'url_parsing': 'URL 输入解析错误',
'url_scheme': 'URL 输入方案错误',
'url_syntax_violation': 'URL 输入语法错误',
'url_too_long': 'URL 输入过长',
'url_type': 'URL 类型输入错误',
'uuid_parsing': 'UUID 输入解析错误',
'uuid_type': 'UUID 类型输入错误',
'uuid_version': 'UUID 版本类型输入错误',
'value_error': '值输入错误',
'no_such_attribute': "对象没有属性 '{attribute}'",
'json_invalid': '无效的 JSON: {error}',
'json_type': 'JSON 输入应为字符串、字节或字节数组',
'recursion_loop': '递归错误 - 检测到循环引用',
'model_type': '输入应为有效的字典或 {class_name} 的实例',
'model_attributes_type': '输入应为有效的字典或可提取字段的对象',
'dataclass_exact_type': '输入应为 {class_name} 的实例',
'dataclass_type': '输入应为字典或 {class_name} 的实例',
'missing': '字段为必填项',
'frozen_field': '字段已冻结',
'frozen_instance': '实例已冻结',
'extra_forbidden': '不允许额外的输入',
'invalid_key': '键应为字符串',
'get_attribute_error': '提取属性时出错: {error}',
'none_required': '输入应为 None',
'enum': '输入应为 {expected}',
'greater_than': '输入应大于 {gt}',
'greater_than_equal': '输入应大于或等于 {ge}',
'less_than': '输入应小于 {lt}',
'less_than_equal': '输入应小于或等于 {le}',
'finite_number': '输入应为有限数字',
'too_short': '{field_type} 在验证后应至少有 {min_length} 个项目,而不是 {actual_length}',
'too_long': '{field_type} 在验证后最多应有 {max_length} 个项目,而不是 {actual_length}',
'string_type': '输入应为有效的字符串',
'string_sub_type': '输入应为字符串,而不是 str 子类的实例',
'string_unicode': '输入应为有效的字符串,无法将原始数据解析为 Unicode 字符串',
'string_pattern_mismatch': "字符串应匹配模式 '{pattern}'",
'string_too_short': '字符串应至少有 {min_length} 个字符',
'string_too_long': '字符串最多应有 {max_length} 个字符',
'dict_type': '输入应为有效的字典',
'mapping_type': '输入应为有效的映射,错误: {error}',
'iterable_type': '输入应为可迭代对象',
'iteration_error': '迭代对象时出错,错误: {error}',
'list_type': '输入应为有效的列表',
'tuple_type': '输入应为有效的元组',
'set_type': '输入应为有效的集合',
'bool_type': '输入应为有效的布尔',
'bool_parsing': '输入应为有效的布尔值,无法解释输入',
'int_type': '输入应为有效的整数',
'int_parsing': '输入应为有效的整数,无法将字符串解析为整数',
'int_parsing_size': '无法将输入字符串解析为整数,超出最大大小',
'int_from_float': '输入应为有效的整数,得到一个带有小数部分的数字',
'multiple_of': '输入应为 {multiple_of} 的倍数',
'float_type': '输入应为有效的数字',
'float_parsing': '输入应为有效的数字,无法将字符串解析为数字',
'bytes_type': '输入应为有效的字节',
'bytes_too_short': '数据应至少有 {min_length} 个字节',
'bytes_too_long': '数据最多应有 {max_length} 个字节',
'value_error': '值错误,{error}',
'assertion_error': '断言失败,{error}',
'literal_error': '输入应为 {expected}',
'date_type': '输入应为有效的日期',
'date_parsing': '输入应为 YYYY-MM-DD 格式的有效日期,{error}',
'date_from_datetime_parsing': '输入应为有效的日期或日期时间,{error}',
'date_from_datetime_inexact': '提供给日期的日期时间应具有零时间 - 例如为精确日期',
'date_past': '日期应为过去的时间',
'date_future': '日期应为未来的时间',
'time_type': '输入应为有效的时间',
'time_parsing': '输入应为有效的时间格式,{error}',
'datetime_type': '输入应为有效的日期时间',
'datetime_parsing': '输入应为有效的日期时间,{error}',
'datetime_object_invalid': '无效的日期时间对象,得到 {error}',
'datetime_past': '输入应为过去的时间',
'datetime_future': '输入应为未来的时间',
'timezone_naive': '输入不应包含时区信息',
'timezone_aware': '输入应包含时区信息',
'timezone_offset': '需要时区偏移为 {tz_expected},实际得到 {tz_actual}',
'time_delta_type': '输入应为有效的时间差',
'time_delta_parsing': '输入应为有效的时间差,{error}',
'frozen_set_type': '输入应为有效的冻结集合',
'is_instance_of': '输入应为 {class} 的实例',
'is_subclass_of': '输入应为 {class} 的子类',
'callable_type': '输入应为可调用对象',
'union_tag_invalid': "使用 {discriminator} 找到的输入标签 '{tag}' 与任何预期标签不匹配: {expected_tags}",
'union_tag_not_found': '无法使用区分器 {discriminator} 提取标签',
'arguments_type': '参数必须是元组、列表或字典',
'missing_argument': '缺少必需参数',
'unexpected_keyword_argument': '意外的关键字参数',
'missing_keyword_only_argument': '缺少必需的关键字专用参数',
'unexpected_positional_argument': '意外的位置参数',
'missing_positional_only_argument': '缺少必需的位置专用参数',
'multiple_argument_values': '为参数提供了多个值',
'url_type': 'URL 输入应为字符串或 URL',
'url_parsing': '输入应为有效的 URL{error}',
'url_syntax_violation': '输入违反了严格的 URL 语法规则,{error}',
'url_too_long': 'URL 最多应有 {max_length} 个字符',
'url_scheme': 'URL 方案应为 {expected_schemes}',
'uuid_type': 'UUID 输入应为字符串、字节或 UUID 对象',
'uuid_parsing': '输入应为有效的 UUID{error}',
'uuid_version': '预期 UUID 版本为 {expected_version}',
'decimal_type': '十进制输入应为整数、浮点数、字符串或 Decimal 对象',
'decimal_parsing': '输入应为有效的十进制数',
'decimal_max_digits': '十进制输入总共应不超过 {max_digits} 位数字',
'decimal_max_places': '十进制输入应不超过 {decimal_places} 位小数',
'decimal_whole_digits': '十进制输入在小数点前应不超过 {whole_digits} 位数字',
}
CustomPhoneNumber = Annotated[str, Field(pattern=r'^1[3-9]\d{9}$')]
+54 -35
View File
@@ -4,7 +4,9 @@ from typing import TYPE_CHECKING
from fastapi import Request
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.exception import errors
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
if TYPE_CHECKING:
from backend.app.admin.schema.data_rule import GetDataRuleDetail
from backend.app.admin.model import DataRule
class RequestPermission:
@@ -47,33 +49,48 @@ class RequestPermission:
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 请求对象
:return:
"""
# 获取用户角色和规则
data_rules = []
# 获取用户角色和数据范围
data_scopes = []
for role in request.user.roles:
data_rules.extend(role.rules)
user_data_rules: list[GetDataRuleDetail] = list(dict.fromkeys(data_rules))
for scope in role.scopes:
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)
# 获取数据范围规则
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_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:
raise errors.NotFoundError(msg='数据规则模型不存在')
model_ins = dynamic_import_data_model(settings.DATA_PERMISSION_MODELS[rule_model])
@@ -82,39 +99,41 @@ def filter_data_permission(request: Request) -> ColumnElement[bool]:
model_columns = [
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:
raise errors.NotFoundError(msg='数据规则模型列不存在')
# 构建过滤条件
column_obj = getattr(model_ins, column)
rule_expression = rule.expression
rule_expression = data_rule.expression
condition = None
if rule_expression == RoleDataRuleExpressionType.eq:
condition = column_obj == rule.value
elif rule_expression == RoleDataRuleExpressionType.ne:
condition = column_obj != rule.value
elif rule_expression == RoleDataRuleExpressionType.gt:
condition = column_obj > rule.value
elif rule_expression == RoleDataRuleExpressionType.ge:
condition = column_obj >= rule.value
elif rule_expression == RoleDataRuleExpressionType.lt:
condition = column_obj < rule.value
elif rule_expression == RoleDataRuleExpressionType.le:
condition = column_obj <= rule.value
elif rule_expression == RoleDataRuleExpressionType.in_:
values = rule.value.split(',') if isinstance(rule.value, str) else rule.value
condition = column_obj.in_(values)
elif rule.expression == RoleDataRuleExpressionType.not_in:
values = rule.value.split(',') if isinstance(rule.value, str) else rule.value
condition = ~column_obj.in_(values)
match rule_expression:
case RoleDataRuleExpressionType.eq:
condition = column_obj == data_rule.value
case RoleDataRuleExpressionType.ne:
condition = column_obj != data_rule.value
case RoleDataRuleExpressionType.gt:
condition = column_obj > data_rule.value
case RoleDataRuleExpressionType.ge:
condition = column_obj >= data_rule.value
case RoleDataRuleExpressionType.lt:
condition = column_obj < data_rule.value
case RoleDataRuleExpressionType.le:
condition = column_obj <= data_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)
case RoleDataRuleExpressionType.not_in:
values = data_rule.value.split(',') if isinstance(data_rule.value, str) else data_rule.value
condition = column_obj.not_in(values)
# 根据运算符添加到对应列表
if condition is not None:
if rule.operator == RoleDataRuleOperatorType.AND:
where_and_list.append(condition)
elif rule.operator == RoleDataRuleOperatorType.OR:
where_or_list.append(condition)
match data_rule.operator:
case RoleDataRuleOperatorType.AND:
where_and_list.append(condition)
case RoleDataRuleOperatorType.OR:
where_or_list.append(condition)
# 组合所有条件
where_list = []
+10 -1
View File
@@ -3,10 +3,12 @@
from fastapi import Depends, Request
from backend.common.enums import MethodType, StatusType
from backend.common.exception import errors
from backend.common.exception.errors import AuthorizationError, TokenError
from backend.common.log import log
from backend.common.security.jwt import DependsJwtAuth
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:
@@ -67,6 +69,13 @@ async def rbac_verify(request: Request, _token: str = DependsJwtAuth) -> None:
if path_auth_perm not in allow_perms:
raise AuthorizationError
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)
+3 -2
View File
@@ -77,7 +77,7 @@ class Settings(BaseSettings):
JWT_USER_REDIS_EXPIRE_SECONDS: int = 60 * 60 * 24 * 7 # 7 天
# RBAC
RBAC_ROLE_MENU_MODE: bool = False
RBAC_ROLE_MENU_MODE: bool = True
RBAC_ROLE_MENU_EXCLUDE: list[str] = [
'sys:monitor:redis',
'sys:monitor:server',
@@ -89,11 +89,12 @@ class Settings(BaseSettings):
# 数据权限配置
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 模型列
'id',
'sort',
'del_flag',
'created_time',
'updated_time',
]
-3
View File
@@ -17,9 +17,6 @@ STATIC_DIR = BASE_PATH / 'static'
# 上传文件目录
UPLOAD_DIR = STATIC_DIR / 'upload'
# jinja2 模版文件路径
JINJA2_TEMPLATE_DIR = BASE_PATH / 'templates'
# 插件目录
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.opera_log_middleware import OperaLogMiddleware
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.health_check import ensure_unique_route_names, http_limit_callback
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
# 插件路由
plugin_router_inject()
# 系统路由(必须在插件路由注入后导入)
from backend.app.router import router
# API
router = build_final_router()
app.include_router(router, dependencies=dependencies)
# Extra
+1 -1
View File
@@ -71,7 +71,7 @@ class JwtAuthMiddleware(AuthenticationBackend):
except TokenError as exc:
raise _AuthenticationError(code=exc.code, msg=exc.detail, headers=exc.headers)
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'))
# 请注意,此返回使用非标准模式,所以在认证通过时,将丢失某些标准特性
-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 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.security.jwt import DependsJwtAuth
from backend.common.security.permission import RequestPermission
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()
@@ -4,12 +4,12 @@ from typing import Annotated
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.security.jwt import DependsJwtAuth
from backend.common.security.permission import RequestPermission
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()
@@ -5,13 +5,13 @@ from typing import Annotated
from fastapi import APIRouter, Depends, Path, Query
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.security.jwt import DependsJwtAuth
from backend.common.security.permission import RequestPermission
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()
@@ -8,9 +8,6 @@ from pydantic_settings import BaseSettings
class GeneratorSettings(BaseSettings):
"""代码生成配置"""
# 模版
TEMPLATE_BACKEND_DIR_NAME: str = 'py'
# 代码下载
DOWNLOAD_ZIP_FILENAME: str = 'fba_generator'
@@ -5,8 +5,8 @@ from typing import Sequence
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy_crud_plus import CRUDPlus
from backend.app.generator.model import GenBusiness
from backend.app.generator.schema.gen_business import CreateGenBusinessParam, UpdateGenBusinessParam
from backend.plugin.code_generator.model import GenBusiness
from backend.plugin.code_generator.schema.business import CreateGenBusinessParam, UpdateGenBusinessParam
class CRUDGenBusiness(CRUDPlus[GenBusiness]):
@@ -30,7 +30,7 @@ class CRUDGenBusiness(CRUDPlus[GenBusiness]):
:param name: 表名
: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]:
"""
@@ -5,14 +5,14 @@ from typing import Sequence
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy_crud_plus import CRUDPlus
from backend.app.generator.model import GenModel
from backend.app.generator.schema.gen_model import CreateGenModelParam, UpdateGenModelParam
from backend.plugin.code_generator.model import GenColumn
from backend.plugin.code_generator.schema.column import CreateGenModelParam, UpdateGenModelParam
class CRUDGenModel(CRUDPlus[GenModel]):
class CRUDGenModel(CRUDPlus[GenColumn]):
"""代码生成模型 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)
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)
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)
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)
gen_model_dao: CRUDGenModel = CRUDGenModel(GenModel)
gen_model_dao: CRUDGenModel = CRUDGenModel(GenColumn)
@@ -12,7 +12,7 @@ class CRUDGen:
"""代码生成 CRUD 类"""
@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
if TYPE_CHECKING:
from backend.app.generator.model import GenModel
from backend.plugin.code_generator.model import GenColumn
class GenBusiness(Base):
"""代码生成业务表"""
__tablename__ = 'sys_gen_business'
__tablename__ = 'gen_business'
id: Mapped[id_key] = mapped_column(init=False)
app_name: Mapped[str] = mapped_column(String(50), comment='应用名称(英文)')
table_name_en: Mapped[str] = mapped_column(String(255), unique=True, comment='表名称(英文)')
table_name_zh: Mapped[str] = mapped_column(String(255), comment='表名称(中文)')
table_simple_name_zh: Mapped[str] = mapped_column(String(255), comment='表名称(中文简称)')
table_name: Mapped[str] = mapped_column(String(255), unique=True, comment='表名称(英文)')
doc_comment: Mapped[str] = mapped_column(String(255), comment='档注释(用于函数/参数文档')
table_comment: Mapped[str | None] = mapped_column(String(255), 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='是否存在默认时间列')
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 根路径)')
@@ -33,4 +34,4 @@ class GenBusiness(Base):
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
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)
name: Mapped[str] = mapped_column(String(50), 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 类型')
default: Mapped[str | None] = mapped_column(
LONGTEXT().with_variant(TEXT, 'postgresql'), default=None, comment='列默认值'
@@ -33,6 +33,6 @@ class GenModel(DataClassBase):
# 代码生成业务模型一对多
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 -*-
from datetime import datetime
from pydantic import ConfigDict, Field, model_validator
from typing_extensions import Self
from pydantic import ConfigDict, Field
from backend.common.schema import SchemaBase
@@ -12,23 +11,17 @@ class GenBusinessSchemaBase(SchemaBase):
"""代码生成业务基础模型"""
app_name: str = Field(description='应用名称(英文)')
table_name_en: str = Field(description='表名称(英文)')
table_name_zh: str = Field(description='表名称(中文)')
table_simple_name_zh: str = Field(description='表名称(中文简称)')
table_name: str = Field(description='表名称(英文)')
doc_comment: str = Field(description='档注释(用于函数/参数文档')
table_comment: str | None = Field(None, description='表描述')
class_name: str | None = Field(None, description='基础类名 (默认为英文表名称)')
schema_name: str | None = Field(None, description='Schema 名称 (默认为英文表名称)')
filename: str | None = Field(None, description='基础文件名(默认为英文表名称)')
default_datetime_column: bool = Field(True, description='是否存在默认时间列')
api_version: str = Field('v1', description='代码生成 api 版本')
gen_path: str | None = Field(None, description='代码生成路径(默认为 app 根路径)')
remark: str | None = Field(None, description='备注')
@model_validator(mode='after')
def check_schema_name(self) -> Self:
"""检查并设置 schema 名称"""
if self.schema_name is None:
self.schema_name = self.table_name_en
return self
class CreateGenBusinessParam(GenBusinessSchemaBase):
"""创建代码生成业务参数"""
@@ -3,7 +3,7 @@
from pydantic import ConfigDict, Field, field_validator
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):
@@ -9,5 +9,5 @@ class ImportParam(SchemaBase):
"""导入参数"""
app: str = Field(description='应用名称,用于代码生成到指定 app')
table_name: str = Field(description='数据库表名')
table_schema: str = Field(description='数据库名')
table_name: str = Field(description='数据库表名')
@@ -2,11 +2,11 @@
# -*- coding: utf-8 -*-
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.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:
@@ -41,7 +41,7 @@ class GenBusinessService:
:return:
"""
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:
raise errors.ForbiddenError(msg='代码生成业务已存在')
await gen_business_dao.create(db, obj)
@@ -2,20 +2,20 @@
# -*- coding: utf-8 -*-
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.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:
"""代码生成模型服务类"""
@staticmethod
async def get(*, pk: int) -> GenModel:
async def get(*, pk: int) -> GenColumn:
"""
获取指定 ID 的模型
@@ -36,7 +36,7 @@ class GenModelService:
return types
@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
from pathlib import Path
from typing import Sequence
import aiofiles
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.core.path_conf import BASE_PATH
from backend.database.db import async_db_session
from backend.utils.gen_template import gen_template
from backend.utils.type_conversion import sql_type_to_pydantic
from backend.plugin.code_generator.crud.crud_business import gen_business_dao
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:
"""代码生成服务类"""
@staticmethod
async def get_tables(*, table_schema: str) -> list[str]:
async def get_tables(*, table_schema: str) -> Sequence[str]:
"""
获取指定 schema 下的所有表名
@@ -57,14 +58,17 @@ class GenService:
raise errors.ForbiddenError(msg='已存在相同数据库表业务')
table_name = table_info[0]
business_data = {
'app_name': obj.app,
'table_name_en': table_name,
'table_name_zh': table_info[1] or ' '.join(table_name.split('_')),
'table_simple_name_zh': table_info[1] or table_name.split('_')[-1],
'table_comment': table_info[1],
}
new_business = GenBusiness(**CreateGenBusinessParam(**business_data).model_dump())
new_business = GenBusiness(
**CreateGenBusinessParam(
app_name=obj.app,
table_name=table_name,
doc_comment=table_info[1] or table_name.split('_')[-1],
table_comment=table_info[1],
class_name=table_name,
schema_name=table_name,
filename=table_name,
).model_dump()
)
db.add(new_business)
await db.flush()
@@ -72,17 +76,20 @@ class GenService:
for column in column_info:
column_type = column[-1].split('(')[0].upper()
pd_type = sql_type_to_pydantic(column_type)
model_data = {
'name': column[0],
'comment': column[-2],
'type': column_type,
'sort': column[-3],
'length': column[-1].split('(')[1][:-1] if pd_type == 'str' and '(' in column[-1] else 0,
'is_pk': column[1],
'is_nullable': column[2],
'gen_business_id': new_business.id,
}
await gen_model_dao.create(db, CreateGenModelParam(**model_data), pd_type=pd_type)
await gen_model_dao.create(
db,
CreateGenModelParam(
name=column[0],
comment=column[-2],
type=column_type,
sort=column[-3],
length=column[-1].split('(')[1][:-1] if pd_type == 'str' and '(' in column[-1] else 0,
is_pk=column[1],
is_nullable=column[2],
gen_business_id=new_business.id,
),
pd_type=pd_type,
)
@staticmethod
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)
return {
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]:
@@ -115,10 +122,13 @@ class GenService:
raise errors.NotFoundError(msg='业务不存在')
tpl_code_map = await self.render_tpl_code(business=business)
return {
tpl.replace('.jinja', '.py') if tpl.startswith('py') else ...: code.encode('utf-8')
for tpl, code in tpl_code_map.items()
}
codes = {}
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
async def get_generate_path(*, pk: int) -> list[str]:
@@ -133,9 +143,10 @@ class GenService:
if not business:
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)
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:
"""
@@ -155,41 +166,42 @@ class GenService:
for tpl_path, code in tpl_code_map.items():
code_filepath = os.path.join(
gen_path,
*gen_template.get_code_gen_path(tpl_path, business).split('/')[1:],
*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 文件
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')
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)
# api __init__.py
if 'api' in str_code_filepath:
api_init_filepath = code_folder.parent.joinpath('__init__.py')
async with aiofiles.open(api_init_filepath, 'w', encoding='utf-8') as f:
await f.write(gen_template.init_content)
if 'api' in str(code_folder):
# api __init__.py
api_init_filepath = code_folder.parent.joinpath('__init__.py')
if not api_init_filepath.exists():
async with aiofiles.open(api_init_filepath, 'w', encoding='utf-8') as f:
await f.write(gen_template.init_content)
# app __init__.py
app_init_filepath = api_init_filepath.parent.joinpath('__init__.py')
if not app_init_filepath.exists():
async with aiofiles.open(app_init_filepath, 'w', encoding='utf-8') as f:
await f.write(gen_template.init_content)
# 写入代码文件
async with aiofiles.open(code_filepath, 'w', encoding='utf-8') as f:
await f.write(code)
# app __init__.py
if 'service' in str_code_filepath:
app_init_filepath = code_folder.parent.joinpath('__init__.py')
async with aiofiles.open(app_init_filepath, 'w', encoding='utf-8') as f:
await f.write(gen_template.init_content)
# model init 文件补充
if code_folder.name == 'model':
async with aiofiles.open(init_filepath, 'a', encoding='utf-8') as f:
await f.write(
f'from backend.app.{business.app_name}.model.{business.table_name_en} '
f'import {to_pascal(business.table_name_en)}\n',
f'from backend.app.{business.app_name}.model.{business.table_name} '
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:
"""
下载生成的代码
@@ -206,27 +218,34 @@ class GenService:
with zipfile.ZipFile(bio, 'w') as zf:
tpl_code_map = await self.render_tpl_code(business=business)
for tpl_path, code in tpl_code_map.items():
# 写入代码文件
new_code_path = gen_template.get_code_gen_path(tpl_path, business)
zf.writestr(new_code_path, code)
code_filepath = gen_template.get_code_gen_path(tpl_path, business)
# 写入 init 文件
init_filepath = os.path.join(*new_code_path.split('/')[:-1], '__init__.py')
if 'model' not in new_code_path.split('/'):
code_dir = os.path.dirname(code_filepath)
init_filepath = os.path.join(code_dir, '__init__.py')
if 'model' not in code_filepath.split('/'):
zf.writestr(init_filepath, gen_template.init_content)
else:
zf.writestr(
init_filepath,
f'{gen_template.init_content}'
f'from backend.app.{business.app_name}.model.{business.table_name_en} '
f'import {to_pascal(business.table_name_en)}\n',
f'from backend.app.{business.app_name}.model.{business.table_name} '
f'import {to_pascal(business.table_name)}\n',
)
if 'api' in new_code_path:
# api __init__.py
api_init_filepath = os.path.join(*new_code_path.split('/')[:-2], '__init__.py')
# api __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)
# 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)
return bio

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