test(ops): 新增安保模块单元测试

覆盖 SecurityOrderService、SecurityOrderEventListener、
SecurityAreaAssignStrategy、SecurityOrderExtQueryHandler、
OpsAreaSecurityUserService 的核心逻辑测试。

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
lzh
2026-03-11 17:33:28 +08:00
parent 4d36bf5b1c
commit 0f2fb3c50e
5 changed files with 1134 additions and 0 deletions

View File

@@ -0,0 +1,277 @@
package com.viewsh.module.ops.security.integration.listener;
import com.viewsh.module.ops.core.dispatch.DispatchEngine;
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.enums.PriorityEnum;
import com.viewsh.module.ops.enums.WorkOrderStatusEnum;
import com.viewsh.module.ops.security.dal.dataobject.workorder.OpsOrderSecurityExtDO;
import com.viewsh.module.ops.security.dal.mysql.workorder.OpsOrderSecurityExtMapper;
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 static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
/**
* 安保工单事件监听器测试
*
* @author lzh
*/
@ExtendWith(MockitoExtension.class)
public class SecurityOrderEventListenerTest {
@InjectMocks
private SecurityOrderEventListener listener;
@Mock
private OpsOrderSecurityExtMapper securityExtMapper;
@Mock
private DispatchEngine dispatchEngine;
private static final Long TEST_ORDER_ID = 10001L;
private static final String TEST_ORDER_CODE = "SECURITY-20260310-0001";
// ==================== onOrderCreated 测试 ====================
@Test
void testOnOrderCreated_SecurityType_TriggersDispatch() {
OrderCreatedEvent event = OrderCreatedEvent.builder()
.orderId(TEST_ORDER_ID)
.orderType("SECURITY")
.orderCode(TEST_ORDER_CODE)
.title("入侵告警")
.areaId(100L)
.priority(PriorityEnum.P1.getPriority())
.build();
// 执行
listener.onOrderCreated(event);
// 验证触发了派单asyncDispatchAfterCreated 是自调用,实际同步执行)
ArgumentCaptor<OrderDispatchContext> contextCaptor = ArgumentCaptor.forClass(OrderDispatchContext.class);
verify(dispatchEngine).dispatch(contextCaptor.capture());
OrderDispatchContext ctx = contextCaptor.getValue();
assertEquals(TEST_ORDER_ID, ctx.getOrderId());
assertEquals(TEST_ORDER_CODE, ctx.getOrderCode());
assertEquals("入侵告警", ctx.getOrderTitle());
assertEquals("SECURITY", ctx.getBusinessType());
assertEquals(100L, ctx.getAreaId());
assertEquals(PriorityEnum.P1, ctx.getPriority());
}
@Test
void testOnOrderCreated_CleanType_Ignored() {
OrderCreatedEvent event = OrderCreatedEvent.builder()
.orderId(TEST_ORDER_ID)
.orderType("CLEAN")
.build();
// 执行
listener.onOrderCreated(event);
// 验证不触发派单
verify(dispatchEngine, never()).dispatch(any());
}
@Test
void testOnOrderCreated_DispatchException_Caught() {
OrderCreatedEvent event = OrderCreatedEvent.builder()
.orderId(TEST_ORDER_ID)
.orderType("SECURITY")
.orderCode(TEST_ORDER_CODE)
.areaId(100L)
.priority(PriorityEnum.P1.getPriority())
.build();
when(dispatchEngine.dispatch(any())).thenThrow(new RuntimeException("调度引擎异常"));
// 执行:不应抛出异常
assertDoesNotThrow(() -> listener.onOrderCreated(event));
}
// ==================== onOrderStateChanged 测试 ====================
@Test
void testOnOrderStateChanged_Dispatched_RecordsTime() {
OrderStateChangedEvent event = buildStateChangedEvent(
WorkOrderStatusEnum.PENDING, WorkOrderStatusEnum.DISPATCHED);
lenient().when(securityExtMapper.insertOrUpdateSelective(any())).thenReturn(1);
// 执行
listener.onOrderStateChanged(event);
// 验证扩展表写入 dispatchedTime
ArgumentCaptor<OpsOrderSecurityExtDO> captor = ArgumentCaptor.forClass(OpsOrderSecurityExtDO.class);
verify(securityExtMapper).insertOrUpdateSelective(captor.capture());
OpsOrderSecurityExtDO ext = captor.getValue();
assertEquals(TEST_ORDER_ID, ext.getOpsOrderId());
assertNotNull(ext.getDispatchedTime());
}
@Test
void testOnOrderStateChanged_Confirmed_RecordsTime() {
OrderStateChangedEvent event = buildStateChangedEvent(
WorkOrderStatusEnum.DISPATCHED, WorkOrderStatusEnum.CONFIRMED);
lenient().when(securityExtMapper.insertOrUpdateSelective(any())).thenReturn(1);
// 执行
listener.onOrderStateChanged(event);
// 验证扩展表写入 confirmedTime
ArgumentCaptor<OpsOrderSecurityExtDO> captor = ArgumentCaptor.forClass(OpsOrderSecurityExtDO.class);
verify(securityExtMapper).insertOrUpdateSelective(captor.capture());
OpsOrderSecurityExtDO ext = captor.getValue();
assertEquals(TEST_ORDER_ID, ext.getOpsOrderId());
assertNotNull(ext.getConfirmedTime());
}
@Test
void testOnOrderStateChanged_Completed_RecordsTime() {
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);
// 执行
listener.onOrderStateChanged(event);
// 验证扩展表写入 completedTime
ArgumentCaptor<OpsOrderSecurityExtDO> captor = ArgumentCaptor.forClass(OpsOrderSecurityExtDO.class);
verify(securityExtMapper).insertOrUpdateSelective(captor.capture());
OpsOrderSecurityExtDO ext = captor.getValue();
assertEquals(TEST_ORDER_ID, ext.getOpsOrderId());
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()
.orderId(TEST_ORDER_ID)
.orderType("CLEAN")
.oldStatus(WorkOrderStatusEnum.PENDING)
.newStatus(WorkOrderStatusEnum.DISPATCHED)
.build();
// 执行
listener.onOrderStateChanged(event);
// 验证不触发任何扩展表操作
verify(securityExtMapper, never()).insertOrUpdateSelective(any());
verify(securityExtMapper, never()).selectByOpsOrderId(anyLong());
}
@Test
void testOnOrderStateChanged_OtherStatus_NoAction() {
// CANCELLED 等状态无需额外处理
OrderStateChangedEvent event = buildStateChangedEvent(
WorkOrderStatusEnum.PENDING, WorkOrderStatusEnum.CANCELLED);
// 执行
listener.onOrderStateChanged(event);
// 验证无扩展表操作
verify(securityExtMapper, never()).insertOrUpdateSelective(any());
}
// ==================== onOrderCompleted 测试 ====================
@Test
void testOnOrderCompleted_HasAssignee_DispatchNext() {
OrderCompletedEvent event = OrderCompletedEvent.builder()
.orderId(TEST_ORDER_ID)
.orderType("SECURITY")
.assigneeId(2001L)
.build();
// 执行
listener.onOrderCompleted(event);
// 验证调用自动派送下一个
verify(dispatchEngine).autoDispatchNext(TEST_ORDER_ID, 2001L);
}
@Test
void testOnOrderCompleted_NoAssignee_SkipDispatch() {
OrderCompletedEvent event = OrderCompletedEvent.builder()
.orderId(TEST_ORDER_ID)
.orderType("SECURITY")
.assigneeId(null) // 无分配人
.build();
// 执行
listener.onOrderCompleted(event);
// 验证不调用自动派送
verify(dispatchEngine, never()).autoDispatchNext(anyLong(), anyLong());
}
@Test
void testOnOrderCompleted_CleanType_Ignored() {
OrderCompletedEvent event = OrderCompletedEvent.builder()
.orderId(TEST_ORDER_ID)
.orderType("CLEAN")
.assigneeId(2001L)
.build();
// 执行
listener.onOrderCompleted(event);
// 验证不触发
verify(dispatchEngine, never()).autoDispatchNext(anyLong(), anyLong());
}
@Test
void testOnOrderCompleted_DispatchException_Caught() {
OrderCompletedEvent event = OrderCompletedEvent.builder()
.orderId(TEST_ORDER_ID)
.orderType("SECURITY")
.assigneeId(2001L)
.build();
doThrow(new RuntimeException("派送异常")).when(dispatchEngine)
.autoDispatchNext(anyLong(), anyLong());
// 执行:不应抛出异常
assertDoesNotThrow(() -> listener.onOrderCompleted(event));
}
// ==================== 辅助方法 ====================
private OrderStateChangedEvent buildStateChangedEvent(
WorkOrderStatusEnum oldStatus, WorkOrderStatusEnum newStatus) {
return OrderStateChangedEvent.builder()
.orderId(TEST_ORDER_ID)
.orderType("SECURITY")
.oldStatus(oldStatus)
.newStatus(newStatus)
.build();
}
}

View File

@@ -0,0 +1,122 @@
package com.viewsh.module.ops.security.service.area;
import com.viewsh.framework.common.exception.ServiceException;
import com.viewsh.module.ops.security.dal.dataobject.area.OpsAreaSecurityUserDO;
import com.viewsh.module.ops.security.dal.mysql.area.OpsAreaSecurityUserMapper;
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 java.util.Arrays;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
/**
* 区域安保人员绑定服务单元测试
*
* @author lzh
*/
@ExtendWith(MockitoExtension.class)
public class OpsAreaSecurityUserServiceTest {
@InjectMocks
private OpsAreaSecurityUserServiceImpl service;
@Mock
private OpsAreaSecurityUserMapper areaSecurityUserMapper;
@Test
void bindUser_success() {
when(areaSecurityUserMapper.selectByAreaIdAndUserId(100L, 2001L)).thenReturn(null);
when(areaSecurityUserMapper.insert(any(OpsAreaSecurityUserDO.class))).thenReturn(1);
Long id = service.bindUser(100L, 2001L, "张三", 10L, 0);
ArgumentCaptor<OpsAreaSecurityUserDO> captor = ArgumentCaptor.forClass(OpsAreaSecurityUserDO.class);
verify(areaSecurityUserMapper).insert(captor.capture());
OpsAreaSecurityUserDO saved = captor.getValue();
assertEquals(100L, saved.getAreaId());
assertEquals(2001L, saved.getUserId());
assertEquals("张三", saved.getUserName());
assertEquals(10L, saved.getTeamId());
assertTrue(saved.getEnabled());
assertEquals(0, saved.getSort());
}
@Test
void bindUser_duplicate_throws() {
OpsAreaSecurityUserDO existing = OpsAreaSecurityUserDO.builder()
.id(1L).areaId(100L).userId(2001L).build();
when(areaSecurityUserMapper.selectByAreaIdAndUserId(100L, 2001L)).thenReturn(existing);
assertThrows(ServiceException.class, () -> service.bindUser(100L, 2001L, "张三", null, null));
verify(areaSecurityUserMapper, never()).insert(any(OpsAreaSecurityUserDO.class));
}
@Test
void unbindUser_success() {
OpsAreaSecurityUserDO existing = OpsAreaSecurityUserDO.builder()
.id(1L).areaId(100L).userId(2001L).build();
when(areaSecurityUserMapper.selectById(1L)).thenReturn(existing);
when(areaSecurityUserMapper.deleteById(1L)).thenReturn(1);
service.unbindUser(1L);
verify(areaSecurityUserMapper).deleteById(1L);
}
@Test
void unbindUser_notFound_throws() {
when(areaSecurityUserMapper.selectById(999L)).thenReturn(null);
assertThrows(ServiceException.class, () -> service.unbindUser(999L));
verify(areaSecurityUserMapper, never()).deleteById(any());
}
@Test
void listByAreaId_returnsEnabledOnly() {
OpsAreaSecurityUserDO user1 = OpsAreaSecurityUserDO.builder()
.id(1L).areaId(100L).userId(2001L).userName("张三").enabled(true).build();
OpsAreaSecurityUserDO user2 = OpsAreaSecurityUserDO.builder()
.id(2L).areaId(100L).userId(2002L).userName("李四").enabled(true).build();
when(areaSecurityUserMapper.selectListByAreaId(100L)).thenReturn(Arrays.asList(user1, user2));
List<OpsAreaSecurityUserDO> result = service.listByAreaId(100L);
assertEquals(2, result.size());
verify(areaSecurityUserMapper).selectListByAreaId(100L);
}
@Test
void updateBinding_success() {
OpsAreaSecurityUserDO existing = OpsAreaSecurityUserDO.builder()
.id(1L).areaId(100L).userId(2001L).enabled(true).sort(0).build();
when(areaSecurityUserMapper.selectById(1L)).thenReturn(existing);
when(areaSecurityUserMapper.updateById(any(OpsAreaSecurityUserDO.class))).thenReturn(1);
service.updateBinding(1L, false, 5, 20L);
ArgumentCaptor<OpsAreaSecurityUserDO> captor = ArgumentCaptor.forClass(OpsAreaSecurityUserDO.class);
verify(areaSecurityUserMapper).updateById(captor.capture());
OpsAreaSecurityUserDO updated = captor.getValue();
assertEquals(1L, updated.getId());
assertFalse(updated.getEnabled());
assertEquals(5, updated.getSort());
assertEquals(20L, updated.getTeamId());
}
@Test
void updateBinding_notFound_throws() {
when(areaSecurityUserMapper.selectById(999L)).thenReturn(null);
assertThrows(ServiceException.class, () -> service.updateBinding(999L, true, null, null));
}
}

View File

@@ -0,0 +1,108 @@
package com.viewsh.module.ops.security.service.dispatch;
import com.viewsh.module.ops.core.dispatch.model.AssigneeRecommendation;
import com.viewsh.module.ops.core.dispatch.model.OrderDispatchContext;
import com.viewsh.module.ops.security.dal.dataobject.area.OpsAreaSecurityUserDO;
import com.viewsh.module.ops.security.dal.mysql.area.OpsAreaSecurityUserMapper;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.util.Arrays;
import java.util.Collections;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
/**
* 安保区域分配策略单元测试
*
* @author lzh
*/
@ExtendWith(MockitoExtension.class)
public class SecurityAreaAssignStrategyTest {
@InjectMocks
private SecurityAreaAssignStrategy strategy;
@Mock
private OpsAreaSecurityUserMapper areaSecurityUserMapper;
@Test
void recommend_noAreaId_returnsNone() {
OrderDispatchContext context = OrderDispatchContext.builder()
.orderId(1001L)
.areaId(null)
.build();
AssigneeRecommendation result = strategy.recommend(context);
assertFalse(result.hasRecommendation());
verify(areaSecurityUserMapper, never()).selectListByAreaId(any());
}
@Test
void recommend_noUsersInArea_returnsNone() {
Long areaId = 100L;
OrderDispatchContext context = OrderDispatchContext.builder()
.orderId(1001L)
.areaId(areaId)
.build();
when(areaSecurityUserMapper.selectListByAreaId(areaId)).thenReturn(Collections.emptyList());
AssigneeRecommendation result = strategy.recommend(context);
assertFalse(result.hasRecommendation());
verify(areaSecurityUserMapper).selectListByAreaId(areaId);
}
@Test
void recommend_hasUsers_returnsRecommendation() {
Long areaId = 100L;
OrderDispatchContext context = OrderDispatchContext.builder()
.orderId(1001L)
.areaId(areaId)
.build();
OpsAreaSecurityUserDO user1 = OpsAreaSecurityUserDO.builder()
.id(1L).areaId(areaId).userId(2001L).userName("张三").enabled(true).build();
OpsAreaSecurityUserDO user2 = OpsAreaSecurityUserDO.builder()
.id(2L).areaId(areaId).userId(2002L).userName("李四").enabled(true).build();
when(areaSecurityUserMapper.selectListByAreaId(areaId)).thenReturn(Arrays.asList(user1, user2));
AssigneeRecommendation result = strategy.recommend(context);
assertTrue(result.hasRecommendation());
assertNotNull(result.getAssigneeId());
assertTrue(result.getAssigneeId().equals(2001L) || result.getAssigneeId().equals(2002L));
assertEquals(50, result.getScore());
assertEquals("区域随机分配", result.getReason());
assertEquals(areaId, result.getAreaId());
}
@Test
void recommend_singleUser_returnsThatUser() {
Long areaId = 100L;
OrderDispatchContext context = OrderDispatchContext.builder()
.orderId(1001L)
.areaId(areaId)
.build();
OpsAreaSecurityUserDO user = OpsAreaSecurityUserDO.builder()
.id(1L).areaId(areaId).userId(2001L).userName("张三").enabled(true).build();
when(areaSecurityUserMapper.selectListByAreaId(areaId)).thenReturn(Collections.singletonList(user));
AssigneeRecommendation result = strategy.recommend(context);
assertTrue(result.hasRecommendation());
assertEquals(2001L, result.getAssigneeId());
assertEquals("张三", result.getAssigneeName());
assertEquals(areaId, result.getAreaId());
}
}

View File

@@ -0,0 +1,190 @@
package com.viewsh.module.ops.security.service.securityorder;
import com.viewsh.module.ops.dal.dataobject.workorder.OpsOrderDO;
import com.viewsh.module.ops.enums.PriorityEnum;
import com.viewsh.module.ops.enums.WorkOrderStatusEnum;
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.OrderDetailVO;
import com.viewsh.module.ops.service.OrderSummaryVO;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import java.time.LocalDateTime;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
/**
* 安保工单扩展查询处理器测试
*
* @author lzh
*/
@ExtendWith(MockitoExtension.class)
public class SecurityOrderExtQueryHandlerTest {
@InjectMocks
private SecurityOrderExtQueryHandler handler;
@Mock
private OpsOrderSecurityExtMapper securityExtMapper;
private static final Long TEST_ORDER_ID = 10001L;
private OpsOrderSecurityExtDO testSecurityExt;
private OpsOrderDO testOrder;
@BeforeEach
void setUp() {
testSecurityExt = OpsOrderSecurityExtDO.builder()
.id(1L)
.opsOrderId(TEST_ORDER_ID)
.alarmId("ALM20260310001")
.alarmType("intrusion")
.cameraId("CAM_001")
.roiId("ROI_001")
.imageUrl("https://oss.example.com/snapshot.jpg")
.assignedUserId(2001L)
.assignedUserName("张安保")
.assignedTeamId(301L)
.result("已排查,系误报")
.resultImgUrls("[\"https://oss/r1.jpg\"]")
.dispatchedTime(LocalDateTime.of(2026, 3, 10, 10, 0))
.confirmedTime(LocalDateTime.of(2026, 3, 10, 10, 2))
.completedTime(LocalDateTime.of(2026, 3, 10, 10, 30))
.build();
testOrder = OpsOrderDO.builder()
.id(TEST_ORDER_ID)
.orderCode("SECURITY-20260310-0001")
.orderType("SECURITY")
.sourceType("ALARM")
.title("A栋3层入侵告警")
.description("摄像头检测到异常人员")
.priority(PriorityEnum.P1.getPriority())
.status(WorkOrderStatusEnum.COMPLETED.getStatus())
.areaId(100L)
.location("A栋3层东侧走廊")
.assigneeId(2001L)
.assigneeName("张安保")
.startTime(LocalDateTime.of(2026, 3, 10, 10, 5))
.endTime(LocalDateTime.of(2026, 3, 10, 10, 30))
.responseSeconds(300)
.completionSeconds(1500)
.build();
}
// ==================== supports() 测试 ====================
@Test
void testSupports_Security_ReturnsTrue() {
assertTrue(handler.supports("SECURITY"));
}
@Test
void testSupports_Clean_ReturnsFalse() {
assertFalse(handler.supports("CLEAN"));
}
@Test
void testSupports_Null_ReturnsFalse() {
assertFalse(handler.supports(null));
}
@Test
void testSupports_Empty_ReturnsFalse() {
assertFalse(handler.supports(""));
}
// ==================== enrichWithExtInfo() 测试 ====================
@Test
void testEnrichWithExtInfo_HasExt_FieldsPopulated() {
when(securityExtMapper.selectByOpsOrderId(TEST_ORDER_ID)).thenReturn(testSecurityExt);
OrderSummaryVO vo = new OrderSummaryVO();
handler.enrichWithExtInfo(vo, TEST_ORDER_ID);
// 验证 extInfo 非空
assertNotNull(vo.getExtInfo());
Map<String, Object> extInfo = vo.getExtInfo();
// 验证各字段映射
assertEquals("ALM20260310001", extInfo.get("alarmId"));
assertEquals("intrusion", extInfo.get("alarmType"));
assertEquals("CAM_001", extInfo.get("cameraId"));
assertEquals("https://oss.example.com/snapshot.jpg", extInfo.get("imageUrl"));
assertEquals("张安保", extInfo.get("assignedUserName"));
assertEquals(testSecurityExt.getConfirmedTime(), extInfo.get("confirmedTime"));
assertEquals(testSecurityExt.getCompletedTime(), extInfo.get("completedTime"));
assertEquals("已排查,系误报", extInfo.get("result"));
}
@Test
void testEnrichWithExtInfo_NoExt_ExtInfoNotSet() {
when(securityExtMapper.selectByOpsOrderId(TEST_ORDER_ID)).thenReturn(null);
OrderSummaryVO vo = new OrderSummaryVO();
handler.enrichWithExtInfo(vo, TEST_ORDER_ID);
// 无扩展记录时extInfo 未被填充
assertTrue(vo.getExtInfo() == null || vo.getExtInfo().isEmpty());
}
// ==================== buildDetailVO() 测试 ====================
@Test
void testBuildDetailVO_HasExt_FullVO() {
when(securityExtMapper.selectByOpsOrderId(TEST_ORDER_ID)).thenReturn(testSecurityExt);
OrderDetailVO vo = handler.buildDetailVO(testOrder);
// 验证主表字段
assertNotNull(vo);
assertEquals(TEST_ORDER_ID, vo.getId());
assertEquals("SECURITY-20260310-0001", vo.getOrderCode());
assertEquals("SECURITY", vo.getOrderType());
assertEquals("ALARM", vo.getSourceType());
assertEquals("A栋3层入侵告警", vo.getTitle());
assertEquals(PriorityEnum.P1.getPriority(), vo.getPriority());
assertEquals(WorkOrderStatusEnum.COMPLETED.getStatus(), vo.getStatus());
assertEquals(100L, vo.getAreaId());
assertEquals("A栋3层东侧走廊", vo.getLocation());
// 验证扩展字段
assertNotNull(vo.getExtInfo());
Map<String, Object> extInfo = vo.getExtInfo();
assertEquals("ALM20260310001", extInfo.get("alarmId"));
assertEquals("intrusion", extInfo.get("alarmType"));
assertEquals("CAM_001", extInfo.get("cameraId"));
assertEquals("ROI_001", extInfo.get("roiId"));
assertEquals("https://oss.example.com/snapshot.jpg", extInfo.get("imageUrl"));
assertEquals(2001L, extInfo.get("assignedUserId"));
assertEquals("张安保", extInfo.get("assignedUserName"));
assertEquals(301L, extInfo.get("assignedTeamId"));
assertEquals("已排查,系误报", extInfo.get("result"));
assertEquals("[\"https://oss/r1.jpg\"]", extInfo.get("resultImgUrls"));
assertEquals(testSecurityExt.getDispatchedTime(), extInfo.get("dispatchedTime"));
assertEquals(testSecurityExt.getConfirmedTime(), extInfo.get("confirmedTime"));
assertEquals(testSecurityExt.getCompletedTime(), extInfo.get("completedTime"));
}
@Test
void testBuildDetailVO_NoExt_ExtInfoNull() {
when(securityExtMapper.selectByOpsOrderId(TEST_ORDER_ID)).thenReturn(null);
OrderDetailVO vo = handler.buildDetailVO(testOrder);
// 主表字段正常
assertNotNull(vo);
assertEquals(TEST_ORDER_ID, vo.getId());
assertEquals("SECURITY", vo.getOrderType());
// 无扩展记录时extInfo 未被填充
assertTrue(vo.getExtInfo() == null || vo.getExtInfo().isEmpty());
}
}

View File

@@ -0,0 +1,437 @@
package com.viewsh.module.ops.security.service.securityorder;
import com.viewsh.framework.common.exception.ServiceException;
import com.viewsh.module.ops.core.event.OrderCreatedEvent;
import com.viewsh.module.ops.core.event.OrderEventPublisher;
import com.viewsh.module.ops.dal.dataobject.workorder.OpsOrderDO;
import com.viewsh.module.ops.dal.mysql.workorder.OpsOrderMapper;
import com.viewsh.module.ops.enums.OperatorTypeEnum;
import com.viewsh.module.ops.enums.PriorityEnum;
import com.viewsh.module.ops.enums.SourceTypeEnum;
import com.viewsh.module.ops.enums.WorkOrderStatusEnum;
import com.viewsh.module.ops.infrastructure.code.OrderCodeGenerator;
import com.viewsh.module.ops.infrastructure.id.OrderIdGenerator;
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.fsm.OrderStateMachine;
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 java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
/**
* 安保工单服务单元测试
*
* @author lzh
*/
@ExtendWith(MockitoExtension.class)
public class SecurityOrderServiceTest {
@InjectMocks
private SecurityOrderServiceImpl securityOrderService;
@Mock
private OpsOrderMapper opsOrderMapper;
@Mock
private OpsOrderSecurityExtMapper securityExtMapper;
@Mock
private OrderIdGenerator orderIdGenerator;
@Mock
private OrderCodeGenerator orderCodeGenerator;
@Mock
private OrderEventPublisher orderEventPublisher;
@Mock
private OrderStateMachine orderStateMachine;
// 模拟数据库
private Map<Long, OpsOrderDO> orderDB;
private static final Long TEST_ORDER_ID = 10001L;
private static final String TEST_ORDER_CODE = "SECURITY-20260310-0001";
@BeforeEach
void setUp() {
orderDB = new HashMap<>();
// 配置 ID/编号 生成器
lenient().when(orderIdGenerator.generate()).thenReturn(TEST_ORDER_ID);
lenient().when(orderCodeGenerator.generate("SECURITY")).thenReturn(TEST_ORDER_CODE);
// 配置 Mapper 模拟
lenient().when(opsOrderMapper.insert(any(OpsOrderDO.class))).thenAnswer(i -> {
OpsOrderDO order = i.getArgument(0);
orderDB.put(order.getId(), order);
return 1;
});
lenient().when(opsOrderMapper.selectById(anyLong())).thenAnswer(i -> orderDB.get(i.getArgument(0)));
lenient().when(opsOrderMapper.updateById(any(OpsOrderDO.class))).thenReturn(1);
lenient().when(securityExtMapper.insert(any(OpsOrderSecurityExtDO.class))).thenReturn(1);
lenient().when(securityExtMapper.insertOrUpdateSelective(any(OpsOrderSecurityExtDO.class))).thenReturn(1);
}
// ==================== 创建工单测试 ====================
@Test
void testCreateSecurityOrder_AlarmSource_Success() {
// 准备:告警来源请求
SecurityOrderCreateReqDTO req = new SecurityOrderCreateReqDTO();
req.setTitle("A栋3层入侵告警");
req.setDescription("摄像头检测到异常人员");
req.setPriority(PriorityEnum.P1.getPriority());
req.setAreaId(100L);
req.setLocation("A栋3层东侧走廊");
req.setAlarmId("ALM20260310001");
req.setAlarmType("intrusion");
req.setCameraId("CAM_001");
req.setRoiId("ROI_001");
req.setImageUrl("https://oss.example.com/alarm/snapshot.jpg");
// 执行
Long orderId = securityOrderService.createSecurityOrder(req);
// 验证返回值
assertEquals(TEST_ORDER_ID, orderId);
// 验证主表写入
ArgumentCaptor<OpsOrderDO> orderCaptor = ArgumentCaptor.forClass(OpsOrderDO.class);
verify(opsOrderMapper).insert(orderCaptor.capture());
OpsOrderDO savedOrder = orderCaptor.getValue();
assertEquals(TEST_ORDER_ID, savedOrder.getId());
assertEquals(TEST_ORDER_CODE, savedOrder.getOrderCode());
assertEquals("SECURITY", savedOrder.getOrderType());
assertEquals(SourceTypeEnum.ALARM.getType(), savedOrder.getSourceType());
assertEquals("A栋3层入侵告警", savedOrder.getTitle());
assertEquals(PriorityEnum.P1.getPriority(), savedOrder.getPriority());
assertEquals(WorkOrderStatusEnum.PENDING.getStatus(), savedOrder.getStatus());
assertEquals(100L, savedOrder.getAreaId());
// 验证扩展表写入
ArgumentCaptor<OpsOrderSecurityExtDO> extCaptor = ArgumentCaptor.forClass(OpsOrderSecurityExtDO.class);
verify(securityExtMapper).insert(extCaptor.capture());
OpsOrderSecurityExtDO savedExt = extCaptor.getValue();
assertEquals(TEST_ORDER_ID, savedExt.getOpsOrderId());
assertEquals("ALM20260310001", savedExt.getAlarmId());
assertEquals("intrusion", savedExt.getAlarmType());
assertEquals("CAM_001", savedExt.getCameraId());
assertEquals("ROI_001", savedExt.getRoiId());
assertEquals("https://oss.example.com/alarm/snapshot.jpg", savedExt.getImageUrl());
// 验证事件发布
ArgumentCaptor<OrderCreatedEvent> eventCaptor = ArgumentCaptor.forClass(OrderCreatedEvent.class);
verify(orderEventPublisher).publishOrderCreated(eventCaptor.capture());
OrderCreatedEvent event = eventCaptor.getValue();
assertEquals(TEST_ORDER_ID, event.getOrderId());
assertEquals("SECURITY", event.getOrderType());
assertEquals(TEST_ORDER_CODE, event.getOrderCode());
assertEquals(100L, event.getAreaId());
}
@Test
void testCreateSecurityOrder_ManualSource_Success() {
// 准备手动创建无告警ID
SecurityOrderCreateReqDTO req = new SecurityOrderCreateReqDTO();
req.setTitle("保安巡查发现异常");
req.setAreaId(200L);
// 执行
Long orderId = securityOrderService.createSecurityOrder(req);
// 验证来源类型为 MANUAL
ArgumentCaptor<OpsOrderDO> orderCaptor = ArgumentCaptor.forClass(OpsOrderDO.class);
verify(opsOrderMapper).insert(orderCaptor.capture());
assertEquals(SourceTypeEnum.MANUAL.getType(), orderCaptor.getValue().getSourceType());
}
@Test
void testCreateSecurityOrder_ExplicitSourceType_Success() {
// 准备:显式指定来源类型
SecurityOrderCreateReqDTO req = new SecurityOrderCreateReqDTO();
req.setTitle("手动创建");
req.setAreaId(200L);
req.setAlarmId("ALM001"); // 有告警ID
req.setSourceType("CUSTOM_SOURCE"); // 但显式指定了sourceType
// 执行
securityOrderService.createSecurityOrder(req);
// 验证:显式指定的 sourceType 优先于自动推断
ArgumentCaptor<OpsOrderDO> orderCaptor = ArgumentCaptor.forClass(OpsOrderDO.class);
verify(opsOrderMapper).insert(orderCaptor.capture());
assertEquals("CUSTOM_SOURCE", orderCaptor.getValue().getSourceType());
}
@Test
void testCreateSecurityOrder_DefaultPriority_P2() {
// 准备:不设置优先级
SecurityOrderCreateReqDTO req = new SecurityOrderCreateReqDTO();
req.setTitle("默认优先级工单");
req.setAreaId(100L);
// 执行
securityOrderService.createSecurityOrder(req);
// 验证默认 P2
ArgumentCaptor<OpsOrderDO> orderCaptor = ArgumentCaptor.forClass(OpsOrderDO.class);
verify(opsOrderMapper).insert(orderCaptor.capture());
assertEquals(PriorityEnum.P2.getPriority(), orderCaptor.getValue().getPriority());
}
// ==================== 确认工单测试 ====================
@Test
void testConfirmOrder_Success() {
// 准备:已存在的 SECURITY 工单
OpsOrderDO order = buildSecurityOrder(TEST_ORDER_ID, WorkOrderStatusEnum.DISPATCHED);
orderDB.put(TEST_ORDER_ID, order);
Long userId = 2001L;
// 执行
securityOrderService.confirmOrder(TEST_ORDER_ID, userId);
// 验证状态机调用
verify(orderStateMachine).transition(
eq(order),
eq(WorkOrderStatusEnum.CONFIRMED),
eq(OperatorTypeEnum.SECURITY_GUARD),
eq(userId),
eq("安保人员确认接单")
);
// 验证不再直接写扩展表时间(由 EventListener 统一处理)
verify(securityExtMapper, never()).insertOrUpdateSelective(any());
}
@Test
void testConfirmOrder_OrderNotFound_ThrowsException() {
// 执行 + 验证:工单不存在抛异常
ServiceException exception = assertThrows(ServiceException.class,
() -> securityOrderService.confirmOrder(999L, 2001L));
assertTrue(exception.getMessage().contains("工单不存在"));
}
@Test
void testConfirmOrder_WrongOrderType_ThrowsException() {
// 准备CLEAN 类型工单
OpsOrderDO cleanOrder = OpsOrderDO.builder()
.id(TEST_ORDER_ID)
.orderType("CLEAN")
.status(WorkOrderStatusEnum.DISPATCHED.getStatus())
.build();
orderDB.put(TEST_ORDER_ID, cleanOrder);
// 执行 + 验证:类型不匹配抛异常
ServiceException exception = assertThrows(ServiceException.class,
() -> securityOrderService.confirmOrder(TEST_ORDER_ID, 2001L));
assertTrue(exception.getMessage().contains("工单类型不匹配"));
// 验证状态机未被调用
verify(orderStateMachine, never()).transition(
any(), any(), any(), anyLong(), anyString());
}
// ==================== 自动完单测试 ====================
@Test
void testAutoCompleteOrder_WithRemark_Success() {
// 准备
OpsOrderDO order = buildSecurityOrder(TEST_ORDER_ID, WorkOrderStatusEnum.ARRIVED);
orderDB.put(TEST_ORDER_ID, order);
// 执行
securityOrderService.autoCompleteOrder(TEST_ORDER_ID, "告警自动解除");
// 验证状态机调用
verify(orderStateMachine).transition(
eq(order),
eq(WorkOrderStatusEnum.COMPLETED),
eq(OperatorTypeEnum.SYSTEM),
isNull(),
eq("告警自动解除")
);
// 验证主表 endTime 更新
ArgumentCaptor<OpsOrderDO> updateCaptor = ArgumentCaptor.forClass(OpsOrderDO.class);
verify(opsOrderMapper).updateById(updateCaptor.capture());
OpsOrderDO updated = updateCaptor.getValue();
assertEquals(TEST_ORDER_ID, updated.getId());
assertNotNull(updated.getEndTime());
// 验证不再直接写扩展表时间
verify(securityExtMapper, never()).insertOrUpdateSelective(any());
}
@Test
void testAutoCompleteOrder_WithoutRemark_DefaultReason() {
// 准备
OpsOrderDO order = buildSecurityOrder(TEST_ORDER_ID, WorkOrderStatusEnum.ARRIVED);
orderDB.put(TEST_ORDER_ID, order);
// 执行remark 为空
securityOrderService.autoCompleteOrder(TEST_ORDER_ID, null);
// 验证使用默认备注
verify(orderStateMachine).transition(
any(), eq(WorkOrderStatusEnum.COMPLETED),
eq(OperatorTypeEnum.SYSTEM), isNull(),
eq("系统自动完单")
);
}
@Test
void testAutoCompleteOrder_BlankRemark_DefaultReason() {
// 准备
OpsOrderDO order = buildSecurityOrder(TEST_ORDER_ID, WorkOrderStatusEnum.ARRIVED);
orderDB.put(TEST_ORDER_ID, order);
// 执行remark 为空白字符串
securityOrderService.autoCompleteOrder(TEST_ORDER_ID, " ");
// 验证使用默认备注
verify(orderStateMachine).transition(
any(), eq(WorkOrderStatusEnum.COMPLETED),
eq(OperatorTypeEnum.SYSTEM), isNull(),
eq("系统自动完单")
);
}
@Test
void testAutoCompleteOrder_OrderNotFound_ThrowsException() {
assertThrows(ServiceException.class,
() -> securityOrderService.autoCompleteOrder(999L, "test"));
}
// ==================== 人工完单测试 ====================
@Test
void testManualCompleteOrder_WithImages_Success() {
// 准备
OpsOrderDO order = buildSecurityOrder(TEST_ORDER_ID, WorkOrderStatusEnum.ARRIVED);
orderDB.put(TEST_ORDER_ID, order);
// 准备完单请求包含result和图片
SecurityOrderCompleteReqDTO req = new SecurityOrderCompleteReqDTO();
req.setOrderId(TEST_ORDER_ID);
req.setResult("已到现场排查,系误报");
req.setResultImgUrls(Arrays.asList("https://oss/result1.jpg", "https://oss/result2.jpg"));
req.setOperatorId(2001L);
// 执行
securityOrderService.manualCompleteOrder(req);
// 验证状态机调用
verify(orderStateMachine).transition(
eq(order),
eq(WorkOrderStatusEnum.COMPLETED),
eq(OperatorTypeEnum.SECURITY_GUARD),
eq(2001L),
eq("安保人员提交处理结果")
);
// 验证扩展表:写入 result + resultImgUrls不含 completedTime
ArgumentCaptor<OpsOrderSecurityExtDO> extCaptor = ArgumentCaptor.forClass(OpsOrderSecurityExtDO.class);
verify(securityExtMapper).insertOrUpdateSelective(extCaptor.capture());
OpsOrderSecurityExtDO extUpdate = extCaptor.getValue();
assertEquals(TEST_ORDER_ID, extUpdate.getOpsOrderId());
assertEquals("已到现场排查,系误报", extUpdate.getResult());
assertNotNull(extUpdate.getResultImgUrls());
assertTrue(extUpdate.getResultImgUrls().contains("result1.jpg"));
assertNull(extUpdate.getCompletedTime()); // 时间由 EventListener 写入
// 验证主表 endTime 更新
ArgumentCaptor<OpsOrderDO> orderUpdateCaptor = ArgumentCaptor.forClass(OpsOrderDO.class);
verify(opsOrderMapper).updateById(orderUpdateCaptor.capture());
assertNotNull(orderUpdateCaptor.getValue().getEndTime());
}
@Test
void testManualCompleteOrder_WithoutImages_Success() {
// 准备
OpsOrderDO order = buildSecurityOrder(TEST_ORDER_ID, WorkOrderStatusEnum.ARRIVED);
orderDB.put(TEST_ORDER_ID, order);
SecurityOrderCompleteReqDTO req = new SecurityOrderCompleteReqDTO();
req.setOrderId(TEST_ORDER_ID);
req.setResult("已处理完毕");
req.setResultImgUrls(null); // 无图片
req.setOperatorId(2001L);
// 执行
securityOrderService.manualCompleteOrder(req);
// 验证扩展表result 有值resultImgUrls 为 null
ArgumentCaptor<OpsOrderSecurityExtDO> extCaptor = ArgumentCaptor.forClass(OpsOrderSecurityExtDO.class);
verify(securityExtMapper).insertOrUpdateSelective(extCaptor.capture());
assertEquals("已处理完毕", extCaptor.getValue().getResult());
assertNull(extCaptor.getValue().getResultImgUrls());
}
@Test
void testManualCompleteOrder_EmptyImagesList_NoJsonWrite() {
// 准备
OpsOrderDO order = buildSecurityOrder(TEST_ORDER_ID, WorkOrderStatusEnum.ARRIVED);
orderDB.put(TEST_ORDER_ID, order);
SecurityOrderCompleteReqDTO req = new SecurityOrderCompleteReqDTO();
req.setOrderId(TEST_ORDER_ID);
req.setResult("已处理");
req.setResultImgUrls(Arrays.asList()); // 空列表
req.setOperatorId(2001L);
// 执行
securityOrderService.manualCompleteOrder(req);
// 验证:空列表不写入 resultImgUrls
ArgumentCaptor<OpsOrderSecurityExtDO> extCaptor = ArgumentCaptor.forClass(OpsOrderSecurityExtDO.class);
verify(securityExtMapper).insertOrUpdateSelective(extCaptor.capture());
assertNull(extCaptor.getValue().getResultImgUrls());
}
// ==================== 查询测试 ====================
@Test
void testGetSecurityExt_Exists() {
OpsOrderSecurityExtDO ext = OpsOrderSecurityExtDO.builder()
.id(1L).opsOrderId(TEST_ORDER_ID).alarmId("ALM001").build();
when(securityExtMapper.selectByOpsOrderId(TEST_ORDER_ID)).thenReturn(ext);
OpsOrderSecurityExtDO result = securityOrderService.getSecurityExt(TEST_ORDER_ID);
assertNotNull(result);
assertEquals("ALM001", result.getAlarmId());
}
@Test
void testGetSecurityExt_NotExists() {
when(securityExtMapper.selectByOpsOrderId(999L)).thenReturn(null);
OpsOrderSecurityExtDO result = securityOrderService.getSecurityExt(999L);
assertNull(result);
}
// ==================== 辅助方法 ====================
private OpsOrderDO buildSecurityOrder(Long orderId, WorkOrderStatusEnum status) {
return OpsOrderDO.builder()
.id(orderId)
.orderCode(TEST_ORDER_CODE)
.orderType("SECURITY")
.sourceType(SourceTypeEnum.ALARM.getType())
.title("安保测试工单")
.priority(PriorityEnum.P1.getPriority())
.status(status.getStatus())
.areaId(100L)
.location("A栋3层")
.build();
}
}