Trabalho Final - Disciplina de Engenharia de Software I - CK0247 2026.1 API REST para plataforma de preparação para pré-vestibulandos, com questões ENEM, turmas, simulados e gamificação.
- Visão Geral
- Funcionalidades
- Arquitetura
- Stack Tecnológica
- Estrutura do Projeto
- Modelo de Dados
- Endpoints da API
- Segurança
- Integração com API Externa
- Configuração e Execução
- Variáveis de Ambiente
- Documentação Swagger
- Decisões de Design
- Equipe
O NeoQuest é uma API REST desenvolvida em Java com Spring Boot voltada para estudantes que se preparam para o ENEM. A plataforma permite que alunos acessem questões de provas anteriores, organizem grupos de estudo (turmas), realizem simulados cronometrados e acompanhem seu desempenho por meio de um sistema de gamificação com pontos e streaks.
O projeto integra uma API pública do ENEM para importar e armazenar questões reais de provas anteriores (2009–2023) em cache local, garantindo alta disponibilidade e performance nas consultas.
- Cadastro de novos usuários com validação de senha forte
- Login com geração de token JWT
- Consulta e edição de perfil (nome, e-mail, senha, foto, data de nascimento)
- Deleção de conta com confirmação de senha
- Listagem de questões com filtros por ano, disciplina, idioma e termo de busca
- Paginação e ordenação por número de questão
- Favoritar/desfavoritar questões (toggle)
- Listagem de questões favoritas do usuário autenticado
- Registro e histórico de respostas por questão
- Criação, Edição e exclusão de questões personalizadas criadas dentro de turmas
- Endpoints utilitários para importação de questões da API externa por ano (completo ou paginado)
- Criação e gestão de turmas com código de convite único
- Ingresso em turmas via código
- Roles por turma:
MEMBROeADMINISTRADOR - Promoção, rebaixamento, saída e remoção de membros
- Publicações na turma com comentários
- Associação de questões personalizadas e simulados à turma
- Criação de simulados vinculados a uma turma
- Aplicação de simulado e envio de respostas pelo aluno
- Registro de resultado com acertos, tempo gasto e desempenho detalhado
- Consulta de desempenho por turma e histórico de tentativas
- Pontos acumulados por acertos e atividades
- Streak de dias consecutivos de acesso
- Ranking de usuários dentro de turmas
O NeoQuest segue uma arquitetura em camadas clássica do ecossistema Spring:
Controller → Service → Repository → Database (Supabase / PostgreSQL)
↓
Feign Client → API Externa (api.enem.dev)
Princípios aplicados:
- Separação de responsabilidades: lógica de negócio exclusivamente nos Services
- DTOs na borda: entidades JPA nunca são expostas diretamente; DTOs controlam entrada e saída
- Cache local: questões da API externa são persistidas no banco para evitar dependência contínua do serviço externo
- Stateless: autenticação via JWT, sem sessão no servidor
- Tratamento centralizado de erros:
GlobalExceptionHandlercobre todos os cenários de exceção
| Camada | Tecnologia |
|---|---|
| Linguagem | Java 17 |
| Framework | Spring Boot 3.5.14 |
| Segurança | Spring Security + JWT (jjwt 0.12.6) |
| Persistência | Spring Data JPA / Hibernate |
| Banco de Dados | PostgreSQL (via Supabase) |
| Cliente HTTP | Spring Cloud OpenFeign |
| Validação | Jakarta Validation (Bean Validation 3) |
| Spring Mail | |
| Mapeamento | Mapper manual (QuestaoMapper, SimuladoMapper, TurmaMapper, UsuarioMapper) |
| Documentação | Não configurada no estado atual do projeto |
| Build | Maven |
| Variáveis de Ambiente | java-dotenv |
| Testes | JUnit 5 + H2 em memória |
| Utilitários | Lombok |
src/main/java/NeoQuestGroup/NeoQuest/
│
├── NeoQuestApplication.java
├── client/ # integração com a API externa do ENEM
│ ├── FeignClientConfig.java
│ └── QuestaoApiClient.java
├── controller/ # endpoints REST da aplicação
│ ├── AuthController.java
│ ├── QuestaoController.java
│ ├── SimuladoController.java
│ ├── TurmaController.java
│ └── UsuarioController.java
├── dto/ # contratos de entrada e saída
│ ├── AlternativaDTO.java
│ ├── CadastroDTO.java
│ ├── ComentarioTurmaDTO.java
│ ├── ComentarioTurmaResponseDTO.java
│ ├── CriarSimuladoDTO.java
│ ├── CriarTurmaDTO.java
│ ├── DeletarContaDTO.java
│ ├── DesempenhoAlunoSimuladoDTO.java
│ ├── EditarQuestaoDTO.java
│ ├── EditarTurmaDTO.java
│ ├── EditarUsuarioDTO.java
│ ├── FavoritoResponseDTO.java
│ ├── HistoricoRespostaDTO.java
│ ├── LoginDTO.java
│ ├── MembroResponseDTO.java
│ ├── PublicacaoTurmaDTO.java
│ ├── PublicacaoTurmaResponseDTO.java
│ ├── QuestaoDTO.java
│ ├── QuestaoResponseDTO.java
│ ├── RespostaDTO.java
│ ├── RespostaQuestaoResponseDTO.java
│ ├── RespostaSimuladoResponseDTO.java
│ ├── ResultadoDetalhadoResponseDTO.java
│ ├── ResultadoResponseDTO.java
│ ├── SimuladoResponseDTO.java
│ ├── SubmissaoDTO.java
│ ├── TokenResponseDTO.java
│ ├── TurmaResponseDTO.java
│ ├── UsuarioResponseDTO.java
│ └── externa/
├── exception/ # tratamento centralizado de erros
│ ├── ApiError.java
│ └── GlobalExceptionHandler.java
├── mapper/ # conversão entre DTOs e entidades
│ ├── QuestaoMapper.java
│ ├── SimuladoMapper.java
│ ├── TurmaMapper.java
│ └── UsuarioMapper.java
├── model/ # entidades JPA, enums e relacionamentos
│ ├── AlternativaCache.java
│ ├── ComentarioPost.java
│ ├── ComentarioTurma.java
│ ├── Favorito.java
│ ├── Post.java
│ ├── PublicacaoTurma.java
│ ├── QuestaoCache.java
│ ├── RespostaQuestao.java
│ ├── ResultadoSimulado.java
│ ├── ResultadoSimuladoResposta.java
│ ├── RoleTurmaEnum.java
│ ├── Simulado.java
│ ├── TipoPublicacaoEnum.java
│ ├── TipoQuestaoEnum.java
│ ├── Turma.java
│ ├── Usuario.java
│ └── UsuarioTurma.java
├── repository/ # acesso e consulta de dados
│ ├── ComentarioPostRepository.java
│ ├── ComentarioTurmaRepository.java
│ ├── FavoritoRepository.java
│ ├── PostRepository.java
│ ├── PublicacaoTurmaRepository.java
│ ├── QuestaoCacheRepository.java
│ ├── RespostaQuestaoRepository.java
│ ├── ResultadoSimuladoRepository.java
│ ├── SimuladoRepository.java
│ ├── TurmaRepository.java
│ ├── UsuarioRepository.java
│ └── UsuarioTurmaRepository.java
├── security/ # autenticação, JWT e regras de acesso
│ ├── JwtAuthFilter.java
│ ├── SecurityConfig.java
│ ├── UserDetailsServiceImpl.java
│ ├── UsuarioPrincipal.java
│ └── JwtUtil.java
├── service/ # regras de negócio da aplicação
│ ├── AuthService.java
│ ├── GamificacaoService.java
│ ├── QuestaoService.java
│ ├── SimuladoService.java
│ ├── TurmaService.java
│ └── UsuarioService.java
└── util/ # helpers e utilidades auxiliares
├── AuthUtils.java
└── JwtUtil.java
Usuario (1) ──── (N) UsuarioTurma (N) ──── (1) Turma
└── RoleTurmaEnum {MEMBRO, ADMINISTRADOR}
Turma (1) ──── (N) PublicacaoTurma (1) ──── (N) ComentarioTurma
Turma (1) ──── (N) Simulado (N) ──── (M) QuestaoCache
Turma (1) ──── (N) QuestaoCache [turmaOrigem]
Usuario (1) ──── (N) ResultadoSimulado (N) ──── (1) Simulado
Usuario (1) ──── (N) Favorito (N) ──── (1) QuestaoCache
Usuario (1) ──── (N) RespostaQuestao (N) ──── (1) QuestaoCache
Usuario (1) ──── (N) Post (1) ──── (N) ComentarioPost
Usuario (1) ──── (N) Post [curtidas - ManyToMany]
QuestaoCache (1) ──── (N) AlternativaCache
QuestaoCache ──── ElementCollection: files (URLs de imagens)
questao_cache possui constraint única em (ano, index_questao, lingua) para evitar duplicatas durante a importação da API externa.
resposta_questao possui constraint única em (usuario_id, questao_cache_id) para impedir que o mesmo usuário responda a mesma questão mais de uma vez.
| Método | Endpoint | Auth | Descrição |
|---|---|---|---|
POST |
/auth/register |
❌ | Cadastrar novo usuário |
POST |
/auth/login |
❌ | Autenticar e obter JWT |
Exemplo — Cadastro:
POST /auth/register
{
"nome": "João Silva",
"email": "joao@email.com",
"senha": "Senha@123"
}Exemplo — Login:
POST /auth/login
{
"email": "joao@email.com",
"senha": "Senha@123"
}
// Resposta: { "token": "eyJhbGci..." }| Método | Endpoint | Auth | Descrição |
|---|---|---|---|
GET |
/usuarios/me |
✅ | Retornar perfil do usuário autenticado |
PUT |
/usuarios/me |
✅ | Editar nome, e-mail, senha, foto, data de nascimento |
DELETE |
/usuarios/me |
✅ | Deletar conta (requer confirmação de senha) |
Regra de negócio: Alterar e-mail ou senha exige que senhaAtual seja fornecida e correta.
| Método | Endpoint | Auth | Descrição |
|---|---|---|---|
GET |
/questoes |
Opcional | Listar questões com filtros e paginação |
POST |
/questoes/{id}/favoritar |
✅ | Toggle favorito (favorita ou desfavorita) |
GET |
/questoes/favoritas |
✅ | Listar questões favoritas do usuário |
POST |
/questoes/{id}/responder |
✅ | Registrar resposta do aluno para uma questão |
GET |
/questoes/historico |
✅ | Consultar histórico de respostas do usuário |
PUT |
/questoes/{id} |
✅ | Editar questão personalizada da turma |
DELETE |
/questoes/{id} |
✅ | Excluir questão personalizada da turma |
POST |
/questoes/admin/download-completo |
✅ | Importar todas as questões de um ano (utilitário de desenvolvimento) |
POST |
/questoes/admin/download-parcial |
✅ | Importar um bloco de questões a partir de um offset |
Parâmetros de filtro (GET /questoes):
| Parâmetro | Tipo | Obrigatório | Valores válidos |
|---|---|---|---|
ano |
Integer |
✅ | 2009 a 2023 |
disciplina |
String |
❌ | linguagens, matematica, ciencias-natureza, ciencias-humanas |
language |
String |
❌ | ingles, espanhol |
termo |
String |
❌ | Busca textual no enunciado e contexto |
page |
int |
❌ | Default: 0 |
size |
int |
❌ | Default: 10 |
Exemplo de resposta:
{
"content": [
{
"id": 42,
"ano": 2022,
"index": 1,
"disciplina": "matematica",
"lingua": null,
"contexto": "Um investidor...",
"comandoQuestao": "Qual é o rendimento...",
"alternativaCorreta": "C",
"files": [],
"alternativas": [
{ "letter": "A", "text": "...", "file": null, "isCorrect": false },
{ "letter": "C", "text": "...", "file": null, "isCorrect": true }
],
"favoritada": false
}
],
"totalElements": 180,
"totalPages": 18
}Nota: O campo
favoritadaénullpara usuários anônimos etrue/falsepara autenticados.
| Método | Endpoint | Auth | Descrição |
|---|---|---|---|
POST |
/simulados |
✅ | Criar um novo simulado |
GET |
/simulados/{id} |
✅ | Abrir um simulado para resposta |
POST |
/simulados/{id}/respostas |
✅ | Enviar as respostas e processar o resultado |
GET |
/simulados/{id}/resultado |
✅ | Consultar resultado detalhado |
GET |
/simulados/{id}/desempenho |
✅ | Ver desempenho da turma no simulado |
DELETE |
/simulados/{id} |
✅ | Excluir simulado |
| Método | Endpoint | Auth | Descrição |
|---|---|---|---|
POST |
/turmas |
✅ | Criar uma nova turma |
POST |
/turmas/entrar?codigo=... |
✅ | Entrar em uma turma por código |
GET |
/turmas/participo |
✅ | Listar turmas das quais o usuário participa |
GET |
/turmas/{id}/membros |
✅ | Consultar membros da turma |
PATCH |
/turmas/{id}/membros/{userId}/promover |
✅ | Promover membro a administrador |
PATCH |
/turmas/{id}/membros/{userId}/rebaixar |
✅ | Rebaixar administrador a membro |
POST |
/turmas/{id}/publicacoes |
✅ | Criar publicação na turma |
GET |
/turmas/{id}/publicacoes |
✅ | Listar publicações da turma |
POST |
/turmas/{id}/publicacoes/{pubId}/comentarios |
✅ | Adicionar comentário em uma publicação |
POST |
/turmas/{turmaId}/questoes |
✅ | Criar questão personalizada para a turma |
1. Cliente → POST /auth/login com {email, senha}
2. Spring Security autentica via DaoAuthenticationProvider
3. JwtUtil gera token HS256 assinado (claim: email + userId)
4. Cliente inclui token em requisições: Authorization: Bearer <token>
5. JwtAuthFilter extrai e valida o token a cada requisição
6. UsuarioPrincipal wraps a entidade Usuario — sem second query ao banco
/auth/** público (sem autenticação)
/public/** público
/** requer token JWT válido
OPTIONS /** permitido para pré-flight CORS
A senha deve conter entre 8 e 30 caracteres e incluir obrigatoriamente:
- Ao menos uma letra maiúscula
- Ao menos uma letra minúscula
- Ao menos um número
- Ao menos um caractere especial (
@$!%*?&)
Todos os erros seguem o formato padronizado ApiError:
{
"timestamp": "2024-11-15T10:30:00.000Z",
"status": 400,
"error": "Validation Failed",
"message": "Um ou mais campos são inválidos.",
"path": "/auth/register",
"errors": {
"senha": "A senha deve conter entre 8 e 30 caracteres..."
}
}Exceções tratadas pelo GlobalExceptionHandler:
| Exceção | Status HTTP |
|---|---|
MethodArgumentNotValidException |
400 Bad Request |
ConstraintViolationException |
400 Bad Request |
HttpMessageNotReadableException |
400 Bad Request |
MethodArgumentTypeMismatchException |
400 Bad Request |
IllegalArgumentException |
400 Bad Request |
EntityNotFoundException |
404 Not Found |
IllegalStateException |
409 Conflict |
DataIntegrityViolationException |
409 Conflict |
HttpRequestMethodNotSupportedException |
405 Method Not Allowed |
BadCredentialsException / UsernameNotFoundException |
401 Unauthorized |
AccessDeniedException |
403 Forbidden |
FeignException |
502 Bad Gateway |
feign.RetryableException |
504 Gateway Timeout |
Exception (fallback) |
500 Internal Server Error |
Em ambiente
dev(spring.profiles.active=dev), o campodetailda resposta expõe a mensagem técnica da exceção.
O NeoQuest consome uma API pública do ENEM via Spring Cloud OpenFeign.
Para evitar dependência contínua da API externa e garantir performance nas consultas:
- As questões são baixadas sob demanda pelos endpoints admin (
/questoes/admin/download-completoou/questoes/admin/download-parcial). - Cada questão é persistida na tabela
questao_cachecom constraint de unicidade por(ano, index_questao, lingua). - Nas consultas públicas (
GET /questoes), o banco local é sempre consultado com queries nativas usandoILIKEdo PostgreSQL para busca case-insensitive.
| Parâmetro | Valor |
|---|---|
| Timeout de conexão | 3 segundos |
| Timeout de leitura | 30 segundos |
| Tentativas de retry | 3 (em erros de rede) |
| Intervalo entre retries | 1s inicial, máx 3s |
| Log level | BASIC |
POST /questoes/admin/download-completo?ano=2022
└── QuestaoService.forcarDownloadCompleto()
└── Loop: QuestaoApiClient.listarQuestoesPorAno(ano, limit=50, offset)
└── Até que metadata.hasMore == false
└── TransactionTemplate: persistirNovasQuestoes()
└── Filtra duplicatas por chave (ano-index-lingua)
└── QuestaoCacheRepository.saveAll()
- Java 17+
- Maven 3.8+
- Instância PostgreSQL (local ou Supabase)
- Arquivo
.envna raiz do projeto
# Clonar o repositório
git clone https://github.com/neoquest/NeoQuestBack.git
cd NeoQuestBack
# Instalar dependências e compilar
mvn clean install -DskipTests
# Executar a aplicação
mvn spring-boot:runJWT_SECRET=sua_chave_secreta_muito_longa_e_segura_aqui
SUPABASE_URL=jdbc:postgresql://<host>:<porta>/<banco>
SUPABASE_USERNAME=postgres
SUPABASE_PASSWORD=sua_senha_aquispring:
datasource:
url: ${SUPABASE_URL}
username: ${SUPABASE_USERNAME}
password: ${SUPABASE_PASSWORD}
sql:
init:
mode: never
jpa:
hibernate:
ddl-auto: none
show-sql: true
server:
port: 8080
jwt:
secret: ${JWT_SECRET}
expiration: 7200000
enem:
api:
url: https://api.enem.dev/v1| Variável | Descrição | Obrigatória |
|---|---|---|
JWT_SECRET |
Chave secreta para assinatura dos tokens JWT (mín. 32 caracteres) | ✅ |
SUPABASE_URL |
URL JDBC de conexão ao banco PostgreSQL | ✅ |
SUPABASE_USERNAME |
Usuário do banco de dados | ✅ |
SUPABASE_PASSWORD |
Senha do banco de dados | ✅ |
As variáveis são carregadas pelo
java-dotenvno métodomain()deNeoQuestApplicatione injetadas comoSystem.setProperty, ficando disponíveis para o Spring via${...}.
No estado atual do projeto, não há documentação Swagger/OpenAPI habilitada. A API é consumida diretamente pelos controllers e os contratos principais ficam representados nos DTOs e no próprio README.
Métodos residem nos Services, mantendo as entidades JPA como simples mapeamentos do banco.
Simulado representa o template (questões + configurações). ResultadoSimulado registra a tentativa de um usuário específico. Essa separação permite múltiplos usuários realizando o mesmo simulado com resultados independentes.
A relação Usuário-Turma é muitos-para-muitos (N-N) com dados adicionais (role e último acesso). Modelar como entidade explícita UsuarioTurma com RoleTurmaEnum é mais expressivo que uma tabela de junção simples.
Ao carregar a entidade Usuario completa durante a autenticação e encapsulá-la no UsuarioPrincipal, elimina-se a necessidade de consultas adicionais ao banco para obter dados do usuário nas requisições subsequentes.
O QuestaoService usa TransactionTemplate em vez de @Transactional porque o download das questões (chamadas HTTP) não deve ocorrer dentro de uma transação de banco de dados aberta — apenas a persistência deve ser transacional.
Os métodos de download e persistência são synchronized para evitar condições de corrida quando múltiplas requisições admin tentam importar o mesmo ano simultaneamente.
A busca textual usa SQL nativo com ILIKE (extensão PostgreSQL) em vez de JPQL para garantir busca case-insensitive eficiente em colunas TEXT sem problemas de cast.
Equipe M — Disciplina de Engenharia de Software
| Membro | Papel |
|---|---|
| Aluizio Almendra | DevOps / Analista de requisitos |
| Américo Vitor | Database Dev / FullStack Dev |
| Gabriel Teixeira | UI/UX / Frontend Dev |
| João Guilherme | UI/UX |
| Jonathan Duarte | QA Tester / Backend Dev |
| Samuel Augusto | Backend Dev / Gerente de projeto |
Este projeto foi desenvolvido para fins acadêmicos no contexto da disciplina de Engenharia de Software.
Última atualização: 03 de Julho de 2026