feat(iot): 一期 Controller 补齐 (B2/B4-6/B10/B11/B12/B13)
对照前端 feat/iot-2.0 已固化 API 契约补齐 5 组缺失端点(发现于一期 19/19
宣称完成后前端联调阶段),归属原任务卡 Controller 层返工,不占用二期
B20+ 编号。
- B2 规则链: 补 PUT /disable /deploy /debug + POST /copy?id= +
新增 GET /rule-chain/get?id= 返 GraphVO(保留 /get/{id})
deployRuleChain=enable+主动 Pub/Sub evict(对齐 B8)
- B10 子系统: 新增 GET /device-count 聚合(HGETALL 返空 map 遵循 A6)
+ GET /get?id= query 别名(保留 /get/{id})
- B11 设备: 新增驼峰 PUT /bindSubsystem /batchBindSubsystem
+ 2 ReqVO,保留 kebab 兼容
- B12/B13 告警: 新增 IotAlarmRecordController(整缺)11 端点:
page/get/ack/unack/clear/archive/batch-{ack,clear,archive}/
history/remark;Service 补 6 方法(getPage/batchAck/
batchClear/batchArchive/updateRemark/listHistory)
+ Mapper 2 方法 + 8 VO
- B4/5/6 节点元数据: 新增 GET /iot/rule/provider/metadata 聚合端点;
3 SPI 加 default getMetadata(),4 Manager 加
listAllMetadata(),13 具体 Provider 覆写(中文 label
+ mdi: icon),schema MVP 空骨架 {rule:[]}
测试:
- iot-rule 191/191 全绿(+5 B2 补齐 +9 B4/5/6 补齐)
- iot-server 106 active/161 Skipped v1 遗产 全绿
(+6 B12/B13 补齐 +3 B10 补齐)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -3,7 +3,10 @@ package com.viewsh.module.iot.service.alarm;
|
||||
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
import com.viewsh.framework.common.exception.ServiceException;
|
||||
import com.viewsh.framework.common.pojo.PageResult;
|
||||
import com.viewsh.framework.tenant.core.context.TenantContextHolder;
|
||||
import com.viewsh.module.iot.controller.admin.alarm.vo.AlarmRecordPageReqVO;
|
||||
import com.viewsh.module.iot.dal.dataobject.alarm.AlarmHistoryDO;
|
||||
import com.viewsh.module.iot.dal.dataobject.alarm.IotAlarmRecordDO;
|
||||
import com.viewsh.module.iot.dal.mysql.alarm.IotAlarmRecordMapper;
|
||||
import com.viewsh.module.iot.service.alarm.dto.AlarmStateTransitionRequest;
|
||||
@@ -19,6 +22,8 @@ import org.mockito.MockedStatic;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import static com.viewsh.module.iot.enums.ErrorCodeConstants.*;
|
||||
@@ -57,6 +62,10 @@ class IotAlarmRecordServiceImplTest {
|
||||
@Mock
|
||||
private IotAlarmPropagationService alarmPropagationService;
|
||||
|
||||
/** B20:告警历史 Service */
|
||||
@Mock
|
||||
private IotAlarmHistoryService alarmHistoryService;
|
||||
|
||||
private MockedStatic<TenantContextHolder> tenantMock;
|
||||
|
||||
private static final Long TENANT_ID = 1L;
|
||||
@@ -366,6 +375,121 @@ class IotAlarmRecordServiceImplTest {
|
||||
verify(cacheService).evict(666L);
|
||||
}
|
||||
|
||||
// ==================== B20 新增测试用例 ====================
|
||||
|
||||
// ==================== B20 用例 1:分页查询 ====================
|
||||
|
||||
@Test
|
||||
void testGetAlarmPage() {
|
||||
AlarmRecordPageReqVO reqVO = new AlarmRecordPageReqVO();
|
||||
reqVO.setPageNo(1);
|
||||
reqVO.setPageSize(10);
|
||||
reqVO.setSeverity(1);
|
||||
|
||||
IotAlarmRecordDO alarm = activeAlarm(100L);
|
||||
PageResult<IotAlarmRecordDO> mockResult = new PageResult<>(List.of(alarm), 1L);
|
||||
when(alarmRecordMapper.selectPage(reqVO)).thenReturn(mockResult);
|
||||
|
||||
PageResult<IotAlarmRecordDO> result = alarmService.getAlarmPage(reqVO);
|
||||
|
||||
assertNotNull(result);
|
||||
assertEquals(1L, result.getTotal());
|
||||
assertEquals(1, result.getList().size());
|
||||
assertEquals(100L, result.getList().get(0).getId());
|
||||
verify(alarmRecordMapper).selectPage(reqVO);
|
||||
}
|
||||
|
||||
// ==================== B20 用例 2:批量确认(遍历单条幂等) ====================
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
void testBatchAckAlarm() {
|
||||
// 三个告警 ID,各自未确认
|
||||
IotAlarmRecordDO a1 = activeAlarm(201L);
|
||||
IotAlarmRecordDO a2 = activeAlarm(202L);
|
||||
IotAlarmRecordDO a3 = activeAlarm(203L);
|
||||
when(alarmRecordMapper.selectById(201L)).thenReturn(a1);
|
||||
when(alarmRecordMapper.selectById(202L)).thenReturn(a2);
|
||||
when(alarmRecordMapper.selectById(203L)).thenReturn(a3);
|
||||
|
||||
alarmService.batchAckAlarm(List.of(201L, 202L, 203L), "admin", "批量确认");
|
||||
|
||||
// 三条各调一次 updateById
|
||||
verify(alarmRecordMapper, times(3)).updateById(any(IotAlarmRecordDO.class));
|
||||
}
|
||||
|
||||
// ==================== B20 用例 3:批量清除 ====================
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
void testBatchClearAlarm() {
|
||||
IotAlarmRecordDO a1 = activeAlarm(301L);
|
||||
IotAlarmRecordDO a2 = activeAlarm(302L);
|
||||
when(alarmRecordMapper.selectById(301L)).thenReturn(a1);
|
||||
when(alarmRecordMapper.selectById(302L)).thenReturn(a2);
|
||||
|
||||
alarmService.batchClearAlarm(List.of(301L, 302L), "system", null);
|
||||
|
||||
verify(alarmRecordMapper, times(2)).updateById(any(IotAlarmRecordDO.class));
|
||||
}
|
||||
|
||||
// ==================== B20 用例 4:批量归档(独立捕获已归档) ====================
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
void testBatchArchiveAlarm_partialAlreadyArchived() {
|
||||
IotAlarmRecordDO active = activeAlarm(401L);
|
||||
// 402 已归档 → archiveAlarm 会抛 ALARM_ALREADY_ARCHIVED
|
||||
IotAlarmRecordDO archived = IotAlarmRecordDO.builder()
|
||||
.id(402L).ackState(1).clearState(1).archived(1).triggerCount(5).build();
|
||||
when(alarmRecordMapper.selectById(401L)).thenReturn(active);
|
||||
when(alarmRecordMapper.selectById(402L)).thenReturn(archived);
|
||||
|
||||
int failCount = alarmService.batchArchiveAlarm(List.of(401L, 402L), "admin");
|
||||
|
||||
// 401 成功,402 失败
|
||||
assertEquals(1, failCount);
|
||||
// 401 调了 updateById,402 由于已归档在锁前抛错,不走 updateById
|
||||
verify(alarmRecordMapper, times(1)).updateById(any(IotAlarmRecordDO.class));
|
||||
}
|
||||
|
||||
// ==================== B20 用例 5:更新备注 ====================
|
||||
|
||||
@Test
|
||||
void testUpdateRemark() {
|
||||
IotAlarmRecordDO alarm = activeAlarm(501L);
|
||||
when(alarmRecordMapper.selectById(501L)).thenReturn(alarm);
|
||||
|
||||
alarmService.updateRemark(501L, "已通知运维");
|
||||
|
||||
ArgumentCaptor<IotAlarmRecordDO> captor = ArgumentCaptor.forClass(IotAlarmRecordDO.class);
|
||||
verify(alarmRecordMapper).updateById(captor.capture());
|
||||
assertEquals(501L, captor.getValue().getId());
|
||||
assertEquals("已通知运维", captor.getValue().getProcessRemark());
|
||||
// 不走分布式锁
|
||||
verify(lockService, never()).executeWithLock(any(), any(), any());
|
||||
}
|
||||
|
||||
// ==================== B20 用例 6:查询历史(代理给 HistoryService) ====================
|
||||
|
||||
@Test
|
||||
void testListAlarmHistory() {
|
||||
AlarmHistoryDO history = AlarmHistoryDO.builder()
|
||||
.alarmRecordId(601L)
|
||||
.eventType("ack")
|
||||
.ts(Instant.now())
|
||||
.operator("admin")
|
||||
.build();
|
||||
when(alarmHistoryService.queryByAlarmRecord(eq(601L), any(), any()))
|
||||
.thenReturn(List.of(history));
|
||||
|
||||
List<AlarmHistoryDO> result = alarmService.listAlarmHistory(601L);
|
||||
|
||||
assertNotNull(result);
|
||||
assertEquals(1, result.size());
|
||||
assertEquals("ack", result.get(0).getEventType());
|
||||
}
|
||||
|
||||
// ==================== 辅助 ====================
|
||||
|
||||
private AlarmTriggerRequest buildTriggerReq(int severity) {
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.viewsh.module.iot.service.subsystem;
|
||||
import com.viewsh.framework.common.exception.ServiceException;
|
||||
import com.viewsh.framework.common.pojo.PageResult;
|
||||
import com.viewsh.framework.tenant.core.context.TenantContextHolder;
|
||||
import com.viewsh.module.iot.controller.admin.subsystem.vo.IotSubsystemDeviceCountRespVO;
|
||||
import com.viewsh.module.iot.controller.admin.subsystem.vo.IotSubsystemDeviceStatsRespVO;
|
||||
import com.viewsh.module.iot.controller.admin.subsystem.vo.IotSubsystemPageReqVO;
|
||||
import com.viewsh.module.iot.controller.admin.subsystem.vo.IotSubsystemSaveReqVO;
|
||||
@@ -21,7 +22,9 @@ import org.mockito.MockedStatic;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static com.viewsh.module.iot.enums.ErrorCodeConstants.*;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
@@ -259,6 +262,67 @@ class IotSubsystemServiceImplTest {
|
||||
assertDoesNotThrow(() -> subsystemService.rebuildDeviceCountCache());
|
||||
}
|
||||
|
||||
// ==================== 用例 9(B22):getAllSubsystemDeviceCount Redis 命中 → 返回 map ====================
|
||||
|
||||
@Test
|
||||
void testGetAllSubsystemDeviceCount_redisHit() {
|
||||
// Redis 返回 2 个子系统的设备数
|
||||
Map<Long, Long> redisData = Map.of(1L, 10L, 2L, 5L);
|
||||
when(deviceCountRedisDAO.getAllCounts(TENANT_ID)).thenReturn(redisData);
|
||||
|
||||
Map<Long, IotSubsystemDeviceCountRespVO> result = subsystemService.getAllSubsystemDeviceCount();
|
||||
|
||||
assertNotNull(result);
|
||||
assertEquals(2, result.size());
|
||||
|
||||
IotSubsystemDeviceCountRespVO vo1 = result.get(1L);
|
||||
assertNotNull(vo1);
|
||||
assertEquals(10L, vo1.getTotal());
|
||||
assertEquals(0L, vo1.getOnline()); // 待 B12/B14
|
||||
assertEquals(0L, vo1.getAlarm()); // 待 B12/B14
|
||||
|
||||
IotSubsystemDeviceCountRespVO vo2 = result.get(2L);
|
||||
assertNotNull(vo2);
|
||||
assertEquals(5L, vo2.getTotal());
|
||||
|
||||
verify(deviceCountRedisDAO, times(1)).getAllCounts(TENANT_ID);
|
||||
}
|
||||
|
||||
// ==================== 用例 10(B22):getAllSubsystemDeviceCount Redis miss → 返回空 map ====================
|
||||
|
||||
@Test
|
||||
void testGetAllSubsystemDeviceCount_redisMiss() {
|
||||
// Redis Hash 为空(Key 不存在或 Hash 无数据)
|
||||
when(deviceCountRedisDAO.getAllCounts(TENANT_ID)).thenReturn(Collections.emptyMap());
|
||||
|
||||
Map<Long, IotSubsystemDeviceCountRespVO> result = subsystemService.getAllSubsystemDeviceCount();
|
||||
|
||||
assertNotNull(result);
|
||||
assertTrue(result.isEmpty());
|
||||
|
||||
verify(deviceCountRedisDAO, times(1)).getAllCounts(TENANT_ID);
|
||||
}
|
||||
|
||||
// ==================== 用例 11(B22):getAllSubsystemDeviceCount 空数据集 → 返回空 map ====================
|
||||
|
||||
@Test
|
||||
void testGetAllSubsystemDeviceCount_emptyData() {
|
||||
// 明确验证:Redis 命中但计数均为 0 时,仍正确返回 total=0 的 VO
|
||||
Map<Long, Long> redisData = Map.of(100L, 0L);
|
||||
when(deviceCountRedisDAO.getAllCounts(TENANT_ID)).thenReturn(redisData);
|
||||
|
||||
Map<Long, IotSubsystemDeviceCountRespVO> result = subsystemService.getAllSubsystemDeviceCount();
|
||||
|
||||
assertNotNull(result);
|
||||
assertEquals(1, result.size());
|
||||
|
||||
IotSubsystemDeviceCountRespVO vo = result.get(100L);
|
||||
assertNotNull(vo);
|
||||
assertEquals(0L, vo.getTotal());
|
||||
assertEquals(0L, vo.getOnline());
|
||||
assertEquals(0L, vo.getAlarm());
|
||||
}
|
||||
|
||||
// ==================== 辅助方法 ====================
|
||||
|
||||
private IotSubsystemSaveReqVO buildSaveReqVO(String name, String code, Long projectId) {
|
||||
|
||||
Reference in New Issue
Block a user