240 lines
11 KiB
Markdown
240 lines
11 KiB
Markdown
# 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<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}`:
|
|
|
|
```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.
|