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 aprocesar()yexportar(), y guarda elResultadoen 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.
- Subida. El frontend envía el plano y los parámetros (escala, espesor para el plan B, alternativa).
- 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.
- Registro. Se guarda el plano en el volumen, se crea la conversión en estado PENDIENTE y se devuelve su identificador (202).
- Procesado (en segundo plano, estado PROCESANDO). El servicio llama a
motor.api.procesar(), que ejecuta las etapas del motor:- lectura del plano, unidades y escala;
- separación e identificación de alzado, planta y perfil (con una sola vista, plan B: extrusión);
- sistema de proyección ISO-E / ISO-A;
- reconstrucción: celdas y búsqueda con CP-SAT;
- verificación: proyectar las aristas y compararlas con el plano.
- Exportación. Si hay sólido, se generan el STEP y el STL con
motor.api.exportar()y se guardan en el volumen. - 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.
- 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 contratomotor/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
TestClientde 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.