| import json |
| import os |
| import zipfile |
|
|
| from django.conf import settings |
| from django.http import FileResponse, Http404 |
| from django.utils import timezone |
| from rest_framework import permissions, status |
| from rest_framework.parsers import FormParser, JSONParser, MultiPartParser |
| from rest_framework.response import Response |
| from rest_framework.views import APIView |
|
|
| from api.models import AnalysisTask, Project |
| from api.serializers import AnalysisTaskSerializer |
| from api.utils.r_executor import RExecutor |
|
|
|
|
| class _AnalysisViewMixin: |
| permission_classes = [permissions.IsAuthenticated] |
| DATA_IMAGE_EXTENSIONS = {".png", ".jpg", ".jpeg", ".gif", ".bmp", ".tif", ".tiff", ".svg", ".pdf"} |
|
|
| @staticmethod |
| def _get_project(request, project_id): |
| return Project.objects.get(id=project_id, user=request.user) |
|
|
| @staticmethod |
| def _get_project_media_dir(user_id, project_id): |
| return os.path.join(settings.MEDIA_ROOT, f"users/{user_id}/projects/{project_id}") |
|
|
| @staticmethod |
| def _get_results_dir(user_id, project_id): |
| return os.path.join(settings.MEDIA_ROOT, f"users/{user_id}/projects/{project_id}/imgs/analysis") |
|
|
| @staticmethod |
| def _get_images_dir(user_id, project_id): |
| return os.path.join(settings.MEDIA_ROOT, f"users/{user_id}/projects/{project_id}/imgs") |
|
|
| @staticmethod |
| def _get_temp_dir(user_id, project_id): |
| return os.path.join(settings.MEDIA_ROOT, f"users/{user_id}/projects/{project_id}/temp") |
|
|
| @staticmethod |
| def _parse_params(request): |
| params = request.data |
| if "params" in params and isinstance(params["params"], str): |
| params = json.loads(params["params"]) |
| return dict(params) |
|
|
| @staticmethod |
| def _save_optional_uploads(request, project_id, user_id, params): |
| if "bands_csv" not in request.FILES: |
| return params |
|
|
| file_path = os.path.join( |
| settings.MEDIA_ROOT, |
| f"users/{user_id}/projects/{project_id}/data/bands_ann.csv", |
| ) |
| os.makedirs(os.path.dirname(file_path), exist_ok=True) |
|
|
| with open(file_path, "wb+") as destination: |
| for chunk in request.FILES["bands_csv"].chunks(): |
| destination.write(chunk) |
|
|
| params["bands_csv_path"] = file_path |
| return params |
|
|
| @staticmethod |
| def _build_empty_result(): |
| return {"status": "success", "analyses": {}, "plots": {}, "artifacts": {}} |
|
|
| @classmethod |
| def _should_export_file(cls, project_media_dir, file_path, temp_dir): |
| rel_path = os.path.relpath(file_path, project_media_dir) |
| normalized_rel_path = rel_path.replace(os.sep, "/") |
|
|
| if os.path.normpath(file_path).startswith(os.path.normpath(temp_dir)): |
| return False |
|
|
| |
| |
| |
| if normalized_rel_path.startswith("data/"): |
| _, ext = os.path.splitext(file_path) |
| if ext.lower() in cls.DATA_IMAGE_EXTENSIONS: |
| return False |
|
|
| return True |
|
|
|
|
| class AnalysisStatusView(_AnalysisViewMixin, APIView): |
| def get(self, request, project_id): |
| try: |
| project = self._get_project(request, project_id) |
| except Project.DoesNotExist: |
| return Response( |
| {"error": {"code": "project_not_found", "message": "项目不存在"}}, |
| status=status.HTTP_404_NOT_FOUND, |
| ) |
|
|
| task = AnalysisTask.objects.filter(project=project).order_by("-created_at").first() |
| if not task: |
| return Response({"status": "not_started"}, status=status.HTTP_200_OK) |
|
|
| serializer = AnalysisTaskSerializer(task) |
| return Response(serializer.data, status=status.HTTP_200_OK) |
|
|
|
|
| class AnalysisTaskView(_AnalysisViewMixin, APIView): |
| parser_classes = [JSONParser, MultiPartParser, FormParser] |
|
|
| def post(self, request, project_id): |
| try: |
| project = self._get_project(request, project_id) |
| except Project.DoesNotExist: |
| return Response( |
| {"error": {"code": "project_not_found", "message": "项目不存在"}}, |
| status=status.HTTP_404_NOT_FOUND, |
| ) |
|
|
| if project.status not in ["data_loaded", "preprocessing_completed", "analysis", "completed"]: |
| return Response( |
| {"error": {"code": "invalid_status", "message": "项目当前状态不允许执行分析"}}, |
| status=status.HTTP_400_BAD_REQUEST, |
| ) |
|
|
| try: |
| params = self._parse_params(request) |
| except json.JSONDecodeError: |
| return Response( |
| {"error": {"code": "invalid_params", "message": "参数格式无效"}}, |
| status=status.HTTP_400_BAD_REQUEST, |
| ) |
|
|
| params = self._save_optional_uploads(request, project_id, request.user.id, params) |
| if not RExecutor.has_selected_work(params): |
| return Response( |
| {"error": {"code": "no_method_selected", "message": "请至少选择一种需要执行的算法或处理步骤"}}, |
| status=status.HTTP_400_BAD_REQUEST, |
| ) |
|
|
| task = AnalysisTask.objects.create( |
| project=project, |
| analysis_type=RExecutor.infer_analysis_type(params), |
| parameters=params, |
| status="processing", |
| ) |
| project.status = "analysis" |
| project.save() |
|
|
| user_id = str(request.user.id) |
| results_dir = self._get_results_dir(user_id, project_id) |
| os.makedirs(results_dir, exist_ok=True) |
|
|
| with open(os.path.join(results_dir, "analysis_params.json"), "w", encoding="utf-8") as file_obj: |
| json.dump(params, file_obj, ensure_ascii=False, indent=4) |
|
|
| try: |
| result = RExecutor.execute_analysis(project_id=str(project.id), params=params) |
| if result.get("status") == "error": |
| raise RuntimeError(result.get("message", "分析执行失败")) |
|
|
| with open(os.path.join(results_dir, "analysis_results.json"), "w", encoding="utf-8") as file_obj: |
| json.dump(result, file_obj, ensure_ascii=False, indent=4) |
|
|
| task.status = "completed" |
| task.result = result |
| task.completed_at = timezone.now() |
| task.save() |
|
|
| project.status = "completed" |
| project.save() |
|
|
| return Response( |
| {"status": "success", "message": "分析完成", "result": result}, |
| status=status.HTTP_200_OK, |
| ) |
| except Exception as exc: |
| task.status = "error" |
| task.result = {"error": str(exc)} |
| task.completed_at = timezone.now() |
| task.save() |
|
|
| project.status = "error" |
| project.save() |
|
|
| return Response( |
| {"status": "error", "error": {"message": str(exc)}}, |
| status=status.HTTP_500_INTERNAL_SERVER_ERROR, |
| ) |
|
|
|
|
| class AnalysisResultsView(_AnalysisViewMixin, APIView): |
| def get(self, request, project_id): |
| try: |
| project = self._get_project(request, project_id) |
| except Project.DoesNotExist: |
| return Response( |
| {"error": {"code": "project_not_found", "message": "项目不存在"}}, |
| status=status.HTTP_404_NOT_FOUND, |
| ) |
|
|
| user_id = str(request.user.id) |
| results_dir = self._get_results_dir(user_id, project_id) |
| results_file = os.path.join(results_dir, "analysis_results.json") |
| params_file = os.path.join(results_dir, "analysis_params.json") |
|
|
| if os.path.exists(results_file): |
| try: |
| with open(results_file, "r", encoding="utf-8") as file_obj: |
| saved_results = json.load(file_obj) |
|
|
| if os.path.exists(params_file) and "parameters" not in saved_results: |
| with open(params_file, "r", encoding="utf-8") as file_obj: |
| saved_results["parameters"] = json.load(file_obj) |
|
|
| return Response({"status": "success", "result": saved_results}, status=status.HTTP_200_OK) |
| except Exception: |
| pass |
|
|
| task = ( |
| AnalysisTask.objects.filter(project=project, status="completed") |
| .order_by("-completed_at") |
| .first() |
| ) |
| if task: |
| result = task.result or self._build_empty_result() |
| if isinstance(result, dict) and "parameters" not in result: |
| result["parameters"] = task.parameters |
| return Response({"status": "success", "result": result}, status=status.HTTP_200_OK) |
|
|
| if AnalysisTask.objects.filter(project=project, status="processing").exists(): |
| return Response( |
| {"status": "processing", "message": "分析任务正在进行中"}, |
| status=status.HTTP_200_OK, |
| ) |
|
|
| return Response( |
| { |
| "status": "success", |
| "message": "没有找到已完成的分析结果", |
| "result": self._build_empty_result(), |
| }, |
| status=status.HTTP_200_OK, |
| ) |
|
|
|
|
| class AnalysisExportView(_AnalysisViewMixin, APIView): |
| def get(self, request, project_id): |
| try: |
| project = self._get_project(request, project_id) |
| except Project.DoesNotExist: |
| return Response( |
| {"error": {"code": "project_not_found", "message": "项目不存在"}}, |
| status=status.HTTP_404_NOT_FOUND, |
| ) |
|
|
| user_id = str(request.user.id) |
| results_dir = self._get_results_dir(user_id, project_id) |
| images_dir = self._get_images_dir(user_id, project_id) |
| temp_dir = self._get_temp_dir(user_id, project_id) |
|
|
| has_results = os.path.exists(results_dir) and any( |
| os.path.isfile(os.path.join(results_dir, name)) for name in os.listdir(results_dir) |
| ) |
| has_images = os.path.exists(images_dir) and any( |
| os.path.isfile(os.path.join(root, name)) |
| for root, _, files in os.walk(images_dir) |
| for name in files |
| ) |
|
|
| if not has_results and not has_images: |
| return Response( |
| {"error": {"code": "no_results", "message": "没有找到可导出的分析结果"}}, |
| status=status.HTTP_404_NOT_FOUND, |
| ) |
|
|
| os.makedirs(temp_dir, exist_ok=True) |
| zip_filename = f"analysis_results_{project.name}_{timezone.now().strftime('%Y%m%d%H%M%S')}.zip" |
| zip_filepath = os.path.join(temp_dir, zip_filename) |
|
|
| with zipfile.ZipFile(zip_filepath, "w", zipfile.ZIP_DEFLATED) as zip_file: |
| project_media_dir = self._get_project_media_dir(user_id, project_id) |
| for root, _, files in os.walk(project_media_dir): |
| for file_name in files: |
| file_path = os.path.join(root, file_name) |
| if not self._should_export_file(project_media_dir, file_path, temp_dir): |
| continue |
| arcname = os.path.relpath(file_path, project_media_dir) |
| zip_file.write(file_path, arcname) |
|
|
| if os.path.exists(zip_filepath): |
| response = FileResponse(open(zip_filepath, "rb"), as_attachment=True, filename=zip_filename) |
| response["Content-Type"] = "application/zip" |
| response["X-Accel-Redirect"] = zip_filepath |
| return response |
|
|
| raise Http404("无法创建导出文件") |
|
|