plano3d/docs/propuesta-lizette/03-arquitectura-backend.md

11 KiB

Arquitectura del backend

Este documento describe cómo se organiza el backend: los componentes que lo forman, el flujo de una conversión, la API que usa el frontend, el modelo de datos, la estructura del código y el entorno Docker.

1. Visión general

Plano3D sigue una arquitectura cliente-servidor. La aplicación de escritorio (frontend, de Joel) es el cliente y se comunica con el backend mediante una API REST que intercambia JSON. El backend se ejecuta en contenedores Docker: uno para la aplicación Python (servidor y motor) y otro para MySQL, más un volumen donde se guardan los archivos.

flowchart LR
    APP[Frontend<br>aplicación de escritorio] -- HTTP + JSON<br>token JWT --> API
    subgraph Contenedor backend
        API[API REST<br>FastAPI] --> SEG[Seguridad<br>JWT, bcrypt]
        API --> SRV[Servicios]
        SRV --> REP[Repositorios<br>SQLAlchemy]
        SRV --> MOT[Motor<br>motor.api.procesar]
    end
    REP --> DB[(MySQL)]
    SRV --> VOL[/Volumen de archivos<br>DXF, STEP, STL/]
Componente Responsabilidad
API REST (FastAPI + Uvicorn) Recibe las peticiones del frontend, valida los datos y devuelve las respuestas.
Seguridad Registro, inicio de sesión, hash de contraseñas y verificación de los tokens JWT.
Servicios Lógica de negocio: proyectos, lanzamiento de conversiones en segundo plano, guardado de resultados.
Motor (motor/) Convierte el plano en pieza: lectura, vistas, reconstrucción, verificación y exportación. Se usa solo a través de motor/api.py.
Repositorios (SQLAlchemy) Todas las consultas a la base de datos.
MySQL Usuarios, proyectos, conversiones, avisos y metadatos de los archivos.
Volumen de archivos Guarda los DXF, STEP y STL. En la base de datos solo se guarda su ruta.

Dos contratos

  • API REST (servidor ↔ frontend). Es lo único que Joel necesita conocer del backend. Se documenta sola en /docs (Swagger) y cualquier cambio se acuerda antes.
  • motor/api.py (servidor ↔ motor). Es interno del backend: el servidor llama a procesar() y exportar(), y guarda el Resultado en la base de datos. Mientras el motor real no esté terminado, procesar() devuelve el motor simulado (CUBE01), de modo que el servidor y el frontend pueden avanzar en paralelo.

2. Flujo de una conversión

La conversión puede tardar varios segundos, así que no se hace dentro de la propia petición. La API responde enseguida con el identificador de la conversión y el trabajo sigue en segundo plano. El frontend consulta el estado hasta que termina y entonces descarga los archivos.

  1. Subida. El frontend envía el plano y los parámetros (escala, espesor para el plan B, alternativa).
  2. Validación. Se comprueba la extensión, que no supere 20 MB y que se pueda abrir. Si falla, se responde 400 con el motivo.
  3. Registro. Se guarda el plano en el volumen, se crea la conversión en estado PENDIENTE y se devuelve su identificador (202).
  4. Procesado (en segundo plano, estado PROCESANDO). El servicio llama a motor.api.procesar(), que ejecuta las etapas del motor:
    1. lectura del plano, unidades y escala;
    2. separación e identificación de alzado, planta y perfil (con una sola vista, plan B: extrusión);
    3. sistema de proyección ISO-E / ISO-A;
    4. reconstrucción: celdas y búsqueda con CP-SAT;
    5. verificación: proyectar las aristas y compararlas con el plano.
  5. Exportación. Si hay sólido, se generan el STEP y el STL con motor.api.exportar() y se guardan en el volumen.
  6. Registro final. Se guardan en MySQL el estado del resultado (OK, CON_AVISOS, IMPOSIBLE), los avisos, la escala aplicada, el número de alternativas y los archivos. La conversión pasa a COMPLETADA, o a ERROR si el resultado es IMPOSIBLE o ha fallado algo inesperado.
  7. Descarga. El frontend consulta el estado, muestra los avisos y descarga los archivos.

3. API REST

Todos los endpoints, salvo el registro y el inicio de sesión, piden la cabecera Authorization: Bearer <token>.

Método Ruta Descripción
POST /auth/registro Crear una cuenta.
POST /auth/login Iniciar sesión y obtener el token JWT.
GET /proyectos Listar los proyectos del usuario.
POST /proyectos Crear un proyecto subiendo un plano.
GET /proyectos/{id} Consultar un proyecto y su historial de conversiones.
DELETE /proyectos/{id} Eliminar un proyecto y sus archivos.
POST /proyectos/{id}/conversiones Lanzar una conversión con sus parámetros.
GET /conversiones/{id} Consultar el estado, el resultado y los avisos de una conversión.
GET /archivos/{id} Descargar un archivo (plano, STEP o STL).

Ejemplo de respuesta de GET /conversiones/{id}:

{
  "id": 12,
  "estado": "COMPLETADA",
  "resultado": "CON_AVISOS",
  "escala": 2.0,
  "alternativas": 1,
  "avisos": [
    {"codigo": "SIN_COTAS", "gravedad": "aviso",
     "mensaje": "El plano no tiene cotas: las medidas se deducen de la geometría y la escala.",
     "vista": null},
    {"codigo": "PROYECCION_MIXTA", "gravedad": "aviso",
     "mensaje": "Sistemas de proyección mezclados: ...", "vista": "perfil"}
  ],
  "archivos": [{"id": 31, "tipo": "STEP"}, {"id": 32, "tipo": "STL"}]
}

Códigos de respuesta

Código Significado
200 / 201 Operación correcta / recurso creado.
202 Conversión aceptada y en proceso.
400 Archivo o datos no válidos (se indica el motivo).
401 Falta el token o no es válido.
404 El recurso no existe o es de otro usuario.
413 El archivo supera el tamaño máximo.

4. Modelo de datos

erDiagram
    USUARIO ||--o{ PROYECTO : tiene
    PROYECTO ||--o{ CONVERSION : tiene
    PROYECTO ||--o{ ARCHIVO : guarda
    CONVERSION ||--o{ AVISO : genera
    CONVERSION |o--o{ ARCHIVO : produce
Tabla Campos principales
USUARIO id, correo (único), hash de la contraseña, fecha de alta
PROYECTO id, usuario_id, nombre, fecha de creación
CONVERSION id, proyecto_id, parámetros (escala, espesor, alternativa), estado (PENDIENTE, PROCESANDO, COMPLETADA, ERROR), resultado (OK, CON_AVISOS, IMPOSIBLE), escala aplicada, alternativas, duración, fechas
AVISO id, conversion_id, código, gravedad, mensaje, vista
ARCHIVO id, proyecto_id, conversion_id (nulo para el plano original), tipo (DXF, DWG, PDF, STEP, STL), ruta, tamaño, hash SHA-256

5. Estructura del código

Se añade la carpeta servidor/ al repositorio. El motor sigue en motor/ y el frontend en app/.

plano3d/
├── servidor/
│   ├── main.py              # arranque de FastAPI
│   ├── api/routers/         # auth, proyectos, conversiones, archivos
│   ├── schemas/             # modelos Pydantic (entrada/salida)
│   ├── services/            # proyectos, conversiones (llama a motor.api)
│   ├── repositories/        # consultas a la base de datos
│   ├── models/              # tablas SQLAlchemy
│   └── core/                # configuración y seguridad (JWT, bcrypt)
├── motor/
│   ├── api.py               # contrato servidor ↔ motor
│   ├── simulado.py          # motor simulado (CUBE01)
│   ├── procesado.py         # orquesta las etapas
│   ├── lectura.py           # DXF a segmentos, textos y cotas
│   ├── escala.py            # unidades y escala
│   ├── vistas.py            # alzado, planta y perfil
│   ├── proyeccion.py        # ISO-E / ISO-A
│   ├── reconstruccion.py    # celdas y CP-SAT
│   ├── pieza_plana.py       # plan B: extrusión
│   ├── verificacion.py      # comparar el sólido con el plano
│   └── avisos.py            # catálogo de códigos de aviso
├── alembic/                 # migraciones de la base de datos
├── pruebas/                 # pytest: motor, catálogo y API
├── Dockerfile
├── docker-compose.yml
└── .env.example             # variables necesarias, sin valores reales

Las rutas no tienen lógica de negocio ni acceden a la base de datos. Las excepciones del motor se capturan en el servicio de conversiones: la conversión queda en ERROR con el motivo y el servidor sigue funcionando.

6. Catálogo de avisos

Los códigos son estables: el frontend puede usarlos para mostrar ayuda o resaltar la vista afectada. Los dos primeros ya aparecen en el motor simulado. El resto es una propuesta que hay que acordar con Joel.

Código Gravedad Cuándo
SIN_COTAS Aviso El plano no tiene cotas
PROYECCION_MIXTA Aviso Planta y perfil siguen sistemas de proyección distintos
ESCALA_DETECTADA Info Se ha leído la escala del cajetín
SIN_ESCALA Aviso No hay escala ni en el plano ni en los parámetros
SIN_UNIDADES Info El DXF no define unidades; se suponen mm
ARCHIVO_NO_VALIDO Bloqueante No se puede abrir el archivo
VISTAS_NO_IDENTIFICADAS Bloqueante No se encuentran alzado, planta y perfil alineados
SIN_PERFIL_CERRADO Bloqueante Plan B: no hay ningún contorno cerrado
ENTIDAD_NO_SOPORTADA Aviso Hay splines u otras entidades que se ignoran
ROSCA Info Se ha modelado una rosca como agujero
PLANO_AMBIGUO Aviso Varias piezas reproducen el plano
NO_REPRODUCE Aviso La proyección del sólido no coincide del todo con el plano
COTA_INCOHERENTE Aviso Una cota no coincide con la geometría

7. Entorno Docker

docker-compose.yml define tres servicios:

Servicio Qué hace
mysql MySQL 8 con un volumen para sus datos.
backend La imagen del backend ejecutando Uvicorn en el puerto 8000. La documentación de la API queda en http://localhost:8000/docs.
pruebas La misma imagen, ejecutando pytest contra una base de datos de pruebas que se crea y se borra en cada ejecución.

Además hay un volumen para los archivos generados. Las credenciales se leen de un archivo .env que no se sube al repositorio; en él se incluye .env.example con los nombres de las variables.

La imagen parte de python:3.12-slim, instala las bibliotecas del sistema que necesita OpenCASCADE (libgl1, libglib2.0-0, libxrender1) y las dependencias con uv sync --frozen, usando el mismo uv.lock que en local. Así las pruebas dan el mismo resultado en el contenedor y en cualquier equipo.

docker compose up -d                    # levantar backend y MySQL
docker compose run --rm pruebas         # ejecutar todas las pruebas
docker compose run --rm pruebas pytest pruebas/test_contrato.py   # una sola

8. Pruebas

  • pruebas/test_contrato.py (ya existe): comprueba el contrato motor/api.py.
  • Una prueba por pieza del catálogo (pruebas/catalogo/): volumen esperado y códigos de aviso esperados.
  • Pruebas unitarias de cada etapa del motor con planos pequeños generados en la prueba.
  • Pruebas de la API con el TestClient de FastAPI: registro e inicio de sesión, acceso sin token (401), acceso a proyectos de otro usuario (404), subida de un archivo no válido (400), y una conversión completa de CUBE01 hasta la descarga.