diff --git a/src/api/routes.py b/src/api/routes.py index 13a4ae3..05d6e6a 100644 --- a/src/api/routes.py +++ b/src/api/routes.py @@ -107,102 +107,106 @@ async def get_status(task_id: str, db_session: AsyncSession = Depends(get_db_ses 如果内存中不存在,则从 PostgreSQL + RustFS 组装一个持久化的任务视图, 结构与内存任务保持尽量一致,便于前端集中展示总结性信息。 """ - # 1. 内存任务(进行中的任务) - if task_id in tasks: - task = tasks[task_id] - logger.info(f"返回内存任务状态:{task_id} - {task['status']}") - logger.info(f"内存任务 analysis_result: {task.get('analysis_result', 'None')}") - logger.info(f"内存任务 html_file: {task.get('html_file', 'None')}") - return task + try: + # 1. 内存任务(进行中的任务) + if task_id in tasks: + task = tasks[task_id] + logger.info(f"返回内存任务状态:{task_id} - {task['status']}") + logger.info(f"内存任务 analysis_result: {task.get('analysis_result', 'None')}") + logger.info(f"内存任务 html_file: {task.get('html_file', 'None')}") + return task - # 2. 持久化任务(已完成/失败,或服务重启后的任务) - from sqlalchemy import select - from models.database import ProcessingTask, STPFile, GeometryData, MeshData, MoldCavityData - storage_service = StorageIntegrationService() + # 2. 持久化任务(已完成/失败,或服务重启后的任务) + from sqlalchemy import select + from models.database import ProcessingTask, STPFile, GeometryData, MeshData, MoldCavityData + storage_service = StorageIntegrationService() - # 查询任务和文件元数据 - result = await db_session.execute( - select(ProcessingTask, STPFile) - .join(STPFile, ProcessingTask.stp_file_id == STPFile.id) - .where(ProcessingTask.task_id == task_id) - ) - row = result.first() - if not row: - raise HTTPException(404, "任务不存在") + # 查询任务和文件元数据 + result = await db_session.execute( + select(ProcessingTask, STPFile) + .join(STPFile, ProcessingTask.stp_file_id == STPFile.id) + .where(ProcessingTask.task_id == task_id) + ) + row = result.first() + if not row: + raise HTTPException(404, "任务不存在") - processing_task, stp_file = row + processing_task, stp_file = row - # 从 RustFS 取几何 / 型腔 / 网格详细 JSON(小量数据,便于前端展示汇总) - file_with_data = await storage_service.get_stp_file_with_data( - db_session, stp_file_id=stp_file.id - ) + # 从 RustFS 取几何 / 型腔 / 网格详细 JSON(小量数据,便于前端展示汇总) + try: + file_with_data = await storage_service.get_stp_file_with_data( + db_session, stp_file_id=stp_file.id + ) + except Exception as e: + logger.error(f"获取文件数据失败: {e}") + file_with_data = {} - geometry_json: Optional[Dict[str, Any]] = None - if file_with_data.get("geometry_data"): - # save_geometry_data 保存时可能有两种格式: - # 1. 直接保存 geometry_data 字典 - # 2. 保存 {"geometry_data": {...}} 格式 - # get_stp_file_with_data 返回的是从RustFS下载的原始JSON - geo_raw = file_with_data["geometry_data"] - if isinstance(geo_raw, dict): - # 如果是包装格式,提取内部数据 - if "geometry_data" in geo_raw: - geometry_json = geo_raw["geometry_data"] - else: - # 直接就是几何数据字典 - geometry_json = geo_raw + geometry_json: Optional[Dict[str, Any]] = None + if file_with_data.get("geometry_data"): + geo_raw = file_with_data["geometry_data"] + if isinstance(geo_raw, dict): + if "geometry_data" in geo_raw: + geometry_json = geo_raw["geometry_data"] + else: + geometry_json = geo_raw - cavity_json: Optional[Dict[str, Any]] = file_with_data.get("mold_cavity_data") - features_json: List[Dict[str, Any]] = file_with_data.get("features", []) - recommendations_json: List[Dict[str, Any]] = file_with_data.get("recommendations", []) + cavity_json: Optional[Dict[str, Any]] = file_with_data.get("mold_cavity_data") + features_json: List[Dict[str, Any]] = file_with_data.get("features", []) + recommendations_json: List[Dict[str, Any]] = file_with_data.get("recommendations", []) - # 组装网格摘要:从 MeshData 表中读出摘要字段,避免把完整网格 JSON 丢给前端 - mesh_summary = None - mesh_record = await db_session.execute( - select(MeshData).where(MeshData.stp_file_id == stp_file.id) - ) - mesh_record = mesh_record.scalar_one_or_none() - if mesh_record: - mesh_summary = { - "vertex_count": mesh_record.vertex_count, - "face_count": mesh_record.face_count, - "point_count": mesh_record.point_count, - "quality": mesh_record.quality, + # 组装网格摘要 + mesh_summary = None + mesh_record = await db_session.execute( + select(MeshData).where(MeshData.stp_file_id == stp_file.id) + ) + mesh_record = mesh_record.scalar_one_or_none() + if mesh_record: + mesh_summary = { + "vertex_count": mesh_record.vertex_count, + "face_count": mesh_record.face_count, + "point_count": mesh_record.point_count, + "quality": mesh_record.quality, + } + + # 构造与内存任务兼容的任务视图 + task_view = { + "task_id": processing_task.task_id, + "status": processing_task.status, + "filename": stp_file.original_filename if stp_file else "", + "file_path": stp_file.file_path or "", + "file_size": stp_file.file_size if stp_file else 0, + "upload_time": processing_task.created_time.isoformat() + if processing_task.created_time + else "", + "completed_at": processing_task.completed_time.isoformat() + if processing_task.completed_time + else "", + "geometry_data": geometry_json, + "key_info": cavity_json, + "cavity_data": cavity_json, + "mesh_summary": mesh_summary, + "analysis_result": { + "geometry_data": geometry_json, + "detected_features": features_json, + "design_recommendations": recommendations_json, + "quality_metrics": { + "volume_utilization": file_with_data.get("analysis_metrics", {}).get("volume_utilization", 0), + "topology_complexity": file_with_data.get("analysis_metrics", {}).get("topology_complexity", 0), + "wall_uniformity": file_with_data.get("analysis_metrics", {}).get("wall_uniformity", 0) + }, + "analysis_summary": file_with_data.get("analysis_metrics", {}).get("analysis_summary", "分析完成") + } if geometry_json or features_json or recommendations_json else None, + "error": processing_task.error_message or stp_file.error_message or None, } - # 构造与内存任务兼容的任务视图 - task_view = { - "task_id": processing_task.task_id, - "status": processing_task.status, - "filename": stp_file.original_filename if stp_file else "", - "file_path": stp_file.file_path or "", - "file_size": stp_file.file_size if stp_file else 0, - "upload_time": processing_task.created_time.isoformat() - if processing_task.created_time - else "", - "completed_at": processing_task.completed_time.isoformat() - if processing_task.completed_time - else "", - "geometry_data": geometry_json, - "key_info": cavity_json, - "cavity_data": cavity_json, - "mesh_summary": mesh_summary, - "analysis_result": { - "geometry_data": geometry_json, - "detected_features": features_json, - "design_recommendations": recommendations_json, - "quality_metrics": { - "volume_utilization": file_with_data.get("analysis_metrics", {}).get("volume_utilization", 0), - "topology_complexity": file_with_data.get("analysis_metrics", {}).get("topology_complexity", 0), - "wall_uniformity": file_with_data.get("analysis_metrics", {}).get("wall_uniformity", 0) - }, - "analysis_summary": file_with_data.get("analysis_metrics", {}).get("analysis_summary", "分析完成") - } if geometry_json or features_json or recommendations_json else None, - "error": processing_task.error_message or stp_file.error_message or None, - } - - logger.info(f"返回持久化任务状态: {task_id} - {processing_task.status}") - return task_view + logger.info(f"返回持久化任务状态: {task_id} - {processing_task.status}") + return task_view + except HTTPException: + raise + except Exception as e: + logger.error(f"获取任务状态失败: {e}") + raise HTTPException(500, f"获取任务状态失败: {str(e)}") @router.get("/debug/tasks") diff --git a/src/services/storage_integration_rustfs.py b/src/services/storage_integration_rustfs.py index c5b6eb7..cf70a8f 100644 --- a/src/services/storage_integration_rustfs.py +++ b/src/services/storage_integration_rustfs.py @@ -425,11 +425,25 @@ class StorageIntegrationService: async def get_stp_file_with_data(self, session: AsyncSession, stp_file_id: int) -> Dict[str, Any]: """获取STP文件及其所有关联数据""" - - # 1. 获取STP文件记录 - stp_file = await session.get(STPFile, stp_file_id) - if not stp_file: - raise ValueError(f"STP文件不存在: {stp_file_id}") + from sqlalchemy.orm import joinedload + + try: + # 1. 获取STP文件记录(使用 joinedload 预加载关联数据) + result = await session.execute( + select(STPFile).options( + joinedload(STPFile.geometry_data), + joinedload(STPFile.mesh_data), + joinedload(STPFile.mold_cavity_data), + joinedload(STPFile.html_file), + joinedload(STPFile.analysis_metrics) + ).where(STPFile.id == stp_file_id) + ) + stp_file = result.scalar_one_or_none() + if not stp_file: + raise ValueError(f"STP文件不存在: {stp_file_id}") + except Exception as e: + logger.error(f"获取STP文件记录失败: {e}") + raise result = { 'metadata': { diff --git a/src/storage/rustfs_storage.py b/src/storage/rustfs_storage.py index 12ce446..a9cd136 100644 --- a/src/storage/rustfs_storage.py +++ b/src/storage/rustfs_storage.py @@ -52,7 +52,8 @@ class RustFSManager: ) # 测试连接 - self.client.list_buckets() + import asyncio + await asyncio.to_thread(self.client.list_buckets) self.is_connected = True logger.info(f"RustFS 连接成功: {endpoint}") @@ -77,9 +78,17 @@ class RustFSManager: async def _ensure_buckets(self): """确保项目存储桶存在""" - try: + import asyncio + + def _check_and_create(): if not self.client.bucket_exists(self.bucket_name): self.client.make_bucket(self.bucket_name) + return True + return False + + try: + created = await asyncio.to_thread(_check_and_create) + if created: logger.info(f"创建项目存储桶: {self.bucket_name}") else: logger.debug(f"项目存储桶已存在: {self.bucket_name}") @@ -111,6 +120,8 @@ class RustFSManager: original_filename: str, metadata: Optional[Dict] = None) -> Dict[str, Any]: """上传文件到 RustFS""" + import asyncio + if not self.is_connected: raise RuntimeError("RustFS 未连接") @@ -124,8 +135,8 @@ class RustFSManager: object_key = self._generate_object_key(original_filename, file_type) # 上传文件 - try: - result = self.client.fput_object( + def _upload(): + return self.client.fput_object( self.bucket_name, object_key, str(file_path), @@ -133,6 +144,9 @@ class RustFSManager: metadata=metadata or {} ) + try: + result = await asyncio.to_thread(_upload) + logger.info(f"文件上传成功 RustFS: {self.bucket_name}/{object_key}") # 获取文件大小 @@ -154,6 +168,8 @@ class RustFSManager: json_data: Dict[str, Any], file_hash: str) -> Dict[str, Any]: """上传JSON数据到 RustFS""" + import asyncio + if not self.is_connected: raise RuntimeError("RustFS 未连接") @@ -168,8 +184,8 @@ class RustFSManager: import json json_bytes = json.dumps(json_data, ensure_ascii=False).encode('utf-8') - try: - result = self.client.put_object( + def _upload(): + return self.client.put_object( self.bucket_name, object_key, BytesIO(json_bytes), @@ -177,6 +193,9 @@ class RustFSManager: content_type='application/json' ) + try: + result = await asyncio.to_thread(_upload) + logger.info(f"JSON数据上传成功 RustFS: {self.bucket_name}/{object_key}") return { @@ -192,18 +211,23 @@ class RustFSManager: async def download_file(self, file_type: str, object_key: str) -> bytes: """从 RustFS 下载文件""" + import asyncio + if not self.is_connected: raise RuntimeError("RustFS 未连接") if file_type not in self.file_types: raise ValueError(f"未知的文件类型: {file_type}") - try: + def _download(): response = self.client.get_object(self.bucket_name, object_key) data = response.read() response.close() response.release_conn() + return data + try: + data = await asyncio.to_thread(_download) logger.debug(f"文件下载成功: {self.bucket_name}/{object_key}") return data @@ -213,14 +237,19 @@ class RustFSManager: async def get_file_info(self, file_type: str, object_key: str) -> Dict[str, Any]: """获取文件信息""" + import asyncio + if not self.is_connected: raise RuntimeError("RustFS 未连接") if file_type not in self.file_types: raise ValueError(f"未知的文件类型: {file_type}") + def _get_stat(): + return self.client.stat_object(self.bucket_name, object_key) + try: - stat = self.client.stat_object(self.bucket_name, object_key) + stat = await asyncio.to_thread(_get_stat) return { 'size': stat.size, 'etag': stat.etag, @@ -234,14 +263,19 @@ class RustFSManager: async def delete_file(self, file_type: str, object_key: str): """删除 RustFS 中的文件""" + import asyncio + if not self.is_connected: raise RuntimeError("RustFS 未连接") if file_type not in self.file_types: raise ValueError(f"未知的文件类型: {file_type}") - try: + def _delete(): self.client.remove_object(self.bucket_name, object_key) + + try: + await asyncio.to_thread(_delete) logger.info(f"文件删除成功: {self.bucket_name}/{object_key}") except S3Error as e: @@ -250,6 +284,8 @@ class RustFSManager: async def list_files(self, file_type: str, prefix: str = '') -> list: """列出存储桶中的文件""" + import asyncio + if not self.is_connected: raise RuntimeError("RustFS 未连接") @@ -262,8 +298,11 @@ class RustFSManager: if prefix: full_prefix += prefix + def _list(): + return list(self.client.list_objects(self.bucket_name, prefix=full_prefix, recursive=True)) + try: - objects = self.client.list_objects(self.bucket_name, prefix=full_prefix, recursive=True) + objects = await asyncio.to_thread(_list) return [ { 'object_key': obj.object_name, @@ -283,18 +322,23 @@ class RustFSManager: expires: int = 3600, method: str = 'GET') -> str: """生成预签名URL(临时访问链接)""" + import asyncio + if not self.is_connected: raise RuntimeError("RustFS 未连接") if file_type not in self.file_types: raise ValueError(f"未知的文件类型: {file_type}") - try: - url = self.client.presigned_get_object( + def _generate_url(): + return self.client.presigned_get_object( self.bucket_name, object_key, expires=timedelta(seconds=expires) ) + + try: + url = await asyncio.to_thread(_generate_url) return url except S3Error as e: @@ -311,14 +355,16 @@ class RustFSManager: async def get_storage_stats(self) -> Dict[str, Any]: """获取存储统计信息""" - try: + import asyncio + + def _get_stats(): buckets = self.client.list_buckets() total_objects = 0 total_size = 0 namespace_stats = {} for bucket in buckets: - objects = self.client.list_objects(bucket.name, recursive=True) + objects = list(self.client.list_objects(bucket.name, recursive=True)) bucket_count = 0 bucket_size = 0 @@ -340,6 +386,9 @@ class RustFSManager: 'namespace_stats': namespace_stats } + try: + return await asyncio.to_thread(_get_stats) + except S3Error as e: logger.error(f"RustFS 获取统计信息失败: {e}") raise