Mini-programa de marketplace para estudantes construído com UniApp

Este mini-programa permite que alunos comprem e vendam produtos usados dentro do campus. A solução é multiplataforma: roda em WeChat, Alipay, Baidu e QQ Mini-Programs, além de possuir build para Web e App nativo.

Arquitetura geral

O projeto segue separação clara entre cliente e servidor:

  • Cliente: desenvolvido em UniApp + Vue3 Composition API.
  • Servidor: Spring Boot 3 + MyBatis-Plus.
  • Banco: MySQL 8 com InnoDB.
  • Infra: Docker Compose para subir rapidamente ambiente local.

Organização de pastas


campus-market/
├─ miniapp/                 # Código UniApp
│  ├─ pages/               # Páginas
│  ├─ components/          # Componentes reutilizáveis
│  ├─ static/              # Imagens, fontes
│  ├─ utils/               # Requisições, autenticação
│  └─ manifest.json        # Configurações de plataforma
├─ backend/
│  ├─ src/main/java/
│  │  ├─ controller/
│  │  ├─ service/
│  │  ├─ mapper/
│  │  └─ model/
│  └─ resources/
│     ├─ mapper/
│     └─ application.yml
└─ docker-compose.yml

Fluxo de autenticação JWT

O token é gerado no login e renovado automaticamente quando expira. Abaixo, versão simplificada do endpoint:

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

    @PostMapping("/login")
    public ApiResp<String> login(@RequestBody LoginDto dto) {
        Usuario u = usuarioMapper.selectOne(
            Wrappers.<Usuario>lambdaQuery()
                     .eq(Usuario::getEmail, dto.getEmail())
        );
        if (u == null || !BCrypt.checkpw(dto.getSenha(), u.getSenhaHash())) {
            throw new Unauthorized("Credenciais inválidas");
        }
        String jwt = jwtUtil.gerar(u.getId(), u.getEmail(), u.getRole());
        return ApiResp.ok(jwt);
    }
}

Interceptor que protege rotas privadas:

@Component
public class JwtInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest req,
                             HttpServletResponse resp,
                             Object handler) throws Exception {

        if (handler instanceof HandlerMethod hm) {
            if (hm.hasMethodAnnotation(PublicRoute.class)) {
                return true;
            }
        }

        String header = req.getHeader("Authorization");
        if (header == null || !header.startsWith("Bearer ")) {
            resp.setStatus(401);
            return false;
        }

        String token = header.substring(7);
        if (!jwtUtil.validar(token)) {
            resp.setStatus(401);
            return false;
        }

        req.setAttribute("uid", jwtUtil.extrairId(token));
        return true;
    }
}

Modelos de dados principais

CREATE TABLE usuario (
  id          BIGINT AUTO_INCREMENT PRIMARY KEY,
  email       VARCHAR(120) NOT NULL UNIQUE,
  senha_hash  CHAR(60)     NOT NULL,
  nome        VARCHAR(80)  NOT NULL,
  avatar_url  VARCHAR(255),
  saldo       DECIMAL(10,2) DEFAULT 0,
  criado_em   TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE TABLE produto (
  id          BIGINT AUTO_INCREMENT PRIMARY KEY,
  vendedor_id BIGINT NOT NULL,
  titulo      VARCHAR(120) NOT NULL,
  descricao   TEXT,
  preco       DECIMAL(10,2) NOT NULL,
  estoque     INT DEFAULT 1,
  status      ENUM('ATIVO','VENDIDO','EXPIRADO') DEFAULT 'ATIVO',
  criado_em   TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  FOREIGN KEY (vendedor_id) REFERENCES usuario(id)
);

CREATE TABLE pedido (
  id          BIGINT AUTO_INCREMENT PRIMARY KEY,
  comprador_id BIGINT NOT NULL,
  produto_id   BIGINT NOT NULL,
  quantidade   INT DEFAULT 1,
  total        DECIMAL(10,2) NOT NULL,
  status       ENUM('PENDENTE','PAGO','CONCLUIDO','CANCELADO') DEFAULT 'PENDENTE',
  criado_em    TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  FOREIGN KEY (comprador_id) REFERENCES usuario(id),
  FOREIGN KEY (produto_id)   REFERENCES produto(id)
);

Exemplo de página de listagem (UniApp)

<template>
  <view class="container">
    <view v-for="item in produtos" :key="item.id" class="card">
      <image :src="item.imagem" mode="aspectFill" class="thumb"/>
      <view class="info">
        <text class="titulo">{{ item.titulo }}</text>
        <text class="preco">R$ {{ item.preco.toFixed(2) }}</text>
      </view>
    </view>
    <uni-load-more :status="loadingStatus" @click="loadMore"/>
  </view>
</template>

<script setup>
import { ref, onMounted } from 'vue'
import { getProdutos } from '@/utils/api'

const produtos = ref([])
const pagina = ref(1)
const loadingStatus = ref('more')

async function loadMore() {
  if (loadingStatus.value === 'loading') return
  loadingStatus.value = 'loading'
  const data = await getProdutos({ page: pagina.value, size: 10 })
  produtos.value.push(...data.records)
  pagina.value++
  loadingStatus.value = data.records.length ? 'more' : 'noMore'
}

onMounted(loadMore)
</script>

Testes automatizados rápidos

Cenário Entrada Resultado esperado Status
Login bem-sucedido email=joao@edu.br senha=123456 HTTP 200 + JWT
Senha incorreta email=joao@edu.br senha=000000 HTTP 401
Cadastrar produto sem token POST /api/produto HTTP 401
Listar produtos GET /api/produto?page=1;1 Array paginado

Subindo tudo com Docker

# 1. Clone o repositório
git clone https://github.com/seuuser/campus-market.git
cd campus-market

# 2. Configure variáveis de ambiente
cp .env.example .env
# Edite .env com suas credenciais

# 3. Execute
docker-compose up -d

# 4. Importe o dump inicial
docker exec -i campus-db mysql -uroot -proot campus < sql/init.sql

Após o comando, o backend estará em http://localhost:8080 e o H5 do UniApp em http://localhost:3000.

Deploy para produção

  1. Build do mini-programa via HBuilderX ou CLI: npm run build:mp-weixin.
  2. Faz upload do diretório dist/build/mp-weixin no WeChat DevTools.
  3. Configura domínios permitidos e HTTPS no painel do Mini-Program.
  4. Build da imagem Docker do backend: docker build -t campus-api ..
  5. Deploy no provedor de preferência (Tencent Cloud, Alibaba Cloud, etc.).

Tags: uniapp vue3 spring-boot MyBatis-Plus jwt

Publicado em 9-4 17:20