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