test: Add comprehensive tests for dispatch strategies and IoT signal processing, fix SignalLossRuleProcessor bug

This commit is contained in:
lzh
2026-01-23 11:44:49 +08:00
parent 9750088ca6
commit 5bb3ff6979
5 changed files with 466 additions and 6 deletions

View File

@@ -85,12 +85,12 @@ public class SignalLossRuleProcessor {
// 解析 deviceId 和 areaId
// Key 格式iot:clean:signal:loss:{deviceId}:{areaId}
String[] parts = key.split(":");
if (parts.length < 5) {
continue;
}
Long deviceId = Long.parseLong(parts[3]);
Long areaId = Long.parseLong(parts[4]);
if (parts.length < 6) {
continue;
}
Long deviceId = Long.parseLong(parts[4]);
Long areaId = Long.parseLong(parts[5]);
// 检查超时
checkTimeoutForDevice(deviceId, areaId);

View File

@@ -0,0 +1,126 @@
package com.viewsh.module.iot.service.rule.clean.detector;
import com.viewsh.module.iot.dal.dataobject.integration.clean.BeaconPresenceConfig;
import org.junit.jupiter.api.Test;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
/**
* RSSI 滑动窗口检测器测试
*/
class RssiSlidingWindowDetectorTest {
private final RssiSlidingWindowDetector detector = new RssiSlidingWindowDetector();
@Test
void testDetect_ArriveConfirmed() {
// 准备配置:进入阈值 -70窗口 3命中 2
BeaconPresenceConfig.EnterConfig enterConfig = new BeaconPresenceConfig.EnterConfig();
enterConfig.setRssiThreshold(-70);
enterConfig.setWindowSize(3);
enterConfig.setHitCount(2);
// 退出配置(此场景不重要,设宽一点)
BeaconPresenceConfig.ExitConfig exitConfig = new BeaconPresenceConfig.ExitConfig();
exitConfig.setWeakRssiThreshold(-85);
exitConfig.setHitCount(2);
// 场景13次采样[-65, -68, -75] -> 2次 >= -70 -> 应该确认到达
List<Integer> window = Arrays.asList(-65, -68, -75);
RssiSlidingWindowDetector.DetectionResult result = detector.detect(
window, enterConfig, exitConfig, RssiSlidingWindowDetector.AreaState.OUT_AREA);
assertEquals(RssiSlidingWindowDetector.DetectionResult.ARRIVE_CONFIRMED, result);
}
@Test
void testDetect_ArriveFailed_NotEnoughHits() {
BeaconPresenceConfig.EnterConfig enterConfig = new BeaconPresenceConfig.EnterConfig();
enterConfig.setRssiThreshold(-70);
enterConfig.setWindowSize(3);
enterConfig.setHitCount(2);
BeaconPresenceConfig.ExitConfig exitConfig = new BeaconPresenceConfig.ExitConfig();
exitConfig.setWeakRssiThreshold(-85);
exitConfig.setHitCount(2);
// 场景3次采样[-65, -75, -80] -> 只有1次 >= -70 -> 无变化
List<Integer> window = Arrays.asList(-65, -75, -80);
RssiSlidingWindowDetector.DetectionResult result = detector.detect(
window, enterConfig, exitConfig, RssiSlidingWindowDetector.AreaState.OUT_AREA);
assertEquals(RssiSlidingWindowDetector.DetectionResult.NO_CHANGE, result);
}
@Test
void testDetect_LeaveConfirmed() {
BeaconPresenceConfig.EnterConfig enterConfig = new BeaconPresenceConfig.EnterConfig();
enterConfig.setRssiThreshold(-70);
enterConfig.setHitCount(2);
// 退出配置:弱阈值 -85窗口 3命中 2
BeaconPresenceConfig.ExitConfig exitConfig = new BeaconPresenceConfig.ExitConfig();
exitConfig.setWeakRssiThreshold(-85);
exitConfig.setWindowSize(3);
exitConfig.setHitCount(2);
// 场景:[-90, -88, -80] -> 2次 < -85 -> 应该确认离开
List<Integer> window = Arrays.asList(-90, -88, -80);
RssiSlidingWindowDetector.DetectionResult result = detector.detect(
window, enterConfig, exitConfig, RssiSlidingWindowDetector.AreaState.IN_AREA);
assertEquals(RssiSlidingWindowDetector.DetectionResult.LEAVE_CONFIRMED, result);
}
@Test
void testDetect_LeaveConfirmed_WithMissingSignal() {
BeaconPresenceConfig.EnterConfig enterConfig = new BeaconPresenceConfig.EnterConfig();
enterConfig.setRssiThreshold(-70);
enterConfig.setHitCount(2);
BeaconPresenceConfig.ExitConfig exitConfig = new BeaconPresenceConfig.ExitConfig();
exitConfig.setWeakRssiThreshold(-85);
exitConfig.setWindowSize(3);
exitConfig.setHitCount(2);
// 场景:[-999, -999, -80] -> -999表示丢失满足退出条件
List<Integer> window = Arrays.asList(-999, -999, -80);
RssiSlidingWindowDetector.DetectionResult result = detector.detect(
window, enterConfig, exitConfig, RssiSlidingWindowDetector.AreaState.IN_AREA);
assertEquals(RssiSlidingWindowDetector.DetectionResult.LEAVE_CONFIRMED, result);
}
@Test
void testDetect_NoChange_SignalFluctuation() {
BeaconPresenceConfig.EnterConfig enterConfig = new BeaconPresenceConfig.EnterConfig();
enterConfig.setRssiThreshold(-70);
enterConfig.setHitCount(2);
BeaconPresenceConfig.ExitConfig exitConfig = new BeaconPresenceConfig.ExitConfig();
exitConfig.setWeakRssiThreshold(-85);
exitConfig.setHitCount(2);
// 场景:在区域内,信号变弱但未达退出阈值 [-80, -82, -84] -> 都在 -70 和 -85 之间 -> 无变化
List<Integer> window = Arrays.asList(-80, -82, -84);
RssiSlidingWindowDetector.DetectionResult result = detector.detect(
window, enterConfig, exitConfig, RssiSlidingWindowDetector.AreaState.IN_AREA);
assertEquals(RssiSlidingWindowDetector.DetectionResult.NO_CHANGE, result);
}
@Test
void testEmptyWindow() {
BeaconPresenceConfig.EnterConfig enterConfig = new BeaconPresenceConfig.EnterConfig();
BeaconPresenceConfig.ExitConfig exitConfig = new BeaconPresenceConfig.ExitConfig();
RssiSlidingWindowDetector.DetectionResult result = detector.detect(
Collections.emptyList(), enterConfig, exitConfig, RssiSlidingWindowDetector.AreaState.OUT_AREA);
assertEquals(RssiSlidingWindowDetector.DetectionResult.NO_CHANGE, result);
}
}

View File

@@ -0,0 +1,157 @@
package com.viewsh.module.iot.service.rule.clean.processor;
import com.viewsh.module.iot.core.integration.constants.CleanOrderTopics;
import com.viewsh.module.iot.dal.dataobject.integration.clean.BeaconPresenceConfig;
import com.viewsh.module.iot.dal.dataobject.integration.clean.CleanOrderIntegrationConfig;
import com.viewsh.module.iot.dal.redis.clean.BeaconArrivedTimeRedisDAO;
import com.viewsh.module.iot.dal.redis.clean.BeaconRssiWindowRedisDAO;
import com.viewsh.module.iot.dal.redis.clean.DeviceCurrentOrderRedisDAO;
import com.viewsh.module.iot.dal.redis.clean.SignalLossRedisDAO;
import com.viewsh.module.iot.service.integration.clean.CleanOrderIntegrationConfigService;
import org.apache.rocketmq.spring.core.RocketMQTemplate;
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 org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.messaging.Message;
import java.util.Collections;
import java.util.Set;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
@ExtendWith(MockitoExtension.class)
class SignalLossRuleProcessorTest {
@InjectMocks
private SignalLossRuleProcessor processor;
@Mock
private SignalLossRedisDAO signalLossRedisDAO;
@Mock
private BeaconArrivedTimeRedisDAO arrivedTimeRedisDAO;
@Mock
private BeaconRssiWindowRedisDAO windowRedisDAO;
@Mock
private DeviceCurrentOrderRedisDAO deviceCurrentOrderRedisDAO;
@Mock
private CleanOrderIntegrationConfigService configService;
@Mock
private RocketMQTemplate rocketMQTemplate;
@Mock
private StringRedisTemplate stringRedisTemplate;
private final Long DEVICE_ID = 1001L;
private final Long AREA_ID = 2001L;
private final String DEVICE_KEY = "badge-001";
@BeforeEach
void setUp() {
// Mock Redis keys scan
when(stringRedisTemplate.keys(anyString())).thenReturn(
Set.of("iot:clean:signal:loss:" + DEVICE_ID + ":" + AREA_ID)
);
}
@Test
void testCheckLossTimeout_TriggerComplete() {
// Setup times
long now = System.currentTimeMillis();
long firstLossTime = now - 6 * 60 * 1000; // 6 minutes ago
long lastLossTime = now;
long arrivedTime = now - 20 * 60 * 1000; // 20 minutes ago
// Mock DAOs
when(signalLossRedisDAO.getFirstLossTime(DEVICE_ID, AREA_ID)).thenReturn(firstLossTime);
when(signalLossRedisDAO.getLastLossTime(DEVICE_ID, AREA_ID)).thenReturn(lastLossTime);
when(arrivedTimeRedisDAO.getArrivedTime(DEVICE_ID, AREA_ID)).thenReturn(arrivedTime);
// Mock Current Order
DeviceCurrentOrderRedisDAO.OrderCacheInfo orderInfo = new DeviceCurrentOrderRedisDAO.OrderCacheInfo();
orderInfo.setOrderId(500L);
orderInfo.setAreaId(AREA_ID); // Same area, valid
when(deviceCurrentOrderRedisDAO.getCurrentOrder(DEVICE_ID)).thenReturn(orderInfo);
// Mock Config
BeaconPresenceConfig.ExitConfig exitConfig = new BeaconPresenceConfig.ExitConfig();
exitConfig.setLossTimeoutMinutes(5);
exitConfig.setMinValidWorkMinutes(10);
BeaconPresenceConfig bpConfig = new BeaconPresenceConfig();
bpConfig.setExit(exitConfig);
CleanOrderIntegrationConfig mainConfig = new CleanOrderIntegrationConfig();
mainConfig.setBeaconPresence(bpConfig);
CleanOrderIntegrationConfigService.AreaDeviceConfigWrapper wrapper =
new CleanOrderIntegrationConfigService.AreaDeviceConfigWrapper();
wrapper.setConfig(mainConfig);
wrapper.setDeviceKey(DEVICE_KEY);
when(configService.getConfigWrapperByDeviceId(DEVICE_ID)).thenReturn(wrapper);
// Execute
processor.checkLossTimeout();
// Verify
// 1. Should send complete message
verify(rocketMQTemplate).syncSend(eq(CleanOrderTopics.ORDER_COMPLETE), any(Message.class));
// 2. Should clear redis data
verify(signalLossRedisDAO).clearLossRecord(DEVICE_ID, AREA_ID);
verify(arrivedTimeRedisDAO).clearArrivedTime(DEVICE_ID, AREA_ID);
verify(windowRedisDAO).clearWindow(DEVICE_ID, AREA_ID);
}
@Test
void testCheckLossTimeout_Suppressed_InvalidDuration() {
// Setup times
long now = System.currentTimeMillis();
long firstLossTime = now - 6 * 60 * 1000; // 6 minutes ago (timeout)
long lastLossTime = now;
long arrivedTime = now - 5 * 60 * 1000; // Only 5 minutes work (min is 10)
// Mock DAOs
when(signalLossRedisDAO.getFirstLossTime(DEVICE_ID, AREA_ID)).thenReturn(firstLossTime);
when(signalLossRedisDAO.getLastLossTime(DEVICE_ID, AREA_ID)).thenReturn(lastLossTime);
when(arrivedTimeRedisDAO.getArrivedTime(DEVICE_ID, AREA_ID)).thenReturn(arrivedTime);
// Mock Current Order (Valid area)
DeviceCurrentOrderRedisDAO.OrderCacheInfo orderInfo = new DeviceCurrentOrderRedisDAO.OrderCacheInfo();
orderInfo.setOrderId(500L);
orderInfo.setAreaId(AREA_ID);
when(deviceCurrentOrderRedisDAO.getCurrentOrder(DEVICE_ID)).thenReturn(orderInfo);
// Mock Config
BeaconPresenceConfig.ExitConfig exitConfig = new BeaconPresenceConfig.ExitConfig();
exitConfig.setLossTimeoutMinutes(5);
exitConfig.setMinValidWorkMinutes(10);
BeaconPresenceConfig bpConfig = new BeaconPresenceConfig();
bpConfig.setExit(exitConfig);
CleanOrderIntegrationConfig mainConfig = new CleanOrderIntegrationConfig();
mainConfig.setBeaconPresence(bpConfig);
CleanOrderIntegrationConfigService.AreaDeviceConfigWrapper wrapper =
new CleanOrderIntegrationConfigService.AreaDeviceConfigWrapper();
wrapper.setConfig(mainConfig);
wrapper.setDeviceKey(DEVICE_KEY);
when(configService.getConfigWrapperByDeviceId(DEVICE_ID)).thenReturn(wrapper);
// Execute
processor.checkLossTimeout();
// Verify
// 1. Should NOT send complete message
verify(rocketMQTemplate, never()).syncSend(eq(CleanOrderTopics.ORDER_COMPLETE), any(Message.class));
// 2. Should send Audit Event (TTS)
verify(rocketMQTemplate, atLeastOnce()).syncSend(eq(CleanOrderTopics.ORDER_AUDIT), any(Message.class));
}
}