Backend do sistema do Observatório de Turismo de Olímpia (Spring Boot + PostgreSQL): inventário dos meios de hospedagem, pesquisas de ocupação com os parceiros, cálculo e publicação dos resultados pela metodologia do OTO, série histórica e dados dos dashboards.
-
Copie o arquivo
.env.examplerenomeando para.enve preencha:POSTGRES_DB,POSTGRES_USER,POSTGRES_PASSWORD: o container do PostgreSQL cria esse banco e esse usuário na primeira subida, então não é preciso criar nada antes;JWT_SEGREDO: segredo dos tokens de login, com pelo menos 32 caracteres (gere comopenssl rand -hex 32). Sem ele a aplicação não sobe;ADMIN_EMAILeADMIN_SENHA: o primeiro administrador, criado na subida se ainda não existir um usuário com esse e-mail.
O
.envnão vai para o Git: cada pessoa cria o seu. -
No IntelliJ, abra a pasta do projeto (a que tem o
build.gradle). Com o Docker aberto, rode a taskbootRun(aba Gradle → Tasks → application → bootRun), a classeOtoProjectApplicationou./gradlew bootRunno terminal. O Spring Boot sobe o PostgreSQL docompose.yaml, espera ele ficar pronto e conecta sozinho; o Flyway aplica as migrations em seguida. Ocompose.yamle o.envsão lidos da pasta em que a aplicação roda: com uma pasta acima aberta no IntelliJ, a subida falha comNo Docker Compose file found. -
Entre com o e-mail e a senha do administrador (
POST /api/auth/login) e troque a senha pelo sistema (PUT /api/me/senha).
O banco continua rodando quando a aplicação para. Para desligar: docker compose stop.
Para rodar tudo no Docker, sem IDE (por exemplo, para o front-end usar a API):
docker compose --profile app up --build # http://localhost:8080As variáveis do .env do banco só são aplicadas quando o volume é criado. Para trocar a
senha do banco depois, recrie com docker compose down -v (isso apaga os dados locais).
- Documentação interativa: http://localhost:8080/swagger-ui.html (use Authorize com o token do login).
- Exemplos de corpo e de resposta de cada rota, e o fluxo de uma pesquisa: docs/API.md.
Todas as rotas exigem login, menos as marcadas como público na tabela de rotas.
- O front chama
POST /api/auth/logincom{ "email": "...", "senha": "..." }e recebe{ "token": "...", "expiraEm": "...", "usuario": { ... } }. - Em todas as outras chamadas, manda o token no cabeçalho
Authorization: Bearer <token>. - O token vale 8 horas. Vencido ou inválido, a API responde 401 e o front volta para o login (não há renovação automática).
const login = await fetch('http://localhost:8080/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, senha }),
});
const { token, usuario } = await login.json();
const meusDados = await fetch('http://localhost:8080/api/me', {
headers: { Authorization: `Bearer ${token}` },
});O perfil vem em usuario.perfil (no login e em GET /api/me). O front pode usá-lo para mostrar
ou esconder telas, mas quem barra é a API: chamada fora do perfil recebe 403.
O primeiro acesso não tem senha: o ADMIN cadastra o usuário e recebe um link de convite para
mandar a ele. A página de senha do front lê o token do link e chama
POST /api/auth/definir-senha com { "token": "...", "novaSenha": "..." }.
O front precisa estar num endereço liberado em CORS_ORIGENS (no .env). O padrão libera
localhost e 127.0.0.1 em qualquer porta.
Erros vêm no formato Problem Details (RFC 9457), com a mensagem em detail:
| Status | Quando |
|---|---|
| 400 | Corpo ou parâmetro inválido; campos traz a mensagem de cada campo |
| 401 | Sem token, token vencido ou e-mail e senha errados no login |
| 403 | O perfil não pode usar a rota, ou o estabelecimento não é do parceiro |
| 404 | Registro não encontrado |
| 409 | Registro repetido no banco (raro) |
| 422 | Regra do OTO, como pesquisa fechada ou mínimo de leitos para publicar |
ADMIN (equipe do OTO, tudo), PARCEIRO (inventário e respostas dos seus estabelecimentos,
resultados gerais) e VISUALIZADOR (só consulta). Respostas individuais de ocupação são
sigilosas: só o ADMIN e o próprio parceiro as veem.
A equipe do OTO (ADMIN) pode digitar ou corrigir à mão qualquer dado de entrada, inclusive os
que normalmente chegam pela pesquisa: respostas (também depois do encerramento, até publicar),
diárias médias por tipo, capacidade, RHC, parâmetros, catálogo do inventário, tabelas de
referência e série histórica. Os resultados não são digitados: saem sempre do cálculo. A lista
completa está em docs/API.md.
Na coluna Quem pode: público dispensa o token; todos é qualquer usuário logado; "só os seus" quer dizer que o parceiro só acessa os estabelecimentos vinculados a ele.
Login e conta
| Método | Rota | Quem pode | Para quê |
|---|---|---|---|
| POST | /api/auth/login |
público | Entrar: devolve o token e os dados do usuário |
| POST | /api/auth/definir-senha |
público | Definir a senha pelo link de convite ou de redefinição |
| GET | /api/me |
todos | Dados do usuário logado (para o parceiro, inclui os estabelecimentos) |
| PUT | /api/me/senha |
todos | Trocar a própria senha (senhaAtual, novaSenha) |
Usuários
| Método | Rota | Quem pode | Para quê |
|---|---|---|---|
| GET | /api/usuarios |
ADMIN | Listar usuários |
| GET | /api/usuarios/{id} |
ADMIN | Ver um usuário |
| POST | /api/usuarios |
ADMIN | Cadastrar usuário; devolve o link de convite para enviar |
| PUT | /api/usuarios/{id} |
ADMIN | Alterar nome, perfil e estabelecimentos vinculados |
| POST | /api/usuarios/{id}/inativar · /reativar |
ADMIN | Bloquear ou liberar o acesso |
| POST | /api/usuarios/{id}/link-de-senha |
ADMIN | Gerar novo link de acesso (convite ou senha esquecida) |
Tabelas de referência
| Método | Rota | Quem pode | Para quê |
|---|---|---|---|
| GET | /api/referencias |
todos | Tipos de hospedagem, feriados e faixas de ocupação (selects e legendas) |
| POST | /api/referencias/tipos-hospedagem |
ADMIN | Cadastrar tipo de hospedagem |
| PUT · DELETE | /api/referencias/tipos-hospedagem/{codigo} |
ADMIN | Alterar; excluir só se nunca foi usado |
| POST | /api/referencias/feriados |
ADMIN | Cadastrar feriado ou evento |
| PUT · DELETE | /api/referencias/feriados/{codigo} |
ADMIN | Alterar; excluir só se nunca foi usado |
| POST | /api/referencias/faixas |
ADMIN | Cadastrar faixa de ocupação |
| PUT · DELETE | /api/referencias/faixas/{codigo} |
ADMIN | Alterar; excluir só se nunca foi usada em publicação |
Estabelecimentos e inventário
| Método | Rota | Quem pode | Para quê |
|---|---|---|---|
| GET | /api/estabelecimentos?tipo=&ativo= |
todos (parceiro: só os seus) | Lista resumida, com UH e leitos vigentes |
| GET | /api/estabelecimentos/{id} |
todos (parceiro: só os seus) | Dados cadastrais e da Secretaria |
| POST | /api/estabelecimentos |
ADMIN | Cadastrar, já com a capacidade inicial |
| PUT | /api/estabelecimentos/{id} |
ADMIN, PARCEIRO (só os seus) | Alterar dados; o bloco da Secretaria só vale para o ADMIN |
| POST | /api/estabelecimentos/{id}/inativar · /reativar |
ADMIN | Tirar ou voltar das pesquisas e dos cálculos |
| GET | /api/estabelecimentos/{id}/capacidades |
todos (parceiro: só os seus) | Histórico de UH e leitos |
| POST | /api/estabelecimentos/{id}/capacidades |
ADMIN, PARCEIRO (só os seus) | Nova capacidade a partir de uma data |
| PUT · DELETE | /api/estabelecimentos/{id}/capacidades/{capacidadeId} |
ADMIN | Corrigir ou excluir um lançamento digitado errado |
| GET | /api/estabelecimentos/{id}/inventario |
todos (parceiro: só os seus) | Itens que o estabelecimento possui |
| PUT | /api/estabelecimentos/{id}/inventario |
ADMIN, PARCEIRO (só os seus) | Salvar os itens marcados (o parceiro não altera os grupos da Secretaria) |
| GET | /api/inventario/catalogo |
todos | Perguntas do inventário (grupos e itens ativos) para montar o formulário |
Catálogo do inventário
| Método | Rota | Quem pode | Para quê |
|---|---|---|---|
| GET | /api/inventario/catalogo/completo |
ADMIN | Catálogo inteiro, com grupos e itens inativos |
| POST | /api/inventario/grupos |
ADMIN | Criar grupo de perguntas |
| PUT · DELETE | /api/inventario/grupos/{id} |
ADMIN | Alterar; excluir só grupo sem itens |
| POST | /api/inventario/grupos/{id}/inativar · /reativar |
ADMIN | Tirar ou voltar o grupo do formulário |
| POST | /api/inventario/grupos/{grupoId}/itens |
ADMIN | Criar item no grupo |
| PUT · DELETE | /api/inventario/itens/{id} |
ADMIN | Alterar; excluir só item que nenhum estabelecimento marcou |
| POST | /api/inventario/itens/{id}/inativar · /reativar |
ADMIN | Tirar ou voltar o item do formulário |
Hospedagem caseira (RHC) e parâmetros de cálculo
| Método | Rota | Quem pode | Para quê |
|---|---|---|---|
| GET | /api/rhc |
ADMIN, VISUALIZADOR | Totais de RHC por vigência |
| GET | /api/rhc/vigente?data= |
ADMIN, VISUALIZADOR | Totais vigentes na data (padrão: hoje) |
| POST | /api/rhc |
ADMIN | Lançar totais a partir de uma data |
| PUT · DELETE | /api/rhc/{id} |
ADMIN | Corrigir ou excluir um lançamento |
| GET | /api/parametros-calculo |
ADMIN, VISUALIZADOR | Todas as vigências dos parâmetros |
| GET | /api/parametros-calculo/vigente?data= |
todos | Parâmetros vigentes na data (notas metodológicas) |
| POST | /api/parametros-calculo |
ADMIN | Nova vigência |
| PUT · DELETE | /api/parametros-calculo/{id} |
ADMIN | Corrigir ou excluir, se nenhuma publicação usou |
Pesquisas e campanhas
| Método | Rota | Quem pode | Para quê |
|---|---|---|---|
| GET | /api/rodadas?status= |
ADMIN | Listar pesquisas, com as campanhas |
| GET | /api/rodadas/{id} |
ADMIN | Ver uma pesquisa |
| POST | /api/rodadas |
ADMIN | Criar pesquisa (em rascunho) |
| PUT · DELETE | /api/rodadas/{id} |
ADMIN | Alterar ou excluir (só em rascunho) |
| POST | /api/rodadas/{id}/campanhas |
ADMIN | Incluir campanha (um mês ou um feriado) |
| PUT · DELETE | /api/rodadas/{id}/campanhas/{campanhaId} |
ADMIN | Alterar ou excluir campanha (só em rascunho) |
| POST | /api/rodadas/{id}/abrir |
ADMIN | Abrir para respostas, com o prazo |
| POST | /api/rodadas/{id}/prorrogar |
ADMIN | Mudar o prazo |
| POST | /api/rodadas/{id}/encerrar |
ADMIN | Fechar para respostas dos parceiros |
| POST | /api/rodadas/{id}/reabrir |
ADMIN | Reabrir (se nenhuma campanha foi publicada) |
| GET | /api/rodadas/{id}/acompanhamento |
ADMIN | Quem respondeu e quem falta, com contatos e % de leitos |
Respostas
| Método | Rota | Quem pode | Para quê |
|---|---|---|---|
| GET | /api/respostas/pendencias |
PARCEIRO | Campanhas abertas para cada estabelecimento dele (tela "Responder pesquisa") |
| PUT | /api/campanhas/{campanhaId}/respostas/{estabelecimentoId} |
ADMIN, PARCEIRO (só os seus) | Enviar ou corrigir a resposta (taxaOcupacao e diariaMedia); vale o último envio |
| GET | /api/respostas/minhas |
PARCEIRO | Respostas dele, com a taxa da cidade quando publicada |
| GET | /api/respostas/{id}/historico |
ADMIN, PARCEIRO (só os seus) | Todas as versões enviadas |
| GET | /api/campanhas/{campanhaId}/respostas |
ADMIN | Respostas da campanha (dado individual) |
O parceiro responde enquanto a pesquisa está aberta; o ADMIN também depois do encerramento, até a campanha ser publicada.
Diárias e resultados
| Método | Rota | Quem pode | Para quê |
|---|---|---|---|
| GET · PUT | /api/campanhas/{campanhaId}/diarias |
ADMIN | Diárias médias digitadas pela equipe (por tipo e a geral) |
| GET | /api/campanhas/{id}/previa |
ADMIN | Simular o resultado e ver o que impede a publicação |
| POST | /api/campanhas/{id}/publicacao |
ADMIN | Calcular e publicar |
| DELETE | /api/campanhas/{id}/publicacao |
ADMIN | Cancelar uma publicação feita por engano |
| GET | /api/campanhas/{id}/resultado |
todos | Resultado publicado (para quem não é ADMIN, tipos com menos de 3 respondentes vêm ocultos) |
| GET | /api/campanhas/{id}/resultado.csv |
todos | O mesmo resultado em CSV para Excel |
| GET | /api/campanhas/publicadas?modalidade=&tipoPeriodo=&ano= |
todos | Campanhas publicadas, mais recentes primeiro |
Dashboards e série histórica
| Método | Rota | Quem pode | Para quê |
|---|---|---|---|
| GET | /api/dashboard/resumo?modalidade= |
todos | Cartões da tela inicial: último mês e último feriado, comparativo com o ano anterior e acumulado do ano |
| GET | /api/dashboard/serie-mensal?indicador=&modalidade=&anoInicio=&anoFim= |
todos | Gráfico mês a mês, uma linha por ano |
| GET | /api/dashboard/feriados?modalidade=&feriado=&anoInicio=&anoFim= |
todos | Gráfico dos feriados, ano a ano |
| GET | /api/historico?indicador=&modalidade=&ano=&tipo= |
todos | Valores da série histórica (antes do sistema) |
| POST | /api/historico |
ADMIN | Gravar um valor (substitui o da mesma combinação) |
| PUT · DELETE | /api/historico/{id} |
ADMIN | Corrigir ou excluir um valor |
| POST | /api/historico/importacao |
ADMIN | Importar CSV (multipart: arquivo e fonte) |
Parâmetros depois de ? são opcionais. Valores aceitos: modalidade = CONSOLIDADA ou
EXPECTATIVA; tipoPeriodo = MENSAL ou FERIADO; indicador = TAXA_OCUPACAO,
FLUXO_TURISTAS ou DIARIA_MEDIA; status = RASCUNHO, ABERTA ou ENCERRADA; os códigos de
tipo e de feriado vêm de GET /api/referencias.
./gradlew testCalculadoraOcupacaoTest: a metodologia de cálculo, sem banco.OtoProjectApplicationTests: sobe a aplicação inteira contra o PostgreSQL docompose.yaml(o Spring Boot inicia o container sozinho, então o Docker precisa estar aberto e o.envpreenchido). Confere migrations, mapeamento das entidades, consultas e configuração.
| Pacote | Conteúdo |
|---|---|
auth |
Login, convites e redefinição de senha (links de uso único) |
usuario |
Usuários, perfis e o administrador inicial |
estabelecimento |
Inventário dos meios de hospedagem, capacidade (UH e leitos) e RHC |
inventario |
Catálogo de serviços, equipamentos, acessibilidade e ODS |
pesquisa |
Pesquisas (rodadas), campanhas, respostas, diárias digitadas e parâmetros de cálculo |
calculo |
Metodologia (CalculadoraOcupacao), prévia, publicação e resultados |
historico |
Série histórica de 2017 em diante e importação de CSV |
dashboard |
Dados dos gráficos da tela inicial |
referencia |
Tipos de hospedagem, feriados e faixas de ocupação (mantidos pela equipe) |
comum |
Segurança (JWT), tratamento de erros, auditoria, OpenAPI e geração de códigos |
CalculadoraOcupacao reproduz as fórmulas da planilha de tabulação do OTO (abas MH e
TABULAÇÃO): taxa ponderada pelas UH, fluxo de respondentes, não respondentes, RHC e day use,
representatividade pelos leitos, diária, RevPAR e receita por tipo, e a distribuição dos
respondentes por faixa. Com as mesmas entradas, os resultados batem com os da planilha.
A diária de cada tipo é a média das respostas ponderada pelos leitos. Quando a equipe digita a diária da campanha (como na planilha, que usa a pesquisa de tarifas), vale a digitada.
Uma diferença esperada: o sistema sempre calcula os leitos úteis como UH × (1 + máximo) / 2.
Na planilha de 2026, algumas linhas da aba MH têm esse valor digitado à mão (arredondado, como
28 no lugar de 27,5, ou zerado); nesses casos os totais do sistema ficam um pouco diferentes.
O schema é criado e versionado pelo Flyway, com os scripts de
src/main/resources/db/migration, aplicados na subida da aplicação. O Hibernate só
valida as entidades (ddl-auto=validate); ele nunca cria tabelas.
Regras para migrations:
- Nome no formato
V<número>__descricao.sql(dois sublinhados). Ex.:V11__adicionar_coluna_x.sql. - Nunca altere uma migration que já foi aplicada (o Flyway confere o checksum e a aplicação não sobe). Para mudar algo, crie uma nova migration.
- Combine no grupo quem vai usar o próximo número, para evitar duas migrations com a mesma versão.
- Para recriar o banco local do zero:
docker compose down -ve suba de novo.