mirror of
https://github.com/fastapi-practices/fastapi-best-architecture.git
synced 2026-09-21 05:02:49 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
16a0f9646d | ||
|
|
032364e48e | ||
|
|
bc5d142920 | ||
|
|
9b3425d801 | ||
|
|
5842127c2b | ||
|
|
193eba3c58 | ||
|
|
338e50e255 | ||
|
|
65500d7a53 | ||
|
|
0833cd5443 | ||
|
|
e8c844ead7 | ||
|
|
1d5f2dc5f0 | ||
|
|
9a36154564 | ||
|
|
5f8ae74ea5 | ||
|
|
da8e7c1b01 | ||
|
|
f513b98f15 | ||
|
|
80cef6dde7 | ||
|
|
2d504f281e | ||
|
|
fc34c98f0a | ||
|
|
1cc020efdd | ||
|
|
bb33e99e33 | ||
|
|
6c70b34ae6 | ||
|
|
960d9f6215 | ||
|
|
085f08ec3b | ||
|
|
2a8722f56e | ||
|
|
4372b9bbc2 | ||
|
|
db81319dc4 | ||
|
|
644f7a0413 | ||
|
|
b93ff19ebe | ||
|
|
4c007cb47d | ||
|
|
c9e9a45288 | ||
|
|
d44a5491f5 | ||
|
|
289cf5b974 | ||
|
|
e207622a70 | ||
|
|
38d9d9fda5 | ||
|
|
996895e775 | ||
|
|
0675e53f2a |
@@ -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 }}
|
||||
|
||||
@@ -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
@@ -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
@@ -1,6 +1,6 @@
|
||||
__pycache__/
|
||||
.env
|
||||
log/
|
||||
alembic/versions/
|
||||
static/media/
|
||||
*.log
|
||||
celerybeat-schedule.*
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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=['系统插件'])
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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,6 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, File, UploadFile
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
@@ -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:
|
||||
"""
|
||||
|
||||
@@ -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:
|
||||
"""
|
||||
获取登录日志列表
|
||||
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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:
|
||||
"""
|
||||
获取操作日志列表
|
||||
|
||||
|
||||
@@ -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:
|
||||
"""
|
||||
|
||||
@@ -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,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
|
||||
|
||||
@@ -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='运算符(0:and、1:or)')
|
||||
expression: Mapped[int] = mapped_column(
|
||||
comment='表达式(0:==、1:!=、2:>、3:>=、4:<、5:<=、6:in、7:not_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')
|
||||
|
||||
@@ -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')
|
||||
@@ -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',
|
||||
),
|
||||
)
|
||||
|
||||
@@ -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='备注'
|
||||
)
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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='字段评论')
|
||||
|
||||
@@ -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='数据规则列表')
|
||||
@@ -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='备注')
|
||||
|
||||
|
||||
|
||||
@@ -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='数据范围列表')
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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()
|
||||
@@ -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:
|
||||
"""
|
||||
获取登录日志列表查询条件
|
||||
|
||||
|
||||
@@ -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:
|
||||
"""
|
||||
获取操作日志列表查询条件
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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')
|
||||
@@ -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
|
||||
@@ -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)
|
||||
|
||||
@@ -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=['任务'])
|
||||
|
||||
@@ -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
@@ -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'
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
@@ -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}$')]
|
||||
|
||||
@@ -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,38 +99,40 @@ 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
|
||||
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)
|
||||
elif rule.expression == RoleDataRuleExpressionType.not_in:
|
||||
values = rule.value.split(',') if isinstance(rule.value, str) else 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:
|
||||
match data_rule.operator:
|
||||
case RoleDataRuleOperatorType.AND:
|
||||
where_and_list.append(condition)
|
||||
elif rule.operator == RoleDataRuleOperatorType.OR:
|
||||
case RoleDataRuleOperatorType.OR:
|
||||
where_or_list.append(condition)
|
||||
|
||||
# 组合所有条件
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
@@ -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',
|
||||
]
|
||||
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'))
|
||||
|
||||
# 请注意,此返回使用非标准模式,所以在认证通过时,将丢失某些标准特性
|
||||
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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)}">'
|
||||
@@ -1,10 +0,0 @@
|
||||
[app]
|
||||
include = 'admin'
|
||||
|
||||
[api.api]
|
||||
prefix = '/apis'
|
||||
tags = '系统API'
|
||||
|
||||
[api.casbin]
|
||||
prefix = '/casbin'
|
||||
tags = '系统Casbin权限'
|
||||
@@ -1,2 +0,0 @@
|
||||
casbin>=1.38.0
|
||||
casbin_async_sqlalchemy_adapter>=1.7.0
|
||||
@@ -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()
|
||||
@@ -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')
|
||||
+8
-8
@@ -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()
|
||||
|
||||
+2
-2
@@ -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'
|
||||
|
||||
+3
-3
@@ -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]:
|
||||
"""
|
||||
+8
-8
@@ -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)
|
||||
+1
-1
@@ -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]:
|
||||
"""
|
||||
获取所有表名
|
||||
|
||||
@@ -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
|
||||
+8
-7
@@ -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')
|
||||
+7
-7
@@ -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']
|
||||
+5
-12
@@ -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):
|
||||
"""创建代码生成业务参数"""
|
||||
+1
-1
@@ -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='数据库表名')
|
||||
+4
-4
@@ -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)
|
||||
+7
-7
@@ -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]:
|
||||
"""
|
||||
获取指定业务的所有模型
|
||||
|
||||
+79
-60
@@ -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:
|
||||
await f.write(gen_template.init_content)
|
||||
|
||||
if 'api' in str(code_folder):
|
||||
# api __init__.py
|
||||
if 'api' in str_code_filepath:
|
||||
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():
|
||||
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)
|
||||
|
||||
# 写入代码文件
|
||||
async with aiofiles.open(code_filepath, 'w', encoding='utf-8') as f:
|
||||
await f.write(code)
|
||||
|
||||
# model init 文件补充
|
||||
if code_folder.name == 'model':
|
||||
async with aiofiles.open(init_filepath, 'a', encoding='utf-8') as f:
|
||||
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')
|
||||
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
Reference in New Issue
Block a user