Este documento detalla el diseño y la implementación de una plataforma de comercio electrónico dedicada a la venta de libros y materiales de estudio para la preparación de exámenes universitarios. La solución se concibe como una aplicación web robusta, que combina tecnologías modernas de backend y frontend para ofrecer una experiencia de usuario fluida y una gestión eficiente de los recursos.
Tecnologías Fundamentales
Backend: Spring Boot
Spring Boot se ha establecido como un marco de trabajo esencial para el desarrollo rápido de aplicaciones Java. Su principal ventaja radica en la reducción drástica de la configuración inicial, permitiendo a los desarrolladores concentrarse en la lógica de negocio. Incluye servidores embebidos como Tomcat, Jetty o Undertow, eliminando la necesidad de despliegues externos. La función de autoconfiguración es clave, ya que adapta la aplicación según las dependencias del proyecto. Además, su amplio ecosistema de módulos como Spring Data, Spring Security y Spring Cloud facilita la integración y escalabilidad, acelerando la construcción de aplicaciones de alta calidad.
Frontend: Vue.js
Vue.js es un framework progresivo para la construcción de interfaces de usuario. Su diseño se centra en la adaptabilidad y la facilidad de aprendizaje, permitiendo su integración en proyectos de diversas escalas. En su núcleo, Vue.js utiliza una Virtual DOM, una representación ligera de la interfaz de usuario en memoria, que optimiza las operaciones de manipulación del DOM real. Las características de reactividad de Vue.js aseguran que la interfaz de usuario se actualice automáticamente cuando los datos subyacentes cambian, lo que simplifica anormemente el desarrollo, liberando al programador de la gestión manual de las actualizaciones visuales. Su arquitectura basada en componentes promueve la modularidad y reutilización del código, resultando en aplicaciones más organizadas, eficientes y fáciles de mantener.
Persistencia de Datos: MyBatis-Plus
MyBatis-Plus es una capa de mejora para el framework MyBatis, diseñada para simplificar y acelerar el desarrollo de la capa de acceso a datos en aplicaciones Java. Este proyecto de código abierto es compatible con múltiples bases de datos relacionales, incluyendo MySQL, Oracle, SQL Server y PostgreSQL. Proporciona una rica API y anotaciones que facilitan las operaciones ORM (Mapeo Objeto-Relacional) con una mínima configuración, reduciendo significativamente la cantidad de SQL manual necesario. Adicionalmente, MyBatis-Plus incluye un generador de código que puede crear automáticamente clases de entidad, interfaces Mapper y archivos de mapeo XML, optimizando aún más el proceso de desarrollo. Ofrece funcionalidades avanzadas como paginación, consultas dinámicas, soporte para bloqueo optimista y análisis de rendimiento, lo que permite a los desarrolladores construir capas de acceso a datos eficientes y de alto rendimiento.
Proceso de Pruebas del Sistema
La fase de pruebas es un componente crítico en el ciclo de vida de desarrollo del sistema, cuyo objetivo principal es identificar y corregir posibles deficiencias antes de la implementación final. Se adopta una estrategia de pruebas exhaustiva para asegurar que el sistema no solo cumpla con los requisitos funcionales, sino que también ofrezca una experiencia de usuario robusta y sin errores.
Objetivos de las Pruebas
Las pruebas del sistema son esenciales para garantizar su calidad y fiabilidad. Constituyen la última línea de defensa en la detección de fallos y la verificación de la integridad general del software. El enfoque principal es prevenir problemas durante el uso por parte del usuario final, mejorando así la satisfacción. Esto implica simular diversos escenarios y comportamientos del usuario para descubrir defectos y validar que las funcionalidades se comporten según lo esperado, confirmando que el sistema satisface todas las especificaciones definidas en la etapa de diseño.
Pruebas Funcionales Detalladas
Se realizaron pruebas de caja negra para evaluar los módulos funcionales, centrándose en la interacción del usuario a través de clics, entradas de datos (incluyendo valores límite) y validaciones de campos obligatorios/opcionales. El proceso incluyó la definición de casos de prueba específicos, la ejecución de dichos casos y la evaluación de los resultados obtenidos.
Prueba de Inicio de Sesión
El proceso de inicio de sesión se validó verificando la autenticación de credenciales. El sistema debe validar que el nombre de usuario y la contraseña coincidan con los datos registrados. Cualquier discrepancia resultará en un mensaje de error. También se verificó la correcta asignación de roles de usuario al iniciar sesión.
| Datos de Entrada | Resultado Esperado | Resultado Obtenido | Análisis |
|---|---|---|---|
| Usuario: admin, Contraseña: 123456 | Acceso concedido al sistema | Inicio de sesión exitoso | Validación correcta |
| Usuario: admin, Contraseña: 654321 | Mensaje: "Contraseña incorrecta" | Mensaje: "Contraseña incorrecta, por favor reintente" | Validación correcta |
| Usuario: (vacío), Contraseña: 123456 | Mensaje: "El nombre de usuario es obligatorio" | Mensaje: "Por favor, ingrese el nombre de usuario" | Validación correcta |
| Usuario: admin, Contraseña: (vacía) | Mensaje: "La contraseña es obligatoria" | Mensaje: "La contraseña es obligatoria, por favor reintente" | Validación correcta |
Prueba de Gestión de Usuarios
La gestión de usuarios incluye las funciones de añadir, editar, eliminar y buscar usuarios. Se realizaron pruebas para validar la integridad de los datos, la unicidad de los nombres de usuario y la correcta persistencia de los cambios.
| Datos de Entrada | Resultado Esperado | Resultado Obtenido | Análisis |
|---|---|---|---|
| Datos básicos de usuario válidos | Usuario añadido, visible en la lista | Usuario aparece en la lista correctamente | Validación correcta |
| Modificación de información de usuario existente | Información editada y actualizada | Datos del usuario modificados correctamente | Validación correcta |
| Seleccionar usuario para eliminar | Confirmación de eliminación, usuario removido de la lista | Sistema pregunta confirmación, usuario no encontrado | Validación correcta |
| Añadir usuario sin nombre de usuario | Mensaje: "El nombre de usuario no puede estar vacío" | Mensaje: "El nombre de usuario no puede estar vacío" | Validación correcta |
| Añadir usuario con nombre de usuario existente | Mensaje: "El nombre de usuario ya está en uso" | Mensaje: "El nombre de usuario ya está en uso" | Validación corrrecta |
Conclusiones de las Pruebas
La estrategia principal de pruebas fue la de caja negra, simulando interacciones de usuarios para verificar la funcionalidad del sistema. Este enfoque fue crucial para asegurar la correcta ejecución de los flujos de trabajo del sistema y su usabilidad. El objetivo final de las pruebas es validar que el sistema cumpla con los requisitos funcionales y lógicos, priorizando una interacción sencilla para el usuario. Los resultados obtenidos confirman que el sistema implementado satisface los requisitos de diseño tanto en funcionalidad como en rendimiento.
Ejemplos de Código
Controlador de Autenticación
El siguiente fragmento de código ilustra un controlador REST para la autenticación de usuarios. Maneja la solicitud de inicio de sesión, valida las credenciales y, si son correctas, genera un token de sesión.
package com.example.ecommerce.auth.controller;
import com.example.ecommerce.auth.service.TokenService;
import com.example.ecommerce.user.entity.UsuarioEntidad;
import com.example.ecommerce.user.service.UsuarioService;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.Map;
@RestController
@RequestMapping("/api/autenticacion")
public class AutenticacionController {
@Autowired
private UsuarioService usuarioService;
@Autowired
private TokenService tokenService;
@PostMapping("/iniciar-sesion")
public ResponseEntity<Map<String, String>> iniciarSesion(@RequestBody Map<String, String> credenciales, HttpServletRequest request) {
String nombreUsuario = credenciales.get("nombreUsuario");
String contrasena = credenciales.get("contrasena");
String codigoCaptcha = credenciales.get("captcha"); // Opcional, según implementación
// Validación básica de credenciales
UsuarioEntidad usuario = usuarioService.obtenerUsuarioPorNombre(nombreUsuario);
if (usuario == null || !usuario.getContrasena().equals(contrasena)) { // En un entorno real, la contraseña estaría hasheada
Map<String, String> errorResponse = new HashMap<>();
errorResponse.put("mensaje", "Nombre de usuario o contraseña incorrectos.");
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(errorResponse);
}
// Generar y guardar token de sesión
String tokenGenerado = tokenService.generarTokenParaUsuario(usuario.getId(), usuario.getNombreUsuario(), "usuarios", usuario.getRol());
Map<String, String> successResponse = new HashMap<>();
successResponse.put("token", tokenGenerado);
successResponse.put("mensaje", "Inicio de sesión exitoso.");
return ResponseEntity.ok(successResponse);
}
}
Servicio de Gestión de Tokens
Este servicio es responsable de generar, actualizar y gestionar los tokens de sesión de los usuarios. Permite reutilizar un token existente o crear uno nuevo con una fecha de expiración actualizada.
package com.example.ecommerce.auth.service;
import com.example.ecommerce.auth.entity.TokenSesionEntidad;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.example.ecommerce.auth.mapper.TokenSesionMapper;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.util.UUID;
@Service
public class TokenService extends ServiceImpl<TokenSesionMapper, TokenSesionEntidad> {
/**
* Genera o actualiza un token de sesión para un usuario dado.
* Si el usuario ya tiene un token activo para ese rol, lo actualiza.
* De lo contrario, crea un nuevo token.
*
* @param idUsuario ID del usuario.
* @param nombreUsuario Nombre de usuario.
* @param nombreTabla Nombre de la tabla de referencia (e.g., "usuarios").
* @param rol Rol del usuario.
* @return El token de sesión generado o actualizado.
*/
public String generarTokenParaUsuario(Long idUsuario, String nombreUsuario, String nombreTabla, String rol) {
// Buscar un token existente para el usuario y rol
QueryWrapper<TokenSesionEntidad> queryWrapper = new QueryWrapper<TokenSesionEntidad>()
.eq("id_usuario", idUsuario)
.eq("rol", rol);
TokenSesionEntidad tokenSesion = this.getOne(queryWrapper);
String nuevoToken = UUID.randomUUID().toString().replace("-", ""); // Genera un token aleatorio
LocalDateTime fechaExpiracion = LocalDateTime.now().plusHours(1); // Token válido por 1 hora
if (tokenSesion != null) {
// Actualizar token existente
tokenSesion.setToken(nuevoToken);
tokenSesion.setFechaExpiracion(fechaExpiracion);
this.updateById(tokenSesion);
} else {
// Crear nuevo token
tokenSesion = new TokenSesionEntidad(idUsuario, nombreUsuario, nombreTabla, rol, nuevoToken, fechaExpiracion);
this.save(tokenSesion);
}
return nuevoToken;
}
/**
* Obtiene los detalles de un token de sesión si es válido y no ha expirado.
*
* @param token El valor del token.
* @return TokenSesionEntidad si es válido, de lo contrario null.
*/
public TokenSesionEntidad obtenerDetallesToken(String token) {
QueryWrapper<TokenSesionEntidad> queryWrapper = new QueryWrapper<TokenSesionEntidad>()
.eq("token_sesion", token);
TokenSesionEntidad tokenEntidad = this.getOne(queryWrapper);
if (tokenEntidad != null && tokenEntidad.getFechaExpiracion().isAfter(LocalDateTime.now())) {
return tokenEntidad;
}
return null; // Token no encontrado o expirado
}
}
Interceptor de Autorización
Este interceptor de Spring se encarga de verificar la autenticación y autorización de las solicitudes entrantes. Extrae el token de la cabecera, lo valida y, si es correcto, inyecta los detalles del usuario en la solicitud para su uso posterior.
package com.example.ecommerce.config;
import com.example.ecommerce.auth.entity.TokenSesionEntidad;
import com.example.ecommerce.auth.service.TokenService;
import com.example.ecommerce.util.PublicEndpoint; // Anotación personalizada
import com.alibaba.fastjson.JSONObject; // Asumiendo uso de FastJSON para respuestas JSON
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;
@Component
public class InterceptorAutorizacion implements HandlerInterceptor {
public static final String CABECERA_TOKEN_AUTH = "X-Auth-Token"; // Nombre de la cabecera para el token
@Autowired
private TokenService tokenService;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// Configuración para permitir solicitudes 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,request-source," + CABECERA_TOKEN_AUTH + ", Origin, Content-Type, cache-control, postman-token, Cookie, Accept, authorization");
response.setHeader("Access-Control-Allow-Origin", request.getHeader("Origin"));
// Si es una solicitud OPTIONS (preflight CORS), responder con OK
if (request.getMethod().equals(HttpMethod.OPTIONS.name())) {
response.setStatus(HttpStatus.OK.value());
return false;
}
PublicEndpoint publicEndpointAnnotation = null;
if (handler instanceof HandlerMethod) {
publicEndpointAnnotation = ((HandlerMethod) handler).getMethodAnnotation(PublicEndpoint.class);
}
// Si el método tiene la anotación @PublicEndpoint, no requiere autenticación
if (publicEndpointAnnotation != null) {
return true;
}
String tokenSesion = request.getHeader(CABECERA_TOKEN_AUTH);
if (StringUtils.isBlank(tokenSesion)) {
enviarRespuestaError(response, HttpStatus.UNAUTHORIZED.value(), "Acceso denegado: Token de autenticación ausente.");
return false;
}
TokenSesionEntidad entidadToken = tokenService.obtenerDetallesToken(tokenSesion);
if (entidadToken == null) {
enviarRespuestaError(response, HttpStatus.UNAUTHORIZED.value(), "Acceso denegado: Token de autenticación inválido o expirado.");
return false;
}
// Almacenar los detalles del usuario en los atributos de la solicitud
request.setAttribute("idUsuarioAutenticado", entidadToken.getIdUsuario());
request.setAttribute("rolUsuarioAutenticado", entidadToken.getRol());
request.setAttribute("nombreTablaUsuario", entidadToken.getNombreTabla());
request.setAttribute("nombreUsuarioAutenticado", entidadToken.getNombreUsuario());
return true;
}
/**
* Envía una respuesta de error en formato JSON al cliente.
* @param response Objeto HttpServletResponse.
* @param statusCode Código de estado HTTP.
* @param message Mensaje de error.
* @throws IOException Si ocurre un error de E/S.
*/
private void enviarRespuestaError(HttpServletResponse response, int statusCode, String message) throws IOException {
response.setStatus(statusCode);
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json; charset=utf-8");
PrintWriter writer = response.getWriter();
Map<String, Object> errorMap = new HashMap<>();
errorMap.put("codigo", statusCode);
errorMap.put("mensaje", message);
writer.print(JSONObject.toJSONString(errorMap));
writer.flush();
}
}
Esquema de Base de Datos
Tabla de Tokens de Sesión
La siguiente definición de esquema SQL describe la estructura de la tabla utilizada para almacenar los tokens de sesión de los usuarios, incluyendo su ID, nombre de usuario, rol, el token en sí y las fechas de creación y expiración.
-- ----------------------------
-- Estructura de la tabla `tokens_sesion`
-- ----------------------------
DROP TABLE IF EXISTS `tokens_sesion`;
CREATE TABLE `tokens_sesion` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'Clave primaria',
`id_usuario` bigint(20) NOT NULL COMMENT 'Identificador del usuario',
`nombre_usuario` varchar(100) NOT NULL COMMENT 'Nombre de usuario asociado al token',
`nombre_tabla` varchar(100) DEFAULT NULL COMMENT 'Tabla de origen del usuario (e.g., usuarios, administradores)',
`rol` varchar(100) DEFAULT NULL COMMENT 'Rol del usuario (e.g., ADMIN, CLIENTE)',
`token_sesion` varchar(200) NOT NULL COMMENT 'Cadena del token de sesión',
`fecha_creacion` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'Momento de creación del token',
`fecha_expiracion` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00' COMMENT 'Momento de expiración del token',
PRIMARY KEY (`id`) USING BTREE,
UNIQUE KEY `idx_token_sesion` (`token_sesion`) -- Índice para búsqueda rápida por token
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT COMMENT='Tabla para gestionar tokens de sesión de usuarios';
-- ----------------------------
-- Datos de ejemplo para `tokens_sesion`
-- ----------------------------
INSERT INTO `tokens_sesion` VALUES (1, 1, 'admin', 'usuarios', 'ADMIN', 'a4b1c2d3e4f5g6h7i8j9k0l1m2n3o4p5', '2023-10-26 10:00:00', '2023-10-26 11:00:00');
INSERT INTO `tokens_sesion` VALUES (2, 5, 'estudianteX', 'estudiantes', 'ESTUDIANTE', 'b8c7d6e5f4g3h2i1j0k9l8m7n6o5p4q3', '2023-10-26 10:15:00', '2023-10-26 11:15:00');
INSERT INTO `tokens_sesion` VALUES (3, 12, 'profesorY', 'profesores', 'PROFESOR', 'c9d8e7f6g5h4i3j2k1l0m9n8o7p6q5r4', '2023-10-26 10:30:00', '2023-10-26 11:30:00');