app.py 3.3 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798
  1. """方案计划文档管理系统(DMS)Web 后端.
  2. 提供 Flask 服务:
  3. - /api/v1 业务接口(认证、组织用户、角色权限、分类、文档全生命周期、审计等);
  4. - /api/health 健康检查;
  5. - 前端 dist 静态托管(一体化分发)。
  6. 安全说明:Info Extractor 遗留 AI 接口(/api/upload、/api/definition*、
  7. /api/extract、/api/annotation)已下线——原实现无鉴权且 /api/extract 的
  8. base_url 参数存在 SSRF 风险,不再由本服务暴露。
  9. """
  10. from __future__ import annotations
  11. import logging
  12. import os
  13. import sys
  14. from pathlib import Path
  15. from flask import Flask, jsonify, send_from_directory
  16. from werkzeug.exceptions import NotFound
  17. from dms import init_dms
  18. logging.basicConfig(
  19. level=logging.INFO,
  20. format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
  21. )
  22. logger = logging.getLogger(__name__)
  23. app = Flask(__name__)
  24. # CORS 白名单:默认不启用跨域(前端 dist 由本服务同源托管);
  25. # 开发或分域部署时通过 DMS_CORS_ORIGINS 指定允许的 Origin(逗号分隔)。
  26. _cors_origins = [
  27. item.strip()
  28. for item in os.environ.get("DMS_CORS_ORIGINS", "").split(",")
  29. if item.strip()
  30. ]
  31. if _cors_origins:
  32. from flask_cors import CORS
  33. CORS(app, origins=_cors_origins)
  34. logger.info("CORS allowed origins: %s", _cors_origins)
  35. # DMS业务使用独立的 /api/v1 Blueprint。初始化仅注册DMS基础设施。
  36. init_dms(app)
  37. # ===== 前端静态托管(一体化分发用)=====
  38. def _resolve_frontend_dist() -> Path | None:
  39. """定位前端 dist 目录:优先环境变量,其次 PyInstaller 资源,最后开发路径"""
  40. env_path = os.environ.get("DMS_FRONTEND_DIST", "").strip()
  41. if env_path:
  42. p = Path(env_path)
  43. return p if p.exists() else None
  44. if hasattr(sys, "_MEIPASS"):
  45. p = Path(sys._MEIPASS) / "frontend_dist"
  46. return p if p.exists() else None
  47. dev = Path(__file__).resolve().parent.parent / "frontend" / "dist"
  48. return dev if dev.exists() else None
  49. FRONTEND_DIST = _resolve_frontend_dist()
  50. if FRONTEND_DIST is not None:
  51. @app.route("/", defaults={"path": ""})
  52. @app.route("/<path:path>")
  53. def serve_spa(path: str):
  54. # /api/* 已由上方蓝图和显式路由优先匹配;漏网的 api 请求返回 404
  55. if path.startswith("api/"):
  56. # /api/v1 漏网请求抛出路由404,交给DMS统一错误处理器,
  57. # 保持错误结构与X-Request-Id响应头一致(一体化部署下同样生效)
  58. if path == "api/v1" or path.startswith("api/v1/"):
  59. raise NotFound()
  60. return jsonify({"error": "Not found"}), 404
  61. candidate = FRONTEND_DIST / path
  62. if path and candidate.is_file():
  63. return send_from_directory(FRONTEND_DIST, path)
  64. # 其余路径统一返回 index.html,交给前端路由
  65. return send_from_directory(FRONTEND_DIST, "index.html")
  66. logger.info("Frontend dist mounted at: %s", FRONTEND_DIST)
  67. else:
  68. logger.warning("Frontend dist not found; running in API-only mode")
  69. # ===== 前端静态托管 END =====
  70. @app.route("/api/health", methods=["GET"])
  71. def health():
  72. return jsonify({"status": "ok"})
  73. if __name__ == "__main__":
  74. app.run(
  75. host=os.environ.get("DMS_HOST", "0.0.0.0"),
  76. port=int(os.environ.get("DMS_PORT", "9345")),
  77. debug=False,
  78. )