Skip to content

Commit 2c577c9

Browse files
committed
update plugin route inject
1 parent e9d3623 commit 2c577c9

File tree

9 files changed

+95
-62
lines changed

9 files changed

+95
-62
lines changed

backend/app/admin/service/data_rule_service.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
from backend.core.conf import settings
1414
from backend.database.db import async_db_session
1515
from backend.database.redis import redis_client
16-
from backend.utils.import_parse import dynamic_import
16+
from backend.utils.import_parse import dynamic_import_data_model
1717

1818

1919
class DataRuleService:
@@ -43,7 +43,7 @@ async def get_columns(model: str) -> list[str]:
4343
if model not in settings.DATA_PERMISSION_MODELS:
4444
raise errors.NotFoundError(msg='数据模型不存在')
4545
try:
46-
model_ins = dynamic_import(settings.DATA_PERMISSION_MODELS[model])
46+
model_ins = dynamic_import_data_model(settings.DATA_PERMISSION_MODELS[model])
4747
except (ImportError, AttributeError):
4848
raise errors.ServerError(msg=f'数据模型 {model} 动态导入失败,请联系系统超级管理员')
4949
model_columns = [

backend/common/security/permission.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from backend.common.exception import errors
1010
from backend.common.exception.errors import ServerError
1111
from backend.core.conf import settings
12-
from backend.utils.import_parse import dynamic_import
12+
from backend.utils.import_parse import dynamic_import_data_model
1313

1414
if TYPE_CHECKING:
1515
from backend.app.admin.schema.data_rule import GetDataRuleDetail
@@ -61,7 +61,7 @@ def filter_data_permission(request: Request) -> ColumnElement[bool]:
6161
if rule_model not in settings.DATA_PERMISSION_MODELS:
6262
raise errors.NotFoundError(msg='数据规则模型不存在')
6363
try:
64-
model_ins = dynamic_import(settings.DATA_PERMISSION_MODELS[rule_model])
64+
model_ins = dynamic_import_data_model(settings.DATA_PERMISSION_MODELS[rule_model])
6565
except (ImportError, AttributeError):
6666
raise errors.ServerError(msg=f'数据模型 {rule_model} 动态导入失败,请联系系统超级管理员')
6767
model_columns = [

backend/core/registrar.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
from backend.middleware.jwt_auth_middleware import JwtAuthMiddleware
2121
from backend.middleware.opera_log_middleware import OperaLogMiddleware
2222
from backend.middleware.state_middleware import StateMiddleware
23-
from backend.plugin.tools import register_plugin_router
23+
from backend.plugin.tools import plugin_router_inject
2424
from backend.utils.demo_site import demo_site
2525
from backend.utils.health_check import ensure_unique_route_names, http_limit_callback
2626
from backend.utils.openapi import simplify_operation_ids
@@ -80,7 +80,6 @@ def register_app():
8080

8181
# 路由
8282
register_router(app)
83-
register_plugin_router(app)
8483

8584
# 分页
8685
register_page(app)
@@ -160,7 +159,8 @@ def register_router(app: FastAPI):
160159
dependencies = [Depends(demo_site)] if settings.DEMO_MODE else None
161160

162161
# API
163-
app.include_router(router, dependencies=dependencies)
162+
new_router = plugin_router_inject(router)
163+
app.include_router(new_router, dependencies=dependencies)
164164

165165
# Extra
166166
ensure_unique_route_names(app)
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
#!/usr/bin/env python3
2+
# -*- coding: utf-8 -*-
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
#!/usr/bin/env python3
2+
# -*- coding: utf-8 -*-
File renamed without changes.

backend/plugin/notice/plugin.toml

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
# 属于哪个 app,如果为独立 app,应设置为 ''
22
app = 'admin'
33

4-
# api 配置,仅针对于非独立 app
4+
# api 路由配置,仅对于非独立 app 可用
55
[api]
6-
# api 注入模块,将生成对应路由
7-
module = 'v1.sys'
8-
# 路由 prefix 配置
9-
prefix = 'notices'
6+
# prefix 必须带前导 /
7+
prefix = '/notices'
8+
tags = '系统通知公告'

backend/plugin/tools.py

Lines changed: 70 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
#!/usr/bin/env python3
22
# -*- coding: utf-8 -*-
3+
import importlib
34
import inspect
45
import os
6+
import warnings
57

68
import rtoml
79

8-
from fastapi import FastAPI
10+
from fastapi import APIRouter
911

1012
from backend.core.path_conf import PLUGIN_DIR
11-
from backend.utils.import_parse import import_module_cached
1213

1314

1415
def get_plugins() -> list[str]:
@@ -31,59 +32,88 @@ def get_plugin_models() -> list:
3132
plugins = get_plugins()
3233
for plugin in plugins:
3334
module_path = f'backend.plugin.{plugin}.model'
34-
module = import_module_cached(module_path)
35+
module = importlib.import_module(module_path)
3536
for name, obj in inspect.getmembers(module):
3637
if inspect.isclass(obj):
3738
classes.append(obj)
3839
return classes
3940

4041

41-
def register_plugin_router(app: FastAPI):
42+
def plugin_router_inject(router: APIRouter) -> APIRouter:
4243
"""
4344
插件路由注入
4445
45-
:param app:
46-
:return:
46+
:param router: 源总路由器
47+
:return: 注入插件路由后的总路由器
4748
"""
4849
plugins = get_plugins()
4950
for plugin in plugins:
5051
toml_path = os.path.join(PLUGIN_DIR, plugin, 'plugin.toml')
5152
if not os.path.exists(toml_path):
52-
raise FileNotFoundError('插件缺少 plugin.toml 配置文件')
53+
raise FileNotFoundError(f'插件 {plugin} 缺少 plugin.toml 配置文件,请检查插件是否合法')
5354

54-
toml_data = rtoml.load(toml_path)
55-
app_name = toml_data.get('app', '')
56-
api_module = toml_data.get('api', {}).get('module', '')
57-
prefix = toml_data.get('api', {}).get('prefix', '')
55+
# 解析 plugin.toml
56+
with open(toml_path, 'r', encoding='utf-8') as f:
57+
data = rtoml.load(f)
58+
app_name = data.get('app', '')
59+
prefix = data.get('api', {}).get('prefix', '')
60+
tags = data.get('api', {}).get('tags', [])
5861

59-
if app_name:
60-
if '.' in api_module:
61-
module_path = f'backend.app.{app_name}.api.{api_module}'
62-
else:
63-
module_path = f'backend.app.{app_name}.api'
64-
else:
65-
module_path = 'backend.app'
62+
# 插件中 API 路由文件的路径
63+
plugin_api_path = os.path.join(PLUGIN_DIR, plugin, 'api')
64+
if not os.path.exists(plugin_api_path):
65+
raise FileNotFoundError(f'插件 {plugin} 缺少 api 目录,请检查插件文件是否完整')
6666

67-
try:
68-
module = import_module_cached(module_path)
69-
except ImportError as e:
70-
raise ImportError(f'导入模块 {module_path} 失败:{e}') from e
67+
# 路由注入
68+
if app_name:
69+
# 非独立应用:将插件中的路由注入到源程序对应模块的路由中
70+
for root, _, api_files in os.walk(plugin_api_path):
71+
for file in api_files:
72+
if file.endswith('.py') and file != '__init__.py':
73+
api_files_path = os.path.join(root, file)
74+
75+
# 获取插件路由模块
76+
path_to_module_str = os.path.relpath(api_files_path, PLUGIN_DIR).replace(os.sep, '.')[:-3]
77+
module_path = f'backend.plugin.{path_to_module_str}'
78+
try:
79+
module = importlib.import_module(module_path)
80+
except ImportError as e:
81+
raise ImportError(f'导入模块 {module_path} 失败:{e}') from e
82+
plugin_router = getattr(module, 'router', None)
83+
if not plugin_router:
84+
warnings.warn(
85+
f'目标模块 {module_path} 中没有有效的 router,请检查插件文件是否完整',
86+
FutureWarning,
87+
)
88+
continue
89+
90+
# 获取源程序路由模块
91+
relative_path = os.path.relpath(root, plugin_api_path)
92+
target_module_path = f'backend.app.{app_name}.api.{relative_path.replace(os.sep, ".")}'
93+
try:
94+
target_module = importlib.import_module(target_module_path)
95+
except ImportError as e:
96+
raise ImportError(f'导入目标模块 {target_module_path} 失败:{e}') from e
97+
target_router = getattr(target_module, 'router', None)
98+
if not target_router or not isinstance(target_router, APIRouter):
99+
raise AttributeError(f'目标模块 {module_path} 中没有有效的 router,请检查插件文件是否完整')
100+
101+
# 将插件路由注入到目标 router 中
102+
target_router.include_router(
103+
router=plugin_router,
104+
prefix=prefix,
105+
tags=tags if type(tags) is list else [tags],
106+
)
71107
else:
72-
if app_name and '.' in api_module:
73-
# 从 backend.app.xxx.api.vx.xxx.__init__.py 文件中获取路由
74-
router = getattr(module, 'router', None)
75-
else:
76-
if 'api' in module_path:
77-
# 从 backend.app.xxx.api 下的 router.py 文件中获取路由
78-
router = getattr(module, api_module.split('.')[0], None)
79-
else:
80-
# 从 backend.app 下的 router.py 文件中获取路由
81-
router = getattr(module, 'router', None)
82-
83-
if not router:
84-
raise ImportError(f'模块 {module_path} 中不存在路由')
85-
86-
if app_name:
87-
app.include_router(router, prefix=f'/{prefix}')
88-
else:
89-
app.include_router(router)
108+
# 独立应用:将插件中的路由直接注入到 app 中
109+
module_path = f'backend.plugin.{plugin}.api.router'
110+
try:
111+
target_module = importlib.import_module(module_path)
112+
except ImportError as e:
113+
raise ImportError(f'导入目标模块 {module_path} 失败:{e}') from e
114+
target_router = getattr(target_module, 'router', None)
115+
if not target_router or not isinstance(target_router, APIRouter):
116+
raise AttributeError(f'目标模块 {module_path} 中没有有效的 router,请检查插件文件是否完整')
117+
router.include_router(target_router)
118+
119+
return router

backend/utils/import_parse.py

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,34 +8,34 @@
88

99
def module_parse(module_path: str) -> tuple:
1010
"""
11-
Parse a module string into a Python module and class/function.
11+
Parse a python module string into a python module and class/function.
1212
1313
:param module_path:
1414
:return:
1515
"""
16-
module_name, class_or_func = module_path.rsplit('.', 1)
17-
return module_name, class_or_func
16+
module_path, class_or_func = module_path.rsplit('.', 1)
17+
return module_path, class_or_func
1818

1919

2020
@lru_cache(maxsize=512)
21-
def import_module_cached(module_name: str) -> Any:
21+
def import_module_cached(module_path: str) -> Any:
2222
"""
2323
缓存导入模块
2424
25-
:param module_name:
25+
:param module_path:
2626
:return:
2727
"""
28-
return importlib.import_module(module_name)
28+
return importlib.import_module(module_path)
2929

3030

31-
def dynamic_import(module_path: str) -> Any:
31+
def dynamic_import_data_model(module_path: str) -> Any:
3232
"""
33-
动态导入
33+
动态导入数据模型
3434
3535
:param module_path:
3636
:return:
3737
"""
38-
module_name, class_or_func = module_parse(module_path)
39-
module = import_module_cached(module_name)
38+
module_path, class_or_func = module_parse(module_path)
39+
module = import_module_cached(module_path)
4040
ins = getattr(module, class_or_func)
4141
return ins

0 commit comments

Comments
 (0)