discover.py 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171
  1. """
  2. 简化的动态路由发现与注册
  3. 目录与命名规范(不满足则无法注册或导入失败):
  4. - 插件必须放在 ``app/plugin`` 下,且**顶级目录名**必须以 ``module_`` 开头,例如
  5. ``module_example``、``module_yourfeature``(扫描模式:``module_*/**/controller.py``)。
  6. - 控制器文件名必须为 ``controller.py``(大小写敏感,Linux 上 ``Controller.py`` 无效)。
  7. - 从 ``module_xxx`` 到 ``controller.py`` 的**每一级目录名**须为合法 Python 标识符
  8. (仅字母数字下划线、不以数字开头;不要使用中划线、空格、中文目录名等)。
  9. - 每一级目录应可作为包导入:通常需有 ``__init__.py``(或符合 namespace package 规则)。
  10. - 在 ``controller.py`` 的**模块顶层**定义 ``APIRouter`` 实例并赋值给变量
  11. (如 ``DemoRouter = APIRouter(...)``);定义在函数内部的 router **不会被**扫描到。
  12. 路由前缀:顶级目录 ``module_xxx`` 映射为容器前缀 ``/xxx``(去掉前缀 ``module_`` 共 7 个字符)。
  13. 常见「路由没注册」原因:
  14. - 目录不叫 ``module_*``,或 ``controller.py`` 不在该树下的任意子路径中。
  15. - 包无法导入:缺 ``__init__.py``、目录名非法、拼写不一致。
  16. - ``controller.py`` 无语法错误但模块内没有任何顶层 ``APIRouter`` 变量。
  17. """
  18. # 标准库导入
  19. import importlib
  20. from pathlib import Path
  21. # 第三方库导入
  22. from fastapi import APIRouter
  23. # 内部库导入
  24. from app.core.logger import log
  25. def _import_failure_hint(exc: BaseException) -> str:
  26. """根据异常类型给出简短排查提示(中文日志)。"""
  27. if isinstance(exc, ModuleNotFoundError):
  28. missing = getattr(exc, "name", None) or str(exc)
  29. return (
  30. f"无法解析模块(ModuleNotFoundError: {missing})。"
  31. "常见原因:① 从 app.plugin 到 controller 的某级目录缺少 __init__.py;"
  32. "② 目录名不是合法 Python 标识符(禁用连字符、空格、中文等);"
  33. "③ 磁盘路径与 import 路径不一致(大小写、子目录名拼写)。"
  34. )
  35. if isinstance(exc, ImportError):
  36. return (
  37. "导入失败(ImportError)。常见原因:controller 或其依赖模块循环导入、"
  38. "第三方依赖未安装、或相对导入路径错误。"
  39. )
  40. if isinstance(exc, SyntaxError):
  41. return f"controller.py 存在语法错误:{exc.msg}(约第 {exc.lineno} 行)。"
  42. if isinstance(exc, PermissionError):
  43. return (
  44. "权限错误(PermissionError)。多见于受限环境(沙箱、部分 CI):"
  45. "import 链上某模块初始化时调用了被禁止的系统能力(如进程池),与目录命名无关。"
  46. "在完整操作系统下重试;若仍失败再结合堆栈排查。"
  47. )
  48. return (
  49. f"未分类异常({type(exc).__name__})。请查看下方堆栈;"
  50. "若与命名/包结构无关,可能是 controller 顶层 import 的依赖在加载时失败。"
  51. )
  52. def get_dynamic_router() -> APIRouter:
  53. """
  54. 执行动态路由发现与注册,返回包含所有动态路由的根路由实例
  55. 返回:
  56. - APIRouter: 包含所有动态路由的根路由实例
  57. """
  58. log.info("🚀 开始动态路由发现与注册")
  59. # 创建根路由实例
  60. root_router = APIRouter()
  61. # 已注册的路由ID集合,用于避免重复注册
  62. seen_router_ids: set[int] = set()
  63. try:
  64. # 获取app.plugin包的路径
  65. base_package = importlib.import_module("app.plugin")
  66. base_dir = Path(next(iter(base_package.__path__)))
  67. # 查找所有符合条件的controller.py文件
  68. # 只扫描module_*目录下的文件
  69. controller_files = list(base_dir.glob("module_*/**/controller.py"))
  70. # 按路径排序,确保注册顺序一致
  71. controller_files.sort()
  72. # 容器路由映射 {prefix: container_router}
  73. container_routers: dict[str, APIRouter] = {}
  74. for file in controller_files:
  75. # 解析文件路径
  76. rel_path = file.relative_to(base_dir)
  77. path_parts = rel_path.parts
  78. # 获取顶级模块名
  79. top_module = path_parts[0]
  80. # 生成路由前缀 (module_xxx -> /xxx)
  81. suffix = top_module[7:] if top_module.startswith("module_") else ""
  82. if not suffix:
  83. log.error(
  84. f"❌ 跳过异常顶级目录名(须为 module_ 前缀且后面还有名称): {top_module!r},"
  85. f"文件: {file}"
  86. )
  87. continue
  88. prefix = f"/{suffix}"
  89. # 获取或创建容器路由
  90. if prefix not in container_routers:
  91. container_routers[prefix] = APIRouter(prefix=prefix)
  92. container_router = container_routers[prefix]
  93. # 生成模块导入路径
  94. module_path = f"app.plugin.{'.'.join(path_parts[:-1])}.controller"
  95. try:
  96. # 动态导入模块
  97. module = importlib.import_module(module_path)
  98. # 查找并注册所有APIRouter实例
  99. registered_here = 0
  100. for attr_name in dir(module):
  101. attr_value = getattr(module, attr_name, None)
  102. # 只注册APIRouter实例,且避免重复注册
  103. if isinstance(attr_value, APIRouter):
  104. router_id = id(attr_value)
  105. if router_id not in seen_router_ids:
  106. seen_router_ids.add(router_id)
  107. container_router.include_router(attr_value)
  108. registered_here += 1
  109. log.debug(f" ↳ 注册 APIRouter 变量 `{attr_name}` ← {module_path}")
  110. if registered_here == 0:
  111. log.warning(
  112. f"⚠️ 模块已加载但未注册任何路由: {module_path}\n"
  113. f" 原因:该文件中未找到**顶层** APIRouter 实例。\n"
  114. f" 规范:在 controller.py 模块顶层定义,例如 "
  115. f"`XxxRouter = APIRouter(route_class=..., prefix=..., tags=[...])`,"
  116. f"不要仅在函数内创建 APIRouter。"
  117. )
  118. except Exception as e:
  119. hint = _import_failure_hint(e)
  120. log.exception(
  121. f"❌ 处理模块失败: {module_path}\n {hint}\n 异常: {e!s}"
  122. )
  123. # 将所有容器路由注册到根路由
  124. for prefix, container_router in sorted(container_routers.items()):
  125. route_count = len(container_router.routes)
  126. root_router.include_router(container_router)
  127. if route_count == 0:
  128. log.warning(
  129. f"⚠️ 容器前缀 {prefix} 下未挂载任何子路由(可能该 module 下所有 controller 均未导出 APIRouter)"
  130. )
  131. log.info(f"✅️ 注册容器: {prefix} (子路由数: {route_count})")
  132. log.info(f"✅️ 动态路由发现完成: 共 {len(container_routers)} 个容器前缀")
  133. return root_router
  134. except Exception as e:
  135. log.exception(f"❌ 动态路由发现整体失败: {e!s}")
  136. # 如果失败,返回一个空的路由实例
  137. return root_router
  138. # 重新导出函数供外部使用
  139. __all__ = ["get_dynamic_router"]