|
|
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
|
|
|
from .events import init_event_handlers
|
|
|
|
|
|
|
|
|
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_event_handlers(app)
|
|
|
|
|
|
# 初始化路由
|
|
|
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)
|