# services/stp_materializer.py """按 task_id 把 STP 原件从持久化存储落盘为临时文件(方案 B)。 任务完成后 TopoDS_Shape 不驻留内存/Redis;需要几何的端点(倒扣检测、 按需重导出等)通过 STP 原件重建:PG(object_key) -> RustFS 下载 -> 临时文件, OCC 解析在常驻子进程内完成(occ_worker 的 detect_undercuts 等操作,见 occ_process_pool)。本模块只负责把原件落到调用方可传路径的临时文件。 """ import tempfile from pathlib import Path from typing import Optional from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from moldinsight.models import ProcessingTask, STPFile from shared.utils.logger import get_logger logger = get_logger(__name__) class STPMaterializer: """任务 STP 原件落盘器""" async def materialize_stp_for_task( self, db_session: AsyncSession, task_id: str ) -> Optional[Path]: """重建任务的产品几何原件为临时文件。 任务不存在或 STP 原件不可用时返回 None;返回的临时文件由调用方 finally 清理(unlink)。 """ 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: logger.warning(f"STP 原件落盘失败:任务不存在 {task_id}") return None _, stp_file = row if not stp_file.object_key: logger.warning(f"STP 原件落盘失败:任务缺少 object_key {task_id}") return None from moldinsight.storage.rustfs_storage import rustfs_manager try: data = await rustfs_manager.download_file( file_type="stp_files", object_key=stp_file.object_key ) except Exception as exc: logger.error(f"STP 原件落盘失败:下载失败 {task_id}: {exc}") return None original_name = Path(stp_file.original_filename or "model.stp").name or "model.stp" tmp = tempfile.NamedTemporaryFile( suffix=Path(original_name).suffix or ".stp", prefix=f"mold_{task_id}_", delete=False ) tmp.write(data) tmp.close() logger.debug(f"STP 原件已落盘: {tmp.name}") return Path(tmp.name) # 惰性单例 _stp_materializer: Optional[STPMaterializer] = None def get_stp_materializer() -> STPMaterializer: global _stp_materializer if _stp_materializer is None: _stp_materializer = STPMaterializer() return _stp_materializer