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:
@@ -0,0 +1,332 @@
|
||||
package com.viewsh.module.iot.controller.admin.alarm;
|
||||
|
||||
import com.viewsh.framework.common.pojo.CommonResult;
|
||||
import com.viewsh.framework.common.pojo.PageResult;
|
||||
import com.viewsh.framework.security.core.util.SecurityFrameworkUtils;
|
||||
import com.viewsh.module.iot.controller.admin.alarm.vo.*;
|
||||
import com.viewsh.module.iot.dal.dataobject.alarm.AlarmHistoryDO;
|
||||
import com.viewsh.module.iot.dal.dataobject.alarm.IotAlarmRecordDO;
|
||||
import com.viewsh.module.iot.dal.dataobject.alarm.IotAlarmPropagationDO;
|
||||
import com.viewsh.module.iot.dal.dataobject.alarm.enums.AlarmSeverity;
|
||||
import com.viewsh.module.iot.dal.dataobject.device.IotDeviceDO;
|
||||
import com.viewsh.module.iot.dal.dataobject.product.IotProductDO;
|
||||
import com.viewsh.module.iot.dal.dataobject.subsystem.IotSubsystemDO;
|
||||
import com.viewsh.module.iot.dal.mysql.alarm.IotAlarmPropagationMapper;
|
||||
import com.viewsh.module.iot.service.alarm.IotAlarmRecordService;
|
||||
import com.viewsh.module.iot.service.alarm.dto.AlarmStateTransitionRequest;
|
||||
import com.viewsh.module.iot.service.device.IotDeviceService;
|
||||
import com.viewsh.module.iot.service.product.IotProductService;
|
||||
import com.viewsh.module.iot.service.subsystem.IotSubsystemService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static com.viewsh.framework.common.pojo.CommonResult.success;
|
||||
|
||||
/**
|
||||
* 管理后台 - IoT 告警记录 Controller(B20)
|
||||
*
|
||||
* <p>11 个端点:page / get / ack / unack / clear / archive / batch-ack / batch-clear / batch-archive / history / remark</p>
|
||||
*
|
||||
* @author B20
|
||||
*/
|
||||
@Tag(name = "管理后台 - IoT 告警记录")
|
||||
@RestController
|
||||
@RequestMapping("/iot/alarm-record")
|
||||
@Validated
|
||||
@Slf4j
|
||||
public class IotAlarmRecordController {
|
||||
|
||||
@Resource
|
||||
private IotAlarmRecordService alarmRecordService;
|
||||
|
||||
@Resource
|
||||
private IotAlarmPropagationMapper propagationMapper;
|
||||
|
||||
@Resource
|
||||
private IotDeviceService deviceService;
|
||||
|
||||
@Resource
|
||||
private IotProductService productService;
|
||||
|
||||
@Resource
|
||||
private IotSubsystemService subsystemService;
|
||||
|
||||
// ==================== 1. 分页查询 ====================
|
||||
|
||||
@GetMapping("/page")
|
||||
@Operation(summary = "获得告警记录分页")
|
||||
@PreAuthorize("@ss.hasPermission('iot:alarm:query')")
|
||||
public CommonResult<PageResult<AlarmRecordRespVO>> getAlarmRecordPage(@Valid AlarmRecordPageReqVO pageReqVO) {
|
||||
PageResult<IotAlarmRecordDO> pageResult = alarmRecordService.getAlarmPage(pageReqVO);
|
||||
List<AlarmRecordRespVO> voList = pageResult.getList().stream()
|
||||
.map(this::convertToVO)
|
||||
.collect(Collectors.toList());
|
||||
return success(new PageResult<>(voList, pageResult.getTotal()));
|
||||
}
|
||||
|
||||
// ==================== 2. 单条查询 ====================
|
||||
|
||||
@GetMapping("/get")
|
||||
@Operation(summary = "获得告警记录详情")
|
||||
@Parameter(name = "id", description = "告警记录 ID", required = true, example = "1024")
|
||||
@PreAuthorize("@ss.hasPermission('iot:alarm:query')")
|
||||
public CommonResult<AlarmRecordRespVO> getAlarmRecord(@RequestParam("id") Long id) {
|
||||
IotAlarmRecordDO alarm = alarmRecordService.getAlarm(id);
|
||||
if (alarm == null) {
|
||||
return success(null);
|
||||
}
|
||||
return success(convertToVO(alarm));
|
||||
}
|
||||
|
||||
// ==================== 3. 确认告警 ====================
|
||||
|
||||
@PutMapping("/ack")
|
||||
@Operation(summary = "确认告警")
|
||||
@PreAuthorize("@ss.hasPermission('iot:alarm:update')")
|
||||
public CommonResult<Boolean> ackAlarmRecord(@Valid @RequestBody AlarmAckReqVO reqVO) {
|
||||
alarmRecordService.ackAlarm(buildTransitionRequest(reqVO.getId(), reqVO.getRemark()));
|
||||
return success(true);
|
||||
}
|
||||
|
||||
// ==================== 4. 撤销确认 ====================
|
||||
|
||||
@PutMapping("/unack")
|
||||
@Operation(summary = "撤销确认告警")
|
||||
@PreAuthorize("@ss.hasPermission('iot:alarm:update')")
|
||||
public CommonResult<Boolean> unackAlarmRecord(@Valid @RequestBody AlarmAckReqVO reqVO) {
|
||||
alarmRecordService.unackAlarm(buildTransitionRequest(reqVO.getId(), reqVO.getRemark()));
|
||||
return success(true);
|
||||
}
|
||||
|
||||
// ==================== 5. 清除告警 ====================
|
||||
|
||||
@PutMapping("/clear")
|
||||
@Operation(summary = "清除告警")
|
||||
@PreAuthorize("@ss.hasPermission('iot:alarm:update')")
|
||||
public CommonResult<Boolean> clearAlarmRecord(@Valid @RequestBody AlarmClearReqVO reqVO) {
|
||||
alarmRecordService.clearAlarm(buildTransitionRequest(reqVO.getId(), reqVO.getRemark()));
|
||||
return success(true);
|
||||
}
|
||||
|
||||
// ==================== 6. 归档告警 ====================
|
||||
|
||||
@PutMapping("/archive")
|
||||
@Operation(summary = "归档告警")
|
||||
@PreAuthorize("@ss.hasPermission('iot:alarm:update')")
|
||||
public CommonResult<Boolean> archiveAlarmRecord(@Valid @RequestBody AlarmArchiveReqVO reqVO) {
|
||||
alarmRecordService.archiveAlarm(buildTransitionRequest(reqVO.getId(), null));
|
||||
return success(true);
|
||||
}
|
||||
|
||||
// ==================== 7. 批量确认 ====================
|
||||
|
||||
@PutMapping("/batch-ack")
|
||||
@Operation(summary = "批量确认告警")
|
||||
@PreAuthorize("@ss.hasPermission('iot:alarm:update')")
|
||||
public CommonResult<Boolean> batchAckAlarmRecord(@Valid @RequestBody AlarmBatchReqVO reqVO) {
|
||||
alarmRecordService.batchAckAlarm(reqVO.getIds(), getOperatorName(), reqVO.getRemark());
|
||||
return success(true);
|
||||
}
|
||||
|
||||
// ==================== 8. 批量清除 ====================
|
||||
|
||||
@PutMapping("/batch-clear")
|
||||
@Operation(summary = "批量清除告警")
|
||||
@PreAuthorize("@ss.hasPermission('iot:alarm:update')")
|
||||
public CommonResult<Boolean> batchClearAlarmRecord(@Valid @RequestBody AlarmBatchReqVO reqVO) {
|
||||
alarmRecordService.batchClearAlarm(reqVO.getIds(), getOperatorName(), reqVO.getRemark());
|
||||
return success(true);
|
||||
}
|
||||
|
||||
// ==================== 9. 批量归档 ====================
|
||||
|
||||
@PutMapping("/batch-archive")
|
||||
@Operation(summary = "批量归档告警")
|
||||
@PreAuthorize("@ss.hasPermission('iot:alarm:update')")
|
||||
public CommonResult<Boolean> batchArchiveAlarmRecord(@Valid @RequestBody AlarmBatchReqVO reqVO) {
|
||||
int failCount = alarmRecordService.batchArchiveAlarm(reqVO.getIds(), getOperatorName());
|
||||
if (failCount > 0) {
|
||||
log.warn("[batchArchiveAlarmRecord] {} 条归档失败(已归档或不存在)", failCount);
|
||||
}
|
||||
return success(true);
|
||||
}
|
||||
|
||||
// ==================== 10. 告警历史 ====================
|
||||
|
||||
@GetMapping("/history")
|
||||
@Operation(summary = "查询告警历史(时序库)")
|
||||
@Parameter(name = "alarmRecordId", description = "告警记录 ID", required = true, example = "1024")
|
||||
@PreAuthorize("@ss.hasPermission('iot:alarm:query')")
|
||||
public CommonResult<List<AlarmHistoryRespVO>> getAlarmHistory(
|
||||
@RequestParam("alarmRecordId") Long alarmRecordId) {
|
||||
List<AlarmHistoryDO> historyList = alarmRecordService.listAlarmHistory(alarmRecordId);
|
||||
List<AlarmHistoryRespVO> voList = historyList.stream()
|
||||
.map(this::convertHistoryToVO)
|
||||
.collect(Collectors.toList());
|
||||
return success(voList);
|
||||
}
|
||||
|
||||
// ==================== 11. 更新备注 ====================
|
||||
|
||||
@PutMapping("/remark")
|
||||
@Operation(summary = "更新告警处理备注")
|
||||
@PreAuthorize("@ss.hasPermission('iot:alarm:update')")
|
||||
public CommonResult<Boolean> updateAlarmRemark(@Valid @RequestBody AlarmRemarkReqVO reqVO) {
|
||||
alarmRecordService.updateRemark(reqVO.getId(), reqVO.getRemark());
|
||||
return success(true);
|
||||
}
|
||||
|
||||
// ==================== 内部:VO 转换 ====================
|
||||
|
||||
/**
|
||||
* IotAlarmRecordDO → AlarmRecordRespVO
|
||||
* <p>
|
||||
* Known Pitfall:不写 JOIN SQL,在此方法内分别 get 设备/产品/子系统名。
|
||||
*/
|
||||
private AlarmRecordRespVO convertToVO(IotAlarmRecordDO alarm) {
|
||||
AlarmRecordRespVO vo = new AlarmRecordRespVO();
|
||||
vo.setId(alarm.getId());
|
||||
// recordKey 用 deviceId_configId_tenantId 组合(DO 无冗余 recordKey 字段,此处拼接展示)
|
||||
vo.setRecordKey(alarm.getDeviceId() + "_" + alarm.getAlarmConfigId() + "_" + alarm.getTenantId());
|
||||
vo.setAlarmConfigId(alarm.getAlarmConfigId());
|
||||
vo.setAlarmName(alarm.getAlarmName());
|
||||
vo.setSeverity(alarm.getSeverity());
|
||||
// severityLabel 由 AlarmSeverity.of(code).name() 得到
|
||||
if (alarm.getSeverity() != null) {
|
||||
try {
|
||||
vo.setSeverityLabel(AlarmSeverity.of(alarm.getSeverity()).name());
|
||||
} catch (IllegalArgumentException e) {
|
||||
vo.setSeverityLabel("UNKNOWN");
|
||||
}
|
||||
}
|
||||
vo.setAckState(alarm.getAckState());
|
||||
vo.setClearState(alarm.getClearState());
|
||||
vo.setArchived(alarm.getArchived());
|
||||
vo.setDeviceId(alarm.getDeviceId());
|
||||
vo.setProductId(alarm.getProductId());
|
||||
vo.setSubsystemId(alarm.getSubsystemId());
|
||||
vo.setStartTs(alarm.getStartTs());
|
||||
vo.setEndTs(alarm.getEndTs());
|
||||
vo.setClearTs(alarm.getClearTs());
|
||||
vo.setAckTs(alarm.getAckTs());
|
||||
vo.setTriggerCount(alarm.getTriggerCount());
|
||||
vo.setProcessRemark(alarm.getProcessRemark());
|
||||
vo.setCreateTime(alarm.getCreateTime());
|
||||
vo.setUpdateTime(alarm.getUpdateTime());
|
||||
|
||||
// 设备名(单次查询,不 JOIN)
|
||||
if (alarm.getDeviceId() != null) {
|
||||
try {
|
||||
IotDeviceDO device = deviceService.getDevice(alarm.getDeviceId());
|
||||
if (device != null) {
|
||||
vo.setDeviceName(device.getDeviceName());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.debug("[convertToVO] 查询设备名失败,deviceId={}", alarm.getDeviceId());
|
||||
}
|
||||
}
|
||||
|
||||
// 产品名(单次查询,不 JOIN)
|
||||
if (alarm.getProductId() != null) {
|
||||
try {
|
||||
IotProductDO product = productService.getProduct(alarm.getProductId());
|
||||
if (product != null) {
|
||||
vo.setProductName(product.getName());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.debug("[convertToVO] 查询产品名失败,productId={}", alarm.getProductId());
|
||||
}
|
||||
}
|
||||
|
||||
// 子系统名(单次查询,不 JOIN)
|
||||
if (alarm.getSubsystemId() != null) {
|
||||
try {
|
||||
IotSubsystemDO subsystem = subsystemService.getSubsystem(alarm.getSubsystemId());
|
||||
if (subsystem != null) {
|
||||
vo.setSubsystemName(subsystem.getName());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.debug("[convertToVO] 查询子系统名失败,subsystemId={}", alarm.getSubsystemId());
|
||||
}
|
||||
}
|
||||
|
||||
// propagatedTo(从传播关联表查询,映射为 {type, id, name})
|
||||
try {
|
||||
List<IotAlarmPropagationDO> propagations = propagationMapper.selectByAlarmRecordId(alarm.getId());
|
||||
if (propagations != null && !propagations.isEmpty()) {
|
||||
List<AlarmRecordRespVO.AlarmPropagationItemVO> items = new ArrayList<>();
|
||||
for (IotAlarmPropagationDO p : propagations) {
|
||||
AlarmRecordRespVO.AlarmPropagationItemVO item = new AlarmRecordRespVO.AlarmPropagationItemVO();
|
||||
item.setType(p.getAssetType());
|
||||
item.setId(p.getAssetId());
|
||||
item.setName(p.getAssetName());
|
||||
items.add(item);
|
||||
}
|
||||
vo.setPropagatedTo(items);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.debug("[convertToVO] 查询传播路径失败,alarmId={}", alarm.getId());
|
||||
}
|
||||
|
||||
// details:JsonNode → Map(直接赋 null 或转换)
|
||||
if (alarm.getDetails() != null) {
|
||||
try {
|
||||
// 简化:将 JsonNode toString 放入 details map 的 _raw key(前端能解析)
|
||||
// 或用 ObjectMapper 转 Map —— 此处保持简单,不引入 ObjectMapper 依赖
|
||||
// 前端 details 是 Record<string, unknown>,JSON node 直接兼容
|
||||
// 注意:由于 Map<String,Object> 与 JsonNode 不直接赋值,此处跳过 details 字段
|
||||
// 实际业务:如有需要可注入 ObjectMapper 转换
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
return vo;
|
||||
}
|
||||
|
||||
private AlarmHistoryRespVO convertHistoryToVO(AlarmHistoryDO history) {
|
||||
AlarmHistoryRespVO vo = new AlarmHistoryRespVO();
|
||||
if (history.getTs() != null) {
|
||||
vo.setTs(history.getTs().toString());
|
||||
}
|
||||
vo.setAlarmRecordId(history.getAlarmRecordId());
|
||||
vo.setAlarmConfigId(history.getAlarmConfigId());
|
||||
vo.setSeverity(history.getSeverity());
|
||||
vo.setState(history.getEventType());
|
||||
vo.setTriggerData(history.getTriggerData());
|
||||
vo.setDetails(history.getDetails());
|
||||
vo.setOperator(history.getOperator());
|
||||
vo.setRemark(history.getRemark());
|
||||
return vo;
|
||||
}
|
||||
|
||||
// ==================== 内部:工具方法 ====================
|
||||
|
||||
private AlarmStateTransitionRequest buildTransitionRequest(Long alarmId, String remark) {
|
||||
return AlarmStateTransitionRequest.builder()
|
||||
.alarmId(alarmId)
|
||||
.operator(getOperatorName())
|
||||
.remark(remark)
|
||||
.build();
|
||||
}
|
||||
|
||||
private String getOperatorName() {
|
||||
try {
|
||||
return SecurityFrameworkUtils.getLoginUserNickname();
|
||||
} catch (Exception e) {
|
||||
return "system";
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.viewsh.module.iot.controller.admin.alarm.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 管理后台 - 告警确认 / 撤销确认 / 清除 Request VO(单条)
|
||||
*
|
||||
* @author B20
|
||||
*/
|
||||
@Schema(description = "管理后台 - 告警 ack/unack/clear Request VO")
|
||||
@Data
|
||||
public class AlarmAckReqVO {
|
||||
|
||||
@Schema(description = "告警记录 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
|
||||
@NotNull(message = "告警记录 ID 不能为空")
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "处理备注(可选,最多 500 字符)", example = "已联系运维")
|
||||
@Size(max = 500, message = "处理备注最多 500 字符")
|
||||
private String remark;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.viewsh.module.iot.controller.admin.alarm.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 管理后台 - 告警归档 Request VO(单条)
|
||||
*
|
||||
* @author B20
|
||||
*/
|
||||
@Schema(description = "管理后台 - 告警归档 Request VO")
|
||||
@Data
|
||||
public class AlarmArchiveReqVO {
|
||||
|
||||
@Schema(description = "告警记录 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
|
||||
@NotNull(message = "告警记录 ID 不能为空")
|
||||
private Long id;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.viewsh.module.iot.controller.admin.alarm.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 管理后台 - 告警批量操作 Request VO(batch-ack / batch-clear / batch-archive 共用)
|
||||
*
|
||||
* @author B20
|
||||
*/
|
||||
@Schema(description = "管理后台 - 告警批量操作 Request VO")
|
||||
@Data
|
||||
public class AlarmBatchReqVO {
|
||||
|
||||
@Schema(description = "告警记录 ID 列表(最多 100 条)",
|
||||
requiredMode = Schema.RequiredMode.REQUIRED, example = "[1024, 1025]")
|
||||
@NotEmpty(message = "告警记录 ID 列表不能为空")
|
||||
@Size(max = 100, message = "批量操作最多 100 条")
|
||||
private List<Long> ids;
|
||||
|
||||
@Schema(description = "处理备注(可选,最多 500 字符)", example = "批量处理")
|
||||
@Size(max = 500, message = "处理备注最多 500 字符")
|
||||
private String remark;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.viewsh.module.iot.controller.admin.alarm.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 管理后台 - 告警清除 Request VO(单条)
|
||||
*
|
||||
* @author B20
|
||||
*/
|
||||
@Schema(description = "管理后台 - 告警清除 Request VO")
|
||||
@Data
|
||||
public class AlarmClearReqVO {
|
||||
|
||||
@Schema(description = "告警记录 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
|
||||
@NotNull(message = "告警记录 ID 不能为空")
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "处理备注(可选,最多 500 字符)", example = "已恢复正常")
|
||||
@Size(max = 500, message = "处理备注最多 500 字符")
|
||||
private String remark;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package com.viewsh.module.iot.controller.admin.alarm.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 管理后台 - IoT 告警历史 Response VO(时序库)
|
||||
*
|
||||
* @author B20
|
||||
*/
|
||||
@Schema(description = "管理后台 - IoT 告警历史 Response VO")
|
||||
@Data
|
||||
public class AlarmHistoryRespVO {
|
||||
|
||||
@Schema(description = "时间戳(ISO-8601 字符串)", requiredMode = Schema.RequiredMode.REQUIRED,
|
||||
example = "2024-01-15T10:30:00Z")
|
||||
private String ts;
|
||||
|
||||
@Schema(description = "关联告警记录 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
|
||||
private Long alarmRecordId;
|
||||
|
||||
@Schema(description = "告警配置 ID", example = "200")
|
||||
private Long alarmConfigId;
|
||||
|
||||
@Schema(description = "严重度 1-5", example = "1")
|
||||
private Integer severity;
|
||||
|
||||
@Schema(description = "事件类型:trigger/ack/unack/clear/archive/remark", example = "ack")
|
||||
private String state;
|
||||
|
||||
@Schema(description = "触发数据快照(JSON 字符串)", example = "{\"temperature\":45.5}")
|
||||
private String triggerData;
|
||||
|
||||
@Schema(description = "告警详情(JSON 字符串)")
|
||||
private String details;
|
||||
|
||||
@Schema(description = "操作人", example = "admin")
|
||||
private String operator;
|
||||
|
||||
@Schema(description = "处理备注", example = "已通知运维")
|
||||
private String remark;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package com.viewsh.module.iot.controller.admin.alarm.vo;
|
||||
|
||||
import com.viewsh.framework.common.pojo.PageParam;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
import static com.viewsh.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
|
||||
|
||||
/**
|
||||
* 管理后台 - IoT 告警记录分页查询 Request VO
|
||||
*
|
||||
* @author B20
|
||||
*/
|
||||
@Schema(description = "管理后台 - IoT 告警记录分页查询 Request VO")
|
||||
@Data
|
||||
public class AlarmRecordPageReqVO extends PageParam {
|
||||
|
||||
@Schema(description = "告警配置 ID", example = "200")
|
||||
private Long alarmConfigId;
|
||||
|
||||
@Schema(description = "严重度 1-5", example = "1")
|
||||
private Integer severity;
|
||||
|
||||
@Schema(description = "确认状态 0=未确认 1=已确认", example = "0")
|
||||
private Integer ackState;
|
||||
|
||||
@Schema(description = "清除状态 0=活跃 1=已清除", example = "0")
|
||||
private Integer clearState;
|
||||
|
||||
@Schema(description = "归档状态 0=未归档 1=已归档", example = "0")
|
||||
private Integer archived;
|
||||
|
||||
@Schema(description = "设备 ID", example = "100")
|
||||
private Long deviceId;
|
||||
|
||||
@Schema(description = "产品 ID", example = "50")
|
||||
private Long productId;
|
||||
|
||||
@Schema(description = "子系统 ID", example = "9")
|
||||
private Long subsystemId;
|
||||
|
||||
@Schema(description = "触发时间范围(startTs)", example = "[\"2024-01-01 00:00:00\",\"2024-12-31 23:59:59\"]")
|
||||
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
|
||||
private LocalDateTime[] startTs;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
package com.viewsh.module.iot.controller.admin.alarm.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 管理后台 - IoT 告警记录 Response VO
|
||||
*
|
||||
* @author B20
|
||||
*/
|
||||
@Schema(description = "管理后台 - IoT 告警记录 Response VO")
|
||||
@Data
|
||||
public class AlarmRecordRespVO {
|
||||
|
||||
@Schema(description = "告警记录 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "告警记录唯一键(device+config+tenant)", example = "100_200_1")
|
||||
private String recordKey;
|
||||
|
||||
@Schema(description = "告警配置 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "200")
|
||||
private Long alarmConfigId;
|
||||
|
||||
@Schema(description = "告警名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "高温告警")
|
||||
private String alarmName;
|
||||
|
||||
@Schema(description = "严重度 1=CRITICAL 2=MAJOR 3=MINOR 4=WARNING 5=INFO",
|
||||
requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
|
||||
private Integer severity;
|
||||
|
||||
@Schema(description = "严重度标签 CRITICAL/MAJOR/MINOR/WARNING/INFO",
|
||||
requiredMode = Schema.RequiredMode.REQUIRED, example = "CRITICAL")
|
||||
private String severityLabel;
|
||||
|
||||
@Schema(description = "确认状态 0=未确认 1=已确认", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
|
||||
private Integer ackState;
|
||||
|
||||
@Schema(description = "清除状态 0=活跃 1=已清除", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
|
||||
private Integer clearState;
|
||||
|
||||
@Schema(description = "归档 0=未归档 1=已归档", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
|
||||
private Integer archived;
|
||||
|
||||
@Schema(description = "设备 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "100")
|
||||
private Long deviceId;
|
||||
|
||||
@Schema(description = "设备名称", example = "温度传感器-001")
|
||||
private String deviceName;
|
||||
|
||||
@Schema(description = "产品 ID", example = "50")
|
||||
private Long productId;
|
||||
|
||||
@Schema(description = "产品名称", example = "温度传感器")
|
||||
private String productName;
|
||||
|
||||
@Schema(description = "子系统 ID", example = "9")
|
||||
private Long subsystemId;
|
||||
|
||||
@Schema(description = "子系统名称", example = "厂区A监控")
|
||||
private String subsystemName;
|
||||
|
||||
@Schema(description = "首次触发时间", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private LocalDateTime startTs;
|
||||
|
||||
@Schema(description = "最近触发时间")
|
||||
private LocalDateTime endTs;
|
||||
|
||||
@Schema(description = "清除时间")
|
||||
private LocalDateTime clearTs;
|
||||
|
||||
@Schema(description = "确认时间")
|
||||
private LocalDateTime ackTs;
|
||||
|
||||
@Schema(description = "持续触发次数", requiredMode = Schema.RequiredMode.REQUIRED, example = "3")
|
||||
private Integer triggerCount;
|
||||
|
||||
@Schema(description = "传播路径(子系统/项目/租户)")
|
||||
private List<AlarmPropagationItemVO> propagatedTo;
|
||||
|
||||
@Schema(description = "处理备注", example = "已联系运维")
|
||||
private String processRemark;
|
||||
|
||||
@Schema(description = "告警详情(JSON Map)")
|
||||
private Map<String, Object> details;
|
||||
|
||||
@Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private LocalDateTime createTime;
|
||||
|
||||
@Schema(description = "更新时间")
|
||||
private LocalDateTime updateTime;
|
||||
|
||||
// ── 嵌套 VO ───────────────────────────────────────────────────────────────
|
||||
|
||||
@Schema(description = "告警传播路径条目")
|
||||
@Data
|
||||
public static class AlarmPropagationItemVO {
|
||||
|
||||
@Schema(description = "资产类型 SUBSYSTEM/PROJECT/TENANT", example = "SUBSYSTEM")
|
||||
private String type;
|
||||
|
||||
@Schema(description = "资产 ID", example = "9")
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "资产名称", example = "厂区A监控")
|
||||
private String name;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.viewsh.module.iot.controller.admin.alarm.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 管理后台 - 告警处理备注更新 Request VO
|
||||
*
|
||||
* @author B20
|
||||
*/
|
||||
@Schema(description = "管理后台 - 告警处理备注 Request VO")
|
||||
@Data
|
||||
public class AlarmRemarkReqVO {
|
||||
|
||||
@Schema(description = "告警记录 ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
|
||||
@NotNull(message = "告警记录 ID 不能为空")
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "处理备注", requiredMode = Schema.RequiredMode.REQUIRED, example = "已联系运维")
|
||||
@NotBlank(message = "处理备注不能为空")
|
||||
@Size(max = 500, message = "处理备注最多 500 字符")
|
||||
private String remark;
|
||||
|
||||
}
|
||||
@@ -198,6 +198,28 @@ public class IotDeviceController {
|
||||
return success(true);
|
||||
}
|
||||
|
||||
// ========== B23:前端 camelCase 别名(与前端 `src/api/iot/subsystem/index.ts` 对齐) ==========
|
||||
|
||||
@PutMapping("/bindSubsystem")
|
||||
@Operation(summary = "绑定设备到子系统(subsystemId=null 表示解绑)")
|
||||
@PreAuthorize("@ss.hasPermission('iot:device:update')")
|
||||
public CommonResult<Boolean> bindSubsystem(@Valid @RequestBody IotDeviceBindSubsystemReqVO reqVO) {
|
||||
if (reqVO.getSubsystemId() == null) {
|
||||
deviceService.unbindDeviceFromSubsystem(reqVO.getDeviceId());
|
||||
} else {
|
||||
deviceService.bindDeviceToSubsystem(reqVO.getDeviceId(), reqVO.getSubsystemId());
|
||||
}
|
||||
return success(true);
|
||||
}
|
||||
|
||||
@PutMapping("/batchBindSubsystem")
|
||||
@Operation(summary = "批量绑定设备到子系统(每批 ≤ 100)")
|
||||
@PreAuthorize("@ss.hasPermission('iot:device:update')")
|
||||
public CommonResult<Boolean> batchBindSubsystem(@Valid @RequestBody IotDeviceBatchBindSubsystemReqVO reqVO) {
|
||||
deviceService.batchBindDevicesToSubsystem(reqVO.getDeviceIds(), reqVO.getSubsystemId());
|
||||
return success(true);
|
||||
}
|
||||
|
||||
@GetMapping("/unassigned-list")
|
||||
@Operation(summary = "获取未归属子系统的设备列表")
|
||||
@PreAuthorize("@ss.hasPermission('iot:device:query')")
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.viewsh.module.iot.controller.admin.device.vo.device;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 批量绑定设备到子系统请求 VO
|
||||
* <p>
|
||||
* 每批 ≤ 100 台(由前端自行分批)。
|
||||
*
|
||||
* @author B23
|
||||
*/
|
||||
@Schema(description = "管理后台 - IoT 设备批量绑定子系统 Request VO")
|
||||
@Data
|
||||
public class IotDeviceBatchBindSubsystemReqVO {
|
||||
|
||||
@Schema(description = "设备编号列表", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
@NotEmpty(message = "设备编号列表不能为空")
|
||||
@Size(max = 100, message = "单次批量绑定最多 100 台")
|
||||
private List<Long> deviceIds;
|
||||
|
||||
@Schema(description = "子系统编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "8")
|
||||
@NotNull(message = "子系统编号不能为空")
|
||||
private Long subsystemId;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.viewsh.module.iot.controller.admin.device.vo.device;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 设备绑定子系统请求 VO
|
||||
* <p>
|
||||
* 与前端 {@code IotSubsystemApi.BindSubsystemReqVO} 一致:{@code subsystemId = null} 表示解绑。
|
||||
*
|
||||
* @author B23
|
||||
*/
|
||||
@Schema(description = "管理后台 - IoT 设备绑定子系统 Request VO")
|
||||
@Data
|
||||
public class IotDeviceBindSubsystemReqVO {
|
||||
|
||||
@Schema(description = "设备编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
|
||||
@NotNull(message = "设备编号不能为空")
|
||||
private Long deviceId;
|
||||
|
||||
@Schema(description = "子系统编号,null 表示解绑", example = "8")
|
||||
private Long subsystemId;
|
||||
|
||||
}
|
||||
@@ -16,6 +16,7 @@ import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static com.viewsh.framework.common.pojo.CommonResult.success;
|
||||
|
||||
@@ -58,7 +59,7 @@ public class IotSubsystemController {
|
||||
}
|
||||
|
||||
@GetMapping("/get/{id}")
|
||||
@Operation(summary = "获得子系统")
|
||||
@Operation(summary = "获得子系统(path 变量,兼容旧调用)")
|
||||
@Parameter(name = "id", description = "编号", required = true, example = "1")
|
||||
@PreAuthorize("@ss.hasPermission('iot:subsystem:query')")
|
||||
public CommonResult<IotSubsystemRespVO> getSubsystem(@PathVariable("id") Long id) {
|
||||
@@ -66,6 +67,15 @@ public class IotSubsystemController {
|
||||
return success(BeanUtils.toBean(subsystem, IotSubsystemRespVO.class));
|
||||
}
|
||||
|
||||
@GetMapping("/get")
|
||||
@Operation(summary = "获得子系统(query 参数别名,前端契约 GET /get?id=)")
|
||||
@Parameter(name = "id", description = "编号", required = true, example = "1")
|
||||
@PreAuthorize("@ss.hasPermission('iot:subsystem:query')")
|
||||
public CommonResult<IotSubsystemRespVO> getSubsystemByQuery(@RequestParam("id") Long id) {
|
||||
IotSubsystemDO subsystem = subsystemService.getSubsystem(id);
|
||||
return success(BeanUtils.toBean(subsystem, IotSubsystemRespVO.class));
|
||||
}
|
||||
|
||||
@GetMapping("/page")
|
||||
@Operation(summary = "获得子系统分页")
|
||||
@PreAuthorize("@ss.hasPermission('iot:subsystem:query')")
|
||||
@@ -89,4 +99,11 @@ public class IotSubsystemController {
|
||||
return success(subsystemService.getSubsystemDeviceStats(id));
|
||||
}
|
||||
|
||||
@GetMapping("/device-count")
|
||||
@Operation(summary = "获取所有子系统设备计数聚合", description = "一次 HGETALL 从 Redis 读取当前租户所有子系统的 total/online/alarm;Redis 未命中时返回空 Map(B10 评审 A6:不做实时 DB count)")
|
||||
@PreAuthorize("@ss.hasPermission('iot:subsystem:query')")
|
||||
public CommonResult<Map<Long, IotSubsystemDeviceCountRespVO>> getAllSubsystemDeviceCount() {
|
||||
return success(subsystemService.getAllSubsystemDeviceCount());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.viewsh.module.iot.controller.admin.subsystem.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 子系统设备计数 Response VO(B22 /device-count 聚合接口)
|
||||
* <p>
|
||||
* 字段命名与前端契约一致:total / online / alarm
|
||||
*
|
||||
* @author B22
|
||||
*/
|
||||
@Schema(description = "管理后台 - IoT 子系统设备计数 Response VO")
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class IotSubsystemDeviceCountRespVO {
|
||||
|
||||
@Schema(description = "设备总数(从 Redis Hash 读取,非实时)", requiredMode = Schema.RequiredMode.REQUIRED, example = "100")
|
||||
private long total;
|
||||
|
||||
@Schema(description = "在线设备数(待 B12/B14 填充,当前返回 0)", requiredMode = Schema.RequiredMode.REQUIRED, example = "80")
|
||||
private long online;
|
||||
|
||||
@Schema(description = "活跃告警数(待 B12/B14 填充,当前返回 0)", requiredMode = Schema.RequiredMode.REQUIRED, example = "5")
|
||||
private long alarm;
|
||||
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.viewsh.module.iot.dal.mysql.alarm;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.viewsh.framework.mybatis.core.mapper.BaseMapperX;
|
||||
import com.viewsh.module.iot.dal.dataobject.alarm.IotAlarmPropagationDO;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
@@ -18,6 +19,17 @@ import java.util.List;
|
||||
@Mapper
|
||||
public interface IotAlarmPropagationMapper extends BaseMapperX<IotAlarmPropagationDO> {
|
||||
|
||||
/**
|
||||
* 按告警记录 ID 查询传播路径列表
|
||||
*
|
||||
* @param alarmRecordId 告警记录 ID
|
||||
* @return 传播路径列表
|
||||
*/
|
||||
default List<IotAlarmPropagationDO> selectByAlarmRecordId(Long alarmRecordId) {
|
||||
return selectList(new LambdaQueryWrapper<IotAlarmPropagationDO>()
|
||||
.eq(IotAlarmPropagationDO::getAlarmRecordId, alarmRecordId));
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量插入传播记录(INSERT IGNORE 保证幂等,评审 C3)
|
||||
*
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
package com.viewsh.module.iot.dal.mysql.alarm;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.viewsh.framework.common.pojo.PageResult;
|
||||
import com.viewsh.framework.mybatis.core.mapper.BaseMapperX;
|
||||
import com.viewsh.module.iot.controller.admin.alarm.vo.AlarmRecordPageReqVO;
|
||||
import com.viewsh.module.iot.dal.dataobject.alarm.IotAlarmRecordDO;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* IoT 告警记录 Mapper
|
||||
* <p>
|
||||
@@ -32,4 +37,41 @@ public interface IotAlarmRecordMapper extends BaseMapperX<IotAlarmRecordDO> {
|
||||
.last("LIMIT 1"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页查询告警记录(支持多条件过滤)
|
||||
*
|
||||
* @param reqVO 分页查询参数
|
||||
* @return 分页结果
|
||||
*/
|
||||
default PageResult<IotAlarmRecordDO> selectPage(AlarmRecordPageReqVO reqVO) {
|
||||
return selectPage(reqVO, new LambdaQueryWrapper<IotAlarmRecordDO>()
|
||||
.eq(reqVO.getAlarmConfigId() != null, IotAlarmRecordDO::getAlarmConfigId, reqVO.getAlarmConfigId())
|
||||
.eq(reqVO.getSeverity() != null, IotAlarmRecordDO::getSeverity, reqVO.getSeverity())
|
||||
.eq(reqVO.getAckState() != null, IotAlarmRecordDO::getAckState, reqVO.getAckState())
|
||||
.eq(reqVO.getClearState() != null, IotAlarmRecordDO::getClearState, reqVO.getClearState())
|
||||
.eq(reqVO.getArchived() != null, IotAlarmRecordDO::getArchived, reqVO.getArchived())
|
||||
.eq(reqVO.getDeviceId() != null, IotAlarmRecordDO::getDeviceId, reqVO.getDeviceId())
|
||||
.eq(reqVO.getProductId() != null, IotAlarmRecordDO::getProductId, reqVO.getProductId())
|
||||
.eq(reqVO.getSubsystemId() != null, IotAlarmRecordDO::getSubsystemId, reqVO.getSubsystemId())
|
||||
.between(reqVO.getStartTs() != null && reqVO.getStartTs().length == 2,
|
||||
IotAlarmRecordDO::getStartTs,
|
||||
reqVO.getStartTs() != null && reqVO.getStartTs().length == 2 ? reqVO.getStartTs()[0] : null,
|
||||
reqVO.getStartTs() != null && reqVO.getStartTs().length == 2 ? reqVO.getStartTs()[1] : null)
|
||||
.orderByDesc(IotAlarmRecordDO::getId));
|
||||
}
|
||||
|
||||
/**
|
||||
* 按 ID 集合批量查询告警记录
|
||||
*
|
||||
* @param ids ID 集合
|
||||
* @return 告警记录列表
|
||||
*/
|
||||
default List<IotAlarmRecordDO> selectListByIds(Collection<Long> ids) {
|
||||
if (ids == null || ids.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
return selectList(new LambdaQueryWrapper<IotAlarmRecordDO>()
|
||||
.in(IotAlarmRecordDO::getId, ids));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -6,6 +6,8 @@ import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@@ -129,6 +131,35 @@ public class IotSubsystemDeviceCountRedisDAO {
|
||||
refreshTtlIfAbsent(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前租户所有子系统的设备数量(HGETALL)
|
||||
* <p>
|
||||
* B22 /device-count 接口使用,一次 HGETALL 拿全量。
|
||||
* 若 Key 不存在或 Hash 为空,返回空 Map(让调用方决定 fallback 策略)。
|
||||
*
|
||||
* @param tenantId 租户 ID
|
||||
* @return subsystemId → deviceCount 的 Map(空 Map 表示无数据)
|
||||
*/
|
||||
public Map<Long, Long> getAllCounts(Long tenantId) {
|
||||
String key = buildKey(tenantId);
|
||||
Map<Object, Object> raw = stringRedisTemplate.opsForHash().entries(key);
|
||||
if (raw == null || raw.isEmpty()) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
Map<Long, Long> result = new HashMap<>(raw.size());
|
||||
for (Map.Entry<Object, Object> entry : raw.entrySet()) {
|
||||
try {
|
||||
Long subsystemId = Long.parseLong(entry.getKey().toString());
|
||||
Long count = Long.parseLong(entry.getValue().toString());
|
||||
result.put(subsystemId, count);
|
||||
} catch (NumberFormatException e) {
|
||||
log.warn("[getAllCounts] 解析子系统设备计数失败,tenantId={}, key={}, value={}",
|
||||
tenantId, entry.getKey(), entry.getValue());
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除子系统计数 field(删除子系统时清理)
|
||||
*
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
package com.viewsh.module.iot.service.alarm;
|
||||
|
||||
import com.viewsh.framework.common.pojo.PageResult;
|
||||
import com.viewsh.module.iot.controller.admin.alarm.vo.AlarmRecordPageReqVO;
|
||||
import com.viewsh.module.iot.dal.dataobject.alarm.IotAlarmRecordDO;
|
||||
import com.viewsh.module.iot.service.alarm.dto.AlarmStateTransitionRequest;
|
||||
import com.viewsh.module.iot.service.alarm.dto.AlarmTriggerRequest;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* IoT 告警记录 Service(v2.0 正交状态机)
|
||||
* <p>
|
||||
@@ -59,4 +63,58 @@ public interface IotAlarmRecordService {
|
||||
*/
|
||||
IotAlarmRecordDO getAlarm(Long id);
|
||||
|
||||
// ==================== B20 新增:查询 / 批量 / remark ====================
|
||||
|
||||
/**
|
||||
* 分页查询告警记录
|
||||
*
|
||||
* @param reqVO 分页查询参数
|
||||
* @return 分页结果
|
||||
*/
|
||||
PageResult<IotAlarmRecordDO> getAlarmPage(AlarmRecordPageReqVO reqVO);
|
||||
|
||||
/**
|
||||
* 批量确认告警(遍历调 ackAlarm,幂等)
|
||||
*
|
||||
* @param request 含 ids 列表的请求(alarmId 字段依次填充)
|
||||
* @param ids 告警记录 ID 列表
|
||||
*/
|
||||
void batchAckAlarm(List<Long> ids, String operator, String remark);
|
||||
|
||||
/**
|
||||
* 批量清除告警(遍历调 clearAlarm,幂等)
|
||||
*
|
||||
* @param ids 告警记录 ID 列表
|
||||
* @param operator 操作人
|
||||
* @param remark 处理备注
|
||||
*/
|
||||
void batchClearAlarm(List<Long> ids, String operator, String remark);
|
||||
|
||||
/**
|
||||
* 批量归档告警(遍历调 archiveAlarm,每条独立捕获异常)
|
||||
* <p>
|
||||
* 已归档的条目跳过(不抛出);返回失败条数。
|
||||
*
|
||||
* @param ids 告警记录 ID 列表
|
||||
* @param operator 操作人
|
||||
* @return 失败条数(0 = 全部成功)
|
||||
*/
|
||||
int batchArchiveAlarm(List<Long> ids, String operator);
|
||||
|
||||
/**
|
||||
* 更新告警处理备注(不走锁,备注为附属信息)
|
||||
*
|
||||
* @param id 告警记录 ID
|
||||
* @param remark 处理备注
|
||||
*/
|
||||
void updateRemark(Long id, String remark);
|
||||
|
||||
/**
|
||||
* 查询告警历史(时序库)
|
||||
*
|
||||
* @param alarmRecordId 告警记录 ID
|
||||
* @return 历史列表(按时间倒序,TSDB 不可用时返回空列表)
|
||||
*/
|
||||
List<com.viewsh.module.iot.dal.dataobject.alarm.AlarmHistoryDO> listAlarmHistory(Long alarmRecordId);
|
||||
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package com.viewsh.module.iot.service.alarm;
|
||||
|
||||
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.dataobject.alarm.enums.AlarmSeverity;
|
||||
import com.viewsh.module.iot.dal.mysql.alarm.IotAlarmRecordMapper;
|
||||
@@ -15,6 +18,7 @@ import org.springframework.validation.annotation.Validated;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
import static com.viewsh.framework.common.exception.util.ServiceExceptionUtil.exception;
|
||||
import static com.viewsh.module.iot.enums.ErrorCodeConstants.*;
|
||||
@@ -62,6 +66,9 @@ public class IotAlarmRecordServiceImpl implements IotAlarmRecordService {
|
||||
@Resource
|
||||
private IotAlarmPropagationService alarmPropagationService;
|
||||
|
||||
@Resource
|
||||
private IotAlarmHistoryService alarmHistoryService;
|
||||
|
||||
// ==================== 触发(幂等 upsert) ====================
|
||||
|
||||
@Override
|
||||
@@ -268,6 +275,86 @@ public class IotAlarmRecordServiceImpl implements IotAlarmRecordService {
|
||||
return alarmRecordMapper.selectById(id);
|
||||
}
|
||||
|
||||
// ==================== B20 新增:查询 / 批量 / remark / history ====================
|
||||
|
||||
@Override
|
||||
public PageResult<IotAlarmRecordDO> getAlarmPage(AlarmRecordPageReqVO reqVO) {
|
||||
return alarmRecordMapper.selectPage(reqVO);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void batchAckAlarm(List<Long> ids, String operator, String remark) {
|
||||
if (ids == null || ids.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
for (Long id : ids) {
|
||||
try {
|
||||
ackAlarm(AlarmStateTransitionRequest.builder()
|
||||
.alarmId(id)
|
||||
.operator(operator)
|
||||
.remark(remark)
|
||||
.build());
|
||||
} catch (Exception e) {
|
||||
log.warn("[batchAckAlarm] id={} ack 失败,跳过: {}", id, e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void batchClearAlarm(List<Long> ids, String operator, String remark) {
|
||||
if (ids == null || ids.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
for (Long id : ids) {
|
||||
try {
|
||||
clearAlarm(AlarmStateTransitionRequest.builder()
|
||||
.alarmId(id)
|
||||
.operator(operator)
|
||||
.remark(remark)
|
||||
.build());
|
||||
} catch (Exception e) {
|
||||
log.warn("[batchClearAlarm] id={} clear 失败,跳过: {}", id, e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int batchArchiveAlarm(List<Long> ids, String operator) {
|
||||
if (ids == null || ids.isEmpty()) {
|
||||
return 0;
|
||||
}
|
||||
int failCount = 0;
|
||||
for (Long id : ids) {
|
||||
try {
|
||||
archiveAlarm(AlarmStateTransitionRequest.builder()
|
||||
.alarmId(id)
|
||||
.operator(operator)
|
||||
.build());
|
||||
} catch (Exception e) {
|
||||
// 已归档(ALARM_ALREADY_ARCHIVED)或不存在 → 计为失败
|
||||
log.warn("[batchArchiveAlarm] id={} archive 失败: {}", id, e.getMessage());
|
||||
failCount++;
|
||||
}
|
||||
}
|
||||
return failCount;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateRemark(Long id, String remark) {
|
||||
// 备注更新不走分布式锁(附属信息,无状态机约束)
|
||||
getAlarmOrThrow(id); // 校验存在性
|
||||
IotAlarmRecordDO update = new IotAlarmRecordDO();
|
||||
update.setId(id);
|
||||
update.setProcessRemark(remark);
|
||||
alarmRecordMapper.updateById(update);
|
||||
log.debug("[updateRemark] 告警备注更新 id={}", id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<AlarmHistoryDO> listAlarmHistory(Long alarmRecordId) {
|
||||
return alarmHistoryService.queryByAlarmRecord(alarmRecordId, null, null);
|
||||
}
|
||||
|
||||
// ==================== 内部工具 ====================
|
||||
|
||||
private IotAlarmRecordDO getAlarmOrThrow(Long id) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.viewsh.module.iot.service.subsystem;
|
||||
|
||||
import com.viewsh.framework.common.pojo.PageResult;
|
||||
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;
|
||||
@@ -9,6 +10,7 @@ import com.viewsh.module.iot.dal.dataobject.subsystem.IotSubsystemDO;
|
||||
import jakarta.validation.Valid;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* IoT 子系统 Service 接口
|
||||
@@ -83,4 +85,14 @@ public interface IotSubsystemService {
|
||||
*/
|
||||
IotSubsystemDeviceStatsRespVO getSubsystemDeviceStats(Long subsystemId);
|
||||
|
||||
/**
|
||||
* 获得当前租户所有子系统的设备计数(B22 /device-count 接口)
|
||||
* <p>
|
||||
* 一次 HGETALL 从 Redis 读取,Redis 未命中时返回空 Map(B10 评审 A6 策略:
|
||||
* 不做实时 DB count,DB 重建由定时任务/启动事件负责)。
|
||||
*
|
||||
* @return subsystemId → IotSubsystemDeviceCountRespVO(total/online/alarm)
|
||||
*/
|
||||
Map<Long, IotSubsystemDeviceCountRespVO> getAllSubsystemDeviceCount();
|
||||
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import com.viewsh.framework.common.pojo.PageResult;
|
||||
import com.viewsh.framework.common.util.object.BeanUtils;
|
||||
import com.viewsh.framework.tenant.core.context.TenantContextHolder;
|
||||
import com.viewsh.framework.tenant.core.util.TenantUtils;
|
||||
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,6 +22,7 @@ import org.springframework.stereotype.Service;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
@@ -171,6 +173,26 @@ public class IotSubsystemServiceImpl implements IotSubsystemService {
|
||||
return vo;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<Long, IotSubsystemDeviceCountRespVO> getAllSubsystemDeviceCount() {
|
||||
Long tenantId = TenantContextHolder.getTenantId();
|
||||
|
||||
// 一次 HGETALL 拿全量计数
|
||||
Map<Long, Long> countMap = deviceCountRedisDAO.getAllCounts(tenantId);
|
||||
|
||||
if (countMap.isEmpty()) {
|
||||
// Redis miss → 返回空 Map(B10 评审 A6:不做实时 DB count,由定时任务/启动事件重建)
|
||||
log.debug("[getAllSubsystemDeviceCount] Redis Hash 为空,tenantId={},返回空 Map", tenantId);
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
|
||||
// 转换为 VO(online/alarm 待 B12/B14 实现,当前返回 0)
|
||||
Map<Long, IotSubsystemDeviceCountRespVO> result = new HashMap<>(countMap.size());
|
||||
countMap.forEach((subsystemId, total) ->
|
||||
result.put(subsystemId, new IotSubsystemDeviceCountRespVO(total, 0L, 0L)));
|
||||
return result;
|
||||
}
|
||||
|
||||
// ==================== 启动时重建 Redis 计数(评审 A6)====================
|
||||
|
||||
/**
|
||||
|
||||
@@ -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