Compare commits

...
24 Commits
Author SHA1 Message Date
Wu Clan 5f8ae74ea5 Fix list query in the dict plugin (#582) 2025-04-18 23:16:03 +08:00
Wu Clan da8e7c1b01 Fix the plugin system route injection (#581) 2025-04-18 23:09:25 +08:00
Wu Clan f513b98f15 Update the changelog for v1.1.0 (#580) 2025-04-17 17:54:52 +08:00
Wu Clan 80cef6dde7 Update code generator file and table naming (#579)
* Update code generator file and table naming

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

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

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

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

* Update sidebar api description
2025-04-02 22:20:19 +08:00
Wu Clan 289cf5b974 Update dict data and type to plugin (#562) 2025-04-02 14:04:06 +08:00
Wu Clan e207622a70 Update the system config to plugin (#561) 2025-04-02 13:43:24 +08:00
Wu Clan 38d9d9fda5 Add python 3.13 to GitHub ci (#560) 2025-03-28 14:22:45 +08:00
Wu Clan 996895e775 Bump dependencies and pre-commits (#559) 2025-03-28 14:22:20 +08:00
Wu Clan 0675e53f2a Update the changelog for v1.0.4 (#558) 2025-03-28 10:15:11 +08:00
119 changed files with 1551 additions and 1181 deletions
+2 -2
View File
@@ -12,13 +12,13 @@ jobs:
name: lint ${{ matrix.python-version }} name: lint ${{ matrix.python-version }}
strategy: strategy:
matrix: matrix:
python-version: [ '3.10', '3.11', '3.12' ] python-version: [ '3.10', '3.11', '3.12', '3.13' ]
fail-fast: false fail-fast: false
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Install uv - name: Install uv
uses: astral-sh/setup-uv@v3 uses: astral-sh/setup-uv@v5
- name: Set up Python ${{ matrix.python-version }} - name: Set up Python ${{ matrix.python-version }}
run: uv python install ${{ matrix.python-version }} run: uv python install ${{ matrix.python-version }}
+2 -2
View File
@@ -8,7 +8,7 @@ repos:
- id: check-toml - id: check-toml
- repo: https://github.com/charliermarsh/ruff-pre-commit - repo: https://github.com/charliermarsh/ruff-pre-commit
rev: v0.9.5 rev: v0.11.2
hooks: hooks:
- id: ruff - id: ruff
args: args:
@@ -20,7 +20,7 @@ repos:
- id: ruff-format - id: ruff-format
- repo: https://github.com/astral-sh/uv-pre-commit - repo: https://github.com/astral-sh/uv-pre-commit
rev: 0.5.29 rev: 0.6.10
hooks: hooks:
- id: uv-lock - id: uv-lock
- id: uv-export - id: uv-export
+74
View File
@@ -1,3 +1,74 @@
<a id="v1.1.0"></a>
# [v1.1.0](https://github.com/fastapi-practices/fastapi_best_architecture/releases/tag/v1.1.0) - 2025-04-17
## What's Changed
* Update the changelog for v1.0.5 by [@wu-clan](https://github.com/wu-clan) in [#572](https://github.com/fastapi-practices/fastapi_best_architecture/pull/572)
* Update the default value for some functions by [@wu-clan](https://github.com/wu-clan) in [#573](https://github.com/fastapi-practices/fastapi_best_architecture/pull/573)
* Optimize the file structure of code generator by [@wu-clan](https://github.com/wu-clan) in [#574](https://github.com/fastapi-practices/fastapi_best_architecture/pull/574)
* Update casbin RBAC verify to dynamic import by [@wu-clan](https://github.com/wu-clan) in [#576](https://github.com/fastapi-practices/fastapi_best_architecture/pull/576)
* Update unique columns in dict models by [@wu-clan](https://github.com/wu-clan) in [#577](https://github.com/fastapi-practices/fastapi_best_architecture/pull/577)
* Update the code generator to plugin by [@wu-clan](https://github.com/wu-clan) in [#578](https://github.com/fastapi-practices/fastapi_best_architecture/pull/578)
* Fix avatar url type of update avatar by [@huyuwei1996](https://github.com/huyuwei1996) in [#575](https://github.com/fastapi-practices/fastapi_best_architecture/pull/575)
* Update code generator file and table naming by [@wu-clan](https://github.com/wu-clan) in [#579](https://github.com/fastapi-practices/fastapi_best_architecture/pull/579)
**Full Changelog**: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.0.5...v1.1.0
[Changes][v1.1.0]
<a id="v1.0.5"></a>
# [v1.0.5](https://github.com/fastapi-practices/fastapi_best_architecture/releases/tag/v1.0.5) - 2025-04-09
## What's Changed
* Update the changelog for v1.0.4 by [@wu-clan](https://github.com/wu-clan) in [#558](https://github.com/fastapi-practices/fastapi_best_architecture/pull/558)
* Bump dependencies and pre-commits by [@wu-clan](https://github.com/wu-clan) in [#559](https://github.com/fastapi-practices/fastapi_best_architecture/pull/559)
* Add python 3.13 to GitHub ci by [@wu-clan](https://github.com/wu-clan) in [#560](https://github.com/fastapi-practices/fastapi_best_architecture/pull/560)
* Update the system config to plugin by [@wu-clan](https://github.com/wu-clan) in [#561](https://github.com/fastapi-practices/fastapi_best_architecture/pull/561)
* Update dict data and type to plugin by [@wu-clan](https://github.com/wu-clan) in [#562](https://github.com/fastapi-practices/fastapi_best_architecture/pull/562)
* Update menu and add vben5 compatibility by [@wu-clan](https://github.com/wu-clan) in [#563](https://github.com/fastapi-practices/fastapi_best_architecture/pull/563)
* Update the vben5 tree data structure by [@wu-clan](https://github.com/wu-clan) in [#564](https://github.com/fastapi-practices/fastapi_best_architecture/pull/564)
* Update custom validation error messages by [@wu-clan](https://github.com/wu-clan) in [#566](https://github.com/fastapi-practices/fastapi_best_architecture/pull/566)
* Update the number of pagination le by [@wu-clan](https://github.com/wu-clan) in [#565](https://github.com/fastapi-practices/fastapi_best_architecture/pull/565)
* Fix the login password verification by [@wu-clan](https://github.com/wu-clan) in [#568](https://github.com/fastapi-practices/fastapi_best_architecture/pull/568)
* Fix the failure hook of celery task by [@wu-clan](https://github.com/wu-clan) in [#569](https://github.com/fastapi-practices/fastapi_best_architecture/pull/569)
* Bump fastapi oauth2 from 0.0.1a2 to 0.0.1 by [@wu-clan](https://github.com/wu-clan) in [#570](https://github.com/fastapi-practices/fastapi_best_architecture/pull/570)
* Fix the log rule in gitignore by [@wu-clan](https://github.com/wu-clan) in [#571](https://github.com/fastapi-practices/fastapi_best_architecture/pull/571)
**Full Changelog**: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.0.4...v1.0.5
[Changes][v1.0.5]
<a id="v1.0.4"></a>
# [v1.0.4](https://github.com/fastapi-practices/fastapi_best_architecture/releases/tag/v1.0.4) - 2025-03-28
## What's Changed
* Update the changelog for v1.0.3 by [@wu-clan](https://github.com/wu-clan) in [#543](https://github.com/fastapi-practices/fastapi_best_architecture/pull/543)
* Updated the backend contribution guide by [@wu-clan](https://github.com/wu-clan) in [#544](https://github.com/fastapi-practices/fastapi_best_architecture/pull/544)
* Optimize the return of relationship interfaces by [@wu-clan](https://github.com/wu-clan) in [#545](https://github.com/fastapi-practices/fastapi_best_architecture/pull/545)
* Optimize the dynamic import of data models by [@wu-clan](https://github.com/wu-clan) in [#546](https://github.com/fastapi-practices/fastapi_best_architecture/pull/546)
* Update git and docker ignore files by [@wu-clan](https://github.com/wu-clan) in [#547](https://github.com/fastapi-practices/fastapi_best_architecture/pull/547)
* Optimize dependencies to reduce package size by [@wu-clan](https://github.com/wu-clan) in [#548](https://github.com/fastapi-practices/fastapi_best_architecture/pull/548)
* Fix async install plugin dependencies for windows by [@wu-clan](https://github.com/wu-clan) in [#549](https://github.com/fastapi-practices/fastapi_best_architecture/pull/549)
* Fix return schema of the config api by [@wu-clan](https://github.com/wu-clan) in [#551](https://github.com/fastapi-practices/fastapi_best_architecture/pull/551)
* Optimize schemas with model relationships by [@wu-clan](https://github.com/wu-clan) in [#552](https://github.com/fastapi-practices/fastapi_best_architecture/pull/552)
* Fix filters for opera log query list by [@ThankCat](https://github.com/ThankCat) in [#554](https://github.com/fastapi-practices/fastapi_best_architecture/pull/554)
* Fix the celery env in docker compose by [@wu-clan](https://github.com/wu-clan) in [#555](https://github.com/fastapi-practices/fastapi_best_architecture/pull/555)
* Update volumes of redis in docker compose by [@wu-clan](https://github.com/wu-clan) in [#556](https://github.com/fastapi-practices/fastapi_best_architecture/pull/556)
* Fix the query for the sub department by [@PoetryL](https://github.com/PoetryL) in [#557](https://github.com/fastapi-practices/fastapi_best_architecture/pull/557)
* Optimize codes and comments with cursor by [@wu-clan](https://github.com/wu-clan) in [#550](https://github.com/fastapi-practices/fastapi_best_architecture/pull/550)
## New Contributors
* [@ThankCat](https://github.com/ThankCat) made their first contribution in [#554](https://github.com/fastapi-practices/fastapi_best_architecture/pull/554)
* [@PoetryL](https://github.com/PoetryL) made their first contribution in [#557](https://github.com/fastapi-practices/fastapi_best_architecture/pull/557)
**Full Changelog**: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.0.3...v1.0.4
[Changes][v1.0.4]
<a id="v1.0.3"></a> <a id="v1.0.3"></a>
# [v1.0.3](https://github.com/fastapi-practices/fastapi_best_architecture/releases/tag/v1.0.3) - 2025-03-11 # [v1.0.3](https://github.com/fastapi-practices/fastapi_best_architecture/releases/tag/v1.0.3) - 2025-03-11
@@ -375,6 +446,9 @@
[Changes][v1.0.0] [Changes][v1.0.0]
[v1.1.0]: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.0.5...v1.1.0
[v1.0.5]: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.0.4...v1.0.5
[v1.0.4]: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.0.3...v1.0.4
[v1.0.3]: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.0.2...v1.0.3 [v1.0.3]: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.0.2...v1.0.3
[v1.0.2]: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.0.1...v1.0.2 [v1.0.2]: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.0.1...v1.0.2
[v1.0.1]: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.0.0...v1.0.1 [v1.0.1]: https://github.com/fastapi-practices/fastapi_best_architecture/compare/v1.0.0...v1.0.1
+1 -1
View File
@@ -1,6 +1,6 @@
__pycache__/ __pycache__/
.env .env
log/
alembic/versions/ alembic/versions/
static/media/ static/media/
*.log
celerybeat-schedule.* celerybeat-schedule.*
+1 -1
View File
@@ -20,7 +20,7 @@ from backend.plugin.tools import get_plugin_models
# import your new model here # import your new model here
from backend.app.admin.model import * # noqa: F401 from backend.app.admin.model import * # noqa: F401
from backend.app.generator.model import * # noqa: F401 from backend.plugin.code_generator.model import * # noqa: F401
# import plugin model # import plugin model
for cls in get_plugin_models(): for cls in get_plugin_models():
+3 -3
View File
@@ -13,12 +13,12 @@ from backend.common.response.response_schema import ResponseSchemaModel, respons
router = APIRouter() router = APIRouter()
_github_client = GitHubOAuth20(admin_settings.OAUTH2_GITHUB_CLIENT_ID, admin_settings.OAUTH2_GITHUB_CLIENT_SECRET) _github_client = GitHubOAuth20(admin_settings.OAUTH2_GITHUB_CLIENT_ID, admin_settings.OAUTH2_GITHUB_CLIENT_SECRET)
_github_oauth2 = FastAPIOAuth20(_github_client, admin_settings.OAUTH2_GITHUB_REDIRECT_URI) _github_oauth2 = FastAPIOAuth20(_github_client, redirect_route_name='github_login')
@router.get('', summary='获取 Github 授权链接') @router.get('', summary='获取 Github 授权链接')
async def github_auth2() -> ResponseSchemaModel[str]: async def github_oauth2(request: Request) -> ResponseSchemaModel[str]:
auth_url = await _github_client.get_authorization_url(redirect_uri=admin_settings.OAUTH2_GITHUB_REDIRECT_URI) auth_url = await _github_client.get_authorization_url(redirect_uri=f'{request.url}/callback')
return response_base.success(data=auth_url) return response_base.success(data=auth_url)
+3 -3
View File
@@ -16,12 +16,12 @@ _linux_do_client = LinuxDoOAuth20(
admin_settings.OAUTH2_LINUX_DO_CLIENT_ID, admin_settings.OAUTH2_LINUX_DO_CLIENT_ID,
admin_settings.OAUTH2_LINUX_DO_CLIENT_SECRET, admin_settings.OAUTH2_LINUX_DO_CLIENT_SECRET,
) )
_linux_do_oauth2 = FastAPIOAuth20(_linux_do_client, admin_settings.OAUTH2_LINUX_DO_REDIRECT_URI) _linux_do_oauth2 = FastAPIOAuth20(_linux_do_client, redirect_route_name='linux_do_login')
@router.get('', summary='获取 LinuxDo 授权链接') @router.get('', summary='获取 LinuxDo 授权链接')
async def linux_do_auth2() -> ResponseSchemaModel[str]: async def linux_do_oauth2(request: Request) -> ResponseSchemaModel[str]:
auth_url = await _linux_do_client.get_authorization_url(redirect_uri=admin_settings.OAUTH2_LINUX_DO_REDIRECT_URI) auth_url = await _linux_do_client.get_authorization_url(redirect_uri=f'{request.url}/callback')
return response_base.success(data=auth_url) return response_base.success(data=auth_url)
-6
View File
@@ -2,11 +2,8 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from fastapi import APIRouter from fastapi import APIRouter
from backend.app.admin.api.v1.sys.config import router as config_router
from backend.app.admin.api.v1.sys.data_rule import router as data_rule_router from backend.app.admin.api.v1.sys.data_rule import router as data_rule_router
from backend.app.admin.api.v1.sys.dept import router as dept_router from backend.app.admin.api.v1.sys.dept import router as dept_router
from backend.app.admin.api.v1.sys.dict_data import router as dict_data_router
from backend.app.admin.api.v1.sys.dict_type import router as dict_type_router
from backend.app.admin.api.v1.sys.menu import router as menu_router from backend.app.admin.api.v1.sys.menu import router as menu_router
from backend.app.admin.api.v1.sys.plugin import router as plugin_router from backend.app.admin.api.v1.sys.plugin import router as plugin_router
from backend.app.admin.api.v1.sys.role import router as role_router from backend.app.admin.api.v1.sys.role import router as role_router
@@ -16,10 +13,7 @@ from backend.app.admin.api.v1.sys.user import router as user_router
router = APIRouter(prefix='/sys') router = APIRouter(prefix='/sys')
router.include_router(config_router, prefix='/configs', tags=['系统参数配置'])
router.include_router(dept_router, prefix='/depts', tags=['系统部门']) router.include_router(dept_router, prefix='/depts', tags=['系统部门'])
router.include_router(dict_data_router, prefix='/dict-datas', tags=['系统字典数据'])
router.include_router(dict_type_router, prefix='/dict-types', tags=['系统字典类型'])
router.include_router(menu_router, prefix='/menus', tags=['系统菜单']) router.include_router(menu_router, prefix='/menus', tags=['系统菜单'])
router.include_router(role_router, prefix='/roles', tags=['系统角色']) router.include_router(role_router, prefix='/roles', tags=['系统角色'])
router.include_router(user_router, prefix='/users', tags=['系统用户']) router.include_router(user_router, prefix='/users', tags=['系统用户'])
+1 -1
View File
@@ -14,7 +14,7 @@ from backend.common.security.rbac import DependsRBAC
router = APIRouter() router = APIRouter()
@router.get('/sidebar', summary='获取用户侧边栏', dependencies=[DependsJwtAuth]) @router.get('/sidebar', summary='获取用户菜单侧边栏', description='适配 vben5', dependencies=[DependsJwtAuth])
async def get_user_sidebar(request: Request) -> ResponseSchemaModel[list[dict[str, Any]]]: async def get_user_sidebar(request: Request) -> ResponseSchemaModel[list[dict[str, Any]]]:
menu = await menu_service.get_user_menu_tree(request=request) menu = await menu_service.get_user_menu_tree(request=request)
return response_base.success(data=menu) return response_base.success(data=menu)
+2 -1
View File
@@ -61,7 +61,8 @@ async def get_tokens(
extra_info = await redis_client.get(f'{settings.TOKEN_EXTRA_INFO_REDIS_PREFIX}:{session_uuid}') extra_info = await redis_client.get(f'{settings.TOKEN_EXTRA_INFO_REDIS_PREFIX}:{session_uuid}')
if extra_info: if extra_info:
extra_info = json.loads(extra_info) extra_info = json.loads(extra_info)
if extra_info.get('login_type') != 'swagger': # 排除 swagger 登录生成的 token
if extra_info.get('swagger') is None:
if username is not None: if username is not None:
if username == extra_info.get('username'): if username == extra_info.get('username'):
append_token_detail() append_token_detail()
+1 -6
View File
@@ -19,21 +19,16 @@ class AdminSettings(BaseSettings):
OAUTH2_LINUX_DO_CLIENT_SECRET: str OAUTH2_LINUX_DO_CLIENT_SECRET: str
# OAuth2 # OAuth2
OAUTH2_GITHUB_REDIRECT_URI: str = 'http://127.0.0.1:8000/api/v1/oauth2/github/callback'
OAUTH2_LINUX_DO_REDIRECT_URI: str = 'http://127.0.0.1:8000/api/v1/oauth2/linux-do/callback'
OAUTH2_FRONTEND_REDIRECT_URI: str = 'http://localhost:5173/oauth2/callback' OAUTH2_FRONTEND_REDIRECT_URI: str = 'http://localhost:5173/oauth2/callback'
# 验证码 # 验证码
CAPTCHA_LOGIN_REDIS_PREFIX: str = 'fba:login:captcha' CAPTCHA_LOGIN_REDIS_PREFIX: str = 'fba:login:captcha'
CAPTCHA_LOGIN_EXPIRE_SECONDS: int = 60 * 5 # 3 分钟 CAPTCHA_LOGIN_EXPIRE_SECONDS: int = 60 * 5 # 3 分钟
# 参数配置
CONFIG_BUILT_IN_TYPES: list[str] = ['website', 'protocol', 'policy']
@lru_cache @lru_cache
def get_admin_settings() -> AdminSettings: def get_admin_settings() -> AdminSettings:
"""获取 admin 参数配置""" """获取 admin 配置"""
return AdminSettings() return AdminSettings()
+1 -1
View File
@@ -24,7 +24,7 @@ class CRUDDataRule(CRUDPlus[DataRule]):
""" """
return await self.select_model(db, pk) return await self.select_model(db, pk)
async def get_list(self, name: str | None = None) -> Select: async def get_list(self, name: str | None) -> Select:
""" """
获取规则列表 获取规则列表
+1 -6
View File
@@ -35,12 +35,7 @@ class CRUDDept(CRUDPlus[Dept]):
return await self.select_model_by_column(db, name=name, del_flag=0) return await self.select_model_by_column(db, name=name, del_flag=0)
async def get_all( async def get_all(
self, self, db: AsyncSession, name: str | None, leader: str | None, phone: str | None, status: int | None
db: AsyncSession,
name: str | None = None,
leader: str | None = None,
phone: str | None = None,
status: int | None = None,
) -> Sequence[Dept]: ) -> Sequence[Dept]:
""" """
获取所有部门 获取所有部门
+1 -1
View File
@@ -11,7 +11,7 @@ from backend.app.admin.schema.login_log import CreateLoginLogParam
class CRUDLoginLog(CRUDPlus[LoginLog]): class CRUDLoginLog(CRUDPlus[LoginLog]):
"""登录日志数据库操作类""" """登录日志数据库操作类"""
async def get_list(self, username: str | None = None, status: int | None = None, ip: str | None = None) -> Select: async def get_list(self, username: str | None, status: int | None, ip: str | None) -> Select:
""" """
获取登录日志列表 获取登录日志列表
+2 -2
View File
@@ -34,7 +34,7 @@ class CRUDMenu(CRUDPlus[Menu]):
""" """
return await self.select_model_by_column(db, title=title, menu_type__ne=2) return await self.select_model_by_column(db, title=title, menu_type__ne=2)
async def get_all(self, db: AsyncSession, title: str | None = None, status: int | None = None) -> Sequence[Menu]: async def get_all(self, db: AsyncSession, title: str | None, status: int | None) -> Sequence[Menu]:
""" """
获取菜单列表 获取菜单列表
@@ -60,7 +60,7 @@ class CRUDMenu(CRUDPlus[Menu]):
:return: :return:
""" """
stmt = select(self.model).order_by(asc(self.model.sort)) stmt = select(self.model).order_by(asc(self.model.sort))
filters = [self.model.menu_type.in_([0, 1])] filters = [self.model.type.in_([0, 1])]
if not superuser: if not superuser:
filters.append(self.model.id.in_(menu_ids)) filters.append(self.model.id.in_(menu_ids))
stmt = stmt.where(and_(*filters)) stmt = stmt.where(and_(*filters))
+1 -1
View File
@@ -11,7 +11,7 @@ from backend.app.admin.schema.opera_log import CreateOperaLogParam
class CRUDOperaLogDao(CRUDPlus[OperaLog]): class CRUDOperaLogDao(CRUDPlus[OperaLog]):
"""操作日志数据库操作类""" """操作日志数据库操作类"""
async def get_list(self, username: str | None = None, status: int | None = None, ip: str | None = None) -> Select: async def get_list(self, username: str | None, status: int | None, ip: str | None) -> Select:
""" """
获取操作日志列表 获取操作日志列表
+1 -1
View File
@@ -66,7 +66,7 @@ class CRUDRole(CRUDPlus[Role]):
roles = await db.execute(stmt) roles = await db.execute(stmt)
return roles.scalars().all() return roles.scalars().all()
async def get_list(self, name: str | None = None, status: int | None = None) -> Select: async def get_list(self, name: str | None, status: int | None) -> Select:
""" """
获取角色列表 获取角色列表
+2 -4
View File
@@ -142,7 +142,7 @@ class CRUDUser(CRUDPlus[User]):
:param avatar: 头像地址 :param avatar: 头像地址
:return: :return:
""" """
return await self.update_model(db, input_user, {'avatar': avatar.url}) return await self.update_model(db, input_user, {'avatar': str(avatar.url)})
async def delete(self, db: AsyncSession, user_id: int) -> int: async def delete(self, db: AsyncSession, user_id: int) -> int:
""" """
@@ -175,9 +175,7 @@ class CRUDUser(CRUDPlus[User]):
""" """
return await self.update_model(db, pk, {'password': new_pwd}) return await self.update_model(db, pk, {'password': new_pwd})
async def get_list( async def get_list(self, dept: int | None, username: str | None, phone: str | None, status: int | None) -> Select:
self, dept: int | None = None, username: str | None = None, phone: str | None = None, status: int | None = None
) -> Select:
""" """
获取用户列表 获取用户列表
-3
View File
@@ -1,10 +1,7 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from backend.app.admin.model.config import Config
from backend.app.admin.model.data_rule import DataRule from backend.app.admin.model.data_rule import DataRule
from backend.app.admin.model.dept import Dept from backend.app.admin.model.dept import Dept
from backend.app.admin.model.dict_data import DictData
from backend.app.admin.model.dict_type import DictType
from backend.app.admin.model.login_log import LoginLog from backend.app.admin.model.login_log import LoginLog
from backend.app.admin.model.menu import Menu from backend.app.admin.model.menu import Menu
from backend.app.admin.model.opera_log import OperaLog from backend.app.admin.model.opera_log import OperaLog
+5 -2
View File
@@ -24,15 +24,18 @@ class Menu(Base):
id: Mapped[id_key] = mapped_column(init=False) id: Mapped[id_key] = mapped_column(init=False)
title: Mapped[str] = mapped_column(String(50), comment='菜单标题') title: Mapped[str] = mapped_column(String(50), comment='菜单标题')
name: Mapped[str] = mapped_column(String(50), comment='菜单名称') name: Mapped[str] = mapped_column(String(50), comment='菜单名称')
path: Mapped[str] = mapped_column(String(200), comment='路由地址')
sort: Mapped[int] = mapped_column(default=0, comment='排序') sort: Mapped[int] = mapped_column(default=0, comment='排序')
icon: Mapped[str | None] = mapped_column(String(100), default=None, comment='菜单图标') icon: Mapped[str | None] = mapped_column(String(100), default=None, comment='菜单图标')
path: Mapped[str | None] = mapped_column(String(200), default=None, comment='路由地址') type: Mapped[int] = mapped_column(default=0, comment='菜单类型(0目录 1菜单 2按钮)')
menu_type: Mapped[int] = mapped_column(default=0, comment='菜单类型(0目录 1菜单 2按钮)')
component: Mapped[str | None] = mapped_column(String(255), default=None, comment='组件路径') component: Mapped[str | None] = mapped_column(String(255), default=None, comment='组件路径')
perms: Mapped[str | None] = mapped_column(String(100), default=None, comment='权限标识') perms: Mapped[str | None] = mapped_column(String(100), default=None, comment='权限标识')
status: Mapped[int] = mapped_column(default=1, comment='菜单状态(0停用 1正常)') status: Mapped[int] = mapped_column(default=1, comment='菜单状态(0停用 1正常)')
display: Mapped[int] = mapped_column(default=1, comment='是否显示(0否 1是)') display: Mapped[int] = mapped_column(default=1, comment='是否显示(0否 1是)')
cache: Mapped[int] = mapped_column(default=1, comment='是否缓存(0否 1是)') cache: Mapped[int] = mapped_column(default=1, comment='是否缓存(0否 1是)')
link: Mapped[str | None] = mapped_column(
LONGTEXT().with_variant(TEXT, 'postgresql'), default=None, comment='外链地址'
)
remark: Mapped[str | None] = mapped_column( remark: Mapped[str | None] = mapped_column(
LONGTEXT().with_variant(TEXT, 'postgresql'), default=None, comment='备注' LONGTEXT().with_variant(TEXT, 'postgresql'), default=None, comment='备注'
) )
+3 -2
View File
@@ -13,16 +13,17 @@ class MenuSchemaBase(SchemaBase):
title: str = Field(description='菜单标题') title: str = Field(description='菜单标题')
name: str = Field(description='菜单名称') name: str = Field(description='菜单名称')
path: str = Field(description='路由路径')
parent_id: int | None = Field(None, description='菜单父级 ID') parent_id: int | None = Field(None, description='菜单父级 ID')
sort: int = Field(0, ge=0, description='排序') sort: int = Field(0, ge=0, description='排序')
icon: str | None = Field(None, description='图标') icon: str | None = Field(None, description='图标')
path: str | None = Field(None, description='路由路径') type: MenuType = Field(MenuType.directory, description='菜单类型(0目录 1菜单 2按钮)')
menu_type: MenuType = Field(MenuType.directory, description='菜单类型(0目录 1菜单 2按钮)')
component: str | None = Field(None, description='组件路径') component: str | None = Field(None, description='组件路径')
perms: str | None = Field(None, description='权限标识') perms: str | None = Field(None, description='权限标识')
status: StatusType = Field(StatusType.enable, description='状态') status: StatusType = Field(StatusType.enable, description='状态')
display: StatusType = Field(StatusType.enable, description='是否显示') display: StatusType = Field(StatusType.enable, description='是否显示')
cache: StatusType = Field(StatusType.enable, description='是否缓存') cache: StatusType = Field(StatusType.enable, description='是否缓存')
link: str | None = Field(None, description='外链地址')
remark: str | None = Field(None, description='备注') remark: str | None = Field(None, description='备注')
+10 -4
View File
@@ -33,7 +33,7 @@ class AuthService:
"""认证服务类""" """认证服务类"""
@staticmethod @staticmethod
async def user_verify(db: AsyncSession, username: str, password: str) -> User: async def user_verify(db: AsyncSession, username: str, password: str | None) -> User:
""" """
验证用户名和密码 验证用户名和密码
@@ -45,10 +45,16 @@ class AuthService:
user = await user_dao.get_by_username(db, username) user = await user_dao.get_by_username(db, username)
if not user: if not user:
raise errors.NotFoundError(msg='用户名或密码有误') raise errors.NotFoundError(msg='用户名或密码有误')
elif not password_verify(password, user.password):
if user.password is None:
raise errors.AuthorizationError(msg='用户名或密码有误') raise errors.AuthorizationError(msg='用户名或密码有误')
elif not user.status: else:
if not password_verify(password, user.password):
raise errors.AuthorizationError(msg='用户名或密码有误')
if not user.status:
raise errors.AuthorizationError(msg='用户已被锁定, 请联系统管理员') raise errors.AuthorizationError(msg='用户已被锁定, 请联系统管理员')
return user return user
async def swagger_login(self, *, obj: HTTPBasicCredentials) -> tuple[str, User]: async def swagger_login(self, *, obj: HTTPBasicCredentials) -> tuple[str, User]:
@@ -65,7 +71,7 @@ class AuthService:
str(user.id), str(user.id),
user.is_multi_login, user.is_multi_login,
# extra info # extra info
login_type='swagger', swagger=True,
) )
return a_token.access_token, user return a_token.access_token, user
@@ -70,7 +70,7 @@ class DataRuleService:
return model_columns return model_columns
@staticmethod @staticmethod
async def get_select(*, name: str | None = None) -> Select: async def get_select(*, name: str | None) -> Select:
""" """
获取数据规则列表查询条件 获取数据规则列表查询条件
+1 -1
View File
@@ -33,7 +33,7 @@ class DeptService:
@staticmethod @staticmethod
async def get_dept_tree( async def get_dept_tree(
*, name: str | None = None, leader: str | None = None, phone: str | None = None, status: int | None = None *, name: str | None, leader: str | None, phone: str | None, status: int | None
) -> list[dict[str, Any]]: ) -> list[dict[str, Any]]:
""" """
获取部门树形结构 获取部门树形结构
@@ -16,7 +16,7 @@ class LoginLogService:
"""登录日志服务类""" """登录日志服务类"""
@staticmethod @staticmethod
async def get_select(*, username: str | None = None, status: int | None = None, ip: str | None = None) -> Select: async def get_select(*, username: str | None, status: int | None, ip: str | None) -> Select:
""" """
获取登录日志列表查询条件 获取登录日志列表查询条件
+3 -3
View File
@@ -12,7 +12,7 @@ from backend.common.exception import errors
from backend.core.conf import settings from backend.core.conf import settings
from backend.database.db import async_db_session from backend.database.db import async_db_session
from backend.database.redis import redis_client from backend.database.redis import redis_client
from backend.utils.build_tree import get_tree_data from backend.utils.build_tree import get_tree_data, get_vben5_tree_data
class MenuService: class MenuService:
@@ -33,7 +33,7 @@ class MenuService:
return menu return menu
@staticmethod @staticmethod
async def get_menu_tree(*, title: str | None = None, status: int | None = None) -> list[dict[str, Any]]: async def get_menu_tree(*, title: str | None, status: int | None) -> list[dict[str, Any]]:
""" """
获取菜单树形结构 获取菜单树形结构
@@ -79,7 +79,7 @@ class MenuService:
for role in roles: for role in roles:
menu_ids.extend([menu.id for menu in role.menus]) menu_ids.extend([menu.id for menu in role.menus])
menu_select = await menu_dao.get_role_menus(db, request.user.is_superuser, menu_ids) menu_select = await menu_dao.get_role_menus(db, request.user.is_superuser, menu_ids)
menu_tree = get_tree_data(menu_select) menu_tree = get_vben5_tree_data(menu_select)
return menu_tree return menu_tree
@staticmethod @staticmethod
@@ -11,7 +11,7 @@ class OperaLogService:
"""操作日志服务类""" """操作日志服务类"""
@staticmethod @staticmethod
async def get_select(*, username: str | None = None, status: int | None = None, ip: str | None = None) -> Select: async def get_select(*, username: str | None, status: int | None, ip: str | None) -> Select:
""" """
获取操作日志列表查询条件 获取操作日志列表查询条件
+1 -1
View File
@@ -58,7 +58,7 @@ class RoleService:
return roles return roles
@staticmethod @staticmethod
async def get_select(*, name: str | None = None, status: int | None = None) -> Select: async def get_select(*, name: str | None, status: int | None) -> Select:
""" """
获取角色列表查询条件 获取角色列表查询条件
+1 -1
View File
@@ -200,7 +200,7 @@ class UserService:
return count return count
@staticmethod @staticmethod
async def get_select(*, dept: int, username: str = None, phone: str = None, status: int = None) -> Select: async def get_select(*, dept: int, username: str, phone: str, status: int) -> Select:
""" """
获取用户列表查询条件 获取用户列表查询条件
-14
View File
@@ -1,14 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from fastapi import APIRouter
from backend.app.generator.api.v1.gen import router as gen_router
from backend.app.generator.api.v1.gen_business import router as gen_business_router
from backend.app.generator.api.v1.gen_model import router as gen_model_router
from backend.core.conf import settings
v1 = APIRouter(prefix=f'{settings.FASTAPI_API_V1_PATH}/gen', tags=['代码生成'])
v1.include_router(gen_router)
v1.include_router(gen_business_router, prefix='/businesses')
v1.include_router(gen_model_router, prefix='/models')
-4
View File
@@ -1,4 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from backend.app.generator.model.gen_business import GenBusiness
from backend.app.generator.model.gen_model import GenModel
-2
View File
@@ -3,11 +3,9 @@
from fastapi import APIRouter from fastapi import APIRouter
from backend.app.admin.api.router import v1 as admin_v1 from backend.app.admin.api.router import v1 as admin_v1
from backend.app.generator.api.router import v1 as generator_v1
from backend.app.task.api.router import v1 as task_v1 from backend.app.task.api.router import v1 as task_v1
router = APIRouter() router = APIRouter()
router.include_router(admin_v1) router.include_router(admin_v1)
router.include_router(generator_v1)
router.include_router(task_v1) router.include_router(task_v1)
+5 -2
View File
@@ -1,5 +1,7 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import asyncio
from typing import Any from typing import Any
from celery import Task from celery import Task
@@ -34,7 +36,7 @@ class TaskBase(Task):
""" """
await task_notification(msg=f'任务 {task_id} 执行成功') await task_notification(msg=f'任务 {task_id} 执行成功')
async def on_failure(self, exc: Exception, task_id: str, args, kwargs, einfo) -> None: def on_failure(self, exc: Exception, task_id: str, args, kwargs, einfo) -> None:
""" """
任务失败后执行钩子 任务失败后执行钩子
@@ -43,4 +45,5 @@ class TaskBase(Task):
:param einfo: 异常信息 :param einfo: 异常信息
:return: :return:
""" """
await task_notification(msg=f'任务 {task_id} 执行失败') loop = asyncio.get_event_loop()
loop.create_task(task_notification(msg=f'任务 {task_id} 执行失败'))
-150
View File
@@ -119,153 +119,3 @@ class FileType(StrEnum):
image = 'image' image = 'image'
video = 'video' video = 'video'
class GenModelMySQLColumnType(StrEnum):
"""代码生成模型列类型(MySQL"""
# Python 类型映射
BIGINT = 'int'
BigInteger = 'int' # BIGINT
BINARY = 'bytes'
BLOB = 'bytes'
BOOLEAN = 'bool' # BOOL
Boolean = 'bool' # BOOL
CHAR = 'str'
CLOB = 'str'
DATE = 'date'
Date = 'date' # DATE
DATETIME = 'datetime'
DateTime = 'datetime' # DATETIME
DECIMAL = 'Decimal'
DOUBLE = 'float'
Double = 'float' # DOUBLE
DOUBLE_PRECISION = 'float'
Enum = 'Enum' # Enum()
FLOAT = 'float'
Float = 'float' # FLOAT
INT = 'int' # INTEGER
INTEGER = 'int'
Integer = 'int' # INTEGER
Interval = 'timedelta' # DATETIME
JSON = 'dict'
LargeBinary = 'bytes' # BLOB
NCHAR = 'str'
NUMERIC = 'Decimal'
Numeric = 'Decimal' # NUMERIC
NVARCHAR = 'str' # String
PickleType = 'bytes' # BLOB
REAL = 'float'
SMALLINT = 'int'
SmallInteger = 'int' # SMALLINT
String = 'str' # String
TEXT = 'str'
Text = 'str' # TEXT
TIME = 'time'
Time = 'time' # TIME
TIMESTAMP = 'datetime'
Unicode = 'str' # String
UnicodeText = 'str' # TEXT
UUID = 'str | UUID'
Uuid = 'str' # CHAR(32)
VARBINARY = 'bytes'
VARCHAR = 'str' # String
# sa.dialects.mysql 导入
BIT = 'bool'
ENUM = 'Enum'
LONGBLOB = 'bytes'
LONGTEXT = 'str'
MEDIUMBLOB = 'bytes'
MEDIUMINT = 'int'
MEDIUMTEXT = 'str'
SET = 'list[str]'
TINYBLOB = 'bytes'
TINYINT = 'int'
TINYTEXT = 'str'
YEAR = 'int'
class GenModelPostgreSQLColumnType(StrEnum):
"""代码生成模型列类型(PostgreSQL)"""
# Python 类型映射
BIGINT = 'int'
BigInteger = 'int' # BIGINT
BINARY = 'bytes'
BLOB = 'bytes'
BOOLEAN = 'bool'
Boolean = 'bool' # BOOLEAN
CHAR = 'str'
CLOB = 'str'
DATE = 'date'
Date = 'date' # DATE
DATETIME = 'datetime'
DateTime = 'datetime' # TIMESTAMP WITHOUT TIME ZONE
DECIMAL = 'Decimal'
DOUBLE = 'float'
Double = 'float' # DOUBLE PRECISION
DOUBLE_PRECISION = 'float' # DOUBLE PRECISION
Enum = 'Enum' # Enum(name='enum')
FLOAT = 'float'
Float = 'float' # FLOAT
INT = 'int' # INTEGER
INTEGER = 'int'
Integer = 'int' # INTEGER
Interval = 'timedelta' # INTERVAL
JSON = 'dict'
LargeBinary = 'bytes' # BYTEA
NCHAR = 'str'
NUMERIC = 'Decimal'
Numeric = 'Decimal' # NUMERIC
NVARCHAR = 'str' # String
PickleType = 'bytes' # BYTEA
REAL = 'float'
SMALLINT = 'int'
SmallInteger = 'int' # SMALLINT
String = 'str' # String
TEXT = 'str'
Text = 'str' # TEXT
TIME = 'time' # TIME WITHOUT TIME ZONE
Time = 'time' # TIME WITHOUT TIME ZONE
TIMESTAMP = 'datetime' # TIMESTAMP WITHOUT TIME ZONE
Unicode = 'str' # String
UnicodeText = 'str' # TEXT
UUID = 'str | UUID'
Uuid = 'str'
VARBINARY = 'bytes'
VARCHAR = 'str' # String
# sa.dialects.postgresql 导入
ARRAY = 'list'
BIT = 'bool'
BYTEA = 'bytes'
CIDR = 'str'
CITEXT = 'str'
DATEMULTIRANGE = 'list[date]'
DATERANGE = 'tuple[date, date]'
DOMAIN = 'str'
ENUM = 'Enum'
HSTORE = 'dict'
INET = 'str'
INT4MULTIRANGE = 'list[int]'
INT4RANGE = 'tuple[int, int]'
INT8MULTIRANGE = 'list[int]'
INT8RANGE = 'tuple[int, int]'
INTERVAL = 'timedelta'
JSONB = 'dict'
JSONPATH = 'str'
MACADDR = 'str'
MACADDR8 = 'str'
MONEY = 'Decimal'
NUMMULTIRANGE = 'list[Decimal]'
NUMRANGE = 'tuple[Decimal, Decimal]'
OID = 'int'
REGCLASS = 'str'
REGCONFIG = 'str'
TSMULTIRANGE = 'list[datetime]'
TSQUERY = 'str'
TSRANGE = 'tuple[datetime, datetime]'
TSTZMULTIRANGE = 'list[datetime]'
TSTZRANGE = 'tuple[datetime, datetime]'
TSVECTOR = 'str'
+1 -1
View File
@@ -24,7 +24,7 @@ class _CustomPageParams(BaseModel, AbstractParams):
"""自定义分页参数""" """自定义分页参数"""
page: int = Query(1, ge=1, description='页码') page: int = Query(1, ge=1, description='页码')
size: int = Query(20, gt=0, le=100, description='每页数量') size: int = Query(20, gt=0, le=200, description='每页数量')
def to_raw_params(self) -> RawParams: def to_raw_params(self) -> RawParams:
return RawParams( return RawParams(
+3 -1
View File
@@ -68,7 +68,9 @@ class ResponseBase:
@staticmethod @staticmethod
def __response( def __response(
*, res: CustomResponseCode | CustomResponse = None, data: Any | None = None *,
res: CustomResponseCode | CustomResponse,
data: Any | None,
) -> ResponseModel | ResponseSchemaModel: ) -> ResponseModel | ResponseSchemaModel:
""" """
请求返回通用方法 请求返回通用方法
+96 -97
View File
@@ -7,106 +7,105 @@ from pydantic import BaseModel, ConfigDict, EmailStr, Field, validate_email
from backend.core.conf import settings from backend.core.conf import settings
# 自定义验证错误信息 # 自定义验证错误信息,参考:
# 不包含验证预期内容(也就是输入内容),受支持的预期内容字段参考以下链接
# https://github.com/pydantic/pydantic-core/blob/a5cb7382643415b716b1a7a5392914e50f726528/tests/test_errors.py#L266 # https://github.com/pydantic/pydantic-core/blob/a5cb7382643415b716b1a7a5392914e50f726528/tests/test_errors.py#L266
# 替换预期内容字段方式,参考以下链接
# https://github.com/pydantic/pydantic/blob/caa78016433ec9b16a973f92f187a7b6bfde6cb5/docs/errors/errors.md?plain=1#L232 # https://github.com/pydantic/pydantic/blob/caa78016433ec9b16a973f92f187a7b6bfde6cb5/docs/errors/errors.md?plain=1#L232
CUSTOM_VALIDATION_ERROR_MESSAGES = { CUSTOM_VALIDATION_ERROR_MESSAGES = {
'arguments_type': '参数类型输入错误', 'no_such_attribute': "对象没有属性 '{attribute}'",
'assertion_error': '断言执行错误', 'json_invalid': '无效的 JSON: {error}',
'bool_parsing': '布尔值输入解析错误', 'json_type': 'JSON 输入应为字符串、字节或字节数组',
'bool_type': '布尔值类型输入错误', 'recursion_loop': '递归错误 - 检测到循环引用',
'bytes_too_long': '字节长度输入过长', 'model_type': '输入应为有效的字典或 {class_name} 的实例',
'bytes_too_short': '字节长度输入过短', 'model_attributes_type': '输入应为有效的字典或可提取字段的对象',
'bytes_type': '字节类型输入错误', 'dataclass_exact_type': '输入应为 {class_name} 的实例',
'callable_type': '可调用对象类型输入错误', 'dataclass_type': '输入应为字典或 {class_name} 的实例',
'dataclass_exact_type': '数据类实例类型输入错误', 'missing': '字段为必填项',
'dataclass_type': '数据类类型输入错误', 'frozen_field': '字段已冻结',
'date_from_datetime_inexact': '日期分量输入非零', 'frozen_instance': '实例已冻结',
'date_from_datetime_parsing': '日期输入解析错误', 'extra_forbidden': '不允许额外的输入',
'date_future': '日期输入非将来时', 'invalid_key': '键应为字符串',
'date_parsing': '日期输入验证错误', 'get_attribute_error': '提取属性时出错: {error}',
'date_past': '日期输入非过去时', 'none_required': '输入应为 None',
'date_type': '日期类型输入错误', 'enum': '输入应为 {expected}',
'datetime_future': '日期时间输入非将来时间', 'greater_than': '输入应大于 {gt}',
'datetime_object_invalid': '日期时间输入对象无效', 'greater_than_equal': '输入应大于或等于 {ge}',
'datetime_parsing': '日期时间输入解析错误', 'less_than': '输入应小于 {lt}',
'datetime_past': '日期时间输入非过去时间', 'less_than_equal': '输入应小于或等于 {le}',
'datetime_type': '日期时间类型输入错误', 'finite_number': '输入应为有限数字',
'decimal_max_digits': '小数位数输入过多', 'too_short': '{field_type} 在验证后应至少有 {min_length} 个项目,而不是 {actual_length}',
'decimal_max_places': '小数位数输入错误', 'too_long': '{field_type} 在验证后最多应有 {max_length} 个项目,而不是 {actual_length}',
'decimal_parsing': '小数输入解析错误', 'string_type': '输入应为有效的字符串',
'decimal_type': '小数类型输入错误', 'string_sub_type': '输入应为字符串,而不是 str 子类的实例',
'decimal_whole_digits': '小数位数输入错误', 'string_unicode': '输入应为有效的字符串,无法将原始数据解析为 Unicode 字符串',
'dict_type': '字典类型输入错误', 'string_pattern_mismatch': "字符串应匹配模式 '{pattern}'",
'enum': '枚举成员输入错误,允许 {expected}', 'string_too_short': '字符串应至少有 {min_length} 个字符',
'extra_forbidden': '禁止额外字段输入', 'string_too_long': '字符串最多应有 {max_length} 个字符',
'finite_number': '有限值输入错误', 'dict_type': '输入应为有效的字典',
'float_parsing': '浮点数输入解析错误', 'mapping_type': '输入应为有效的映射,错误: {error}',
'float_type': '浮点数类型输入错误', 'iterable_type': '输入应为可迭代对象',
'frozen_field': '冻结字段输入错误', 'iteration_error': '迭代对象时出错,错误: {error}',
'frozen_instance': '冻结实例禁止修改', 'list_type': '输入应为有效的列表',
'frozen_set_type': '冻结类型禁止输入', 'tuple_type': '输入应为有效的元组',
'get_attribute_error': '获取属性错误', 'set_type': '输入应为有效的集合',
'greater_than': '输入值过大', 'bool_type': '输入应为有效的布尔',
'greater_than_equal': '输入值过大或相等', 'bool_parsing': '输入应为有效的布尔值,无法解释输入',
'int_from_float': '整数类型输入错误', 'int_type': '输入应为有效的整数',
'int_parsing': '整数输入解析错误', 'int_parsing': '输入应为有效的整数,无法将字符串解析为整数',
'int_parsing_size': '整数输入解析长度错误', 'int_parsing_size': '无法将输入字符串解析为整数,超出最大大小',
'int_type': '整数类型输入错误', 'int_from_float': '输入应为有效的整数,得到一个带有小数部分的数字',
'invalid_key': '输入无效键值', 'multiple_of': '输入应为 {multiple_of} 的倍数',
'is_instance_of': '类型实例输入错误', 'float_type': '输入应为有效的数字',
'is_subclass_of': '类型子类输入错误', 'float_parsing': '输入应为有效的数字,无法将字符串解析为数字',
'iterable_type': '可迭代类型输入错误', 'bytes_type': '输入应为有效的字节',
'iteration_error': '迭代值输入错误', 'bytes_too_short': '数据应至少有 {min_length} 个字节',
'json_invalid': 'JSON 字符串输入错误', 'bytes_too_long': '数据最多应有 {max_length} 个字节',
'json_type': 'JSON 类型输入错误', 'value_error': '值错误,{error}',
'less_than': '输入值过小', 'assertion_error': '断言失败,{error}',
'less_than_equal': '输入值过小或相等', 'literal_error': '输入应为 {expected}',
'list_type': '列表类型输入错误', 'date_type': '输入应为有效的日期',
'literal_error': '字面值输入错误', 'date_parsing': '输入应为 YYYY-MM-DD 格式的有效日期,{error}',
'mapping_type': '映射类型输入错误', 'date_from_datetime_parsing': '输入应为有效的日期或日期时间,{error}',
'missing': '缺少必填字段', 'date_from_datetime_inexact': '提供给日期的日期时间应具有零时间 - 例如为精确日期',
'missing_argument': '缺少参数', 'date_past': '日期应为过去的时间',
'missing_keyword_only_argument': '缺少关键字参数', 'date_future': '日期应为未来的时间',
'missing_positional_only_argument': '缺少位置参数', 'time_type': '输入应为有效的时间',
'model_attributes_type': '模型属性类型输入错误', 'time_parsing': '输入应为有效的时间格式,{error}',
'model_type': '模型实例输入错误', 'datetime_type': '输入应为有效的日期时间',
'multiple_argument_values': '参数值输入过多', 'datetime_parsing': '输入应为有效的日期时间,{error}',
'multiple_of': '输入值非倍数', 'datetime_object_invalid': '无效的日期时间对象,得到 {error}',
'no_such_attribute': '分配无效属性值', 'datetime_past': '输入应为过去的时间',
'none_required': '输入值必须为 None', 'datetime_future': '输入应为未来的时间',
'recursion_loop': '输入循环赋值', 'timezone_naive': '输入不应包含时区信息',
'set_type': '集合类型输入错误', 'timezone_aware': '输入应包含时区信息',
'string_pattern_mismatch': '字符串约束模式输入不匹配', 'timezone_offset': '需要时区偏移为 {tz_expected},实际得到 {tz_actual}',
'string_sub_type': '字符串子类型(非严格实例)输入错误', 'time_delta_type': '输入应为有效的时间差',
'string_too_long': '字符串输入过长', 'time_delta_parsing': '输入应为有效的时间差,{error}',
'string_too_short': '字符串输入过短', 'frozen_set_type': '输入应为有效的冻结集合',
'string_type': '字符串类型输入错误', 'is_instance_of': '输入应为 {class} 的实例',
'string_unicode': '字符串输入非 Unicode', 'is_subclass_of': '输入应为 {class} 的子类',
'time_delta_parsing': '时间差输入解析错误', 'callable_type': '输入应为可调用对象',
'time_delta_type': '时间差类型输入错误', 'union_tag_invalid': "使用 {discriminator} 找到的输入标签 '{tag}' 与任何预期标签不匹配: {expected_tags}",
'time_parsing': '时间输入解析错误', 'union_tag_not_found': '无法使用区分器 {discriminator} 提取标签',
'time_type': '时间类型输入错误', 'arguments_type': '参数必须是元组、列表或字典',
'timezone_aware': '缺少时区输入信息', 'missing_argument': '缺少必需参数',
'timezone_naive': '禁止时区输入信息', 'unexpected_keyword_argument': '意外的关键字参数',
'too_long': '输入过长', 'missing_keyword_only_argument': '缺少必需的关键字专用参数',
'too_short': '输入过短', 'unexpected_positional_argument': '意外的位置参数',
'tuple_type': '元组类型输入错误', 'missing_positional_only_argument': '缺少必需的位置专用参数',
'unexpected_keyword_argument': '输入意外关键字参数', 'multiple_argument_values': '为参数提供了多个值',
'unexpected_positional_argument': '输入意外位置参数', 'url_type': 'URL 输入应为字符串或 URL',
'union_tag_invalid': '联合类型字面值输入错误', 'url_parsing': '输入应为有效的 URL{error}',
'union_tag_not_found': '联合类型参数输入未找到', 'url_syntax_violation': '输入违反了严格的 URL 语法规则,{error}',
'url_parsing': 'URL 输入解析错误', 'url_too_long': 'URL 最多应有 {max_length} 个字符',
'url_scheme': 'URL 输入方案错误', 'url_scheme': 'URL 方案应为 {expected_schemes}',
'url_syntax_violation': 'URL 输入语法错误', 'uuid_type': 'UUID 输入应为字符串、字节或 UUID 对象',
'url_too_long': 'URL 输入过长', 'uuid_parsing': '输入应为有效的 UUID{error}',
'url_type': 'URL 类型输入错误', 'uuid_version': '预期 UUID 版本为 {expected_version}',
'uuid_parsing': 'UUID 输入解析错误', 'decimal_type': '十进制输入应为整数、浮点数、字符串或 Decimal 对象',
'uuid_type': 'UUID 类型输入错误', 'decimal_parsing': '输入应为有效的十进制数',
'uuid_version': 'UUID 版本类型输入错误', 'decimal_max_digits': '十进制输入总共应不超过 {max_digits} 位数字',
'value_error': '值输入错误', 'decimal_max_places': '十进制输入应不超过 {decimal_places} 位小数',
'decimal_whole_digits': '十进制输入在小数点前应不超过 {whole_digits} 位数字',
} }
CustomPhoneNumber = Annotated[str, Field(pattern=r'^1[3-9]\d{9}$')] CustomPhoneNumber = Annotated[str, Field(pattern=r'^1[3-9]\d{9}$')]
+10 -1
View File
@@ -3,10 +3,12 @@
from fastapi import Depends, Request from fastapi import Depends, Request
from backend.common.enums import MethodType, StatusType from backend.common.enums import MethodType, StatusType
from backend.common.exception import errors
from backend.common.exception.errors import AuthorizationError, TokenError from backend.common.exception.errors import AuthorizationError, TokenError
from backend.common.log import log
from backend.common.security.jwt import DependsJwtAuth from backend.common.security.jwt import DependsJwtAuth
from backend.core.conf import settings from backend.core.conf import settings
from backend.plugin.casbin.utils.rbac import casbin_verify from backend.utils.import_parse import import_module_cached
async def rbac_verify(request: Request, _token: str = DependsJwtAuth) -> None: async def rbac_verify(request: Request, _token: str = DependsJwtAuth) -> None:
@@ -67,6 +69,13 @@ async def rbac_verify(request: Request, _token: str = DependsJwtAuth) -> None:
if path_auth_perm not in allow_perms: if path_auth_perm not in allow_perms:
raise AuthorizationError raise AuthorizationError
else: else:
try:
casbin_rbac = import_module_cached('backend.plugin.casbin.utils.rbac')
casbin_verify = getattr(casbin_rbac, 'casbin_verify')
except (ImportError, AttributeError) as e:
log.error(f'正在通过 casbin 执行 RBAC 权限校验,但此插件不存在: {e}')
raise errors.ServerError(msg='权限校验失败,请联系系统管理员')
await casbin_verify(request) await casbin_verify(request)
-3
View File
@@ -17,9 +17,6 @@ STATIC_DIR = BASE_PATH / 'static'
# 上传文件目录 # 上传文件目录
UPLOAD_DIR = STATIC_DIR / 'upload' UPLOAD_DIR = STATIC_DIR / 'upload'
# jinja2 模版文件路径
JINJA2_TEMPLATE_DIR = BASE_PATH / 'templates'
# 插件目录 # 插件目录
PLUGIN_DIR = BASE_PATH / 'plugin' PLUGIN_DIR = BASE_PATH / 'plugin'
+3 -7
View File
@@ -23,7 +23,7 @@ from backend.database.redis import redis_client
from backend.middleware.jwt_auth_middleware import JwtAuthMiddleware from backend.middleware.jwt_auth_middleware import JwtAuthMiddleware
from backend.middleware.opera_log_middleware import OperaLogMiddleware from backend.middleware.opera_log_middleware import OperaLogMiddleware
from backend.middleware.state_middleware import StateMiddleware from backend.middleware.state_middleware import StateMiddleware
from backend.plugin.tools import plugin_router_inject from backend.plugin.tools import build_final_router
from backend.utils.demo_site import demo_site from backend.utils.demo_site import demo_site
from backend.utils.health_check import ensure_unique_route_names, http_limit_callback from backend.utils.health_check import ensure_unique_route_names, http_limit_callback
from backend.utils.openapi import simplify_operation_ids from backend.utils.openapi import simplify_operation_ids
@@ -157,12 +157,8 @@ def register_router(app: FastAPI) -> None:
""" """
dependencies = [Depends(demo_site)] if settings.DEMO_MODE else None dependencies = [Depends(demo_site)] if settings.DEMO_MODE else None
# 插件路由 # API
plugin_router_inject() router = build_final_router()
# 系统路由(必须在插件路由注入后导入)
from backend.app.router import router
app.include_router(router, dependencies=dependencies) app.include_router(router, dependencies=dependencies)
# Extra # Extra
@@ -34,7 +34,7 @@ class CasbinService:
return await casbin_dao.get_list(ptype, sub) return await casbin_dao.get_list(ptype, sub)
@staticmethod @staticmethod
async def get_policy_list(*, role: int | None = None) -> list: async def get_policy_list(*, role: int | None) -> list:
""" """
获取 P 策略列表 获取 P 策略列表
@@ -0,0 +1,14 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from fastapi import APIRouter
from backend.core.conf import settings
from backend.plugin.code_generator.api.v1.business import router as business_router
from backend.plugin.code_generator.api.v1.column import router as model_router
from backend.plugin.code_generator.api.v1.gen import router as gen_router
v1 = APIRouter(prefix=f'{settings.FASTAPI_API_V1_PATH}/gen', tags=['代码生成'])
v1.include_router(gen_router)
v1.include_router(business_router, prefix='/businesses')
v1.include_router(model_router, prefix='/models')
@@ -4,18 +4,18 @@ from typing import Annotated
from fastapi import APIRouter, Depends, Path from fastapi import APIRouter, Depends, Path
from backend.app.generator.schema.gen_business import (
CreateGenBusinessParam,
GetGenBusinessDetail,
UpdateGenBusinessParam,
)
from backend.app.generator.schema.gen_model import GetGenModelDetail
from backend.app.generator.service.gen_business_service import gen_business_service
from backend.app.generator.service.gen_model_service import gen_model_service
from backend.common.response.response_schema import ResponseModel, ResponseSchemaModel, response_base from backend.common.response.response_schema import ResponseModel, ResponseSchemaModel, response_base
from backend.common.security.jwt import DependsJwtAuth from backend.common.security.jwt import DependsJwtAuth
from backend.common.security.permission import RequestPermission from backend.common.security.permission import RequestPermission
from backend.common.security.rbac import DependsRBAC from backend.common.security.rbac import DependsRBAC
from backend.plugin.code_generator.schema.business import (
CreateGenBusinessParam,
GetGenBusinessDetail,
UpdateGenBusinessParam,
)
from backend.plugin.code_generator.schema.column import GetGenModelDetail
from backend.plugin.code_generator.service.business_service import gen_business_service
from backend.plugin.code_generator.service.column_service import gen_model_service
router = APIRouter() router = APIRouter()
@@ -4,12 +4,12 @@ from typing import Annotated
from fastapi import APIRouter, Depends, Path from fastapi import APIRouter, Depends, Path
from backend.app.generator.schema.gen_model import CreateGenModelParam, GetGenModelDetail, UpdateGenModelParam
from backend.app.generator.service.gen_model_service import gen_model_service
from backend.common.response.response_schema import ResponseModel, ResponseSchemaModel, response_base from backend.common.response.response_schema import ResponseModel, ResponseSchemaModel, response_base
from backend.common.security.jwt import DependsJwtAuth from backend.common.security.jwt import DependsJwtAuth
from backend.common.security.permission import RequestPermission from backend.common.security.permission import RequestPermission
from backend.common.security.rbac import DependsRBAC from backend.common.security.rbac import DependsRBAC
from backend.plugin.code_generator.schema.column import CreateGenModelParam, GetGenModelDetail, UpdateGenModelParam
from backend.plugin.code_generator.service.column_service import gen_model_service
router = APIRouter() router = APIRouter()
@@ -5,13 +5,13 @@ from typing import Annotated
from fastapi import APIRouter, Depends, Path, Query from fastapi import APIRouter, Depends, Path, Query
from fastapi.responses import StreamingResponse from fastapi.responses import StreamingResponse
from backend.app.generator.conf import generator_settings
from backend.app.generator.schema.gen import ImportParam
from backend.app.generator.service.gen_service import gen_service
from backend.common.response.response_schema import ResponseModel, ResponseSchemaModel, response_base from backend.common.response.response_schema import ResponseModel, ResponseSchemaModel, response_base
from backend.common.security.jwt import DependsJwtAuth from backend.common.security.jwt import DependsJwtAuth
from backend.common.security.permission import RequestPermission from backend.common.security.permission import RequestPermission
from backend.common.security.rbac import DependsRBAC from backend.common.security.rbac import DependsRBAC
from backend.plugin.code_generator.conf import generator_settings
from backend.plugin.code_generator.schema.gen import ImportParam
from backend.plugin.code_generator.service.gen_service import gen_service
router = APIRouter() router = APIRouter()
@@ -8,9 +8,6 @@ from pydantic_settings import BaseSettings
class GeneratorSettings(BaseSettings): class GeneratorSettings(BaseSettings):
"""代码生成配置""" """代码生成配置"""
# 模版
TEMPLATE_BACKEND_DIR_NAME: str = 'py'
# 代码下载 # 代码下载
DOWNLOAD_ZIP_FILENAME: str = 'fba_generator' DOWNLOAD_ZIP_FILENAME: str = 'fba_generator'
@@ -5,8 +5,8 @@ from typing import Sequence
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy_crud_plus import CRUDPlus from sqlalchemy_crud_plus import CRUDPlus
from backend.app.generator.model import GenBusiness from backend.plugin.code_generator.model import GenBusiness
from backend.app.generator.schema.gen_business import CreateGenBusinessParam, UpdateGenBusinessParam from backend.plugin.code_generator.schema.business import CreateGenBusinessParam, UpdateGenBusinessParam
class CRUDGenBusiness(CRUDPlus[GenBusiness]): class CRUDGenBusiness(CRUDPlus[GenBusiness]):
@@ -5,14 +5,14 @@ from typing import Sequence
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy_crud_plus import CRUDPlus from sqlalchemy_crud_plus import CRUDPlus
from backend.app.generator.model import GenModel from backend.plugin.code_generator.model import GenColumn
from backend.app.generator.schema.gen_model import CreateGenModelParam, UpdateGenModelParam from backend.plugin.code_generator.schema.column import CreateGenModelParam, UpdateGenModelParam
class CRUDGenModel(CRUDPlus[GenModel]): class CRUDGenModel(CRUDPlus[GenColumn]):
"""代码生成模型 CRUD 类""" """代码生成模型 CRUD 类"""
async def get(self, db: AsyncSession, pk: int) -> GenModel | None: async def get(self, db: AsyncSession, pk: int) -> GenColumn | None:
""" """
获取代码生成模型列 获取代码生成模型列
@@ -22,7 +22,7 @@ class CRUDGenModel(CRUDPlus[GenModel]):
""" """
return await self.select_model(db, pk) return await self.select_model(db, pk)
async def get_all_by_business(self, db: AsyncSession, business_id: int) -> Sequence[GenModel]: async def get_all_by_business(self, db: AsyncSession, business_id: int) -> Sequence[GenColumn]:
""" """
获取所有代码生成模型列 获取所有代码生成模型列
@@ -32,7 +32,7 @@ class CRUDGenModel(CRUDPlus[GenModel]):
""" """
return await self.select_models_order(db, sort_columns='sort', gen_business_id=business_id) return await self.select_models_order(db, sort_columns='sort', gen_business_id=business_id)
async def create(self, db: AsyncSession, obj: CreateGenModelParam, pd_type: str | None = None) -> None: async def create(self, db: AsyncSession, obj: CreateGenModelParam, pd_type: str | None) -> None:
""" """
创建代码生成模型 创建代码生成模型
@@ -43,7 +43,7 @@ class CRUDGenModel(CRUDPlus[GenModel]):
""" """
await self.create_model(db, obj, pd_type=pd_type) await self.create_model(db, obj, pd_type=pd_type)
async def update(self, db: AsyncSession, pk: int, obj: UpdateGenModelParam, pd_type: str | None = None) -> int: async def update(self, db: AsyncSession, pk: int, obj: UpdateGenModelParam, pd_type: str | None) -> int:
""" """
更新代码生成模型 更新代码生成模型
@@ -66,4 +66,4 @@ class CRUDGenModel(CRUDPlus[GenModel]):
return await self.delete_model(db, pk) return await self.delete_model(db, pk)
gen_model_dao: CRUDGenModel = CRUDGenModel(GenModel) gen_model_dao: CRUDGenModel = CRUDGenModel(GenColumn)
@@ -12,7 +12,7 @@ class CRUDGen:
"""代码生成 CRUD 类""" """代码生成 CRUD 类"""
@staticmethod @staticmethod
async def get_all_tables(db: AsyncSession, table_schema: str) -> list[str]: async def get_all_tables(db: AsyncSession, table_schema: str) -> Sequence[str]:
""" """
获取所有表名 获取所有表名
+153
View File
@@ -0,0 +1,153 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from backend.common.enums import StrEnum
class GenModelMySQLColumnType(StrEnum):
"""代码生成模型列类型(MySQL"""
# Python 类型映射
BIGINT = 'int'
BigInteger = 'int' # BIGINT
BINARY = 'bytes'
BLOB = 'bytes'
BOOLEAN = 'bool' # BOOL
Boolean = 'bool' # BOOL
CHAR = 'str'
CLOB = 'str'
DATE = 'date'
Date = 'date' # DATE
DATETIME = 'datetime'
DateTime = 'datetime' # DATETIME
DECIMAL = 'Decimal'
DOUBLE = 'float'
Double = 'float' # DOUBLE
DOUBLE_PRECISION = 'float'
Enum = 'Enum' # Enum()
FLOAT = 'float'
Float = 'float' # FLOAT
INT = 'int' # INTEGER
INTEGER = 'int'
Integer = 'int' # INTEGER
Interval = 'timedelta' # DATETIME
JSON = 'dict'
LargeBinary = 'bytes' # BLOB
NCHAR = 'str'
NUMERIC = 'Decimal'
Numeric = 'Decimal' # NUMERIC
NVARCHAR = 'str' # String
PickleType = 'bytes' # BLOB
REAL = 'float'
SMALLINT = 'int'
SmallInteger = 'int' # SMALLINT
String = 'str' # String
TEXT = 'str'
Text = 'str' # TEXT
TIME = 'time'
Time = 'time' # TIME
TIMESTAMP = 'datetime'
Unicode = 'str' # String
UnicodeText = 'str' # TEXT
UUID = 'str | UUID'
Uuid = 'str' # CHAR(32)
VARBINARY = 'bytes'
VARCHAR = 'str' # String
# sa.dialects.mysql 导入
BIT = 'bool'
ENUM = 'Enum'
LONGBLOB = 'bytes'
LONGTEXT = 'str'
MEDIUMBLOB = 'bytes'
MEDIUMINT = 'int'
MEDIUMTEXT = 'str'
SET = 'list[str]'
TINYBLOB = 'bytes'
TINYINT = 'int'
TINYTEXT = 'str'
YEAR = 'int'
class GenModelPostgreSQLColumnType(StrEnum):
"""代码生成模型列类型(PostgreSQL)"""
# Python 类型映射
BIGINT = 'int'
BigInteger = 'int' # BIGINT
BINARY = 'bytes'
BLOB = 'bytes'
BOOLEAN = 'bool'
Boolean = 'bool' # BOOLEAN
CHAR = 'str'
CLOB = 'str'
DATE = 'date'
Date = 'date' # DATE
DATETIME = 'datetime'
DateTime = 'datetime' # TIMESTAMP WITHOUT TIME ZONE
DECIMAL = 'Decimal'
DOUBLE = 'float'
Double = 'float' # DOUBLE PRECISION
DOUBLE_PRECISION = 'float' # DOUBLE PRECISION
Enum = 'Enum' # Enum(name='enum')
FLOAT = 'float'
Float = 'float' # FLOAT
INT = 'int' # INTEGER
INTEGER = 'int'
Integer = 'int' # INTEGER
Interval = 'timedelta' # INTERVAL
JSON = 'dict'
LargeBinary = 'bytes' # BYTEA
NCHAR = 'str'
NUMERIC = 'Decimal'
Numeric = 'Decimal' # NUMERIC
NVARCHAR = 'str' # String
PickleType = 'bytes' # BYTEA
REAL = 'float'
SMALLINT = 'int'
SmallInteger = 'int' # SMALLINT
String = 'str' # String
TEXT = 'str'
Text = 'str' # TEXT
TIME = 'time' # TIME WITHOUT TIME ZONE
Time = 'time' # TIME WITHOUT TIME ZONE
TIMESTAMP = 'datetime' # TIMESTAMP WITHOUT TIME ZONE
Unicode = 'str' # String
UnicodeText = 'str' # TEXT
UUID = 'str | UUID'
Uuid = 'str'
VARBINARY = 'bytes'
VARCHAR = 'str' # String
# sa.dialects.postgresql 导入
ARRAY = 'list'
BIT = 'bool'
BYTEA = 'bytes'
CIDR = 'str'
CITEXT = 'str'
DATEMULTIRANGE = 'list[date]'
DATERANGE = 'tuple[date, date]'
DOMAIN = 'str'
ENUM = 'Enum'
HSTORE = 'dict'
INET = 'str'
INT4MULTIRANGE = 'list[int]'
INT4RANGE = 'tuple[int, int]'
INT8MULTIRANGE = 'list[int]'
INT8RANGE = 'tuple[int, int]'
INTERVAL = 'timedelta'
JSONB = 'dict'
JSONPATH = 'str'
MACADDR = 'str'
MACADDR8 = 'str'
MONEY = 'Decimal'
NUMMULTIRANGE = 'list[Decimal]'
NUMRANGE = 'tuple[Decimal, Decimal]'
OID = 'int'
REGCLASS = 'str'
REGCONFIG = 'str'
TSMULTIRANGE = 'list[datetime]'
TSQUERY = 'str'
TSRANGE = 'tuple[datetime, datetime]'
TSTZMULTIRANGE = 'list[datetime]'
TSTZRANGE = 'tuple[datetime, datetime]'
TSVECTOR = 'str'
@@ -0,0 +1,4 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from backend.plugin.code_generator.model.business import GenBusiness
from backend.plugin.code_generator.model.column import GenColumn
@@ -10,13 +10,13 @@ from sqlalchemy.orm import Mapped, mapped_column, relationship
from backend.common.model import Base, id_key from backend.common.model import Base, id_key
if TYPE_CHECKING: if TYPE_CHECKING:
from backend.app.generator.model import GenModel from backend.plugin.code_generator.model import GenColumn
class GenBusiness(Base): class GenBusiness(Base):
"""代码生成业务表""" """代码生成业务表"""
__tablename__ = 'sys_gen_business' __tablename__ = 'gen_business'
id: Mapped[id_key] = mapped_column(init=False) id: Mapped[id_key] = mapped_column(init=False)
app_name: Mapped[str] = mapped_column(String(50), comment='应用名称(英文)') app_name: Mapped[str] = mapped_column(String(50), comment='应用名称(英文)')
@@ -26,6 +26,7 @@ class GenBusiness(Base):
table_comment: Mapped[str | None] = mapped_column(String(255), default=None, comment='表描述') table_comment: Mapped[str | None] = mapped_column(String(255), default=None, comment='表描述')
# relate_model_fk: Mapped[int | None] = mapped_column(default=None, comment='关联表外键') # relate_model_fk: Mapped[int | None] = mapped_column(default=None, comment='关联表外键')
schema_name: Mapped[str | None] = mapped_column(String(255), default=None, comment='Schema 名称 (默认为英文表名称)') schema_name: Mapped[str | None] = mapped_column(String(255), default=None, comment='Schema 名称 (默认为英文表名称)')
filename: Mapped[str | None] = mapped_column(String(20), default=None, comment='基础文件名(默认为英文表名称)')
default_datetime_column: Mapped[bool] = mapped_column(default=True, comment='是否存在默认时间列') default_datetime_column: Mapped[bool] = mapped_column(default=True, comment='是否存在默认时间列')
api_version: Mapped[str] = mapped_column(String(20), default='v1', comment='代码生成 api 版本,默认为 v1') api_version: Mapped[str] = mapped_column(String(20), default='v1', comment='代码生成 api 版本,默认为 v1')
gen_path: Mapped[str | None] = mapped_column(String(255), default=None, comment='代码生成路径(默认为 app 根路径)') gen_path: Mapped[str | None] = mapped_column(String(255), default=None, comment='代码生成路径(默认为 app 根路径)')
@@ -33,4 +34,4 @@ class GenBusiness(Base):
LONGTEXT().with_variant(TEXT, 'postgresql'), default=None, comment='备注' LONGTEXT().with_variant(TEXT, 'postgresql'), default=None, comment='备注'
) )
# 代码生成业务模型一对多 # 代码生成业务模型一对多
gen_model: Mapped[list['GenModel']] = relationship(init=False, back_populates='gen_business') gen_column: Mapped[list['GenColumn']] = relationship(init=False, back_populates='gen_business')
@@ -10,18 +10,18 @@ from sqlalchemy.orm import Mapped, mapped_column, relationship
from backend.common.model import DataClassBase, id_key from backend.common.model import DataClassBase, id_key
if TYPE_CHECKING: if TYPE_CHECKING:
from backend.app.generator.model import GenBusiness from backend.plugin.code_generator.model import GenBusiness
class GenModel(DataClassBase): class GenColumn(DataClassBase):
"""代码生成模型表""" """代码生成模型"""
__tablename__ = 'sys_gen_model' __tablename__ = 'gen_column'
id: Mapped[id_key] = mapped_column(init=False) id: Mapped[id_key] = mapped_column(init=False)
name: Mapped[str] = mapped_column(String(50), comment='列名称') name: Mapped[str] = mapped_column(String(50), comment='列名称')
comment: Mapped[str | None] = mapped_column(String(255), default=None, comment='列描述') comment: Mapped[str | None] = mapped_column(String(255), default=None, comment='列描述')
type: Mapped[str] = mapped_column(String(20), default='str', comment='SQLA 模型列类型') type: Mapped[str] = mapped_column(String(20), default='String', comment='SQLA 模型列类型')
pd_type: Mapped[str] = mapped_column(String(20), default='str', comment='列类型对应的 pydantic 类型') pd_type: Mapped[str] = mapped_column(String(20), default='str', comment='列类型对应的 pydantic 类型')
default: Mapped[str | None] = mapped_column( default: Mapped[str | None] = mapped_column(
LONGTEXT().with_variant(TEXT, 'postgresql'), default=None, comment='列默认值' LONGTEXT().with_variant(TEXT, 'postgresql'), default=None, comment='列默认值'
@@ -33,6 +33,6 @@ class GenModel(DataClassBase):
# 代码生成业务模型一对多 # 代码生成业务模型一对多
gen_business_id: Mapped[int] = mapped_column( gen_business_id: Mapped[int] = mapped_column(
ForeignKey('sys_gen_business.id', ondelete='CASCADE'), default=0, comment='代码生成业务ID' ForeignKey('gen_business.id', ondelete='CASCADE'), default=0, comment='代码生成业务ID'
) )
gen_business: Mapped[Union['GenBusiness', None]] = relationship(init=False, back_populates='gen_model') gen_business: Mapped[Union['GenBusiness', None]] = relationship(init=False, back_populates='gen_column')
@@ -0,0 +1,6 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from backend.core.path_conf import PLUGIN_DIR
# jinja2 模版文件路径
JINJA2_TEMPLATE_DIR = PLUGIN_DIR / 'code_generator' / 'templates'
@@ -0,0 +1,2 @@
[app]
router = ['v1']
@@ -2,8 +2,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from datetime import datetime from datetime import datetime
from pydantic import ConfigDict, Field, model_validator from pydantic import ConfigDict, Field
from typing_extensions import Self
from backend.common.schema import SchemaBase from backend.common.schema import SchemaBase
@@ -17,18 +16,12 @@ class GenBusinessSchemaBase(SchemaBase):
table_simple_name_zh: str = Field(description='表名称(中文简称)') table_simple_name_zh: str = Field(description='表名称(中文简称)')
table_comment: str | None = Field(None, description='表描述') table_comment: str | None = Field(None, description='表描述')
schema_name: str | None = Field(None, description='Schema 名称 (默认为英文表名称)') schema_name: str | None = Field(None, description='Schema 名称 (默认为英文表名称)')
filename: str | None = Field(None, description='基础文件名(默认为英文表名称)')
default_datetime_column: bool = Field(True, description='是否存在默认时间列') default_datetime_column: bool = Field(True, description='是否存在默认时间列')
api_version: str = Field('v1', description='代码生成 api 版本') api_version: str = Field('v1', description='代码生成 api 版本')
gen_path: str | None = Field(None, description='代码生成路径(默认为 app 根路径)') gen_path: str | None = Field(None, description='代码生成路径(默认为 app 根路径)')
remark: str | None = Field(None, description='备注') remark: str | None = Field(None, description='备注')
@model_validator(mode='after')
def check_schema_name(self) -> Self:
"""检查并设置 schema 名称"""
if self.schema_name is None:
self.schema_name = self.table_name_en
return self
class CreateGenBusinessParam(GenBusinessSchemaBase): class CreateGenBusinessParam(GenBusinessSchemaBase):
"""创建代码生成业务参数""" """创建代码生成业务参数"""
@@ -3,7 +3,7 @@
from pydantic import ConfigDict, Field, field_validator from pydantic import ConfigDict, Field, field_validator
from backend.common.schema import SchemaBase from backend.common.schema import SchemaBase
from backend.utils.type_conversion import sql_type_to_sqlalchemy from backend.plugin.code_generator.utils.type_conversion import sql_type_to_sqlalchemy
class GenModelSchemaBase(SchemaBase): class GenModelSchemaBase(SchemaBase):
@@ -9,5 +9,5 @@ class ImportParam(SchemaBase):
"""导入参数""" """导入参数"""
app: str = Field(description='应用名称,用于代码生成到指定 app') app: str = Field(description='应用名称,用于代码生成到指定 app')
table_name: str = Field(description='数据库表名')
table_schema: str = Field(description='数据库名') table_schema: str = Field(description='数据库名')
table_name: str = Field(description='数据库表名')
@@ -2,11 +2,11 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from typing import Sequence from typing import Sequence
from backend.app.generator.crud.crud_gen_business import gen_business_dao
from backend.app.generator.model import GenBusiness
from backend.app.generator.schema.gen_business import CreateGenBusinessParam, UpdateGenBusinessParam
from backend.common.exception import errors from backend.common.exception import errors
from backend.database.db import async_db_session from backend.database.db import async_db_session
from backend.plugin.code_generator.crud.crud_business import gen_business_dao
from backend.plugin.code_generator.model import GenBusiness
from backend.plugin.code_generator.schema.business import CreateGenBusinessParam, UpdateGenBusinessParam
class GenBusinessService: class GenBusinessService:
@@ -2,20 +2,20 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from typing import Sequence from typing import Sequence
from backend.app.generator.crud.crud_gen_model import gen_model_dao
from backend.app.generator.model import GenModel
from backend.app.generator.schema.gen_model import CreateGenModelParam, UpdateGenModelParam
from backend.common.enums import GenModelMySQLColumnType
from backend.common.exception import errors from backend.common.exception import errors
from backend.database.db import async_db_session from backend.database.db import async_db_session
from backend.utils.type_conversion import sql_type_to_pydantic from backend.plugin.code_generator.crud.crud_column import gen_model_dao
from backend.plugin.code_generator.enums import GenModelMySQLColumnType
from backend.plugin.code_generator.model import GenColumn
from backend.plugin.code_generator.schema.column import CreateGenModelParam, UpdateGenModelParam
from backend.plugin.code_generator.utils.type_conversion import sql_type_to_pydantic
class GenModelService: class GenModelService:
"""代码生成模型服务类""" """代码生成模型服务类"""
@staticmethod @staticmethod
async def get(*, pk: int) -> GenModel: async def get(*, pk: int) -> GenColumn:
""" """
获取指定 ID 的模型 获取指定 ID 的模型
@@ -36,7 +36,7 @@ class GenModelService:
return types return types
@staticmethod @staticmethod
async def get_by_business(*, business_id: int) -> Sequence[GenModel]: async def get_by_business(*, business_id: int) -> Sequence[GenColumn]:
""" """
获取指定业务的所有模型 获取指定业务的所有模型
@@ -5,31 +5,32 @@ import os.path
import zipfile import zipfile
from pathlib import Path from pathlib import Path
from typing import Sequence
import aiofiles import aiofiles
from pydantic.alias_generators import to_pascal from pydantic.alias_generators import to_pascal
from backend.app.generator.crud.crud_gen import gen_dao
from backend.app.generator.crud.crud_gen_business import gen_business_dao
from backend.app.generator.crud.crud_gen_model import gen_model_dao
from backend.app.generator.model import GenBusiness
from backend.app.generator.schema.gen import ImportParam
from backend.app.generator.schema.gen_business import CreateGenBusinessParam
from backend.app.generator.schema.gen_model import CreateGenModelParam
from backend.app.generator.service.gen_model_service import gen_model_service
from backend.common.exception import errors from backend.common.exception import errors
from backend.core.path_conf import BASE_PATH from backend.core.path_conf import BASE_PATH
from backend.database.db import async_db_session from backend.database.db import async_db_session
from backend.utils.gen_template import gen_template from backend.plugin.code_generator.crud.crud_business import gen_business_dao
from backend.utils.type_conversion import sql_type_to_pydantic from backend.plugin.code_generator.crud.crud_column import gen_model_dao
from backend.plugin.code_generator.crud.crud_gen import gen_dao
from backend.plugin.code_generator.model import GenBusiness
from backend.plugin.code_generator.schema.business import CreateGenBusinessParam
from backend.plugin.code_generator.schema.column import CreateGenModelParam
from backend.plugin.code_generator.schema.gen import ImportParam
from backend.plugin.code_generator.service.column_service import gen_model_service
from backend.plugin.code_generator.utils.gen_template import gen_template
from backend.plugin.code_generator.utils.type_conversion import sql_type_to_pydantic
class GenService: class GenService:
"""代码生成服务类""" """代码生成服务类"""
@staticmethod @staticmethod
async def get_tables(*, table_schema: str) -> list[str]: async def get_tables(*, table_schema: str) -> Sequence[str]:
""" """
获取指定 schema 下的所有表名 获取指定 schema 下的所有表名
@@ -57,14 +58,17 @@ class GenService:
raise errors.ForbiddenError(msg='已存在相同数据库表业务') raise errors.ForbiddenError(msg='已存在相同数据库表业务')
table_name = table_info[0] table_name = table_info[0]
business_data = { new_business = GenBusiness(
'app_name': obj.app, **CreateGenBusinessParam(
'table_name_en': table_name, app_name=obj.app,
'table_name_zh': table_info[1] or ' '.join(table_name.split('_')), table_name_en=table_name,
'table_simple_name_zh': table_info[1] or table_name.split('_')[-1], table_name_zh=table_info[1] or ' '.join(table_name.split('_')),
'table_comment': table_info[1], 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()) schema_name=table_name,
filename=table_name,
).model_dump()
)
db.add(new_business) db.add(new_business)
await db.flush() await db.flush()
@@ -72,17 +76,20 @@ class GenService:
for column in column_info: for column in column_info:
column_type = column[-1].split('(')[0].upper() column_type = column[-1].split('(')[0].upper()
pd_type = sql_type_to_pydantic(column_type) pd_type = sql_type_to_pydantic(column_type)
model_data = { await gen_model_dao.create(
'name': column[0], db,
'comment': column[-2], CreateGenModelParam(
'type': column_type, name=column[0],
'sort': column[-3], comment=column[-2],
'length': column[-1].split('(')[1][:-1] if pd_type == 'str' and '(' in column[-1] else 0, type=column_type,
'is_pk': column[1], sort=column[-3],
'is_nullable': column[2], length=column[-1].split('(')[1][:-1] if pd_type == 'str' and '(' in column[-1] else 0,
'gen_business_id': new_business.id, is_pk=column[1],
} is_nullable=column[2],
await gen_model_dao.create(db, CreateGenModelParam(**model_data), pd_type=pd_type) gen_business_id=new_business.id,
),
pd_type=pd_type,
)
@staticmethod @staticmethod
async def render_tpl_code(*, business: GenBusiness) -> dict[str, str]: async def render_tpl_code(*, business: GenBusiness) -> dict[str, str]:
@@ -99,7 +106,7 @@ class GenService:
gen_vars = gen_template.get_vars(business, gen_models) gen_vars = gen_template.get_vars(business, gen_models)
return { return {
tpl_path: await gen_template.get_template(tpl_path).render_async(**gen_vars) tpl_path: await gen_template.get_template(tpl_path).render_async(**gen_vars)
for tpl_path in gen_template.get_template_paths() for tpl_path in gen_template.get_template_files()
} }
async def preview(self, *, pk: int) -> dict[str, bytes]: async def preview(self, *, pk: int) -> dict[str, bytes]:
@@ -115,10 +122,13 @@ class GenService:
raise errors.NotFoundError(msg='业务不存在') raise errors.NotFoundError(msg='业务不存在')
tpl_code_map = await self.render_tpl_code(business=business) tpl_code_map = await self.render_tpl_code(business=business)
return {
tpl.replace('.jinja', '.py') if tpl.startswith('py') else ...: code.encode('utf-8') codes = {}
for tpl, code in tpl_code_map.items() for tpl, code in tpl_code_map.items():
} if tpl.startswith('python'):
codes[tpl.replace('.jinja', '.py').split('/')[-1]] = code.encode('utf-8')
return codes
@staticmethod @staticmethod
async def get_generate_path(*, pk: int) -> list[str]: async def get_generate_path(*, pk: int) -> list[str]:
@@ -133,9 +143,10 @@ class GenService:
if not business: if not business:
raise errors.NotFoundError(msg='业务不存在') raise errors.NotFoundError(msg='业务不存在')
gen_path = business.gen_path or 'fba-backend-app-path' gen_path = business.gen_path or 'fba-backend-app-dir'
target_files = gen_template.get_code_gen_paths(business) target_files = gen_template.get_code_gen_paths(business)
return [os.path.join(gen_path, *target_file.split('/')[1:]) for target_file in target_files]
return [os.path.join(gen_path, *target_file.split('/')) for target_file in target_files]
async def generate(self, *, pk: int) -> None: async def generate(self, *, pk: int) -> None:
""" """
@@ -155,33 +166,30 @@ class GenService:
for tpl_path, code in tpl_code_map.items(): for tpl_path, code in tpl_code_map.items():
code_filepath = os.path.join( code_filepath = os.path.join(
gen_path, gen_path,
*gen_template.get_code_gen_path(tpl_path, business).split('/')[1:], *gen_template.get_code_gen_path(tpl_path, business).split('/'),
) )
code_folder = Path(str(code_filepath)).parent
code_folder.mkdir(parents=True, exist_ok=True)
# 写入 init 文件 # 写入 init 文件
str_code_filepath = str(code_filepath)
code_folder = Path(str_code_filepath).parent
code_folder.mkdir(parents=True, exist_ok=True)
init_filepath = code_folder.joinpath('__init__.py') init_filepath = code_folder.joinpath('__init__.py')
if not init_filepath.exists():
async with aiofiles.open(init_filepath, 'w', encoding='utf-8') as f: async with aiofiles.open(init_filepath, 'w', encoding='utf-8') as f:
await f.write(gen_template.init_content) await f.write(gen_template.init_content)
if 'api' in str(code_folder):
# api __init__.py # api __init__.py
if 'api' in str_code_filepath:
api_init_filepath = code_folder.parent.joinpath('__init__.py') api_init_filepath = code_folder.parent.joinpath('__init__.py')
if not api_init_filepath.exists():
async with aiofiles.open(api_init_filepath, 'w', encoding='utf-8') as f: async with aiofiles.open(api_init_filepath, 'w', encoding='utf-8') as f:
await f.write(gen_template.init_content) await f.write(gen_template.init_content)
# app __init__.py # app __init__.py
app_init_filepath = api_init_filepath.parent.joinpath('__init__.py') if 'service' in str_code_filepath:
if not app_init_filepath.exists(): app_init_filepath = code_folder.parent.joinpath('__init__.py')
async with aiofiles.open(app_init_filepath, 'w', encoding='utf-8') as f: async with aiofiles.open(app_init_filepath, 'w', encoding='utf-8') as f:
await f.write(gen_template.init_content) await f.write(gen_template.init_content)
# 写入代码文件
async with aiofiles.open(code_filepath, 'w', encoding='utf-8') as f:
await f.write(code)
# model init 文件补充 # model init 文件补充
if code_folder.name == 'model': if code_folder.name == 'model':
async with aiofiles.open(init_filepath, 'a', encoding='utf-8') as f: async with aiofiles.open(init_filepath, 'a', encoding='utf-8') as f:
@@ -190,6 +198,10 @@ class GenService:
f'import {to_pascal(business.table_name_en)}\n', f'import {to_pascal(business.table_name_en)}\n',
) )
# 写入代码文件
async with aiofiles.open(code_filepath, 'w', encoding='utf-8') as f:
await f.write(code)
async def download(self, *, pk: int) -> io.BytesIO: async def download(self, *, pk: int) -> io.BytesIO:
""" """
下载生成的代码 下载生成的代码
@@ -206,13 +218,12 @@ class GenService:
with zipfile.ZipFile(bio, 'w') as zf: with zipfile.ZipFile(bio, 'w') as zf:
tpl_code_map = await self.render_tpl_code(business=business) tpl_code_map = await self.render_tpl_code(business=business)
for tpl_path, code in tpl_code_map.items(): for tpl_path, code in tpl_code_map.items():
# 写入代码文件 code_filepath = gen_template.get_code_gen_path(tpl_path, business)
new_code_path = gen_template.get_code_gen_path(tpl_path, business)
zf.writestr(new_code_path, code)
# 写入 init 文件 # 写入 init 文件
init_filepath = os.path.join(*new_code_path.split('/')[:-1], '__init__.py') code_dir = os.path.dirname(code_filepath)
if 'model' not in new_code_path.split('/'): init_filepath = os.path.join(code_dir, '__init__.py')
if 'model' not in code_filepath.split('/'):
zf.writestr(init_filepath, gen_template.init_content) zf.writestr(init_filepath, gen_template.init_content)
else: else:
zf.writestr( zf.writestr(
@@ -222,11 +233,19 @@ class GenService:
f'import {to_pascal(business.table_name_en)}\n', f'import {to_pascal(business.table_name_en)}\n',
) )
if 'api' in new_code_path:
# api __init__.py # api __init__.py
api_init_filepath = os.path.join(*new_code_path.split('/')[:-2], '__init__.py') if 'api' in code_dir:
api_init_filepath = os.path.join(os.path.dirname(code_dir), '__init__.py')
zf.writestr(api_init_filepath, gen_template.init_content) zf.writestr(api_init_filepath, gen_template.init_content)
# app __init__.py
if 'service' in code_dir:
app_init_filepath = os.path.join(os.path.dirname(code_dir), '__init__.py')
zf.writestr(app_init_filepath, gen_template.init_content)
# 写入代码文件
zf.writestr(code_filepath, code)
bio.seek(0) bio.seek(0)
return bio return bio
@@ -0,0 +1,2 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
@@ -5,10 +5,9 @@ from typing import Sequence
from jinja2 import Environment, FileSystemLoader, Template, select_autoescape from jinja2 import Environment, FileSystemLoader, Template, select_autoescape
from pydantic.alias_generators import to_pascal, to_snake from pydantic.alias_generators import to_pascal, to_snake
from backend.app.generator.conf import generator_settings
from backend.app.generator.model import GenBusiness, GenModel
from backend.core.conf import settings from backend.core.conf import settings
from backend.core.path_conf import JINJA2_TEMPLATE_DIR from backend.plugin.code_generator.model import GenBusiness, GenColumn
from backend.plugin.code_generator.path_conf import JINJA2_TEMPLATE_DIR
class GenTemplate: class GenTemplate:
@@ -34,19 +33,19 @@ class GenTemplate:
return self.env.get_template(jinja_file) return self.env.get_template(jinja_file)
@staticmethod @staticmethod
def get_template_paths() -> list[str]: def get_template_files() -> list[str]:
""" """
获取模板文件路径列表 获取模板文件列表
:return: :return:
""" """
return [ files = []
f'{generator_settings.TEMPLATE_BACKEND_DIR_NAME}/api.jinja',
f'{generator_settings.TEMPLATE_BACKEND_DIR_NAME}/crud.jinja', # python
f'{generator_settings.TEMPLATE_BACKEND_DIR_NAME}/model.jinja', python_template_path = JINJA2_TEMPLATE_DIR / 'python'
f'{generator_settings.TEMPLATE_BACKEND_DIR_NAME}/schema.jinja', files.extend([f'python/{file.name}' for file in python_template_path.iterdir() if file.is_file()])
f'{generator_settings.TEMPLATE_BACKEND_DIR_NAME}/service.jinja',
] return files
@staticmethod @staticmethod
def get_code_gen_paths(business: GenBusiness) -> list[str]: def get_code_gen_paths(business: GenBusiness) -> list[str]:
@@ -57,13 +56,13 @@ class GenTemplate:
:return: :return:
""" """
app_name = business.app_name app_name = business.app_name
module_name = business.table_name_en filename = business.filename
return [ return [
f'{generator_settings.TEMPLATE_BACKEND_DIR_NAME}/{app_name}/api/{business.api_version}/{module_name}.py', f'{app_name}/api/{business.api_version}/{filename}.py',
f'{generator_settings.TEMPLATE_BACKEND_DIR_NAME}/{app_name}/crud/crud_{module_name}.py', f'{app_name}/crud/crud_{filename}.py',
f'{generator_settings.TEMPLATE_BACKEND_DIR_NAME}/{app_name}/model/{module_name}.py', f'{app_name}/model/{filename}.py',
f'{generator_settings.TEMPLATE_BACKEND_DIR_NAME}/{app_name}/schema/{module_name}.py', f'{app_name}/schema/{filename}.py',
f'{generator_settings.TEMPLATE_BACKEND_DIR_NAME}/{app_name}/service/{module_name}_service.py', f'{app_name}/service/{filename}_service.py',
] ]
def get_code_gen_path(self, tpl_path: str, business: GenBusiness) -> str: def get_code_gen_path(self, tpl_path: str, business: GenBusiness) -> str:
@@ -75,11 +74,11 @@ class GenTemplate:
:return: :return:
""" """
target_files = self.get_code_gen_paths(business) target_files = self.get_code_gen_paths(business)
code_gen_path_mapping = dict(zip(self.get_template_paths(), target_files)) code_gen_path_mapping = dict(zip(self.get_template_files(), target_files))
return code_gen_path_mapping[tpl_path] return code_gen_path_mapping[tpl_path]
@staticmethod @staticmethod
def get_vars(business: GenBusiness, models: Sequence[GenModel]) -> dict[str, str | Sequence[GenModel]]: def get_vars(business: GenBusiness, models: Sequence[GenColumn]) -> dict[str, str | Sequence[GenColumn]]:
""" """
获取模板变量 获取模板变量
@@ -1,7 +1,7 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from backend.common.enums import GenModelMySQLColumnType, GenModelPostgreSQLColumnType
from backend.core.conf import settings from backend.core.conf import settings
from backend.plugin.code_generator.enums import GenModelMySQLColumnType, GenModelPostgreSQLColumnType
def sql_type_to_sqlalchemy(typing: str) -> str: def sql_type_to_sqlalchemy(typing: str) -> str:
+2
View File
@@ -0,0 +1,2 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
+2
View File
@@ -0,0 +1,2 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
+2
View File
@@ -0,0 +1,2 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
@@ -0,0 +1,2 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
@@ -4,19 +4,19 @@ from typing import Annotated
from fastapi import APIRouter, Depends, Path, Query from fastapi import APIRouter, Depends, Path, Query
from backend.app.admin.schema.config import (
CreateConfigParam,
GetConfigDetail,
SaveBuiltInConfigParam,
UpdateConfigParam,
)
from backend.app.admin.service.config_service import config_service
from backend.common.pagination import DependsPagination, PageData, paging_data from backend.common.pagination import DependsPagination, PageData, paging_data
from backend.common.response.response_schema import ResponseModel, ResponseSchemaModel, response_base from backend.common.response.response_schema import ResponseModel, ResponseSchemaModel, response_base
from backend.common.security.jwt import DependsJwtAuth from backend.common.security.jwt import DependsJwtAuth
from backend.common.security.permission import RequestPermission from backend.common.security.permission import RequestPermission
from backend.common.security.rbac import DependsRBAC from backend.common.security.rbac import DependsRBAC
from backend.database.db import CurrentSession from backend.database.db import CurrentSession
from backend.plugin.config.schema.config import (
CreateConfigParam,
GetConfigDetail,
SaveBuiltInConfigParam,
UpdateConfigParam,
)
from backend.plugin.config.service.config_service import config_service
router = APIRouter() router = APIRouter()
+21
View File
@@ -0,0 +1,21 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from functools import lru_cache
from pydantic_settings import BaseSettings
class ConfigSettings(BaseSettings):
"""参数配置"""
# 参数
CONFIG_BUILT_IN_TYPES: list[str] = ['website', 'protocol', 'policy']
@lru_cache
def get_config_settings() -> ConfigSettings:
"""获取参数配置"""
return ConfigSettings()
config_settings = get_config_settings()
+2
View File
@@ -0,0 +1,2 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
@@ -6,9 +6,9 @@ from sqlalchemy import Select
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy_crud_plus import CRUDPlus from sqlalchemy_crud_plus import CRUDPlus
from backend.app.admin.conf import admin_settings from backend.plugin.config.conf import config_settings
from backend.app.admin.model import Config from backend.plugin.config.model import Config
from backend.app.admin.schema.config import CreateConfigParam, UpdateConfigParam from backend.plugin.config.schema.config import CreateConfigParam, UpdateConfigParam
class CRUDConfig(CRUDPlus[Config]): class CRUDConfig(CRUDPlus[Config]):
@@ -22,7 +22,7 @@ class CRUDConfig(CRUDPlus[Config]):
:param pk: 参数配置 ID :param pk: 参数配置 ID
:return: :return:
""" """
return await self.select_model_by_column(db, id=pk, type__not_in=admin_settings.CONFIG_BUILT_IN_TYPES) return await self.select_model_by_column(db, id=pk, type__not_in=config_settings.CONFIG_BUILT_IN_TYPES)
async def get_by_type(self, db: AsyncSession, type: str) -> Sequence[Config]: async def get_by_type(self, db: AsyncSession, type: str) -> Sequence[Config]:
""" """
@@ -55,7 +55,7 @@ class CRUDConfig(CRUDPlus[Config]):
""" """
return await self.select_model_by_column(db, key=key) return await self.select_model_by_column(db, key=key)
async def get_list(self, name: str | None = None, type: str | None = None) -> Select: async def get_list(self, name: str | None, type: str | None) -> Select:
""" """
获取参数配置列表 获取参数配置列表
@@ -63,7 +63,7 @@ class CRUDConfig(CRUDPlus[Config]):
:param type: 参数配置类型 :param type: 参数配置类型
:return: :return:
""" """
filters = {'type__not_in': admin_settings.CONFIG_BUILT_IN_TYPES} filters = {'type__not_in': config_settings.CONFIG_BUILT_IN_TYPES}
if name is not None: if name is not None:
filters.update(name__like=f'%{name}%') filters.update(name__like=f'%{name}%')
if type is not None: if type is not None:
@@ -100,7 +100,7 @@ class CRUDConfig(CRUDPlus[Config]):
:return: :return:
""" """
return await self.delete_model_by_column( return await self.delete_model_by_column(
db, allow_multiple=True, id__in=pk, type__not_in=admin_settings.CONFIG_BUILT_IN_TYPES db, allow_multiple=True, id__in=pk, type__not_in=config_settings.CONFIG_BUILT_IN_TYPES
) )
+3
View File
@@ -0,0 +1,3 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from backend.plugin.config.model.config import Config
+6
View File
@@ -0,0 +1,6 @@
[app]
include = 'admin'
[api.config]
prefix = '/configs'
tags = '系统参数配置'
+2
View File
@@ -0,0 +1,2 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
@@ -0,0 +1,2 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
@@ -4,16 +4,16 @@ from typing import Sequence
from sqlalchemy import Select from sqlalchemy import Select
from backend.app.admin.conf import admin_settings from backend.common.exception import errors
from backend.app.admin.crud.crud_config import config_dao from backend.database.db import async_db_session
from backend.app.admin.model import Config from backend.plugin.config.conf import config_settings
from backend.app.admin.schema.config import ( from backend.plugin.config.crud.crud_config import config_dao
from backend.plugin.config.model import Config
from backend.plugin.config.schema.config import (
CreateConfigParam, CreateConfigParam,
SaveBuiltInConfigParam, SaveBuiltInConfigParam,
UpdateConfigParam, UpdateConfigParam,
) )
from backend.common.exception import errors
from backend.database.db import async_db_session
class ConfigService: class ConfigService:
@@ -64,7 +64,7 @@ class ConfigService:
return config return config
@staticmethod @staticmethod
async def get_select(*, name: str | None = None, type: str | None = None) -> Select: async def get_select(*, name: str | None, type: str | None) -> Select:
""" """
获取参数配置列表查询条件 获取参数配置列表查询条件
@@ -83,7 +83,7 @@ class ConfigService:
:return: :return:
""" """
async with async_db_session.begin() as db: async with async_db_session.begin() as db:
if obj.type in admin_settings.CONFIG_BUILT_IN_TYPES: if obj.type in config_settings.CONFIG_BUILT_IN_TYPES:
raise errors.ForbiddenError(msg='非法类型参数') raise errors.ForbiddenError(msg='非法类型参数')
config = await config_dao.get_by_key(db, obj.key) config = await config_dao.get_by_key(db, obj.key)
if config: if config:
+2
View File
@@ -0,0 +1,2 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
+2
View File
@@ -0,0 +1,2 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
+2
View File
@@ -0,0 +1,2 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
@@ -0,0 +1,2 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
@@ -4,19 +4,19 @@ from typing import Annotated
from fastapi import APIRouter, Depends, Path, Query from fastapi import APIRouter, Depends, Path, Query
from backend.app.admin.schema.dict_data import (
CreateDictDataParam,
GetDictDataDetail,
GetDictDataWithRelation,
UpdateDictDataParam,
)
from backend.app.admin.service.dict_data_service import dict_data_service
from backend.common.pagination import DependsPagination, PageData, paging_data from backend.common.pagination import DependsPagination, PageData, paging_data
from backend.common.response.response_schema import ResponseModel, ResponseSchemaModel, response_base from backend.common.response.response_schema import ResponseModel, ResponseSchemaModel, response_base
from backend.common.security.jwt import DependsJwtAuth from backend.common.security.jwt import DependsJwtAuth
from backend.common.security.permission import RequestPermission from backend.common.security.permission import RequestPermission
from backend.common.security.rbac import DependsRBAC from backend.common.security.rbac import DependsRBAC
from backend.database.db import CurrentSession from backend.database.db import CurrentSession
from backend.plugin.dict.schema.dict_data import (
CreateDictDataParam,
GetDictDataDetail,
GetDictDataWithRelation,
UpdateDictDataParam,
)
from backend.plugin.dict.service.dict_data_service import dict_data_service
router = APIRouter() router = APIRouter()
@@ -4,14 +4,14 @@ from typing import Annotated
from fastapi import APIRouter, Depends, Path, Query from fastapi import APIRouter, Depends, Path, Query
from backend.app.admin.schema.dict_type import CreateDictTypeParam, GetDictTypeDetail, UpdateDictTypeParam
from backend.app.admin.service.dict_type_service import dict_type_service
from backend.common.pagination import DependsPagination, PageData, paging_data from backend.common.pagination import DependsPagination, PageData, paging_data
from backend.common.response.response_schema import ResponseModel, ResponseSchemaModel, response_base from backend.common.response.response_schema import ResponseModel, ResponseSchemaModel, response_base
from backend.common.security.jwt import DependsJwtAuth from backend.common.security.jwt import DependsJwtAuth
from backend.common.security.permission import RequestPermission from backend.common.security.permission import RequestPermission
from backend.common.security.rbac import DependsRBAC from backend.common.security.rbac import DependsRBAC
from backend.database.db import CurrentSession from backend.database.db import CurrentSession
from backend.plugin.dict.schema.dict_type import CreateDictTypeParam, GetDictTypeDetail, UpdateDictTypeParam
from backend.plugin.dict.service.dict_type_service import dict_type_service
router = APIRouter() router = APIRouter()
+2
View File
@@ -0,0 +1,2 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
@@ -5,8 +5,8 @@ from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import noload, selectinload from sqlalchemy.orm import noload, selectinload
from sqlalchemy_crud_plus import CRUDPlus from sqlalchemy_crud_plus import CRUDPlus
from backend.app.admin.model import DictData from backend.plugin.dict.model import DictData
from backend.app.admin.schema.dict_data import CreateDictDataParam, UpdateDictDataParam from backend.plugin.dict.schema.dict_data import CreateDictDataParam, UpdateDictDataParam
class CRUDDictData(CRUDPlus[DictData]): class CRUDDictData(CRUDPlus[DictData]):
@@ -22,7 +22,7 @@ class CRUDDictData(CRUDPlus[DictData]):
""" """
return await self.select_model(db, pk) return await self.select_model(db, pk)
async def get_list(self, label: str | None = None, value: str | None = None, status: int | None = None) -> Select: async def get_list(self, label: str | None, value: str | None, status: int | None) -> Select:
""" """
获取字典数据列表 获取字典数据列表
@@ -1,11 +1,12 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from sqlalchemy import Select from sqlalchemy import Select, and_, desc, select
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import noload
from sqlalchemy_crud_plus import CRUDPlus from sqlalchemy_crud_plus import CRUDPlus
from backend.app.admin.model import DictType from backend.plugin.dict.model import DictType
from backend.app.admin.schema.dict_type import CreateDictTypeParam, UpdateDictTypeParam from backend.plugin.dict.schema.dict_type import CreateDictTypeParam, UpdateDictTypeParam
class CRUDDictType(CRUDPlus[DictType]): class CRUDDictType(CRUDPlus[DictType]):
@@ -21,7 +22,7 @@ class CRUDDictType(CRUDPlus[DictType]):
""" """
return await self.select_model(db, pk) return await self.select_model(db, pk)
async def get_list(self, *, name: str | None = None, code: str | None = None, status: int | None = None) -> Select: async def get_list(self, *, name: str | None, code: str | None, status: int | None) -> Select:
""" """
获取字典类型列表 获取字典类型列表
@@ -30,14 +31,20 @@ class CRUDDictType(CRUDPlus[DictType]):
:param status: 字典状态 :param status: 字典状态
:return: :return:
""" """
filters = {} stmt = select(self.model).options(noload(self.model.datas)).order_by(desc(self.model.created_time))
filters = []
if name is not None: if name is not None:
filters.update(name__like=f'%{name}%') filters.append(self.model.name.like(f'%{name}%'))
if code is not None: if code is not None:
filters.update(code__like=f'%{code}%') filters.append(self.model.code.like(f'%{code}%'))
if status is not None: if status is not None:
filters.update(status=status) filters.append(self.model.status == status)
return await self.select_order('created_time', 'desc', **filters)
if filters:
stmt = stmt.where(and_(*filters))
return stmt
async def get_by_code(self, db: AsyncSession, code: str) -> DictType | None: async def get_by_code(self, db: AsyncSession, code: str) -> DictType | None:
""" """

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