602 lines
20 KiB
Python
602 lines
20 KiB
Python
# api/routes.py
|
||
from fastapi import APIRouter, UploadFile, File, HTTPException, Request, Depends
|
||
from typing import Optional, Dict, Any, List
|
||
import uuid
|
||
from datetime import datetime
|
||
import os
|
||
from pathlib import Path
|
||
|
||
from models.schemas import ProcessingStatus, create_task_info
|
||
from utils.file_handler import FileHandler
|
||
from services.storage_integration_rustfs import StorageIntegrationService
|
||
from services.redis_task_manager import redis_task_manager
|
||
from services.task_query_service import TaskQueryService
|
||
from database.database import get_db_session
|
||
|
||
try:
|
||
from celery_tasks import process_stp_task
|
||
_use_celery = True
|
||
except ImportError:
|
||
process_stp_task = None
|
||
_use_celery = False
|
||
from utils.logger import get_logger
|
||
from sqlalchemy.ext.asyncio import AsyncSession
|
||
from core.cavity_layout_optimizer import CavityLayoutOptimizer
|
||
from core.mold_system_designer import MoldSystemDesigner
|
||
from core.side_action_designer import SideActionDesigner
|
||
from core.mold_cam import MoldCAMDesigner
|
||
from core.mold_machining import CollisionDetector, ToolpathOptimizer, EDMElectrodeDesigner, MachiningSimulator
|
||
from core.cad_exporter import CADExporter
|
||
from services.auth_service import get_current_active_user
|
||
from models.database import User
|
||
|
||
logger = get_logger(__name__)
|
||
|
||
router = APIRouter()
|
||
|
||
file_handler = FileHandler()
|
||
cavity_layout_optimizer = CavityLayoutOptimizer()
|
||
mold_system_designer = MoldSystemDesigner()
|
||
side_action_designer = SideActionDesigner()
|
||
mold_cam_designer = MoldCAMDesigner()
|
||
collision_detector = CollisionDetector()
|
||
toolpath_optimizer = ToolpathOptimizer()
|
||
edm_designer = EDMElectrodeDesigner()
|
||
machining_simulator = MachiningSimulator()
|
||
cad_exporter = CADExporter()
|
||
|
||
|
||
@router.get("/health")
|
||
@router.post("/health")
|
||
async def health():
|
||
task_count = await redis_task_manager.get_task_count()
|
||
return {
|
||
"status": "healthy",
|
||
"pythonocc": True,
|
||
"total_tasks": task_count,
|
||
"redis_connected": redis_task_manager.is_connected,
|
||
}
|
||
|
||
|
||
@router.post("/upload")
|
||
async def upload_stp(
|
||
file: UploadFile = File(...),
|
||
material: Optional[str] = "ABS",
|
||
db_session: AsyncSession = Depends(get_db_session),
|
||
current_user: User = Depends(get_current_active_user)
|
||
):
|
||
"""上传STP文件并存储到数据库"""
|
||
logger.info(
|
||
f"[UPLOAD] 用户={current_user.username}(id={current_user.id}) "
|
||
f"文件={file.filename} 材料={material}"
|
||
)
|
||
|
||
if not file.filename.lower().endswith(('.stp', '.step')):
|
||
logger.warning(f"[UPLOAD] 拒绝: 不支持的文件类型 - {file.filename}")
|
||
raise HTTPException(400, "只支持STP/STEP文件")
|
||
|
||
task_id = str(uuid.uuid4())
|
||
|
||
# 保存文件
|
||
file_path, file_size = await file_handler.save_uploaded_file(file)
|
||
logger.info(f"[UPLOAD] 文件已保存: {file_path} ({file_size} bytes), task_id={task_id}")
|
||
|
||
# 创建存储集成服务实例
|
||
storage_service = StorageIntegrationService()
|
||
|
||
# 保存STP文件到RustFS + PostgreSQL
|
||
stp_file = await storage_service.save_stp_file(
|
||
session=db_session,
|
||
file_path=file_path,
|
||
original_filename=file.filename,
|
||
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)
|
||
|
||
# 创建任务记录(Redis 为主,内存作为兼容回退)
|
||
task_info = create_task_info(
|
||
task_id=task_id,
|
||
status=ProcessingStatus.PROCESSING,
|
||
filename=file.filename,
|
||
file_path=str(file_path),
|
||
file_size=file_size,
|
||
upload_time=str(datetime.now())
|
||
)
|
||
await redis_task_manager.set_task(task_id, task_info)
|
||
|
||
# 后台处理走统一编排服务,避免请求会话在后台失效
|
||
process_params = {"material": material}
|
||
if _use_celery:
|
||
process_stp_task.delay(task_id, str(file_path), stp_file.id, process_params)
|
||
logger.info(f"[UPLOAD] Celery 任务已调度: task_id={task_id}")
|
||
else:
|
||
import asyncio
|
||
from services.processing_service import processing_service
|
||
asyncio.create_task(processing_service.process_file_with_storage(
|
||
task_id, str(file_path), stp_file.id, process_params
|
||
))
|
||
logger.info(f"[UPLOAD] 直接后台处理: task_id={task_id} (celery 未安装)")
|
||
|
||
return {
|
||
"task_id": task_id,
|
||
"status": "processing",
|
||
"message": "文件上传成功,开始处理并存储到数据库",
|
||
"file_info": {
|
||
"filename": file.filename,
|
||
"size": file_size,
|
||
"pythonocc_available": True,
|
||
"database_file_id": stp_file.id
|
||
}
|
||
}
|
||
|
||
|
||
@router.get("/status/{task_id}")
|
||
@router.post("/status/{task_id}")
|
||
async def get_status(task_id: str, db_session: AsyncSession = Depends(get_db_session)):
|
||
"""
|
||
获取任务状态
|
||
|
||
优先返回内存中的任务信息;
|
||
如果内存中不存在,则从 PostgreSQL + RustFS 组装一个持久化的任务视图,
|
||
结构与内存任务保持尽量一致,便于前端集中展示总结性信息。
|
||
"""
|
||
try:
|
||
task_view = await TaskQueryService.get_task_view(db_session, task_id)
|
||
if task_view is None:
|
||
raise HTTPException(404, "任务不存在")
|
||
return task_view
|
||
except HTTPException:
|
||
raise
|
||
except Exception as e:
|
||
logger.error(f"获取任务状态失败: {e}")
|
||
raise HTTPException(500, f"获取任务状态失败: {str(e)}")
|
||
|
||
|
||
@router.get("/debug/tasks")
|
||
@router.post("/debug/tasks")
|
||
async def debug_tasks():
|
||
"""调试接口:查看所有任务"""
|
||
all_tasks = await redis_task_manager.get_all_tasks()
|
||
return {
|
||
"total_tasks": len(all_tasks),
|
||
"tasks": all_tasks,
|
||
"redis_connected": redis_task_manager.is_connected,
|
||
}
|
||
|
||
|
||
@router.get("/history")
|
||
@router.post("/history")
|
||
async def get_file_history(db_session: AsyncSession = Depends(get_db_session)):
|
||
"""获取按文件名分组的文件历史记录(支持多上传)"""
|
||
storage_service = StorageIntegrationService()
|
||
|
||
file_groups = await storage_service.get_all_file_groups(db_session)
|
||
|
||
return {
|
||
"total_files": len(file_groups),
|
||
"files": file_groups
|
||
}
|
||
|
||
|
||
@router.get("/history/{filename}")
|
||
@router.post("/history/{filename}")
|
||
async def get_file_records(filename: str, db_session: AsyncSession = Depends(get_db_session)):
|
||
"""获取指定文件名的所有上传记录(支持多上传历史)"""
|
||
import urllib.parse
|
||
decoded_filename = urllib.parse.unquote(filename)
|
||
|
||
storage_service = StorageIntegrationService()
|
||
file_records = await storage_service.get_file_history_by_filename(
|
||
db_session,
|
||
decoded_filename
|
||
)
|
||
|
||
return file_records
|
||
|
||
|
||
@router.get("/result/{task_id}")
|
||
@router.post("/result/{task_id}")
|
||
async def result_page(request: Request, task_id: str, db_session: AsyncSession = Depends(get_db_session)):
|
||
"""结果详情页面"""
|
||
from sqlalchemy import select
|
||
from models.database import ProcessingTask, STPFile, GeometryData, MoldCavityData, HTMLFile
|
||
|
||
# 从数据库查询任务详情
|
||
result = await db_session.execute(
|
||
select(ProcessingTask, STPFile)
|
||
.join(STPFile, ProcessingTask.stp_file_id == STPFile.id)
|
||
.where(ProcessingTask.task_id == task_id)
|
||
)
|
||
|
||
task_record = result.first()
|
||
|
||
if not task_record:
|
||
raise HTTPException(404, "任务不存在")
|
||
|
||
task, stp_file = task_record
|
||
|
||
# 构建任务详情数据(先只包含基本数据)
|
||
task_data = {
|
||
"task_id": task.task_id,
|
||
"filename": stp_file.original_filename if stp_file else "",
|
||
"file_size": stp_file.file_size if stp_file else 0,
|
||
"status": task.status,
|
||
"progress": task.progress,
|
||
"current_step": task.current_step,
|
||
"created_at": task.created_time.isoformat() if task.created_time else "",
|
||
"completed_at": task.completed_time.isoformat() if task.completed_time else "",
|
||
"error": task.error_message if task.error_message else ""
|
||
}
|
||
|
||
from fastapi.templating import Jinja2Templates
|
||
import os
|
||
# 简化路径配置,直接使用当前工作目录下的templates文件夹
|
||
templates_dir = os.path.join(os.getcwd(), "templates")
|
||
templates = Jinja2Templates(directory=templates_dir)
|
||
return templates.TemplateResponse("result.html", {
|
||
"request": request,
|
||
"task": task_data,
|
||
"pythonocc_available": True,
|
||
"version": "3.0.0"
|
||
})
|
||
|
||
|
||
# ==================== P3 新增 API ====================
|
||
|
||
@router.post("/optimize-layout")
|
||
async def optimize_cavity_layout(
|
||
request: Request,
|
||
current_user: User = Depends(get_current_active_user),
|
||
):
|
||
"""多型腔布局优化"""
|
||
body = await request.json()
|
||
product_bbox = body.get("product_bbox", {"dimensions": [100, 100, 50]})
|
||
cavity_count = body.get("cavity_count", 1)
|
||
mold_base_size = body.get("mold_base_size")
|
||
layout_type = body.get("layout_type", "auto")
|
||
|
||
if cavity_count < 1 or cavity_count > 64:
|
||
raise HTTPException(400, "型腔数量必须在 1-64 之间")
|
||
|
||
result = cavity_layout_optimizer.optimize_layout(
|
||
product_bbox=product_bbox,
|
||
cavity_count=cavity_count,
|
||
mold_base_size=mold_base_size,
|
||
layout_type=layout_type,
|
||
)
|
||
|
||
return {"status": "success", "data": result}
|
||
|
||
|
||
@router.post("/design-cooling")
|
||
async def design_cooling_system(
|
||
request: Request,
|
||
current_user: User = Depends(get_current_active_user),
|
||
):
|
||
"""冷却系统设计"""
|
||
body = await request.json()
|
||
mold_size = body.get("mold_size", {"length": 300, "width": 300, "height": 200})
|
||
product_bbox = body.get("product_bbox", {"dimensions": [100, 100, 50]})
|
||
material = body.get("material", "ABS")
|
||
cavity_count = body.get("cavity_count", 1)
|
||
cycle_time_target = body.get("cycle_time_target")
|
||
|
||
from core.mold_system_designer import CoolingSystemDesigner
|
||
designer = CoolingSystemDesigner()
|
||
result = designer.design_cooling_system(
|
||
mold_size=mold_size,
|
||
product_bbox=product_bbox,
|
||
material=material,
|
||
cavity_count=cavity_count,
|
||
cycle_time_target=cycle_time_target,
|
||
)
|
||
|
||
return {"status": "success", "data": result}
|
||
|
||
|
||
@router.post("/design-gating")
|
||
async def design_gating_system(
|
||
request: Request,
|
||
current_user: User = Depends(get_current_active_user),
|
||
):
|
||
"""浇注系统设计"""
|
||
body = await request.json()
|
||
product_bbox = body.get("product_bbox", {"dimensions": [100, 100, 50]})
|
||
material = body.get("material", "ABS")
|
||
cavity_count = body.get("cavity_count", 1)
|
||
gate_type = body.get("gate_type", "auto")
|
||
layout_positions = body.get("layout_positions")
|
||
|
||
from core.mold_system_designer import GatingSystemDesigner
|
||
designer = GatingSystemDesigner()
|
||
result = designer.design_gating_system(
|
||
product_bbox=product_bbox,
|
||
material=material,
|
||
cavity_count=cavity_count,
|
||
gate_type=gate_type,
|
||
layout_positions=layout_positions,
|
||
)
|
||
|
||
return {"status": "success", "data": result}
|
||
|
||
|
||
@router.post("/design-mold-system")
|
||
async def design_complete_mold_system(
|
||
request: Request,
|
||
current_user: User = Depends(get_current_active_user),
|
||
):
|
||
"""综合模具系统设计(冷却+浇注)"""
|
||
body = await request.json()
|
||
mold_size = body.get("mold_size", {"length": 300, "width": 300, "height": 200})
|
||
product_bbox = body.get("product_bbox", {"dimensions": [100, 100, 50]})
|
||
material = body.get("material", "ABS")
|
||
cavity_count = body.get("cavity_count", 1)
|
||
gate_type = body.get("gate_type", "auto")
|
||
cycle_time_target = body.get("cycle_time_target")
|
||
layout_positions = body.get("layout_positions")
|
||
|
||
result = mold_system_designer.design_complete_system(
|
||
mold_size=mold_size,
|
||
product_bbox=product_bbox,
|
||
material=material,
|
||
cavity_count=cavity_count,
|
||
gate_type=gate_type,
|
||
cycle_time_target=cycle_time_target,
|
||
layout_positions=layout_positions,
|
||
)
|
||
|
||
return {"status": "success", "data": result}
|
||
|
||
|
||
@router.post("/ai-parting-detect")
|
||
async def ai_parting_surface_detect(
|
||
request: Request,
|
||
current_user: User = Depends(get_current_active_user),
|
||
):
|
||
"""AI 分型面检测"""
|
||
body = await request.json()
|
||
task_id = body.get("task_id")
|
||
|
||
if not task_id:
|
||
raise HTTPException(404, "缺少 task_id")
|
||
|
||
task_data = await redis_task_manager.get_task(task_id)
|
||
if not task_data:
|
||
raise HTTPException(404, "任务不存在")
|
||
|
||
geometry_data = task_data.get("geometry_data")
|
||
if not geometry_data:
|
||
raise HTTPException(400, "该任务尚未完成几何分析")
|
||
|
||
from core.ai_parting_detector import AIPartingSurfaceDetectorV2
|
||
detector = AIPartingSurfaceDetectorV2(use_gnn=True)
|
||
|
||
result = detector._detect_with_geometry(None, geometry_data)
|
||
|
||
return {"status": "success", "data": result}
|
||
|
||
|
||
@router.post("/detect-undercuts")
|
||
async def detect_undercuts(
|
||
request: Request,
|
||
current_user: User = Depends(get_current_active_user),
|
||
):
|
||
"""倒扣区域检测与滑块/斜顶机构设计"""
|
||
body = await request.json()
|
||
task_id = body.get("task_id")
|
||
parting_direction = body.get("parting_direction", [0, 0, 1])
|
||
mold_size = body.get("mold_size", {"length": 300, "width": 300, "height": 200})
|
||
|
||
if not task_id:
|
||
raise HTTPException(404, "缺少 task_id")
|
||
|
||
task_data = await redis_task_manager.get_task(task_id)
|
||
if not task_data:
|
||
raise HTTPException(404, "任务不存在")
|
||
|
||
geometry_data = task_data.get("geometry_data")
|
||
if not geometry_data:
|
||
raise HTTPException(400, "该任务尚未完成几何分析")
|
||
|
||
result = side_action_designer.analyze_and_design(
|
||
shape=None, parting_direction=parting_direction, mold_size=mold_size
|
||
)
|
||
|
||
return {"status": "success", "data": result}
|
||
|
||
|
||
@router.post("/design-cam")
|
||
async def design_mold_cam(
|
||
request: Request,
|
||
current_user: User = Depends(get_current_active_user),
|
||
):
|
||
"""模具CAM刀路设计"""
|
||
body = await request.json()
|
||
cavity_bbox = body.get("cavity_bbox", {"dimensions": [100, 100, 50], "min": [-50, -50, -25], "max": [50, 50, 25]})
|
||
stock_bbox = body.get("stock_bbox", {"dimensions": [150, 150, 100], "min": [-75, -75, -50], "max": [75, 75, 50]})
|
||
mold_steel = body.get("mold_steel", "P20")
|
||
surface_quality = body.get("surface_quality", "standard")
|
||
controller = body.get("controller", "fanuc")
|
||
|
||
result = mold_cam_designer.design_mold_cam(
|
||
cavity_bbox=cavity_bbox,
|
||
stock_bbox=stock_bbox,
|
||
mold_steel=mold_steel,
|
||
surface_quality=surface_quality,
|
||
controller=controller,
|
||
)
|
||
|
||
return {"status": "success", "data": result}
|
||
|
||
|
||
@router.post("/check-collision")
|
||
async def check_toolpath_collision(
|
||
request: Request,
|
||
current_user: User = Depends(get_current_active_user),
|
||
):
|
||
"""刀路碰撞检测"""
|
||
body = await request.json()
|
||
toolpath_points = body.get("toolpath_points", [[0, 0, 50], [10, 10, -5], [20, 20, -10]])
|
||
tool = body.get("tool", {"diameter": 10, "flute_length": 30, "shank_diameter": 10})
|
||
stock_bbox = body.get("stock_bbox", {"min": [-50, -50, -25], "max": [50, 50, 25]})
|
||
clamp_positions = body.get("clamp_positions")
|
||
|
||
result = collision_detector.check_toolpath_safety(
|
||
toolpath_points, tool, stock_bbox, clamp_positions
|
||
)
|
||
|
||
return {"status": "success", "data": result}
|
||
|
||
|
||
@router.post("/optimize-toolpath")
|
||
async def optimize_toolpath(
|
||
request: Request,
|
||
current_user: User = Depends(get_current_active_user),
|
||
):
|
||
"""刀路优化"""
|
||
body = await request.json()
|
||
toolpath_points = body.get("toolpath_points", [[0, 0, 50], [10, 10, -5], [20, 20, -10]])
|
||
cutting_params = body.get("cutting_params", {"feed_rate_mm_min": 500})
|
||
stock_bbox = body.get("stock_bbox")
|
||
|
||
result = toolpath_optimizer.optimize_toolpath(
|
||
toolpath_points, cutting_params, stock_bbox
|
||
)
|
||
|
||
return {"status": "success", "data": result}
|
||
|
||
|
||
@router.post("/design-electrodes")
|
||
async def design_edm_electrodes(
|
||
request: Request,
|
||
current_user: User = Depends(get_current_active_user),
|
||
):
|
||
"""EDM电极设计"""
|
||
body = await request.json()
|
||
undercut_regions = body.get("undercut_regions", [{"center": [0, 0, 0], "area": 100, "type": "undercut"}])
|
||
cavity_bbox = body.get("cavity_bbox", {"dimensions": [100, 100, 50]})
|
||
material = body.get("material", "copper")
|
||
spark_gap = body.get("spark_gap", 0.05)
|
||
overburn = body.get("overburn", 0.1)
|
||
|
||
result = edm_designer.design_electrodes(
|
||
undercut_regions, cavity_bbox, material, spark_gap, overburn
|
||
)
|
||
|
||
return {"status": "success", "data": result}
|
||
|
||
|
||
@router.post("/simulate-machining")
|
||
async def simulate_machining(
|
||
request: Request,
|
||
current_user: User = Depends(get_current_active_user),
|
||
):
|
||
"""加工仿真"""
|
||
body = await request.json()
|
||
operations = body.get("operations", [{"strategy": "z_level_roughing", "levels": [{"z": -5}]}])
|
||
stock_bbox = body.get("stock_bbox", {"dimensions": [100, 100, 50], "min": [-50, -50, -25], "max": [50, 50, 25]})
|
||
resolution = body.get("resolution", 2.0)
|
||
|
||
result = machining_simulator.simulate_machining(
|
||
operations, stock_bbox, resolution
|
||
)
|
||
|
||
return {"status": "success", "data": result}
|
||
|
||
|
||
# ==================== CAD 导出 API ====================
|
||
|
||
@router.post("/export-mold")
|
||
async def export_mold_results(
|
||
request: Request,
|
||
current_user: User = Depends(get_current_active_user),
|
||
):
|
||
"""导出模具设计结果(STEP/IGES/STL/BRep)"""
|
||
body = await request.json()
|
||
task_id = body.get("task_id")
|
||
formats = body.get("formats", ["step", "stl"])
|
||
components = body.get("components", ["cavity", "core"])
|
||
scheme_id = body.get("scheme_id")
|
||
|
||
if not task_id:
|
||
raise HTTPException(404, "缺少 task_id")
|
||
|
||
cavity_shapes = processing_service.get_export_shapes(task_id, scheme_id=scheme_id)
|
||
if not cavity_shapes:
|
||
task_data = await redis_task_manager.get_task(task_id)
|
||
if not task_data:
|
||
raise HTTPException(404, "任务不存在")
|
||
filename = task_data.get("filename", f"mold_{task_id}")
|
||
raise HTTPException(
|
||
400,
|
||
f"该任务的 OCC 形状数据已过期(仅保留 STEP 导出文件),"
|
||
f"请通过历史页面的下载链接获取已导出的 STEP 文件",
|
||
)
|
||
|
||
task_data = await redis_task_manager.get_task(task_id)
|
||
base_filename = (
|
||
Path(task_data.get("filename", f"mold_{task_id}")).stem
|
||
if task_data
|
||
else f"mold_{task_id}"
|
||
)
|
||
|
||
result = cad_exporter.export_mold_results(
|
||
cavity_data=cavity_shapes,
|
||
base_filename=base_filename,
|
||
formats=formats,
|
||
components=components,
|
||
)
|
||
|
||
return {"status": "success", "data": result}
|
||
|
||
|
||
@router.get("/export-download/{filepath:path}")
|
||
async def download_export_file(
|
||
filepath: str,
|
||
current_user: User = Depends(get_current_active_user),
|
||
):
|
||
"""下载导出的CAD文件"""
|
||
from fastapi.responses import FileResponse
|
||
|
||
full_path = os.path.join(cad_exporter.output_dir, filepath)
|
||
|
||
if not os.path.exists(full_path):
|
||
raise HTTPException(404, "文件不存在")
|
||
|
||
if not os.path.abspath(full_path).startswith(os.path.abspath(cad_exporter.output_dir)):
|
||
raise HTTPException(403, "禁止访问")
|
||
|
||
media_types = {
|
||
".step": "application/step",
|
||
".stp": "application/step",
|
||
".iges": "application/iges",
|
||
".igs": "application/iges",
|
||
".stl": "model/stl",
|
||
".brep": "application/octet-stream",
|
||
}
|
||
|
||
ext = Path(full_path).suffix.lower()
|
||
media_type = media_types.get(ext, "application/octet-stream")
|
||
|
||
return FileResponse(
|
||
full_path,
|
||
media_type=media_type,
|
||
filename=os.path.basename(full_path),
|
||
)
|
||
|
||
|
||
@router.get("/export-recommendations")
|
||
async def get_export_recommendations(
|
||
target: str = "ug",
|
||
current_user: User = Depends(get_current_active_user),
|
||
):
|
||
"""获取导出格式建议(UG/FreeCAD/SolidWorks)"""
|
||
result = cad_exporter.get_export_recommendations(target)
|
||
return {"status": "success", "data": result}
|
||
|
||
|
||
|