x
This commit is contained in:
+197
-16
@@ -1,17 +1,28 @@
|
||||
from pathlib import Path
|
||||
import os
|
||||
from datetime import datetime
|
||||
from urllib.parse import quote
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from services.auth_service import get_current_active_user
|
||||
from services.redis_task_manager import redis_task_manager
|
||||
from services.processing_service import processing_service
|
||||
from services.storage_integration_rustfs import StorageIntegrationService
|
||||
from services.task_query_service import TaskQueryService
|
||||
from database.database import get_db_session
|
||||
from models.database import User
|
||||
from models.database import ProcessingTask, STPFile
|
||||
from core.cad_exporter import CADExporter
|
||||
from utils.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
router = APIRouter()
|
||||
cad_exporter = CADExporter()
|
||||
storage_service = StorageIntegrationService()
|
||||
|
||||
_api_routes_cache = {}
|
||||
|
||||
@@ -51,6 +62,125 @@ async def _get_task_data(task_id: str) -> dict:
|
||||
return None
|
||||
|
||||
|
||||
async def _ensure_task_access(
|
||||
db_session: AsyncSession,
|
||||
task_id: str,
|
||||
user_id: int,
|
||||
):
|
||||
row = await db_session.execute(
|
||||
select(ProcessingTask, STPFile)
|
||||
.join(STPFile, ProcessingTask.stp_file_id == STPFile.id)
|
||||
.where(ProcessingTask.task_id == task_id)
|
||||
)
|
||||
row = row.first()
|
||||
if not row:
|
||||
raise HTTPException(404, "任务不存在")
|
||||
|
||||
_, stp_file = row
|
||||
owner_id = getattr(stp_file, "user_id", None)
|
||||
if owner_id is not None and owner_id != user_id:
|
||||
raise HTTPException(403, "无权访问该任务的导出文件")
|
||||
|
||||
return row
|
||||
|
||||
|
||||
def _get_export_artifacts(task_data: dict) -> dict:
|
||||
if not isinstance(task_data, dict):
|
||||
return {}
|
||||
direct = task_data.get("export_artifacts")
|
||||
if isinstance(direct, dict):
|
||||
return direct
|
||||
parameters = task_data.get("parameters")
|
||||
if isinstance(parameters, dict) and isinstance(parameters.get("export_artifacts"), dict):
|
||||
return parameters.get("export_artifacts")
|
||||
return {}
|
||||
|
||||
|
||||
def _expand_components(components):
|
||||
requested = components or ["cavity", "core"]
|
||||
if "all" in requested:
|
||||
return ["cavity", "core", "parting_surface"]
|
||||
return list(dict.fromkeys(requested))
|
||||
|
||||
|
||||
def _augment_export_files(task_id: str, files):
|
||||
items = []
|
||||
for file in files or []:
|
||||
item = dict(file)
|
||||
relative_path = item.get("relative_path")
|
||||
if not relative_path and item.get("filepath"):
|
||||
relative_path = cad_exporter.get_relative_path(item["filepath"])
|
||||
if relative_path:
|
||||
relative_path = str(relative_path).replace("\\", "/").strip("/")
|
||||
item["relative_path"] = relative_path
|
||||
item["download_path"] = f"/api/export-download/{quote(relative_path, safe='/')}?task_id={task_id}"
|
||||
items.append(item)
|
||||
return items
|
||||
|
||||
|
||||
def _merge_export_artifacts(existing: dict, export_result: dict) -> dict:
|
||||
merged = dict(existing or {})
|
||||
schemes = dict(merged.get("schemes") or {})
|
||||
scheme_id = export_result.get("scheme_id") or "default"
|
||||
previous = dict(schemes.get(scheme_id) or {})
|
||||
|
||||
file_map = {}
|
||||
for file in previous.get("files", []):
|
||||
file_map[(file.get("component"), file.get("format"))] = file
|
||||
for file in export_result.get("files", []):
|
||||
file_map[(file.get("component"), file.get("format"))] = file
|
||||
|
||||
schemes[scheme_id] = {
|
||||
"base_filename": export_result.get("base_filename") or previous.get("base_filename"),
|
||||
"generated_at": datetime.now().isoformat(),
|
||||
"files": sorted(
|
||||
file_map.values(),
|
||||
key=lambda item: (item.get("component", ""), item.get("format", "")),
|
||||
),
|
||||
"errors": export_result.get("errors", []),
|
||||
"total_files": len(file_map),
|
||||
"total_errors": len(export_result.get("errors", [])),
|
||||
}
|
||||
|
||||
merged["version"] = 1
|
||||
merged["task_id"] = export_result.get("task_id") or merged.get("task_id")
|
||||
merged["generated_at"] = merged.get("generated_at") or datetime.now().isoformat()
|
||||
merged["schemes"] = schemes
|
||||
return merged
|
||||
|
||||
|
||||
def _select_persisted_files(task_id: str, task_data: dict, scheme_id: str, formats, components):
|
||||
artifacts = _get_export_artifacts(task_data)
|
||||
scheme_data = (artifacts.get("schemes") or {}).get(scheme_id)
|
||||
if not scheme_data:
|
||||
return None
|
||||
|
||||
component_list = _expand_components(components)
|
||||
format_list = list(dict.fromkeys(formats or ["step", "stl"]))
|
||||
expected = {(component, fmt) for component in component_list for fmt in format_list}
|
||||
|
||||
available = []
|
||||
available_keys = set()
|
||||
for file in scheme_data.get("files", []):
|
||||
component = file.get("component")
|
||||
fmt = file.get("format")
|
||||
if component not in component_list or fmt not in format_list:
|
||||
continue
|
||||
relative_path = str(file.get("relative_path") or "").replace("\\", "/").strip("/")
|
||||
if not relative_path:
|
||||
continue
|
||||
full_path = os.path.join(cad_exporter.output_dir, relative_path.replace("/", os.sep))
|
||||
if not os.path.exists(full_path):
|
||||
continue
|
||||
available.append(file)
|
||||
available_keys.add((component, fmt))
|
||||
|
||||
if expected and not expected.issubset(available_keys):
|
||||
return None
|
||||
|
||||
return _augment_export_files(task_id, available)
|
||||
|
||||
|
||||
@router.post("/optimize-layout")
|
||||
async def optimize_cavity_layout(
|
||||
request: Request,
|
||||
@@ -279,6 +409,7 @@ async def simulate_machining(
|
||||
async def export_mold_results(
|
||||
request: Request,
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
db_session: AsyncSession = Depends(get_db_session),
|
||||
):
|
||||
body = await request.json()
|
||||
task_id = body.get("task_id")
|
||||
@@ -289,13 +420,37 @@ async def export_mold_results(
|
||||
if not task_id:
|
||||
raise HTTPException(404, "缺少 task_id")
|
||||
|
||||
task_data = await _get_task_data(task_id)
|
||||
await _ensure_task_access(db_session, task_id, current_user.id)
|
||||
task_data = await TaskQueryService.get_task_view(db_session, task_id)
|
||||
if not task_data:
|
||||
raise HTTPException(404, "任务不存在")
|
||||
|
||||
resolved_scheme_id = scheme_id or task_data.get("best_scheme_id") or "default"
|
||||
persisted_files = _select_persisted_files(
|
||||
task_id=task_id,
|
||||
task_data=task_data,
|
||||
scheme_id=resolved_scheme_id,
|
||||
formats=formats,
|
||||
components=components,
|
||||
)
|
||||
if persisted_files:
|
||||
return {
|
||||
"status": "success",
|
||||
"data": {
|
||||
"base_filename": Path(task_data.get("filename", f"mold_{task_id}")).stem,
|
||||
"task_id": task_id,
|
||||
"scheme_id": resolved_scheme_id,
|
||||
"files": persisted_files,
|
||||
"errors": [],
|
||||
"total_files": len(persisted_files),
|
||||
"total_errors": 0,
|
||||
"source": "persisted",
|
||||
},
|
||||
}
|
||||
|
||||
cavity_shapes = processing_service.get_export_shapes(
|
||||
task_id,
|
||||
scheme_id or task_data.get("best_scheme_id"),
|
||||
resolved_scheme_id,
|
||||
)
|
||||
filename = task_data.get("filename", f"mold_{task_id}")
|
||||
|
||||
@@ -305,33 +460,62 @@ async def export_mold_results(
|
||||
"导出缓存已失效或任务尚未完成,请重新分析后再导出以保证方案一致性",
|
||||
)
|
||||
|
||||
exporter = _get_cached("cad_exporter")
|
||||
if not exporter:
|
||||
raise HTTPException(503, "服务不可用:核心模块未加载,请检查 PythonOCC 环境")
|
||||
|
||||
base_filename = Path(filename).stem
|
||||
result = exporter.export_mold_results(
|
||||
result = cad_exporter.export_mold_results(
|
||||
cavity_data=cavity_shapes,
|
||||
base_filename=base_filename,
|
||||
formats=formats,
|
||||
components=components,
|
||||
task_id=task_id,
|
||||
scheme_id=resolved_scheme_id,
|
||||
)
|
||||
result["files"] = _augment_export_files(task_id, result.get("files", []))
|
||||
result["source"] = "generated"
|
||||
|
||||
merged_artifacts = _merge_export_artifacts(_get_export_artifacts(task_data), result)
|
||||
await storage_service.update_task_parameters(
|
||||
db_session,
|
||||
task_id,
|
||||
{"export_artifacts": merged_artifacts},
|
||||
)
|
||||
await redis_task_manager.update_task(task_id, {"export_artifacts": merged_artifacts})
|
||||
|
||||
return {"status": "success", "data": result}
|
||||
|
||||
|
||||
@router.get("/export-download/{filepath:path}")
|
||||
async def download_export_file(
|
||||
filepath: str,
|
||||
task_id: str,
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
db_session: AsyncSession = Depends(get_db_session),
|
||||
):
|
||||
from fastapi.responses import FileResponse
|
||||
exporter = _get_cached("cad_exporter")
|
||||
if not exporter:
|
||||
raise HTTPException(503, "服务不可用")
|
||||
full_path = os.path.join(exporter.output_dir, filepath)
|
||||
|
||||
if not task_id:
|
||||
raise HTTPException(400, "缺少 task_id")
|
||||
|
||||
await _ensure_task_access(db_session, task_id, current_user.id)
|
||||
task_data = await TaskQueryService.get_task_view(db_session, task_id)
|
||||
if not task_data:
|
||||
raise HTTPException(404, "任务不存在")
|
||||
|
||||
allowed_paths = set()
|
||||
artifacts = _get_export_artifacts(task_data)
|
||||
for scheme in (artifacts.get("schemes") or {}).values():
|
||||
for file in scheme.get("files", []):
|
||||
relative_path = str(file.get("relative_path") or "").replace("\\", "/").strip("/")
|
||||
if relative_path:
|
||||
allowed_paths.add(relative_path)
|
||||
|
||||
normalized_path = str(filepath or "").replace("\\", "/").strip("/")
|
||||
if normalized_path not in allowed_paths:
|
||||
raise HTTPException(403, "该文件不在任务允许下载清单中")
|
||||
|
||||
full_path = os.path.join(cad_exporter.output_dir, normalized_path.replace("/", os.sep))
|
||||
if not os.path.exists(full_path):
|
||||
raise HTTPException(404, "文件不存在")
|
||||
if not os.path.abspath(full_path).startswith(os.path.abspath(exporter.output_dir)):
|
||||
if not os.path.abspath(full_path).startswith(os.path.abspath(cad_exporter.output_dir)):
|
||||
raise HTTPException(403, "禁止访问")
|
||||
media_types = {
|
||||
".step": "application/step", ".stp": "application/step",
|
||||
@@ -348,10 +532,7 @@ async def get_export_recommendations(
|
||||
target: str = "ug",
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
):
|
||||
exporter = _get_cached("cad_exporter")
|
||||
if not exporter:
|
||||
raise HTTPException(503, "服务不可用")
|
||||
result = exporter.get_export_recommendations(target)
|
||||
result = cad_exporter.get_export_recommendations(target)
|
||||
return {"status": "success", "data": result}
|
||||
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ FreeCAD 导入建议:
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
from typing import Dict, List, Any, Optional, Tuple
|
||||
from pathlib import Path
|
||||
from utils.logger import get_logger
|
||||
@@ -40,6 +41,35 @@ class CADExporter:
|
||||
self.output_dir = output_dir
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
|
||||
@staticmethod
|
||||
def _safe_segment(value: Optional[str], fallback: str) -> str:
|
||||
text = str(value or "").strip()
|
||||
if not text:
|
||||
text = fallback
|
||||
text = re.sub(r"[^A-Za-z0-9._-]+", "_", text)
|
||||
return text[:80] or fallback
|
||||
|
||||
def build_export_dir(
|
||||
self,
|
||||
base_filename: str,
|
||||
task_id: Optional[str] = None,
|
||||
scheme_id: Optional[str] = None,
|
||||
) -> str:
|
||||
if task_id:
|
||||
task_segment = self._safe_segment(task_id, "task")
|
||||
scheme_segment = self._safe_segment(scheme_id, "default")
|
||||
return os.path.join(self.output_dir, task_segment, scheme_segment)
|
||||
return os.path.join(self.output_dir, self._safe_segment(base_filename, "mold"))
|
||||
|
||||
def get_relative_path(self, filepath: str) -> str:
|
||||
full_path = Path(filepath).resolve()
|
||||
output_root = Path(self.output_dir).resolve()
|
||||
try:
|
||||
relative = full_path.relative_to(output_root)
|
||||
except ValueError:
|
||||
relative = Path(os.path.basename(filepath))
|
||||
return relative.as_posix()
|
||||
|
||||
def export_step(self, shape: Any, filepath: str,
|
||||
schema: str = "AP214") -> bool:
|
||||
"""
|
||||
@@ -207,7 +237,9 @@ class CADExporter:
|
||||
def export_mold_results(self, cavity_data: Dict,
|
||||
base_filename: str,
|
||||
formats: List[str] = None,
|
||||
components: List[str] = None) -> Dict[str, Any]:
|
||||
components: List[str] = None,
|
||||
task_id: Optional[str] = None,
|
||||
scheme_id: Optional[str] = None) -> Dict[str, Any]:
|
||||
"""
|
||||
批量导出模具设计结果
|
||||
|
||||
@@ -225,11 +257,17 @@ class CADExporter:
|
||||
if components is None:
|
||||
components = ["cavity", "core"]
|
||||
|
||||
export_dir = os.path.join(self.output_dir, base_filename)
|
||||
export_dir = self.build_export_dir(
|
||||
base_filename=base_filename,
|
||||
task_id=task_id,
|
||||
scheme_id=scheme_id,
|
||||
)
|
||||
os.makedirs(export_dir, exist_ok=True)
|
||||
|
||||
results = {
|
||||
"base_filename": base_filename,
|
||||
"task_id": task_id,
|
||||
"scheme_id": scheme_id,
|
||||
"export_dir": export_dir,
|
||||
"files": [],
|
||||
"errors": [],
|
||||
@@ -277,11 +315,13 @@ class CADExporter:
|
||||
|
||||
if success:
|
||||
file_size = os.path.getsize(filepath)
|
||||
relative_path = self.get_relative_path(filepath)
|
||||
results["files"].append({
|
||||
"component": comp_name,
|
||||
"component_label": label,
|
||||
"format": fmt,
|
||||
"filepath": filepath,
|
||||
"relative_path": relative_path,
|
||||
"filename": os.path.basename(filepath),
|
||||
"size_bytes": file_size,
|
||||
"size_readable": self._format_file_size(file_size),
|
||||
|
||||
@@ -9,12 +9,14 @@ class PartingSchemeScorer:
|
||||
scored = []
|
||||
for scheme in schemes:
|
||||
score_breakdown = self._score_scheme(scheme)
|
||||
undercut_priority_bonus = self._build_undercut_priority_bonus(scheme, score_breakdown)
|
||||
total_score = round(
|
||||
score_breakdown["manufacturability"] * 0.30
|
||||
+ score_breakdown["undercut_complexity"] * 0.25
|
||||
+ score_breakdown["parting_quality"] * 0.20
|
||||
score_breakdown["manufacturability"] * 0.25
|
||||
+ score_breakdown["undercut_complexity"] * 0.35
|
||||
+ score_breakdown["parting_quality"] * 0.15
|
||||
+ score_breakdown["machining_cost"] * 0.15
|
||||
+ score_breakdown["risk"] * 0.10,
|
||||
+ score_breakdown["risk"] * 0.10
|
||||
+ undercut_priority_bonus,
|
||||
2,
|
||||
)
|
||||
|
||||
@@ -22,6 +24,7 @@ class PartingSchemeScorer:
|
||||
fallback = self._assess_fallback(scored_scheme, score_breakdown)
|
||||
scored_scheme["score_breakdown"] = score_breakdown
|
||||
scored_scheme["score"] = total_score
|
||||
scored_scheme["undercut_priority_bonus"] = round(undercut_priority_bonus, 2)
|
||||
scored_scheme["is_fallback"] = fallback["is_fallback"]
|
||||
scored_scheme["fallback_reason"] = fallback["fallback_reason"]
|
||||
scored_scheme["dfm_violations"] = self._build_dfm_violations(scored_scheme)
|
||||
@@ -61,10 +64,34 @@ class PartingSchemeScorer:
|
||||
summary = side_actions.get("summary", {})
|
||||
slider_count = len(side_actions.get("slider_mechanisms", []))
|
||||
lifter_count = len(side_actions.get("lifter_mechanisms", []))
|
||||
total_mechanism_count = int(summary.get("total_mechanism_count", slider_count + lifter_count) or 0)
|
||||
total_undercut_area = float(
|
||||
(side_actions.get("undercut_analysis", {}) or {}).get("total_undercut_area")
|
||||
or 0.0
|
||||
)
|
||||
has_pneumatic = any(
|
||||
str(item.get("actuation", "")).lower() == "pneumatic"
|
||||
for item in side_actions.get("slider_mechanisms", [])
|
||||
)
|
||||
undercut_count = len(undercut_regions)
|
||||
undercut_complexity = max(35.0, 100.0 - undercut_count * 12.0 - slider_count * 8.0 - lifter_count * 6.0)
|
||||
if summary.get("complexity") == "high":
|
||||
undercut_complexity = max(30.0, undercut_complexity - 10.0)
|
||||
complexity = str(summary.get("complexity", "")).lower()
|
||||
|
||||
undercut_penalty = 0.0
|
||||
if total_mechanism_count > 0:
|
||||
undercut_penalty += 20.0
|
||||
undercut_penalty += undercut_count * 10.0
|
||||
undercut_penalty += slider_count * 6.0
|
||||
undercut_penalty += lifter_count * 5.0
|
||||
if has_pneumatic:
|
||||
undercut_penalty += 10.0
|
||||
if complexity == "moderate":
|
||||
undercut_penalty += 6.0
|
||||
elif complexity == "complex":
|
||||
undercut_penalty += 14.0
|
||||
elif complexity == "very_complex":
|
||||
undercut_penalty += 24.0
|
||||
undercut_penalty += min(total_undercut_area / 500.0, 12.0)
|
||||
undercut_complexity = max(20.0, 100.0 - undercut_penalty)
|
||||
|
||||
parting_line = scheme.get("parting", {}).get("line", [])
|
||||
parting_length = self._calculate_polyline_length(parting_line)
|
||||
@@ -106,6 +133,33 @@ class PartingSchemeScorer:
|
||||
"risk": round(risk, 2),
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _build_undercut_priority_bonus(
|
||||
scheme: Dict[str, Any],
|
||||
score_breakdown: Dict[str, float],
|
||||
) -> float:
|
||||
cavity_data = scheme.get("cavity_data", {})
|
||||
quality_checks = cavity_data.get("quality_checks", {})
|
||||
side_actions = quality_checks.get("side_actions") or cavity_data.get("side_actions", {})
|
||||
summary = side_actions.get("summary", {})
|
||||
slider_count = len(side_actions.get("slider_mechanisms", []))
|
||||
lifter_count = len(side_actions.get("lifter_mechanisms", []))
|
||||
total_mechanism_count = int(summary.get("total_mechanism_count", slider_count + lifter_count) or 0)
|
||||
has_pneumatic = any(
|
||||
str(item.get("actuation", "")).lower() == "pneumatic"
|
||||
for item in side_actions.get("slider_mechanisms", [])
|
||||
)
|
||||
|
||||
if total_mechanism_count == 0:
|
||||
return 18.0
|
||||
|
||||
penalty = 12.0 + total_mechanism_count * 4.0
|
||||
if has_pneumatic:
|
||||
penalty += 8.0
|
||||
if float(score_breakdown.get("manufacturability", 0.0)) < 80.0:
|
||||
penalty += 4.0
|
||||
return -penalty
|
||||
|
||||
def _assess_fallback(self, scheme: Dict[str, Any], score_breakdown: Dict[str, float]) -> Dict[str, Any]:
|
||||
cavity_data = scheme.get("cavity_data", {})
|
||||
mold_cavities = cavity_data.get("mold_cavities", {})
|
||||
|
||||
@@ -253,7 +253,7 @@ class SliderMechanismDesigner:
|
||||
"block_size": slide_block_size,
|
||||
"guide_type": guide_type,
|
||||
"locking_mechanism": self._select_locking(slide_angle),
|
||||
"actuation": "hydraulic" if slide_stroke > 50 else "mechanical",
|
||||
"actuation": "pneumatic" if slide_stroke > 50 else "mechanical",
|
||||
"components": self._generate_components(slide_block_size, guide_type),
|
||||
"manufacturing_notes": self._generate_slider_notes(slide_angle, slide_stroke),
|
||||
}
|
||||
@@ -341,7 +341,7 @@ class SliderMechanismDesigner:
|
||||
if angle > 25:
|
||||
notes.append("滑块角度较大,需确保锁紧可靠")
|
||||
if stroke > 50:
|
||||
notes.append("抽芯行程较长,建议使用液压抽芯")
|
||||
notes.append("抽芯行程较长,建议使用气动抽芯")
|
||||
if stroke > 80:
|
||||
notes.append("大行程抽芯,需校核导滑槽强度")
|
||||
notes.append("滑块需设置限位装置,防止脱出")
|
||||
@@ -485,7 +485,6 @@ class SideActionDesigner:
|
||||
"total_lifter_count": len(lifter_mechanisms),
|
||||
"total_mechanism_count": total_mechanisms,
|
||||
"complexity": undercut_result["complexity"],
|
||||
"has_hydraulic": any(s.get("actuation") == "hydraulic" for s in slider_mechanisms),
|
||||
}
|
||||
|
||||
recommendations = self._generate_overall_recommendations(summary, undercut_result)
|
||||
@@ -518,8 +517,8 @@ class SideActionDesigner:
|
||||
if summary["total_lifter_count"] > 0:
|
||||
recs.append(f"需要 {summary['total_lifter_count']} 个斜顶机构处理内侧倒扣")
|
||||
|
||||
if summary["has_hydraulic"]:
|
||||
recs.append("大行程抽芯需使用液压系统,需配置液压站")
|
||||
if summary["total_slider_count"] > 0:
|
||||
recs.append("如存在大行程滑块,建议优先评估气动抽芯回路并预留稳定供气")
|
||||
|
||||
if summary["complexity"] == "very_complex":
|
||||
recs.append("侧向分型机构复杂,建议评估是否可通过产品修改简化")
|
||||
|
||||
@@ -17,6 +17,7 @@ from core.aluminum_foam_mold import AluminumFoamMoldGenerator
|
||||
from core.mold_quality_inspector import AluminumFoamMoldQualityInspector
|
||||
from core.mesh_generator import MeshGenerator
|
||||
from core.multi_scheme_planner import MultiSchemeMoldPlanner
|
||||
from core.cad_exporter import CADExporter
|
||||
from services.storage_integration_rustfs import StorageIntegrationService
|
||||
from services.redis_task_manager import redis_task_manager
|
||||
from services.material_service import MaterialService
|
||||
@@ -43,6 +44,7 @@ class ProcessingService:
|
||||
self.html_generator = HTMLGenerator()
|
||||
self.storage_service = StorageIntegrationService()
|
||||
self.multi_scheme_planner = MultiSchemeMoldPlanner()
|
||||
self.cad_exporter = CADExporter()
|
||||
self._export_shapes_cache: Dict[str, Dict[str, Dict[str, Any]]] = {}
|
||||
|
||||
# ─── 对外入口 ───
|
||||
@@ -145,10 +147,16 @@ class ProcessingService:
|
||||
)
|
||||
stage_timings["generate_cavity"] = round(time.perf_counter() - stage_started, 3)
|
||||
export_shapes = {}
|
||||
export_artifacts = None
|
||||
if plan_result:
|
||||
export_shapes = plan_result.pop("_export_shapes", {}) or {}
|
||||
if export_shapes:
|
||||
self._cache_export_shapes(task_id, export_shapes)
|
||||
export_artifacts = self._persist_step_exports(
|
||||
task_id=task_id,
|
||||
original_filename=Path(file_path).name,
|
||||
export_shapes=export_shapes,
|
||||
)
|
||||
|
||||
# 4. 生成详细JSON数据 — 委托 CalculationService
|
||||
await self.storage_service.update_task_status(
|
||||
@@ -315,6 +323,7 @@ class ProcessingService:
|
||||
"material": requested_material,
|
||||
"verification": verification_result,
|
||||
"llm_report": llm_report,
|
||||
"export_artifacts": export_artifacts,
|
||||
**process_params,
|
||||
},
|
||||
)
|
||||
@@ -334,6 +343,7 @@ class ProcessingService:
|
||||
"html_file": best_scheme.get("html_file", f"/html/{Path(html_file_path).name}") if best_scheme else f"/html/{Path(html_file_path).name}",
|
||||
"verification": verification_result,
|
||||
"llm_report": llm_report,
|
||||
"export_artifacts": export_artifacts,
|
||||
"status": ProcessingStatus.COMPLETED,
|
||||
"completed_at": str(datetime.now()),
|
||||
})
|
||||
@@ -447,6 +457,55 @@ class ProcessingService:
|
||||
def _cache_export_shapes(self, task_id: str, export_shapes: Dict[str, Dict[str, Any]]):
|
||||
self._export_shapes_cache[task_id] = export_shapes
|
||||
|
||||
def _persist_step_exports(
|
||||
self,
|
||||
task_id: str,
|
||||
original_filename: str,
|
||||
export_shapes: Dict[str, Dict[str, Any]],
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
if not export_shapes:
|
||||
return None
|
||||
|
||||
base_filename = Path(original_filename).stem or f"mold_{task_id}"
|
||||
manifest = {
|
||||
"version": 1,
|
||||
"task_id": task_id,
|
||||
"generated_at": datetime.now().isoformat(),
|
||||
"schemes": {},
|
||||
}
|
||||
components = ["cavity", "core", "parting_surface"]
|
||||
|
||||
for scheme_id, cavity_data in export_shapes.items():
|
||||
try:
|
||||
result = self.cad_exporter.export_mold_results(
|
||||
cavity_data=cavity_data,
|
||||
base_filename=base_filename,
|
||||
formats=["step"],
|
||||
components=components,
|
||||
task_id=task_id,
|
||||
scheme_id=scheme_id,
|
||||
)
|
||||
manifest["schemes"][scheme_id] = {
|
||||
"base_filename": result.get("base_filename"),
|
||||
"generated_at": datetime.now().isoformat(),
|
||||
"files": result.get("files", []),
|
||||
"errors": result.get("errors", []),
|
||||
"total_files": result.get("total_files", 0),
|
||||
"total_errors": result.get("total_errors", 0),
|
||||
}
|
||||
except Exception as exc:
|
||||
logger.warning("持久化 STEP 导出失败: task=%s scheme=%s error=%s", task_id, scheme_id, exc)
|
||||
manifest["schemes"][scheme_id] = {
|
||||
"base_filename": base_filename,
|
||||
"generated_at": datetime.now().isoformat(),
|
||||
"files": [],
|
||||
"errors": [str(exc)],
|
||||
"total_files": 0,
|
||||
"total_errors": 1,
|
||||
}
|
||||
|
||||
return manifest
|
||||
|
||||
def get_export_shapes(self, task_id: str, scheme_id: Optional[str] = None) -> Optional[Dict[str, Any]]:
|
||||
scheme_map = self._export_shapes_cache.get(task_id, {})
|
||||
if not scheme_map:
|
||||
|
||||
@@ -112,6 +112,7 @@ class TaskQueryService:
|
||||
"html_file": html_file_url,
|
||||
"material": task_parameters.get("material"),
|
||||
"parameters": task_parameters,
|
||||
"export_artifacts": task_parameters.get("export_artifacts"),
|
||||
"stage_timings": task_parameters.get("stage_timings", {}),
|
||||
"verification": task_parameters.get("verification")
|
||||
or file_with_data.get("analysis_metrics", {}).get("verification_details"),
|
||||
|
||||
+5
-2
@@ -1598,7 +1598,7 @@ const ResultView = {
|
||||
reasons: [
|
||||
totalCount > 0 ? `规则分析识别出 ${totalCount} 处侧向机构需求` : '规则分析未识别出明确侧向机构需求',
|
||||
summary.complexity ? `当前复杂度判定为 ${summary.complexity}` : '当前复杂度信息不足',
|
||||
summary.has_hydraulic ? '规则分析提示可能涉及液压抽芯' : '未发现液压抽芯硬性提示'
|
||||
'当前倒扣方案若存在大行程滑块,优先按气动抽芯条件评估'
|
||||
].filter(Boolean),
|
||||
standard_advice: (sideActions.recommendations || []).slice(0, 4),
|
||||
manual_review_items: [
|
||||
@@ -1826,7 +1826,10 @@ const ResultView = {
|
||||
|
||||
if (result.status === 'success' && result.data.files) {
|
||||
for (const file of result.data.files) {
|
||||
const downloadUrl = `/api/export-download/${file.filepath.replace(/\\/g, '/').split('/').slice(-2).join('/')}`;
|
||||
const relativePath = file.relative_path
|
||||
|| (file.filepath ? file.filepath.replace(/\\/g, '/').split('/').slice(-2).join('/') : '');
|
||||
const downloadUrl = file.download_path
|
||||
|| `/api/export-download/${encodeURI(relativePath)}?task_id=${encodeURIComponent(taskId)}`;
|
||||
await downloadWithAuth(downloadUrl, file.filename);
|
||||
}
|
||||
addNotification(`已导出 ${result.data.files.length} 个 ${format.toUpperCase()} 文件`, 'success');
|
||||
|
||||
Reference in New Issue
Block a user