Files
FastapiAdmin/backend/app/core/discover.py
T
zhangtao 64bd82ee75 refactor: 完成项目代码结构优化与权限清理
主要变更:
1. 重构健康检查模块,迁移目录结构并合并重复代码
2. 清理路由标签冗余的"系统管理/平台管理"前缀,简化标签名称
3. 重构工作流相关目录与API,统一命名为flow和nodes
4. 重命名缓存删除接口为clear,同步前后端路由与权限
5. 简化接口参数描述,统一批量状态修改的请求体格式
6. 重构动态路由加载逻辑,优化初始化流程
7. 清理废弃的self_service模块,整合租户自助服务到租户API
8. 重构发票管理接口,统一路由前缀与权限控制
9. 优化代码生成模板,补全参数注解与描述
10: 修复日志删除接口的参数解析逻辑
2026-07-02 00:10:02 +08:00

220 lines
9.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
简化的动态路由发现与注册。
目录与命名规范(不满足则无法注册或导入失败):
- 插件必须放在 ``app/plugin`` 下,且**顶级目录名**必须以 ``module_`` 开头,例如
``module_example``、``module_yourfeature``(扫描模式:``module_*/**/controller.py``)。
- 控制器文件名必须为 ``controller.py``(大小写敏感,Linux 上 ``Controller.py`` 无效)。
- 从 ``module_xxx`` 到 ``controller.py`` 的**每一级目录名**须为合法 Python 标识符
(仅字母数字下划线、不以数字开头;不要使用中划线、空格、中文目录名等)。
- 每一级目录应可作为包导入:通常需有 ``__init__.py``(或符合 namespace package 规则)。
- 在 ``controller.py`` 的**模块顶层**定义 ``APIRouter`` 实例并赋值给变量
(如 ``DemoRouter = APIRouter(...)``);定义在函数内部的 router **不会被**扫描到。
路由前缀:顶级目录 ``module_xxx`` 映射为容器前缀 ``/xxx``(去掉前缀 ``module_`` 共 7 个字符)。
常见「路由没注册」原因:
- 目录不叫 ``module_*``,或 ``controller.py`` 不在该树下的任意子路径中。
- 包无法导入:缺 ``__init__.py``、目录名非法、拼写不一致。
- ``controller.py`` 无语法错误但模块内没有任何顶层 ``APIRouter`` 变量。
"""
# 标准库导入
import importlib
import sys
from pathlib import Path
# 第三方库导入
from fastapi import APIRouter
from fastapi import FastAPI as _FastAPI
# 内部库导入
from app.core.logger import logger
class DynamicRouterRegistry:
"""动态路由注册器,管理插件路由的扫描、注册与热重载。
用法::
app.include_router(dynamic_router.init_app(app))
"""
def __init__(self) -> None:
self._app: _FastAPI | None = None
self._cache: APIRouter | None = None
self._registered_prefixes: set[str] = set()
# ── 公开 API ─────────────────────────────────────
def init_app(self, app: _FastAPI) -> "DynamicRouterRegistry":
"""构建动态路由、注册到 app、保存引用——返回 self 支持链式调用。"""
self._app = app
router = self._build()
app.include_router(router)
return self
def reload(self) -> APIRouter:
"""清除缓存并重新扫描,同时更新运行中的 app 路由。"""
if not self._app:
logger.warning("⚠️ app 未初始化(未调用 init_app),无法更新运行中的路由")
return self._build()
# ── 1. 从运行中的 app 移除旧的插件路由 ──
if self._registered_prefixes:
before = len(self._app.routes)
self._app.routes = [
r for r in self._app.routes
if not any(
getattr(r, "path", "").startswith(p)
for p in self._registered_prefixes
)
]
removed = before - len(self._app.routes)
logger.info(f"🧹 已移除 {removed} 条旧插件路由(前缀: {self._registered_prefixes})")
# ── 2. 清除插件模块缓存 ──
self._purge_modules()
# ── 3. 清除内部缓存,重新构建并注册 ──
self._cache = None
router = self._build()
self._app.include_router(router)
logger.info("✅ 新插件路由已挂载到运行中的 app")
return router
# ── 内部方法 ─────────────────────────────────────
def _build(self) -> APIRouter:
"""扫描并构建动态路由(带缓存)。"""
if self._cache is not None:
return self._cache
logger.info("🚀 开始动态路由发现与注册")
root_router = APIRouter()
seen_router_ids: set[int] = set()
try:
base_package = importlib.import_module("app.plugin")
base_dir = Path(next(iter(base_package.__path__)))
controller_files = list(base_dir.glob("module_*/**/controller.py"))
controller_files.sort()
container_routers: dict[str, APIRouter] = {}
for file in controller_files:
rel_path = file.relative_to(base_dir)
path_parts = rel_path.parts
top_module = path_parts[0]
suffix = top_module[7:] if top_module.startswith("module_") else ""
if not suffix:
logger.error(
f"❌ 跳过异常顶级目录名(须为 module_ 前缀且后面还有名称): {top_module!r},"
f"文件: {file}"
)
continue
prefix = f"/{suffix}"
if prefix not in container_routers:
container_routers[prefix] = APIRouter(prefix=prefix)
container_router = container_routers[prefix]
module_path = f"app.plugin.{'.'.join(path_parts[:-1])}.controller"
try:
module = importlib.import_module(module_path)
registered_here = 0
for attr_name in dir(module):
attr_value = getattr(module, attr_name, None)
if isinstance(attr_value, APIRouter):
router_id = id(attr_value)
if router_id not in seen_router_ids:
seen_router_ids.add(router_id)
container_router.include_router(attr_value)
registered_here += 1
logger.info(f" ↳ 注册 APIRouter 变量 `{attr_name}` ← {module_path}")
if registered_here == 0:
logger.warning(
f"⚠️ 模块已加载但未注册任何路由: {module_path}\n"
f" 原因:该文件中未找到**顶层** APIRouter 实例。\n"
f" 规范:在 controller.py 模块顶层定义,例如 "
f"`XxxRouter = APIRouter(route_class=..., prefix=..., tags=[...])`,"
f"不要仅在函数内创建 APIRouter。"
)
except Exception as e:
hint = _import_failure_hint(e)
logger.error(f"❌ 处理模块失败: {module_path}\n {hint}\n 异常: {e!s}")
for prefix, container_router in sorted(container_routers.items()):
route_count = len(container_router.routes)
root_router.include_router(container_router)
if route_count == 0:
logger.warning(
f"⚠️ 容器前缀 {prefix} 下未挂载任何子路由(可能该 module 下所有 controller 均未导出 APIRouter)"
)
logger.info(f"✅ 注册容器: {prefix} (子路由数: {route_count})")
self._registered_prefixes = set(container_routers.keys())
logger.info(f"✅ 动态路由发现完成: 共 {len(container_routers)} 个容器前缀")
self._cache = root_router
return root_router
except Exception as e:
logger.error(f"❌ 动态路由发现整体失败: {e!s}")
return root_router
@staticmethod
def _purge_modules() -> None:
"""从 sys.modules 中清除所有 app.plugin.module_* 模块。"""
prefix = "app.plugin.module_"
purged: list[str] = []
for mod_name in list(sys.modules):
if mod_name.startswith(prefix):
del sys.modules[mod_name]
purged.append(mod_name)
if purged:
logger.info(f"🧹 已清除 {len(purged)} 个插件模块缓存,将重新导入")
else:
logger.info("🧹 未发现需要清除的插件模块缓存")
# ── 模块级单例 ─────────────────────────────────
dynamic_router = DynamicRouterRegistry()
def _import_failure_hint(exc: BaseException) -> str:
"""根据异常类型给出简短排查提示(中文日志)。"""
if isinstance(exc, ModuleNotFoundError):
missing = getattr(exc, "name", None) or str(exc)
return (
f"无法解析模块(ModuleNotFoundError: {missing})。"
"常见原因:① 从 app.plugin 到 controller 的某级目录缺少 __init__.py;"
"② 目录名不是合法 Python 标识符(禁用连字符、空格、中文等);"
"③ 磁盘路径与 import 路径不一致(大小写、子目录名拼写)。"
)
if isinstance(exc, ImportError):
return (
"导入失败(ImportError)。常见原因:controller 或其依赖模块循环导入、"
"第三方依赖未安装、或相对导入路径错误。"
)
if isinstance(exc, SyntaxError):
return f"controller.py 存在语法错误:{exc.msg}(约第 {exc.lineno} 行)。"
if isinstance(exc, PermissionError):
return (
"权限错误(PermissionError)。多见于受限环境(沙箱、部分 CI):"
"import 链上某模块初始化时调用了被禁止的系统能力(如进程池),与目录命名无关。"
"在完整操作系统下重试;若仍失败再结合堆栈排查。"
)
return (
f"未分类异常({type(exc).__name__})。请查看下方堆栈;"
"若与命名/包结构无关,可能是 controller 顶层 import 的依赖在加载时失败。"
)
# 重新导出函数供外部使用
__all__ = ["dynamic_router"]