ShiErFeng/shierfeng-fastapi-backend/cli/groups/app/controller.py
2026-07-24 09:43:10 +08:00

237 lines
8.7 KiB
Python

from collections import defaultdict
from typing import Any
from fastapi.routing import APIRoute
from cli.bootstrap import APP_BOOTSTRAP, AppBootstrapService
from cli.core import (
DEFAULT_CORE_SERVICES,
CliContextFactory,
CliExecutionService,
)
from cli.exit_codes import DEPENDENCY_ERROR, SUCCESS
from cli.runtime.app import APP_RUNTIME, AppRuntimeService
from cli.runtime.crypto import CRYPTO_RUNTIME, CryptoRuntimeService
from cli.runtime.db import DATABASE_RUNTIME, DatabaseRuntimeService
from cli.runtime.ops import OPERATIONS_RUNTIME, OperationsRuntimeService
from .presenter import AppCommandPresenter
class AppCommandController:
"""
应用命令控制器。
该控制器负责组织 `app` 命令组的上下文准备、runtime 调用、
payload 构建,以及基于输出格式选择 presenter 或直接返回 JSON。
"""
def __init__(
self,
*,
context_factory: CliContextFactory | None = None,
execution_service: CliExecutionService | None = None,
presenter: AppCommandPresenter | None = None,
runtime_service: AppRuntimeService | None = None,
database_runtime: DatabaseRuntimeService | None = None,
operations_runtime: OperationsRuntimeService | None = None,
crypto_runtime: CryptoRuntimeService | None = None,
bootstrap_service: AppBootstrapService | None = None,
) -> None:
"""
初始化应用命令控制器。
:param context_factory: CLI 上下文工厂
:param execution_service: CLI 执行服务
:param presenter: 应用命令文本渲染器
:param runtime_service: 应用运行时服务
:param database_runtime: 数据库运行时服务
:param operations_runtime: 运维运行时服务
:param crypto_runtime: 传输加密运行时服务
:param bootstrap_service: 应用引导服务
:return: None
"""
self.context_factory = context_factory or DEFAULT_CORE_SERVICES.context_factory
self.execution_service = execution_service or DEFAULT_CORE_SERVICES.execution_service
self.presenter = presenter or AppCommandPresenter()
self.runtime_service = runtime_service or APP_RUNTIME
self.database_runtime = database_runtime or DATABASE_RUNTIME
self.operations_runtime = operations_runtime or OPERATIONS_RUNTIME
self.crypto_runtime = crypto_runtime or CRYPTO_RUNTIME
self.bootstrap_service = bootstrap_service or APP_BOOTSTRAP
def run_app(self, env: str) -> None:
"""
启动当前 FastAPI 应用。
:param env: 当前命令运行环境
:return: None
"""
self.bootstrap_service.exec_app_run_command(env)
def doctor(self, env: str, output: str) -> None:
"""
执行应用启动前检查。
:param env: 当前命令运行环境
:param output: 输出格式
:return: None
"""
ctx = self.context_factory.build_readonly(env, output)
db_status = self.execution_service.run_async(self.database_runtime.ping_database())
redis_status = self.execution_service.run_async(self.operations_runtime.ping_redis())
crypto_status = self.crypto_runtime.validate_crypto_config()
payload = {
'env': ctx.env,
'database': db_status,
'redis': redis_status,
'crypto': crypto_status,
}
payload['ok'] = all(item.get('ok', False) for item in (db_status, redis_status, crypto_status))
exit_code = SUCCESS if payload['ok'] else DEPENDENCY_ERROR
self.execution_service.complete_payload_with_text(
ctx,
payload,
text_builder=self.presenter.build_doctor_text,
default_exit_code=exit_code,
)
def show_config(self, env: str, output: str) -> None:
"""
输出当前应用配置快照。
:param env: 当前命令运行环境
:param output: 输出格式
:return: None
"""
ctx = self.context_factory.build_readonly(env, output)
self.execution_service.complete_payload_result(
ctx,
{'ok': True, 'env': ctx.env, 'config': self.runtime_service.get_app_config_snapshot()},
text_builder=self.presenter.build_app_config_text,
default_exit_code=SUCCESS,
)
def show_env(self, env: str, output: str) -> None:
"""
输出当前 CLI 解析到的应用环境信息。
:param env: 当前命令运行环境
:param output: 输出格式
:return: None
"""
ctx = self.context_factory.build_readonly(env, output)
self.execution_service.complete_payload_result(
ctx,
{'ok': True, 'env': ctx.env, 'runtime': self.runtime_service.get_app_env_snapshot()},
text_builder=self.presenter.build_app_env_text,
default_exit_code=SUCCESS,
)
def show_routes(
self,
env: str,
output: str,
*,
path_prefix: str,
method: str,
group_by: str,
include_hidden: bool,
) -> None:
"""
输出当前应用注册路由清单。
:param env: 当前命令运行环境
:param output: 输出格式
:param path_prefix: 路由前缀过滤条件
:param method: 请求方法过滤条件
:param group_by: 路由分组方式
:param include_hidden: 是否包含隐藏路由
:return: None
"""
ctx = self.context_factory.build_readonly(env, output)
routes_payload = self._serialize_routes(
self.runtime_service.build_app_instance(),
path_prefix=path_prefix,
method=method,
include_hidden=include_hidden,
)
grouped_routes = self._group_routes_by_tag(routes_payload) if group_by == 'tag' else None
payload = {
'ok': True,
'env': ctx.env,
'count': len(routes_payload),
'filters': {
'pathPrefix': path_prefix,
'method': method.upper().strip(),
'groupBy': group_by,
'includeHidden': include_hidden,
},
'routes': routes_payload,
'groupedRoutes': grouped_routes,
}
self.execution_service.complete_payload_with_text(
ctx,
payload,
text_builder=self.presenter.build_routes_text,
default_exit_code=SUCCESS,
text_condition=lambda data: data.get('ok', False),
)
@staticmethod
def _serialize_routes(
app_instance: Any,
*,
path_prefix: str = '',
method: str = '',
include_hidden: bool = False,
) -> list[dict[str, Any]]:
"""
序列化 FastAPI 路由信息。
:param app_instance: FastAPI 应用实例
:param path_prefix: 路径前缀过滤条件
:param method: 请求方法过滤条件
:param include_hidden: 是否包含未出现在 OpenAPI 中的路由
:return: 序列化后的路由列表
"""
normalized_method = method.upper().strip()
routes = []
for route in app_instance.routes:
if not isinstance(route, APIRoute):
continue
if not include_hidden and not route.include_in_schema:
continue
if path_prefix and not route.path.startswith(path_prefix):
continue
route_methods = sorted(item for item in route.methods if item not in {'HEAD', 'OPTIONS'})
if normalized_method and normalized_method not in route_methods:
continue
routes.append(
{
'path': route.path,
'methods': route_methods,
'name': route.name,
'summary': route.summary or '',
'operationId': route.operation_id or '',
'tags': route.tags or [],
'includeInSchema': route.include_in_schema,
}
)
return sorted(routes, key=lambda item: (item['path'], ','.join(item['methods'])))
@staticmethod
def _group_routes_by_tag(routes_payload: list[dict[str, Any]]) -> dict[str, list[dict[str, Any]]]:
"""
按标签对路由信息分组。
:param routes_payload: 原始路由列表
:return: 按标签分组后的路由映射
"""
grouped_routes: dict[str, list[dict[str, Any]]] = defaultdict(list)
for route in routes_payload:
tags = route.get('tags') or ['__untagged__']
for tag in tags:
grouped_routes[tag].append(route)
return dict(sorted(grouped_routes.items(), key=lambda item: item[0]))