import json from typing import Optional from fastapi import APIRouter, Request, UploadFile, status from fastapi.responses import FileResponse, JSONResponse from app.core.errors import bad_request, not_found from app.utils.r_core.outputs import empty_result from app.schemas.analysis import AnalysisResultsResponse, AnalysisRunResponse, AnalysisTaskResponse from app.services import analysis_service router = APIRouter(prefix="/projects/{project_id}/analysis", tags=["analysis"]) def _parse_json_field(raw_value: object) -> Optional[dict]: if not raw_value or not isinstance(raw_value, str): return None try: parsed = json.loads(raw_value) except json.JSONDecodeError: bad_request("invalid_params", "参数格式无效") return parsed if isinstance(parsed, dict) else None async def _parse_analysis_params(request: Request) -> tuple[dict, Optional[dict], Optional[UploadFile]]: content_type = request.headers.get("content-type", "") if content_type.startswith("multipart/form-data"): form = await request.form() params = _parse_json_field(form.get("params")) or {} workflow = _parse_json_field(form.get("workflow")) upload = form.get("bands_csv") if upload is not None and hasattr(upload, "filename") and hasattr(upload, "file"): return params, workflow, upload return params, workflow, None try: payload = await request.json() except json.JSONDecodeError: bad_request("invalid_params", "参数格式无效") if not isinstance(payload, dict): return {}, None, None workflow = payload.get("workflow") if isinstance(payload.get("workflow"), dict) else None return payload, workflow, None @router.get("/status/", response_model=AnalysisTaskResponse) def analysis_status(project_id: str) -> dict: try: task = analysis_service.latest_task(project_id) except KeyError: not_found("project_not_found", "项目不存在") if not task: return JSONResponse(status_code=status.HTTP_200_OK, content={"status": "not_started"}) return task @router.post("/", response_model=AnalysisRunResponse) async def run_analysis(project_id: str, request: Request): params, workflow, bands_csv = await _parse_analysis_params(request) try: result = analysis_service.run_analysis(project_id, params, bands_csv=bands_csv, workflow=workflow) except KeyError: not_found("project_not_found", "项目不存在") except RuntimeError as exc: return JSONResponse( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, content={"status": "error", "error": {"message": str(exc)}}, ) return {"status": "success", "message": "分析完成", "result": result} @router.get("/results/", response_model=AnalysisResultsResponse) def analysis_results(project_id: str) -> dict: try: task = analysis_service.latest_task(project_id) except KeyError: not_found("project_not_found", "项目不存在") if task and task["status"] == "processing": return {"status": "processing", "message": "分析任务正在进行中"} result = analysis_service.latest_completed_result(project_id) if result: return {"status": "success", "result": result} return { "status": "success", "message": "没有找到已完成的分析结果", "result": empty_result(), } @router.get("/export/") def export_results(project_id: str): try: zip_path = analysis_service.export_latest_results(project_id) except KeyError: not_found("project_not_found", "项目不存在") except FileNotFoundError: not_found("no_results", "没有找到可导出的分析结果") return FileResponse( path=zip_path, media_type="application/zip", filename=zip_path.name, )