from pathlib import Path from flask import Flask, jsonify, render_template, request, send_file from werkzeug.exceptions import RequestEntityTooLarge from aging_tool.service import ( AgingSummaryError, AnalysisTaskNotFound as AgingTaskNotFound, get_task_status as get_aging_task_status, start_analysis_task as start_aging_analysis_task, ) from keyword_tool.service import ( AnalysisTaskNotFound as KeywordTaskNotFound, KeywordSummaryError, get_task_status as get_keyword_task_status, start_analysis_task as start_keyword_analysis_task, ) from sguid_tool.processor import ProcessingError from sguid_tool.service import ( AnalysisTaskNotFound as SguidTaskNotFound, get_task_status as get_sguid_task_status, start_analysis_task as start_sguid_analysis_task, ) from sguid_tool.stats import get_usage_stats, record_usage BASE_DIR = Path(__file__).resolve().parent def create_app() -> Flask: app = Flask(__name__) app.config["MAX_CONTENT_LENGTH"] = 100 * 1024 * 1024 app.config["UPLOAD_FOLDER"] = BASE_DIR / "uploads" app.config["OUTPUT_FOLDER"] = BASE_DIR / "saved_outputs" app.config["DATA_FOLDER"] = BASE_DIR / "data" app.config["KEYWORD_UPLOAD_FOLDER"] = BASE_DIR / "uploads" / "keyword_summary" app.config["KEYWORD_OUTPUT_FOLDER"] = BASE_DIR / "saved_outputs" / "keyword_summary" app.config["AGING_UPLOAD_FOLDER"] = BASE_DIR / "uploads" / "aging_stock_summary" app.config["AGING_OUTPUT_FOLDER"] = BASE_DIR / "saved_outputs" / "aging_stock_summary" for key in [ "UPLOAD_FOLDER", "OUTPUT_FOLDER", "DATA_FOLDER", "KEYWORD_UPLOAD_FOLDER", "KEYWORD_OUTPUT_FOLDER", "AGING_UPLOAD_FOLDER", "AGING_OUTPUT_FOLDER", ]: app.config[key].mkdir(parents=True, exist_ok=True) @app.get("/") def index(): return render_template("index.html", active_nav="sgu") @app.get("/keyword-summary") def keyword_summary(): return render_template("keyword_summary.html", active_nav="keyword") @app.get("/placement-summary") def placement_summary(): return render_template("keyword_summary.html", active_nav="placement") @app.get("/aging-stock") def aging_stock(): return render_template("aging_stock.html", active_nav="aging") @app.get("/api/stats") def stats(): return jsonify(get_usage_stats(app.config["DATA_FOLDER"])) @app.post("/api/process") def process_file(): uploaded_file = request.files.get("file") if not uploaded_file or not uploaded_file.filename: return jsonify({"success": False, "error": "请选择一个 .xlsx 或 .csv 文件。"}), 400 record_usage(app.config["DATA_FOLDER"]) try: task_id = start_sguid_analysis_task( uploaded_file=uploaded_file, upload_dir=app.config["UPLOAD_FOLDER"], output_dir=app.config["OUTPUT_FOLDER"], ) except ProcessingError as exc: return jsonify({"success": False, "error": str(exc)}), 400 return jsonify({"success": True, "task_id": task_id, "message": "文件上传成功,正在生成 SGU 汇总结果..."}) @app.get("/api/process/status/") def process_file_status(task_id: str): try: return jsonify(get_sguid_task_status(task_id)) except SguidTaskNotFound as exc: return jsonify({"success": False, "error": str(exc)}), 404 @app.get("/api/process/download/") def process_file_download(filename: str): file_path = app.config["OUTPUT_FOLDER"] / filename if not file_path.exists(): return jsonify({"success": False, "error": "结果文件不存在。"}), 404 return send_file( file_path, as_attachment=True, download_name=filename, mimetype="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", ) @app.post("/keyword-summary/api/upload") @app.post("/placement-summary/api/upload") def keyword_summary_upload(): uploaded_file = request.files.get("file") if not uploaded_file or not uploaded_file.filename: return jsonify({"success": False, "error": "请选择一个 Excel 或 CSV 文件。"}), 400 record_usage(app.config["DATA_FOLDER"]) try: task_id = start_keyword_analysis_task( uploaded_file=uploaded_file, upload_dir=app.config["KEYWORD_UPLOAD_FOLDER"], output_dir=app.config["KEYWORD_OUTPUT_FOLDER"], ) except KeywordSummaryError as exc: return jsonify({"success": False, "error": str(exc)}), 400 return jsonify( { "success": True, "task_id": task_id, "message": "文件上传成功,正在生成搜索词与投放词汇总...", } ) @app.get("/keyword-summary/api/status/") @app.get("/placement-summary/api/status/") def keyword_summary_status(task_id: str): try: return jsonify(get_keyword_task_status(task_id)) except KeywordTaskNotFound as exc: return jsonify({"success": False, "error": str(exc)}), 404 @app.get("/keyword-summary/api/download/") @app.get("/placement-summary/api/download/") def keyword_summary_download(filename: str): file_path = app.config["KEYWORD_OUTPUT_FOLDER"] / filename if not file_path.exists(): return jsonify({"success": False, "error": "结果文件不存在。"}), 404 return send_file( file_path, as_attachment=True, download_name=filename, mimetype="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", ) @app.post("/aging-stock/api/upload") def aging_stock_upload(): uploaded_file = request.files.get("file") if not uploaded_file or not uploaded_file.filename: return jsonify({"success": False, "error": "请选择库龄 Excel 或 CSV 文件。"}), 400 record_usage(app.config["DATA_FOLDER"]) try: task_id = start_aging_analysis_task( uploaded_file=uploaded_file, upload_dir=app.config["AGING_UPLOAD_FOLDER"], output_dir=app.config["AGING_OUTPUT_FOLDER"], ) except AgingSummaryError as exc: return jsonify({"success": False, "error": str(exc)}), 400 return jsonify({"success": True, "task_id": task_id, "message": "文件上传成功,正在汇总超龄库存数据..."}) @app.get("/aging-stock/api/status/") def aging_stock_status(task_id: str): try: return jsonify(get_aging_task_status(task_id)) except AgingTaskNotFound as exc: return jsonify({"success": False, "error": str(exc)}), 404 @app.get("/aging-stock/api/download/") def aging_stock_download(filename: str): file_path = app.config["AGING_OUTPUT_FOLDER"] / filename if not file_path.exists(): return jsonify({"success": False, "error": "结果文件不存在。"}), 404 return send_file( file_path, as_attachment=True, download_name=filename, mimetype="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", ) @app.errorhandler(RequestEntityTooLarge) def handle_large_file(_error): return jsonify({"message": "上传文件过大,当前限制为 100MB,请压缩源文件后重试。"}), 413 return app app = create_app() if __name__ == "__main__": app.run(host="0.0.0.0", port=5001, debug=True)