Plataforma Multicanal de Telemedicina com Java

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.

Tags: Spring Boot MyBatis Plus java uniapp WebRTC

Publicado em 7-11 18:48