This commit is contained in:
xds
2026-03-22 12:40:33 +03:00
commit 28a5d51389
61 changed files with 6085 additions and 0 deletions

View 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,
),
)