Sistema de Submissão de Trabalhos Acadêmicos com Spring Boot, Vue.js e uni-app: Design e Implementação

O projeto consiste em uma plataforma completa para gestão de submissão e avaliação de trabalhos acadêmicos, dividida em três camadas principais: uma API REST construída com Spring Boot, um painel administrativo desenvolvido em Vue.js e um aplicativo móvel/multiplataforma baseado em uni-app. O objetivo é oferecer fluxo contínuo entre autores, revisores e administradores, garantindo rastreabilidade do estado de cada artigo.

Arquitetura e stack tecnológica

A solução adota arquitetura em camadas com separação clara entre interface, lógica de negócio e persistência. A comunicação entre frontend e backend ocorre exclusivamente via APIs REST protegidas por tokens de sessão.

  • Backend: Spring Boot 2.x/3.x, Maven, Spring MVC, MyBatis-Plus.
  • Painel web: Vue.js 2.x/3.x, Vue Router, Axios, Element Plus/Vuetify.
  • Aplicativo: uni-app (Vue-based), compilado para H5, iOS e Android.
  • Banco de dados: MySQL 8.0, com pool de conexões gerenciado pelo HikariCP.

Backend com Spring Boot

O backend é responsável por expor endpoints REST, aplicar regras de negócio, gerenciar autenticação e persistir dados. O Spring Boot simplifica a configuração por meio de starters e autoconfiguração, reduzindo a necessidade de XMLs manuais. A aplicação é empacotada como um JAR executável com servidor Tomcat embarcado.

Principais vantagens observadas:

  • Configuração automática de datasource, segurança e validação a partir das dependências declaradas.
  • Tomcat/Jetty/Undertow embarcados, dispensando deploy em servidor externo durante o desenvolvimento.
  • Integração simplificada com Spring Data, Spring Security e serviços em nuvem.

Frontend web com Vue.js

O painel administrativo utiliza Vue.js para construir interfaces reativas e modulares. O padrão de componentes permite reutilizar elementos como tabelas de submissões, formulários de avaliação e gráficos de status. A reatividade do Vue garante que a interface seja atualizada automaticamente quando os dados da API são modificados, sem manipulação direta do DOM.

Aplicativo móvel com uni-app

O uni-app foi escolhido para unificar o código entre H5 e aplicativos nativos. A mesma base em Vue é compilada para diferentes plataformas, mantendo a lógica de submissão, notificações e consulta de pareceres. A comunicação com a API utiliza uni.request, equivalente ao Axios, com interceptadores centralizados para inclusão do token e tratamento de erros.

Persistência com MyBatis-Plus

MyBatis-Plus atua como camada de persistência, reduzindo a necessidade de SQL manual para operações CRUD. Ele oferece:

  • Mapper genérico com métodos prontos como insert, selectById, updateById e deleteById.
  • Wrapper para construção de consultas dinâmicas.
  • Paginação automática e otimizada para o MySQL.
  • Geração de código a partir das tabelas do banco.

Implementação da autenticação baseada em token

A autenticação segue o modelo de sessão stateless por token. Após o login bem-sucedido, o servidor gera um token aleatório, armazena metadados associados e devolve o token ao cliente. Em cada requisição subsequente, o token é enviado no cabeçalho Token e validado por um interceptor.

Exemplo do endpoint de login:

@IgnoreAuth
@PostMapping("/login")
public R login(String username, String password, String captcha, HttpServletRequest request) {
    UsuarioEntity usuario = usuarioService.findOne(
        new QueryWrapper<UsuarioEntity>().eq("username", username)
    );

    if (usuario == null || !usuario.getPassword().equals(password)) {
        return R.error("Usuário ou senha inválidos");
    }

    String token = sessaoService.createToken(usuario.getId(), username, "usuarios", usuario.getRole());
    return R.ok().put("token", token);
}

Serviço de geração e renovação do token:

@Override
public String createToken(Long userId, String username, String tableName, String role) {
    SessaoEntity sessao = this.findOne(
        new QueryWrapper<SessaoEntity>()
            .eq("user_id", userId)
            .eq("role", role)
    );

    String token = UUID.randomUUID().toString().replace("-", "");
    Date expiration = Date.from(LocalDateTime.now().plusHours(1).atZone(ZoneId.systemDefault()).toInstant());

    if (sessao != null) {
        sessao.setToken(token);
        sessao.setExpirationTime(expiration);
        this.updateById(sessao);
    } else {
        this.insert(new SessaoEntity(userId, username, tableName, role, token, expiration));
    }

    return token;
}

Interceptor de validação do token e configuração de CORS:

@Component
public class AuthInterceptor implements HandlerInterceptor {

    public static final String HEADER_TOKEN = "Token";

    @Autowired
    private SessaoService sessaoService;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        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,Token,Origin,Content-Type,cache-control,Accept,Authorization");
        response.setHeader("Access-Control-Allow-Origin", request.getHeader("Origin"));

        if (RequestMethod.OPTIONS.name().equalsIgnoreCase(request.getMethod())) {
            response.setStatus(HttpStatus.OK.value());
            return false;
        }

        if (!(handler instanceof HandlerMethod)) {
            return true;
        }

        IgnoreAuth ignoreAuth = ((HandlerMethod) handler).getMethodAnnotation(IgnoreAuth.class);
        if (ignoreAuth != null) {
            return true;
        }

        String token = request.getHeader(HEADER_TOKEN);
        SessaoEntity sessao = null;
        if (StringUtils.isNotBlank(token)) {
            sessao = sessaoService.findByToken(token);
        }

        if (sessao != null && sessao.getExpirationTime().after(new Date())) {
            request.getSession().setAttribute("userId", sessao.getUserId());
            request.getSession().setAttribute("role", sessao.getRole());
            request.getSession().setAttribute("tableName", sessao.getTableName());
            request.getSession().setAttribute("username", sessao.getUsername());
            return true;
        }

        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json; charset=utf-8");
        try (PrintWriter writer = response.getWriter()) {
            writer.print(JSONObject.toJSONString(R.error(401, "Sessão inválida ou expirada")));
        }
        return false;
    }
}

Modelo de dados para controle de sessão

A tabela de sessões armazena o token gerado, o identificador do usuário, a role e a data de expiração. Abaixo está um exemplo de DDL e registros iniciais:

DROP TABLE IF EXISTS `sessao`;
CREATE TABLE `sessao` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'Identificador único',
  `user_id` bigint(20) NOT NULL COMMENT 'Identificador do usuário',
  `username` varchar(100) NOT NULL COMMENT 'Nome de usuário',
  `table_name` varchar(100) DEFAULT NULL COMMENT 'Tabela de origem do usuário',
  `role` varchar(100) DEFAULT NULL COMMENT 'Papel/perfil do usuário',
  `token` varchar(200) NOT NULL COMMENT 'Token de sessão',
  `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'Criado em',
  `expiration_time` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00' COMMENT 'Expira em',
  PRIMARY KEY (`id`) USING BTREE,
  KEY `idx_token` (`token`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='Controle de sessões';

INSERT INTO `sessao` VALUES
(1, 23, 'cd01', 'estudante', 'ESTUDANTE', 'al6svx5qkei1wljry5o1npswhdpqcpcg', '2023-02-23 21:46:45', '2023-03-15 14:01:36'),
(2, 11, 'xh01', 'estudante', 'ESTUDANTE', 'fahmrd9bkhqy04sq0fzrl4h9m86cu6kx', '2023-02-27 18:33:52', '2023-03-17 18:27:42'),
(3, 1, 'admin', 'usuarios', 'ADMIN', 'h1pqzsb9bldh93m92j9m2sljy9bt1wdh', '2023-02-27 19:37:01', '2023-03-17 18:23:02');

Testes do sistema

Os testes foram conduzidos com abordagem de caixa-preta, simulando o comportamento do usuário final. O objetivo é detectar falhas de validação, permissões e fluxo antes da entrega.

Testes de login

Dados de entrada Resultado esperado
Usuário e senha corretos, captcha válido Acesso ao sistema com token retornado
Senha incorreta Mensagem de erro e bloqueio do login
Captcha incorreto Erro de validação do captcha
Campo de usuário vazio Notificação de campo obrigatório

Testes de gestão de usuários

Cenário Resultado esperado
Cadastro com dados válidos Usuário aparece na listagem
Edição de informações Dados atualizados na interface
Exclusão confirmada Registro removido do banco
Cadastro com usuário duplicado Erro informando duplicidade

Após a execução dos testes, o sistema apresnetou comportamento conforme o especificado, com fluxo de autenticação, controle de permissões e operações CRUD funcionando de maneira estável.

Tags: Spring Boot Vue.js uni-app MyBatis Plus java

Publicado em 7-1 00:07