"""方案计划文档管理系统(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("/") 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, )