No contexto da transformação digital da saúde, uma plataforma robusta para telemedicina torna-se essenciall. O sistema descrito oferece uma solução completa para consultas médicas remotas, integrando múltiplos canais de acesso e um backend escalável desenvolvido em Java. A arquitetura visa desconectar módulos de negócio críticos para suportar alta demanda, utilizando frameworks como SpringBoot e MyBatis Plus, com armazenamento em MySQL e otimização de desempenho através de caching com Redis.
Funcionalidades Nucleares
Roteamento Inteligente para Consultas
O módulo central analisa os sintomas informados pelo paciente para direcioná-lo ao especialista ou departamento mais adequado, com base em um algoritmo de correspondência.
@Service
public class DiagnosticRoutingEngine {
private final DoctorRepository doctorRepo;
private final SymptomClassifier symptomClassifier;
public DiagnosticRoutingEngine(DoctorRepository doctorRepo, SymptomClassifier symptomClassifier) {
this.doctorRepo = doctorRepo;
this.symptomClassifier = symptomClassifier;
}
public RoutingOutcome processConsultationRequest(ConsultationRequestDTO request) {
CategoryPrediction prediction = symptomClassifier.predict(request.getSymptomDescription());
List<medicalprofessional> candidates = doctorRepo.findAvailableBySpecialty(
prediction.getSpecialtyCode(),
request.getUserLocation()
);
ConsultSession newSession = new ConsultSession();
newSession.setPatientId(request.getPatientId());
newSession.setAssignedDoctors(candidates);
newSession.setInitiationTime(Instant.now());
newSession.setStatus(SessionStatus.PENDING);
return new RoutingOutcome(prediction, candidates, newSession);
}
}
</medicalprofessional>
Descoberta de Serviços por Localização
A funcionalidade de busca permite localizar hospitais e clínicas proximas, com filtros por especialidade e ordenação por distância geográfica.
@Component
public class GeospatialClinicLocator {
private final ClinicRepository clinicRepository;
public List<ClinicResult> findNearbyFacilities(FacilitySearchParams params) {
Specification<Clinic> spec = Specification.where(null);
if (params.getKeyword() != null && !params.getKeyword().isEmpty()) {
spec = spec.and((root, query, cb) ->
cb.or(
cb.like(root.get("name"), "%" + params.getKeyword() + "%"),
cb.like(root.get("services"), "%" + params.getKeyword() + "%")
)
);
}
if (params.getDepartmentFilter() != null) {
spec = spec.and((root, query, cb) ->
cb.equal(root.get("departmentId"), params.getDepartmentFilter())
);
}
List<Clinic> results = clinicRepository.findAll(spec);
if (params.getOriginLatitude() != null && params.getOriginLongitude() != null) {
results.sort(Comparator.comparingDouble(c ->
calculateHaversineDistance(
params.getOriginLatitude(), params.getOriginLongitude(),
c.getLatitude(), c.getLongitude()
)
));
}
return results.stream().map(this::mapToResult).collect(Collectors.toList());
}
}
Fábrica de Canais de Comunicação
O sistema suporta múltiplos modos de interação: texto, áudio, vídeo e telefone. Um provedor baseado em enumeração instancia o serviço apropriado.
public enum InteractionChannel {
CHAT, VOICE, VIDEO, PHONE
}
@Component
public class ConsultationChannelProvider {
private final Map<InteractionChannel, ConsultationHandler> handlerMap;
public ConsultationChannelProvider(
ChatConsultationHandler chatHandler,
VoiceConsultationHandler voiceHandler,
VideoConsultationHandler videoHandler,
PhoneConsultationHandler phoneHandler) {
handlerMap = EnumMap.create(InteractionChannel.class);
handlerMap.put(InteractionChannel.CHAT, chatHandler);
handlerMap.put(InteractionChannel.VOICE, voiceHandler);
handlerMap.put(InteractionChannel.VIDEO, videoHandler);
handlerMap.put(InteractionChannel.PHONE, phoneHandler);
}
public ConsultationHandler resolve(InteractionChannel channel) {
ConsultationHandler handler = handlerMap.get(channel);
if (handler == null) {
throw new UnsupportedChannelException("Canal não implementado: " + channel);
}
return handler;
}
}
@Service
@RequiredArgsConstructor
public class VideoConsultationHandler implements ConsultationHandler {
private final MediaRoomService mediaRoomService;
private final VideoSessionRepository sessionRepo;
@Override
public ConsultationSession initiate(ConsultationInitParams params) {
String roomId = mediaRoomService.provisionRoom(params.getInitiatorId(), params.getTargetId());
VideoConsultationSession session = new VideoConsultationSession();
session.setMediaRoomIdentifier(roomId);
session.setStartTime(LocalDateTime.now());
session.setState(ConsultationState.ACTIVE);
return sessionRepo.save(session);
}
}
Fluxo de Credenciamento de Profissionais
Um endpoint dedicado gerencia a inscrição de novos médicos, incluindo validação preliminar de credenciais e submissão para revisão manual.
@RestController
@RequestMapping("/api/v1/professionals")
public class ProfessionalRegistrationApi {
private final CredentialValidator credentialValidator;
private final ProfessionalService professionalService;
@PostMapping("/onboard")
public ResponseEntity<RegistrationResponse> submitApplication(
@Valid @RequestBody ProfessionalApplicationDTO application) {
ValidationResult validation = credentialValidator.validate(application.getLicenses());
if (!validation.isValid()) {
return ResponseEntity.unprocessableEntity()
.body(RegistrationResponse.failure("Documentação inválida"));
}
Professional newProfessional = professionalService.createPendingApplication(application);
eventPublisher.publishEvent(new NewApplicationEvent(newProfessional.getId()));
return ResponseEntity.accepted()
.body(RegistrationResponse.success("Solicitação recebida para análise"));
}
}
Gerenciamento de Níveis de Usuário
Um sistema de membresia controla o acesso a funcionalidades premium e gerencia a associação do usuário a times ou organizações.
@Service
public class UserProfileManager {
private final UserRepository userRepository;
private final MembershipTierRepository tierRepo;
public UserProfileDetails getProfile(Long userId) {
User user = userRepository.findById(userId)
.orElseThrow(UserNotFoundException::new);
MembershipTier currentTier = tierRepo.findByUserId(userId);
List<TeamAffiliation> teams = teamService.getAffiliations(userId);
return UserProfileDetails.builder()
.tierLevel(currentTier.getLevel())
.tierExpiry(currentTier.getExpirationDate())
.associatedTeams(teams.size())
.enabledFeatures(mapFeatures(currentTier.getLevel()))
.build();
}
@Transactional
public void elevateUserTier(Long userId, TierLevel newLevel) {
User user = userRepository.findById(userId)
.orElseThrow(UserNotFoundException::new);
MembershipTier tier = tierRepo.findByUserId(userId);
tier.setLevel(newLevel);
tier.setLastUpdated(Instant.now());
tierRepo.save(tier);
auditService.logTierChange(userId, newLevel);
}
}
Interface do Usuário e Aplicação Móvel
Página Principal Adaptativa
Implementada com UniApp, a interface principal adapta-se a diferentes plataformas (WeChat, H5, APP nativo), apresentando um formulário de sintomas rápido e uma grade de navegação por especialidades.
<template>
<view class="app-container">
<section class="quick-intake">
<view class="input-wrapper">
<textarea class="symptom-field" placeholder="Descreva seus sintomas principais..." v-model="userSymptoms"></textarea>
</view>
<button class="primary-action">
Buscar Especialista
</button>
</section>
<section class="specialty-browser">
<view :key="spec.id" class="spec-card" v-for="spec in medicalSpecialties">
<image :src="spec.iconUrl" class="spec-icon"></image>
<text class="spec-label">{{ spec.name }}</text>
</view>
</section>
<section class="featured-professionals">
<text class="section-heading">Médicos em Destaque</text>
<scroll-view class="doctors-carousel" scroll-x="">
<view :key="doc.id" class="doctor-profile" v-for="doc in featuredDoctors">
<image :src="doc.profileImage" class="profile-image"></image>
<text class="doctor-name">{{ doc.fullName }}</text>
<text class="doctor-specialty">{{ doc.specialty }}</text>
<button class="contact-action">
Iniciar Conversa
</button>
</view>
</scroll-view>
</section>
</view>
</template>
<script>
export default {
data() {
return {
userSymptoms: '',
medicalSpecialties: [],
featuredDoctors: []
}
},
methods: {
async initiateRouting() {
try {
const routingResult = await this.$api.post('/consultations/route', {
description: this.userSymptoms,
userCoords: this.currentUserLocation
});
this.$router.push({
path: '/professional-list',
query: { candidates: JSON.stringify(routingResult.candidates) }
});
} catch (error) {
this.$toast.show('Erro ao processar sua solicitação.');
}
}
}
}
</script>
Interface de Vídeo-Consulta
Componente para comunicação síncrona, integrando streaming de mídia local e remota com controles de sessão.
<template>
<view class="video-session">
<div class="video-streams">
<div class="remote-feed">
<video autoplay="" playsinline="" ref="remoteVideo"></video>
<div class="participant-name">{{ remoteParticipantName }}</div>
</div>
<div class="local-feed">
<video autoplay="" muted="" playsinline="" ref="localVideo"></video>
</div>
</div>
<div class="session-controls">
<button :class="{'muted': !isAudioEnabled}">
<icon :name="isAudioEnabled ? 'mic' : 'mic-off'"></icon>
</button>
<button class="end-session">
Encerrar
</button>
<button :class="{'disabled': !isVideoEnabled}">
<icon :name="isVideoEnabled ? 'camera' : 'camera-off'"></icon>
</button>
</div>
<div class="session-meta">
<span>Duração: {{ formattedDuration }}</span>
<span>Profissional: {{ professionalName }}</span>
</div>
</view>
</template>
<script>
import WebRTCManager from '@/utils/webrtc';
export default {
data() {
return {
isAudioEnabled: true,
isVideoEnabled: true,
sessionDuration: 0,
remoteParticipantName: '',
professionalName: ''
}
},
methods: {
async establishConnection() {
this.webrtc = new WebRTCManager();
const localStream = await this.webrtc.captureLocalMedia();
this.$refs.localVideo.srcObject = localStream;
await this.webrtc.connectToRoom(this.sessionId);
this.webrtc.onRemoteStream((remoteStream) => {
this.$refs.remoteVideo.srcObject = remoteStream;
});
},
toggleAudio() {
this.isAudioEnabled = !this.isAudioEnabled;
this.webrtc.toggleAudioTrack(this.isAudioEnabled);
},
terminateSession() {
this.webrtc.disconnect();
this.$router.go(-1);
}
}
}
</script>
Painel Administrativo
Uma aplicação web baseada em Vue.js fornece ferramentas para gerenciamento de operações, incluindo monitoramento de métricas e revisão de credenciais de profissionais.
<template>
<div class="admin-panel">
<div class="metrics-grid">
<div class="metric-card">
<span class="metric-label">Consultas Hoje</span>
<span class="metric-value">{{ operationalStats.dailyConsultations }}</span>
</div>
<div class="metric-card">
<span class="metric-label">Profissionais Ativos</span>
<span class="metric-value">{{ operationalStats.activeProfessionals }}</span>
</div>
</div>
<div class="approval-queue">
<h3>Fila de Credenciamento</h3>
<table class="data-table">
<thead>
<tr>
<th>Nome</th>
<th>Instituição</th>
<th>Especialidade</th>
<th>Ações</th>
</tr>
</thead>
<tbody>
<tr :key="applicant.id" v-for="applicant in pendingApplicants">
<td>{{ applicant.fullName }}</td>
<td>{{ applicant.institution }}</td>
<td>{{ applicant.specialty }}</td>
<td>
<button class="btn-approve">
Aprovar
</button>
<button class="btn-reject">
Rejeitar
</button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</template>
<script>
export default {
data() {
return {
operationalStats: {},
pendingApplicants: []
}
},
methods: {
async handleApproval(applicantId, decision) {
await this.$api.patch(`/admin/applications/${applicantId}`, { decision });
this.fetchPendingQueue();
},
async fetchPendingQueue() {
this.pendingApplicants = await this.$api.get('/admin/applications?status=PENDING');
}
},
mounted() {
this.fetchPendingQueue();
}
}
</script>
A integração com múltiplos pontos de acesso digitais — como aplicativos super, mini-programas e web responsiva — é crucial para a adoção em larga escala em mercados com diversidade no uso de plataformas. A separação clara entre a lógica de negócio no backend Java e as interfaces reativas no frontend permite ciclos de desenvolvimento independentes e deploys contínuos.