Files
filam3d/backend/app/routers/calculate.py
2026-03-22 12:40:33 +03:00

201 lines
7.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import logging
import os
import tempfile
import uuid
from fastapi import APIRouter, Depends, File, Form, HTTPException, UploadFile
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.config import settings
from app.database import get_db
from app.models.material import Material
from app.models.calculation import Calculation
from app.schemas.calculate import (
BoundingBox,
CalculateResponse,
CalculationResult,
FileInfoResponse,
MaterialInfo,
)
from app.services.file_parser import FileInfo, parse_3d_file, SUPPORTED_EXTENSIONS
from app.services.price_engine import calculate_price
from app.services.storage import upload_file, delete_file
logger = logging.getLogger("app.routers.calculate")
router = APIRouter()
MAX_FILE_SIZE = 50 * 1024 * 1024 # 50 MB
@router.post("/calculate", response_model=CalculateResponse)
async def calculate(
file: UploadFile = File(...),
material_id: int = Form(...),
infill_percent: int = Form(30),
layer_height_mm: float = Form(0.2),
quantity: int = Form(1),
post_processing: str = Form(""),
db: AsyncSession = Depends(get_db),
):
logger.info("===== /api/calculate request =====")
# Validate extension
filename = file.filename or "unknown"
ext = os.path.splitext(filename)[1].lower()
logger.info("File: name=%s, ext=%s, content_type=%s", filename, ext, file.content_type)
if ext not in SUPPORTED_EXTENSIONS:
logger.warning("Unsupported file extension: %s (allowed: %s)", ext, SUPPORTED_EXTENSIONS)
raise HTTPException(400, f"Неподдерживаемый формат файла. Допустимые: {', '.join(SUPPORTED_EXTENSIONS)}")
# Validate params
logger.info("Params: material_id=%d, infill=%d%%, layer=%.2fmm, qty=%d, post_processing='%s'",
material_id, infill_percent, layer_height_mm, quantity, post_processing)
if not 10 <= infill_percent <= 100:
logger.warning("Invalid infill_percent: %d", infill_percent)
raise HTTPException(400, "infill_percent должен быть от 10 до 100")
if not 0.08 <= layer_height_mm <= 0.4:
logger.warning("Invalid layer_height_mm: %.2f", layer_height_mm)
raise HTTPException(400, "layer_height_mm должен быть от 0.08 до 0.4")
if not 1 <= quantity <= 500:
logger.warning("Invalid quantity: %d", quantity)
raise HTTPException(400, "quantity должен быть от 1 до 500")
# Read file
logger.info("Reading uploaded file...")
content = await file.read()
file_size = len(content)
logger.info("File read: %d bytes (%.2f MB)", file_size, file_size / 1024 / 1024)
if file_size > MAX_FILE_SIZE:
logger.warning("File too large: %d bytes (max %d)", file_size, MAX_FILE_SIZE)
raise HTTPException(413, "Файл слишком большой (максимум 50 MB)")
# Save to temp file for parsing, then upload to MinIO
file_id = str(uuid.uuid4())
object_name = f"uploads/{file_id}{ext}"
tmp_path = None
logger.info("Generated file_id: %s, object_name: %s", file_id, object_name)
try:
logger.debug("Writing to temp file for parsing...")
with tempfile.NamedTemporaryFile(suffix=ext, delete=False) as tmp:
tmp.write(content)
tmp_path = tmp.name
logger.debug("Temp file created: %s", tmp_path)
logger.info("Parsing 3D file...")
file_info = parse_3d_file(tmp_path, ext)
logger.info("File parsed successfully: volume=%.2f cm3, triangles=%d, watertight=%s",
file_info.volume_cm3, file_info.triangle_count, file_info.is_watertight)
except Exception as e:
logger.error("File parsing failed: %s", str(e), exc_info=True)
raise HTTPException(400, f"Ошибка парсинга файла: {str(e)}")
finally:
if tmp_path and os.path.exists(tmp_path):
os.remove(tmp_path)
logger.debug("Temp file removed: %s", tmp_path)
# Upload to MinIO
try:
logger.info("Uploading to MinIO: %s", object_name)
upload_file(object_name, content)
logger.info("MinIO upload complete")
except Exception as e:
logger.error("MinIO upload failed: %s", str(e), exc_info=True)
raise HTTPException(500, f"Ошибка загрузки файла в хранилище: {str(e)}")
# Get material
logger.info("Looking up material id=%d...", material_id)
result = await db.execute(select(Material).where(Material.id == material_id))
material = result.scalar_one_or_none()
if not material:
logger.warning("Material not found: id=%d", material_id)
raise HTTPException(400, f"Материал с id={material_id} не найден")
logger.info("Material found: id=%d, name=%s, density=%.2f, price=%.1f RUB/g",
material.id, material.name, material.density_g_cm3, material.price_per_gram)
# Parse post_processing
pp_list = [p.strip() for p in post_processing.split(",") if p.strip()] if post_processing else []
logger.info("Post-processing options: %s", pp_list)
# Calculate price
logger.info("Calculating price...")
price = calculate_price(
file_info=file_info,
density_g_cm3=material.density_g_cm3,
price_per_gram=material.price_per_gram,
flow_rate_mm3_s=material.flow_rate_mm3_s,
infill_percent=infill_percent,
layer_height_mm=layer_height_mm,
quantity=quantity,
post_processing=pp_list,
)
# Save calculation to DB
logger.info("Saving calculation to database...")
calc = Calculation(
file_name=filename,
file_format=ext.lstrip("."),
file_path=object_name,
volume_cm3=file_info.volume_cm3,
surface_area_cm2=file_info.surface_area_cm2,
bounding_box=file_info.bounding_box_mm,
is_watertight=file_info.is_watertight,
triangle_count=file_info.triangle_count,
material_id=material.id,
infill_percent=infill_percent,
layer_height_mm=layer_height_mm,
quantity=quantity,
post_processing=pp_list,
weight_grams=price.weight_grams,
material_cost_rub=price.material_cost_rub,
time_cost_rub=price.time_cost_rub,
print_time_hours=price.print_time_hours,
post_processing_cost_rub=price.post_processing_cost_rub,
total_rub=price.total_rub,
estimated_days=price.estimated_days,
)
db.add(calc)
await db.commit()
await db.refresh(calc)
logger.info("Calculation saved: id=%s, total=%.2f RUB", calc.id, price.total_rub)
logger.info("===== /api/calculate complete: %s -> %.2f RUB =====", filename, price.total_rub)
return CalculateResponse(
success=True,
calculation_id=str(calc.id),
file_info=FileInfoResponse(
filename=filename,
format=ext.lstrip("."),
volume_cm3=round(file_info.volume_cm3, 2),
surface_area_cm2=round(file_info.surface_area_cm2, 2),
bounding_box_mm=BoundingBox(**file_info.bounding_box_mm),
is_watertight=file_info.is_watertight,
triangle_count=file_info.triangle_count,
),
calculation=CalculationResult(
material=MaterialInfo(
id=material.id,
name=material.name,
density_g_cm3=material.density_g_cm3,
price_per_gram=material.price_per_gram,
),
weight_grams=price.weight_grams,
material_cost_rub=price.material_cost_rub,
print_time_hours=price.print_time_hours,
time_cost_rub=price.time_cost_rub,
post_processing_cost_rub=price.post_processing_cost_rub,
subtotal_rub=price.subtotal_rub,
quantity=price.quantity,
quantity_discount_percent=price.quantity_discount_percent,
total_rub=price.total_rub,
estimated_days=price.estimated_days,
),
)