fix(security): sendCard 改为 DISPATCHED 状态变更时发送 + @EventListener
Some checks failed
Java CI with Maven / build (11) (push) Has been cancelled
Java CI with Maven / build (17) (push) Has been cancelled
Java CI with Maven / build (8) (push) Has been cancelled

- 将 sendCard 从 onOrderCreated 移至 handleDispatched
- 使用 @EventListener 替代 @TransactionalEventListener(AFTER_COMMIT)
  确保 autoDispatchNext 场景的 DISPATCHED 事件也能触发
- PAUSED → DISPATCHED 恢复场景跳过重发
- 参考保洁模块 CleanOrderEventListener 的实现模式

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
lzh
2026-03-26 14:06:31 +08:00
parent 68b6f45d53
commit c0c9854e73
4 changed files with 161 additions and 189 deletions

View File

@@ -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 {
/**
* 状态变更事件 - 记录扩展表时间点 + 业务日志
* <p>
* 使用 @EventListener 而非 @TransactionalEventListener(AFTER_COMMIT)
* 确保 autoDispatchNext 在 AFTER_COMMIT 中派发下一单时发布的 DISPATCHED
* 事件也能被捕获。各 handler 方法使用 @Async 异步执行。
* <p>
* 参考保洁模块 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) {

View File

@@ -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发送企微工单卡片
* <p>
* 由 {@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);
}
}

View File

@@ -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()

View File

@@ -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 企微通知时机测试
* <p>
* 验证 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<VspSendCardReqDTO> 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<VspSyncStatusReqDTO> 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<VspSyncStatusReqDTO> 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<VspSyncStatusReqDTO> 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());
}
// ==================== 辅助方法 ====================