From 1bfa6a6ade0278a3c55b7ab55a3c06cb97df25b7 Mon Sep 17 00:00:00 2001 From: elmer-20 <80175046+elmer-20@users.noreply.github.com> Date: Mon, 9 Mar 2026 16:49:46 -0500 Subject: [PATCH] excel_upload --- .env | 2 + Dockerfile.dev | 18 ++ docker-compose.yml | 39 ++-- docker-compose_prod.yml | 27 +++ pom.xml | 25 ++- .../controller/ExcelController.java | 32 +++ .../ingresantes/entity/Asignatura.java | 32 +++ .../ingresantes/entity/CarpetaExamen.java | 29 +++ .../repository/AsignaturaRepository.java | 11 + .../repository/CarpetaExamenRepository.java | 11 + .../repository/InscripcionRepository.java | 6 +- .../ingresantes/service/ExcelService.java | 190 ++++++++++++++++++ src/main/resources/application.properties | 24 ++- 13 files changed, 412 insertions(+), 34 deletions(-) create mode 100644 .env create mode 100644 Dockerfile.dev create mode 100644 docker-compose_prod.yml create mode 100644 src/main/java/com/service/ingresantes/controller/ExcelController.java create mode 100644 src/main/java/com/service/ingresantes/entity/Asignatura.java create mode 100644 src/main/java/com/service/ingresantes/entity/CarpetaExamen.java create mode 100644 src/main/java/com/service/ingresantes/repository/AsignaturaRepository.java create mode 100644 src/main/java/com/service/ingresantes/repository/CarpetaExamenRepository.java create mode 100644 src/main/java/com/service/ingresantes/service/ExcelService.java diff --git a/.env b/.env new file mode 100644 index 0000000..82e1f39 --- /dev/null +++ b/.env @@ -0,0 +1,2 @@ +MYSQL_DATABASE=data_admision +MYSQL_ALLOW_EMPTY_PASSWORD=yes \ No newline at end of file diff --git a/Dockerfile.dev b/Dockerfile.dev new file mode 100644 index 0000000..2c615ed --- /dev/null +++ b/Dockerfile.dev @@ -0,0 +1,18 @@ +# Usa Maven y JDK para compilar y correr código +FROM maven:3.9.2-eclipse-temurin-17 + +WORKDIR /app + +# Copia solo pom.xml primero para cache +COPY pom.xml . + +# Pre-descarga dependencias +RUN mvn dependency:go-offline + +# Copia el código fuente +COPY src ./src + +EXPOSE 8080 + +# Ejecuta Spring Boot directamente (hot reload) +CMD ["mvn", "spring-boot:run"] \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 336184f..ac06adf 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,23 +1,34 @@ services: - - postgres: - image: postgres:15 - container_name: postgres_admision + mysql: + image: mysql:8 + container_name: mysql_admision_dev environment: - POSTGRES_DB: data_admision - POSTGRES_USER: postgres - POSTGRES_PASSWORD: 1234 + MYSQL_DATABASE: admision_db + MYSQL_ALLOW_EMPTY_PASSWORD: "yes" ports: - - "5432:5432" + - "3306:3306" + volumes: + - mysql_data:/var/lib/mysql + restart: unless-stopped backend: - build: . - container_name: spring_admision + build: + context: . + dockerfile: Dockerfile.dev # Dockerfile que corre mvn spring-boot:run + container_name: spring_admision_dev depends_on: - - postgres + - mysql ports: - "8080:8080" + volumes: + - .:/app # Monta tu código para ver cambios sin rebuild + - ~/.m2:/root/.m2 # Cache de Maven para no bajar deps siempre environment: - SPRING_DATASOURCE_URL: jdbc:postgresql://postgres:5432/data_admision - SPRING_DATASOURCE_USERNAME: postgres - SPRING_DATASOURCE_PASSWORD: 1234 \ No newline at end of file + SPRING_DATASOURCE_URL: jdbc:mysql://mysql:3306/admision_db?useSSL=false&serverTimezone=UTC + SPRING_DATASOURCE_USERNAME: root + SPRING_DATASOURCE_PASSWORD: "" + command: mvn spring-boot:run # Ejecuta Spring directamente + restart: unless-stopped + +volumes: + mysql_data: \ No newline at end of file diff --git a/docker-compose_prod.yml b/docker-compose_prod.yml new file mode 100644 index 0000000..7790322 --- /dev/null +++ b/docker-compose_prod.yml @@ -0,0 +1,27 @@ +version: "3.9" + +services: + mysql: + image: mysql:8 + container_name: mysql_admision + environment: + MYSQL_DATABASE: ${MYSQL_DATABASE} + MYSQL_ALLOW_EMPTY_PASSWORD: ${MYSQL_ALLOW_EMPTY_PASSWORD} + ports: + - "3306:3306" + restart: unless-stopped + + backend: + build: + context: . + dockerfile: Dockerfile + container_name: spring_admision + depends_on: + - mysql + ports: + - "8080:8080" + environment: + SPRING_DATASOURCE_URL: jdbc:mysql://mysql:3306/${MYSQL_DATABASE}?useSSL=false&serverTimezone=UTC + SPRING_DATASOURCE_USERNAME: root + SPRING_DATASOURCE_PASSWORD: "" + restart: unless-stopped \ No newline at end of file diff --git a/pom.xml b/pom.xml index 656de31..4d0c690 100644 --- a/pom.xml +++ b/pom.xml @@ -34,10 +34,10 @@ org.springframework.boot spring-boot-starter-data-jpa - + org.springframework.boot spring-boot-starter-webmvc @@ -50,9 +50,19 @@ true - org.postgresql - postgresql - runtime + com.mysql + mysql-connector-j + 8.1.0 + + + org.apache.poi + poi + 5.2.3 + + + org.apache.poi + poi-ooxml + 5.2.3 org.projectlombok @@ -64,17 +74,18 @@ spring-boot-starter-data-jpa-test test - + org.springframework.boot spring-boot-starter-webmvc-test test + diff --git a/src/main/java/com/service/ingresantes/controller/ExcelController.java b/src/main/java/com/service/ingresantes/controller/ExcelController.java new file mode 100644 index 0000000..65a7030 --- /dev/null +++ b/src/main/java/com/service/ingresantes/controller/ExcelController.java @@ -0,0 +1,32 @@ +package com.service.ingresantes.controller; + +import com.service.ingresantes.service.ExcelService; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +@RestController +@RequestMapping("/api/excel") +public class ExcelController { + + private final ExcelService excelService; + + public ExcelController(ExcelService excelService) { + this.excelService = excelService; + } + + @PostMapping("/upload") + public ResponseEntity uploadExcel(@RequestParam("file") MultipartFile file) { + try { + excelService.importarExcel(file); + return ResponseEntity.ok("Archivo subido y procesado correctamente!"); + } catch (Exception e) { + return ResponseEntity.badRequest().body("Error al procesar el archivo: " + e.getMessage()); + } + } + + @GetMapping("/ping") + public String ping() { + return "API funcionando123456!"; + } +} \ No newline at end of file diff --git a/src/main/java/com/service/ingresantes/entity/Asignatura.java b/src/main/java/com/service/ingresantes/entity/Asignatura.java new file mode 100644 index 0000000..67aa945 --- /dev/null +++ b/src/main/java/com/service/ingresantes/entity/Asignatura.java @@ -0,0 +1,32 @@ +package com.service.ingresantes.entity; + +import jakarta.persistence.*; +import lombok.*; + +@Entity +@Table(name = "asignaturas") +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class Asignatura { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private Integer codigo; + + private String nombre; + + private Integer cantidadPreguntas; + + private Double ponderacion; + + // Nuevo campo: puntaje por pregunta + private Double puntajePorPregunta; + + @ManyToOne + @JoinColumn(name = "area_id") + private Area area; +} \ No newline at end of file diff --git a/src/main/java/com/service/ingresantes/entity/CarpetaExamen.java b/src/main/java/com/service/ingresantes/entity/CarpetaExamen.java new file mode 100644 index 0000000..cea235a --- /dev/null +++ b/src/main/java/com/service/ingresantes/entity/CarpetaExamen.java @@ -0,0 +1,29 @@ +package com.service.ingresantes.entity; + +import jakarta.persistence.*; +import lombok.*; + +@Entity +@Table(name = "carpeta_examen") +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class CarpetaExamen { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String tipo; // claves, ids, respuestas + + private String ruta; + + @ManyToOne + @JoinColumn(name = "proceso_id") + private Proceso proceso; + + @ManyToOne + @JoinColumn(name = "area_id") + private Area area; +} \ No newline at end of file diff --git a/src/main/java/com/service/ingresantes/repository/AsignaturaRepository.java b/src/main/java/com/service/ingresantes/repository/AsignaturaRepository.java new file mode 100644 index 0000000..c921dd8 --- /dev/null +++ b/src/main/java/com/service/ingresantes/repository/AsignaturaRepository.java @@ -0,0 +1,11 @@ +package com.service.ingresantes.repository; + +import com.service.ingresantes.entity.Asignatura; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface AsignaturaRepository extends JpaRepository { + // Aquí puedes agregar consultas personalizadas si lo necesitas + // Ejemplo: List findByNombre(String nombre); +} \ No newline at end of file diff --git a/src/main/java/com/service/ingresantes/repository/CarpetaExamenRepository.java b/src/main/java/com/service/ingresantes/repository/CarpetaExamenRepository.java new file mode 100644 index 0000000..3648c7b --- /dev/null +++ b/src/main/java/com/service/ingresantes/repository/CarpetaExamenRepository.java @@ -0,0 +1,11 @@ +package com.service.ingresantes.repository; + +import com.service.ingresantes.entity.CarpetaExamen; +import org.springframework.data.jpa.repository.JpaRepository; +import java.util.List; + +public interface CarpetaExamenRepository extends JpaRepository { + + List findByProcesoIdAndAreaId(Long procesoId, Long areaId); + +} \ No newline at end of file diff --git a/src/main/java/com/service/ingresantes/repository/InscripcionRepository.java b/src/main/java/com/service/ingresantes/repository/InscripcionRepository.java index 3832ae2..1e5201b 100644 --- a/src/main/java/com/service/ingresantes/repository/InscripcionRepository.java +++ b/src/main/java/com/service/ingresantes/repository/InscripcionRepository.java @@ -1,11 +1,13 @@ package com.service.ingresantes.repository; import com.service.ingresantes.entity.Inscripcion; +import com.service.ingresantes.entity.Postulante; +import com.service.ingresantes.entity.Proceso; import org.springframework.data.jpa.repository.JpaRepository; -import java.util.List; public interface InscripcionRepository extends JpaRepository { - List findByProcesoId(Long procesoId); + // Método para verificar si existe una inscripción de un postulante a un proceso + boolean existsByPostulanteAndProceso(Postulante postulante, Proceso proceso); } \ No newline at end of file diff --git a/src/main/java/com/service/ingresantes/service/ExcelService.java b/src/main/java/com/service/ingresantes/service/ExcelService.java new file mode 100644 index 0000000..dcfcc7d --- /dev/null +++ b/src/main/java/com/service/ingresantes/service/ExcelService.java @@ -0,0 +1,190 @@ +package com.service.ingresantes.service; + +import com.service.ingresantes.entity.*; +import com.service.ingresantes.repository.*; +import org.apache.poi.ss.usermodel.*; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; +import jakarta.transaction.Transactional; + +import java.io.InputStream; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Optional; + +@Service +public class ExcelService { + + private final PostulanteRepository postulanteRepo; + private final ProcesoRepository procesoRepo; + private final ProgramaRepository programaRepo; + private final ModalidadRepository modalidadRepo; + private final InscripcionRepository inscripcionRepo; + + public ExcelService(PostulanteRepository postulanteRepo, + ProcesoRepository procesoRepo, + ProgramaRepository programaRepo, + ModalidadRepository modalidadRepo, + InscripcionRepository inscripcionRepo) { + this.postulanteRepo = postulanteRepo; + this.procesoRepo = procesoRepo; + this.programaRepo = programaRepo; + this.modalidadRepo = modalidadRepo; + this.inscripcionRepo = inscripcionRepo; + } + + // --- Método auxiliar para leer cualquier celda como String --- + private String getCellStringValue(Cell cell) { + if (cell == null) return ""; + switch (cell.getCellType()) { + case STRING: + return cell.getStringCellValue().trim(); + case NUMERIC: + if (DateUtil.isCellDateFormatted(cell)) { + return cell.getLocalDateTimeCellValue().toLocalDate().toString(); + } else { + return String.valueOf((long) cell.getNumericCellValue()); + } + case BOOLEAN: + return String.valueOf(cell.getBooleanCellValue()); + case FORMULA: + return cell.getCellFormula(); + default: + return ""; + } + } + + @Transactional + public void importarExcel(MultipartFile file) throws Exception { + InputStream is = file.getInputStream(); + Workbook workbook = WorkbookFactory.create(is); + Sheet sheet = workbook.getSheetAt(0); + + DateTimeFormatter fechaHoraFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + + for (Row row : sheet) { + if (row.getRowNum() == 0) continue; // Saltar encabezado + + String dni = getCellStringValue(row.getCell(0)); + if (dni.isEmpty()) { + System.out.println("Fila " + row.getRowNum() + ": DNI vacío, se omite"); + continue; + } + + Postulante postulante = postulanteRepo.findById(dni).orElseGet(() -> { + try { + Postulante p = new Postulante(); + p.setDni(dni); + p.setPaterno(getCellStringValue(row.getCell(1))); + p.setMaterno(getCellStringValue(row.getCell(2))); + p.setNombres(getCellStringValue(row.getCell(3))); + p.setSexo(getCellStringValue(row.getCell(4))); + + String fechaNacStr = getCellStringValue(row.getCell(5)); + if (!fechaNacStr.isEmpty()) { + try { p.setFechaNacimiento(LocalDate.parse(fechaNacStr)); } catch (Exception e) {} + } + + String edadStr = getCellStringValue(row.getCell(6)); + if (!edadStr.isEmpty()) { + try { p.setEdad(Integer.parseInt(edadStr)); } catch (NumberFormatException ignored) {} + } + + p.setUbigeoResidencia(getCellStringValue(row.getCell(7))); + p.setDepartamentoResidencia(getCellStringValue(row.getCell(8))); + p.setProvinciaResidencia(getCellStringValue(row.getCell(9))); + p.setDistritoResidencia(getCellStringValue(row.getCell(10))); + + String egresoStr = getCellStringValue(row.getCell(11)); + if (!egresoStr.isEmpty()) { + try { p.setEgreso(Integer.parseInt(egresoStr)); } catch (NumberFormatException ignored) {} + } + + p.setCodModular(getCellStringValue(row.getCell(12))); + p.setUbigeoColegio(getCellStringValue(row.getCell(13))); + p.setDepartamentoColegio(getCellStringValue(row.getCell(14))); + p.setProvinciaColegio(getCellStringValue(row.getCell(15))); + p.setDistritoColegio(getCellStringValue(row.getCell(16))); + + System.out.println("Fila " + row.getRowNum() + ": Postulante creado -> " + dni); + return postulanteRepo.save(p); + } catch (Exception e) { + System.out.println("Fila " + row.getRowNum() + ": Error guardando postulante " + dni + " -> " + e.getMessage()); + return null; + } + }); + + if (postulante == null) continue; + + String fechaInsStr = getCellStringValue(row.getCell(17)); + LocalDateTime fechaInscripcion; + if (!fechaInsStr.isEmpty()) { + try { + fechaInscripcion = LocalDateTime.parse(fechaInsStr, fechaHoraFormatter); + } catch (Exception e) { + try { fechaInscripcion = LocalDate.parse(fechaInsStr).atStartOfDay(); } + catch (Exception ex) { fechaInscripcion = LocalDateTime.now(); } + } + } else { + fechaInscripcion = LocalDateTime.now(); + } + + String procesoStr = getCellStringValue(row.getCell(18)); + if (procesoStr.isEmpty()) { + System.out.println("Fila " + row.getRowNum() + ": Proceso vacío, se omite"); + continue; + } + Long procesoId; + try { procesoId = Long.parseLong(procesoStr); } + catch (NumberFormatException e) { + System.out.println("Fila " + row.getRowNum() + ": Proceso inválido -> " + procesoStr); + continue; + } + Optional procesoOpt = procesoRepo.findById(procesoId); + if (procesoOpt.isEmpty()) { + System.out.println("Fila " + row.getRowNum() + ": Proceso no encontrado -> " + procesoId); + continue; + } + Proceso proceso = procesoOpt.get(); + + String programaStr = getCellStringValue(row.getCell(19)); + Optional programaOpt = programaRepo.findById(Long.parseLong(programaStr)); + if (programaOpt.isEmpty()) { + System.out.println("Fila " + row.getRowNum() + ": Programa no encontrado -> " + programaStr); + continue; + } + ProgramaEstudio programa = programaOpt.get(); + + String modalidadStr = getCellStringValue(row.getCell(20)); + Optional modalidadOpt = modalidadRepo.findById(Long.parseLong(modalidadStr)); + if (modalidadOpt.isEmpty()) { + System.out.println("Fila " + row.getRowNum() + ": Modalidad no encontrada -> " + modalidadStr); + continue; + } + Modalidad modalidad = modalidadOpt.get(); + + boolean existe = inscripcionRepo.existsByPostulanteAndProceso(postulante, proceso); + if (existe) { + System.out.println("Fila " + row.getRowNum() + ": La inscripción ya existe para el postulante " + dni); + continue; + } + + try { + Inscripcion inscripcion = new Inscripcion(); + inscripcion.setFechaInscripcion(fechaInscripcion); + inscripcion.setPostulante(postulante); + inscripcion.setProceso(proceso); + inscripcion.setPrograma(programa); + inscripcion.setModalidad(modalidad); + inscripcionRepo.save(inscripcion); + System.out.println("Fila " + row.getRowNum() + ": Inscripción guardada -> " + dni); + } catch (Exception e) { + System.out.println("Fila " + row.getRowNum() + ": Error guardando inscripción -> " + e.getMessage()); + } + } + + workbook.close(); + is.close(); + } +} \ No newline at end of file diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 891fb56..2f0f540 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -14,19 +14,21 @@ # con docker - spring.application.name=ingresantes -# CONEXION POSTGRESQL (VARIABLES DE ENTORNO) -spring.datasource.url=${SPRING_DATASOURCE_URL} -spring.datasource.username=${SPRING_DATASOURCE_USERNAME} -spring.datasource.password=${SPRING_DATASOURCE_PASSWORD} -spring.datasource.driver-class-name=org.postgresql.Driver +spring.datasource.url=${SPRING_DATASOURCE_URL:jdbc:mysql://localhost:3306/data_admision?useSSL=false&serverTimezone=UTC} +spring.datasource.username=${SPRING_DATASOURCE_USERNAME:root} +spring.datasource.password=${SPRING_DATASOURCE_PASSWORD:} +spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver -# JPA / HIBERNATE -spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect +spring.jpa.database-platform=org.hibernate.dialect.MySQLDialect spring.jpa.hibernate.ddl-auto=update - -# MOSTRAR SQL spring.jpa.show-sql=true -spring.jpa.properties.hibernate.format_sql=true \ No newline at end of file +spring.jpa.properties.hibernate.format_sql=true + +# Habilitar subida de archivos +spring.servlet.multipart.enabled=true + +# Tamaño máximo de archivo y request +spring.servlet.multipart.max-file-size=10MB +spring.servlet.multipart.max-request-size=10MB \ No newline at end of file