__init__.py 11 KB

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