__init__.py 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260
  1. import logging
  2. import os
  3. from flask import Flask, jsonify
  4. from flask_cors import CORS
  5. from flask_sqlalchemy import SQLAlchemy
  6. from app.config.config import (
  7. apply_runtime_env_config,
  8. config,
  9. current_env,
  10. log_llm_env_status,
  11. log_service_env_status,
  12. )
  13. from app.config.cors import CORS_OPTIONS
  14. db = SQLAlchemy()
  15. def create_app():
  16. """Create and configure the Flask application"""
  17. app = Flask(__name__)
  18. # 加载配置
  19. app.config.from_object(config[current_env])
  20. apply_runtime_env_config(app)
  21. # 初始化扩展
  22. # 配置CORS以解决跨域问题
  23. CORS(app, **CORS_OPTIONS)
  24. db.init_app(app)
  25. # 注册蓝图
  26. from app.api.business_domain import bp as business_domain_bp
  27. from app.api.data_factory import bp as data_factory_bp
  28. from app.api.data_flow import bp as data_flow_bp
  29. from app.api.data_interface import bp as data_interface_bp
  30. from app.api.data_service import bp as data_service_bp
  31. from app.api.data_source import bp as data_source_bp
  32. from app.api.graph import bp as graph_bp
  33. from app.api.meta_data import bp as meta_bp
  34. from app.api.system import bp as system_bp
  35. app.register_blueprint(meta_bp, url_prefix="/api/meta")
  36. app.register_blueprint(data_interface_bp, url_prefix="/api/interface")
  37. app.register_blueprint(graph_bp, url_prefix="/api/graph")
  38. app.register_blueprint(system_bp, url_prefix="/api/system")
  39. app.register_blueprint(data_source_bp, url_prefix="/api/datasource")
  40. app.register_blueprint(data_flow_bp, url_prefix="/api/dataflow")
  41. app.register_blueprint(business_domain_bp, url_prefix="/api/bd")
  42. app.register_blueprint(data_factory_bp, url_prefix="/api/datafactory")
  43. app.register_blueprint(data_service_bp, url_prefix="/api/dataservice")
  44. # Configure global response headers
  45. configure_response_headers(app)
  46. # Configure logging
  47. configure_logging(app)
  48. log_llm_env_status(app)
  49. log_service_env_status(app)
  50. # 添加全局异常处理器
  51. configure_error_handlers(app)
  52. # 输出启动信息(生产环境由 Gunicorn 按 LISTEN_PORT 监听,此处 PORT 与配置一致)
  53. port = app.config["PORT"]
  54. app.logger.info(
  55. f"Starting server in {current_env} mode on port {port} "
  56. f"(LISTEN_PORT={os.environ.get('LISTEN_PORT', port)})"
  57. )
  58. return app
  59. def configure_response_headers(app):
  60. """Configure global response headers for JSON content"""
  61. @app.after_request
  62. def after_request(response):
  63. from flask import request
  64. # 检查是否是API路径
  65. if request.path.startswith("/api/"):
  66. # 排除文件下载和特殊响应类型
  67. excluded_types = [
  68. "application/octet-stream",
  69. "application/pdf",
  70. "image/",
  71. "text/csv",
  72. "application/vnd.ms-excel",
  73. "application/vnd.openxmlformats-officedocument",
  74. ]
  75. if response.content_type and any(
  76. ct in response.content_type for ct in excluded_types
  77. ):
  78. # 保持原有的文件类型不变
  79. pass
  80. elif response.content_type and "application/json" in response.content_type:
  81. # 确保JSON响应设置正确的Content-Type和charset
  82. ct = "application/json; charset=utf-8"
  83. response.headers["Content-Type"] = ct
  84. elif (
  85. not response.content_type
  86. or response.content_type == "text/html; charset=utf-8"
  87. or response.content_type == "text/plain"
  88. ):
  89. # 对于API路由,默认设置为JSON
  90. ct = "application/json; charset=utf-8"
  91. response.headers["Content-Type"] = ct
  92. # 确保CORS头部不被覆盖
  93. if "Access-Control-Allow-Origin" not in response.headers:
  94. # 动态设置Origin,支持任意前端地址
  95. origin = request.headers.get("Origin")
  96. if origin:
  97. # 允许任意Origin(最灵活的配置)
  98. response.headers["Access-Control-Allow-Origin"] = origin
  99. else:
  100. # 如果没有Origin头部,设置为通配符
  101. response.headers["Access-Control-Allow-Origin"] = "*"
  102. # 专门处理预检请求(OPTIONS方法)
  103. if request.method == "OPTIONS":
  104. origin = request.headers.get("Origin", "*")
  105. response.headers["Access-Control-Allow-Origin"] = origin
  106. methods = "GET, POST, PUT, DELETE, OPTIONS"
  107. response.headers["Access-Control-Allow-Methods"] = methods
  108. headers = (
  109. "Content-Type, Authorization, X-Requested-With, "
  110. "Accept, Origin, Cache-Control, X-File-Name"
  111. )
  112. response.headers["Access-Control-Allow-Headers"] = headers
  113. response.headers["Access-Control-Max-Age"] = "86400"
  114. return response
  115. # 根据配置设置凭据支持
  116. from app.config.cors import ALLOW_ALL_ORIGINS
  117. if "Access-Control-Allow-Credentials" not in response.headers:
  118. if ALLOW_ALL_ORIGINS:
  119. # 通配符时不支持凭据
  120. response.headers["Access-Control-Allow-Credentials"] = "false"
  121. else:
  122. response.headers["Access-Control-Allow-Credentials"] = "true"
  123. if "Access-Control-Allow-Methods" not in response.headers:
  124. methods = "GET, POST, PUT, DELETE, OPTIONS"
  125. response.headers["Access-Control-Allow-Methods"] = methods
  126. if "Access-Control-Allow-Headers" not in response.headers:
  127. headers = (
  128. "Content-Type, Authorization, X-Requested-With, Accept, Origin"
  129. )
  130. response.headers["Access-Control-Allow-Headers"] = headers
  131. # 添加安全头部
  132. if "X-Content-Type-Options" not in response.headers:
  133. response.headers["X-Content-Type-Options"] = "nosniff"
  134. if "X-Frame-Options" not in response.headers:
  135. response.headers["X-Frame-Options"] = "DENY"
  136. if "X-XSS-Protection" not in response.headers:
  137. response.headers["X-XSS-Protection"] = "1; mode=block"
  138. if request.path.startswith("/api/") and request.path != "/api/system/health":
  139. app.logger.info(
  140. "%s %s -> %s",
  141. request.method,
  142. request.path,
  143. response.status_code,
  144. )
  145. return response
  146. def configure_logging(app):
  147. """Configure logging for the application"""
  148. if not app.config.get("LOG_ENABLED", True):
  149. return None
  150. log_file = os.path.abspath(
  151. app.config.get("LOG_FILE", f"flask_{app.config['FLASK_ENV']}.log")
  152. )
  153. log_dir = os.path.dirname(log_file)
  154. if log_dir:
  155. os.makedirs(log_dir, exist_ok=True)
  156. log_level_name = app.config.get("LOG_LEVEL", "INFO")
  157. log_level = getattr(logging, log_level_name)
  158. log_format = app.config.get(
  159. "LOG_FORMAT",
  160. "%(asctime)s - %(levelname)s - %(filename)s - "
  161. "%(funcName)s - %(lineno)s - %(message)s",
  162. )
  163. log_encoding = app.config.get("LOG_ENCODING", "UTF-8")
  164. log_to_console = app.config.get("LOG_TO_CONSOLE", True)
  165. logging_format = logging.Formatter(log_format)
  166. root_logger = logging.getLogger()
  167. root_logger.setLevel(log_level)
  168. root_logger.handlers.clear()
  169. file_handler = logging.FileHandler(log_file, encoding=log_encoding)
  170. file_handler.setLevel(log_level)
  171. file_handler.setFormatter(logging_format)
  172. root_logger.addHandler(file_handler)
  173. if log_to_console:
  174. console = logging.StreamHandler()
  175. console.setLevel(log_level)
  176. console.setFormatter(logging_format)
  177. root_logger.addHandler(console)
  178. # Flask 默认 logger 关闭 propagate,清空 handler 后需要显式开启
  179. app.logger.handlers.clear()
  180. app.logger.propagate = True
  181. app.logger.setLevel(log_level)
  182. for logger_name in ("app", "flask.app"):
  183. named_logger = logging.getLogger(logger_name)
  184. named_logger.handlers.clear()
  185. named_logger.propagate = True
  186. named_logger.setLevel(log_level)
  187. app.logger.info(f"日志配置完成: 级别={log_level_name}, 文件={log_file}")
  188. return logging.getLogger("app")
  189. def configure_error_handlers(app):
  190. """Configure global error handlers for the application"""
  191. @app.errorhandler(Exception)
  192. def handle_exception(e):
  193. """全局异常处理器,捕获所有未处理的异常"""
  194. # 记录详细的错误信息
  195. app.logger.error(f"未处理的异常: {str(e)}", exc_info=True)
  196. # 返回标准化的错误响应
  197. error_response = {
  198. "success": False,
  199. "message": f"服务器内部错误: {str(e)}",
  200. "data": None,
  201. }
  202. return jsonify(error_response), 500
  203. @app.errorhandler(404)
  204. def handle_not_found(e):
  205. """处理404错误"""
  206. app.logger.warning(f"404错误: {str(e)}")
  207. return jsonify(
  208. {"success": False, "message": "请求的资源不存在", "data": None}
  209. ), 404
  210. @app.errorhandler(500)
  211. def handle_internal_error(e):
  212. """处理500错误"""
  213. app.logger.error(f"500错误: {str(e)}", exc_info=True)
  214. return jsonify(
  215. {"success": False, "message": "服务器内部错误", "data": None}
  216. ), 500