Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions backend/alembic/env.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,20 @@
from backend.common.model import MappedBase
from backend.core import path_conf
from backend.database.db import SQLALCHEMY_DATABASE_URL
from backend.plugin.tools import get_plugin_models

# import your new model here
from backend.app.admin.model import * # noqa: F401
from backend.app.generator.model import * # noqa: F401

# import plugin model
for cls in get_plugin_models():
class_name = cls.__name__
if class_name in globals():
print(f'\nWarning: Class "{class_name}" already exists in global namespace.')
else:
globals()[class_name] = cls

if not os.path.exists(path_conf.ALEMBIC_VERSION_DIR):
os.makedirs(path_conf.ALEMBIC_VERSION_DIR)

Expand Down
4 changes: 1 addition & 3 deletions backend/app/admin/api/v1/sys/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
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.notice import router as notice_router
from backend.app.admin.api.v1.sys.role import router as role_router
from backend.app.admin.api.v1.sys.token import router as token_router
from backend.app.admin.api.v1.sys.user import router as user_router
Expand All @@ -23,9 +22,8 @@
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(user_router, prefix='/users', tags=['系统用户'])
router.include_router(data_rule_router, prefix='/data-rules', tags=['系统数据权限规则'])
router.include_router(notice_router, prefix='/notices', tags=['系统通知公告'])
router.include_router(token_router, prefix='/tokens', tags=['系统令牌'])
1 change: 0 additions & 1 deletion backend/app/admin/model/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
from backend.app.admin.model.dict_type import DictType
from backend.app.admin.model.login_log import LoginLog
from backend.app.admin.model.menu import Menu
from backend.app.admin.model.notice import Notice
from backend.app.admin.model.opera_log import OperaLog
from backend.app.admin.model.role import Role
from backend.app.admin.model.user import User
Expand Down
7 changes: 5 additions & 2 deletions backend/app/admin/service/data_rule_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
from backend.core.conf import settings
from backend.database.db import async_db_session
from backend.database.redis import redis_client
from backend.utils.import_parse import dynamic_import
from backend.utils.import_parse import dynamic_import_data_model


class DataRuleService:
Expand Down Expand Up @@ -42,7 +42,10 @@ async def get_models() -> list[str]:
async def get_columns(model: str) -> list[str]:
if model not in settings.DATA_PERMISSION_MODELS:
raise errors.NotFoundError(msg='数据模型不存在')
model_ins = dynamic_import(settings.DATA_PERMISSION_MODELS[model])
try:
model_ins = dynamic_import_data_model(settings.DATA_PERMISSION_MODELS[model])
except (ImportError, AttributeError):
raise errors.ServerError(msg=f'数据模型 {model} 动态导入失败,请联系系统超级管理员')
model_columns = [
key for key in model_ins.__table__.columns.keys() if key not in settings.DATA_PERMISSION_COLUMN_EXCLUDE
]
Expand Down
8 changes: 4 additions & 4 deletions backend/app/router.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
from backend.app.generator.api.router import v1 as generator_v1
from backend.app.task.api.router import v1 as task_v1

route = APIRouter()
router = APIRouter()

route.include_router(admin_v1)
route.include_router(generator_v1)
route.include_router(task_v1)
router.include_router(admin_v1)
router.include_router(generator_v1)
router.include_router(task_v1)
7 changes: 5 additions & 2 deletions backend/common/security/permission.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from backend.common.exception import errors
from backend.common.exception.errors import ServerError
from backend.core.conf import settings
from backend.utils.import_parse import dynamic_import
from backend.utils.import_parse import dynamic_import_data_model

if TYPE_CHECKING:
from backend.app.admin.schema.data_rule import GetDataRuleDetail
Expand Down Expand Up @@ -60,7 +60,10 @@ def filter_data_permission(request: Request) -> ColumnElement[bool]:
rule_model = rule.model
if rule_model not in settings.DATA_PERMISSION_MODELS:
raise errors.NotFoundError(msg='数据规则模型不存在')
model_ins = dynamic_import(settings.DATA_PERMISSION_MODELS[rule_model])
try:
model_ins = dynamic_import_data_model(settings.DATA_PERMISSION_MODELS[rule_model])
except (ImportError, AttributeError):
raise errors.ServerError(msg=f'数据模型 {rule_model} 动态导入失败,请联系系统超级管理员')
model_columns = [
key for key in model_ins.__table__.columns.keys() if key not in settings.DATA_PERMISSION_COLUMN_EXCLUDE
]
Expand Down
3 changes: 3 additions & 0 deletions backend/core/path_conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,6 @@

# jinja2 模版文件路径
JINJA2_TEMPLATE_DIR = os.path.join(BasePath, 'templates')

# 插件目录
PLUGIN_DIR = os.path.join(BasePath, 'plugin')
12 changes: 9 additions & 3 deletions backend/core/registrar.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
from fastapi_pagination import add_pagination
from starlette.middleware.authentication import AuthenticationMiddleware

from backend.app.router import route
from backend.common.exception.exception_handler import register_exception
from backend.common.log import set_customize_logfile, setup_logging
from backend.core.conf import settings
Expand All @@ -20,6 +19,7 @@
from backend.middleware.jwt_auth_middleware import JwtAuthMiddleware
from backend.middleware.opera_log_middleware import OperaLogMiddleware
from backend.middleware.state_middleware import StateMiddleware
from backend.plugin.tools import plugin_router_inject
from backend.utils.demo_site import demo_site
from backend.utils.health_check import ensure_unique_route_names, http_limit_callback
from backend.utils.openapi import simplify_operation_ids
Expand All @@ -39,7 +39,9 @@ async def register_init(app: FastAPI):
await redis_client.open()
# 初始化 limiter
await FastAPILimiter.init(
redis=redis_client, prefix=settings.REQUEST_LIMITER_REDIS_PREFIX, http_callback=http_limit_callback
redis=redis_client,
prefix=settings.REQUEST_LIMITER_REDIS_PREFIX,
http_callback=http_limit_callback,
)

yield
Expand Down Expand Up @@ -156,7 +158,11 @@ def register_router(app: FastAPI):
dependencies = [Depends(demo_site)] if settings.DEMO_MODE else None

# API
app.include_router(route, dependencies=dependencies)
plugin_router_inject()

from backend.app.router import router # 必须在插件路由注入后导入

app.include_router(router, dependencies=dependencies)

# Extra
ensure_unique_route_names(app)
Expand Down
15 changes: 0 additions & 15 deletions backend/main.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,5 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from pathlib import Path

import uvicorn

from backend.core.registrar import register_app

app = register_app()


if __name__ == '__main__':
# 如果你喜欢在 IDE 中进行 DEBUG,main 启动方法会很有帮助
# 如果你喜欢通过 print 方式进行调试,建议使用 fastapi cli 方式启动服务
try:
config = uvicorn.Config(app=f'{Path(__file__).stem}:app', reload=True)
server = uvicorn.Server(config)
server.run()
except Exception as e:
raise e
2 changes: 2 additions & 0 deletions backend/plugin/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
2 changes: 2 additions & 0 deletions backend/plugin/notice/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
2 changes: 2 additions & 0 deletions backend/plugin/notice/api/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
2 changes: 2 additions & 0 deletions backend/plugin/notice/api/v1/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
2 changes: 2 additions & 0 deletions backend/plugin/notice/api/v1/sys/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@

from fastapi import APIRouter, Depends, Path, Query

from backend.app.admin.schema.notice import CreateNoticeParam, GetNoticeDetail, UpdateNoticeParam
from backend.app.admin.service.notice_service import notice_service
from backend.common.pagination import DependsPagination, PageData, paging_data
from backend.common.response.response_schema import ResponseModel, ResponseSchemaModel, response_base
from backend.common.security.jwt import DependsJwtAuth
from backend.common.security.permission import RequestPermission
from backend.common.security.rbac import DependsRBAC
from backend.database.db import CurrentSession
from backend.plugin.notice.schema.notice import CreateNoticeParam, GetNoticeDetail, UpdateNoticeParam
from backend.plugin.notice.service.notice_service import notice_service

router = APIRouter()

Expand Down
2 changes: 2 additions & 0 deletions backend/plugin/notice/crud/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy_crud_plus import CRUDPlus

from backend.app.admin.model import Notice
from backend.app.admin.schema.notice import CreateNoticeParam, UpdateNoticeParam
from backend.plugin.notice.model import Notice
from backend.plugin.notice.schema.notice import CreateNoticeParam, UpdateNoticeParam


class CRUDNotice(CRUDPlus[Notice]):
Expand Down
3 changes: 3 additions & 0 deletions backend/plugin/notice/model/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from backend.plugin.notice.model.notice import Notice
8 changes: 8 additions & 0 deletions backend/plugin/notice/plugin.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# 属于哪个 app,如果为独立 app,应设置为 ''
app = 'admin'

# api 路由配置,仅对于非独立 app 可用
[api]
# prefix 必须带前导 /
prefix = '/notices'
tags = '系统通知公告'
2 changes: 2 additions & 0 deletions backend/plugin/notice/schema/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ class NoticeSchemaBase(SchemaBase):
type: int
author: str
source: str
status: StatusType = Field(StatusType.enable)
status: StatusType = Field(default=StatusType.enable)
content: str


Expand Down
2 changes: 2 additions & 0 deletions backend/plugin/notice/service/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@

from sqlalchemy import Select

from backend.app.admin.crud.crud_notice import notice_dao
from backend.app.admin.model import Notice
from backend.app.admin.schema.notice import CreateNoticeParam, UpdateNoticeParam
from backend.common.exception import errors
from backend.database.db import async_db_session
from backend.plugin.notice.crud.crud_notice import notice_dao
from backend.plugin.notice.model import Notice
from backend.plugin.notice.schema.notice import CreateNoticeParam, UpdateNoticeParam


class NoticeService:
Expand Down
121 changes: 121 additions & 0 deletions backend/plugin/tools.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import inspect
import os
import warnings

import rtoml

from fastapi import APIRouter

from backend.core.path_conf import PLUGIN_DIR
from backend.utils.import_parse import import_module_cached


def get_plugins() -> list[str]:
"""获取插件"""
plugin_packages = []

for item in os.listdir(PLUGIN_DIR):
item_path = os.path.join(PLUGIN_DIR, item)

if os.path.isdir(item_path):
if '__init__.py' in os.listdir(item_path):
plugin_packages.append(item)

return plugin_packages


def get_plugin_models() -> list:
"""获取插件所有模型类"""
classes = []
plugins = get_plugins()
for plugin in plugins:
module_path = f'backend.plugin.{plugin}.model'
module = import_module_cached(module_path)
for name, obj in inspect.getmembers(module):
if inspect.isclass(obj):
classes.append(obj)
return classes


def plugin_router_inject() -> None:
"""
插件路由注入

:return:
"""
plugins = get_plugins()
for plugin in plugins:
toml_path = os.path.join(PLUGIN_DIR, plugin, 'plugin.toml')
if not os.path.exists(toml_path):
raise FileNotFoundError(f'插件 {plugin} 缺少 plugin.toml 配置文件,请检查插件是否合法')

# 解析 plugin.toml
with open(toml_path, 'r', encoding='utf-8') as f:
data = rtoml.load(f)
app_name = data.get('app', '')
prefix = data.get('api', {}).get('prefix', '')
tags = data.get('api', {}).get('tags', [])

# 插件中 API 路由文件的路径
plugin_api_path = os.path.join(PLUGIN_DIR, plugin, 'api')
if not os.path.exists(plugin_api_path):
raise FileNotFoundError(f'插件 {plugin} 缺少 api 目录,请检查插件文件是否完整')

# 路由注入
if app_name:
# 非独立应用:将插件路由注入到对应模块的路由中
for root, _, api_files in os.walk(plugin_api_path):
for file in api_files:
if file.endswith('.py') and file != '__init__.py':
file_path = os.path.join(root, file)

# 获取插件路由模块
path_to_module_str = os.path.relpath(file_path, PLUGIN_DIR).replace(os.sep, '.')[:-3]
module_path = f'backend.plugin.{path_to_module_str}'
try:
module = import_module_cached(module_path)
except ImportError as e:
raise ImportError(f'导入模块 {module_path} 失败:{e}') from e
plugin_router = getattr(module, 'router', None)
if not plugin_router:
warnings.warn(
f'目标模块 {module_path} 中没有有效的 router,请检查插件文件是否完整',
FutureWarning,
)
continue

# 获取源程序路由模块
relative_path = os.path.relpath(root, plugin_api_path)
target_module_path = f'backend.app.{app_name}.api.{relative_path.replace(os.sep, ".")}'
try:
target_module = import_module_cached(target_module_path)
except ImportError as e:
raise ImportError(f'导入目标模块 {target_module_path} 失败:{e}') from e
target_router = getattr(target_module, 'router', None)
if not target_router or not isinstance(target_router, APIRouter):
raise AttributeError(f'目标模块 {module_path} 中没有有效的 router,请检查插件文件是否完整')

# 将插件路由注入到目标 router 中
target_router.include_router(
router=plugin_router,
prefix=prefix,
tags=tags if tags == [] else [tags],
)
else:
# 独立应用:将插件中的路由直接注入到总路由中
module_path = f'backend.plugin.{plugin}.api.router'
try:
module = import_module_cached(module_path)
except ImportError as e:
raise ImportError(f'导入目标模块 {module_path} 失败:{e}') from e
plugin_router = getattr(module, 'router', None)
if not plugin_router or not isinstance(plugin_router, APIRouter):
raise AttributeError(f'目标模块 {module_path} 中没有有效的 router,请检查插件文件是否完整')
target_module_path = 'backend.app.router'
target_module = import_module_cached(target_module_path)
target_router = getattr(target_module, 'router')

# 将插件路由注入到目标 router 中
target_router.include_router(plugin_router)
1 change: 1 addition & 0 deletions backend/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ dependencies = [
"python-jose>=3.3.0",
"python-socketio>=5.12.0",
"redis[hiredis]>=5.2.0",
"rtoml>=0.12.0",
"sqlalchemy-crud-plus==1.6.0",
"sqlalchemy[asyncio]>=2.0.30",
"user-agents==2.2.0",
Expand Down
1 change: 1 addition & 0 deletions backend/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ pyyaml==6.0.2
redis==5.2.1
rich==13.9.4
rsa==4.9
rtoml==0.12.0
ruff==0.9.5
shellingham==1.5.4
simple-websocket==1.1.0
Expand Down
Loading