# 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. ```mermaid flowchart LR APP[Frontend
aplicación de escritorio] -- HTTP + JSON
token JWT --> API subgraph Contenedor backend API[API REST
FastAPI] --> SEG[Seguridad
JWT, bcrypt] API --> SRV[Servicios] SRV --> REP[Repositorios
SQLAlchemy] SRV --> MOT[Motor
motor.api.procesar] end REP --> DB[(MySQL)] SRV --> VOL[/Volumen de archivos
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 `. | 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}`: ```json { "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 ```mermaid 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. ```bash 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.