x
This commit is contained in:
@@ -5,6 +5,7 @@ from fastapi import APIRouter, Depends, HTTPException, Request
|
||||
|
||||
from services.auth_service import get_current_active_user
|
||||
from services.redis_task_manager import redis_task_manager
|
||||
from services.processing_service import processing_service
|
||||
from models.database import User
|
||||
from utils.logger import get_logger
|
||||
|
||||
@@ -281,6 +282,7 @@ async def export_mold_results(
|
||||
):
|
||||
body = await request.json()
|
||||
task_id = body.get("task_id")
|
||||
scheme_id = body.get("scheme_id")
|
||||
formats = body.get("formats", ["step", "stl"])
|
||||
components = body.get("components", ["cavity", "core"])
|
||||
|
||||
@@ -291,16 +293,17 @@ async def export_mold_results(
|
||||
if not task_data:
|
||||
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}")
|
||||
|
||||
if not cavity_shapes:
|
||||
file_path = task_data.get("file_path")
|
||||
if file_path and os.path.exists(str(file_path)):
|
||||
cavity_shapes = await _reparse_stp_for_export(str(file_path), task_data.get("material", "ABS"))
|
||||
|
||||
if not cavity_shapes:
|
||||
raise HTTPException(400, "该任务尚未完成模具生成或形状数据不可用")
|
||||
raise HTTPException(
|
||||
409,
|
||||
"导出缓存已失效或任务尚未完成,请重新分析后再导出以保证方案一致性",
|
||||
)
|
||||
|
||||
exporter = _get_cached("cad_exporter")
|
||||
if not exporter:
|
||||
@@ -352,17 +355,3 @@ async def get_export_recommendations(
|
||||
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
|
||||
from fastapi import APIRouter, UploadFile, File, HTTPException, BackgroundTasks, Depends
|
||||
from typing import Optional
|
||||
from fastapi import APIRouter, UploadFile, File, HTTPException, BackgroundTasks, Depends, Form
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
@@ -27,14 +26,25 @@ file_handler = FileHandler()
|
||||
async def upload_stp(
|
||||
background_tasks: BackgroundTasks,
|
||||
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),
|
||||
current_user: User = Depends(get_current_active_user)
|
||||
):
|
||||
"""上传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(
|
||||
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'}"
|
||||
)
|
||||
|
||||
@@ -44,7 +54,11 @@ async def upload_stp(
|
||||
|
||||
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}")
|
||||
|
||||
storage_service = StorageIntegrationService()
|
||||
@@ -52,12 +66,17 @@ async def upload_stp(
|
||||
stp_file = await storage_service.save_stp_file(
|
||||
session=db_session,
|
||||
file_path=file_path,
|
||||
original_filename=file.filename,
|
||||
original_filename=file_meta["safe_original_name"],
|
||||
user_id=current_user.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_id=task_id,
|
||||
@@ -67,11 +86,14 @@ async def upload_stp(
|
||||
file_size=file_size,
|
||||
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)
|
||||
|
||||
background_tasks.add_task(
|
||||
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}")
|
||||
|
||||
@@ -83,6 +105,8 @@ async def upload_stp(
|
||||
"filename": file.filename,
|
||||
"size": file_size,
|
||||
"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],
|
||||
is_foam_material: bool = False,
|
||||
max_schemes: int = 3,
|
||||
process_params: Optional[Dict[str, Any]] = None,
|
||||
) -> Dict[str, Any]:
|
||||
generator = self.aluminum_foam_generator if is_foam_material else self.mold_generator
|
||||
generator.set_material(material["name"])
|
||||
self._apply_process_params(generator, material, process_params)
|
||||
|
||||
analysis = generator._analyze_product_geometry(shape)
|
||||
analysis["axis_normal_stats"] = self._collect_axis_normal_stats(generator, shape)
|
||||
@@ -67,16 +69,20 @@ class MultiSchemeMoldPlanner:
|
||||
raise ValueError("未能生成任何可用分模方案")
|
||||
|
||||
scored_schemes = self.scheme_scorer.score_schemes(schemes)[:max_schemes]
|
||||
export_shapes = {}
|
||||
for idx, scheme in enumerate(scored_schemes, start=1):
|
||||
scheme["raw_scheme_id"] = scheme.get("scheme_id")
|
||||
scheme["scheme_id"] = f"scheme_{idx}"
|
||||
if scheme.get("cavity_data", {}).get("metadata") is not None:
|
||||
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]
|
||||
|
||||
return {
|
||||
"best_scheme_id": best_scheme["scheme_id"],
|
||||
"candidate_schemes": scored_schemes,
|
||||
"_export_shapes": export_shapes,
|
||||
"global_summary": {
|
||||
"scheme_count": len(scored_schemes),
|
||||
"recommended_reason": best_scheme.get("summary", ""),
|
||||
@@ -178,8 +184,26 @@ class MultiSchemeMoldPlanner:
|
||||
"side_actions": side_action_result,
|
||||
"cavity_data": cavity_data,
|
||||
"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(
|
||||
self,
|
||||
generator: Any,
|
||||
|
||||
+1
-6
@@ -74,15 +74,10 @@ async def log_requests(request: Request, call_next):
|
||||
status = response.status_code
|
||||
|
||||
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(
|
||||
f"[HTTP] {request.method} {request.url.path} -> {status} "
|
||||
f"({duration:.2f}s) "
|
||||
f"token={token_preview or 'none'}"
|
||||
f"client={request.client.host if request.client else 'unknown'}"
|
||||
)
|
||||
return response
|
||||
|
||||
|
||||
@@ -316,7 +316,7 @@ class CalculationService:
|
||||
file_path=file_path,
|
||||
cavity_mesh_data=None,
|
||||
)
|
||||
return {
|
||||
result = {
|
||||
"best_scheme_id": "scheme_1",
|
||||
"candidate_schemes": [
|
||||
{
|
||||
@@ -349,6 +349,8 @@ class CalculationService:
|
||||
"cavity_data": legacy,
|
||||
"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", [])
|
||||
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 {},
|
||||
"key_info": best_scheme.get("key_info", {}) if best_scheme else {},
|
||||
}
|
||||
cls.attach_injection_system_summaries(result, material["name"])
|
||||
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
|
||||
def get_best_scheme(plan_result: Optional[Dict[str, Any]]) -> Optional[Dict[str, Any]]:
|
||||
if not plan_result:
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"""STP 文件处理流程编排器 — 协调解析、网格生成、型腔生成、保存、验证"""
|
||||
|
||||
import asyncio
|
||||
import time
|
||||
import traceback
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
@@ -42,6 +43,7 @@ class ProcessingService:
|
||||
self.html_generator = HTMLGenerator()
|
||||
self.storage_service = StorageIntegrationService()
|
||||
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,
|
||||
file_path: str,
|
||||
stp_file_id: int,
|
||||
material: str = "ABS",
|
||||
process_params: Optional[Dict[str, Any]] = None,
|
||||
):
|
||||
"""处理文件的后台任务 — 使用独立数据库会话"""
|
||||
|
||||
@@ -65,7 +67,7 @@ class ProcessingService:
|
||||
try:
|
||||
await asyncio.wait_for(
|
||||
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,
|
||||
)
|
||||
@@ -96,29 +98,35 @@ class ProcessingService:
|
||||
file_path: str,
|
||||
stp_file_id: int,
|
||||
db_session: AsyncSession,
|
||||
material: str = "ABS",
|
||||
process_params: Optional[Dict[str, Any]] = None,
|
||||
):
|
||||
"""核心处理逻辑"""
|
||||
|
||||
try:
|
||||
logger.info(f"开始处理文件并生成模具型腔: {file_path}")
|
||||
process_params = self._normalize_process_params(process_params)
|
||||
stage_timings: Dict[str, float] = {}
|
||||
|
||||
# 1. 解析STP文件
|
||||
await self.storage_service.update_task_status(
|
||||
db_session, task_id, "processing", 20, "解析STP文件"
|
||||
)
|
||||
|
||||
stage_started = time.perf_counter()
|
||||
shape = self.stp_parser.load_step_file(Path(file_path))
|
||||
geometry_data = self.stp_parser.analyze_geometry(shape)
|
||||
stage_timings["parse_stp"] = round(time.perf_counter() - stage_started, 3)
|
||||
|
||||
# 2. 生成网格数据并持久化
|
||||
await self.storage_service.update_task_status(
|
||||
db_session, task_id, "processing", 30, "生成网格数据"
|
||||
)
|
||||
|
||||
stage_started = time.perf_counter()
|
||||
mesh_result = await self._step_generate_mesh(
|
||||
shape, geometry_data, file_path, db_session, stp_file_id, task_id
|
||||
)
|
||||
stage_timings["generate_mesh"] = round(time.perf_counter() - stage_started, 3)
|
||||
|
||||
# 3. 生成模具型腔
|
||||
await self.storage_service.update_task_status(
|
||||
@@ -126,25 +134,35 @@ class ProcessingService:
|
||||
)
|
||||
|
||||
# 材料属性 — 通过 MaterialService 集中管理
|
||||
requested_material = MaterialService.resolve_material(material)
|
||||
selected_material = MaterialService.get_material(requested_material)
|
||||
requested_material = MaterialService.resolve_material(process_params["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)
|
||||
|
||||
stage_started = time.perf_counter()
|
||||
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
|
||||
await self.storage_service.update_task_status(
|
||||
db_session, task_id, "processing", 60, "生成型腔详细数据"
|
||||
)
|
||||
|
||||
stage_started = time.perf_counter()
|
||||
detailed_cavity_json = CalculationService.build_plan_result(
|
||||
geometry_data=geometry_data,
|
||||
material=selected_material,
|
||||
file_path=str(file_path),
|
||||
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_cavity_data = best_scheme.get("cavity_data", {}) if best_scheme else {}
|
||||
@@ -164,6 +182,7 @@ class ProcessingService:
|
||||
db_session, task_id, "processing", 70, "保存几何数据"
|
||||
)
|
||||
|
||||
stage_started = time.perf_counter()
|
||||
await self.storage_service.save_geometry_data(
|
||||
db_session,
|
||||
stp_file_id,
|
||||
@@ -229,9 +248,15 @@ class ProcessingService:
|
||||
Path(html_file_path).name,
|
||||
html_file_path,
|
||||
)
|
||||
stage_timings["persist_artifacts"] = round(time.perf_counter() - stage_started, 3)
|
||||
|
||||
# 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:
|
||||
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)
|
||||
stage_timings["analyze_design"] = round(time.perf_counter() - stage_started, 3)
|
||||
|
||||
# 9.6 更新STP文件的分析摘要字段
|
||||
await self.storage_service.update_stp_file_analysis_summary(
|
||||
@@ -255,20 +281,35 @@ class ProcessingService:
|
||||
)
|
||||
|
||||
# 9.7 FreeCAD 几何验证
|
||||
stage_started = time.perf_counter()
|
||||
verification_result = await self._step_verify(
|
||||
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 增强分析
|
||||
llm_report = None
|
||||
stage_started = time.perf_counter()
|
||||
if analysis_result:
|
||||
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. 完成处理
|
||||
await self.storage_service.update_stp_file_status(db_session, stp_file_id, "completed")
|
||||
await self.storage_service.update_task_status(
|
||||
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, {
|
||||
@@ -279,6 +320,9 @@ class ProcessingService:
|
||||
"best_scheme_id": detailed_cavity_json.get("best_scheme_id"),
|
||||
"cavity_data": best_cavity_data,
|
||||
"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}",
|
||||
"verification": verification_result,
|
||||
"llm_report": llm_report,
|
||||
@@ -370,7 +414,7 @@ class ProcessingService:
|
||||
return mesh_result
|
||||
|
||||
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]]:
|
||||
"""生成多方案分模结果"""
|
||||
plan_result = None
|
||||
@@ -380,6 +424,7 @@ class ProcessingService:
|
||||
shape=shape,
|
||||
material=selected_material,
|
||||
is_foam_material=is_foam_material,
|
||||
process_params=process_params,
|
||||
)
|
||||
logger.info(
|
||||
f"多方案分模完成: 生成 {len(plan_result.get('candidate_schemes', []))} 套方案"
|
||||
@@ -391,6 +436,28 @@ class ProcessingService:
|
||||
|
||||
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(
|
||||
self, file_path: str, db_session: AsyncSession,
|
||||
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}")
|
||||
return stp_file
|
||||
|
||||
async def create_processing_task(self, session: AsyncSession,
|
||||
task_id: str,
|
||||
stp_file_id: int,
|
||||
task_type: str = "stp_parsing") -> ProcessingTask:
|
||||
async def create_processing_task(
|
||||
self,
|
||||
session: AsyncSession,
|
||||
task_id: str,
|
||||
stp_file_id: int,
|
||||
task_type: str = "stp_parsing",
|
||||
parameters: Optional[Dict[str, Any]] = None,
|
||||
) -> ProcessingTask:
|
||||
"""创建处理任务记录"""
|
||||
try:
|
||||
task = ProcessingTask(
|
||||
@@ -138,7 +142,8 @@ class StorageIntegrationService:
|
||||
stp_file_id=stp_file_id,
|
||||
task_type=task_type,
|
||||
status="pending",
|
||||
started_time=datetime.now()
|
||||
started_time=datetime.now(),
|
||||
parameters=parameters or {},
|
||||
)
|
||||
|
||||
session.add(task)
|
||||
@@ -189,6 +194,30 @@ class StorageIntegrationService:
|
||||
logger.error(f"更新任务状态失败: {e}")
|
||||
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):
|
||||
"""更新STP文件状态"""
|
||||
try:
|
||||
@@ -616,7 +645,11 @@ class StorageIntegrationService:
|
||||
'volume_utilization': stp_file.analysis_metrics.volume_utilization,
|
||||
'topology_complexity': stp_file.analysis_metrics.topology_complexity,
|
||||
'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
|
||||
|
||||
@@ -84,8 +84,10 @@ class TaskQueryService:
|
||||
|
||||
# 构造与内存任务兼容的任务视图
|
||||
cam_preferences = {}
|
||||
task_parameters = {}
|
||||
if isinstance(processing_task.parameters, dict):
|
||||
cam_preferences = processing_task.parameters.get("cam_preferences", {}) or {}
|
||||
task_parameters = dict(processing_task.parameters)
|
||||
|
||||
task_view = {
|
||||
"task_id": processing_task.task_id,
|
||||
@@ -108,6 +110,12 @@ class TaskQueryService:
|
||||
"plan_result": cavity_json,
|
||||
"mesh_summary": mesh_summary,
|
||||
"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": {
|
||||
"geometry_data": geometry_json,
|
||||
"detected_features": features_json,
|
||||
|
||||
@@ -1,24 +1,66 @@
|
||||
# utils/file_handler.py
|
||||
import aiofiles
|
||||
import hashlib
|
||||
import re
|
||||
import uuid
|
||||
from pathlib import Path
|
||||
from fastapi import UploadFile
|
||||
from typing import Tuple
|
||||
from typing import Tuple, Dict, Any
|
||||
|
||||
|
||||
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.mkdir(exist_ok=True)
|
||||
self.max_file_size = max_file_size
|
||||
|
||||
async def save_uploaded_file(self, file: UploadFile) -> Tuple[Path, int]:
|
||||
"""保存上传的文件"""
|
||||
file_path = self.upload_dir / file.filename
|
||||
def _sanitize_filename(self, filename: str) -> str:
|
||||
original = Path(filename or "upload.step").name
|
||||
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()
|
||||
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)
|
||||
|
||||
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):
|
||||
"""清理文件"""
|
||||
|
||||
Reference in New Issue
Block a user