- Listagem de Hospitais
No módulo de gestão hospitalar, a lista de hospitais exibe níveis hospitalares, mas os dados brutos contêm apenas o campo hostype. É necessário consultar o dicionário de dados para obter as informações de nível. Isso requer uma chamada remota do service-hosp para o service-cmn, introduzindo um centro de registro e chamadas de serviço.
Utiliza-se Nacos como centro de registro. Cria-se um projeto service-cmn-client com uma interface DictConsultaClient para chamadas externas.
@FeignClient(value = "servico-cmn", path = "/admin/cmn/dict/")
public interface DictConsultaClient {
// Consulta por código e valor do dicionário
@GetMapping("obterNome/{codigoDado}/{valor}")
public String obterNome(@PathVariable("codigoDado") String codigoDado,
@PathVariable("valor") String valor);
// Consulta apenas por valor
@GetMapping("obterNome/{valor}")
public String obterNome(@PathVariable("valor") String valor);
}
O service-hosp utiliza esta interface cliente para acessar os endpoints do controller do service-cmn.
1.1 Visualização da Lista de Hospitais
Implementação da interface para listar hospitais com paginação e filtros básicos.
1.2 Exibição de Detalhes do Hospital
Exibição de informações detalhadas de um hospital específico, incluindo dados cadastrais e serviços oferecidos.
1.3 Exibição de Informações de Agendamento
O agendamento é dividido em três partes: informações de departamento em formato de árvore, datas de agendamento com paginação e detalhes dos médicos disponíveis para cada data.
A seção esquerda exibe a lista de departamentos do hospital, recuperada por código hospitalar.
1.3.1 Consulta de Árvore de Departamentos
Para obter a estrutura hierárquica de departamentos, agrupam-se os departamentos por código pai.
public List<DepartamentoVo> buscarArvoreDepartamentos(String codigoHospital) {
List<DepartamentoVo> listaResultado = new ArrayList<>();
Departamento consultaDepartamento = new Departamento();
consultaDepartamento.setCodigoHospital(codigoHospital);
Example<Departamento> exemplo = Example.of(consultaDepartamento);
List<Departamento> listaDepartamentos = repositorioDepartamentos.findAll(exemplo);
// Agrupa departamentos por código do departamento pai
Map<String, List<Departamento>> mapaDepartamentos = listaDepartamentos.stream()
.collect(Collectors.groupingBy(Departamento::getCodigoDepartamentoPai));
for (Map.Entry<String, List<Departamento>> entrada : mapaDepartamentos.entrySet()) {
String codigoPai = entrada.getKey();
List<Departamento> subdepartamentos = entrada.getValue();
DepartamentoVo departamentoPai = new DepartamentoVo();
departamentoPai.setCodigo(codigoPai);
departamentoPai.setNome(subdepartamentos.get(0).getNomeDepartamentoPai());
List<DepartamentoVo> filhos = new ArrayList<>();
for (Departamento dept : subdepartamentos) {
DepartamentoVo filho = new DepartamentoVo();
filho.setNome(dept.getNomeDepartamento());
filho.setCodigo(dept.getCodigoDepartamento());
filhos.add(filho);
}
departamentoPai.setFilhos(filhos);
listaResultado.add(departamentoPai);
}
return listaResultado;
}
- Introdução ao API Gateway
Em uma arquitetura de microsserviços, o API Gateway resolve problemas como complexidade do cliente, requisições cross-origin, autenticação fragmentada, dificuldade de refatoração e protocolos incompatíveis. Ele atua como uma camada intermediária, gerenciando segurança, desempenho e monitoramento.
2.1 Spring Cloud Gateway
O Gateway e os microsserviços são registrados no Nacos para roteamento e execução de filtros.
Cria-se o módulo service-gateway com as seguintes configurações:
# Porta do serviço
server.port=80
# Nome da aplicação
spring.application.name=servico-gateway
# Endereço do Nacos
spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848
# Ativação de roteamento por descoberta
spring.cloud.gateway.discovery.locator.enabled=true
# Configuração de rotas
spring.cloud.gateway.routes[0].id=servico-hosp
spring.cloud.gateway.routes[0].uri=lb://servico-hosp
spring.cloud.gateway.routes[0].predicates=Path=/*/hosp/**
spring.cloud.gateway.routes[1].id=servico-cmn
spring.cloud.gateway.routes[1].uri=lb://servico-cmn
spring.cloud.gateway.routes[1].predicates=Path=/*/cmn/**
Se a porta 80 estiver ocupada (por exemplo, pelo Nginx), é necessário liberá-la. No frontend, ajusta-se o BASE_API para http://localhost sem porta explícita.
Para configurar cross-origin, remove-se a anotação @CrossOrigin dos controllers e define-se um filtro global:
@Configuration
public class ConfiguracaoCors {
@Bean
public CorsWebFilter filtroCorsWeb() {
CorsConfiguration config = new CorsConfiguration();
config.addAllowedMethod("*");
config.addAllowedOrigin("*");
config.addAllowedHeader("*");
UrlBasedCorsConfigurationSource fonte = new UrlBasedCorsConfigurationSource(new PathPatternParser());
fonte.registerCorsConfiguration("/**", config);
return new CorsWebFilter(fonte);
}
}
- Configuração do Frontend do Usuário
3.1 Introdução ao Nuxt.js
Nuxt.js é um framework leve baseado em Vue.js para criar aplicações com renderização no lado do servidor (SSR) ou sites estáticos, oferecendo estrutura modular e hot reloading.
Para iniciar: npm install e npm run dev, acessando http://localhost:3000.
3.2 Estrutura de Diretórios do Nuxt
- assets: Recursos estáticos não compilados.
- components: Componentes Vue.js sem extensões especiais do Nuxt.
- layouts: Componentes de layout da aplicação.
- pages: Rotas e visualizações, com geração automática de configurações.
- plugins: Plugins JavaScript para execução antes da instanciação do Vue.
- nuxt.config.js: Configurações personalizadas para sobrescrever padrões.
3.3 Funcionalidade de Login
Implementa-se login via email com código de verificação de seis dígitos, armazenado no Redis.
// Envio de código por email
@GetMapping("enviar/{email}")
public Resultado enviarCodigo(@PathVariable String email) {
String codigo = modeloRedis.opsForValue().obter(email);
if (!StringUtils.isEmpty(codigo)) {
return Resultado.ok();
}
codigo = UtilAleatorio.getCodigoSeisDigitos();
boolean enviado = servicoMsm.enviar(email, codigo);
if (enviado) {
modeloRedis.opsForValue().definir(email, codigo, 2, TimeUnit.MINUTOS);
return Resultado.ok();
} else {
return Resultado.falha().mensagem("Falha ao enviar email");
}
}
Após verificação do código, se o email for novo, registra-se o usuário no banco yygh_user e gera-se um token JWT.
@Override
public Map<String, Object> loginUsuario(LoginVo dadosLogin) {
String email = dadosLogin.getEmail();
String codigo = dadosLogin.getCodigo();
if (StringUtils.isEmpty(email) || StringUtils.isEmpty(codigo)) {
throw new HospitalException(ResultadoEnum.ERRO_PARAMETRO);
}
String codigoRedis = modeloRedis.opsForValue().obter(email);
if (!codigo.equals(codigoRedis)) {
throw new HospitalException(ResultadoEnum.ERRO_CODIGO);
}
InfoUsuario usuario = null;
ConsultaWrapper<InfoUsuario> wrapper = new ConsultaWrapper<>();
wrapper.eq("email", email);
usuario = baseMapper.selecionarUm(wrapper);
if (usuario == null) {
usuario = new InfoUsuario();
usuario.setNome("");
usuario.setEmail(email);
usuario.setStatus(1);
baseMapper.inserir(usuario);
}
if (usuario.getStatus() == 0) {
throw new HospitalException(ResultadoEnum.ERRO_LOGIN_DESATIVADO);
}
Map<String, Object> mapa = new HashMap<>();
String nome = usuario.getNome();
if (StringUtils.isEmpty(nome)) {
nome = usuario.getApelido();
}
if (StringUtils.isEmpty(nome)) {
nome = usuario.getEmail();
}
mapa.put("nome", nome);
String token = JwtAssistente.criarToken(usuario.getId(), nome);
mapa.put("token", token);
return mapa;
}
3.4 Login via WeChat
Utiliza-se QR code em modal para autenticação com WeChat, integrando-se ao módulo service-user.
Configuração no application.properties:
server.port=8160
wx.open.app_id=wxed9954c01bb89b47
wx.open.app_secret=a7482517235173ddb4083788de60b90e
wx.open.redirect_url=http://localhost:8160/api/ucenter/wx/callback
yygh.baseUrl=http://localhost:3000
Classe de configuração para propriedades estáticas:
@Component
public class ConstantesPropriedadesWx implements InitializingBean {
@Value("${wx.open.app_id}")
private String idApp;
@Value("${wx.open.app_secret}")
private String segredoApp;
@Value("${wx.open.redirect_url}")
private String urlRedirecionamento;
@Value("${yygh.baseUrl}")
private String baseUrlYygh;
public static String ID_APP_WX;
public static String SEGREDO_APP_WX;
public static String URL_REDIRECIONAMENTO_WX;
public static String BASE_URL_YYGH;
@Override
public void afterPropertiesSet() {
ID_APP_WX = idApp;
SEGREDO_APP_WX = segredoApp;
URL_REDIRECIONAMENTO_WX = urlRedirecionamento;
BASE_URL_YYGH = baseUrlYygh;
}
}
Controller para geração de QR code:
@GetMapping("obterParametrosLogin")
@ResponseBody
public Resultado gerarQrConnect() {
try {
Map<String, Object> mapa = new HashMap<>();
mapa.put("appid", ConstantesPropriedadesWx.ID_APP_WX);
mapa.put("scope", "snsapi_login");
String urlRedirecionamento = URLEncoder.encode(ConstantesPropriedadesWx.URL_REDIRECIONAMENTO_WX, "utf-8");
mapa.put("redirect_uri", urlRedirecionamento);
mapa.put("state", System.currentTimeMillis() + "");
return Resultado.ok(mapa);
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
return null;
}
}
No callback após escaneamento, obtém-se um código temporário para troca por token de acesso e informações do usuário via APIs do WeChat.
@GetMapping("callback")
public String callback(String codigo, String estado) {
// Passo 1: Obter código temporário
String urlTokenAcesso = String.format(
"https://api.weixin.qq.com/sns/oauth2/access_token?appid=%s&secret=%s&code=%s&grant_type=authorization_code",
ConstantesPropriedadesWx.ID_APP_WX, ConstantesPropriedadesWx.SEGREDO_APP_WX, codigo
);
try {
String infoToken = ClienteHttp.obter(urlTokenAcesso);
JSONObject jsonToken = JSONObject.parseObject(infoToken);
String tokenAcesso = jsonToken.getString("access_token");
String idAberto = jsonToken.getString("openid");
InfoUsuario usuario = servicoUsuario.selecionarPorIdAberto(idAberto);
if (usuario == null) {
String urlInfoUsuario = String.format(
"https://api.weixin.qq.com/sns/userinfo?access_token=%s&openid=%s",
tokenAcesso, idAberto
);
String infoUsuarioStr = ClienteHttp.obter(urlInfoUsuario);
JSONObject jsonUsuario = JSONObject.parseObject(infoUsuarioStr);
String apelido = jsonUsuario.getString("nickname");
String urlImagem = jsonUsuario.getString("headimgurl");
usuario = new InfoUsuario();
usuario.setApelido(apelido);
usuario.setIdAberto(idAberto);
usuario.setStatus(1);
servicoUsuario.salvar(usuario);
}
Map<String, String> mapa = new HashMap<>();
String nome = usuario.getNome();
if (StringUtils.isEmpty(nome)) {
nome = usuario.getApelido();
}
if (StringUtils.isEmpty(nome)) {
nome = usuario.getTelefone();
}
mapa.put("nome", nome);
if (StringUtils.isEmpty(usuario.getTelefone())) {
mapa.put("openid", usuario.getIdAberto());
} else {
mapa.put("openid", "");
}
String token = JwtAssistente.criarToken(usuario.getId(), nome);
mapa.put("token", token);
return "redirect:" + ConstantesPropriedadesWx.BASE_URL_YYGH + "/weixin/callback?token=" + mapa.get("token") +
"&openid=" + mapa.get("openid") + "&nome=" + URLEncoder.encode(mapa.get("nome"), "utf-8");
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
3.5 Autenticação Real
Endpoints para autenticação do usuário com dados de identidade, armazenados na tabela user-info.
// Autenticação do usuário
@PostMapping("autenticar/autenticarUsuario")
public Resultado autenticarUsuario(@RequestBody UserAuthVo dadosAuth, HttpServletRequest requisicao) {
servicoUsuario.autenticarUsuario(AuthContextHolder.obterIdUsuario(requisicao), dadosAuth);
return Resultado.ok();
}
// Obter informações do usuário por ID
@GetMapping("autenticar/obterInfoUsuario")
public Resultado obterInfoUsuario(HttpServletRequest requisicao) {
Long idUsuario = AuthContextHolder.obterIdUsuario(requisicao);
InfoUsuario usuario = servicoUsuario.obterPorId(idUsuario);
return Resultado.ok(usuario);
}
3.6 Gerenciamento de Usuários
Interfaces no service-user para administração, incluindo listagem, bloqueio e aprovação de usuários.
// Lista de usuários com paginação e filtro
@GetMapping("{pagina}/{limite}")
public Resultado listar(@PathVariable Long pagina,
@PathVariable Long limite,
UserInfoQueryVo consulta) {
Pagina<InfoUsuario> paginaParam = new Pagina<>(pagina, limite);
IPagina<InfoUsuario> modeloPagina = servicoUsuario.selecionarPagina(paginaParam, consulta);
return Resultado.ok(modeloPagina);
}
// Alterar estado de bloqueio do usuário
@GetMapping("bloquear/{idUsuario}/{status}")
public Resultado bloquear(@PathVariable Long idUsuario, @PathVariable Integer status) {
servicoUsuario.bloquear(idUsuario, status);
return Resultado.ok();
}
// Detalhes do usuário
@GetMapping("exibir/{idUsuario}")
public Resultado exibir(@PathVariable Long idUsuario) {
Map<String, Object> mapa = servicoUsuario.exibir(idUsuario);
return Resultado.ok(mapa);
}
// Alterar status de aprovação da autenticação
@GetMapping("aprovar/{idUsuario}/{statusAuth}")
public Resultado aprovar(@PathVariable Long idUsuario, @PathVariable Integer statusAuth) {
servicoUsuario.aprovar(idUsuario, statusAuth);
return Resultado.ok();
}