Parcourir la source

feat: 补充HIS路由上下文重载

WangKang il y a 4 jours
Parent
commit
013c28abfe

+ 70 - 4
emoon-infra/emoon-modules/emoon-ai/emoon-ai-mcp/src/main/java/com/emoon/ai/mcp/application/McpToolService.java

@@ -3,6 +3,8 @@ package com.emoon.ai.mcp.application;
 import com.emoon.common.core.exception.exception.ServiceException;
 import com.emoon.mcp.his.client.HisClient;
 import com.emoon.mcp.his.domain.*;
+import com.emoon.mcp.his.routing.HisClientRouter;
+import com.emoon.mcp.his.routing.HisRouteContext;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Service;
@@ -22,32 +24,64 @@ import java.util.Map;
 @RequiredArgsConstructor
 public class McpToolService {
 
-    private final HisClient hisClient;
+    private final HisClientRouter hisClientRouter;
 
     public List<HisDepartment> queryDepartments() {
+        return queryDepartments(HisRouteContext.empty());
+    }
+
+    public List<HisDepartment> queryDepartments(HisRouteContext context) {
+        HisClient hisClient = hisClientRouter.route(context);
         return hisClient.getDepartments();
     }
 
     public List<HisDoctor> queryDoctors(String departmentId) {
+        return queryDoctors(departmentId, HisRouteContext.empty());
+    }
+
+    public List<HisDoctor> queryDoctors(String departmentId, HisRouteContext context) {
+        HisClient hisClient = hisClientRouter.route(context);
         return hisClient.getDoctors(departmentId);
     }
 
     public List<HisScheduleSlot> querySchedules(String doctorId, LocalDate date) {
+        return querySchedules(doctorId, date, HisRouteContext.empty());
+    }
+
+    public List<HisScheduleSlot> querySchedules(String doctorId, LocalDate date, HisRouteContext context) {
+        HisClient hisClient = hisClientRouter.route(context);
         return hisClient.getDoctorSchedules(doctorId, date != null ? date : LocalDate.now().plusDays(1));
     }
 
     public HisSlotLockResult lockSlot(String slotId, String traceId, String idempotencyKey) {
+        return lockSlot(slotId, traceId, idempotencyKey, HisRouteContext.empty());
+    }
+
+    public HisSlotLockResult lockSlot(String slotId, String traceId, String idempotencyKey,
+                                      HisRouteContext context) {
         log.info("lockSlot slotId={} traceId={} idempotencyKey={}", slotId, traceId, idempotencyKey);
+        HisClient hisClient = hisClientRouter.route(context);
         return hisClient.lockSlot(slotId, traceId, idempotencyKey);
     }
 
     public HisSlotLockResult releaseSchedule(String lockId, String traceId) {
+        return releaseSchedule(lockId, traceId, HisRouteContext.empty());
+    }
+
+    public HisSlotLockResult releaseSchedule(String lockId, String traceId, HisRouteContext context) {
         log.info("releaseSchedule lockId={} traceId={}", lockId, traceId);
+        HisClient hisClient = hisClientRouter.route(context);
         return hisClient.releaseSlot(lockId, traceId);
     }
 
     public Map<String, Object> createPaymentOrder(String lockId, String traceId, String idempotencyKey) {
+        return createPaymentOrder(lockId, traceId, idempotencyKey, HisRouteContext.empty());
+    }
+
+    public Map<String, Object> createPaymentOrder(String lockId, String traceId, String idempotencyKey,
+                                                   HisRouteContext context) {
         log.info("createPaymentOrder lockId={} traceId={} idempotencyKey={}", lockId, traceId, idempotencyKey);
+        HisClient hisClient = hisClientRouter.route(context);
         HisPaymentOrder order = hisClient.createPaymentOrder(lockId, traceId, idempotencyKey);
         return Map.of(
             "orderId", order.getOrderId(),
@@ -55,32 +89,52 @@ public class McpToolService {
             "amountYuan", String.format("%.2f", order.getAmountFen() / 100.0),
             "qrCodeContent", order.getQrCodeContent(),
             "status", order.getStatus(),
-            "mock", true,
+            "mock", hisClient.isMock(),
             "message", order.getMessage() != null ? order.getMessage() : ""
         );
     }
 
     public Map<String, Object> markMockPaid(String orderId, String traceId) {
+        return markMockPaid(orderId, traceId, HisRouteContext.empty());
+    }
+
+    public Map<String, Object> markMockPaid(String orderId, String traceId, HisRouteContext context) {
         log.info("markMockPaid orderId={} traceId={}", orderId, traceId);
+        HisClient hisClient = hisClientRouter.route(context);
         HisPaymentOrder order = hisClient.markPaid(orderId, traceId);
         return Map.of(
             "orderId", order.getOrderId(),
             "lockId", order.getLockId(),
             "status", order.getStatus(),
-            "mock", true,
+            "mock", hisClient.isMock(),
             "message", order.getMessage() != null ? order.getMessage() : ""
         );
     }
 
     public HisPaymentOrder queryPaymentStatus(String orderId, String traceId) {
+        return queryPaymentStatus(orderId, traceId, HisRouteContext.empty());
+    }
+
+    public HisPaymentOrder queryPaymentStatus(String orderId, String traceId, HisRouteContext context) {
+        HisClient hisClient = hisClientRouter.route(context);
         return hisClient.queryPaymentStatus(orderId, traceId);
     }
 
     public List<HisPatient> searchPatient(String phone, String idCard) {
+        return searchPatient(phone, idCard, HisRouteContext.empty());
+    }
+
+    public List<HisPatient> searchPatient(String phone, String idCard, HisRouteContext context) {
+        HisClient hisClient = hisClientRouter.route(context);
         return hisClient.searchPatient(phone, idCard);
     }
 
     public HisPatient createPatient(HisPatient patient, String idempotencyKey) {
+        return createPatient(patient, idempotencyKey, HisRouteContext.empty());
+    }
+
+    public HisPatient createPatient(HisPatient patient, String idempotencyKey, HisRouteContext context) {
+        HisClient hisClient = hisClientRouter.route(context);
         return hisClient.createPatient(patient, idempotencyKey);
     }
 
@@ -88,6 +142,16 @@ public class McpToolService {
                                                    String doctorId, String slotId,
                                                    String patientId, String departmentId,
                                                    String traceId, String idempotencyKey) {
+        return createAppointment(lockId, orderId, doctorId, slotId, patientId, departmentId,
+                traceId, idempotencyKey, HisRouteContext.empty());
+    }
+
+    public Map<String, Object> createAppointment(String lockId, String orderId,
+                                                   String doctorId, String slotId,
+                                                   String patientId, String departmentId,
+                                                   String traceId, String idempotencyKey,
+                                                   HisRouteContext context) {
+        HisClient hisClient = hisClientRouter.route(context);
         if (orderId != null) {
             HisPaymentOrder order = hisClient.queryPaymentStatus(orderId, traceId);
             if (order == null || !"PAID".equals(order.getStatus())) {
@@ -110,7 +174,7 @@ public class McpToolService {
         return Map.of(
             "appointmentNo", result.getAppointmentNo() != null ? result.getAppointmentNo() : result.getRegistrationId(),
             "queueNo", result.getQueueNo(),
-            "mock", true,
+            "mock", hisClient.isMock(),
             "message", "联调演示 / Mock 预约结果,不代表真实医院挂号成功。"
         );
     }
@@ -121,6 +185,7 @@ public class McpToolService {
     @Deprecated
     public HisSlotLock lockScheduleLegacy(String scheduleId, String patientId, String idempotencyKey) {
         log.warn("deprecated lockSchedule called, forwarding to lockSlot");
+        HisClient hisClient = hisClientRouter.route(HisRouteContext.empty());
         HisSlotLockResult result = hisClient.lockSlot(scheduleId, "legacy", idempotencyKey);
         return HisSlotLock.builder()
                 .lockId(result.getLockId())
@@ -136,6 +201,7 @@ public class McpToolService {
     @Deprecated
     public HisSlotLock releaseSchedule(String lockId) {
         log.warn("deprecated releaseSchedule called, forwarding to releaseSlot");
+        HisClient hisClient = hisClientRouter.route(HisRouteContext.empty());
         hisClient.releaseSlot(lockId, "legacy");
         return HisSlotLock.builder().lockId(lockId).status("RELEASED").mock(true).build();
     }

+ 43 - 0
emoon-infra/emoon-modules/emoon-ai/emoon-ai-mcp/src/test/java/com/emoon/ai/mcp/application/McpToolServiceTest.java

@@ -3,6 +3,9 @@ package com.emoon.ai.mcp.application;
 import com.emoon.common.core.exception.exception.ServiceException;
 import com.emoon.mcp.his.client.HisClient;
 import com.emoon.mcp.his.domain.*;
+import com.emoon.mcp.his.routing.HisClientRouter;
+import com.emoon.mcp.his.routing.HisRouteContext;
+import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.DisplayName;
 import org.junit.jupiter.api.Tag;
 import org.junit.jupiter.api.Test;
@@ -17,6 +20,8 @@ import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.lenient;
+import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
 @Tag("dev")
@@ -26,9 +31,18 @@ class McpToolServiceTest {
     @Mock
     private HisClient hisClient;
 
+    @Mock
+    private HisClientRouter hisClientRouter;
+
     @InjectMocks
     private McpToolService service;
 
+    @BeforeEach
+    void setUp() {
+        lenient().when(hisClientRouter.route(any(HisRouteContext.class))).thenReturn(hisClient);
+        lenient().when(hisClient.isMock()).thenReturn(true);
+    }
+
     @Test
     @DisplayName("queryDepartments 委托 HisClient")
     void queryDepartmentsDelegatesToHisClient() {
@@ -39,6 +53,20 @@ class McpToolServiceTest {
 
         assertThat(result).hasSize(1);
         assertThat(result.get(0).getDepartmentId()).isEqualTo("neurology");
+        verify(hisClientRouter).route(HisRouteContext.empty());
+    }
+
+    @Test
+    @DisplayName("queryDepartments 使用作用域医院客户端")
+    void queryDepartmentsUsesScopedHospitalClient() {
+        HisRouteContext context = new HisRouteContext("tenant-a", "7", "hospital-a");
+        HisClient hospitalClient = org.mockito.Mockito.mock(HisClient.class);
+        when(hisClientRouter.route(context)).thenReturn(hospitalClient);
+        when(hospitalClient.getDepartments()).thenReturn(List.of());
+
+        service.queryDepartments(context);
+
+        verify(hospitalClient).getDepartments();
     }
 
     @Test
@@ -86,4 +114,19 @@ class McpToolServiceTest {
                 .isInstanceOf(ServiceException.class)
                 .hasMessageContaining("未完成支付");
     }
+
+    @Test
+    @DisplayName("真实 HIS 支付订单不标记为 Mock")
+    void realClientPaymentResultIsNotMarkedAsMock() {
+        HisRouteContext context = new HisRouteContext("tenant-a", "7", "hospital-a");
+        when(hisClient.isMock()).thenReturn(false);
+        when(hisClient.createPaymentOrder("lock_001", "trace_001", "key-001")).thenReturn(
+                HisPaymentOrder.builder().orderId("pay_001").lockId("lock_001")
+                        .amountFen(3000).status("PENDING").qrCodeContent("pay://001").build());
+
+        var result = service.createPaymentOrder("lock_001", "trace_001", "key-001", context);
+
+        assertThat(result.get("mock")).isEqualTo(false);
+        verify(hisClientRouter).route(context);
+    }
 }