Tecnologías Clave para el Desarrollo del Sistema
La implementación de un sistema robusto y eficiente para la gestión de bibliotecas o librerías requiere una combinación de tecnologías modernas tanto en el backend como en el frontend, así como para la persistencia de datos. Este proyecto se construye utilizando un stack tecnológico que incluye Spring Boot para el servidor, Vue.js para la interfaz de usuario, y MyBatis para la capa de acceso a datos, garantizando escalabilidad, rendimiento y facilidad de mantenimiento.
Spring Boot: La Base del Backend
Spring Boot se ha establecido como el marco de trabajo de facto para el desarrollo de aplicaciones basadas en Spring en el ecosistema Java. Su principal objetivo es simplificar el proceso de configuración y despliegue, permitiendo a los desarrolladores crear aplicaciones Spring de producción listas para ejecutar de manera rápida y con una mínima configuración. Al adoptar el principio de "convención sobre configuración", Spring Boot reduce drásticamente el código boilerplate y las decisiones de configuración manuales.
Entre sus características destacadas, Spring Boot ofrece:
- **Auto-configuración inteligente:** Detecta las dependencias en el classpath y configura automáticamente los componentes necesarios.
- **Servidores embebidos:** Permite empaquetar la aplicación con servidores como Tomcat, Jetty o Undertow, facilitando el despliegue de aplicaciones web como archivos JAR ejecutables.
- **Manejo de dependencias simplificado:** A través de sus "starters", agrupa colecciones de dependencias comunes, lo que simplifica la gestión del proyecto.
Vue.js: Interfaz de Usuario Reactiva
Vue.js es un marco de JavaScript progresivo y de código abierto para construir interfaces de usuario. Su diseño está centrado en ser incrementalmente adaptable, lo que significa que se puede adoptar fácilmente en una pequeña parte de una aplicación existente o utilizarse para desarrollar aplicaciones de una sola página (SPA) complejas. Vue.js es conocido por su facilidad de aprendizaje, rendimiento y flexibilidad.
Las ventajas clave de Vue.js incluyen:
- **Reactividad:** Ofrece un sistema de datos reactivo que actualiza automáticamente la interfaz de usuario cuando los datos subyacentes cambian, eliminando la necesidad de manipulaciones manuales del DOM.
- **Virtual DOM:** Utiliza un DOM virtual para optimizar las actualizaciones de la interfaz, lo que resulta en un rendimiento rápido y eficiente.
- **Arquitectura basada en componentes:** Promueve la creación de componentes reutilizables y autocontenidos, lo que mejora la modularidad, la mantenibilidad y la colaboración en equipos de desarrollo.
MyBatis: Acceso a Datos Flexible
MyBatis es un marco de persistencia de primera clase que simplifica la interacción con bases de datos relacionales en aplicaciones Java. A diferencia de los ORM completos, MyBatis proporciona un mapeo entre objetos Java y sentencias SQL utilizando archivos XML o anotaciones. Esto permite a los desarrolladores mantener un control total sobre el SQL, lo que es especialmente útil en escenarios donde se requiere optimizar consultas complejas o trabajar con bases de datos heredadas.
Los principales beneficios de MyBatis son:
- **Separación de SQL y código Java:** Fomenta la separación clara entre las sentencias SQL y la lógica de la aplicación, mejorando la legibilidad y el mantenimiento.
- **SQL dinámico:** Permite construir sentencias SQL de forma dinámica en función de las condiciones de entrada, lo que ofrece una flexibilidad considerable para consultas complejas.
- **Mapeo de resultados avanzado:** Facilita el mapeo de resultados de consultas directamente a objetos Java, minimizando el código boilerplate para la recuperación de datos.
- **Soporte de caché:** Incorpora un sistema de caché de primer y segundo nivel para reducir las llamadas repetidas a la base de datos y mejorar el rendimiento.
Ejemplos de Implementación de Código
A continuación, se presentan fragmentos de código que ilustran la implementación de la autenticación de usuarios y la gestión de tokens en el backend utilizando Spring Boot.
Controlador de Autenticación
Este controlador gestiona las solicitudes de inicio de sesión, validando las credenciales del usuario y generando un token de sesión.
import org.springframework.web.bind.annotation.*;
import org.springframework.beans.factory.annotation.Autowired;
import jakarta.servlet.http.HttpServletRequest;
// Anotación personalizada para ignorar la validación de autenticación
@NoAuthRequired
@RestController
@RequestMapping("/api/auth")
public class AutenticacionControlador {
@Autowired
private ServicioUsuario servicioUsuario;
@Autowired
private ServicioToken servicioToken;
@PostMapping("/iniciarSesion")
public RespuestaServicio iniciarSesion(@RequestParam String nombreUsuario, @RequestParam String contrasena,
@RequestParam String codigoCaptcha, HttpServletRequest solicitud) {
// Simulación de verificación de captcha
if (!"1234".equals(codigoCaptcha)) { // Reemplazar con lógica de captcha real
return RespuestaServicio.error("Código CAPTCHA incorrecto");
}
EntidadUsuario usuario = servicioUsuario.buscarPorCredenciales(nombreUsuario, contrasena);
if (usuario == null) {
return RespuestaServicio.error("Credenciales inválidas");
}
String tokenGenerado = servicioToken.generarNuevoToken(usuario.getId(), usuario.getNombreUsuario(), "usuarios", usuario.getRol());
return RespuestaServicio.ok().agregarDatos("token", tokenGenerado);
}
}
Servicio de Gestión de Tokens
Este servicio se encarga de crear, actualizar y validar los tokens de sesión de los usuarios.
import org.springframework.stereotype.Service;
import java.util.Calendar;
import java.util.Date;
import java.util.UUID; // Para generar tokens aleatorios
@Service
public class ServicioTokenImpl implements ServicioToken {
// Dependencia del repositorio de tokens
@Autowired
private RepositorioToken repositorioToken;
@Override
public String generarNuevoToken(Long idUsuario, String nombreUsuario, String tipoEntidad, String rolUsuario) {
EntidadToken tokenExistente = repositorioToken.buscarPorUsuarioYRol(idUsuario, rolUsuario);
String valorToken = UUID.randomUUID().toString().replace("-", ""); // Genera un token aleatorio
Calendar cal = Calendar.getInstance();
cal.setTime(new Date());
cal.add(Calendar.HOUR_OF_DAY, 2); // Token válido por 2 horas
if (tokenExistente != null) {
tokenExistente.setToken(valorToken);
tokenExistente.setFechaExpiracion(cal.getTime());
repositorioToken.guardar(tokenExistente);
} else {
EntidadToken nuevoToken = new EntidadToken(idUsuario, nombreUsuario, tipoEntidad, rolUsuario, valorToken, cal.getTime());
repositorioToken.guardar(nuevoToken);
}
return valorToken;
}
@Override
public EntidadToken obtenerTokenPorValor(String token) {
return repositorioToken.buscarPorToken(token);
}
}
Interceptor de Autorización
Este interceptor verifica la validez del token en cada solicitud, asegurando que solo los usuarios autenticados puedan acceder a los recursos protegidos.
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.method.HandlerMethod;
import org.springframework.http.HttpStatus;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import com.fasterxml.jackson.databind.ObjectMapper; // Para serializar respuestas JSON
import java.io.PrintWriter;
import org.springframework.util.StringUtils;
@Component
public class InterceptorAutorizacion implements HandlerInterceptor {
public static final String CABECERA_TOKEN = "Authorization"; // Clave del token en la cabecera
@Autowired
private ServicioToken servicioToken;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// Configuración de CORS
response.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE, PUT");
response.setHeader("Access-Control-Max-Age", "3600");
response.setHeader("Access-Control-Allow-Credentials", "true");
response.setHeader("Access-Control-Allow-Headers", "x-requested-with, Authorization, Content-Type, Accept, Origin");
response.setHeader("Access-Control-Allow-Origin", request.getHeader("Origin"));
// Manejo de solicitudes OPTIONS (preflight)
if (request.getMethod().equalsIgnoreCase("OPTIONS")) {
response.setStatus(HttpStatus.OK.value());
return false;
}
NoAuthRequired anotacionSinAuth = null;
if (handler instanceof HandlerMethod) {
anotacionSinAuth = ((HandlerMethod) handler).getMethodAnnotation(NoAuthRequired.class);
}
// Si el método tiene la anotación NoAuthRequired, se permite el acceso sin verificación
if (anotacionSinAuth != null) {
return true;
}
String tokenAcceso = request.getHeader(CABECERA_TOKEN);
if (StringUtils.isEmpty(tokenAcceso) || !tokenAcceso.startsWith("Bearer ")) {
return enviarError(response, HttpStatus.UNAUTHORIZED.value(), "Token de acceso no proporcionado o formato inválido.");
}
tokenAcceso = tokenAcceso.substring(7); // Eliminar prefijo "Bearer "
EntidadToken tokenEntidad = servicioToken.obtenerTokenPorValor(tokenAcceso);
if (tokenEntidad != null && tokenEntidad.getFechaExpiracion().after(new Date())) {
// Guardar información del usuario en la sesión para uso posterior
request.getSession().setAttribute("idSesionUsuario", tokenEntidad.getIdUsuario());
request.getSession().setAttribute("rolSesionUsuario", tokenEntidad.getRolUsuario());
request.getSession().setAttribute("nombreTablaEntidad", tokenEntidad.getTipoEntidad());
request.getSession().setAttribute("nombreUsuarioSesion", tokenEntidad.getNombreUsuario());
return true;
}
return enviarError(response, HttpStatus.UNAUTHORIZED.value(), "Sesión expirada o token inválido. Por favor, inicie sesión nuevamente.");
}
private boolean enviarError(HttpServletResponse response, int status, String message) throws Exception {
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json; charset=utf-8");
response.setStatus(status);
try (PrintWriter writer = response.getWriter()) {
ObjectMapper mapper = new ObjectMapper();
writer.print(mapper.writeValueAsString(RespuestaServicio.error(status, message)));
}
return false;
}
}
Diseño de la Base de Datos
La estructura de la base de datos se define mediante los siguientes esquemas de tablas, utilizando MySQL como sistema de gestión de bases de datos.
Tabla de Direcciones de Envío (direcciones\_envio)
Almacena las direcciones de los usuarios para fines de envío de productos o libros.
DROP TABLE IF EXISTS `direcciones_envio`;
CREATE TABLE `direcciones_envio` (
`id_direccion` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT 'Identificador único de la dirección',
`fecha_creacion` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'Momento de creación del registro',
`usuario_id` BIGINT(20) NOT NULL COMMENT 'Identificador del usuario asociado a la dirección',
`calle_numero` VARCHAR(255) NOT NULL COMMENT 'Calle y número de la dirección',
`nombre_receptor` VARCHAR(200) NOT NULL COMMENT 'Nombre completo del destinatario',
`telefono_contacto` VARCHAR(20) NOT NULL COMMENT 'Número de teléfono del destinatario',
`es_principal` BOOLEAN NOT NULL DEFAULT FALSE COMMENT 'Indica si es la dirección predeterminada [verdadero/falso]',
PRIMARY KEY (`id_direccion`),
KEY `fk_usuario_direccion` (`usuario_id`)
) ENGINE=InnoDB AUTO_INCREMENT=100 DEFAULT CHARSET=utf8mb4 COMMENT='Registro de direcciones de envío de usuarios';
Tabla del Foro de la Comunidad (foro\_comunidad)
Permite a los usuarios interactuar, publicar preguntas y respuestas, y participar en discusiones.
DROP TABLE IF EXISTS `foro_comunidad`;
CREATE TABLE `foro_comunidad` (
`id_publicacion` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT 'Identificador único de la publicación',
`fecha_publicacion` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'Marca de tiempo de la creación de la publicación',
`titulo_publicacion` VARCHAR(200) DEFAULT NULL COMMENT 'Título del tema del foro',
`contenido_post` LONGTEXT NOT NULL COMMENT 'Contenido detallado de la publicación o comentario',
`id_padre` BIGINT(20) DEFAULT NULL COMMENT 'ID de la publicación padre si es un comentario',
`autor_id` BIGINT(20) NOT NULL COMMENT 'ID del usuario que realizó la publicación',
`nombre_autor` VARCHAR(200) DEFAULT NULL COMMENT 'Nombre de usuario del autor',
`url_avatar` LONGTEXT COMMENT 'URL del avatar del autor',
`estado_moderacion` VARCHAR(50) DEFAULT 'pendiente' COMMENT 'Estado de la publicación (aprobado, pendiente, rechazado)',
`es_fijado` INT(11) DEFAULT '0' COMMENT 'Indica si la publicación está fijada (0: no, 1: sí)',
`fecha_fijado` DATETIME DEFAULT NULL COMMENT 'Fecha y hora en que se fijó la publicación',
PRIMARY KEY (`id_publicacion`),
KEY `fk_autor_foro` (`autor_id`),
KEY `fk_padre_foro` (`id_padre`)
) ENGINE=InnoDB AUTO_INCREMENT=200 DEFAULT CHARSET=utf8mb4 COMMENT='Tabla para el foro de discusión y comunidad';
Tabla de Usuarios del Sistema (usuarios\_sistema)
Contiene la información de los usuarios que interactúan con el sistema, incluyendo credenciales y detalles personales.
DROP TABLE IF EXISTS `usuarios_sistema`;
CREATE TABLE `usuarios_sistema` (
`id_usuario` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT 'Clave primaria, identificador único de usuario',
`fecha_registro` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'Fecha de creación del registro de usuario',
`nombre_acceso` VARCHAR(100) NOT NULL COMMENT 'Nombre de usuario para iniciar sesión',
`contrasena_hash` VARCHAR(255) NOT NULL COMMENT 'Contraseña cifrada o hashed del usuario',
`nombre_completo` VARCHAR(200) NOT NULL COMMENT 'Nombre completo del usuario',
`genero_usuario` VARCHAR(10) DEFAULT NULL COMMENT 'Género del usuario (ej. "Masculino", "Femenino")',
`telefono_movil` VARCHAR(20) DEFAULT NULL COMMENT 'Número de teléfono del usuario',
`url_avatar` LONGTEXT COMMENT 'URL de la imagen de perfil del usuario',
`puntos_fidelidad` DOUBLE DEFAULT '0.0' COMMENT 'Puntos acumulados por el usuario',
`saldo_cuenta` DOUBLE DEFAULT '0.0' COMMENT 'Saldo de la cuenta del usuario',
PRIMARY KEY (`id_usuario`),
UNIQUE KEY `idx_nombre_acceso` (`nombre_acceso`)
) ENGINE=InnoDB AUTO_INCREMENT=300 DEFAULT CHARSET=utf8mb4 COMMENT='Usuarios registrados en el sistema';
Ejemplo de Inserción de Datos
Aquí se muestran algunos ejemplos de cómo insertar datos en una tabla de productos (asumiendo que existe una tabla catalogo_productos).
INSERT INTO `catalogo_productos` (`nombre_item`, `precio_unitario`, `descripcion_detallada`, `cantidad_inventario`)
VALUES ('El Gran Gatsby', 12.50, 'Novela clásica de F. Scott Fitzgerald', 250);
INSERT INTO `catalogo_productos` (`nombre_item`, `precio_unitario`, `descripcion_detallada`, `cantidad_inventario`)
VALUES ('Cien años de soledad', 15.00, 'Obra maestra de Gabriel García Márquez', 180);
INSERT INTO `catalogo_productos` (`nombre_item`, `precio_unitario`, `descripcion_detallada`, `cantidad_inventario`)
VALUES ('1984', 9.99, 'Distopía de George Orwell', 300);
Casos de Prueba y Verificación Funcional
Se realizaron pruebas exhaustivas para asegurar la correcta funcionalidad del sistema. Los siguientes ejemplos ilustran algunos de los escenarios de prueba ejecutados para la autenticación y la gestión de usuarios.
Pruebas de Inicio de Sesión
Se verificó la robustez del proceso de inicio de sesión ante diferentes entradas.
| Datos de Entrada | Resultado Esperado | Resultado Obtenido | Análisis |
|---|---|---|---|
| Usuario: admin, Contraseña: admin123, CAPTCHA: correcto | Acceso concedido al sistema | Sesión iniciada correctamente | Coincidencia |
| Usuario: admin, Contraseña: contraIncorrecta, CAPTCHA: correcto | Mensaje de error: "Credenciales inválidas" | "Credenciales inválidas" | Coincidencia |
| Usuario: admin, Contraseña: admin123, CAPTCHA: incorrecto | Mensaje de error: "Código CAPTCHA incorrecto" | "Código CAPTCHA incorrecto" | Coincidencia |
| Usuario: vacío, Contraseña: admin123, CAPTCHA: correcto | Mensaje de error: "El nombre de usuario es obligatorio" | Validación de campo: "Nombre de usuario requerido" | Coincidencia |
| Usuario: admin, Contraseña: vacío, CAPTCHA: correcto | Mensaje de error: "La contraseña es obligatoria" | Validación de campo: "Contraseña requerida" | Coincidencia |
Pruebas de Gestión de Usuarios
El módulo de administración de usuarios, que incluye la creación, modificación y eliminación, también fue sometido a pruebas detalladas.
1. Casos de Prueba para Añadir Usuario:
| Datos de Entrada | Resultado Esperado | Resultado Obtenido | Análisis |
|---|---|---|---|
| Nombre: nuevoUser, Contraseña: pass123, Rol: Lector | Usuario añadido y visible en la lista | "nuevoUser" aparece en la tabla de usuarios | Coincidencia |
| Nombre: otroUser, Contraseña: pass456, Rol: Editor | Usuario añadido y visible en la lista | "otroUser" aparece en la tabla de usuarios | Coincidencia |
| Nombre: nuevoUser, Contraseña: diferente, Rol: Lector | Mensaje de error: "El nombre de usuario ya existe" | "El nombre de usuario ya existe" | Coincidencia |
| Nombre: vacío, Contraseña: cualquierPass, Rol: Lector | Mensaje de error: "El nombre de usuario no puede estar vacío" | "El nombre de usuario no puede estar vacío" | Coincidencia |
2. Casos de Prueba para Editar Usuario:
| Datos de Entrada | Resultado Esperado | Resultado Obtenido | Análisis |
|---|---|---|---|
| Seleccionar "nuevoUser", Cambiar contraseña a "newPass789" | Contraseña de "nuevoUser" actualizada | Contraseña de "nuevoUser" cambiada a "newPass789" | Coincidencia |
| Seleccionar "otroUser", Cambiar Rol a Administrador | Rol de "otroUser" actualizado | Rol de "otroUser" modificado a Administrador | Coincidencia |
| Seleccionar "nuevoUser", Vaciar campo de Nombre de Usuario | Mensaje de error: "El nombre de usuario no puede estar vacío" | "El nombre de usuario no puede estar vacío" | Coincidencia |
3. Casos de Prueba para Eliminar Usuario:
| Datos de Entrada | Resultado Esperado | Resultado Obtenido | Análisis |
|---|---|---|---|
| Eliminar "nuevoUser" y confirmar | "nuevoUser" se elimina de la lista de usuarios | "nuevoUser" ya no está presente | Coincidencia |
| Eliminar "otroUser" y cancelar | "otroUser" permanece en la lista de usuarios | "otroUser" sigue visible | Coincidencia |