Files
FastapiAdmin/backend/app/core/discover.py
T
zhangtao 2c7174db32 feat: 重构代码生成模块并优化定时任务功能
refactor(backend): 清理冗余模板文件并统一模板结构
fix(backend): 修复定时任务状态显示与操作逻辑
perf(backend): 优化数据库连接日志和初始化流程
style(frontend): 调整应用列表空状态显示样式
docs: 更新README中的命令说明和环境配置
chore: 添加banner.txt和统一环境变量格式
2025-11-02 18:01:05 +08:00

171 lines
5.6 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.
# -*- coding: utf-8 -*-
"""
集中式路由发现与注册
约定:
- 仅扫描 `app.api.v1` 包内,顶级目录以 `module_` 开头的模块。
- 在各模块任意子目录下的 `controller.py` 中定义的 `APIRouter` 实例会自动被注册。
- 顶级目录 `module_xxx` 会映射为容器路由前缀 `/<xxx>`。
设计目标:
- 稳定、可预测:有序扫描与注册,确定性日志输出。
- 简洁、易维护:职责拆分成小函数,类型提示与清晰注释。
- 安全、可控:去重处理、异常分层记录、可配置的前缀映射与忽略规则。
"""
from __future__ import annotations
import importlib
from pathlib import Path
from typing import Dict, Iterable, Optional, Set, Tuple, List
from fastapi import APIRouter
from app.core.logger import logger
# ----- 约定与配置 -----
MODULE_PREFIX = "module_"
# 可选:前缀映射与忽略规则(需要时可在此处进行配置)
PREFIX_MAP: Dict[str, str] = {
# e.g. "module_application": "/application",
}
EXCLUDE_DIRS: Set[str] = set()
EXCLUDE_FILES: Set[str] = set()
# 创建根路由(对外暴露)
router = APIRouter()
# 记录已注册的 APIRouter,避免重复 include(既包含控制器路由,也包含容器路由)
_seen_router_ids: Set[int] = set()
# ----- 工具函数 -----
def _get_v1_base_dir_and_pkg() -> Tuple[Path, str]:
"""定位 `app.api.v1` 包的文件系统路径与包名。
返回:
- (Path, str): (`app/api/v1` 的路径, 包名 `app.api.v1`)
"""
try:
v1_pkg = importlib.import_module("app.api.v1")
base_dir = Path(next(iter(v1_pkg.__path__)))
base_pkg = v1_pkg.__name__
return base_dir, base_pkg
except Exception as e:
logger.error(f"❌️ 定位 app.api.v1 失败: {e}")
raise
def _iter_controller_files(base_dir: Path) -> Iterable[Path]:
"""递归查找并返回所有 `controller.py` 文件,按路径排序保证确定性。"""
return sorted(base_dir.rglob("controller.py"), key=lambda p: p.as_posix())
def _resolve_prefix(top_module: str) -> Optional[str]:
"""将顶级模块目录名解析为容器前缀。
- 过滤被忽略的目录。
- 仅处理以 `module_` 开头的目录。
- 支持通过 `PREFIX_MAP` 自定义映射,否则默认 `/<xxx>`。
"""
if top_module in EXCLUDE_DIRS:
return None
if not top_module.startswith(MODULE_PREFIX):
return None
mapped = PREFIX_MAP.get(top_module)
return mapped or f"/{top_module[len(MODULE_PREFIX):]}"
def _include_module_routers(mod: object, container: APIRouter) -> int:
"""将模块中的所有 `APIRouter` 实例包含到指定容器路由中。
返回:
- int: 新增注册的路由数量
"""
from fastapi import APIRouter as _APIRouter
added = 0
for attr_name in dir(mod):
attr = getattr(mod, attr_name, None)
if isinstance(attr, _APIRouter):
rid = id(attr)
if rid in _seen_router_ids:
continue
_seen_router_ids.add(rid)
container.include_router(attr)
added += 1
return added
# ----- 主流程 -----
def _discover_and_register() -> None:
"""通过文件系统递归扫描并注册路由到根路由。"""
base_dir, base_pkg = _get_v1_base_dir_and_pkg()
containers: Dict[str, APIRouter] = {}
container_counts: Dict[str, int] = {}
scanned_files = 0
imported_modules = 0
included_routers = 0
for file in _iter_controller_files(base_dir):
rel_path = file.relative_to(base_dir).as_posix()
scanned_files += 1
if rel_path in EXCLUDE_FILES:
continue
parts = file.relative_to(base_dir).parts
if len(parts) < 2:
# 至少应包含顶级目录和文件名(如 module_xxx/.../controller.py
continue
top_module = parts[0]
prefix = _resolve_prefix(top_module)
if not prefix:
# 非约定模块或被忽略
continue
# 拼接模块导入路径: app.api.v1.<...>.controller
mod_path = ".".join((base_pkg,) + tuple(parts[:-1]) + ("controller",))
try:
mod = importlib.import_module(mod_path)
imported_modules += 1
except ModuleNotFoundError:
logger.warning(f"❌️ 未找到控制器模块: {mod_path}")
continue
except Exception as e:
logger.error(f"❌️ 导入控制器失败: {mod_path} -> {e}")
continue
container = containers.setdefault(prefix, APIRouter(prefix=prefix))
try:
added = _include_module_routers(mod, container)
included_routers += added
container_counts[prefix] = container_counts.get(prefix, 0) + added
except Exception as e:
logger.error(f"❌️ 注册控制器路由失败: {mod_path} -> {e}")
# 将容器路由按前缀名称排序后注册到根路由,保证顺序稳定
for prefix in sorted(containers.keys()):
container = containers[prefix]
rid = id(container)
if rid in _seen_router_ids:
continue
_seen_router_ids.add(rid)
router.include_router(container)
# 更丰富的注册日志(含路由数量)
logger.info(f"✅️ 已注册模块容器: {prefix}")
logger.info(
(
f"✅️ 路由发现完成: 扫描文件 {scanned_files}, "
f"导入模块 {imported_modules}, 注册路由 {included_routers}, "
f"容器 {len(containers)}"
)
)
# 执行自动发现注册(模块导入即生效)
_discover_and_register()