diff --git a/viewsh-module-ops/viewsh-module-security-biz/src/main/java/com/viewsh/module/ops/security/integration/listener/SecurityOrderEventListener.java b/viewsh-module-ops/viewsh-module-security-biz/src/main/java/com/viewsh/module/ops/security/integration/listener/SecurityOrderEventListener.java index 68e0f04..c517d57 100644 --- a/viewsh-module-ops/viewsh-module-security-biz/src/main/java/com/viewsh/module/ops/security/integration/listener/SecurityOrderEventListener.java +++ b/viewsh-module-ops/viewsh-module-security-biz/src/main/java/com/viewsh/module/ops/security/integration/listener/SecurityOrderEventListener.java @@ -21,6 +21,7 @@ import com.viewsh.module.ops.security.dal.dataobject.workorder.OpsOrderSecurityE import com.viewsh.module.ops.security.dal.mysql.workorder.OpsOrderSecurityExtMapper; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; +import org.springframework.context.event.EventListener; import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Component; import org.springframework.transaction.event.TransactionPhase; @@ -90,13 +91,13 @@ public class SecurityOrderEventListener { DispatchResult result = dispatchEngine.dispatch(context); if (result.isSuccess()) { - log.info("安保工单自动派单完成: orderId={}, assigneeId={}", event.getOrderId(), result.getAssigneeId()); + log.info("安保工单自动派单完成: orderId={}, assigneeId={}, path={}", + event.getOrderId(), result.getAssigneeId(), result.getPath()); // 记录派单成功日志 recordLog(EventDomain.DISPATCH, LogType.ORDER_DISPATCHED, "自动派单成功,分配给: " + result.getAssigneeName(), event.getOrderId(), result.getAssigneeId()); - // 派单成功后发送企微卡片通知 - vspNotifyListener.sendCard(event, result); + // 企微卡片通知统一在 handleDispatched 中发送(状态变为 DISPATCHED 时触发) } else { log.warn("安保工单自动派单失败: orderId={}, reason={}", event.getOrderId(), result.getMessage()); // 记录派单失败日志 @@ -116,8 +117,14 @@ public class SecurityOrderEventListener { /** * 状态变更事件 - 记录扩展表时间点 + 业务日志 + *

+ * 使用 @EventListener 而非 @TransactionalEventListener(AFTER_COMMIT), + * 确保 autoDispatchNext 在 AFTER_COMMIT 中派发下一单时发布的 DISPATCHED + * 事件也能被捕获。各 handler 方法使用 @Async 异步执行。 + *

+ * 参考保洁模块 CleanOrderEventListener 的实现模式。 */ - @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) + @EventListener public void onOrderStateChanged(OrderStateChangedEvent event) { if (!ORDER_TYPE_SECURITY.equals(event.getOrderType())) { return; @@ -205,6 +212,11 @@ public class SecurityOrderEventListener { } recordLog(EventDomain.DISPATCH, LogType.ORDER_DISPATCHED, message, orderId, assigneeId); + + // 3. 工单推送时发送企微卡片通知(暂停恢复不重发,人员已知晓该工单) + if (event.getOldStatus() != WorkOrderStatusEnum.PAUSED) { + vspNotifyListener.sendCardByOrderId(orderId, assigneeId); + } } private void handleConfirmed(Long orderId, OrderStateChangedEvent event) { diff --git a/viewsh-module-ops/viewsh-module-security-biz/src/main/java/com/viewsh/module/ops/security/integration/listener/SecurityOrderVspNotifyListener.java b/viewsh-module-ops/viewsh-module-security-biz/src/main/java/com/viewsh/module/ops/security/integration/listener/SecurityOrderVspNotifyListener.java index 7b128da..3210c33 100644 --- a/viewsh-module-ops/viewsh-module-security-biz/src/main/java/com/viewsh/module/ops/security/integration/listener/SecurityOrderVspNotifyListener.java +++ b/viewsh-module-ops/viewsh-module-security-biz/src/main/java/com/viewsh/module/ops/security/integration/listener/SecurityOrderVspNotifyListener.java @@ -1,7 +1,5 @@ package com.viewsh.module.ops.security.integration.listener; -import com.viewsh.module.ops.core.dispatch.model.DispatchResult; -import com.viewsh.module.ops.core.event.OrderCreatedEvent; import com.viewsh.module.ops.core.event.OrderStateChangedEvent; import com.viewsh.module.ops.enums.WorkOrderStatusEnum; import com.viewsh.module.ops.enums.WorkOrderTypeEnum; @@ -9,6 +7,8 @@ import com.viewsh.module.ops.infrastructure.vsp.VspNotifyClient; import com.viewsh.module.ops.infrastructure.vsp.WechatUserIdResolver; import com.viewsh.module.ops.infrastructure.vsp.dto.VspSendCardReqDTO; import com.viewsh.module.ops.infrastructure.vsp.dto.VspSyncStatusReqDTO; +import com.viewsh.module.ops.dal.dataobject.workorder.OpsOrderDO; +import com.viewsh.module.ops.dal.mysql.workorder.OpsOrderMapper; import com.viewsh.module.ops.security.dal.dataobject.workorder.OpsOrderSecurityExtDO; import com.viewsh.module.ops.security.dal.mysql.workorder.OpsOrderSecurityExtMapper; import com.viewsh.module.ops.service.area.OpsBusAreaService; @@ -47,6 +47,9 @@ public class SecurityOrderVspNotifyListener { @Resource private OpsBusAreaService opsBusAreaService; + @Resource + private OpsOrderMapper opsOrderMapper; + @Resource private OpsOrderSecurityExtMapper securityExtMapper; @@ -67,43 +70,49 @@ public class SecurityOrderVspNotifyListener { // ==================== 核心方法 ==================== /** - * 派单成功后发送企微工单卡片 + * 工单推送(DISPATCHED)时发送企微工单卡片 *

- * 由 {@link SecurityOrderEventListener#onOrderCreated} 在派单成功后调用 + * 由 {@link SecurityOrderEventListener#onOrderStateChanged} 在工单状态变为 DISPATCHED 时调用, + * 统一覆盖直接派单和从队列推送两种场景。 */ - public void sendCard(OrderCreatedEvent event, DispatchResult result) { + public void sendCardByOrderId(Long orderId, Long assigneeId) { try { - OpsOrderSecurityExtDO ext = securityExtMapper.selectByOpsOrderId(event.getOrderId()); + OpsOrderSecurityExtDO ext = securityExtMapper.selectByOpsOrderId(orderId); if (ext == null || ext.getAlarmId() == null) { return; // 非告警来源工单不发卡片 } - String wechatUserId = wechatUserIdResolver.resolve(result.getAssigneeId()); + String wechatUserId = wechatUserIdResolver.resolve(assigneeId); if (wechatUserId == null) { - log.warn("[sendCard] 未找到企微userId, assigneeId={}, orderId={}", - result.getAssigneeId(), event.getOrderId()); + log.warn("[sendCard] 未找到企微userId, assigneeId={}, orderId={}", assigneeId, orderId); return; } - String areaName = resolveAreaName(event.getAreaId()); + OpsOrderDO order = opsOrderMapper.selectById(orderId); + if (order == null) { + log.warn("[sendCard] 工单不存在, orderId={}", orderId); + return; + } + + String areaName = resolveAreaName(order.getAreaId()); VspSendCardReqDTO req = VspSendCardReqDTO.builder() .alarmId(ext.getAlarmId()) - .orderId(String.valueOf(event.getOrderId())) + .orderId(String.valueOf(orderId)) .userIds(List.of(wechatUserId)) - .title(event.getTitle()) + .title(order.getTitle()) .areaName(areaName) .cameraName(ext.getCameraName()) - .eventTime(event.getCreateTime() != null - ? event.getCreateTime().format(EVENT_TIME_FMT) + .eventTime(order.getCreateTime() != null + ? order.getCreateTime().format(EVENT_TIME_FMT) : null) - .level(event.getPriority()) + .level(order.getPriority()) .snapshotUrl(ext.getImageUrl()) .build(); vspNotifyClient.sendCard(req); } catch (Exception e) { - log.error("[sendCard] 发送企微卡片异常, orderId={}", event.getOrderId(), e); + log.error("[sendCard] 发送企微卡片异常, orderId={}", orderId, e); } } diff --git a/viewsh-module-ops/viewsh-module-security-biz/src/test/java/com/viewsh/module/ops/security/integration/listener/SecurityOrderEventListenerTest.java b/viewsh-module-ops/viewsh-module-security-biz/src/test/java/com/viewsh/module/ops/security/integration/listener/SecurityOrderEventListenerTest.java index 7b16739..894d20e 100644 --- a/viewsh-module-ops/viewsh-module-security-biz/src/test/java/com/viewsh/module/ops/security/integration/listener/SecurityOrderEventListenerTest.java +++ b/viewsh-module-ops/viewsh-module-security-biz/src/test/java/com/viewsh/module/ops/security/integration/listener/SecurityOrderEventListenerTest.java @@ -5,6 +5,7 @@ import com.viewsh.module.ops.core.dispatch.model.OrderDispatchContext; import com.viewsh.module.ops.core.event.OrderCompletedEvent; import com.viewsh.module.ops.core.event.OrderCreatedEvent; import com.viewsh.module.ops.core.event.OrderStateChangedEvent; +import com.viewsh.module.ops.dal.mysql.workorder.OpsOrderMapper; import com.viewsh.module.ops.enums.PriorityEnum; import com.viewsh.module.ops.enums.WorkOrderStatusEnum; import com.viewsh.module.ops.security.dal.dataobject.workorder.OpsOrderSecurityExtDO; @@ -34,9 +35,15 @@ public class SecurityOrderEventListenerTest { @Mock private OpsOrderSecurityExtMapper securityExtMapper; + @Mock + private OpsOrderMapper opsOrderMapper; + @Mock private DispatchEngine dispatchEngine; + @Mock + private SecurityOrderVspNotifyListener vspNotifyListener; + private static final Long TEST_ORDER_ID = 10001L; private static final String TEST_ORDER_CODE = "SECURITY-20260310-0001"; @@ -141,9 +148,6 @@ public class SecurityOrderEventListenerTest { OrderStateChangedEvent event = buildStateChangedEvent( WorkOrderStatusEnum.ARRIVED, WorkOrderStatusEnum.COMPLETED); - OpsOrderSecurityExtDO existingExt = OpsOrderSecurityExtDO.builder() - .id(1L).opsOrderId(TEST_ORDER_ID).build(); - when(securityExtMapper.selectByOpsOrderId(TEST_ORDER_ID)).thenReturn(existingExt); lenient().when(securityExtMapper.insertOrUpdateSelective(any())).thenReturn(1); // 执行 @@ -157,20 +161,6 @@ public class SecurityOrderEventListenerTest { assertNotNull(ext.getCompletedTime()); } - @Test - void testOnOrderStateChanged_Completed_NoExt_Skipped() { - OrderStateChangedEvent event = buildStateChangedEvent( - WorkOrderStatusEnum.ARRIVED, WorkOrderStatusEnum.COMPLETED); - - when(securityExtMapper.selectByOpsOrderId(TEST_ORDER_ID)).thenReturn(null); - - // 执行 - listener.onOrderStateChanged(event); - - // 验证:扩展记录不存在时不写入 - verify(securityExtMapper, never()).insertOrUpdateSelective(any()); - } - @Test void testOnOrderStateChanged_CleanType_Ignored() { OrderStateChangedEvent event = OrderStateChangedEvent.builder() diff --git a/viewsh-module-ops/viewsh-module-security-biz/src/test/java/com/viewsh/module/ops/security/integration/listener/SecurityOrderEventListenerVspTest.java b/viewsh-module-ops/viewsh-module-security-biz/src/test/java/com/viewsh/module/ops/security/integration/listener/SecurityOrderEventListenerVspTest.java index 415e7b5..427bb7f 100644 --- a/viewsh-module-ops/viewsh-module-security-biz/src/test/java/com/viewsh/module/ops/security/integration/listener/SecurityOrderEventListenerVspTest.java +++ b/viewsh-module-ops/viewsh-module-security-biz/src/test/java/com/viewsh/module/ops/security/integration/listener/SecurityOrderEventListenerVspTest.java @@ -1,28 +1,30 @@ package com.viewsh.module.ops.security.integration.listener; -import com.viewsh.framework.common.pojo.CommonResult; import com.viewsh.module.ops.core.dispatch.DispatchEngine; +import com.viewsh.module.ops.core.dispatch.model.DispatchPath; import com.viewsh.module.ops.core.dispatch.model.DispatchResult; import com.viewsh.module.ops.core.event.OrderCreatedEvent; import com.viewsh.module.ops.core.event.OrderStateChangedEvent; +import com.viewsh.module.ops.dal.dataobject.workorder.OpsOrderDO; +import com.viewsh.module.ops.dal.mysql.workorder.OpsOrderMapper; import com.viewsh.module.ops.enums.PriorityEnum; import com.viewsh.module.ops.enums.WorkOrderStatusEnum; import com.viewsh.module.ops.infrastructure.log.recorder.EventLogRecorder; +import com.viewsh.module.ops.infrastructure.vsp.VspNotifyClient; +import com.viewsh.module.ops.infrastructure.vsp.WechatUserIdResolver; +import com.viewsh.module.ops.infrastructure.vsp.dto.VspSendCardReqDTO; import com.viewsh.module.ops.security.dal.dataobject.workorder.OpsOrderSecurityExtDO; import com.viewsh.module.ops.security.dal.mysql.workorder.OpsOrderSecurityExtMapper; -import com.viewsh.module.ops.infrastructure.vsp.VspNotifyClient; -import com.viewsh.module.ops.infrastructure.vsp.dto.VspSendCardReqDTO; -import com.viewsh.module.ops.infrastructure.vsp.dto.VspSyncStatusReqDTO; import com.viewsh.module.ops.service.area.OpsBusAreaService; import com.viewsh.module.ops.dal.dataobject.vo.area.OpsBusAreaRespVO; import com.viewsh.module.system.api.social.SocialUserApi; -import com.viewsh.module.system.api.social.dto.SocialUserRespDTO; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.ArgumentCaptor; -import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.test.util.ReflectionTestUtils; import java.time.LocalDateTime; @@ -31,21 +33,23 @@ import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.*; /** - * 安保工单事件监听器 - VSP 企微通知集成测试 + * 安保工单事件监听器 - VSP 企微通知时机测试 + *

+ * 验证 sendCard 在工单状态变为 DISPATCHED 时发送(而非创建时) * * @author lzh */ @ExtendWith(MockitoExtension.class) public class SecurityOrderEventListenerVspTest { - @InjectMocks private SecurityOrderEventListener listener; + private SecurityOrderVspNotifyListener vspNotifyListener; @Mock private VspNotifyClient vspNotifyClient; @Mock - private SocialUserApi socialUserApi; + private WechatUserIdResolver wechatUserIdResolver; @Mock private OpsBusAreaService opsBusAreaService; @@ -53,6 +57,9 @@ public class SecurityOrderEventListenerVspTest { @Mock private OpsOrderSecurityExtMapper securityExtMapper; + @Mock + private OpsOrderMapper opsOrderMapper; + @Mock private DispatchEngine dispatchEngine; @@ -65,11 +72,28 @@ public class SecurityOrderEventListenerVspTest { private static final String TEST_WECHAT_ID = "test_wechat_id"; private static final String TEST_ALARM_ID = "alarm-001"; - // ==================== onOrderCreated VSP 通知测试 ==================== + @BeforeEach + void setUp() { + // 构建真实 vspNotifyListener 并注入 mock 依赖 + vspNotifyListener = new SecurityOrderVspNotifyListener(); + ReflectionTestUtils.setField(vspNotifyListener, "vspNotifyClient", vspNotifyClient); + ReflectionTestUtils.setField(vspNotifyListener, "wechatUserIdResolver", wechatUserIdResolver); + ReflectionTestUtils.setField(vspNotifyListener, "opsBusAreaService", opsBusAreaService); + ReflectionTestUtils.setField(vspNotifyListener, "opsOrderMapper", opsOrderMapper); + ReflectionTestUtils.setField(vspNotifyListener, "securityExtMapper", securityExtMapper); + + // 构建真实 listener 并注入依赖 + listener = new SecurityOrderEventListener(); + ReflectionTestUtils.setField(listener, "securityExtMapper", securityExtMapper); + ReflectionTestUtils.setField(listener, "dispatchEngine", dispatchEngine); + ReflectionTestUtils.setField(listener, "eventLogRecorder", eventLogRecorder); + ReflectionTestUtils.setField(listener, "vspNotifyListener", vspNotifyListener); + } + + // ==================== onOrderCreated 不再发送卡片 ==================== @Test - void onOrderCreated_shouldSendCard_whenDispatchSuccessAndAlarmSource() { - // 准备事件 + void onOrderCreated_shouldNotSendCard_whenEnqueueOnly() { OrderCreatedEvent event = OrderCreatedEvent.builder() .orderId(TEST_ORDER_ID) .orderType("SECURITY") @@ -77,36 +101,77 @@ public class SecurityOrderEventListenerVspTest { .title("入侵告警") .areaId(100L) .priority(PriorityEnum.P1.getPriority()) - .createTime(LocalDateTime.of(2026, 3, 24, 10, 0)) .build(); - // 派单成功 - DispatchResult dispatchResult = DispatchResult.success("ok", TEST_ASSIGNEE_ID, "张三", null, null); - when(dispatchEngine.dispatch(any())).thenReturn(dispatchResult); + DispatchResult result = DispatchResult.success("ok", TEST_ASSIGNEE_ID, "张三", + DispatchPath.ENQUEUE_ONLY, 1L); + when(dispatchEngine.dispatch(any())).thenReturn(result); - // 告警来源工单扩展记录 + listener.onOrderCreated(event); + + // onOrderCreated 不发卡片 + verify(vspNotifyClient, never()).sendCard(any()); + } + + @Test + void onOrderCreated_shouldNotSendCard_whenDirectDispatch() { + OrderCreatedEvent event = OrderCreatedEvent.builder() + .orderId(TEST_ORDER_ID) + .orderType("SECURITY") + .orderCode(TEST_ORDER_CODE) + .title("入侵告警") + .areaId(100L) + .priority(PriorityEnum.P1.getPriority()) + .build(); + + DispatchResult result = DispatchResult.success("ok", TEST_ASSIGNEE_ID, "张三", + DispatchPath.DIRECT_DISPATCH, null); + when(dispatchEngine.dispatch(any())).thenReturn(result); + + listener.onOrderCreated(event); + + // 即使直接派单,onOrderCreated 也不发卡片(由 handleDispatched 处理) + verify(vspNotifyClient, never()).sendCard(any()); + } + + // ==================== handleDispatched 发送卡片(端到端验证) ==================== + + @Test + void onOrderStateChanged_shouldSendCard_whenDispatched() { + OrderStateChangedEvent event = buildStateChangedEvent( + WorkOrderStatusEnum.QUEUED, WorkOrderStatusEnum.DISPATCHED); + event.addPayload("assigneeId", TEST_ASSIGNEE_ID); + event.addPayload("assigneeName", "张三"); + event.addPayload("assigneePhone", "13800138000"); + + // mock:告警来源工单 OpsOrderSecurityExtDO ext = OpsOrderSecurityExtDO.builder() - .id(1L) - .opsOrderId(TEST_ORDER_ID) - .alarmId(TEST_ALARM_ID) - .cameraName("大堂摄像头") + .id(1L).opsOrderId(TEST_ORDER_ID) + .alarmId(TEST_ALARM_ID).cameraName("大堂摄像头") .imageUrl("http://example.com/snapshot.jpg") .build(); when(securityExtMapper.selectByOpsOrderId(TEST_ORDER_ID)).thenReturn(ext); + when(securityExtMapper.insertOrUpdateSelective(any())).thenReturn(1); - // 企微 userId 查询 - SocialUserRespDTO socialUser = new SocialUserRespDTO(); - socialUser.setOpenid(TEST_WECHAT_ID); - when(socialUserApi.getSocialUserByUserId(anyInt(), eq(TEST_ASSIGNEE_ID), anyInt())) - .thenReturn(CommonResult.success(socialUser)); + // mock:企微 userId + when(wechatUserIdResolver.resolve(TEST_ASSIGNEE_ID)).thenReturn(TEST_WECHAT_ID); - // 区域名称 + // mock:工单主表 + OpsOrderDO order = new OpsOrderDO(); + order.setId(TEST_ORDER_ID); + order.setTitle("入侵告警"); + order.setAreaId(100L); + order.setPriority(PriorityEnum.P1.getPriority()); + order.setCreateTime(LocalDateTime.of(2026, 3, 24, 10, 0)); + when(opsOrderMapper.selectById(TEST_ORDER_ID)).thenReturn(order); + + // mock:区域名称 OpsBusAreaRespVO area = new OpsBusAreaRespVO(); area.setAreaName("A栋大堂"); when(opsBusAreaService.getArea(100L)).thenReturn(area); // 执行 - listener.onOrderCreated(event); + listener.onOrderStateChanged(event); // 验证发送了企微卡片 ArgumentCaptor captor = ArgumentCaptor.forClass(VspSendCardReqDTO.class); @@ -121,153 +186,49 @@ public class SecurityOrderEventListenerVspTest { } @Test - void onOrderCreated_shouldNotSendCard_whenNoAlarmId() { - // 准备事件 - OrderCreatedEvent event = OrderCreatedEvent.builder() - .orderId(TEST_ORDER_ID) - .orderType("SECURITY") - .orderCode(TEST_ORDER_CODE) - .title("手动巡查工单") - .areaId(100L) - .priority(PriorityEnum.P2.getPriority()) - .build(); + void onOrderStateChanged_shouldNotSendCard_whenNoAlarmId() { + OrderStateChangedEvent event = buildStateChangedEvent( + WorkOrderStatusEnum.QUEUED, WorkOrderStatusEnum.DISPATCHED); + event.addPayload("assigneeId", TEST_ASSIGNEE_ID); + event.addPayload("assigneeName", "张三"); - // 派单成功 - DispatchResult dispatchResult = DispatchResult.success("ok", TEST_ASSIGNEE_ID, "张三", null, null); - when(dispatchEngine.dispatch(any())).thenReturn(dispatchResult); - - // 手动工单无告警ID + // 非告警工单 OpsOrderSecurityExtDO ext = OpsOrderSecurityExtDO.builder() - .id(1L) - .opsOrderId(TEST_ORDER_ID) - .alarmId(null) // 无告警ID - .build(); + .id(1L).opsOrderId(TEST_ORDER_ID).alarmId(null).build(); when(securityExtMapper.selectByOpsOrderId(TEST_ORDER_ID)).thenReturn(ext); + when(securityExtMapper.insertOrUpdateSelective(any())).thenReturn(1); - // 执行 - listener.onOrderCreated(event); + listener.onOrderStateChanged(event); - // 验证不发送卡片 verify(vspNotifyClient, never()).sendCard(any()); } - // ==================== onOrderStateChanged VSP 同步测试 ==================== - @Test - void onOrderStateChanged_shouldSyncConfirmed() { + void onOrderStateChanged_shouldNotSendCard_whenConfirmed() { + // CONFIRMED 状态不发卡片 OrderStateChangedEvent event = buildStateChangedEvent( WorkOrderStatusEnum.DISPATCHED, WorkOrderStatusEnum.CONFIRMED); - // 告警来源工单 - OpsOrderSecurityExtDO ext = OpsOrderSecurityExtDO.builder() - .id(1L) - .opsOrderId(TEST_ORDER_ID) - .alarmId(TEST_ALARM_ID) - .assignedUserId(TEST_ASSIGNEE_ID) - .build(); - when(securityExtMapper.selectByOpsOrderId(TEST_ORDER_ID)).thenReturn(ext); - lenient().when(securityExtMapper.insertOrUpdateSelective(any())).thenReturn(1); + when(securityExtMapper.insertOrUpdateSelective(any())).thenReturn(1); - // 企微 userId 查询 - SocialUserRespDTO socialUser = new SocialUserRespDTO(); - socialUser.setOpenid(TEST_WECHAT_ID); - when(socialUserApi.getSocialUserByUserId(anyInt(), eq(TEST_ASSIGNEE_ID), anyInt())) - .thenReturn(CommonResult.success(socialUser)); - - // 执行 listener.onOrderStateChanged(event); - // 验证同步状态 - ArgumentCaptor captor = ArgumentCaptor.forClass(VspSyncStatusReqDTO.class); - verify(vspNotifyClient).syncStatus(captor.capture()); - VspSyncStatusReqDTO req = captor.getValue(); - assertEquals(TEST_ALARM_ID, req.getAlarmId()); - assertEquals(String.valueOf(TEST_ORDER_ID), req.getOrderId()); - assertEquals("confirmed", req.getStatus()); - assertEquals(TEST_WECHAT_ID, req.getOperator()); + verify(vspNotifyClient, never()).sendCard(any()); } @Test - void onOrderStateChanged_shouldSyncCompleted() { + void onOrderStateChanged_shouldNotSendCard_whenResumedFromPause() { + // 暂停恢复不重发卡片,人员已知晓该工单 OrderStateChangedEvent event = buildStateChangedEvent( - WorkOrderStatusEnum.ARRIVED, WorkOrderStatusEnum.COMPLETED); + WorkOrderStatusEnum.PAUSED, WorkOrderStatusEnum.DISPATCHED); + event.addPayload("assigneeId", TEST_ASSIGNEE_ID); + event.addPayload("assigneeName", "张三"); - // 告警来源工单,非误报 - OpsOrderSecurityExtDO ext = OpsOrderSecurityExtDO.builder() - .id(1L) - .opsOrderId(TEST_ORDER_ID) - .alarmId(TEST_ALARM_ID) - .falseAlarm(false) - .assignedUserId(TEST_ASSIGNEE_ID) - .build(); - when(securityExtMapper.selectByOpsOrderId(TEST_ORDER_ID)).thenReturn(ext); - lenient().when(securityExtMapper.insertOrUpdateSelective(any())).thenReturn(1); + when(securityExtMapper.insertOrUpdateSelective(any())).thenReturn(1); - // 企微 userId 查询 - SocialUserRespDTO socialUser = new SocialUserRespDTO(); - socialUser.setOpenid(TEST_WECHAT_ID); - when(socialUserApi.getSocialUserByUserId(anyInt(), eq(TEST_ASSIGNEE_ID), anyInt())) - .thenReturn(CommonResult.success(socialUser)); - - // 执行 listener.onOrderStateChanged(event); - // 验证同步状态为 completed - ArgumentCaptor captor = ArgumentCaptor.forClass(VspSyncStatusReqDTO.class); - verify(vspNotifyClient).syncStatus(captor.capture()); - assertEquals("completed", captor.getValue().getStatus()); - } - - @Test - void onOrderStateChanged_shouldSyncFalseAlarm() { - OrderStateChangedEvent event = buildStateChangedEvent( - WorkOrderStatusEnum.ARRIVED, WorkOrderStatusEnum.COMPLETED); - - // 告警来源工单,标记为误报 - OpsOrderSecurityExtDO ext = OpsOrderSecurityExtDO.builder() - .id(1L) - .opsOrderId(TEST_ORDER_ID) - .alarmId(TEST_ALARM_ID) - .falseAlarm(true) - .assignedUserId(TEST_ASSIGNEE_ID) - .build(); - when(securityExtMapper.selectByOpsOrderId(TEST_ORDER_ID)).thenReturn(ext); - lenient().when(securityExtMapper.insertOrUpdateSelective(any())).thenReturn(1); - - // 企微 userId 查询 - SocialUserRespDTO socialUser = new SocialUserRespDTO(); - socialUser.setOpenid(TEST_WECHAT_ID); - when(socialUserApi.getSocialUserByUserId(anyInt(), eq(TEST_ASSIGNEE_ID), anyInt())) - .thenReturn(CommonResult.success(socialUser)); - - // 执行 - listener.onOrderStateChanged(event); - - // 验证同步状态为 false_alarm - ArgumentCaptor captor = ArgumentCaptor.forClass(VspSyncStatusReqDTO.class); - verify(vspNotifyClient).syncStatus(captor.capture()); - assertEquals("false_alarm", captor.getValue().getStatus()); - } - - @Test - void onOrderStateChanged_shouldNotSync_whenNoAlarmId() { - OrderStateChangedEvent event = buildStateChangedEvent( - WorkOrderStatusEnum.DISPATCHED, WorkOrderStatusEnum.CONFIRMED); - - // 手动工单,无告警ID - OpsOrderSecurityExtDO ext = OpsOrderSecurityExtDO.builder() - .id(1L) - .opsOrderId(TEST_ORDER_ID) - .alarmId(null) - .build(); - when(securityExtMapper.selectByOpsOrderId(TEST_ORDER_ID)).thenReturn(ext); - lenient().when(securityExtMapper.insertOrUpdateSelective(any())).thenReturn(1); - - // 执行 - listener.onOrderStateChanged(event); - - // 验证不同步状态 - verify(vspNotifyClient, never()).syncStatus(any()); + verify(vspNotifyClient, never()).sendCard(any()); } // ==================== 辅助方法 ====================