import logging import os from flask import Flask, jsonify from flask_cors import CORS from flask_sqlalchemy import SQLAlchemy from app.config.config import ( apply_runtime_env_config, config, current_env, log_llm_env_status, log_service_env_status, ) from app.config.cors import CORS_OPTIONS db = SQLAlchemy() def create_app(): """Create and configure the Flask application""" app = Flask(__name__) # 加载配置 app.config.from_object(config[current_env]) apply_runtime_env_config(app) # 初始化扩展 # 配置CORS以解决跨域问题 CORS(app, **CORS_OPTIONS) db.init_app(app) # 注册蓝图 from app.api.business_domain import bp as business_domain_bp from app.api.data_factory import bp as data_factory_bp from app.api.data_flow import bp as data_flow_bp from app.api.data_interface import bp as data_interface_bp from app.api.data_service import bp as data_service_bp from app.api.data_source import bp as data_source_bp from app.api.graph import bp as graph_bp from app.api.meta_data import bp as meta_bp from app.api.system import bp as system_bp app.register_blueprint(meta_bp, url_prefix="/api/meta") app.register_blueprint(data_interface_bp, url_prefix="/api/interface") app.register_blueprint(graph_bp, url_prefix="/api/graph") app.register_blueprint(system_bp, url_prefix="/api/system") app.register_blueprint(data_source_bp, url_prefix="/api/datasource") app.register_blueprint(data_flow_bp, url_prefix="/api/dataflow") app.register_blueprint(business_domain_bp, url_prefix="/api/bd") app.register_blueprint(data_factory_bp, url_prefix="/api/datafactory") app.register_blueprint(data_service_bp, url_prefix="/api/dataservice") # Configure global response headers configure_response_headers(app) # Configure logging configure_logging(app) log_llm_env_status(app) log_service_env_status(app) # 添加全局异常处理器 configure_error_handlers(app) # 输出启动信息(生产环境由 Gunicorn 按 LISTEN_PORT 监听,此处 PORT 与配置一致) port = app.config["PORT"] app.logger.info( f"Starting server in {current_env} mode on port {port} " f"(LISTEN_PORT={os.environ.get('LISTEN_PORT', port)})" ) return app def configure_response_headers(app): """Configure global response headers for JSON content""" @app.after_request def after_request(response): from flask import request # 检查是否是API路径 if request.path.startswith("/api/"): # 排除文件下载和特殊响应类型 excluded_types = [ "application/octet-stream", "application/pdf", "image/", "text/csv", "application/vnd.ms-excel", "application/vnd.openxmlformats-officedocument", ] if response.content_type and any( ct in response.content_type for ct in excluded_types ): # 保持原有的文件类型不变 pass elif response.content_type and "application/json" in response.content_type: # 确保JSON响应设置正确的Content-Type和charset ct = "application/json; charset=utf-8" response.headers["Content-Type"] = ct elif ( not response.content_type or response.content_type == "text/html; charset=utf-8" or response.content_type == "text/plain" ): # 对于API路由,默认设置为JSON ct = "application/json; charset=utf-8" response.headers["Content-Type"] = ct # 确保CORS头部不被覆盖 if "Access-Control-Allow-Origin" not in response.headers: # 动态设置Origin,支持任意前端地址 origin = request.headers.get("Origin") if origin: # 允许任意Origin(最灵活的配置) response.headers["Access-Control-Allow-Origin"] = origin else: # 如果没有Origin头部,设置为通配符 response.headers["Access-Control-Allow-Origin"] = "*" # 专门处理预检请求(OPTIONS方法) if request.method == "OPTIONS": origin = request.headers.get("Origin", "*") response.headers["Access-Control-Allow-Origin"] = origin methods = "GET, POST, PUT, DELETE, OPTIONS" response.headers["Access-Control-Allow-Methods"] = methods headers = ( "Content-Type, Authorization, X-Requested-With, " "Accept, Origin, Cache-Control, X-File-Name" ) response.headers["Access-Control-Allow-Headers"] = headers response.headers["Access-Control-Max-Age"] = "86400" return response # 根据配置设置凭据支持 from app.config.cors import ALLOW_ALL_ORIGINS if "Access-Control-Allow-Credentials" not in response.headers: if ALLOW_ALL_ORIGINS: # 通配符时不支持凭据 response.headers["Access-Control-Allow-Credentials"] = "false" else: response.headers["Access-Control-Allow-Credentials"] = "true" if "Access-Control-Allow-Methods" not in response.headers: methods = "GET, POST, PUT, DELETE, OPTIONS" response.headers["Access-Control-Allow-Methods"] = methods if "Access-Control-Allow-Headers" not in response.headers: headers = ( "Content-Type, Authorization, X-Requested-With, Accept, Origin" ) response.headers["Access-Control-Allow-Headers"] = headers # 添加安全头部 if "X-Content-Type-Options" not in response.headers: response.headers["X-Content-Type-Options"] = "nosniff" if "X-Frame-Options" not in response.headers: response.headers["X-Frame-Options"] = "DENY" if "X-XSS-Protection" not in response.headers: response.headers["X-XSS-Protection"] = "1; mode=block" if request.path.startswith("/api/") and request.path != "/api/system/health": app.logger.info( "%s %s -> %s", request.method, request.path, response.status_code, ) return response def configure_logging(app): """Configure logging for the application""" if not app.config.get("LOG_ENABLED", True): return None log_file = os.path.abspath( app.config.get("LOG_FILE", f"flask_{app.config['FLASK_ENV']}.log") ) log_dir = os.path.dirname(log_file) if log_dir: os.makedirs(log_dir, exist_ok=True) log_level_name = app.config.get("LOG_LEVEL", "INFO") log_level = getattr(logging, log_level_name) log_format = app.config.get( "LOG_FORMAT", "%(asctime)s - %(levelname)s - %(filename)s - " "%(funcName)s - %(lineno)s - %(message)s", ) log_encoding = app.config.get("LOG_ENCODING", "UTF-8") log_to_console = app.config.get("LOG_TO_CONSOLE", True) logging_format = logging.Formatter(log_format) root_logger = logging.getLogger() root_logger.setLevel(log_level) root_logger.handlers.clear() file_handler = logging.FileHandler(log_file, encoding=log_encoding) file_handler.setLevel(log_level) file_handler.setFormatter(logging_format) root_logger.addHandler(file_handler) if log_to_console: console = logging.StreamHandler() console.setLevel(log_level) console.setFormatter(logging_format) root_logger.addHandler(console) # Flask 默认 logger 关闭 propagate,清空 handler 后需要显式开启 app.logger.handlers.clear() app.logger.propagate = True app.logger.setLevel(log_level) for logger_name in ("app", "flask.app"): named_logger = logging.getLogger(logger_name) named_logger.handlers.clear() named_logger.propagate = True named_logger.setLevel(log_level) app.logger.info(f"日志配置完成: 级别={log_level_name}, 文件={log_file}") return logging.getLogger("app") def configure_error_handlers(app): """Configure global error handlers for the application""" @app.errorhandler(Exception) def handle_exception(e): """全局异常处理器,捕获所有未处理的异常""" # 记录详细的错误信息 app.logger.error(f"未处理的异常: {str(e)}", exc_info=True) # 返回标准化的错误响应 error_response = { "success": False, "message": f"服务器内部错误: {str(e)}", "data": None, } return jsonify(error_response), 500 @app.errorhandler(404) def handle_not_found(e): """处理404错误""" app.logger.warning(f"404错误: {str(e)}") return jsonify( {"success": False, "message": "请求的资源不存在", "data": None} ), 404 @app.errorhandler(500) def handle_internal_error(e): """处理500错误""" app.logger.error(f"500错误: {str(e)}", exc_info=True) return jsonify( {"success": False, "message": "服务器内部错误", "data": None} ), 500