feat(ops): 新增 VSP 企微通知网关基础设施
- VspNotifyClient/Impl: HTTP 客户端,支持网络异常重试 + 线性退避 - VspNotifyProperties/Config: 配置属性与 RestTemplate Bean(JdkClientHttpRequestFactory) - VspSendCardReqDTO/VspSyncStatusReqDTO/VspResponseDTO: 请求响应 DTO - WechatUserIdResolver: 企微 userId 查询工具,通过 SocialUserApi 查询,供各业务线复用 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,172 @@
|
||||
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;
|
||||
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.security.dal.dataobject.workorder.OpsOrderSecurityExtDO;
|
||||
import com.viewsh.module.ops.security.dal.mysql.workorder.OpsOrderSecurityExtMapper;
|
||||
import com.viewsh.module.ops.service.area.OpsBusAreaService;
|
||||
import com.viewsh.module.ops.dal.dataobject.vo.area.OpsBusAreaRespVO;
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.transaction.event.TransactionPhase;
|
||||
import org.springframework.transaction.event.TransactionalEventListener;
|
||||
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 安保工单 VSP 企微通知监听器
|
||||
* <p>
|
||||
* 职责单一:监听工单事件,调用 vsp-service 发送企微卡片和同步状态。
|
||||
* 与 {@link SecurityOrderEventListener} 分离,各自独立监听事件。
|
||||
*
|
||||
* @author lzh
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class SecurityOrderVspNotifyListener {
|
||||
|
||||
private static final String ORDER_TYPE_SECURITY = WorkOrderTypeEnum.SECURITY.getType();
|
||||
private static final DateTimeFormatter EVENT_TIME_FMT = DateTimeFormatter.ofPattern("MM-dd HH:mm");
|
||||
|
||||
@Resource
|
||||
private VspNotifyClient vspNotifyClient;
|
||||
|
||||
@Resource
|
||||
private WechatUserIdResolver wechatUserIdResolver;
|
||||
|
||||
@Resource
|
||||
private OpsBusAreaService opsBusAreaService;
|
||||
|
||||
@Resource
|
||||
private OpsOrderSecurityExtMapper securityExtMapper;
|
||||
|
||||
// ==================== 状态变更 → 同步 vsp ====================
|
||||
|
||||
/**
|
||||
* 工单状态变更后异步同步到 vsp-service
|
||||
*/
|
||||
@Async("ops-task-executor")
|
||||
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
|
||||
public void onOrderStateChanged(OrderStateChangedEvent event) {
|
||||
if (!ORDER_TYPE_SECURITY.equals(event.getOrderType())) {
|
||||
return;
|
||||
}
|
||||
syncStatus(event);
|
||||
}
|
||||
|
||||
// ==================== 核心方法 ====================
|
||||
|
||||
/**
|
||||
* 派单成功后发送企微工单卡片
|
||||
* <p>
|
||||
* 由 {@link SecurityOrderEventListener#onOrderCreated} 在派单成功后调用
|
||||
*/
|
||||
public void sendCard(OrderCreatedEvent event, DispatchResult result) {
|
||||
try {
|
||||
OpsOrderSecurityExtDO ext = securityExtMapper.selectByOpsOrderId(event.getOrderId());
|
||||
if (ext == null || ext.getAlarmId() == null) {
|
||||
return; // 非告警来源工单不发卡片
|
||||
}
|
||||
|
||||
String wechatUserId = wechatUserIdResolver.resolve(result.getAssigneeId());
|
||||
if (wechatUserId == null) {
|
||||
log.warn("[sendCard] 未找到企微userId, assigneeId={}, orderId={}",
|
||||
result.getAssigneeId(), event.getOrderId());
|
||||
return;
|
||||
}
|
||||
|
||||
String areaName = resolveAreaName(event.getAreaId());
|
||||
|
||||
VspSendCardReqDTO req = VspSendCardReqDTO.builder()
|
||||
.alarmId(ext.getAlarmId())
|
||||
.orderId(String.valueOf(event.getOrderId()))
|
||||
.userIds(List.of(wechatUserId))
|
||||
.title(event.getTitle())
|
||||
.areaName(areaName)
|
||||
.cameraName(ext.getCameraName())
|
||||
.eventTime(event.getCreateTime() != null
|
||||
? event.getCreateTime().format(EVENT_TIME_FMT)
|
||||
: null)
|
||||
.level(event.getPriority())
|
||||
.snapshotUrl(ext.getImageUrl())
|
||||
.build();
|
||||
|
||||
vspNotifyClient.sendCard(req);
|
||||
} catch (Exception e) {
|
||||
log.error("[sendCard] 发送企微卡片异常, orderId={}", event.getOrderId(), e);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 内部方法 ====================
|
||||
|
||||
private void syncStatus(OrderStateChangedEvent event) {
|
||||
try {
|
||||
OpsOrderSecurityExtDO ext = securityExtMapper.selectByOpsOrderId(event.getOrderId());
|
||||
if (ext == null || ext.getAlarmId() == null) {
|
||||
return; // 非告警来源工单不同步
|
||||
}
|
||||
|
||||
String status = mapToVspStatus(event.getNewStatus(), ext.getFalseAlarm());
|
||||
if (status == null) {
|
||||
return; // PENDING/QUEUED 不通知
|
||||
}
|
||||
|
||||
String operator = null;
|
||||
if (ext.getAssignedUserId() != null) {
|
||||
operator = wechatUserIdResolver.resolve(ext.getAssignedUserId());
|
||||
}
|
||||
|
||||
VspSyncStatusReqDTO req = VspSyncStatusReqDTO.builder()
|
||||
.alarmId(ext.getAlarmId())
|
||||
.orderId(String.valueOf(event.getOrderId()))
|
||||
.status(status)
|
||||
.operator(operator)
|
||||
.remark(event.getRemark())
|
||||
.build();
|
||||
|
||||
vspNotifyClient.syncStatus(req);
|
||||
} catch (Exception e) {
|
||||
log.error("[syncStatus] 同步状态异常, orderId={}", event.getOrderId(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 内部工单状态 → vsp 状态字符串
|
||||
*/
|
||||
private String mapToVspStatus(WorkOrderStatusEnum newStatus, Boolean falseAlarm) {
|
||||
if (newStatus == null) return null;
|
||||
return switch (newStatus) {
|
||||
case DISPATCHED -> "dispatched";
|
||||
case CONFIRMED -> "confirmed";
|
||||
case ARRIVED -> "arrived";
|
||||
case COMPLETED -> Boolean.TRUE.equals(falseAlarm) ? "false_alarm" : "completed";
|
||||
case CANCELLED -> "auto_resolved";
|
||||
case PAUSED -> "paused";
|
||||
default -> null;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询区域名称
|
||||
*/
|
||||
private String resolveAreaName(Long areaId) {
|
||||
if (areaId == null) return null;
|
||||
try {
|
||||
OpsBusAreaRespVO area = opsBusAreaService.getArea(areaId);
|
||||
return area != null ? area.getAreaName() : null;
|
||||
} catch (Exception e) {
|
||||
log.warn("[resolveAreaName] 查询区域失败, areaId={}", areaId);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.viewsh.module.ops.security.integration.vsp;
|
||||
|
||||
import com.viewsh.module.ops.security.integration.vsp.dto.VspSendCardReqDTO;
|
||||
import com.viewsh.module.ops.security.integration.vsp.dto.VspSyncStatusReqDTO;
|
||||
|
||||
public interface VspNotifyClient {
|
||||
|
||||
/**
|
||||
* 发送企微工单卡片
|
||||
*
|
||||
* @param req 卡片内容
|
||||
*/
|
||||
void sendCard(VspSendCardReqDTO req);
|
||||
|
||||
/**
|
||||
* 同步工单状态到 vsp-service
|
||||
*
|
||||
* @param req 状态信息
|
||||
*/
|
||||
void syncStatus(VspSyncStatusReqDTO req);
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package com.viewsh.module.ops.security.integration.vsp;
|
||||
|
||||
import com.viewsh.module.ops.security.integration.vsp.config.VspNotifyProperties;
|
||||
import com.viewsh.module.ops.security.integration.vsp.dto.VspResponseDTO;
|
||||
import com.viewsh.module.ops.security.integration.vsp.dto.VspSendCardReqDTO;
|
||||
import com.viewsh.module.ops.security.integration.vsp.dto.VspSyncStatusReqDTO;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.client.RestClientException;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
@Slf4j
|
||||
@Component
|
||||
public class VspNotifyClientImpl implements VspNotifyClient {
|
||||
|
||||
private final RestTemplate restTemplate;
|
||||
private final VspNotifyProperties properties;
|
||||
|
||||
public VspNotifyClientImpl(@Qualifier("vspRestTemplate") RestTemplate restTemplate,
|
||||
VspNotifyProperties properties) {
|
||||
this.restTemplate = restTemplate;
|
||||
this.properties = properties;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendCard(VspSendCardReqDTO req) {
|
||||
if (!properties.isEnabled()) return;
|
||||
String url = properties.getBaseUrl() + "/api/wechat/notify/send-card";
|
||||
executeWithRetry(url, req, "sendCard", req.getOrderId(), req.getAlarmId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void syncStatus(VspSyncStatusReqDTO req) {
|
||||
if (!properties.isEnabled()) return;
|
||||
String url = properties.getBaseUrl() + "/api/wechat/notify/sync-status";
|
||||
executeWithRetry(url, req, "syncStatus", req.getOrderId(), req.getAlarmId());
|
||||
}
|
||||
|
||||
private void executeWithRetry(String url, Object req,
|
||||
String action, String orderId, String alarmId) {
|
||||
for (int i = 0; i <= properties.getMaxRetry(); i++) {
|
||||
try {
|
||||
VspResponseDTO resp = restTemplate.postForObject(url, req, VspResponseDTO.class);
|
||||
if (resp != null && resp.isSuccess()) {
|
||||
log.info("[{}] vsp通知成功, orderId={}, alarmId={}", action, orderId, alarmId);
|
||||
return;
|
||||
}
|
||||
// 业务失败不重试
|
||||
log.warn("[{}] vsp返回失败: code={}, msg={}, orderId={}",
|
||||
action, resp != null ? resp.getCode() : null,
|
||||
resp != null ? resp.getMsg() : null, orderId);
|
||||
return;
|
||||
} catch (RestClientException e) {
|
||||
if (i < properties.getMaxRetry()) {
|
||||
log.warn("[{}] 第{}次调用失败, orderId={}, 将重试",
|
||||
action, i + 1, orderId);
|
||||
} else {
|
||||
log.error("[{}] 调用vsp失败, 重试耗尽, orderId={}, alarmId={}",
|
||||
action, orderId, alarmId, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.viewsh.module.ops.security.integration.vsp.config;
|
||||
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.http.client.SimpleClientHttpRequestFactory;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
@Configuration
|
||||
@EnableConfigurationProperties(VspNotifyProperties.class)
|
||||
public class VspNotifyConfig {
|
||||
|
||||
@Bean("vspRestTemplate")
|
||||
public RestTemplate vspRestTemplate(VspNotifyProperties properties) {
|
||||
SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
|
||||
factory.setConnectTimeout(properties.getConnectTimeout());
|
||||
factory.setReadTimeout(properties.getReadTimeout());
|
||||
RestTemplate restTemplate = new RestTemplate(factory);
|
||||
return restTemplate;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.viewsh.module.ops.security.integration.vsp.config;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
|
||||
@Data
|
||||
@ConfigurationProperties(prefix = "viewsh.ops.vsp-notify")
|
||||
public class VspNotifyProperties {
|
||||
/** 是否启用 vsp 通知 */
|
||||
private boolean enabled = true;
|
||||
/** vsp-service 基础 URL */
|
||||
private String baseUrl = "http://124.221.55.225:8000";
|
||||
/** 连接超时(ms) */
|
||||
private int connectTimeout = 5000;
|
||||
/** 读取超时(ms) */
|
||||
private int readTimeout = 10000;
|
||||
/** 最大重试次数(网络异常时) */
|
||||
private int maxRetry = 2;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.viewsh.module.ops.security.integration.vsp.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class VspResponseDTO {
|
||||
private Integer code;
|
||||
private String msg;
|
||||
|
||||
public boolean isSuccess() {
|
||||
return code != null && code == 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.viewsh.module.ops.security.integration.vsp.dto;
|
||||
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
public class VspSendCardReqDTO {
|
||||
/** 关联告警ID */
|
||||
private String alarmId;
|
||||
/** 工单ID */
|
||||
private String orderId;
|
||||
/** 接收人企微 userId 列表 */
|
||||
private List<String> userIds;
|
||||
/** 卡片标题(告警类型) */
|
||||
private String title;
|
||||
/** 区域名称 */
|
||||
private String areaName;
|
||||
/** 摄像头名称 */
|
||||
private String cameraName;
|
||||
/** 告警时间,格式 MM-dd HH:mm */
|
||||
private String eventTime;
|
||||
/** 告警级别(0紧急 1重要 2普通 3轻微) */
|
||||
private Integer level;
|
||||
/** 告警截图URL */
|
||||
private String snapshotUrl;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.viewsh.module.ops.security.integration.vsp.dto;
|
||||
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
public class VspSyncStatusReqDTO {
|
||||
/** 关联告警ID */
|
||||
private String alarmId;
|
||||
/** 工单ID */
|
||||
private String orderId;
|
||||
/** 工单状态 */
|
||||
private String status;
|
||||
/** 操作人企微 userId */
|
||||
private String operator;
|
||||
/** 备注 */
|
||||
private String remark;
|
||||
}
|
||||
@@ -0,0 +1,284 @@
|
||||
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.DispatchResult;
|
||||
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.infrastructure.log.recorder.EventLogRecorder;
|
||||
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.security.integration.vsp.VspNotifyClient;
|
||||
import com.viewsh.module.ops.security.integration.vsp.dto.VspSendCardReqDTO;
|
||||
import com.viewsh.module.ops.security.integration.vsp.dto.VspSyncStatusReqDTO;
|
||||
import com.viewsh.module.ops.service.area.OpsBusAreaService;
|
||||
import com.viewsh.module.ops.controller.admin.area.vo.OpsBusAreaRespVO;
|
||||
import com.viewsh.module.system.api.social.SocialUserApi;
|
||||
import com.viewsh.module.system.api.social.dto.SocialUserRespDTO;
|
||||
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.time.LocalDateTime;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
/**
|
||||
* 安保工单事件监听器 - VSP 企微通知集成测试
|
||||
*
|
||||
* @author lzh
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
public class SecurityOrderEventListenerVspTest {
|
||||
|
||||
@InjectMocks
|
||||
private SecurityOrderEventListener listener;
|
||||
|
||||
@Mock
|
||||
private VspNotifyClient vspNotifyClient;
|
||||
|
||||
@Mock
|
||||
private SocialUserApi socialUserApi;
|
||||
|
||||
@Mock
|
||||
private OpsBusAreaService opsBusAreaService;
|
||||
|
||||
@Mock
|
||||
private OpsOrderSecurityExtMapper securityExtMapper;
|
||||
|
||||
@Mock
|
||||
private DispatchEngine dispatchEngine;
|
||||
|
||||
@Mock
|
||||
private EventLogRecorder eventLogRecorder;
|
||||
|
||||
private static final Long TEST_ORDER_ID = 10001L;
|
||||
private static final String TEST_ORDER_CODE = "SECURITY-20260324-0001";
|
||||
private static final Long TEST_ASSIGNEE_ID = 2001L;
|
||||
private static final String TEST_WECHAT_ID = "test_wechat_id";
|
||||
private static final String TEST_ALARM_ID = "alarm-001";
|
||||
|
||||
// ==================== onOrderCreated VSP 通知测试 ====================
|
||||
|
||||
@Test
|
||||
void onOrderCreated_shouldSendCard_whenDispatchSuccessAndAlarmSource() {
|
||||
// 准备事件
|
||||
OrderCreatedEvent event = OrderCreatedEvent.builder()
|
||||
.orderId(TEST_ORDER_ID)
|
||||
.orderType("SECURITY")
|
||||
.orderCode(TEST_ORDER_CODE)
|
||||
.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);
|
||||
|
||||
// 告警来源工单扩展记录
|
||||
OpsOrderSecurityExtDO ext = OpsOrderSecurityExtDO.builder()
|
||||
.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);
|
||||
|
||||
// 企微 userId 查询
|
||||
SocialUserRespDTO socialUser = new SocialUserRespDTO();
|
||||
socialUser.setOpenid(TEST_WECHAT_ID);
|
||||
when(socialUserApi.getSocialUserByUserId(anyInt(), eq(TEST_ASSIGNEE_ID), anyInt()))
|
||||
.thenReturn(CommonResult.success(socialUser));
|
||||
|
||||
// 区域名称
|
||||
OpsBusAreaRespVO area = new OpsBusAreaRespVO();
|
||||
area.setAreaName("A栋大堂");
|
||||
when(opsBusAreaService.getArea(100L)).thenReturn(area);
|
||||
|
||||
// 执行
|
||||
listener.onOrderCreated(event);
|
||||
|
||||
// 验证发送了企微卡片
|
||||
ArgumentCaptor<VspSendCardReqDTO> captor = ArgumentCaptor.forClass(VspSendCardReqDTO.class);
|
||||
verify(vspNotifyClient).sendCard(captor.capture());
|
||||
VspSendCardReqDTO req = captor.getValue();
|
||||
assertEquals(TEST_ALARM_ID, req.getAlarmId());
|
||||
assertEquals(String.valueOf(TEST_ORDER_ID), req.getOrderId());
|
||||
assertTrue(req.getUserIds().contains(TEST_WECHAT_ID));
|
||||
assertEquals("入侵告警", req.getTitle());
|
||||
assertEquals("A栋大堂", req.getAreaName());
|
||||
assertEquals("大堂摄像头", req.getCameraName());
|
||||
}
|
||||
|
||||
@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();
|
||||
|
||||
// 派单成功
|
||||
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();
|
||||
when(securityExtMapper.selectByOpsOrderId(TEST_ORDER_ID)).thenReturn(ext);
|
||||
|
||||
// 执行
|
||||
listener.onOrderCreated(event);
|
||||
|
||||
// 验证不发送卡片
|
||||
verify(vspNotifyClient, never()).sendCard(any());
|
||||
}
|
||||
|
||||
// ==================== onOrderStateChanged VSP 同步测试 ====================
|
||||
|
||||
@Test
|
||||
void onOrderStateChanged_shouldSyncConfirmed() {
|
||||
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);
|
||||
|
||||
// 企微 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());
|
||||
}
|
||||
|
||||
@Test
|
||||
void onOrderStateChanged_shouldSyncCompleted() {
|
||||
OrderStateChangedEvent event = buildStateChangedEvent(
|
||||
WorkOrderStatusEnum.ARRIVED, WorkOrderStatusEnum.COMPLETED);
|
||||
|
||||
// 告警来源工单,非误报
|
||||
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);
|
||||
|
||||
// 企微 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());
|
||||
}
|
||||
|
||||
// ==================== 辅助方法 ====================
|
||||
|
||||
private OrderStateChangedEvent buildStateChangedEvent(
|
||||
WorkOrderStatusEnum oldStatus, WorkOrderStatusEnum newStatus) {
|
||||
return OrderStateChangedEvent.builder()
|
||||
.orderId(TEST_ORDER_ID)
|
||||
.orderType("SECURITY")
|
||||
.oldStatus(oldStatus)
|
||||
.newStatus(newStatus)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
package com.viewsh.module.ops.security.integration.vsp;
|
||||
|
||||
import com.viewsh.module.ops.security.integration.vsp.config.VspNotifyProperties;
|
||||
import com.viewsh.module.ops.security.integration.vsp.dto.VspResponseDTO;
|
||||
import com.viewsh.module.ops.security.integration.vsp.dto.VspSendCardReqDTO;
|
||||
import com.viewsh.module.ops.security.integration.vsp.dto.VspSyncStatusReqDTO;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.web.client.RestClientException;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
/**
|
||||
* VspNotifyClientImpl 单元测试
|
||||
*
|
||||
* @author lzh
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
public class VspNotifyClientImplTest {
|
||||
|
||||
@Mock
|
||||
private RestTemplate restTemplate;
|
||||
|
||||
private VspNotifyProperties properties;
|
||||
|
||||
private VspNotifyClientImpl client;
|
||||
|
||||
private static final String BASE_URL = "http://localhost:8000";
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
properties = new VspNotifyProperties();
|
||||
properties.setEnabled(true);
|
||||
properties.setBaseUrl(BASE_URL);
|
||||
properties.setMaxRetry(2);
|
||||
client = new VspNotifyClientImpl(restTemplate, properties);
|
||||
}
|
||||
|
||||
// ==================== sendCard 测试 ====================
|
||||
|
||||
@Test
|
||||
void sendCard_success() {
|
||||
VspSendCardReqDTO req = buildSendCardReq();
|
||||
|
||||
VspResponseDTO resp = new VspResponseDTO();
|
||||
resp.setCode(0);
|
||||
resp.setMsg("ok");
|
||||
|
||||
when(restTemplate.postForObject(anyString(), any(), eq(VspResponseDTO.class)))
|
||||
.thenReturn(resp);
|
||||
|
||||
// 执行
|
||||
client.sendCard(req);
|
||||
|
||||
// 验证只调用一次
|
||||
verify(restTemplate, times(1)).postForObject(
|
||||
eq(BASE_URL + "/api/wechat/notify/send-card"),
|
||||
eq(req),
|
||||
eq(VspResponseDTO.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void sendCard_retryOnNetworkError() {
|
||||
VspSendCardReqDTO req = buildSendCardReq();
|
||||
|
||||
when(restTemplate.postForObject(anyString(), any(), eq(VspResponseDTO.class)))
|
||||
.thenThrow(new RestClientException("Connection refused"));
|
||||
|
||||
// 执行
|
||||
client.sendCard(req);
|
||||
|
||||
// maxRetry=2,总共调用 1 + 2 = 3 次
|
||||
verify(restTemplate, times(3)).postForObject(
|
||||
eq(BASE_URL + "/api/wechat/notify/send-card"),
|
||||
eq(req),
|
||||
eq(VspResponseDTO.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void sendCard_noRetryOnBusinessError() {
|
||||
VspSendCardReqDTO req = buildSendCardReq();
|
||||
|
||||
VspResponseDTO resp = new VspResponseDTO();
|
||||
resp.setCode(-1);
|
||||
resp.setMsg("业务失败");
|
||||
|
||||
when(restTemplate.postForObject(anyString(), any(), eq(VspResponseDTO.class)))
|
||||
.thenReturn(resp);
|
||||
|
||||
// 执行
|
||||
client.sendCard(req);
|
||||
|
||||
// 业务失败不重试,只调用一次
|
||||
verify(restTemplate, times(1)).postForObject(anyString(), any(), eq(VspResponseDTO.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void sendCard_disabled() {
|
||||
properties.setEnabled(false);
|
||||
|
||||
VspSendCardReqDTO req = buildSendCardReq();
|
||||
|
||||
// 执行
|
||||
client.sendCard(req);
|
||||
|
||||
// 未启用时不调用 RestTemplate
|
||||
verify(restTemplate, never()).postForObject(anyString(), any(), any());
|
||||
}
|
||||
|
||||
// ==================== syncStatus 测试 ====================
|
||||
|
||||
@Test
|
||||
void syncStatus_success() {
|
||||
VspSyncStatusReqDTO req = VspSyncStatusReqDTO.builder()
|
||||
.alarmId("alarm-001")
|
||||
.orderId("10001")
|
||||
.status("confirmed")
|
||||
.operator("wechat_user_1")
|
||||
.remark("确认接单")
|
||||
.build();
|
||||
|
||||
VspResponseDTO resp = new VspResponseDTO();
|
||||
resp.setCode(0);
|
||||
resp.setMsg("ok");
|
||||
|
||||
when(restTemplate.postForObject(anyString(), any(), eq(VspResponseDTO.class)))
|
||||
.thenReturn(resp);
|
||||
|
||||
// 执行
|
||||
client.syncStatus(req);
|
||||
|
||||
// 验证使用 sync-status URL
|
||||
verify(restTemplate, times(1)).postForObject(
|
||||
eq(BASE_URL + "/api/wechat/notify/sync-status"),
|
||||
eq(req),
|
||||
eq(VspResponseDTO.class));
|
||||
}
|
||||
|
||||
// ==================== 辅助方法 ====================
|
||||
|
||||
private VspSendCardReqDTO buildSendCardReq() {
|
||||
return VspSendCardReqDTO.builder()
|
||||
.alarmId("alarm-001")
|
||||
.orderId("10001")
|
||||
.userIds(List.of("wechat_user_1"))
|
||||
.title("入侵告警")
|
||||
.areaName("A栋大堂")
|
||||
.cameraName("大堂摄像头")
|
||||
.eventTime("03-24 10:00")
|
||||
.level(1)
|
||||
.snapshotUrl("http://example.com/snapshot.jpg")
|
||||
.build();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user