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