import os import warnings from collections.abc import Mapping from apiflask import APIFlask from iti.applications.common.utils.schema import custom_schema_name_resolver from iti.applications.service import init_services from iti.modules import init_modules from iti.service_client import init_service_clients from iti.tasks import init_task_runner from .extensions import init_exts from .routes import init_routes from ..config import get_config def create_app(config_name=None, modules=None, config_mapping=None, model_imports=None): """ 应用工厂函数 Args: config_name: 配置名称 ('dev', 'test', 'prod') 如果为 None,则从环境变量 FLASK_ENV 读取 modules: 进程内业务模块列表 config_mapping: 业务项目配置映射,格式与 iti.config.config 一致 model_imports: 业务模型导入函数列表。用于让 Flask-Migrate 看到业务模型 Returns: Flask 应用实例 docs_ui: The UI of API documentation, one of `swagger-ui` (default), `redoc`, `elements`, `rapidoc`, and `rapipdf`. """ # 忽略 apispec 的 schema 名称冲突警告 warnings.filterwarnings( "ignore", message="Multiple schemas resolved to the name", category=UserWarning, module="apispec.ext.marshmallow.openapi", ) app = APIFlask( __name__.split(".")[0], title="iTi-Flask", version="1.0.0", json_errors=True, docs_ui="elements", ) # 加载配置 config_obj = _resolve_config(config_name, config_mapping) app.config.from_object(config_obj) # 配置自定义 schema 名称解析器 # 参考:https://zh.apiflask.com/schema/#%E6%A8%A1%E5%BC%8F%E5%90%8D%E7%A7%B0%E8%A7%A3%E6%9E%90%E5%99%A8 # 用于解决循环引用和嵌套 schema 导致的命名冲突警告 app.schema_name_resolver = custom_schema_name_resolver # 确保必要的目录存在 _ensure_directories(app) # 注册框架 CLI from iti.cli import iti_cli app.cli.add_command(iti_cli, "iti") # 使用第三方JWT,自定义Security,避免doc无法传递header # 等同于 SECURITY_SCHEMES 配置 app.security_schemes = { "JWT": { "type": "apiKey", "in": "header", "name": "Authorization", } } # 保护doc文档(鉴权后才可访问) # app.config['SPEC_DECORATORS'] = [jwt_required()] # app.config['DOCS_DECORATORS'] = [jwt_required()] # 初始化扩展 init_exts(app) # 导入业务模型,确保 Alembic autogenerate 能看到业务表。 for import_models in model_imports or []: import_models() # 初始化可配置服务客户端与任务系统 init_service_clients(app) init_task_runner(app) # 初始化业务模块。模块路由会在系统路由之后注册。 module_registry = init_modules(app, modules) # 初始化路由 init_routes(app) module_registry.run_phase("register_routes", app) module_registry.run_phase("register_permissions", app) module_registry.run_phase("register_menu_seed", app) # 初始化Services init_services(app) # 打印当前环境信息 env = config_name or os.getenv("FLASK_ENV", "dev") print(f"🚀 应用启动 - 环境: {env}") print(f"📊 数据库: {app.config.get('SQLALCHEMY_DATABASE_URI')}") return app def _resolve_config(config_name=None, config_mapping=None): if config_mapping is None: return get_config(config_name) env_name = config_name or os.getenv("FLASK_ENV", "dev") if isinstance(config_mapping, Mapping): return config_mapping.get( env_name, config_mapping.get("default", get_config(config_name)) ) return config_mapping def _ensure_directories(app): """确保必要的目录存在""" # 数据库目录(SQLite) db_uri = app.config.get("SQLALCHEMY_DATABASE_URI", "") if "sqlite:///" in db_uri and not db_uri.endswith(":memory:"): db_path = db_uri.replace("sqlite:///", "") db_dir = os.path.dirname(db_path) if db_dir and not os.path.exists(db_dir): os.makedirs(db_dir, exist_ok=True) file_storage_config = app.config.get("FILE_STORAGE", {}) local_config = file_storage_config.get("LOCAL", {}) local_path = local_config.get("base_path") if local_path: os.makedirs(local_path, exist_ok=True)