This commit is contained in:
2026-05-14 16:56:18 +08:00
parent d38a4630dd
commit 224330598a
15 changed files with 1151 additions and 277 deletions
+10 -21
View File
@@ -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
View File
@@ -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,
}
+24
View File
@@ -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
View File
@@ -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
+49 -1
View File
@@ -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:
+75 -8
View File
@@ -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],
+39 -6
View File
@@ -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
+8
View File
@@ -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,
+49 -7
View File
@@ -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):
"""清理文件"""