x
This commit is contained in:
File diff suppressed because one or more lines are too long
+2
-1
@@ -39,7 +39,7 @@ alembic>=1.11.0
|
|||||||
# 对象存储
|
# 对象存储
|
||||||
# ============================================
|
# ============================================
|
||||||
minio>=7.1.0
|
minio>=7.1.0
|
||||||
aiohttp>=3.8.0
|
aiohttp>=3.13.4
|
||||||
|
|
||||||
# ============================================
|
# ============================================
|
||||||
# 消息队列
|
# 消息队列
|
||||||
@@ -62,6 +62,7 @@ aiofiles>=23.0.0
|
|||||||
orjson>=3.9.0
|
orjson>=3.9.0
|
||||||
python-dotenv>=1.0.0
|
python-dotenv>=1.0.0
|
||||||
jinja2>=3.1.0
|
jinja2>=3.1.0
|
||||||
|
Pillow>=12.2.0
|
||||||
pyyaml>=6.0
|
pyyaml>=6.0
|
||||||
python-dateutil>=2.8.0
|
python-dateutil>=2.8.0
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ from fastapi import APIRouter, Depends, HTTPException, Request
|
|||||||
|
|
||||||
from services.auth_service import get_current_active_user
|
from services.auth_service import get_current_active_user
|
||||||
from services.redis_task_manager import redis_task_manager
|
from services.redis_task_manager import redis_task_manager
|
||||||
|
from services.processing_service import processing_service
|
||||||
from models.database import User
|
from models.database import User
|
||||||
from utils.logger import get_logger
|
from utils.logger import get_logger
|
||||||
|
|
||||||
@@ -281,6 +282,7 @@ async def export_mold_results(
|
|||||||
):
|
):
|
||||||
body = await request.json()
|
body = await request.json()
|
||||||
task_id = body.get("task_id")
|
task_id = body.get("task_id")
|
||||||
|
scheme_id = body.get("scheme_id")
|
||||||
formats = body.get("formats", ["step", "stl"])
|
formats = body.get("formats", ["step", "stl"])
|
||||||
components = body.get("components", ["cavity", "core"])
|
components = body.get("components", ["cavity", "core"])
|
||||||
|
|
||||||
@@ -291,16 +293,17 @@ async def export_mold_results(
|
|||||||
if not task_data:
|
if not task_data:
|
||||||
raise HTTPException(404, "任务不存在")
|
raise HTTPException(404, "任务不存在")
|
||||||
|
|
||||||
cavity_shapes = task_data.get("cavity_shapes")
|
cavity_shapes = processing_service.get_export_shapes(
|
||||||
|
task_id,
|
||||||
|
scheme_id or task_data.get("best_scheme_id"),
|
||||||
|
)
|
||||||
filename = task_data.get("filename", f"mold_{task_id}")
|
filename = task_data.get("filename", f"mold_{task_id}")
|
||||||
|
|
||||||
if not cavity_shapes:
|
if not cavity_shapes:
|
||||||
file_path = task_data.get("file_path")
|
raise HTTPException(
|
||||||
if file_path and os.path.exists(str(file_path)):
|
409,
|
||||||
cavity_shapes = await _reparse_stp_for_export(str(file_path), task_data.get("material", "ABS"))
|
"导出缓存已失效或任务尚未完成,请重新分析后再导出以保证方案一致性",
|
||||||
|
)
|
||||||
if not cavity_shapes:
|
|
||||||
raise HTTPException(400, "该任务尚未完成模具生成或形状数据不可用")
|
|
||||||
|
|
||||||
exporter = _get_cached("cad_exporter")
|
exporter = _get_cached("cad_exporter")
|
||||||
if not exporter:
|
if not exporter:
|
||||||
@@ -352,17 +355,3 @@ async def get_export_recommendations(
|
|||||||
return {"status": "success", "data": result}
|
return {"status": "success", "data": result}
|
||||||
|
|
||||||
|
|
||||||
async def _reparse_stp_for_export(file_path: str, material: str = "ABS") -> dict:
|
|
||||||
try:
|
|
||||||
from core.stp_parser import STPParser
|
|
||||||
from core.mold_generator import MoldCavityGenerator
|
|
||||||
stp_parser = STPParser()
|
|
||||||
shape = stp_parser.load_step_file(Path(file_path))
|
|
||||||
mold_gen = MoldCavityGenerator(shrinkage_rate=0.005)
|
|
||||||
mold_gen.set_material(material)
|
|
||||||
cavity_result = mold_gen.generate_mold_cavities(shape)
|
|
||||||
logger.info(f"重新解析 STP 用于导出: {file_path}")
|
|
||||||
return cavity_result
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning(f"重新解析 STP 导出失败: {e}")
|
|
||||||
return None
|
|
||||||
|
|||||||
+34
-10
@@ -1,6 +1,5 @@
|
|||||||
# api/v1/upload_router.py
|
# api/v1/upload_router.py
|
||||||
from fastapi import APIRouter, UploadFile, File, HTTPException, BackgroundTasks, Depends
|
from fastapi import APIRouter, UploadFile, File, HTTPException, BackgroundTasks, Depends, Form
|
||||||
from typing import Optional
|
|
||||||
import uuid
|
import uuid
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -27,14 +26,25 @@ file_handler = FileHandler()
|
|||||||
async def upload_stp(
|
async def upload_stp(
|
||||||
background_tasks: BackgroundTasks,
|
background_tasks: BackgroundTasks,
|
||||||
file: UploadFile = File(...),
|
file: UploadFile = File(...),
|
||||||
material: Optional[str] = "ABS",
|
material: str = Form(...),
|
||||||
|
draft_angle: float = Form(...),
|
||||||
|
shrinkage_rate: float = Form(...),
|
||||||
|
parting_precision: float = Form(...),
|
||||||
|
cavity_match: int = Form(...),
|
||||||
db_session: AsyncSession = Depends(get_db_session),
|
db_session: AsyncSession = Depends(get_db_session),
|
||||||
current_user: User = Depends(get_current_active_user)
|
current_user: User = Depends(get_current_active_user)
|
||||||
):
|
):
|
||||||
"""上传STP文件并存储到数据库"""
|
"""上传STP文件并存储到数据库"""
|
||||||
|
process_params = {
|
||||||
|
"material": material,
|
||||||
|
"draft_angle": float(draft_angle),
|
||||||
|
"shrinkage_rate": float(shrinkage_rate),
|
||||||
|
"parting_precision": float(parting_precision),
|
||||||
|
"cavity_match": int(cavity_match),
|
||||||
|
}
|
||||||
logger.info(
|
logger.info(
|
||||||
f"[UPLOAD] 用户={current_user.username}(id={current_user.id}) "
|
f"[UPLOAD] 用户={current_user.username}(id={current_user.id}) "
|
||||||
f"文件={file.filename} 材料={material} "
|
f"文件={file.filename} 参数={process_params} "
|
||||||
f"大小={file.size if hasattr(file, 'size') else 'unknown'}"
|
f"大小={file.size if hasattr(file, 'size') else 'unknown'}"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -44,7 +54,11 @@ async def upload_stp(
|
|||||||
|
|
||||||
task_id = str(uuid.uuid4())
|
task_id = str(uuid.uuid4())
|
||||||
|
|
||||||
file_path, file_size = await file_handler.save_uploaded_file(file)
|
try:
|
||||||
|
file_path, file_size, file_meta = await file_handler.save_uploaded_file(file)
|
||||||
|
except ValueError as exc:
|
||||||
|
logger.warning(f"[UPLOAD] 拒绝非法文件: {file.filename}, 原因={exc}")
|
||||||
|
raise HTTPException(400, str(exc)) from exc
|
||||||
logger.info(f"[UPLOAD] 文件已保存: {file_path} ({file_size} bytes), task_id={task_id}")
|
logger.info(f"[UPLOAD] 文件已保存: {file_path} ({file_size} bytes), task_id={task_id}")
|
||||||
|
|
||||||
storage_service = StorageIntegrationService()
|
storage_service = StorageIntegrationService()
|
||||||
@@ -52,12 +66,17 @@ async def upload_stp(
|
|||||||
stp_file = await storage_service.save_stp_file(
|
stp_file = await storage_service.save_stp_file(
|
||||||
session=db_session,
|
session=db_session,
|
||||||
file_path=file_path,
|
file_path=file_path,
|
||||||
original_filename=file.filename,
|
original_filename=file_meta["safe_original_name"],
|
||||||
user_id=current_user.id
|
user_id=current_user.id
|
||||||
)
|
)
|
||||||
logger.info(f"[UPLOAD] STP文件已存入RustFS+PG: stp_file.id={stp_file.id}")
|
logger.info(f"[UPLOAD] STP文件已存入RustFS+PG: stp_file.id={stp_file.id}")
|
||||||
|
|
||||||
await storage_service.create_processing_task(db_session, task_id, stp_file.id)
|
await storage_service.create_processing_task(
|
||||||
|
db_session,
|
||||||
|
task_id,
|
||||||
|
stp_file.id,
|
||||||
|
parameters=process_params,
|
||||||
|
)
|
||||||
|
|
||||||
task_info = create_task_info(
|
task_info = create_task_info(
|
||||||
task_id=task_id,
|
task_id=task_id,
|
||||||
@@ -67,11 +86,14 @@ async def upload_stp(
|
|||||||
file_size=file_size,
|
file_size=file_size,
|
||||||
upload_time=str(datetime.now())
|
upload_time=str(datetime.now())
|
||||||
)
|
)
|
||||||
|
task_info["material"] = material
|
||||||
|
task_info["parameters"] = process_params
|
||||||
|
task_info["file_hash"] = file_meta["sha256"]
|
||||||
await redis_task_manager.set_task(task_id, task_info)
|
await redis_task_manager.set_task(task_id, task_info)
|
||||||
|
|
||||||
background_tasks.add_task(
|
background_tasks.add_task(
|
||||||
processing_service.process_file_with_storage,
|
processing_service.process_file_with_storage,
|
||||||
task_id, file_path, stp_file.id, material
|
task_id, file_path, stp_file.id, process_params
|
||||||
)
|
)
|
||||||
logger.info(f"[UPLOAD] 后台处理已调度: task_id={task_id}")
|
logger.info(f"[UPLOAD] 后台处理已调度: task_id={task_id}")
|
||||||
|
|
||||||
@@ -83,6 +105,8 @@ async def upload_stp(
|
|||||||
"filename": file.filename,
|
"filename": file.filename,
|
||||||
"size": file_size,
|
"size": file_size,
|
||||||
"pythonocc_available": True,
|
"pythonocc_available": True,
|
||||||
"database_file_id": stp_file.id
|
"database_file_id": stp_file.id,
|
||||||
}
|
"sha256": file_meta["sha256"],
|
||||||
|
},
|
||||||
|
"parameters": process_params,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,9 +35,11 @@ class MultiSchemeMoldPlanner:
|
|||||||
material: Dict[str, Any],
|
material: Dict[str, Any],
|
||||||
is_foam_material: bool = False,
|
is_foam_material: bool = False,
|
||||||
max_schemes: int = 3,
|
max_schemes: int = 3,
|
||||||
|
process_params: Optional[Dict[str, Any]] = None,
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
generator = self.aluminum_foam_generator if is_foam_material else self.mold_generator
|
generator = self.aluminum_foam_generator if is_foam_material else self.mold_generator
|
||||||
generator.set_material(material["name"])
|
generator.set_material(material["name"])
|
||||||
|
self._apply_process_params(generator, material, process_params)
|
||||||
|
|
||||||
analysis = generator._analyze_product_geometry(shape)
|
analysis = generator._analyze_product_geometry(shape)
|
||||||
analysis["axis_normal_stats"] = self._collect_axis_normal_stats(generator, shape)
|
analysis["axis_normal_stats"] = self._collect_axis_normal_stats(generator, shape)
|
||||||
@@ -67,16 +69,20 @@ class MultiSchemeMoldPlanner:
|
|||||||
raise ValueError("未能生成任何可用分模方案")
|
raise ValueError("未能生成任何可用分模方案")
|
||||||
|
|
||||||
scored_schemes = self.scheme_scorer.score_schemes(schemes)[:max_schemes]
|
scored_schemes = self.scheme_scorer.score_schemes(schemes)[:max_schemes]
|
||||||
|
export_shapes = {}
|
||||||
for idx, scheme in enumerate(scored_schemes, start=1):
|
for idx, scheme in enumerate(scored_schemes, start=1):
|
||||||
scheme["raw_scheme_id"] = scheme.get("scheme_id")
|
scheme["raw_scheme_id"] = scheme.get("scheme_id")
|
||||||
scheme["scheme_id"] = f"scheme_{idx}"
|
scheme["scheme_id"] = f"scheme_{idx}"
|
||||||
if scheme.get("cavity_data", {}).get("metadata") is not None:
|
if scheme.get("cavity_data", {}).get("metadata") is not None:
|
||||||
scheme["cavity_data"]["metadata"]["scheme_id"] = scheme["scheme_id"]
|
scheme["cavity_data"]["metadata"]["scheme_id"] = scheme["scheme_id"]
|
||||||
|
scheme["cavity_data"]["metadata"]["process_parameters"] = dict(process_params or {})
|
||||||
|
export_shapes[scheme["scheme_id"]] = scheme.pop("_export_shapes", {})
|
||||||
best_scheme = scored_schemes[0]
|
best_scheme = scored_schemes[0]
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"best_scheme_id": best_scheme["scheme_id"],
|
"best_scheme_id": best_scheme["scheme_id"],
|
||||||
"candidate_schemes": scored_schemes,
|
"candidate_schemes": scored_schemes,
|
||||||
|
"_export_shapes": export_shapes,
|
||||||
"global_summary": {
|
"global_summary": {
|
||||||
"scheme_count": len(scored_schemes),
|
"scheme_count": len(scored_schemes),
|
||||||
"recommended_reason": best_scheme.get("summary", ""),
|
"recommended_reason": best_scheme.get("summary", ""),
|
||||||
@@ -178,8 +184,26 @@ class MultiSchemeMoldPlanner:
|
|||||||
"side_actions": side_action_result,
|
"side_actions": side_action_result,
|
||||||
"cavity_data": cavity_data,
|
"cavity_data": cavity_data,
|
||||||
"key_info": key_info,
|
"key_info": key_info,
|
||||||
|
"_export_shapes": {
|
||||||
|
"cavity": cavity,
|
||||||
|
"core": core,
|
||||||
|
"parting_surface": parting_surface,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _apply_process_params(generator: Any, material: Dict[str, Any], process_params: Optional[Dict[str, Any]]):
|
||||||
|
params = process_params or {}
|
||||||
|
draft_angle = float(params.get("draft_angle", getattr(generator, "draft_angle", 2.0)))
|
||||||
|
shrinkage_rate = float(params.get("shrinkage_rate", material.get("shrinkage", 0.005) * 100.0)) / 100.0
|
||||||
|
parting_precision = float(params.get("parting_precision", getattr(generator, "parting_line_tolerance", 0.1)))
|
||||||
|
cavity_match = float(params.get("cavity_match", getattr(generator, "cavity_match_rate", 95.0)))
|
||||||
|
|
||||||
|
generator.draft_angle = draft_angle
|
||||||
|
generator.shrinkage_rate = shrinkage_rate
|
||||||
|
generator.parting_line_tolerance = parting_precision
|
||||||
|
generator.cavity_match_rate = cavity_match
|
||||||
|
|
||||||
def _build_parting_surface(
|
def _build_parting_surface(
|
||||||
self,
|
self,
|
||||||
generator: Any,
|
generator: Any,
|
||||||
|
|||||||
+1
-6
@@ -74,15 +74,10 @@ async def log_requests(request: Request, call_next):
|
|||||||
status = response.status_code
|
status = response.status_code
|
||||||
|
|
||||||
if status >= 400:
|
if status >= 400:
|
||||||
auth_header = request.headers.get("authorization", "")
|
|
||||||
token_preview = ""
|
|
||||||
if auth_header.startswith("Bearer "):
|
|
||||||
token_raw = auth_header[7:]
|
|
||||||
token_preview = token_raw[:20] + "..." if len(token_raw) > 20 else token_raw
|
|
||||||
logger.warning(
|
logger.warning(
|
||||||
f"[HTTP] {request.method} {request.url.path} -> {status} "
|
f"[HTTP] {request.method} {request.url.path} -> {status} "
|
||||||
f"({duration:.2f}s) "
|
f"({duration:.2f}s) "
|
||||||
f"token={token_preview or 'none'}"
|
f"client={request.client.host if request.client else 'unknown'}"
|
||||||
)
|
)
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|||||||
@@ -316,7 +316,7 @@ class CalculationService:
|
|||||||
file_path=file_path,
|
file_path=file_path,
|
||||||
cavity_mesh_data=None,
|
cavity_mesh_data=None,
|
||||||
)
|
)
|
||||||
return {
|
result = {
|
||||||
"best_scheme_id": "scheme_1",
|
"best_scheme_id": "scheme_1",
|
||||||
"candidate_schemes": [
|
"candidate_schemes": [
|
||||||
{
|
{
|
||||||
@@ -349,6 +349,8 @@ class CalculationService:
|
|||||||
"cavity_data": legacy,
|
"cavity_data": legacy,
|
||||||
"key_info": legacy.get("mold_cavities", {}).get("cavity_key_info", {}),
|
"key_info": legacy.get("mold_cavities", {}).get("cavity_key_info", {}),
|
||||||
}
|
}
|
||||||
|
cls.attach_injection_system_summaries(result, material["name"])
|
||||||
|
return result
|
||||||
|
|
||||||
candidate_schemes = plan_result.get("candidate_schemes", [])
|
candidate_schemes = plan_result.get("candidate_schemes", [])
|
||||||
best_scheme = cls.get_best_scheme(plan_result)
|
best_scheme = cls.get_best_scheme(plan_result)
|
||||||
@@ -359,8 +361,54 @@ class CalculationService:
|
|||||||
"cavity_data": best_scheme.get("cavity_data", {}) if best_scheme else {},
|
"cavity_data": best_scheme.get("cavity_data", {}) if best_scheme else {},
|
||||||
"key_info": best_scheme.get("key_info", {}) if best_scheme else {},
|
"key_info": best_scheme.get("key_info", {}) if best_scheme else {},
|
||||||
}
|
}
|
||||||
|
cls.attach_injection_system_summaries(result, material["name"])
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def attach_injection_system_summaries(
|
||||||
|
cls,
|
||||||
|
plan_result: Dict[str, Any],
|
||||||
|
material_name: str,
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""为每个候选方案补充注塑模冷却/浇注摘要。"""
|
||||||
|
from core.mold_system_designer import MoldSystemDesigner
|
||||||
|
|
||||||
|
designer = MoldSystemDesigner()
|
||||||
|
for scheme in plan_result.get("candidate_schemes", []):
|
||||||
|
cavity_data = scheme.get("cavity_data") or {}
|
||||||
|
product_bbox = cavity_data.get("product_analysis", {}).get("bounding_box", {})
|
||||||
|
mold_size = cavity_data.get("manufacturing_info", {}).get("estimated_mold_size", {})
|
||||||
|
cavity_count = cavity_data.get("mold_cavities", {}).get("cavity_count", 1)
|
||||||
|
|
||||||
|
if not product_bbox or not mold_size:
|
||||||
|
continue
|
||||||
|
|
||||||
|
system_result = designer.design_complete_system(
|
||||||
|
mold_size=mold_size,
|
||||||
|
product_bbox=product_bbox,
|
||||||
|
material=material_name,
|
||||||
|
cavity_count=cavity_count,
|
||||||
|
)
|
||||||
|
|
||||||
|
cavity_data["injection_system"] = system_result
|
||||||
|
cavity_data.setdefault("manufacturing_info", {})
|
||||||
|
cavity_data["manufacturing_info"]["cooling_summary"] = {
|
||||||
|
"cooling_time": system_result.get("cooling", {}).get("cooling_time"),
|
||||||
|
"channel_count": system_result.get("cooling", {}).get("thermal_check", {}).get("channel_count"),
|
||||||
|
"flow_rate_lpm": system_result.get("cooling", {}).get("flow_rate", {}).get("flow_rate_lpm"),
|
||||||
|
}
|
||||||
|
cavity_data["manufacturing_info"]["gating_summary"] = {
|
||||||
|
"gate_type": system_result.get("gating", {}).get("gate_type"),
|
||||||
|
"runner_type": system_result.get("gating", {}).get("runner", {}).get("type"),
|
||||||
|
"estimated_cycle_time": system_result.get("overall_assessment", {}).get("estimated_cycle_time"),
|
||||||
|
}
|
||||||
|
|
||||||
|
best_scheme = cls.get_best_scheme(plan_result)
|
||||||
|
if best_scheme:
|
||||||
|
plan_result["injection_system"] = best_scheme.get("cavity_data", {}).get("injection_system")
|
||||||
|
|
||||||
|
return plan_result
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_best_scheme(plan_result: Optional[Dict[str, Any]]) -> Optional[Dict[str, Any]]:
|
def get_best_scheme(plan_result: Optional[Dict[str, Any]]) -> Optional[Dict[str, Any]]:
|
||||||
if not plan_result:
|
if not plan_result:
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
"""STP 文件处理流程编排器 — 协调解析、网格生成、型腔生成、保存、验证"""
|
"""STP 文件处理流程编排器 — 协调解析、网格生成、型腔生成、保存、验证"""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import time
|
||||||
import traceback
|
import traceback
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -42,6 +43,7 @@ class ProcessingService:
|
|||||||
self.html_generator = HTMLGenerator()
|
self.html_generator = HTMLGenerator()
|
||||||
self.storage_service = StorageIntegrationService()
|
self.storage_service = StorageIntegrationService()
|
||||||
self.multi_scheme_planner = MultiSchemeMoldPlanner()
|
self.multi_scheme_planner = MultiSchemeMoldPlanner()
|
||||||
|
self._export_shapes_cache: Dict[str, Dict[str, Dict[str, Any]]] = {}
|
||||||
|
|
||||||
# ─── 对外入口 ───
|
# ─── 对外入口 ───
|
||||||
|
|
||||||
@@ -50,7 +52,7 @@ class ProcessingService:
|
|||||||
task_id: str,
|
task_id: str,
|
||||||
file_path: str,
|
file_path: str,
|
||||||
stp_file_id: int,
|
stp_file_id: int,
|
||||||
material: str = "ABS",
|
process_params: Optional[Dict[str, Any]] = None,
|
||||||
):
|
):
|
||||||
"""处理文件的后台任务 — 使用独立数据库会话"""
|
"""处理文件的后台任务 — 使用独立数据库会话"""
|
||||||
|
|
||||||
@@ -65,7 +67,7 @@ class ProcessingService:
|
|||||||
try:
|
try:
|
||||||
await asyncio.wait_for(
|
await asyncio.wait_for(
|
||||||
self.process_file_core(
|
self.process_file_core(
|
||||||
task_id, file_path, stp_file_id, db_session, material
|
task_id, file_path, stp_file_id, db_session, process_params
|
||||||
),
|
),
|
||||||
timeout_seconds,
|
timeout_seconds,
|
||||||
)
|
)
|
||||||
@@ -96,29 +98,35 @@ class ProcessingService:
|
|||||||
file_path: str,
|
file_path: str,
|
||||||
stp_file_id: int,
|
stp_file_id: int,
|
||||||
db_session: AsyncSession,
|
db_session: AsyncSession,
|
||||||
material: str = "ABS",
|
process_params: Optional[Dict[str, Any]] = None,
|
||||||
):
|
):
|
||||||
"""核心处理逻辑"""
|
"""核心处理逻辑"""
|
||||||
|
|
||||||
try:
|
try:
|
||||||
logger.info(f"开始处理文件并生成模具型腔: {file_path}")
|
logger.info(f"开始处理文件并生成模具型腔: {file_path}")
|
||||||
|
process_params = self._normalize_process_params(process_params)
|
||||||
|
stage_timings: Dict[str, float] = {}
|
||||||
|
|
||||||
# 1. 解析STP文件
|
# 1. 解析STP文件
|
||||||
await self.storage_service.update_task_status(
|
await self.storage_service.update_task_status(
|
||||||
db_session, task_id, "processing", 20, "解析STP文件"
|
db_session, task_id, "processing", 20, "解析STP文件"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
stage_started = time.perf_counter()
|
||||||
shape = self.stp_parser.load_step_file(Path(file_path))
|
shape = self.stp_parser.load_step_file(Path(file_path))
|
||||||
geometry_data = self.stp_parser.analyze_geometry(shape)
|
geometry_data = self.stp_parser.analyze_geometry(shape)
|
||||||
|
stage_timings["parse_stp"] = round(time.perf_counter() - stage_started, 3)
|
||||||
|
|
||||||
# 2. 生成网格数据并持久化
|
# 2. 生成网格数据并持久化
|
||||||
await self.storage_service.update_task_status(
|
await self.storage_service.update_task_status(
|
||||||
db_session, task_id, "processing", 30, "生成网格数据"
|
db_session, task_id, "processing", 30, "生成网格数据"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
stage_started = time.perf_counter()
|
||||||
mesh_result = await self._step_generate_mesh(
|
mesh_result = await self._step_generate_mesh(
|
||||||
shape, geometry_data, file_path, db_session, stp_file_id, task_id
|
shape, geometry_data, file_path, db_session, stp_file_id, task_id
|
||||||
)
|
)
|
||||||
|
stage_timings["generate_mesh"] = round(time.perf_counter() - stage_started, 3)
|
||||||
|
|
||||||
# 3. 生成模具型腔
|
# 3. 生成模具型腔
|
||||||
await self.storage_service.update_task_status(
|
await self.storage_service.update_task_status(
|
||||||
@@ -126,25 +134,35 @@ class ProcessingService:
|
|||||||
)
|
)
|
||||||
|
|
||||||
# 材料属性 — 通过 MaterialService 集中管理
|
# 材料属性 — 通过 MaterialService 集中管理
|
||||||
requested_material = MaterialService.resolve_material(material)
|
requested_material = MaterialService.resolve_material(process_params["material"])
|
||||||
selected_material = MaterialService.get_material(requested_material)
|
selected_material = dict(MaterialService.get_material(requested_material))
|
||||||
|
selected_material["shrinkage"] = process_params["shrinkage_rate"] / 100.0
|
||||||
is_foam_material = MaterialService.is_foam_material(requested_material)
|
is_foam_material = MaterialService.is_foam_material(requested_material)
|
||||||
|
|
||||||
|
stage_started = time.perf_counter()
|
||||||
plan_result = await self._step_generate_cavity(
|
plan_result = await self._step_generate_cavity(
|
||||||
shape, selected_material, is_foam_material
|
shape, selected_material, is_foam_material, process_params
|
||||||
)
|
)
|
||||||
|
stage_timings["generate_cavity"] = round(time.perf_counter() - stage_started, 3)
|
||||||
|
export_shapes = {}
|
||||||
|
if plan_result:
|
||||||
|
export_shapes = plan_result.pop("_export_shapes", {}) or {}
|
||||||
|
if export_shapes:
|
||||||
|
self._cache_export_shapes(task_id, export_shapes)
|
||||||
|
|
||||||
# 4. 生成详细JSON数据 — 委托 CalculationService
|
# 4. 生成详细JSON数据 — 委托 CalculationService
|
||||||
await self.storage_service.update_task_status(
|
await self.storage_service.update_task_status(
|
||||||
db_session, task_id, "processing", 60, "生成型腔详细数据"
|
db_session, task_id, "processing", 60, "生成型腔详细数据"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
stage_started = time.perf_counter()
|
||||||
detailed_cavity_json = CalculationService.build_plan_result(
|
detailed_cavity_json = CalculationService.build_plan_result(
|
||||||
geometry_data=geometry_data,
|
geometry_data=geometry_data,
|
||||||
material=selected_material,
|
material=selected_material,
|
||||||
file_path=str(file_path),
|
file_path=str(file_path),
|
||||||
plan_result=plan_result,
|
plan_result=plan_result,
|
||||||
)
|
)
|
||||||
|
stage_timings["build_plan_result"] = round(time.perf_counter() - stage_started, 3)
|
||||||
|
|
||||||
best_scheme = CalculationService.get_best_scheme(detailed_cavity_json)
|
best_scheme = CalculationService.get_best_scheme(detailed_cavity_json)
|
||||||
best_cavity_data = best_scheme.get("cavity_data", {}) if best_scheme else {}
|
best_cavity_data = best_scheme.get("cavity_data", {}) if best_scheme else {}
|
||||||
@@ -164,6 +182,7 @@ class ProcessingService:
|
|||||||
db_session, task_id, "processing", 70, "保存几何数据"
|
db_session, task_id, "processing", 70, "保存几何数据"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
stage_started = time.perf_counter()
|
||||||
await self.storage_service.save_geometry_data(
|
await self.storage_service.save_geometry_data(
|
||||||
db_session,
|
db_session,
|
||||||
stp_file_id,
|
stp_file_id,
|
||||||
@@ -229,9 +248,15 @@ class ProcessingService:
|
|||||||
Path(html_file_path).name,
|
Path(html_file_path).name,
|
||||||
html_file_path,
|
html_file_path,
|
||||||
)
|
)
|
||||||
|
stage_timings["persist_artifacts"] = round(time.perf_counter() - stage_started, 3)
|
||||||
|
|
||||||
# 9. 分析模具设计
|
# 9. 分析模具设计
|
||||||
analysis_result = self.geometry_analyzer.analyze_mold_design(geometry_data)
|
stage_started = time.perf_counter()
|
||||||
|
analysis_result = self.geometry_analyzer.analyze_mold_design(
|
||||||
|
geometry_data,
|
||||||
|
product_material=requested_material,
|
||||||
|
shape=shape,
|
||||||
|
)
|
||||||
|
|
||||||
if analysis_result:
|
if analysis_result:
|
||||||
await self.storage_service.save_features_and_recommendations(
|
await self.storage_service.save_features_and_recommendations(
|
||||||
@@ -242,6 +267,7 @@ class ProcessingService:
|
|||||||
)
|
)
|
||||||
|
|
||||||
await self._save_analysis_metrics(db_session, stp_file_id, analysis_result)
|
await self._save_analysis_metrics(db_session, stp_file_id, analysis_result)
|
||||||
|
stage_timings["analyze_design"] = round(time.perf_counter() - stage_started, 3)
|
||||||
|
|
||||||
# 9.6 更新STP文件的分析摘要字段
|
# 9.6 更新STP文件的分析摘要字段
|
||||||
await self.storage_service.update_stp_file_analysis_summary(
|
await self.storage_service.update_stp_file_analysis_summary(
|
||||||
@@ -255,20 +281,35 @@ class ProcessingService:
|
|||||||
)
|
)
|
||||||
|
|
||||||
# 9.7 FreeCAD 几何验证
|
# 9.7 FreeCAD 几何验证
|
||||||
|
stage_started = time.perf_counter()
|
||||||
verification_result = await self._step_verify(
|
verification_result = await self._step_verify(
|
||||||
file_path, db_session, task_id, stp_file_id, analysis_result
|
file_path, db_session, task_id, stp_file_id, analysis_result
|
||||||
)
|
)
|
||||||
|
stage_timings["verify_geometry"] = round(time.perf_counter() - stage_started, 3)
|
||||||
|
|
||||||
# 9.8 LLM 增强分析
|
# 9.8 LLM 增强分析
|
||||||
llm_report = None
|
llm_report = None
|
||||||
|
stage_started = time.perf_counter()
|
||||||
if analysis_result:
|
if analysis_result:
|
||||||
llm_report = await llm_service.generate_design_report(analysis_result, detailed_cavity_json)
|
llm_report = await llm_service.generate_design_report(analysis_result, detailed_cavity_json)
|
||||||
|
stage_timings["generate_llm_report"] = round(time.perf_counter() - stage_started, 3)
|
||||||
|
|
||||||
# 10. 完成处理
|
# 10. 完成处理
|
||||||
await self.storage_service.update_stp_file_status(db_session, stp_file_id, "completed")
|
await self.storage_service.update_stp_file_status(db_session, stp_file_id, "completed")
|
||||||
await self.storage_service.update_task_status(
|
await self.storage_service.update_task_status(
|
||||||
db_session, task_id, "completed", 100, "模具型腔生成完成"
|
db_session, task_id, "completed", 100, "模具型腔生成完成"
|
||||||
)
|
)
|
||||||
|
await self.storage_service.update_task_parameters(
|
||||||
|
db_session,
|
||||||
|
task_id,
|
||||||
|
{
|
||||||
|
"stage_timings": stage_timings,
|
||||||
|
"material": requested_material,
|
||||||
|
"verification": verification_result,
|
||||||
|
"llm_report": llm_report,
|
||||||
|
**process_params,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
# 更新任务缓存状态
|
# 更新任务缓存状态
|
||||||
await redis_task_manager.update_task(task_id, {
|
await redis_task_manager.update_task(task_id, {
|
||||||
@@ -279,6 +320,9 @@ class ProcessingService:
|
|||||||
"best_scheme_id": detailed_cavity_json.get("best_scheme_id"),
|
"best_scheme_id": detailed_cavity_json.get("best_scheme_id"),
|
||||||
"cavity_data": best_cavity_data,
|
"cavity_data": best_cavity_data,
|
||||||
"key_info": best_key_info,
|
"key_info": best_key_info,
|
||||||
|
"material": requested_material,
|
||||||
|
"parameters": process_params,
|
||||||
|
"stage_timings": stage_timings,
|
||||||
"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}",
|
"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,
|
"verification": verification_result,
|
||||||
"llm_report": llm_report,
|
"llm_report": llm_report,
|
||||||
@@ -370,7 +414,7 @@ class ProcessingService:
|
|||||||
return mesh_result
|
return mesh_result
|
||||||
|
|
||||||
async def _step_generate_cavity(
|
async def _step_generate_cavity(
|
||||||
self, shape, selected_material: dict, is_foam_material: bool,
|
self, shape, selected_material: dict, is_foam_material: bool, process_params: Dict[str, Any],
|
||||||
) -> Optional[Dict[str, Any]]:
|
) -> Optional[Dict[str, Any]]:
|
||||||
"""生成多方案分模结果"""
|
"""生成多方案分模结果"""
|
||||||
plan_result = None
|
plan_result = None
|
||||||
@@ -380,6 +424,7 @@ class ProcessingService:
|
|||||||
shape=shape,
|
shape=shape,
|
||||||
material=selected_material,
|
material=selected_material,
|
||||||
is_foam_material=is_foam_material,
|
is_foam_material=is_foam_material,
|
||||||
|
process_params=process_params,
|
||||||
)
|
)
|
||||||
logger.info(
|
logger.info(
|
||||||
f"多方案分模完成: 生成 {len(plan_result.get('candidate_schemes', []))} 套方案"
|
f"多方案分模完成: 生成 {len(plan_result.get('candidate_schemes', []))} 套方案"
|
||||||
@@ -391,6 +436,28 @@ class ProcessingService:
|
|||||||
|
|
||||||
return plan_result
|
return plan_result
|
||||||
|
|
||||||
|
def _cache_export_shapes(self, task_id: str, export_shapes: Dict[str, Dict[str, Any]]):
|
||||||
|
self._export_shapes_cache[task_id] = export_shapes
|
||||||
|
|
||||||
|
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:
|
||||||
|
return None
|
||||||
|
if scheme_id:
|
||||||
|
return scheme_map.get(scheme_id)
|
||||||
|
return next(iter(scheme_map.values()), None)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _normalize_process_params(process_params: Optional[Dict[str, Any]]) -> Dict[str, Any]:
|
||||||
|
payload = dict(process_params or {})
|
||||||
|
return {
|
||||||
|
"material": MaterialService.resolve_material(str(payload.get("material", "ABS"))),
|
||||||
|
"draft_angle": float(payload.get("draft_angle", 2.0)),
|
||||||
|
"shrinkage_rate": float(payload.get("shrinkage_rate", 0.5)),
|
||||||
|
"parting_precision": float(payload.get("parting_precision", 0.1)),
|
||||||
|
"cavity_match": int(payload.get("cavity_match", 95)),
|
||||||
|
}
|
||||||
|
|
||||||
async def _step_verify(
|
async def _step_verify(
|
||||||
self, file_path: str, db_session: AsyncSession,
|
self, file_path: str, db_session: AsyncSession,
|
||||||
task_id: str, stp_file_id: int, analysis_result: Optional[dict],
|
task_id: str, stp_file_id: int, analysis_result: Optional[dict],
|
||||||
|
|||||||
@@ -127,10 +127,14 @@ class StorageIntegrationService:
|
|||||||
logger.info(f"STP文件保存成功 RustFS: {stp_file.id}, 批次: {batch_id}")
|
logger.info(f"STP文件保存成功 RustFS: {stp_file.id}, 批次: {batch_id}")
|
||||||
return stp_file
|
return stp_file
|
||||||
|
|
||||||
async def create_processing_task(self, session: AsyncSession,
|
async def create_processing_task(
|
||||||
task_id: str,
|
self,
|
||||||
stp_file_id: int,
|
session: AsyncSession,
|
||||||
task_type: str = "stp_parsing") -> ProcessingTask:
|
task_id: str,
|
||||||
|
stp_file_id: int,
|
||||||
|
task_type: str = "stp_parsing",
|
||||||
|
parameters: Optional[Dict[str, Any]] = None,
|
||||||
|
) -> ProcessingTask:
|
||||||
"""创建处理任务记录"""
|
"""创建处理任务记录"""
|
||||||
try:
|
try:
|
||||||
task = ProcessingTask(
|
task = ProcessingTask(
|
||||||
@@ -138,7 +142,8 @@ class StorageIntegrationService:
|
|||||||
stp_file_id=stp_file_id,
|
stp_file_id=stp_file_id,
|
||||||
task_type=task_type,
|
task_type=task_type,
|
||||||
status="pending",
|
status="pending",
|
||||||
started_time=datetime.now()
|
started_time=datetime.now(),
|
||||||
|
parameters=parameters or {},
|
||||||
)
|
)
|
||||||
|
|
||||||
session.add(task)
|
session.add(task)
|
||||||
@@ -189,6 +194,30 @@ class StorageIntegrationService:
|
|||||||
logger.error(f"更新任务状态失败: {e}")
|
logger.error(f"更新任务状态失败: {e}")
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
async def update_task_parameters(
|
||||||
|
self,
|
||||||
|
session: AsyncSession,
|
||||||
|
task_id: str,
|
||||||
|
parameters: Dict[str, Any],
|
||||||
|
):
|
||||||
|
"""合并更新任务参数,便于保存阶段耗时等元数据。"""
|
||||||
|
try:
|
||||||
|
task = await session.execute(
|
||||||
|
select(ProcessingTask).where(ProcessingTask.task_id == task_id)
|
||||||
|
)
|
||||||
|
task = task.scalar_one_or_none()
|
||||||
|
if task is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
merged = dict(task.parameters or {})
|
||||||
|
merged.update(parameters or {})
|
||||||
|
task.parameters = merged
|
||||||
|
await session.commit()
|
||||||
|
except Exception as e:
|
||||||
|
await session.rollback()
|
||||||
|
logger.error(f"更新任务参数失败: {e}")
|
||||||
|
raise
|
||||||
|
|
||||||
async def update_stp_file_status(self, session: AsyncSession, stp_file_id: int, status: str):
|
async def update_stp_file_status(self, session: AsyncSession, stp_file_id: int, status: str):
|
||||||
"""更新STP文件状态"""
|
"""更新STP文件状态"""
|
||||||
try:
|
try:
|
||||||
@@ -616,7 +645,11 @@ class StorageIntegrationService:
|
|||||||
'volume_utilization': stp_file.analysis_metrics.volume_utilization,
|
'volume_utilization': stp_file.analysis_metrics.volume_utilization,
|
||||||
'topology_complexity': stp_file.analysis_metrics.topology_complexity,
|
'topology_complexity': stp_file.analysis_metrics.topology_complexity,
|
||||||
'wall_uniformity': stp_file.analysis_metrics.wall_uniformity,
|
'wall_uniformity': stp_file.analysis_metrics.wall_uniformity,
|
||||||
'analysis_summary': stp_file.analysis_metrics.analysis_summary
|
'analysis_summary': stp_file.analysis_metrics.analysis_summary,
|
||||||
|
'verification_status': stp_file.analysis_metrics.verification_status,
|
||||||
|
'verification_volume_diff': stp_file.analysis_metrics.verification_volume_diff,
|
||||||
|
'verification_area_diff': stp_file.analysis_metrics.verification_area_diff,
|
||||||
|
'verification_details': stp_file.analysis_metrics.verification_details,
|
||||||
}
|
}
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|||||||
@@ -84,8 +84,10 @@ class TaskQueryService:
|
|||||||
|
|
||||||
# 构造与内存任务兼容的任务视图
|
# 构造与内存任务兼容的任务视图
|
||||||
cam_preferences = {}
|
cam_preferences = {}
|
||||||
|
task_parameters = {}
|
||||||
if isinstance(processing_task.parameters, dict):
|
if isinstance(processing_task.parameters, dict):
|
||||||
cam_preferences = processing_task.parameters.get("cam_preferences", {}) or {}
|
cam_preferences = processing_task.parameters.get("cam_preferences", {}) or {}
|
||||||
|
task_parameters = dict(processing_task.parameters)
|
||||||
|
|
||||||
task_view = {
|
task_view = {
|
||||||
"task_id": processing_task.task_id,
|
"task_id": processing_task.task_id,
|
||||||
@@ -108,6 +110,12 @@ class TaskQueryService:
|
|||||||
"plan_result": cavity_json,
|
"plan_result": cavity_json,
|
||||||
"mesh_summary": mesh_summary,
|
"mesh_summary": mesh_summary,
|
||||||
"html_file": html_file_url,
|
"html_file": html_file_url,
|
||||||
|
"material": task_parameters.get("material"),
|
||||||
|
"parameters": task_parameters,
|
||||||
|
"stage_timings": task_parameters.get("stage_timings", {}),
|
||||||
|
"verification": task_parameters.get("verification")
|
||||||
|
or file_with_data.get("analysis_metrics", {}).get("verification_details"),
|
||||||
|
"llm_report": task_parameters.get("llm_report"),
|
||||||
"analysis_result": {
|
"analysis_result": {
|
||||||
"geometry_data": geometry_json,
|
"geometry_data": geometry_json,
|
||||||
"detected_features": features_json,
|
"detected_features": features_json,
|
||||||
|
|||||||
@@ -1,24 +1,66 @@
|
|||||||
# utils/file_handler.py
|
# utils/file_handler.py
|
||||||
import aiofiles
|
import aiofiles
|
||||||
|
import hashlib
|
||||||
|
import re
|
||||||
|
import uuid
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from fastapi import UploadFile
|
from fastapi import UploadFile
|
||||||
from typing import Tuple
|
from typing import Tuple, Dict, Any
|
||||||
|
|
||||||
|
|
||||||
class FileHandler:
|
class FileHandler:
|
||||||
def __init__(self, upload_dir: str = "uploads"):
|
def __init__(self, upload_dir: str = "uploads", max_file_size: int = 50 * 1024 * 1024):
|
||||||
self.upload_dir = Path(upload_dir)
|
self.upload_dir = Path(upload_dir)
|
||||||
self.upload_dir.mkdir(exist_ok=True)
|
self.upload_dir.mkdir(exist_ok=True)
|
||||||
|
self.max_file_size = max_file_size
|
||||||
|
|
||||||
async def save_uploaded_file(self, file: UploadFile) -> Tuple[Path, int]:
|
def _sanitize_filename(self, filename: str) -> str:
|
||||||
"""保存上传的文件"""
|
original = Path(filename or "upload.step").name
|
||||||
file_path = self.upload_dir / file.filename
|
suffix = Path(original).suffix.lower()
|
||||||
|
stem = Path(original).stem or "upload"
|
||||||
|
safe_stem = re.sub(r"[^A-Za-z0-9._-]+", "_", stem).strip("._-") or "upload"
|
||||||
|
if suffix not in {".stp", ".step"}:
|
||||||
|
suffix = ".step"
|
||||||
|
return f"{safe_stem}{suffix}"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _looks_like_step(content: bytes) -> bool:
|
||||||
|
if not content:
|
||||||
|
return False
|
||||||
|
head = content[:4096].decode("utf-8", errors="ignore").upper()
|
||||||
|
return (
|
||||||
|
"ISO-10303-21" in head
|
||||||
|
or "HEADER;" in head
|
||||||
|
or "FILE_SCHEMA" in head
|
||||||
|
or "DATA;" in head
|
||||||
|
)
|
||||||
|
|
||||||
|
async def save_uploaded_file(self, file: UploadFile) -> Tuple[Path, int, Dict[str, Any]]:
|
||||||
|
"""保存上传的文件并返回安全元数据"""
|
||||||
content = await file.read()
|
content = await file.read()
|
||||||
async with aiofiles.open(file_path, 'wb') as f:
|
|
||||||
|
if not content:
|
||||||
|
raise ValueError("上传文件为空")
|
||||||
|
if len(content) > self.max_file_size:
|
||||||
|
raise ValueError(f"上传文件过大,限制 {self.max_file_size // (1024 * 1024)}MB")
|
||||||
|
if not self._looks_like_step(content):
|
||||||
|
raise ValueError("文件内容不是有效的 STP/STEP 数据")
|
||||||
|
|
||||||
|
safe_name = self._sanitize_filename(file.filename)
|
||||||
|
unique_name = f"{uuid.uuid4().hex}_{safe_name}"
|
||||||
|
file_path = self.upload_dir / unique_name
|
||||||
|
|
||||||
|
async with aiofiles.open(file_path, "wb") as f:
|
||||||
await f.write(content)
|
await f.write(content)
|
||||||
|
|
||||||
return file_path, len(content)
|
metadata = {
|
||||||
|
"original_filename": file.filename,
|
||||||
|
"safe_original_name": safe_name,
|
||||||
|
"stored_filename": unique_name,
|
||||||
|
"sha256": hashlib.sha256(content).hexdigest(),
|
||||||
|
}
|
||||||
|
|
||||||
|
return file_path, len(content), metadata
|
||||||
|
|
||||||
def cleanup_file(self, file_path: Path):
|
def cleanup_file(self, file_path: Path):
|
||||||
"""清理文件"""
|
"""清理文件"""
|
||||||
|
|||||||
@@ -604,6 +604,57 @@ body {
|
|||||||
margin-bottom: var(--space-8);
|
margin-bottom: var(--space-8);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.moldinsight-intro-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
|
gap: var(--space-4);
|
||||||
|
margin-bottom: var(--space-6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.intro-card {
|
||||||
|
background: var(--bg-primary);
|
||||||
|
border: 1px solid var(--border-light);
|
||||||
|
border-radius: var(--radius-xl);
|
||||||
|
padding: var(--space-5);
|
||||||
|
box-shadow: var(--shadow-xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.intro-card-title {
|
||||||
|
font-size: var(--text-xs);
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
margin-bottom: var(--space-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.intro-card-text {
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
line-height: 1.7;
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-weight: var(--font-medium);
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-layout {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1.6fr) minmax(320px, 0.9fr);
|
||||||
|
gap: var(--space-6);
|
||||||
|
align-items: start;
|
||||||
|
margin-bottom: var(--space-8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-main-card,
|
||||||
|
.upload-side-card {
|
||||||
|
background: var(--bg-primary);
|
||||||
|
border: 1px solid var(--border-light);
|
||||||
|
border-radius: var(--radius-xl);
|
||||||
|
padding: var(--space-6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-main-card .section-title,
|
||||||
|
.upload-side-card .section-title {
|
||||||
|
margin-bottom: var(--space-4);
|
||||||
|
}
|
||||||
|
|
||||||
.upload-section .btn-primary {
|
.upload-section .btn-primary {
|
||||||
min-width: 200px;
|
min-width: 200px;
|
||||||
padding: var(--space-4) var(--space-8);
|
padding: var(--space-4) var(--space-8);
|
||||||
@@ -649,6 +700,10 @@ body {
|
|||||||
border: 1px solid #e0e0e0;
|
border: 1px solid #e0e0e0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.compact-panel {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.panel-header {
|
.panel-header {
|
||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
}
|
}
|
||||||
@@ -749,6 +804,58 @@ optgroup {
|
|||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.advanced-params {
|
||||||
|
margin-top: var(--space-4);
|
||||||
|
border: 1px solid var(--border-default);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
background: var(--bg-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.advanced-params summary {
|
||||||
|
cursor: pointer;
|
||||||
|
list-style: none;
|
||||||
|
padding: var(--space-4);
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
font-weight: var(--font-semibold);
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.advanced-params summary::-webkit-details-marker {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.advanced-params-body {
|
||||||
|
padding: 0 var(--space-4) var(--space-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-submit-btn {
|
||||||
|
width: 100%;
|
||||||
|
margin-top: var(--space-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-collapsible {
|
||||||
|
background: var(--bg-primary);
|
||||||
|
border: 1px solid var(--border-light);
|
||||||
|
border-radius: var(--radius-xl);
|
||||||
|
padding: var(--space-5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-summary {
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: var(--text-lg);
|
||||||
|
font-weight: var(--font-semibold);
|
||||||
|
color: var(--text-primary);
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-summary::-webkit-details-marker {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-collapsible[open] .table-container {
|
||||||
|
margin-top: var(--space-4);
|
||||||
|
}
|
||||||
|
|
||||||
.btn-clear {
|
.btn-clear {
|
||||||
background: none;
|
background: none;
|
||||||
border: none;
|
border: none;
|
||||||
@@ -1191,6 +1298,19 @@ optgroup {
|
|||||||
grid-column: 1 / -1;
|
grid-column: 1 / -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.result-card-highlight {
|
||||||
|
border: 1px solid var(--primary-100);
|
||||||
|
background: linear-gradient(180deg, var(--bg-primary) 0%, var(--primary-50) 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-priority-grid {
|
||||||
|
margin-bottom: var(--space-6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.compact-metrics-grid {
|
||||||
|
margin-bottom: var(--space-6);
|
||||||
|
}
|
||||||
|
|
||||||
.single-scheme-grid {
|
.single-scheme-grid {
|
||||||
grid-template-columns: 1fr !important;
|
grid-template-columns: 1fr !important;
|
||||||
}
|
}
|
||||||
@@ -1276,6 +1396,22 @@ optgroup {
|
|||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.diagnostic-panel summary {
|
||||||
|
cursor: pointer;
|
||||||
|
list-style: none;
|
||||||
|
font-size: var(--text-lg);
|
||||||
|
font-weight: var(--font-semibold);
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.diagnostic-panel summary::-webkit-details-marker {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.diagnostic-panel-body {
|
||||||
|
margin-top: var(--space-4);
|
||||||
|
}
|
||||||
|
|
||||||
/* ================================
|
/* ================================
|
||||||
设计建议列表
|
设计建议列表
|
||||||
================================ */
|
================================ */
|
||||||
@@ -2636,6 +2772,11 @@ select.form-input {
|
|||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.export-buttons-block {
|
||||||
|
margin-left: 0;
|
||||||
|
margin-top: var(--space-4);
|
||||||
|
}
|
||||||
|
|
||||||
.export-buttons .btn-sm {
|
.export-buttons .btn-sm {
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
padding: 0.25rem 0.6rem;
|
padding: 0.25rem 0.6rem;
|
||||||
@@ -2695,6 +2836,13 @@ select.form-input {
|
|||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 960px) {
|
||||||
|
.moldinsight-intro-grid,
|
||||||
|
.upload-layout {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* ================================
|
/* ================================
|
||||||
历史记录
|
历史记录
|
||||||
================================ */
|
================================ */
|
||||||
|
|||||||
+411
-217
@@ -981,8 +981,6 @@ const UsersView = {
|
|||||||
const MoldInsightView = {
|
const MoldInsightView = {
|
||||||
setup() {
|
setup() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const deliveryDate = ref(null);
|
|
||||||
const expectedDate = ref(null);
|
|
||||||
const state = reactive({
|
const state = reactive({
|
||||||
selectedFile: null,
|
selectedFile: null,
|
||||||
selectedMaterial: 'ABS',
|
selectedMaterial: 'ABS',
|
||||||
@@ -1079,6 +1077,10 @@ const MoldInsightView = {
|
|||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append("file", state.selectedFile);
|
formData.append("file", state.selectedFile);
|
||||||
formData.append("material", state.selectedMaterial);
|
formData.append("material", state.selectedMaterial);
|
||||||
|
formData.append("draft_angle", String(state.moldParams.draftAngle));
|
||||||
|
formData.append("shrinkage_rate", String(state.moldParams.shrinkageRate));
|
||||||
|
formData.append("parting_precision", String(state.moldParams.partingPrecision));
|
||||||
|
formData.append("cavity_match", String(state.moldParams.cavityMatch));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch("/api/upload", {
|
const res = await fetch("/api/upload", {
|
||||||
@@ -1154,11 +1156,6 @@ const MoldInsightView = {
|
|||||||
loadHistory();
|
loadHistory();
|
||||||
});
|
});
|
||||||
|
|
||||||
const isFoamMaterial = (material) => {
|
|
||||||
const foamMaterials = ['AlSi10Mg', 'AlSi12', 'Pure Al Foam', 'AlSi7Mg'];
|
|
||||||
return foamMaterials.includes(material);
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
state,
|
state,
|
||||||
handleFileChange,
|
handleFileChange,
|
||||||
@@ -1168,56 +1165,74 @@ const MoldInsightView = {
|
|||||||
formatDateTime,
|
formatDateTime,
|
||||||
formatNumber,
|
formatNumber,
|
||||||
toggleFileHistory,
|
toggleFileHistory,
|
||||||
viewResult,
|
viewResult
|
||||||
isFoamMaterial
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
template: `
|
template: `
|
||||||
<div class="page-container">
|
<div class="page-container">
|
||||||
<div class="page-header">
|
<div class="page-header">
|
||||||
<h1>MoldInsight</h1>
|
<h1>注塑模 STP 分析</h1>
|
||||||
<p>STP 模具几何分析</p>
|
<p>上传 STEP/STP 产品件,完成自动分模、工程建议与导出</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="upload-section">
|
<div class="moldinsight-intro-grid">
|
||||||
<div
|
<div class="intro-card">
|
||||||
:class="['upload-zone', { 'drag-over': state.dragOver }]"
|
<div class="intro-card-title">输入</div>
|
||||||
@dragover.prevent="state.dragOver = true"
|
<div class="intro-card-text">STEP/STP 产品件,面向注塑模主流程</div>
|
||||||
@dragleave.prevent="state.dragOver = false"
|
</div>
|
||||||
@drop="handleDrop"
|
<div class="intro-card">
|
||||||
@click="$refs.fileInput.click()"
|
<div class="intro-card-title">输出</div>
|
||||||
>
|
<div class="intro-card-text">分模方案、DFM 风险、注塑模系统摘要与 CAD 导出</div>
|
||||||
<input
|
</div>
|
||||||
ref="fileInput"
|
<div class="intro-card">
|
||||||
type="file"
|
<div class="intro-card-title">目标</div>
|
||||||
accept=".stp,.step"
|
<div class="intro-card-text">先确认推荐方案,再进入导出与 CAM 准备</div>
|
||||||
@change="handleFileChange"
|
</div>
|
||||||
hidden
|
</div>
|
||||||
/>
|
|
||||||
<div class="upload-icon">📁</div>
|
<div class="upload-layout">
|
||||||
<div class="upload-text">
|
<div class="upload-main-card">
|
||||||
<span class="upload-title">点击选择或拖拽文件</span>
|
<h2 class="section-title">1. 上传产品件</h2>
|
||||||
<span class="upload-hint">支持 .stp, .step 格式,最大 100MB</span>
|
<div
|
||||||
|
:class="['upload-zone', { 'drag-over': state.dragOver }]"
|
||||||
|
@dragover.prevent="state.dragOver = true"
|
||||||
|
@dragleave.prevent="state.dragOver = false"
|
||||||
|
@drop="handleDrop"
|
||||||
|
@click="$refs.fileInput.click()"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
ref="fileInput"
|
||||||
|
type="file"
|
||||||
|
accept=".stp,.step"
|
||||||
|
@change="handleFileChange"
|
||||||
|
hidden
|
||||||
|
/>
|
||||||
|
<div class="upload-icon">📁</div>
|
||||||
|
<div class="upload-text">
|
||||||
|
<span class="upload-title">点击选择或拖拽 STP/STEP 文件</span>
|
||||||
|
<span class="upload-hint">支持注塑模产品件分析,最大 100MB</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="state.selectedFile" class="file-info">
|
||||||
|
<span class="file-name">{{ state.selectedFile.name }}</span>
|
||||||
|
<span class="file-size">{{ formatFileSize(state.selectedFile.size) }}</span>
|
||||||
|
<button class="btn-clear" @click="state.selectedFile = null" title="清除文件">×</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="state.error" class="error-message">{{ state.error }}</div>
|
||||||
|
|
||||||
|
<div v-if="state.polling" class="progress-bar">
|
||||||
|
<div class="progress-fill" :style="{ width: state.progress + '%' }"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="state.selectedFile" class="file-info">
|
<div class="upload-side-card">
|
||||||
<span class="file-name">{{ state.selectedFile.name }}</span>
|
<h2 class="section-title">2. 注塑模参数</h2>
|
||||||
<span class="file-size">{{ formatFileSize(state.selectedFile.size) }}</span>
|
<div class="material-panel compact-panel">
|
||||||
<button class="btn-clear" @click="state.selectedFile = null" title="清除文件">×</button>
|
<div class="form-group">
|
||||||
</div>
|
<label class="form-label">产品材料</label>
|
||||||
|
<select v-model="state.selectedMaterial" class="form-select">
|
||||||
<!-- 材料选择面板 -->
|
|
||||||
<div v-if="state.selectedFile" class="material-panel">
|
|
||||||
<div class="panel-header">
|
|
||||||
<span class="panel-title">📦 材料与参数设置</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 材料类型选择 -->
|
|
||||||
<div class="form-group">
|
|
||||||
<label class="form-label">材料类型</label>
|
|
||||||
<select v-model="state.selectedMaterial" class="form-select">
|
|
||||||
<optgroup label="普通塑料">
|
|
||||||
<option value="ABS">ABS (1.05 g/cm³)</option>
|
<option value="ABS">ABS (1.05 g/cm³)</option>
|
||||||
<option value="PP">PP (0.90 g/cm³)</option>
|
<option value="PP">PP (0.90 g/cm³)</option>
|
||||||
<option value="PE">PE (0.95 g/cm³)</option>
|
<option value="PE">PE (0.95 g/cm³)</option>
|
||||||
@@ -1226,70 +1241,58 @@ const MoldInsightView = {
|
|||||||
<option value="POM">POM (1.41 g/cm³)</option>
|
<option value="POM">POM (1.41 g/cm³)</option>
|
||||||
<option value="PMMA">PMMA (1.18 g/cm³)</option>
|
<option value="PMMA">PMMA (1.18 g/cm³)</option>
|
||||||
<option value="PBT">PBT (1.31 g/cm³)</option>
|
<option value="PBT">PBT (1.31 g/cm³)</option>
|
||||||
</optgroup>
|
</select>
|
||||||
<optgroup label="铝泡沫材料">
|
|
||||||
<option value="AlSi10Mg">AlSi10Mg (0.45 g/cm³) - 常用铝硅泡沫</option>
|
|
||||||
<option value="AlSi12">AlSi12 (0.50 g/cm³) - 高强度铝泡沫</option>
|
|
||||||
<option value="Pure Al Foam">Pure Al Foam (0.35 g/cm³) - 纯铝泡沫</option>
|
|
||||||
<option value="AlSi7Mg">AlSi7Mg (0.40 g/cm³) - 轻质铝镁泡沫</option>
|
|
||||||
</optgroup>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 铝泡沫参数(仅在选择泡沫材料时显示) -->
|
|
||||||
<div v-if="isFoamMaterial(state.selectedMaterial)" class="foam-params">
|
|
||||||
<div class="param-section">
|
|
||||||
<span class="param-title">⚙️ 铝泡沫专用参数</span>
|
|
||||||
|
|
||||||
<div class="form-row">
|
|
||||||
<div class="form-group">
|
|
||||||
<label class="form-label">拔模角 (°)</label>
|
|
||||||
<input type="range" v-model.number="state.moldParams.draftAngle" min="1" max="10" step="0.5" class="form-range">
|
|
||||||
<span class="range-value">{{ state.moldParams.draftAngle }}°</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label class="form-label">收缩率 (%)</label>
|
|
||||||
<input type="range" v-model.number="state.moldParams.shrinkageRate" min="0.5" max="3.0" step="0.1" class="form-range">
|
|
||||||
<span class="range-value">{{ state.moldParams.shrinkageRate }}%</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-row">
|
|
||||||
<div class="form-group">
|
|
||||||
<label class="form-label">分型精度 (mm)</label>
|
|
||||||
<input type="range" v-model.number="state.moldParams.partingPrecision" min="0.01" max="1.0" step="0.01" class="form-range">
|
|
||||||
<span class="range-value">{{ state.moldParams.partingPrecision }} mm</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label class="form-label">型腔匹配度 (%)</label>
|
|
||||||
<input type="range" v-model.number="state.moldParams.cavityMatch" min="80" max="100" step="1" class="form-range">
|
|
||||||
<span class="range-value">{{ state.moldParams.cavityMatch }}%</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<details class="advanced-params">
|
||||||
|
<summary>高级工艺参数</summary>
|
||||||
|
<div class="advanced-params-body">
|
||||||
|
<div class="inline-note">默认值适用于多数注塑件;仅在已知工艺约束时再调整。</div>
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">拔模角 (°)</label>
|
||||||
|
<input type="range" v-model.number="state.moldParams.draftAngle" min="1" max="10" step="0.5" class="form-range">
|
||||||
|
<span class="range-value">{{ state.moldParams.draftAngle }}°</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">收缩率 (%)</label>
|
||||||
|
<input type="range" v-model.number="state.moldParams.shrinkageRate" min="0.5" max="3.0" step="0.1" class="form-range">
|
||||||
|
<span class="range-value">{{ state.moldParams.shrinkageRate }}%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">分型精度 (mm)</label>
|
||||||
|
<input type="range" v-model.number="state.moldParams.partingPrecision" min="0.01" max="1.0" step="0.01" class="form-range">
|
||||||
|
<span class="range-value">{{ state.moldParams.partingPrecision }} mm</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">型腔匹配度 (%)</label>
|
||||||
|
<input type="range" v-model.number="state.moldParams.cavityMatch" min="80" max="100" step="1" class="form-range">
|
||||||
|
<span class="range-value">{{ state.moldParams.cavityMatch }}%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
<button
|
||||||
<div v-if="state.error" class="error-message">{{ state.error }}</div>
|
v-if="state.selectedFile"
|
||||||
|
class="btn-primary upload-submit-btn"
|
||||||
<button
|
@click="uploadFile"
|
||||||
v-if="state.selectedFile"
|
:disabled="state.uploading || state.polling"
|
||||||
class="btn-primary"
|
>
|
||||||
@click="uploadFile"
|
{{ state.uploading ? '上传中...' : state.polling ? '分析中...' : '3. 开始注塑模分析' }}
|
||||||
:disabled="state.uploading || state.polling"
|
</button>
|
||||||
>
|
<div v-else class="inline-note">先选择 STP 文件,再填写材料并开始分析。</div>
|
||||||
{{ state.uploading ? '上传中...' : state.polling ? '分析中...' : '开始分析' }}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<div v-if="state.polling" class="progress-bar">
|
|
||||||
<div class="progress-fill" :style="{ width: state.progress + '%' }"></div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="state.history?.files?.length" class="section">
|
<details v-if="state.history?.files?.length" class="history-collapsible section">
|
||||||
<h2 class="section-title">分析历史</h2>
|
<summary class="history-summary">分析历史({{ state.history.files.length }} 个文件)</summary>
|
||||||
<div class="table-container">
|
<div class="table-container">
|
||||||
<table class="data-table">
|
<table class="data-table">
|
||||||
<thead>
|
<thead>
|
||||||
@@ -1369,7 +1372,7 @@ const MoldInsightView = {
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</details>
|
||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
};
|
};
|
||||||
@@ -1457,10 +1460,6 @@ const ResultView = {
|
|||||||
return priorityMap[priority] || priority;
|
return priorityMap[priority] || priority;
|
||||||
};
|
};
|
||||||
|
|
||||||
const isFoamMaterial = (material) => {
|
|
||||||
return material === 'aluminum_foam';
|
|
||||||
};
|
|
||||||
|
|
||||||
const candidateSchemes = computed(() => state.task?.candidate_schemes || []);
|
const candidateSchemes = computed(() => state.task?.candidate_schemes || []);
|
||||||
const hasSingleScheme = computed(() => candidateSchemes.value.length === 1);
|
const hasSingleScheme = computed(() => candidateSchemes.value.length === 1);
|
||||||
const selectedScheme = computed(() => {
|
const selectedScheme = computed(() => {
|
||||||
@@ -1471,6 +1470,33 @@ const ResultView = {
|
|||||||
const selectedKeyInfo = computed(() => selectedScheme.value?.key_info || state.task?.key_info || null);
|
const selectedKeyInfo = computed(() => selectedScheme.value?.key_info || state.task?.key_info || null);
|
||||||
const selectedHtmlFile = computed(() => selectedScheme.value?.html_file || state.task?.html_file || '');
|
const selectedHtmlFile = computed(() => selectedScheme.value?.html_file || state.task?.html_file || '');
|
||||||
const selectedDfmViolations = computed(() => selectedScheme.value?.dfm_violations || []);
|
const selectedDfmViolations = computed(() => selectedScheme.value?.dfm_violations || []);
|
||||||
|
const selectedInjectionSystem = computed(() => selectedCavityData.value?.injection_system || state.task?.plan_result?.injection_system || null);
|
||||||
|
const stageTimingEntries = computed(() => {
|
||||||
|
const timings = state.task?.stage_timings || {};
|
||||||
|
return Object.entries(timings)
|
||||||
|
.filter(([, value]) => typeof value === 'number')
|
||||||
|
.sort((a, b) => b[1] - a[1]);
|
||||||
|
});
|
||||||
|
const sortedRecommendations = computed(() => {
|
||||||
|
const priorityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
|
||||||
|
return [...(state.task?.analysis_result?.design_recommendations || [])].sort((a, b) => {
|
||||||
|
return (priorityOrder[a.priority] ?? 99) - (priorityOrder[b.priority] ?? 99);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
const primaryRecommendation = computed(() => sortedRecommendations.value[0] || null);
|
||||||
|
const selectedDfmCount = computed(() => selectedDfmViolations.value?.length || 0);
|
||||||
|
const selectedCavityCount = computed(() => {
|
||||||
|
const cavityValue = selectedCavityData.value?.mold_cavities?.cavity_count;
|
||||||
|
if (typeof cavityValue === 'number') return cavityValue;
|
||||||
|
const cavityObj = selectedCavityData.value?.mold_cavities || selectedKeyInfo.value?.mold_cavities || {};
|
||||||
|
return cavityObj.cavity_count || Object.keys(cavityObj).filter(key => key.startsWith('cavity_')).length || 1;
|
||||||
|
});
|
||||||
|
const selectedRiskLabel = computed(() => {
|
||||||
|
if (!selectedDfmCount.value) return '低风险';
|
||||||
|
if (selectedDfmCount.value >= 4) return '高风险';
|
||||||
|
if (selectedDfmCount.value >= 2) return '中风险';
|
||||||
|
return '低风险';
|
||||||
|
});
|
||||||
|
|
||||||
const selectScheme = (schemeId) => {
|
const selectScheme = (schemeId) => {
|
||||||
state.selectedSchemeId = schemeId;
|
state.selectedSchemeId = schemeId;
|
||||||
@@ -1495,6 +1521,40 @@ const ResultView = {
|
|||||||
return `[${dir.map(v => Number(v).toFixed(2)).join(', ')}]`;
|
return `[${dir.map(v => Number(v).toFixed(2)).join(', ')}]`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const analysisFeatures = computed(() => state.task?.analysis_result?.detected_features || []);
|
||||||
|
const getFeatureByTypes = (...types) => analysisFeatures.value.find(f => types.includes(f.feature_type));
|
||||||
|
const countFeaturesByTypes = (...types) => analysisFeatures.value.filter(f => types.includes(f.feature_type)).length;
|
||||||
|
const getWallThicknessSummary = () => {
|
||||||
|
const feature = getFeatureByTypes('thin_wall', 'thick_wall', 'wall_non_uniform');
|
||||||
|
if (!feature) return '待分析';
|
||||||
|
const params = feature.parameters || {};
|
||||||
|
if (params.min_thickness != null && params.max_thickness != null) {
|
||||||
|
return `${Number(params.min_thickness).toFixed(2)} - ${Number(params.max_thickness).toFixed(2)} mm`;
|
||||||
|
}
|
||||||
|
if (params.average_thickness != null) {
|
||||||
|
return `平均 ${Number(params.average_thickness).toFixed(2)} mm`;
|
||||||
|
}
|
||||||
|
return feature.recommendations?.[0] || '已完成分析';
|
||||||
|
};
|
||||||
|
const getUndercutCount = () => {
|
||||||
|
const fromScheme = selectedScheme.value?.undercut_regions?.length || selectedCavityData.value?.undercut_regions?.length || 0;
|
||||||
|
return fromScheme || countFeaturesByTypes('undercut');
|
||||||
|
};
|
||||||
|
const formatStageName = (name) => {
|
||||||
|
const stageNameMap = {
|
||||||
|
parse_stp: 'STP解析',
|
||||||
|
generate_mesh: '网格生成',
|
||||||
|
generate_cavity: '分模与型腔生成',
|
||||||
|
build_plan_result: '方案结果组装',
|
||||||
|
persist_artifacts: '结果持久化',
|
||||||
|
analyze_design: '几何分析',
|
||||||
|
verify_geometry: '几何验证',
|
||||||
|
generate_llm_report: 'LLM报告生成'
|
||||||
|
};
|
||||||
|
return stageNameMap[name] || name;
|
||||||
|
};
|
||||||
|
const formatTiming = (value) => `${Number(value || 0).toFixed(3)} s`;
|
||||||
|
|
||||||
const exportCAD = async (format) => {
|
const exportCAD = async (format) => {
|
||||||
try {
|
try {
|
||||||
const taskId = route.params.taskId;
|
const taskId = route.params.taskId;
|
||||||
@@ -1503,6 +1563,7 @@ const ResultView = {
|
|||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
task_id: taskId,
|
task_id: taskId,
|
||||||
|
scheme_id: selectedScheme.value?.scheme_id || state.selectedSchemeId,
|
||||||
formats: [format],
|
formats: [format],
|
||||||
components: ['cavity', 'core', 'parting_surface']
|
components: ['cavity', 'core', 'parting_surface']
|
||||||
})
|
})
|
||||||
@@ -1586,11 +1647,22 @@ const ResultView = {
|
|||||||
selectedKeyInfo,
|
selectedKeyInfo,
|
||||||
selectedHtmlFile,
|
selectedHtmlFile,
|
||||||
selectedDfmViolations,
|
selectedDfmViolations,
|
||||||
|
selectedInjectionSystem,
|
||||||
|
stageTimingEntries,
|
||||||
|
getWallThicknessSummary,
|
||||||
|
countFeaturesByTypes,
|
||||||
|
getUndercutCount,
|
||||||
|
formatStageName,
|
||||||
|
formatTiming,
|
||||||
|
primaryRecommendation,
|
||||||
|
sortedRecommendations,
|
||||||
|
selectedDfmCount,
|
||||||
|
selectedCavityCount,
|
||||||
|
selectedRiskLabel,
|
||||||
formatFileSize,
|
formatFileSize,
|
||||||
formatDateTime,
|
formatDateTime,
|
||||||
formatNumber,
|
formatNumber,
|
||||||
getPriorityText,
|
getPriorityText,
|
||||||
isFoamMaterial,
|
|
||||||
exportCAD,
|
exportCAD,
|
||||||
generateCamPlan,
|
generateCamPlan,
|
||||||
camSteelOptions,
|
camSteelOptions,
|
||||||
@@ -1623,28 +1695,90 @@ const ResultView = {
|
|||||||
<span :class="['badge', state.task.status === 'completed' ? 'badge-success' : 'badge-error']">
|
<span :class="['badge', state.task.status === 'completed' ? 'badge-success' : 'badge-error']">
|
||||||
{{ state.task.status }}
|
{{ state.task.status }}
|
||||||
</span>
|
</span>
|
||||||
<div v-if="state.task.status === 'completed'" class="export-buttons">
|
</div>
|
||||||
<button class="btn-sm btn-primary" :disabled="state.camLoading" @click="generateCamPlan()" title="基于当前选中方案生成CAM工艺计划">
|
|
||||||
{{ state.camLoading ? '生成中...' : '生成 CAM 计划' }}
|
<div class="result-grid result-priority-grid">
|
||||||
</button>
|
<div class="result-card result-card-highlight">
|
||||||
<button class="btn-sm btn-primary" @click="exportCAD('step')" title="导出STEP格式(UG/FreeCAD/SolidWorks通用)">
|
<div class="summary-header">
|
||||||
导出 STEP
|
<h3>推荐方案</h3>
|
||||||
</button>
|
<span class="badge badge-info">{{ selectedScheme?.title || selectedScheme?.scheme_id || '方案待定' }}</span>
|
||||||
<button class="btn-sm btn-secondary" @click="exportCAD('stl')" title="导出STL网格格式(3D打印预览)">
|
</div>
|
||||||
导出 STL
|
<div class="info-list">
|
||||||
</button>
|
<div class="info-item">
|
||||||
<button class="btn-sm btn-secondary" @click="exportCAD('iges')" title="导出IGES格式(兼容旧系统)">
|
<span class="info-label">分型方向</span>
|
||||||
导出 IGES
|
<span class="info-value">{{ formatSchemeDirection(selectedScheme) }}</span>
|
||||||
</button>
|
</div>
|
||||||
<button class="btn-sm btn-secondary" @click="exportCAD('brep')" title="导出BRep格式(FreeCAD原生)">
|
<div class="info-item">
|
||||||
导出 BRep
|
<span class="info-label">分型面位置</span>
|
||||||
</button>
|
<span class="info-value">{{ selectedScheme?.offset_label || '中面' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="info-label">方案总分</span>
|
||||||
|
<span class="info-value">{{ formatNumber(selectedScheme?.score || 0) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="info-label">方案可信度</span>
|
||||||
|
<span class="info-value">{{ formatNumber(selectedScheme?.confidence_score || 0) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="info-label">主要理由</span>
|
||||||
|
<span class="info-value">{{ selectedScheme?.summary || primaryRecommendation?.description || '基于当前几何与制造约束自动推荐' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="info-label">高优先级建议</span>
|
||||||
|
<span class="info-value">{{ primaryRecommendation?.description || '未发现高优先级工艺建议' }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="result-card result-card-highlight">
|
||||||
|
<div class="summary-header">
|
||||||
|
<h3>关键操作</h3>
|
||||||
|
<span :class="['badge', selectedRiskLabel === '高风险' ? 'badge-error' : selectedRiskLabel === '中风险' ? 'badge-warning' : 'badge-success']">
|
||||||
|
{{ selectedRiskLabel }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-list">
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="info-label">DFM 风险数</span>
|
||||||
|
<span class="info-value">{{ selectedDfmCount }} 项</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="info-label">推荐型腔数</span>
|
||||||
|
<span class="info-value">{{ selectedCavityCount }} 腔</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="info-label">预计成型周期</span>
|
||||||
|
<span class="info-value">{{ selectedInjectionSystem?.overall_assessment?.estimated_cycle_time || 'N/A' }} s</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="info-label">下一步</span>
|
||||||
|
<span class="info-value">{{ selectedDfmCount ? '先处理 DFM 风险,再确认导出或 CAM' : '可进入 3D 复核、导出与 CAM 准备' }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="state.task.status === 'completed'" class="export-buttons export-buttons-block">
|
||||||
|
<button class="btn-sm btn-primary" @click="exportCAD('step')" title="导出STEP格式(UG/FreeCAD/SolidWorks通用)">
|
||||||
|
导出 STEP
|
||||||
|
</button>
|
||||||
|
<button class="btn-sm btn-secondary" @click="exportCAD('iges')" title="导出IGES格式(兼容旧系统)">
|
||||||
|
导出 IGES
|
||||||
|
</button>
|
||||||
|
<button class="btn-sm btn-secondary" @click="exportCAD('stl')" title="导出STL网格格式(3D打印预览)">
|
||||||
|
导出 STL
|
||||||
|
</button>
|
||||||
|
<button class="btn-sm btn-secondary" @click="exportCAD('brep')" title="导出BRep格式(FreeCAD原生)">
|
||||||
|
导出 BRep
|
||||||
|
</button>
|
||||||
|
<button class="btn-sm btn-primary" :disabled="state.camLoading" @click="generateCamPlan()" title="基于当前选中方案生成CAM工艺计划">
|
||||||
|
{{ state.camLoading ? '生成中...' : '生成 CAM 计划' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="result-grid">
|
<div class="result-grid compact-metrics-grid">
|
||||||
<div class="result-card">
|
<div class="result-card">
|
||||||
<h3>文件信息</h3>
|
<h3>任务信息</h3>
|
||||||
<div class="info-list">
|
<div class="info-list">
|
||||||
<div class="info-item">
|
<div class="info-item">
|
||||||
<span class="info-label">文件大小</span>
|
<span class="info-label">文件大小</span>
|
||||||
@@ -1662,7 +1796,7 @@ const ResultView = {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="result-card">
|
<div class="result-card">
|
||||||
<h3>几何数据</h3>
|
<h3>几何概览</h3>
|
||||||
<div class="info-list">
|
<div class="info-list">
|
||||||
<div class="info-item" v-if="state.task.mesh_summary">
|
<div class="info-item" v-if="state.task.mesh_summary">
|
||||||
<span class="info-label">顶点数</span>
|
<span class="info-label">顶点数</span>
|
||||||
@@ -1690,6 +1824,7 @@ const ResultView = {
|
|||||||
|
|
||||||
<div v-if="candidateSchemes.length" class="viewer-section">
|
<div v-if="candidateSchemes.length" class="viewer-section">
|
||||||
<h3>候选分模方案</h3>
|
<h3>候选分模方案</h3>
|
||||||
|
<div class="inline-note">先切换并确认推荐方案,再进入后续工程判断、导出与 CAM。</div>
|
||||||
<div :class="['result-grid', hasSingleScheme ? 'single-scheme-grid' : '']">
|
<div :class="['result-grid', hasSingleScheme ? 'single-scheme-grid' : '']">
|
||||||
<div
|
<div
|
||||||
v-for="scheme in candidateSchemes"
|
v-for="scheme in candidateSchemes"
|
||||||
@@ -1765,9 +1900,9 @@ const ResultView = {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="candidateSchemes.length > 1" class="viewer-section">
|
<details v-if="candidateSchemes.length > 1" class="viewer-section diagnostic-panel">
|
||||||
<h3>方案对比</h3>
|
<summary>方案对比</summary>
|
||||||
<div class="result-card full-width">
|
<div class="result-card full-width diagnostic-panel-body">
|
||||||
<table class="data-table">
|
<table class="data-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -1801,13 +1936,13 @@ const ResultView = {
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</details>
|
||||||
|
|
||||||
<div v-if="selectedCavityData || selectedKeyInfo" class="viewer-section">
|
<div v-if="selectedCavityData || selectedKeyInfo" class="viewer-section">
|
||||||
<h3>模具信息</h3>
|
<h3>工程摘要</h3>
|
||||||
<div class="result-grid">
|
<div class="result-grid">
|
||||||
<div class="result-card">
|
<div class="result-card">
|
||||||
<h4>基本信息</h4>
|
<h4>方案与零件</h4>
|
||||||
<div class="info-list">
|
<div class="info-list">
|
||||||
<div class="info-item">
|
<div class="info-item">
|
||||||
<span class="info-label">零件名称</span>
|
<span class="info-label">零件名称</span>
|
||||||
@@ -1824,11 +1959,44 @@ const ResultView = {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="result-card" v-if="selectedCavityData?.mold_cavities || selectedKeyInfo?.mold_cavities">
|
<div class="result-card" v-if="selectedCavityData?.mold_cavities || selectedKeyInfo?.mold_cavities">
|
||||||
<h4>型腔信息</h4>
|
<h4>型腔与锁模</h4>
|
||||||
<div class="info-list">
|
<div class="info-list">
|
||||||
<div class="info-item">
|
<div class="info-item">
|
||||||
<span class="info-label">型腔数量</span>
|
<span class="info-label">型腔数量</span>
|
||||||
<span class="info-value">{{ Object.keys(selectedCavityData?.mold_cavities || selectedKeyInfo?.mold_cavities || {}).length }}</span>
|
<span class="info-value">{{ selectedCavityCount }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="info-label">预估锁模力</span>
|
||||||
|
<span class="info-value">{{ selectedCavityData?.manufacturing_info?.estimated_clamping_force || '自动计算' }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="result-card" v-if="selectedInjectionSystem">
|
||||||
|
<h4>注塑模系统</h4>
|
||||||
|
<div class="info-list">
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="info-label">预计成型周期</span>
|
||||||
|
<span class="info-value">{{ selectedInjectionSystem?.overall_assessment?.estimated_cycle_time || 'N/A' }} s</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="info-label">冷却时间</span>
|
||||||
|
<span class="info-value">{{ selectedInjectionSystem?.cooling?.cooling_time || 'N/A' }} s</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="info-label">水路数量</span>
|
||||||
|
<span class="info-value">{{ selectedInjectionSystem?.cooling?.thermal_check?.channel_count || 0 }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="info-label">冷却流量</span>
|
||||||
|
<span class="info-value">{{ selectedInjectionSystem?.cooling?.flow_rate?.flow_rate_lpm || 'N/A' }} L/min</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="info-label">浇口类型</span>
|
||||||
|
<span class="info-value">{{ selectedInjectionSystem?.gating?.gate_type || 'N/A' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="info-item">
|
||||||
|
<span class="info-label">流道形式</span>
|
||||||
|
<span class="info-value">{{ selectedInjectionSystem?.gating?.runner?.type || 'N/A' }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1861,8 +2029,31 @@ const ResultView = {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div v-if="selectedHtmlFile" class="viewer-section">
|
||||||
|
<h3>3D 预览</h3>
|
||||||
|
<div v-if="state.previewStatus === 'error'" class="inline-alert inline-alert-warning" style="margin-bottom: var(--space-3);">
|
||||||
|
<div class="inline-alert-title">预览加载失败</div>
|
||||||
|
<div class="inline-alert-message">HTML 已生成但加载异常,请检查该链接是否可访问:{{ selectedHtmlFile }}</div>
|
||||||
|
</div>
|
||||||
|
<iframe
|
||||||
|
:key="selectedHtmlFile"
|
||||||
|
:src="selectedHtmlFile"
|
||||||
|
class="viewer-frame"
|
||||||
|
@load="onPreviewLoad"
|
||||||
|
@error="onPreviewError"
|
||||||
|
></iframe>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="viewer-section">
|
||||||
|
<h3>3D 预览</h3>
|
||||||
|
<div class="inline-alert inline-alert-warning">
|
||||||
|
<div class="inline-alert-title">预览未生成</div>
|
||||||
|
<div class="inline-alert-message">当前任务没有返回 HTML 预览链接,属于未生成状态。</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="viewer-section">
|
<div class="viewer-section">
|
||||||
<h3>CAM 准备包</h3>
|
<h3>导出与 CAM</h3>
|
||||||
<div class="result-card" style="margin-bottom: var(--space-4);">
|
<div class="result-card" style="margin-bottom: var(--space-4);">
|
||||||
<h4>CAM 参数</h4>
|
<h4>CAM 参数</h4>
|
||||||
<div class="info-list">
|
<div class="info-list">
|
||||||
@@ -1973,62 +2164,9 @@ const ResultView = {
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="selectedHtmlFile" class="viewer-section">
|
|
||||||
<h3>3D 预览</h3>
|
|
||||||
<div v-if="state.previewStatus === 'error'" class="inline-alert inline-alert-warning" style="margin-bottom: var(--space-3);">
|
|
||||||
<div class="inline-alert-title">预览加载失败</div>
|
|
||||||
<div class="inline-alert-message">HTML 已生成但加载异常,请检查该链接是否可访问:{{ selectedHtmlFile }}</div>
|
|
||||||
</div>
|
|
||||||
<iframe
|
|
||||||
:key="selectedHtmlFile"
|
|
||||||
:src="selectedHtmlFile"
|
|
||||||
class="viewer-frame"
|
|
||||||
@load="onPreviewLoad"
|
|
||||||
@error="onPreviewError"
|
|
||||||
></iframe>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-else class="viewer-section">
|
|
||||||
<h3>3D 预览</h3>
|
|
||||||
<div class="inline-alert inline-alert-warning">
|
|
||||||
<div class="inline-alert-title">预览未生成</div>
|
|
||||||
<div class="inline-alert-message">当前任务没有返回 HTML 预览链接,属于未生成状态。</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="state.task.analysis_result" class="viewer-section">
|
<div v-if="state.task.analysis_result" class="viewer-section">
|
||||||
<h3>分析结果详情</h3>
|
<h3>分析结果详情</h3>
|
||||||
|
|
||||||
<!-- FreeCAD 验证结果 -->
|
|
||||||
<div v-if="state.task.verification" class="verification-section">
|
|
||||||
<div class="verification-summary">
|
|
||||||
<div class="summary-header">
|
|
||||||
<h4>FreeCAD 几何验证</h4>
|
|
||||||
<span :class="['badge', state.task.verification.status === 'passed' ? 'badge-success' : state.task.verification.status === 'failed' ? 'badge-error' : state.task.verification.status === 'error' ? 'badge-warning' : 'badge-info']">
|
|
||||||
{{ state.task.verification.status === 'passed' ? '验证通过' : state.task.verification.status === 'failed' ? '验证失败' : state.task.verification.status === 'error' ? '验证错误' : state.task.verification.status === 'skipped' ? '已跳过' : '未知' }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="verification-details" v-if="state.task.verification.comparison && (state.task.verification.comparison.volume || state.task.verification.comparison.surface_area)">
|
|
||||||
<div class="comparison-grid">
|
|
||||||
<div class="comparison-item" v-if="state.task.verification.comparison.volume">
|
|
||||||
<span class="comparison-label">体积差异</span>
|
|
||||||
<span class="comparison-value" :class="{'text-error': state.task.verification.comparison.volume.difference_percent > 1, 'text-success': state.task.verification.comparison.volume.difference_percent <= 1}">
|
|
||||||
{{ state.task.verification.comparison.volume.difference_percent?.toFixed(4) || 0 }}%
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="comparison-item" v-if="state.task.verification.comparison.surface_area">
|
|
||||||
<span class="comparison-label">表面积差异</span>
|
|
||||||
<span class="comparison-value" :class="{'text-error': state.task.verification.comparison.surface_area.difference_percent > 2, 'text-success': state.task.verification.comparison.surface_area.difference_percent <= 2}">
|
|
||||||
{{ state.task.verification.comparison.surface_area.difference_percent?.toFixed(4) || 0 }}%
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 分析摘要 -->
|
|
||||||
<div class="analysis-summary" v-if="state.task.analysis_result.analysis_summary">
|
<div class="analysis-summary" v-if="state.task.analysis_result.analysis_summary">
|
||||||
<div class="summary-header">
|
<div class="summary-header">
|
||||||
<h4>分析摘要</h4>
|
<h4>分析摘要</h4>
|
||||||
@@ -2038,7 +2176,7 @@ const ResultView = {
|
|||||||
{{ state.task.analysis_result.analysis_summary }}
|
{{ state.task.analysis_result.analysis_summary }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="result-grid">
|
<div class="result-grid">
|
||||||
<div class="result-card full-width">
|
<div class="result-card full-width">
|
||||||
<h4>1. 产品特征识别</h4>
|
<h4>1. 产品特征识别</h4>
|
||||||
@@ -2052,19 +2190,19 @@ const ResultView = {
|
|||||||
</div>
|
</div>
|
||||||
<div class="info-item">
|
<div class="info-item">
|
||||||
<span class="info-label">壁厚分布</span>
|
<span class="info-label">壁厚分布</span>
|
||||||
<span class="info-value">{{ state.task.analysis_result.detected_features?.find(f => f.feature_type === 'wall_thickness')?.description || '待分析' }}</span>
|
<span class="info-value">{{ getWallThicknessSummary() }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="info-item">
|
<div class="info-item">
|
||||||
<span class="info-label">加强筋位置和密度</span>
|
<span class="info-label">加强筋位置和密度</span>
|
||||||
<span class="info-value">{{ state.task.analysis_result.detected_features?.filter(f => f.feature_type === 'rib').length || 0 }} 个加强筋</span>
|
<span class="info-value">{{ countFeaturesByTypes('rib_structure') || 0 }} 个加强筋</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="info-item">
|
<div class="info-item">
|
||||||
<span class="info-label">孔洞和凹槽位置</span>
|
<span class="info-label">柱位和功能特征</span>
|
||||||
<span class="info-value">{{ state.task.analysis_result.detected_features?.filter(f => f.feature_type === 'hole' || f.feature_type === 'pocket').length || 0 }} 个孔洞/凹槽</span>
|
<span class="info-value">{{ countFeaturesByTypes('boss_feature') || 0 }} 个柱位/功能特征</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="info-item">
|
<div class="info-item">
|
||||||
<span class="info-label">倒扣区域检测</span>
|
<span class="info-label">倒扣区域检测</span>
|
||||||
<span class="info-value">{{ state.task.analysis_result.detected_features?.filter(f => f.feature_type === 'undercut').length || 0 }} 个倒扣区域</span>
|
<span class="info-value">{{ getUndercutCount() }} 个倒扣区域</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="info-item">
|
<div class="info-item">
|
||||||
<span class="info-label">对称性分析</span>
|
<span class="info-label">对称性分析</span>
|
||||||
@@ -2081,27 +2219,27 @@ const ResultView = {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="result-card full-width">
|
<div class="result-card full-width">
|
||||||
<h4>2. 泡沫包装设计决策</h4>
|
<h4>2. 注塑模设计决策</h4>
|
||||||
<div class="info-list">
|
<div class="info-list">
|
||||||
<div class="info-item">
|
<div class="info-item">
|
||||||
<span class="info-label">泡沫厚度建议</span>
|
<span class="info-label">高优先级工艺建议</span>
|
||||||
<span class="info-value">{{ state.task.analysis_result.design_recommendations?.find(r => r.priority === 'high')?.recommendation || '根据产品重量和脆弱程度自动计算' }}</span>
|
<span class="info-value">{{ state.task.analysis_result.design_recommendations?.find(r => r.priority === 'high')?.description || '根据几何分析自动生成' }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="info-item">
|
<div class="info-item">
|
||||||
<span class="info-label">加强筋布局</span>
|
<span class="info-label">加强筋布局</span>
|
||||||
<span class="info-value">基于产品薄弱区域自动布置</span>
|
<span class="info-value">{{ countFeaturesByTypes('rib_structure') ? '已识别加强筋区域,建议校核厚度和脱模方向' : '当前未识别明显加强筋特征' }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="info-item">
|
<div class="info-item">
|
||||||
<span class="info-label">取手槽位置</span>
|
<span class="info-label">浇口与进胶提示</span>
|
||||||
<span class="info-value">基于重心位置:{{ state.task.analysis_result.geometry_data?.center_of_mass ? '自动优化' : '手动设置' }}</span>
|
<span class="info-value">{{ state.task.analysis_result.design_recommendations?.find(r => r.type === 'wall_thickness' || r.type === 'draft_angle')?.reason || '建议结合主分型方向确认浇口位置' }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="info-item">
|
<div class="info-item">
|
||||||
<span class="info-label">通风孔位置</span>
|
<span class="info-label">冷却设计提示</span>
|
||||||
<span class="info-value">防止真空吸附:建议在产品最大平面区域设置通风孔</span>
|
<span class="info-value">{{ getWallThicknessSummary() !== '待分析' ? '建议按壁厚分布校核冷却均匀性' : '需先完成壁厚分析后再校核冷却' }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="info-item">
|
<div class="info-item">
|
||||||
<span class="info-label">定位结构设计</span>
|
<span class="info-label">定位结构设计</span>
|
||||||
<span class="info-value">{{ state.task.analysis_result.detected_features?.filter(f => f.feature_type === '定位').length || 0 }} 个定位特征</span>
|
<span class="info-value">{{ countFeaturesByTypes('boss_feature') || 0 }} 个柱位/定位相关特征</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="info-item">
|
<div class="info-item">
|
||||||
<span class="info-label">分型面选择</span>
|
<span class="info-label">分型面选择</span>
|
||||||
@@ -2109,13 +2247,10 @@ const ResultView = {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 设计建议列表 -->
|
<!-- 设计建议列表 -->
|
||||||
<div class="recommendations-section" v-if="state.task.analysis_result.design_recommendations?.length">
|
<div class="recommendations-section" v-if="sortedRecommendations.length">
|
||||||
<h5>详细设计建议</h5>
|
<h5>详细设计建议</h5>
|
||||||
<div class="recommendations-list">
|
<div class="recommendations-list">
|
||||||
<div v-for="rec in state.task.analysis_result.design_recommendations.sort((a, b) => {
|
<div v-for="rec in sortedRecommendations" :key="rec.type" class="recommendation-item" :class="rec.priority">
|
||||||
const priorityOrder = { 'critical': 0, 'high': 1, 'medium': 2, 'low': 3 };
|
|
||||||
return priorityOrder[a.priority] - priorityOrder[b.priority];
|
|
||||||
})" :key="rec.type" class="recommendation-item" :class="rec.priority">
|
|
||||||
<span class="priority-badge" :class="rec.priority">{{ getPriorityText(rec.priority) }}</span>
|
<span class="priority-badge" :class="rec.priority">{{ getPriorityText(rec.priority) }}</span>
|
||||||
<span class="recommendation-text">{{ rec.description }}</span>
|
<span class="recommendation-text">{{ rec.description }}</span>
|
||||||
<span class="recommendation-reason" v-if="rec.reason">({{ rec.reason }})</span>
|
<span class="recommendation-reason" v-if="rec.reason">({{ rec.reason }})</span>
|
||||||
@@ -2179,6 +2314,65 @@ const ResultView = {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<details class="viewer-section diagnostic-panel" v-if="stageTimingEntries.length || state.task.verification || state.task.llm_report">
|
||||||
|
<summary>诊断信息</summary>
|
||||||
|
<div class="diagnostic-panel-body">
|
||||||
|
<div class="result-card full-width" v-if="stageTimingEntries.length">
|
||||||
|
<h4>处理耗时</h4>
|
||||||
|
<table class="data-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>阶段</th>
|
||||||
|
<th>耗时</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="[stage, duration] in stageTimingEntries" :key="'timing-' + stage">
|
||||||
|
<td>{{ formatStageName(stage) }}</td>
|
||||||
|
<td>{{ formatTiming(duration) }}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="state.task.verification" class="verification-section">
|
||||||
|
<div class="verification-summary">
|
||||||
|
<div class="summary-header">
|
||||||
|
<h4>FreeCAD 几何验证</h4>
|
||||||
|
<span :class="['badge', state.task.verification.status === 'passed' ? 'badge-success' : state.task.verification.status === 'failed' ? 'badge-error' : state.task.verification.status === 'error' ? 'badge-warning' : 'badge-info']">
|
||||||
|
{{ state.task.verification.status === 'passed' ? '验证通过' : state.task.verification.status === 'failed' ? '验证失败' : state.task.verification.status === 'error' ? '验证错误' : state.task.verification.status === 'skipped' ? '已跳过' : '未知' }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="verification-details" v-if="state.task.verification.comparison && (state.task.verification.comparison.volume || state.task.verification.comparison.surface_area)">
|
||||||
|
<div class="comparison-grid">
|
||||||
|
<div class="comparison-item" v-if="state.task.verification.comparison.volume">
|
||||||
|
<span class="comparison-label">体积差异</span>
|
||||||
|
<span class="comparison-value" :class="{'text-error': state.task.verification.comparison.volume.difference_percent > 1, 'text-success': state.task.verification.comparison.volume.difference_percent <= 1}">
|
||||||
|
{{ state.task.verification.comparison.volume.difference_percent?.toFixed(4) || 0 }}%
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="comparison-item" v-if="state.task.verification.comparison.surface_area">
|
||||||
|
<span class="comparison-label">表面积差异</span>
|
||||||
|
<span class="comparison-value" :class="{'text-error': state.task.verification.comparison.surface_area.difference_percent > 2, 'text-success': state.task.verification.comparison.surface_area.difference_percent <= 2}">
|
||||||
|
{{ state.task.verification.comparison.surface_area.difference_percent?.toFixed(4) || 0 }}%
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="analysis-summary" v-if="state.task.llm_report">
|
||||||
|
<div class="summary-header">
|
||||||
|
<h4>LLM 设计报告</h4>
|
||||||
|
</div>
|
||||||
|
<div class="summary-content">
|
||||||
|
<pre style="white-space: pre-wrap; margin: 0;">{{ state.task.llm_report }}</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
pythonocc-core>=7.7.0
|
||||||
|
trimesh>=3.21.0
|
||||||
|
numpy>=1.24.0
|
||||||
|
scipy>=1.10.0
|
||||||
|
pyvista>=0.38.0
|
||||||
|
fastapi>=0.100.0
|
||||||
|
uvicorn[standard]>=0.22.0
|
||||||
|
pydantic>=2.0.0
|
||||||
|
python-multipart>=0.0.6
|
||||||
|
sqlalchemy>=2.0.0
|
||||||
|
psycopg2-binary>=2.9.0
|
||||||
|
asyncpg>=0.28.0
|
||||||
|
alembic>=1.11.0
|
||||||
|
minio>=7.1.0
|
||||||
|
aiohttp>=3.8.0
|
||||||
|
kafka-python>=2.0.2
|
||||||
|
redis>=4.5.0
|
||||||
|
python-jose[cryptography]>=3.3.0
|
||||||
|
bcrypt>=4.0.0
|
||||||
|
passlib>=1.7.4
|
||||||
|
email-validator>=2.0.0
|
||||||
|
aiofiles>=23.0.0
|
||||||
|
orjson>=3.9.0
|
||||||
|
python-dotenv>=1.0.0
|
||||||
|
jinja2>=3.1.0
|
||||||
|
pyyaml>=6.0
|
||||||
|
python-dateutil>=2.8.0
|
||||||
|
loguru>=0.7.0
|
||||||
|
pytest>=7.0.0
|
||||||
|
pytest-asyncio>=0.21.0
|
||||||
|
httpx>=0.24.0
|
||||||
|
aiosqlite>=0.19.0
|
||||||
|
black>=23.0.0
|
||||||
|
flake8>=6.0.0
|
||||||
|
mypy>=1.0.0
|
||||||
@@ -0,0 +1,265 @@
|
|||||||
|
import io
|
||||||
|
import importlib.util
|
||||||
|
import sys
|
||||||
|
import types
|
||||||
|
from pathlib import Path
|
||||||
|
from types import SimpleNamespace
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from fastapi import FastAPI, UploadFile
|
||||||
|
from httpx import ASGITransport, AsyncClient
|
||||||
|
|
||||||
|
from services.calculation_service import CalculationService
|
||||||
|
from utils.file_handler import FileHandler
|
||||||
|
|
||||||
|
|
||||||
|
VALID_STEP_BYTES = (
|
||||||
|
b"ISO-10303-21;\n"
|
||||||
|
b"HEADER;\n"
|
||||||
|
b"FILE_DESCRIPTION(('STEP AP214'),'1');\n"
|
||||||
|
b"ENDSEC;\n"
|
||||||
|
b"DATA;\n"
|
||||||
|
b"ENDSEC;\n"
|
||||||
|
b"END-ISO-10303-21;\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _load_module_from_path(module_name: str, file_path: str, stub_modules: dict[str, object]):
|
||||||
|
originals = {}
|
||||||
|
for name, module in stub_modules.items():
|
||||||
|
originals[name] = sys.modules.get(name)
|
||||||
|
sys.modules[name] = module
|
||||||
|
|
||||||
|
try:
|
||||||
|
spec = importlib.util.spec_from_file_location(module_name, file_path)
|
||||||
|
module = importlib.util.module_from_spec(spec)
|
||||||
|
assert spec.loader is not None
|
||||||
|
spec.loader.exec_module(module)
|
||||||
|
return module
|
||||||
|
finally:
|
||||||
|
for name, original in originals.items():
|
||||||
|
if original is None:
|
||||||
|
sys.modules.pop(name, None)
|
||||||
|
else:
|
||||||
|
sys.modules[name] = original
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_file_handler_sanitizes_step_filename(tmp_path):
|
||||||
|
handler = FileHandler(upload_dir=str(tmp_path))
|
||||||
|
upload = UploadFile(filename="../../bad name?.step", file=io.BytesIO(VALID_STEP_BYTES))
|
||||||
|
|
||||||
|
file_path, file_size, meta = await handler.save_uploaded_file(upload)
|
||||||
|
|
||||||
|
assert file_path.exists()
|
||||||
|
assert file_size == len(VALID_STEP_BYTES)
|
||||||
|
assert file_path.parent == tmp_path
|
||||||
|
assert ".." not in file_path.name
|
||||||
|
assert meta["safe_original_name"] == "bad_name.step"
|
||||||
|
assert len(meta["sha256"]) == 64
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_file_handler_rejects_invalid_step_content(tmp_path):
|
||||||
|
handler = FileHandler(upload_dir=str(tmp_path))
|
||||||
|
upload = UploadFile(filename="fake.step", file=io.BytesIO(b"not-a-step"))
|
||||||
|
|
||||||
|
with pytest.raises(ValueError, match="不是有效的 STP/STEP 数据"):
|
||||||
|
await handler.save_uploaded_file(upload)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_export_route_requires_cached_shapes(monkeypatch):
|
||||||
|
fake_processing_service = types.ModuleType("services.processing_service")
|
||||||
|
fake_processing_service.processing_service = SimpleNamespace(
|
||||||
|
get_export_shapes=lambda task_id, scheme_id=None: None,
|
||||||
|
)
|
||||||
|
fake_auth_service = types.ModuleType("services.auth_service")
|
||||||
|
async def fake_current_user():
|
||||||
|
return SimpleNamespace(id=1)
|
||||||
|
fake_auth_service.get_current_active_user = fake_current_user
|
||||||
|
|
||||||
|
fake_redis_task_manager = types.ModuleType("services.redis_task_manager")
|
||||||
|
fake_redis_task_manager.redis_task_manager = SimpleNamespace(get_task=None)
|
||||||
|
|
||||||
|
fake_models_database = types.ModuleType("models.database")
|
||||||
|
fake_models_database.User = SimpleNamespace
|
||||||
|
|
||||||
|
advanced_router = _load_module_from_path(
|
||||||
|
"temp_advanced_router",
|
||||||
|
"d:\\Project\\geMoldInsight\\src\\api\\v1\\advanced_router.py",
|
||||||
|
{
|
||||||
|
"services.processing_service": fake_processing_service,
|
||||||
|
"services.auth_service": fake_auth_service,
|
||||||
|
"services.redis_task_manager": fake_redis_task_manager,
|
||||||
|
"models.database": fake_models_database,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
app = FastAPI()
|
||||||
|
app.include_router(advanced_router.router)
|
||||||
|
app.dependency_overrides[advanced_router.get_current_active_user] = lambda: SimpleNamespace(id=1)
|
||||||
|
|
||||||
|
async def fake_get_task_data(task_id):
|
||||||
|
return {
|
||||||
|
"task_id": task_id,
|
||||||
|
"filename": "demo.step",
|
||||||
|
"best_scheme_id": "scheme_1",
|
||||||
|
}
|
||||||
|
|
||||||
|
monkeypatch.setattr(advanced_router, "_get_task_data", fake_get_task_data)
|
||||||
|
|
||||||
|
transport = ASGITransport(app=app)
|
||||||
|
async with AsyncClient(transport=transport, base_url="http://test") as client:
|
||||||
|
response = await client.post(
|
||||||
|
"/export-mold",
|
||||||
|
json={"task_id": "task-1", "scheme_id": "scheme_1", "formats": ["step"]},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 409
|
||||||
|
assert "导出缓存已失效" in response.json()["detail"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_calculation_service_attaches_injection_system_summary():
|
||||||
|
plan_result = {
|
||||||
|
"best_scheme_id": "scheme_1",
|
||||||
|
"candidate_schemes": [
|
||||||
|
{
|
||||||
|
"scheme_id": "scheme_1",
|
||||||
|
"cavity_data": {
|
||||||
|
"product_analysis": {
|
||||||
|
"bounding_box": {"dimensions": [100, 80, 30]}
|
||||||
|
},
|
||||||
|
"manufacturing_info": {
|
||||||
|
"estimated_mold_size": {"length": 200, "width": 180, "height": 110}
|
||||||
|
},
|
||||||
|
"mold_cavities": {"cavity_count": 1},
|
||||||
|
},
|
||||||
|
"key_info": {},
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
result = CalculationService.attach_injection_system_summaries(plan_result, "ABS")
|
||||||
|
best_scheme = result["candidate_schemes"][0]
|
||||||
|
|
||||||
|
assert "injection_system" in best_scheme["cavity_data"]
|
||||||
|
assert best_scheme["cavity_data"]["manufacturing_info"]["cooling_summary"]["channel_count"] >= 1
|
||||||
|
assert best_scheme["cavity_data"]["manufacturing_info"]["gating_summary"]["gate_type"] in {"auto", "side", "center", "submarine", "fan"}
|
||||||
|
assert "injection_system" in result
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_upload_route_persists_process_parameters(monkeypatch, tmp_path):
|
||||||
|
captured = {}
|
||||||
|
|
||||||
|
class DummyStorageService:
|
||||||
|
async def save_stp_file(self, session, file_path, original_filename, user_id):
|
||||||
|
captured["saved_file"] = {
|
||||||
|
"file_path": str(file_path),
|
||||||
|
"original_filename": original_filename,
|
||||||
|
"user_id": user_id,
|
||||||
|
}
|
||||||
|
return SimpleNamespace(id=42)
|
||||||
|
|
||||||
|
async def create_processing_task(self, session, task_id, stp_file_id, task_type="stp_parsing", parameters=None):
|
||||||
|
captured["task"] = {
|
||||||
|
"task_id": task_id,
|
||||||
|
"stp_file_id": stp_file_id,
|
||||||
|
"task_type": task_type,
|
||||||
|
"parameters": parameters,
|
||||||
|
}
|
||||||
|
|
||||||
|
async def fake_save_uploaded_file(file):
|
||||||
|
target = Path(tmp_path) / "cached_demo.step"
|
||||||
|
target.write_bytes(VALID_STEP_BYTES)
|
||||||
|
return target, len(VALID_STEP_BYTES), {
|
||||||
|
"safe_original_name": "demo.step",
|
||||||
|
"sha256": "a" * 64,
|
||||||
|
"original_filename": "demo.step",
|
||||||
|
"stored_filename": target.name,
|
||||||
|
}
|
||||||
|
|
||||||
|
async def fake_set_task(task_id, task_info):
|
||||||
|
captured["redis"] = {"task_id": task_id, "task_info": task_info}
|
||||||
|
|
||||||
|
async def fake_process_file_with_storage(task_id, file_path, stp_file_id, process_params):
|
||||||
|
captured["background"] = {
|
||||||
|
"task_id": task_id,
|
||||||
|
"file_path": str(file_path),
|
||||||
|
"stp_file_id": stp_file_id,
|
||||||
|
"process_params": process_params,
|
||||||
|
}
|
||||||
|
|
||||||
|
fake_processing_service_module = types.ModuleType("services.processing_service")
|
||||||
|
fake_processing_service_module.processing_service = SimpleNamespace(
|
||||||
|
process_file_with_storage=fake_process_file_with_storage,
|
||||||
|
)
|
||||||
|
|
||||||
|
fake_storage_module = types.ModuleType("services.storage_integration_rustfs")
|
||||||
|
fake_storage_module.StorageIntegrationService = lambda: DummyStorageService()
|
||||||
|
|
||||||
|
fake_redis_task_manager = types.ModuleType("services.redis_task_manager")
|
||||||
|
fake_redis_task_manager.redis_task_manager = SimpleNamespace(
|
||||||
|
set_task=fake_set_task,
|
||||||
|
)
|
||||||
|
|
||||||
|
fake_database_module = types.ModuleType("database.database")
|
||||||
|
async def override_get_db_session():
|
||||||
|
yield object()
|
||||||
|
fake_database_module.get_db_session = override_get_db_session
|
||||||
|
|
||||||
|
fake_auth_service = types.ModuleType("services.auth_service")
|
||||||
|
async def override_get_current_user():
|
||||||
|
return SimpleNamespace(id=7, username="tester")
|
||||||
|
fake_auth_service.get_current_active_user = override_get_current_user
|
||||||
|
|
||||||
|
fake_models_database = types.ModuleType("models.database")
|
||||||
|
fake_models_database.User = SimpleNamespace
|
||||||
|
|
||||||
|
upload_router = _load_module_from_path(
|
||||||
|
"temp_upload_router",
|
||||||
|
"d:\\Project\\geMoldInsight\\src\\api\\v1\\upload_router.py",
|
||||||
|
{
|
||||||
|
"services.processing_service": fake_processing_service_module,
|
||||||
|
"services.storage_integration_rustfs": fake_storage_module,
|
||||||
|
"services.redis_task_manager": fake_redis_task_manager,
|
||||||
|
"database.database": fake_database_module,
|
||||||
|
"services.auth_service": fake_auth_service,
|
||||||
|
"models.database": fake_models_database,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
monkeypatch.setattr(upload_router.file_handler, "save_uploaded_file", fake_save_uploaded_file)
|
||||||
|
|
||||||
|
app = FastAPI()
|
||||||
|
app.include_router(upload_router.router)
|
||||||
|
app.dependency_overrides[upload_router.get_db_session] = override_get_db_session
|
||||||
|
app.dependency_overrides[upload_router.get_current_active_user] = override_get_current_user
|
||||||
|
|
||||||
|
transport = ASGITransport(app=app)
|
||||||
|
async with AsyncClient(transport=transport, base_url="http://test") as client:
|
||||||
|
response = await client.post(
|
||||||
|
"/upload",
|
||||||
|
files={"file": ("demo.step", VALID_STEP_BYTES, "application/step")},
|
||||||
|
data={
|
||||||
|
"material": "ABS",
|
||||||
|
"draft_angle": "3.5",
|
||||||
|
"shrinkage_rate": "0.8",
|
||||||
|
"parting_precision": "0.05",
|
||||||
|
"cavity_match": "96",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status_code == 200
|
||||||
|
payload = response.json()
|
||||||
|
assert payload["parameters"] == {
|
||||||
|
"material": "ABS",
|
||||||
|
"draft_angle": 3.5,
|
||||||
|
"shrinkage_rate": 0.8,
|
||||||
|
"parting_precision": 0.05,
|
||||||
|
"cavity_match": 96,
|
||||||
|
}
|
||||||
|
assert captured["task"]["parameters"] == payload["parameters"]
|
||||||
|
assert captured["redis"]["task_info"]["parameters"] == payload["parameters"]
|
||||||
|
assert captured["background"]["process_params"] == payload["parameters"]
|
||||||
Reference in New Issue
Block a user