discover.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356
  1. # -*- coding: utf-8 -*-
  2. """
  3. 集中式路由发现与注册
  4. 约定:
  5. - 仅扫描 `app.api.v1` 包内,顶级目录以 `module_` 开头的模块。
  6. - 在各模块任意子目录下的 `controller.py` 中定义的 `APIRouter` 实例会自动被注册。
  7. - 顶级目录 `module_xxx` 会映射为容器路由前缀 `/<xxx>`。
  8. 设计目标:
  9. - 稳定、可预测:有序扫描与注册,确定性日志输出。
  10. - 简洁、易维护:职责拆分成小函数,类型提示与清晰注释。
  11. - 安全、可控:去重处理、异常分层记录、可配置的前缀映射与忽略规则。
  12. - 灵活、可扩展:基于类的设计,支持配置自定义和实例化多套路由系统。
  13. """
  14. from __future__ import annotations
  15. import importlib
  16. from enum import Enum
  17. from pathlib import Path
  18. from typing import Callable, Iterable, Any
  19. from functools import wraps
  20. from fastapi import APIRouter
  21. from app.core.logger import log
  22. def _log_error_handling(func: Callable) -> Callable:
  23. """错误处理装饰器,用于统一捕获和记录方法执行过程中的异常"""
  24. @wraps(func)
  25. def wrapper(self: 'DiscoverRouter', *args: Any, **kwargs: Any) -> Any:
  26. method_name = func.__name__
  27. try:
  28. return func(self, *args, **kwargs)
  29. except ModuleNotFoundError as e:
  30. log.error(f"❌️ 模块未找到 [{method_name}]: {str(e)}")
  31. raise
  32. except ImportError as e:
  33. log.error(f"❌️ 导入错误 [{method_name}]: {str(e)}")
  34. raise
  35. except AttributeError as e:
  36. log.error(f"❌️ 属性错误 [{method_name}]: {str(e)}")
  37. raise
  38. except Exception as e:
  39. log.error(f"❌️ 未知错误 [{method_name}]: {str(e)}")
  40. # 在调试模式下打印完整堆栈信息
  41. if getattr(self, 'debug', False):
  42. import traceback
  43. log.error(traceback.format_exc())
  44. raise
  45. return wrapper
  46. class DiscoverRouter:
  47. """
  48. 路由自动发现与注册器
  49. 提供基于约定的路由自动发现与注册功能,支持自定义配置和灵活扩展。
  50. """
  51. def __init__(
  52. self,
  53. module_prefix: str = "module_",
  54. base_package: str = "app.api.v1",
  55. prefix_map: dict[str, str] | None = None,
  56. exclude_dirs: set[str] | None = None,
  57. exclude_files: set[str] | None = None,
  58. auto_discover: bool = True,
  59. debug: bool = False
  60. ) -> None:
  61. """
  62. 初始化路由发现注册器
  63. 参数:
  64. - module_prefix: 模块目录前缀,默认为 "module_"
  65. - base_package: 基础包名,默认为 "app.api.v1"
  66. - prefix_map: 前缀映射字典,用于自定义路由前缀
  67. - exclude_dirs: 排除的目录集合
  68. - exclude_files: 排除的文件集合
  69. - auto_discover: 是否在初始化时自动执行发现和注册,默认为 True
  70. - debug: 是否启用调试模式,在调试模式下会输出更详细的错误信息,默认为 False
  71. """
  72. self.module_prefix = module_prefix
  73. self.base_package = base_package
  74. self.prefix_map = prefix_map or {}
  75. self.exclude_dirs = exclude_dirs or set()
  76. self.exclude_files = exclude_files or set()
  77. self.debug = debug
  78. self._router = APIRouter()
  79. self._seen_router_ids: set[int] = set()
  80. self._discovery_stats: dict[str, int] = {
  81. "scanned_files": 0,
  82. "imported_modules": 0,
  83. "included_routers": 0,
  84. "container_count": 0
  85. }
  86. # 自动执行发现和注册
  87. if auto_discover:
  88. self.discover_and_register()
  89. @property
  90. def router(self) -> APIRouter:
  91. """获取根路由实例"""
  92. return self._router
  93. @property
  94. def discovery_stats(self) -> dict[str, int]:
  95. """获取路由发现统计信息"""
  96. return self._discovery_stats.copy()
  97. @_log_error_handling
  98. def _get_base_dir_and_pkg(self) -> tuple[Path, str]:
  99. """定位基础包的文件系统路径与包名。
  100. 返回:
  101. - (Path, str): (包的路径, 包名)
  102. """
  103. base_pkg = importlib.import_module(self.base_package)
  104. base_dir = Path(next(iter(base_pkg.__path__)))
  105. log.info(f"📁 基础包路径: {base_dir}, 包名: {base_pkg.__name__}")
  106. return base_dir, base_pkg.__name__
  107. def _iter_controller_files(self, base_dir: Path) -> Iterable[Path]:
  108. """递归查找并返回所有 `controller.py` 文件,按路径排序保证确定性。"""
  109. try:
  110. files = sorted(base_dir.rglob("controller.py"), key=lambda p: p.as_posix())
  111. log.info(f"🔍 发现 {len(files)} 个控制器文件")
  112. return files
  113. except PermissionError as e:
  114. log.error(f"❌️ 权限错误: 无法访问目录 {base_dir}: {str(e)}")
  115. return []
  116. except Exception as e:
  117. log.error(f"❌️ 查找控制器文件失败: {str(e)}")
  118. return []
  119. def _resolve_prefix(self, top_module: str) -> str | None:
  120. """将顶级模块目录名解析为容器前缀。"""
  121. if top_module in self.exclude_dirs:
  122. if self.debug:
  123. log.warning(f"⚠️ 目录 {top_module} 被排除")
  124. return None
  125. if not top_module.startswith(self.module_prefix):
  126. if self.debug:
  127. log.warning(f"⚠️ 目录 {top_module} 不符合前缀约定 {self.module_prefix}")
  128. return None
  129. mapped = self.prefix_map.get(top_module)
  130. if mapped:
  131. log.info(f"🔄 模块 {top_module} 映射到前缀 {mapped}")
  132. return mapped
  133. prefix = f"/{top_module[len(self.module_prefix):]}"
  134. if self.debug:
  135. log.debug(f"📋 模块 {top_module} 使用默认前缀 {prefix}")
  136. return prefix
  137. @_log_error_handling
  138. def _include_module_routers(self, mod: object, container: APIRouter) -> int:
  139. """将模块中的所有 `APIRouter` 实例包含到指定容器路由中。
  140. 返回:
  141. - int: 新增注册的路由数量
  142. """
  143. from fastapi import APIRouter as _APIRouter
  144. added = 0
  145. mod_name = getattr(mod, "__name__", "<unknown>")
  146. router_count = 0
  147. for attr_name in dir(mod):
  148. attr = getattr(mod, attr_name, None)
  149. if isinstance(attr, _APIRouter):
  150. router_count += 1
  151. rid = id(attr)
  152. if rid in self._seen_router_ids:
  153. log.warning(f"⚠️ 路由 {attr_name} 在模块 {mod_name} 中已注册,跳过重复注册")
  154. continue
  155. self._seen_router_ids.add(rid)
  156. container.include_router(attr)
  157. added += 1
  158. log.info(f"➕ 注册路由 {attr_name} 到容器")
  159. if router_count == 0:
  160. log.warning(f"⚠️ 模块 {mod_name} 中未发现 APIRouter 实例")
  161. return added
  162. @_log_error_handling
  163. def discover_and_register(self) -> dict[str, int]:
  164. """
  165. 执行路由发现与注册
  166. 返回:
  167. - dict[str, int]: 包含发现统计信息的字典
  168. - scanned_files: 扫描的文件数量
  169. - imported_modules: 导入的模块数量
  170. - included_routers: 注册的路由数量
  171. - container_count: 容器数量
  172. """
  173. log.info("🚀 开始路由发现与注册...")
  174. base_dir, base_pkg = self._get_base_dir_and_pkg()
  175. containers: dict[str, APIRouter] = {}
  176. container_counts: dict[str, int] = {}
  177. scanned_files = 0
  178. imported_modules = 0
  179. included_routers = 0
  180. try:
  181. for file in self._iter_controller_files(base_dir):
  182. rel_path = file.relative_to(base_dir).as_posix()
  183. scanned_files += 1
  184. if rel_path in self.exclude_files:
  185. log.warning(f"⚠️ 文件 {rel_path} 被排除")
  186. continue
  187. parts = file.relative_to(base_dir).parts
  188. if len(parts) < 2:
  189. log.warning(f"⚠️ 文件路径不完整: {rel_path},跳过")
  190. continue
  191. top_module = parts[0]
  192. prefix = self._resolve_prefix(top_module)
  193. if not prefix:
  194. continue
  195. # 拼接模块导入路径
  196. mod_path = ".".join((base_pkg,) + tuple(parts[:-1]) + ("controller",))
  197. try:
  198. mod = importlib.import_module(mod_path)
  199. imported_modules += 1
  200. log.info(f"📥 导入模块: {mod_path}")
  201. except ModuleNotFoundError:
  202. log.error(f"❌️ 未找到控制器模块: {mod_path}")
  203. continue
  204. except ImportError as e:
  205. log.error(f"❌️ 导入控制器失败: {mod_path} -> {str(e)}")
  206. continue
  207. container = containers.setdefault(prefix, APIRouter(prefix=prefix))
  208. try:
  209. added = self._include_module_routers(mod, container)
  210. included_routers += added
  211. container_counts[prefix] = container_counts.get(prefix, 0) + added
  212. except Exception as e:
  213. log.error(f"❌️ 注册控制器路由失败: {mod_path} -> {str(e)}")
  214. # 将容器路由按前缀名称排序后注册到根路由,保证顺序稳定
  215. for prefix in sorted(containers.keys()):
  216. container = containers[prefix]
  217. rid = id(container)
  218. if rid in self._seen_router_ids:
  219. continue
  220. self._seen_router_ids.add(rid)
  221. self._router.include_router(container)
  222. # 更丰富的注册日志(含路由数量)
  223. count = container_counts.get(prefix, 0)
  224. log.info(f"✅️ 已注册模块容器: {prefix} (路由数: {count})")
  225. # 更新统计信息
  226. stats = {
  227. "scanned_files": scanned_files,
  228. "imported_modules": imported_modules,
  229. "included_routers": included_routers,
  230. "container_count": len(containers)
  231. }
  232. self._discovery_stats = stats
  233. # 生成总结日志
  234. log.info(
  235. (
  236. f"✅️ 路由发现完成: 扫描文件 {scanned_files}, "
  237. f"导入模块 {imported_modules}, 注册路由 {included_routers}, "
  238. f"容器 {len(containers)}"
  239. )
  240. )
  241. return stats
  242. except Exception as e:
  243. log.error(f"❌️ 路由发现与注册过程失败: {str(e)}")
  244. # 确保返回统计信息,即使过程中出错
  245. return self._discovery_stats
  246. def set_debug(self, debug: bool) -> 'DiscoverRouter':
  247. """设置调试模式
  248. 参数:
  249. - debug: 是否开启调试模式
  250. 返回:
  251. - self: 支持链式调用
  252. """
  253. self.debug = debug
  254. log_level = "DEBUG" if debug else "INFO"
  255. log.info(f"⚙️ 调试模式已{'开启' if debug else '关闭'},日志级别: {log_level}")
  256. return self
  257. def add_exclude_dir(self, dir_name: str) -> 'DiscoverRouter':
  258. """添加排除的目录
  259. 参数:
  260. - dir_name: 要排除的目录名称
  261. 返回:
  262. - self: 支持链式调用
  263. """
  264. self.exclude_dirs.add(dir_name)
  265. log.info(f"📝 添加排除目录: {dir_name}")
  266. return self
  267. def add_prefix_map(self, module_name: str, prefix: str) -> 'DiscoverRouter':
  268. """添加前缀映射
  269. 参数:
  270. - module_name: 模块名称
  271. - prefix: 对应的路由前缀
  272. 返回:
  273. - self: 支持链式调用
  274. """
  275. self.prefix_map[module_name] = prefix
  276. log.info(f"📝 添加前缀映射: {module_name} -> {prefix}")
  277. return self
  278. @_log_error_handling
  279. def register_router(self, router: APIRouter, tags: list[str | Enum] | None = None) -> None:
  280. """手动注册一个路由实例
  281. 参数:
  282. - router: 要注册的 APIRouter 实例
  283. - tags: 路由标签,用于 API 文档分组
  284. """
  285. rid = id(router)
  286. if rid not in self._seen_router_ids:
  287. self._seen_router_ids.add(rid)
  288. self._router.include_router(router, tags=tags)
  289. log.info(f"📌 手动注册路由,标签: {tags}")
  290. else:
  291. log.warning(f"⚠️ 路由已存在,跳过重复注册")
  292. # 创建默认实例并执行自动发现注册
  293. _discoverer = DiscoverRouter()
  294. # 保持向后兼容,导出原始的 router 变量
  295. router = _discoverer.router
  296. # 导出 DiscoverRouter 类供外部使用
  297. __all__ = ["DiscoverRouter", "router"]
  298. # 执行自动发现注册(已由 DiscoverRouter 实例内部处理)