Sistema de Gestão de Recrutamento de Voluntários com Spring Boot e Vue.js

Arquitetura e Visão Geral do Sistema

O desenvolvimento de plataformas para coordenação de voluntariado exige uma arquitetura escalável, segura e de fácil manutenção. A solução proposta adota o modelo B/S (Browser/Server), separando claramente a camada de apresentação da lógica de negócios. O frontend é construído com Vue.js, garantindo uma interface reativa e modular, enquanto o backend utiliza Spring Boot para expor APIs RESTful padronizadas. O armazenamento de dados é gerenciado pelo MySQL, assegurando integridade relacional e desempenho otimizado em consultas concorrentes.

Implementação do Backend com Spring Boot

O núcleo da aplicação backend segue o princípio de convenção sobre configuração, eliminando a necessidade de arquivos XML extensos e acelerando a inicialização do contexto. A autenticação é baseada em tokens stateless, onde cada requisição válida carrega um identificador de sessão com tempo de vida limitado. O controlador de acesso valida as credenciais recebidas, delega a geração do token a um serviço dedicado e reetorna o identificador ao cliente. Um interceptador global verifica a presença e a validade desse token antes de liberar o acesso aos endpoints protegidos, aplicando políticas de CORS e tratando métodos OPTIONS automaticamente.

Controlador de Autenticação

@RestController
@RequestMapping("/api/auth")
public class AuthController {

    private final VolunteerService volunteerService;
    private final TokenManager tokenManager;

    public AuthController(VolunteerService volunteerService, TokenManager tokenManager) {
        this.volunteerService = volunteerService;
        this.tokenManager = tokenManager;
    }

    @PostMapping("/signin")
    public ResponseEntity<Map<String, Object>> authenticate(@RequestBody LoginRequest credentials) {
        VolunteerEntity account = volunteerService.findByEmail(credentials.getEmail());
        if (account == null || !account.getPasswordHash().equals(credentials.getPassword())) {
            return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
                    .body(Map.of("error", "Credenciais inválidas ou conta inexistente."));
        }
        String sessionToken = tokenManager.createSession(account.getId(), account.getRole());
        return ResponseEntity.ok(Map.of("accessToken", sessionToken));
    }
}

Interceptador de Validação de Sessão

@Component
public class SessionValidator implements HandlerInterceptor {

    private static final String AUTH_HEADER = "X-Session-Token";
    private final TokenRepository tokenRepository;

    public SessionValidator(TokenRepository tokenRepository) {
        this.tokenRepository = tokenRepository;
    }

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        if (request.getMethod().equals("OPTIONS")) {
            response.setStatus(HttpServletResponse.SC_OK);
            return false;
        }

        if (handler instanceof HandlerMethod method) {
            if (method.hasMethodAnnotation(PublicEndpoint.class)) {
                return true;
            }
        }

        String token = request.getHeader(AUTH_HEADER);
        if (token == null || token.isBlank()) {
            sendUnauthorized(response);
            return false;
        }

        SessionRecord record = tokenRepository.findByTokenValue(token);
        if (record == null || record.getExpiresAt().before(new Date())) {
            sendUnauthorized(response);
            return false;
        }

        request.setAttribute("currentUserId", record.getUserId());
        request.setAttribute("currentUserRole", record.getRole());
        return true;
    }

    private void sendUnauthorized(HttpServletResponse res) throws IOException {
        res.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        res.setContentType("application/json;charset=UTF-8");
        res.getWriter().write("{\"message\": \"Acesso negado. Autenticação necessária.\"}");
    }
}

Desenvolvimento Frontend com Vue.js

A camada de apresentação utiliza Vue 3 com a Composition API, facilitando a organização lógica por funcionalidade e melhorando a reutilização de código entre componentes. O roteamento é gerenciado pelo Vue Router, protegendo rotas administrativas através de navigation guards que verificam a existência e a validade do token armazenado. O estado global, como informações do usuário autenticado, listas de campanhas ativas e notificações, é centralizado via Pinia, substituindo abordagens mais verbosas e garantindo sincronização reativa em toda a árvore de componentes.

Modelagem de Dados e Estrutura Relacional

O banco de dados MySQL foi normalizado para evitar redundância e garantir consistência nas operações de recrutamento. As tabelas principais incluem registros de voluntários, campanhas disponíveis, inscrições e configurações globais. Índices compostos são aplicados em colunas frequentemente filtradas, como status de inscrição e datas de campanha, otimizando o tempo de resposta das consultas e reduzindo a carga no servidor de aplicação.

Script de Criação de Tabelas Essenciais

CREATE DATABASE IF NOT EXISTS volunteer_hub DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE volunteer_hub;

CREATE TABLE volunteers (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    full_name VARCHAR(150) NOT NULL,
    email VARCHAR(120) UNIQUE NOT NULL,
    password_hash VARCHAR(255) NOT NULL,
    role ENUM('ADMIN', 'VOLUNTEER') DEFAULT 'VOLUNTEER',
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB;

CREATE TABLE campaigns (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    title VARCHAR(200) NOT NULL,
    description TEXT,
    start_date DATE NOT NULL,
    end_date DATE NOT NULL,
    status ENUM('OPEN', 'CLOSED', 'DRAFT') DEFAULT 'DRAFT',
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB;

CREATE TABLE applications (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    volunteer_id BIGINT NOT NULL,
    campaign_id BIGINT NOT NULL,
    applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    status ENUM('PENDING', 'APPROVED', 'REJECTED') DEFAULT 'PENDING',
    FOREIGN KEY (volunteer_id) REFERENCES volunteers(id) ON DELETE CASCADE,
    FOREIGN KEY (campaign_id) REFERENCES campaigns(id) ON DELETE CASCADE,
    UNIQUE KEY unique_application (volunteer_id, campaign_id)
) ENGINE=InnoDB;

Validação Funcional e Cenários de Teste

A garantia de qualidade foi realizada através de testes de caixa preta, simulando interações reais de usuários e administradores. Cada módulo foi isolado para verificar se as saídas correspondem aos requisitos especificados. A tabela abaixo resume os casos de teste executados na interface do voluntário.

Código Funcionalidade Ação Executada Resultado Esperado Status
T-01 Cadastro Preencher formulário com dados válidos Conta criada e redirecionamento Aprovado
T-02 Autenticação Inserir e-mail e senha corretos Geração de token e acesso ao painel Aprovado
T-03 Inscrição em Campanha Clicar em "Participar" em campanha aberta Registro salvo com status PENDENTE Aprovado
T-04 Consulta de Histórico Acessar aba "Minhas Inscrições" Lista filtrada por usuário exibida Aprovado

Para o painel administrativo, os testes focaram na gestão de campanhas e moderação de inscrições. A validação confirmou que a criação, edição e remoção de registros atualizam coretamente o banco de dados e refletem imediatamente na interface, sem inconsistências de estado. A abordagem de testes contínuos durante o ciclo de desenvolvimento permitiu a identificação precoce de gargalos de desempenho e falhas de validação de entrada, resultando em uma aplicação estável e alinhada aos requisitos operacionais de recrutamento.

Tags: spring-boot vuejs MySQL java rest-api

Publicado em 9-11 07:52