init
This commit is contained in:
200
backend/app/routers/calculate.py
Normal file
200
backend/app/routers/calculate.py
Normal file
@@ -0,0 +1,200 @@
|
||||
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,
|
||||
),
|
||||
)
|
||||
Reference in New Issue
Block a user