feat:新增设备地图、iot管理模块相关接口

master
HuangHuiKang 2 months ago
parent 6a4c55a4c0
commit 4671bdbcab

@ -11,6 +11,9 @@ public interface ErrorCodeConstants {
// ========== 管理客户页面 1-030-100-000 ==========
ErrorCode MANAGEMENT_NOT_EXISTS = new ErrorCode(1_030_100_000, "客户不存在");
ErrorCode MANAGEMENT_CODE_NOT_EXISTS = new ErrorCode(1_030_100_000, "客户编码不能为空");
ErrorCode MANAGEMENT_CUSTOMER_CODE_DUPLICATE = new ErrorCode(1_002_000_001, "客户编码已存在,请勿重复");
}

@ -1,5 +1,6 @@
package cn.iocoder.yudao.module.cus.service.management;
import cn.hutool.core.util.StrUtil;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import org.springframework.validation.annotation.Validated;
@ -12,8 +13,8 @@ import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.cus.dal.mysql.management.ManagementMapper;
import static cn.iocoder.yudao.module.cus.enums.ErrorCodeConstants.MANAGEMENT_NOT_EXISTS;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.cus.enums.ErrorCodeConstants.*;
/**
* Service
@ -29,6 +30,8 @@ public class ManagementServiceImpl implements ManagementService {
@Override
public Long createManagement(ManagementSaveReqVO createReqVO) {
//customerCode 唯一校验
validateCustomerCodeUnique(createReqVO.getCustomerCode(), null);
// 插入
ManagementDO management = BeanUtils.toBean(createReqVO, ManagementDO.class);
managementMapper.insert(management);
@ -36,6 +39,23 @@ public class ManagementServiceImpl implements ManagementService {
// 返回
return management.getId();
}
private void validateCustomerCodeUnique(String customerCode, Long id) {
if (StrUtil.isBlank(customerCode)) {
throw exception(MANAGEMENT_CODE_NOT_EXISTS);
}
ManagementDO exist = managementMapper.selectOne(
ManagementDO::getCustomerCode, customerCode
);
if (exist == null) {
return;
}
// update 场景排除自己create 时 id = null
if (id == null || !Objects.equals(exist.getId(), id)) {
throw exception(MANAGEMENT_CUSTOMER_CODE_DUPLICATE);
}
}
@Override
public void updateManagement(ManagementSaveReqVO updateReqVO) {

@ -101,6 +101,12 @@
<artifactId>spring-boot-starter-amqp</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-module-customer</artifactId>
<version>2026.01-jdk8-SNAPSHOT</version>
<scope>compile</scope>
</dependency>
</dependencies>
</project>

@ -8,6 +8,8 @@ import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
import cn.iocoder.yudao.module.infra.service.job.JobService;
import cn.iocoder.yudao.module.iot.controller.admin.device.vo.*;
//import cn.iocoder.yudao.module.iot.controller.admin.devicecontactmodel.vo.DeviceContactModelPageReqVO;
import cn.iocoder.yudao.module.iot.controller.admin.devicecontactmodel.vo.DeviceContactModelPageReqVO;
import cn.iocoder.yudao.module.iot.controller.admin.devicewarinningrecord.vo.DeviceWarinningRecordPageReqVO;
import cn.iocoder.yudao.module.iot.dal.dataobject.device.DeviceAttributeDO;
import cn.iocoder.yudao.module.iot.dal.dataobject.device.DeviceDO;
//import cn.iocoder.yudao.module.iot.dal.dataobject.devicecontactmodel.DeviceContactModelDO;
@ -120,10 +122,9 @@ public class DeviceController {
ExcelUtils.write(response, "物联设备.xls", "数据", DeviceRespVO.class,list);
}
@GetMapping("/deviceList")
// @PreAuthorize("@ss.hasPermission('iot:device:query')")
@PreAuthorize("@ss.hasPermission('iot:device:query')")
public CommonResult<List<DeviceRespVO>> deviceList(@Valid DevicePageReqVO pageReqVO) {
pageReqVO.setPageSize(PageParam.PAGE_SIZE_NONE);
List<DeviceRespVO> list = deviceService.getDevicePage(pageReqVO).getList();
List<DeviceRespVO> list = deviceService.deviceList(pageReqVO);
return success(list);
}
@ -240,11 +241,11 @@ public class DeviceController {
@GetMapping("/getDeviceOperationalStatus")
@Operation(summary = "获取首页设备运行状态")
@Operation(summary = "获取设备运行状态")
// @PreAuthorize("@ss.hasPermission('iot:device:query')")
@Parameter(name = "orgId", description = "产线组织Id")
public CommonResult<DeviceOperationStatusRespVO> getDeviceOperationalStatus(@RequestParam(name = "orgId",required = false) Long orgId) throws JsonProcessingException {
DeviceOperationStatusRespVO deviceOperationalStatus=deviceService.getDeviceOperationalStatus();
public CommonResult<DeviceOperationStatusRespVO> getDeviceOperationalStatus(LocalDateTime startTime, LocalDateTime endTime) throws JsonProcessingException {
DeviceOperationStatusRespVO deviceOperationalStatus=deviceService.getDeviceOperationalStatus(startTime,endTime);
return success(deviceOperationalStatus);
}
@ -350,4 +351,30 @@ public class DeviceController {
return success(true);
}
@GetMapping("/device-run-status-stats")
@Operation(summary = "查询设备总数及各运行状态数量")
@PreAuthorize("@ss.hasPermission('iot:org-node:query')")
public CommonResult<DeviceRunStatusStatsRespVO> getDeviceRunStatusStats(
@RequestParam(value = "customerId", required = false) Long customerId,
@RequestParam(value = "orgNodeId", required = false) Long orgNodeId) {
return success(deviceService.getDeviceRunStatusStats(customerId, orgNodeId));
}
@GetMapping("/run-status-stats-by-customer")
@Operation(summary = "按客户统计设备总数及运行状态数")
@Parameter(name = "customerId", description = "客户ID", required = true, example = "5")
@PreAuthorize("@ss.hasPermission('iot:device:query')")
public CommonResult<DeviceRunStatusStatsRespVO> getRunStatusStatsByCustomer(@RequestParam("customerId") Long customerId) {
return success(deviceService.getRunStatusStatsByCustomer(customerId));
}
@GetMapping("/status-count-by-customer")
@Operation(summary = "按客户统计设备状态数量")
@PreAuthorize("@ss.hasPermission('iot:device:query')")
public CommonResult<List<CustomerDeviceStatusStatsRespVO>> getStatusCountByCustomer() {
return success(deviceService.getStatusCountByCustomer());
}
}

@ -0,0 +1,9 @@
package cn.iocoder.yudao.module.iot.controller.admin.device.dto;
import lombok.Data;
@Data
public class DeviceLatestRuleDTO {
private Long deviceId;
private String rule;
}

@ -0,0 +1,26 @@
package cn.iocoder.yudao.module.iot.controller.admin.device.vo;
import lombok.Data;
import java.math.BigDecimal;
@Data
public class CustomerDeviceStatusStatsRespVO {
private Long customerId;
private String customerName;
private Long totalDeviceCount;
private Long offlineCount;
private Long runningCount;
private Long standbyCount;
private Long faultStandbyCount;
private Long alarmRunningCount;
/**
*
*/
private BigDecimal longitude;
/**
*
*/
private BigDecimal latitude;
}

@ -31,6 +31,9 @@ public class DeviceOperationStatusRespVO {
@Schema(description = "故障率")
private String faultRate;
@Schema(description = "报告统计数")
private Long warningRecordCount;
}

@ -127,4 +127,10 @@ public class DeviceRespVO {
@Schema(description = "客户组织节点Id")
private Long orgNodeId;
@Schema(description = "客户名称")
private String customerName;
@Schema(description = "组织名称")
private String orgNodeName;
}

@ -0,0 +1,15 @@
package cn.iocoder.yudao.module.iot.controller.admin.device.vo;
import lombok.Data;
@Data
public class DeviceRunStatusStatsRespVO {
private Long totalDeviceCount; // 设备总数
private Long offlineCount; // 0 离线
private Long runningCount; // 1 运行
private Long standbyCount; // 2 待机中
private Long faultStandbyCount; // 3 故障中
private Long alarmRunningCount; // 4 报警中
}

@ -0,0 +1,38 @@
package cn.iocoder.yudao.module.iot.controller.admin.device.vo;
import com.alibaba.excel.annotation.ExcelProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Schema(description = "管理后台 - 设备简要信息 Response VO")
@Data
public class DeviceSimpleRespVO {
@Schema(description = "设备ID", example = "170")
private Long id;
@Schema(description = "设备编号", example = "DEV-001")
private String deviceCode;
@Schema(description = "设备名称", example = "注塑机-01")
private String deviceName;
@Schema(description = "状态", example = "2")
private String status;
@Schema(description = "客户ID", example = "5")
private Long customerId;
@Schema(description = "组织节点ID", example = "12")
private Long orgNodeId;
@Schema(description = "通讯协议", example = "")
private String protocol;
@Schema(description = "运行状态")
private String operatingStatus;
@Schema(description = "是否启用")
private Boolean isEnable;
}

@ -6,9 +6,7 @@ import cn.iocoder.yudao.framework.common.pojo.PageParam;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
import cn.iocoder.yudao.module.iot.controller.admin.devicewarinningrecord.vo.DeviceWarinningRecordPageReqVO;
import cn.iocoder.yudao.module.iot.controller.admin.devicewarinningrecord.vo.DeviceWarinningRecordRespVO;
import cn.iocoder.yudao.module.iot.controller.admin.devicewarinningrecord.vo.DeviceWarinningRecordSaveReqVO;
import cn.iocoder.yudao.module.iot.controller.admin.devicewarinningrecord.vo.*;
import cn.iocoder.yudao.module.iot.dal.dataobject.devicewarinningrecord.DeviceWarinningRecordDO;
import cn.iocoder.yudao.module.iot.service.devicewarinningrecord.DeviceWarinningRecordService;
import io.swagger.v3.oas.annotations.Operation;
@ -73,9 +71,9 @@ public class DeviceWarinningRecordController {
@GetMapping("/page")
@Operation(summary = "获得告警记录分页")
@PreAuthorize("@ss.hasPermission('iot:device-warinning-record:query')")
public CommonResult<PageResult<DeviceWarinningRecordRespVO>> getDeviceWarinningRecordPage(@Valid DeviceWarinningRecordPageReqVO pageReqVO) {
PageResult<DeviceWarinningRecordDO> pageResult = deviceWarinningRecordService.getDeviceWarinningRecordPage(pageReqVO);
return success(BeanUtils.toBean(pageResult, DeviceWarinningRecordRespVO.class));
public CommonResult<PageResult<DeviceWarinningRecordPageRespVO>> getDeviceWarinningRecordPage(@Valid DeviceWarinningRecordPageReqVO pageReqVO) {
PageResult<DeviceWarinningRecordPageRespVO> pageResult = deviceWarinningRecordService.getDeviceWarinningRecordPage(pageReqVO);
return success(pageResult);
}
@GetMapping("/export-excel")
@ -85,22 +83,18 @@ public class DeviceWarinningRecordController {
public void exportDeviceWarinningRecordExcel(@Valid DeviceWarinningRecordPageReqVO pageReqVO,
HttpServletResponse response) throws IOException {
pageReqVO.setPageSize(PageParam.PAGE_SIZE_NONE);
List<DeviceWarinningRecordDO> list = deviceWarinningRecordService.getDeviceWarinningRecordPage(pageReqVO).getList();
List<DeviceWarinningRecordPageRespVO> list = deviceWarinningRecordService.getDeviceWarinningRecordPage(pageReqVO).getList();
// 导出 Excel
ExcelUtils.write(response, "告警记录.xls", "数据", DeviceWarinningRecordRespVO.class,
BeanUtils.toBean(list, DeviceWarinningRecordRespVO.class));
ExcelUtils.write(response, "告警记录.xls", "数据", DeviceWarinningRecordPageRespVO.class,
list);
}
@GetMapping("/getList")
@Operation(summary = "获得告警记录列表")
@Parameter(name = "deviceId", description = "设备Id", required = true, example = "1024")
@Parameter(name = "orgId", description = "产线组织Id", required = true, example = "1024")
@PreAuthorize("@ss.hasPermission('iot:device-warinning-record:query')")
public CommonResult<List<DeviceWarinningRecordDO>> getList(@RequestParam(name = "deviceId" ,required = false) Long id,
@RequestParam(name = "orgId" ,required = false) Long orgId) {
List<DeviceWarinningRecordDO> deviceWarinningRecord = deviceWarinningRecordService.getList(id);
return success(deviceWarinningRecord);
public CommonResult<List<DeviceWarningListRespVO>> getList(@Valid DeviceWarningListReqVO reqVO) {
return success(deviceWarinningRecordService.getList(reqVO));
}
@ -113,4 +107,20 @@ public class DeviceWarinningRecordController {
return success(hourCounts);
}
@GetMapping("/count")
@Operation(summary = "告警数量统计")
@PreAuthorize("@ss.hasPermission('iot:device-warinning-record:query')")
public CommonResult<DeviceWarningCountRespVO> count(@Valid DeviceWarningCountReqVO reqVO) {
return success(deviceWarinningRecordService.getWarningCount(reqVO));
}
@GetMapping("/trend")
@Operation(summary = "告警趋势(今日/本周/本月)")
@PreAuthorize("@ss.hasPermission('iot:device-warinning-record:query')")
public CommonResult<DeviceWarningTrendRespVO> trend(@Valid DeviceWarningTrendReqVO reqVO) {
return CommonResult.success(deviceWarinningRecordService.getWarningTrend(reqVO));
}
}

@ -1,12 +1,14 @@
package cn.iocoder.yudao.module.iot.controller.admin.devicewarinningrecord.vo;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import org.springframework.format.annotation.DateTimeFormat;
import javax.validation.constraints.NotNull;
import java.time.LocalDateTime;
import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
@ -32,9 +34,11 @@ public class DeviceWarinningRecordPageReqVO extends PageParam {
@Schema(description = "地址值")
private String addressValue;
@Schema(description = "创建时间")
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
private LocalDateTime[] createTime;
@NotNull(message = "开始时间不能为空")
private String startTime;
@NotNull(message = "结束时间不能为空")
private String endTime;
@Schema(description = "点位规则Id", example = "25946")
private Long ruleId;

@ -0,0 +1,20 @@
package cn.iocoder.yudao.module.iot.controller.admin.devicewarinningrecord.vo;
import lombok.Data;
@Data
public class DeviceWarinningRecordPageRespVO {
private Long id;
private Long deviceId;
private String alarmLevel;
private String alarmContent;
private String createTime;
private Integer deleted;
private String deviceName;
private String deviceCode;
private String customerCode;
private String customerName;
}

@ -0,0 +1,23 @@
package cn.iocoder.yudao.module.iot.controller.admin.devicewarinningrecord.vo;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import javax.validation.constraints.NotNull;
import java.time.LocalDateTime;
// DeviceWarningCountReqVO.java
@Data
public class DeviceWarningCountReqVO {
@NotNull(message = "开始时间不能为空")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime startTime;
@NotNull(message = "结束时间不能为空")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime endTime;
}

@ -0,0 +1,19 @@
package cn.iocoder.yudao.module.iot.controller.admin.devicewarinningrecord.vo;
import lombok.Data;
// DeviceWarningCountRespVO.java
@Data
public class DeviceWarningCountRespVO {
private Long totalCount;
// alarmLevel = 1
private Long normalCount;
// alarmLevel = 2
private Long tipCount;
// alarmLevel = 0
private Long seriousCount;
}

@ -0,0 +1,21 @@
package cn.iocoder.yudao.module.iot.controller.admin.devicewarinningrecord.vo;
import lombok.Data;
import javax.validation.constraints.NotNull;
import java.time.LocalDateTime;
@Data
public class DeviceWarningListReqVO {
private Long deviceId; // 可选
private Long orgId; // 可选(先保留)
@NotNull(message = "开始时间不能为空")
private LocalDateTime startTime;
@NotNull(message = "结束时间不能为空")
private LocalDateTime endTime;
private String alarmLevel; // 可选
}

@ -0,0 +1,24 @@
package cn.iocoder.yudao.module.iot.controller.admin.devicewarinningrecord.vo;
import lombok.Data;
import java.time.LocalDateTime;
@Data
public class DeviceWarningListRespVO {
private Long id;
private Long deviceId;
private Long modelId;
private String rule;
private String alarmLevel;
private String addressValue;
private Long ruleId;
private String deviceName;
private String modelName;
private String ruleName;
private LocalDateTime createTime;
// 新增
private String customerName;
}

@ -0,0 +1,25 @@
package cn.iocoder.yudao.module.iot.controller.admin.devicewarinningrecord.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import javax.validation.constraints.NotNull;
import java.time.LocalDateTime;
import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
@Data
@Schema(description = "管理后台 - 设备告警趋势 Request VO")
public class DeviceWarningTrendReqVO {
@NotNull(message = "开始时间不能为空")
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
@Schema(description = "开始时间", requiredMode = Schema.RequiredMode.REQUIRED, example = "2026-03-01 00:00:00")
private LocalDateTime startTime;
@NotNull(message = "结束时间不能为空")
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
@Schema(description = "结束时间", requiredMode = Schema.RequiredMode.REQUIRED, example = "2026-03-31 23:59:59")
private LocalDateTime endTime;
}

@ -0,0 +1,16 @@
package cn.iocoder.yudao.module.iot.controller.admin.devicewarinningrecord.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.List;
@Data
public class DeviceWarningTrendRespVO {
@Schema(description = "X轴时间点")
private List<String> timePoints;
@Schema(description = "Y轴告警总数")
private List<Long> counts;
}

@ -1,5 +1,6 @@
package cn.iocoder.yudao.module.iot.controller.admin.orgnode;
import cn.iocoder.yudao.module.iot.controller.admin.device.vo.DeviceSimpleRespVO;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import org.springframework.validation.annotation.Validated;
@ -107,4 +108,12 @@ public class OrgNodeController {
return CommonResult.success(orgNodeService.getOrgTree(customerId));
}
@GetMapping("/device-list-by-node")
@Operation(summary = "按树节点获取设备列表")
public CommonResult<List<DeviceSimpleRespVO>> getDeviceListByNode(@RequestParam("nodeId") Long nodeId,
@RequestParam("nodeType") Integer nodeType) {
return success(orgNodeService.getDeviceListByNode(nodeId, nodeType));
}
}

@ -29,7 +29,7 @@ public class OrgNodeSaveReqVO {
private String name;
@Schema(description = "排序字段,数值越小越靠前", requiredMode = Schema.RequiredMode.REQUIRED)
@NotNull(message = "排序字段,数值越小越靠前不能为空")
// @NotNull(message = "排序字段,数值越小越靠前不能为空")
private Integer sort;
}

@ -8,7 +8,14 @@ import java.util.List;
public class OrgNodeTreeRespVO {
private Long id;
private Long parentId;
private Integer nodeType; // 1客户 2车间 3产线
private Integer nodeType; // 1客户 2车间 3产线 4设备
private String name;
private List<OrgNodeTreeRespVO> children;
private String operateStatus;
// 全局唯一节点标识
private String nodeKey; // C_5 / O_12 / D_170
private String parentKey; // 0 / C_5 / O_12
private String deviceCode; //设备编码
}

@ -0,0 +1,10 @@
package cn.iocoder.yudao.module.iot.dal.dataobject.devicewarinningrecord;
import lombok.Data;
@Data
public class WarningTrendPointRespDTO {
private String timeKey;
private Long count;
}

@ -127,4 +127,21 @@ public interface DeviceMapper extends BaseMapperX<DeviceDO> {
Integer getTotalDeviceCount();
List<Long> getAllDeviceIds();
List<DeviceDO> selectByTenantCustomerAndOrgNodeIds(@Param("tenantId") Long tenantId,
@Param("customerId") Long customerId,
@Param("orgNodeIds") List<Long> orgNodeIds);
List<DeviceDO> selectByCustomerAndOrgNodeIds(@Param("customerId") Long customerId,
@Param("orgNodeIds") List<Long> orgNodeIds);
List<DeviceDO> selectByOrgNodeIds(@Param("orgNodeIds") List<Long> orgNodeIds);
List<DeviceDO> selectByCustomerId(@Param("customerId") Long customerId);
boolean existsByOrgNodeId(@Param("orgNodeId") Long orgNodeId);
}

@ -17,7 +17,7 @@ public interface DevicePointRulesMapper extends BaseMapperX<DevicePointRulesDO>
default PageResult<DevicePointRulesDO> selectPage(DevicePointRulesPageReqVO reqVO) {
return selectPage(reqVO, new LambdaQueryWrapperX<DevicePointRulesDO>()
.eqIfPresent(DevicePointRulesDO::getIdentifier, reqVO.getIdentifier())
.likeIfPresent(DevicePointRulesDO::getIdentifier, reqVO.getIdentifier())
.likeIfPresent(DevicePointRulesDO::getFieldName, reqVO.getFieldName())
.eqIfPresent(DevicePointRulesDO::getFieldRule, reqVO.getFieldRule())
.eqIfPresent(DevicePointRulesDO::getDefaultValue, reqVO.getDefaultValue())

@ -25,7 +25,7 @@ public interface DeviceWarinningRecordMapper extends BaseMapperX<DeviceWarinning
.eqIfPresent(DeviceWarinningRecordDO::getRule, reqVO.getRule())
.eqIfPresent(DeviceWarinningRecordDO::getAlarmLevel, reqVO.getAlarmLevel())
.eqIfPresent(DeviceWarinningRecordDO::getAddressValue, reqVO.getAddressValue())
.betweenIfPresent(DeviceWarinningRecordDO::getCreateTime, reqVO.getCreateTime())
.betweenIfPresent(DeviceWarinningRecordDO::getCreateTime, reqVO.getStartTime(), reqVO.getEndTime())
.eqIfPresent(DeviceWarinningRecordDO::getRuleId, reqVO.getRuleId())
.orderByDesc(DeviceWarinningRecordDO::getId));
}

@ -31,4 +31,20 @@ public interface OrgNodeMapper extends BaseMapperX<OrgNodeDO> {
List<OrgNodeDO> selectListByTenantAndCustomer(@Param("tenantId") Long tenantId,
@Param("customerId") Long customerId);
default List<OrgNodeDO> selectListByCustomer(Long customerId) {
return selectList(new LambdaQueryWrapperX<OrgNodeDO>()
.eq(OrgNodeDO::getCustomerId, customerId)
.eq(OrgNodeDO::getDeleted, false)
.orderByAsc(OrgNodeDO::getSort)
.orderByAsc(OrgNodeDO::getId));
}
List<OrgNodeDO> selectListByCustomerByNodeId(@Param("nodeId") Long nodeId);
boolean existsByParentId(@Param("parentId") Long parentId);
boolean existsByOrgNodeId(@Param("orgNodeId") Long orgNodeId);
}

@ -0,0 +1,46 @@
package cn.iocoder.yudao.module.iot.dal.tdengine;
import cn.iocoder.yudao.module.iot.controller.admin.device.dto.DeviceLatestRuleDTO;
import cn.iocoder.yudao.module.iot.dal.dataobject.devicewarinningrecord.WarningTrendPointRespDTO;
import cn.iocoder.yudao.module.iot.controller.admin.devicewarinningrecord.vo.DeviceWarningCountRespVO;
import cn.iocoder.yudao.module.iot.dal.dataobject.devicewarinningrecord.DeviceWarinningRecordDO;
import cn.iocoder.yudao.module.iot.framework.tdengine.core.TDengineTableField;
import cn.iocoder.yudao.module.iot.framework.tdengine.core.annotation.TDengineDS;
import com.baomidou.mybatisplus.annotation.InterceptorIgnore;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.time.LocalDateTime;
import java.util.List;
@Mapper
@TDengineDS
@InterceptorIgnore(tenantLine = "true") // 避免 SQL 解析,因为 JSqlParser 对 TDengine 的 SQL 解析会报错
public interface IotDeviceMapper {
void alterProductPropertySTableDropField(@Param("productId") Long productId,
@Param("field") TDengineTableField field);
Long countWarningRecordByTime(@Param("startTime") LocalDateTime startTime,
@Param("endTime") LocalDateTime endTime);
List<DeviceWarinningRecordDO> selectWarningRecordList(@Param("deviceId") Long deviceId,
@Param("startTime") LocalDateTime startTime,
@Param("endTime") LocalDateTime endTime,
@Param("alarmLevel") String alarmLevel);
// IotDeviceMapper.java (TDengine mapper)
DeviceWarningCountRespVO selectWarningCount(@Param("startTime") String startTime,
@Param("endTime") String endTime);
List<WarningTrendPointRespDTO> selectWarningTrendByHour(@Param("startTime") String startTime,
@Param("endTime") String endTime);
List<WarningTrendPointRespDTO> selectWarningTrendByDay(@Param("startTime") String startTime,
@Param("endTime") String endTime);
List<DeviceLatestRuleDTO> selectLatestRuleByDeviceIds(@Param("deviceIds") List<Long> deviceIds);
}

@ -1,81 +0,0 @@
package cn.iocoder.yudao.module.iot.dal.tdengine;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.module.iot.framework.tdengine.core.TDengineTableField;
import cn.iocoder.yudao.module.iot.framework.tdengine.core.annotation.TDengineDS;
import com.baomidou.mybatisplus.annotation.InterceptorIgnore;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@Mapper
@TDengineDS
@InterceptorIgnore(tenantLine = "true") // 避免 SQL 解析,因为 JSqlParser 对 TDengine 的 SQL 解析会报错
public interface IotDevicePropertyMapper {
List<TDengineTableField> getProductPropertySTableFieldList(@Param("productId") Long productId);
void createProductPropertySTable(@Param("productId") Long productId,
@Param("fields") List<TDengineTableField> fields);
@SuppressWarnings("SimplifyStreamApiCallChains") // 保持 JDK8 兼容性
default void alterProductPropertySTable(Long productId,
List<TDengineTableField> oldFields,
List<TDengineTableField> newFields) {
oldFields.removeIf(field -> StrUtil.equalsAny(field.getField(),
TDengineTableField.FIELD_TS, "report_time", "device_id"));
List<TDengineTableField> addFields = newFields.stream().filter( // 新增的字段
newField -> oldFields.stream().noneMatch(oldField -> oldField.getField().equals(newField.getField())))
.collect(Collectors.toList());
List<TDengineTableField> dropFields = oldFields.stream().filter( // 删除的字段
oldField -> newFields.stream().noneMatch(n -> n.getField().equals(oldField.getField())))
.collect(Collectors.toList());
List<TDengineTableField> modifyTypeFields = new ArrayList<>(); // 变更类型的字段
List<TDengineTableField> modifyLengthFields = new ArrayList<>(); // 变更长度的字段
newFields.forEach(newField -> {
TDengineTableField oldField = CollUtil.findOne(oldFields, field -> field.getField().equals(newField.getField()));
if (oldField == null) {
return;
}
if (ObjectUtil.notEqual(oldField.getType(), newField.getType())) {
modifyTypeFields.add(newField);
return;
}
if (newField.getLength() != null) {
if (newField.getLength() > oldField.getLength()) {
modifyLengthFields.add(newField);
} else if (newField.getLength() < oldField.getLength()) {
// 特殊TDengine 长度修改时,只允许变长,所以此时认为是修改类型
modifyTypeFields.add(newField);
}
}
});
// 执行
addFields.forEach(field -> alterProductPropertySTableAddField(productId, field));
dropFields.forEach(field -> alterProductPropertySTableDropField(productId, field));
modifyLengthFields.forEach(field -> alterProductPropertySTableModifyField(productId, field));
modifyTypeFields.forEach(field -> {
alterProductPropertySTableDropField(productId, field);
alterProductPropertySTableAddField(productId, field);
});
}
void alterProductPropertySTableAddField(@Param("productId") Long productId,
@Param("field") TDengineTableField field);
void alterProductPropertySTableModifyField(@Param("productId") Long productId,
@Param("field") TDengineTableField field);
void alterProductPropertySTableDropField(@Param("productId") Long productId,
@Param("field") TDengineTableField field);
}

@ -9,34 +9,11 @@ import cn.iocoder.yudao.framework.common.exception.ErrorCode;
*/
public interface ErrorCodeConstants {
// ========== 产品相关 1-050-001-000 ============
ErrorCode PRODUCT_NOT_EXISTS = new ErrorCode(1_050_001_000, "产品不存在");
ErrorCode PRODUCT_KEY_EXISTS = new ErrorCode(1_050_001_001, "产品标识已经存在");
ErrorCode PRODUCT_STATUS_NOT_DELETE = new ErrorCode(1_050_001_002, "产品状是发布状态,不允许删除");
ErrorCode PRODUCT_STATUS_NOT_ALLOW_THING_MODEL = new ErrorCode(1_050_001_003, "产品状是发布状态,不允许操作物模型");
ErrorCode PRODUCT_DELETE_FAIL_HAS_DEVICE = new ErrorCode(1_050_001_004, "产品下存在设备,不允许删除");
// ========== 产品物模型 1-050-002-000 ============
ErrorCode THING_MODEL_NOT_EXISTS = new ErrorCode(1_050_002_000, "产品物模型不存在");
ErrorCode THING_MODEL_EXISTS_BY_PRODUCT_KEY = new ErrorCode(1_050_002_001, "ProductKey 对应的产品物模型已存在");
ErrorCode THING_MODEL_IDENTIFIER_EXISTS = new ErrorCode(1_050_002_002, "存在重复的功能标识符。");
ErrorCode THING_MODEL_NAME_EXISTS = new ErrorCode(1_050_002_003, "存在重复的功能名称。");
ErrorCode THING_MODEL_IDENTIFIER_INVALID = new ErrorCode(1_050_002_003, "产品物模型标识无效");
// ========== 设备 1-050-003-000 ============
ErrorCode DEVICE_NOT_EXISTS = new ErrorCode(1_050_003_000, "设备不存在");
ErrorCode DEVICE_NAME_EXISTS = new ErrorCode(1_050_003_001, "设备名称在同一产品下必须唯一");
ErrorCode DEVICE_GATEWAY_HAS_SUB = new ErrorCode(1_050_003_002, "网关设备存在已绑定的子设备,不允许删除");
ErrorCode GATEWAY_NOT_EXISTS = new ErrorCode(1_003_000_000, "网关不存在");
ErrorCode DEVICE_KEY_EXISTS = new ErrorCode(1_050_003_003, "设备标识已经存在");
ErrorCode DEVICE_GATEWAY_NOT_EXISTS = new ErrorCode(1_050_003_004, "网关设备不存在");
ErrorCode DEVICE_NOT_GATEWAY = new ErrorCode(1_050_003_005, "设备不是网关设备");
ErrorCode DEVICE_IMPORT_LIST_IS_EMPTY = new ErrorCode(1_050_003_006, "导入设备数据不能为空!");
ErrorCode DEVICE_DOWNSTREAM_FAILED_SERVER_ID_NULL = new ErrorCode(1_050_003_007, "下行设备消息失败,原因:设备未连接网关");
ErrorCode DEVICE_SERIAL_NUMBER_EXISTS = new ErrorCode(1_050_003_008, "设备序列号已存在,序列号必须全局唯一");
ErrorCode DEVICE_NOT_GATEWAY_SUB = new ErrorCode(1_050_003_009, "设备【{}/{}】不是网关子设备类型,无法绑定到网关");
ErrorCode DEVICE_GATEWAY_BINDTO_EXISTS = new ErrorCode(1_050_003_010, "设备【{}/{}】已绑定到其他网关,请先解绑");
ErrorCode DEVICE_CONTACT_MODEL_NOT_EXISTS = new ErrorCode(1_050_003_011, "查询不到该点位");
ErrorCode DEVICE_CONTACT_MODEL_NOT_EXISTS = new ErrorCode(1_050_003_011, "查询不到该点位");
ErrorCode DEVICE_MODEL_POINT_CODE_EXISTS = new ErrorCode(1_003_000_005, "采集设备采集点位编码已存在");
ErrorCode DEVICE_MODEL_ATTRIBUTE_POTIN_CODE_EXISTS = new ErrorCode(1_003_000_005, "采集设备模型点位编码已存在");
ErrorCode DEVICE_MODEL_ATTRIBUTE_NOT_EXISTS = new ErrorCode(1_003_000_005, "采集设备模型点位不存在");
@ -57,75 +34,14 @@ public interface ErrorCodeConstants {
ErrorCode DEVICE_ID_MODEL_NOT_EXISTS = new ErrorCode(1_003_000_003, "该设备模型ID不能为空");
ErrorCode DEVICE_DOES_NOT_EXIST= new ErrorCode(1_003_000_010, "该采集设备不存在");
ErrorCode DEVICE_MQTT_TOPIC_EXIST = new ErrorCode(1_003_000_000, "设备MQTT主题不存在。");
ErrorCode DEVICE_MODEL_CODE_EXISTS = new ErrorCode(1_003_000_002, "采集设备模型编码已存在");
ErrorCode DEVICE_WARNING_TIME_REQUIRED = new ErrorCode(1_003_000_002, "开始时间和结束时间不能为空");
ErrorCode ORG_NODE_CUSTOMER_ID_REQUIRED = new ErrorCode(1_003_000_002, "客户节点不能为空");
ErrorCode DEVICE_WARNING_TIME_RANGE_INVALID = new ErrorCode(1_003_000_003, "开始时间不能大于结束时间");
ErrorCode ORG_NODE_DELETE_HAS_CHILDREN = new ErrorCode(1_003_001_001, "当前节点下存在子节点,请先删除子节点");
ErrorCode ORG_NODE_DELETE_HAS_DEVICES = new ErrorCode(1_003_001_002, "当前节点下存在设备,请先解绑或删除设备");
// 拓扑管理相关错误码 1-050-003-100
ErrorCode DEVICE_TOPO_PARAMS_INVALID = new ErrorCode(1_050_003_100, "拓扑管理参数无效");
ErrorCode DEVICE_TOPO_SUB_DEVICE_USERNAME_INVALID = new ErrorCode(1_050_003_101, "子设备用户名格式无效");
ErrorCode DEVICE_TOPO_SUB_DEVICE_AUTH_FAILED = new ErrorCode(1_050_003_102, "子设备认证失败");
ErrorCode DEVICE_TOPO_SUB_NOT_BINDTO_GATEWAY = new ErrorCode(1_050_003_103, "子设备【{}/{}】未绑定到该网关");
// 设备注册相关错误码 1-050-003-200
ErrorCode DEVICE_SUB_REGISTER_PARAMS_INVALID = new ErrorCode(1_050_003_200, "子设备注册参数无效");
ErrorCode DEVICE_SUB_REGISTER_PRODUCT_NOT_GATEWAY_SUB = new ErrorCode(1_050_003_201, "产品【{}】不是网关子设备类型");
ErrorCode DEVICE_REGISTER_DISABLED = new ErrorCode(1_050_003_210, "该产品未开启动态注册功能");
ErrorCode DEVICE_REGISTER_SECRET_INVALID = new ErrorCode(1_050_003_211, "产品密钥验证失败");
ErrorCode DEVICE_REGISTER_ALREADY_EXISTS = new ErrorCode(1_050_003_212, "设备已存在,不允许重复注册");
// ========== 产品分类 1-050-004-000 ==========
ErrorCode PRODUCT_CATEGORY_NOT_EXISTS = new ErrorCode(1_050_004_000, "产品分类不存在");
// ========== 设备分组 1-050-005-000 ==========
ErrorCode DEVICE_GROUP_NOT_EXISTS = new ErrorCode(1_050_005_000, "设备分组不存在");
ErrorCode DEVICE_GROUP_DELETE_FAIL_DEVICE_EXISTS = new ErrorCode(1_050_005_001, "设备分组下存在设备,不允许删除");
// ========== 设备 Modbus 配置 1-050-006-000 ==========
ErrorCode DEVICE_MODBUS_CONFIG_NOT_EXISTS = new ErrorCode(1_050_006_000, "设备 Modbus 连接配置不存在");
ErrorCode DEVICE_MODBUS_CONFIG_EXISTS = new ErrorCode(1_050_006_001, "设备 Modbus 连接配置已存在");
// ========== 设备 Modbus 点位 1-050-007-000 ==========
ErrorCode DEVICE_MODBUS_POINT_NOT_EXISTS = new ErrorCode(1_050_007_000, "设备 Modbus 点位配置不存在");
ErrorCode DEVICE_MODBUS_POINT_EXISTS = new ErrorCode(1_050_007_001, "设备 Modbus 点位配置已存在");
// ========== OTA 固件相关 1-050-008-000 ==========
ErrorCode OTA_FIRMWARE_NOT_EXISTS = new ErrorCode(1_050_008_000, "固件信息不存在");
ErrorCode OTA_FIRMWARE_PRODUCT_VERSION_DUPLICATE = new ErrorCode(1_050_008_001, "产品版本号重复");
// ========== OTA 升级任务相关 1-050-008-100 ==========
ErrorCode OTA_TASK_NOT_EXISTS = new ErrorCode(1_050_008_100, "升级任务不存在");
ErrorCode OTA_TASK_CREATE_FAIL_NAME_DUPLICATE = new ErrorCode(1_050_008_101, "创建 OTA 任务失败,原因:任务名称重复");
ErrorCode OTA_TASK_CREATE_FAIL_DEVICE_FIRMWARE_EXISTS = new ErrorCode(1_050_008_102,
"创建 OTA 任务失败,原因:设备({})已经是该固件版本");
ErrorCode OTA_TASK_CREATE_FAIL_DEVICE_OTA_IN_PROCESS = new ErrorCode(1_050_008_102,
"创建 OTA 任务失败,原因:设备({})已经在升级中...");
ErrorCode OTA_TASK_CREATE_FAIL_DEVICE_EMPTY = new ErrorCode(1_050_008_103, "创建 OTA 任务失败,原因:没有可升级的设备");
ErrorCode OTA_TASK_CANCEL_FAIL_STATUS_END = new ErrorCode(1_050_008_104, "取消 OTA 任务失败,原因:任务状态不是进行中");
// ========== OTA 升级任务记录相关 1-050-008-200 ==========
ErrorCode OTA_TASK_RECORD_NOT_EXISTS = new ErrorCode(1_050_008_200, "升级记录不存在");
ErrorCode OTA_TASK_RECORD_CANCEL_FAIL_STATUS_ERROR = new ErrorCode(1_050_008_201, "取消 OTA 升级记录失败,原因:记录状态不是进行中");
ErrorCode OTA_TASK_RECORD_UPDATE_PROGRESS_FAIL_NO_EXISTS = new ErrorCode(1_050_008_202, "更新 OTA 升级记录进度失败,原因:该设备没有进行中的升级记录");
// ========== IoT 数据流转规则 1-050-010-000 ==========
ErrorCode DATA_RULE_NOT_EXISTS = new ErrorCode(1_050_010_000, "数据流转规则不存在");
ErrorCode DATA_RULE_NAME_EXISTS = new ErrorCode(1_050_010_001, "数据流转规则名称已存在");
// ========== IoT 数据流转目的 1-050-011-000 ==========
ErrorCode DATA_SINK_NOT_EXISTS = new ErrorCode(1_050_011_000, "数据桥梁不存在");
ErrorCode DATA_SINK_DELETE_FAIL_USED_BY_RULE = new ErrorCode(1_050_011_001, "数据流转目的正在被数据流转规则使用,无法删除");
ErrorCode DATA_SINK_NAME_EXISTS = new ErrorCode(1_050_011_002, "数据流转目的名称已存在");
// ========== IoT 场景联动 1-050-012-000 ==========
ErrorCode RULE_SCENE_NOT_EXISTS = new ErrorCode(1_050_012_000, "场景联动不存在");
// ========== IoT 告警配置 1-050-013-000 ==========
ErrorCode ALERT_CONFIG_NOT_EXISTS = new ErrorCode(1_050_013_000, "IoT 告警配置不存在");
// ========== IoT 告警记录 1-050-014-000 ==========
ErrorCode ALERT_RECORD_NOT_EXISTS = new ErrorCode(1_050_014_000, "IoT 告警记录不存在");
// ======================================= Tdengine ============================================
ErrorCode TABLE_CREATION_FAILED = new ErrorCode(1_004_000_008, "TDengine 表创建失败");
ErrorCode COLOUMN_CREATION_FAILED = new ErrorCode(1_004_000_008, "TDengine 列创建失败");

@ -0,0 +1,411 @@
package cn.iocoder.yudao.module.iot.job;
import cn.iocoder.yudao.framework.quartz.core.handler.JobHandler;
import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder;
import cn.iocoder.yudao.module.iot.controller.admin.device.enums.DeviceStatusEnum;
import cn.iocoder.yudao.module.iot.controller.admin.devicemodelrules.vo.PointRulesRespVO;
import cn.iocoder.yudao.module.iot.dal.dataobject.device.DeviceDO;
import cn.iocoder.yudao.module.iot.dal.dataobject.devicecontactmodel.DeviceContactModelDO;
import cn.iocoder.yudao.module.iot.dal.dataobject.deviceoperationrecord.DeviceOperationRecordDO;
import cn.iocoder.yudao.module.iot.dal.dataobject.devicepointrules.DevicePointRulesDO;
import cn.iocoder.yudao.module.iot.dal.mysql.device.DeviceMapper;
import cn.iocoder.yudao.module.iot.dal.mysql.devicecontactmodel.DeviceContactModelMapper;
import cn.iocoder.yudao.module.iot.dal.mysql.deviceoperationrecord.DeviceOperationRecordMapper;
import cn.iocoder.yudao.module.iot.dal.mysql.devicepointrules.DevicePointRulesMapper;
import cn.iocoder.yudao.module.iot.service.device.TDengineService;
import com.alibaba.fastjson.JSON;
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.sql.Timestamp;
import java.time.Duration;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@Slf4j
@Component
public class DeviceJob implements JobHandler {
@Resource
private TDengineService tDengineService;
@Resource
private DeviceMapper deviceMapper;
@Resource
private DeviceContactModelMapper deviceContactModelMapper;
@Resource
private DeviceOperationRecordMapper deviceOperationRecordMapper;
@Resource
private DevicePointRulesMapper devicePointRulesMapper;
@Override
public String execute(String param) throws Exception {
// 设置租户上下文
TenantContextHolder.setTenantId(1L);
// 解析超时时间默认60秒
long timeoutSeconds = 60L;
if (StringUtils.isNotBlank(param)) {
try {
timeoutSeconds = Long.parseLong(param);
} catch (NumberFormatException e) {
log.warn("定时任务参数非法使用默认60秒 param={}", param);
}
}
log.info("定时任务开始, timeoutSeconds={} 时间={}", timeoutSeconds, new Date());
// 查询采集设备列表
List<DeviceDO> deviceDOS =
deviceMapper.selectList(Wrappers.<DeviceDO>lambdaQuery().orderByDesc(DeviceDO::getId));
if (CollectionUtils.isEmpty(deviceDOS)) {
return param;
}
List<Long> deviceIds = deviceDOS.stream()
.map(DeviceDO::getId)
.collect(Collectors.toList());
// 获取设备的每条最新数据
Map<Long, Map<String, Object>> deviceRowMap =
tDengineService.queryDevicesLatestRow(deviceIds, null, null);
Instant now = Instant.now();
// 遍历设备
for (Long deviceId : deviceIds) {
Map<String, Object> row = deviceRowMap.get(deviceId);
boolean isTimeout = false;
if (row == null || row.get("ts") == null) {
isTimeout = true;
} else {
Instant ts = parseTs(row.get("ts"), deviceId);
if (ts == null || Duration.between(ts, now).getSeconds() > timeoutSeconds) {
isTimeout = true;
}
}
if (isTimeout) {
handleDeviceTimeout(deviceId);
} else {
handleDeviceOnline(deviceId, row);
}
}
return param;
}
/**
* ts Instant
*/
private Instant parseTs(Object tsObj, Long deviceId) {
if (tsObj == null) return null;
log.debug("设备 {} tsObj 类型: {}, 值: {}", deviceId, tsObj.getClass().getName(), tsObj);
if (tsObj instanceof Instant) {
return (Instant) tsObj;
} else if (tsObj instanceof Timestamp) {
return ((Timestamp) tsObj).toInstant();
} else if (tsObj instanceof Date) {
return ((Date) tsObj).toInstant();
} else if (tsObj instanceof LocalDateTime) {
return ((LocalDateTime) tsObj).atZone(ZoneId.systemDefault()).toInstant();
} else if (tsObj instanceof String) {
String tsStr = (String) tsObj;
try {
return Instant.parse(tsStr); // ISO 8601
} catch (Exception e1) {
try {
return Timestamp.valueOf(tsStr).toInstant(); // yyyy-MM-dd HH:mm:ss
} catch (Exception e2) {
log.warn("设备 {} ts 字符串解析失败: {}", deviceId, tsStr);
}
}
} else {
log.warn("设备 {} ts 类型未知: {}", deviceId, tsObj);
}
return null;
}
/**
* 线
*/
private void handleDeviceOnline(Long deviceId, Map<String, Object> row) {
if (row == null) return;
// 1. 查询设备规则
DevicePointRulesDO pointRulesDO = devicePointRulesMapper.selectOne(
Wrappers.<DevicePointRulesDO>lambdaQuery()
.eq(DevicePointRulesDO::getDeviceId, deviceId)
.eq(DevicePointRulesDO::getIdentifier, "RUNNING")
.orderByDesc(DevicePointRulesDO::getId)
.last("LIMIT 1")
);
if(pointRulesDO == null || StringUtils.isBlank(pointRulesDO.getFieldRule())){
//处理待机中
DeviceOperationRecordDO record = new DeviceOperationRecordDO();
record.setDeviceId(deviceId);
record.setRule(DeviceStatusEnum.STANDBY.getCode());
record.setCreator("1");
record.setUpdater("1");
// deviceOperationRecordMapper.insert(record);
tDengineService.insertDeviceOperationRecord(record);
return;
}
// 解析规则列表
List<PointRulesRespVO> pointRulesVOList = JSON.parseArray(
pointRulesDO.getFieldRule(), PointRulesRespVO.class
);
if (CollectionUtils.isEmpty(pointRulesVOList)) return;
// 2. 查询设备 contact model
List<DeviceContactModelDO> deviceContactModelDOS = deviceContactModelMapper.selectList(
Wrappers.<DeviceContactModelDO>lambdaQuery().eq(DeviceContactModelDO::getDeviceId, deviceId)
);
if (CollectionUtils.isEmpty(deviceContactModelDOS)) return;
// 3. 遍历规则,匹配成功则保存记录
for (PointRulesRespVO pointRule : pointRulesVOList) {
if (StringUtils.isBlank(pointRule.getCode())) continue;
String ruleCode = pointRule.getCode().toLowerCase();
String processedValue = row.get(ruleCode).toString();
boolean matched = matchRule(processedValue, pointRule);
if (!matched) {
log.debug("规则匹配失败: device={}, value={}, rule={}", deviceId, processedValue, JSON.toJSONString(pointRule));
continue;
}
log.info("规则匹配成功: device={}, value={}, rule={}", deviceId, processedValue, JSON.toJSONString(pointRule));
// 4. 遍历 contact model 查找对应 code
DeviceContactModelDO matchedContact = null;
for (DeviceContactModelDO contact : deviceContactModelDOS) {
if (ruleCode.equalsIgnoreCase(contact.getAttributeCode())) {
matchedContact = contact;
break;
}
}
if (matchedContact == null) {
log.warn("设备 {} 找不到 attributeCode={} 对应的 modelId跳过", deviceId, pointRule.getCode());
continue;
}
// 5. 保存运行记录
DeviceOperationRecordDO record = new DeviceOperationRecordDO();
record.setDeviceId(deviceId);
record.setModelId(matchedContact.getId());
record.setRule(pointRule.getRule());
record.setAddressValue(processedValue);
record.setRuleId(pointRulesDO.getId());
record.setCreator("1");
record.setUpdater("1");
// deviceOperationRecordMapper.insert(record);
tDengineService.insertDeviceOperationRecord(record);
break;
}
}
private void handleDeviceTimeout(Long deviceId) {
DeviceOperationRecordDO record = new DeviceOperationRecordDO();
record.setDeviceId(deviceId);
record.setRule(DeviceStatusEnum.OFFLINE.getCode());
record.setCreator("1");
record.setUpdater("1");
// deviceOperationRecordMapper.insert(record);
tDengineService.insertDeviceOperationRecord(record);
}
/**
*
* : EQ(), NE(), GT(), GE(),
* LT(), LE(), TRUE(), FALSE()
*/
private boolean matchRule(String value, PointRulesRespVO rule) {
if (StringUtils.isBlank(value) || rule == null ||
StringUtils.isBlank(rule.getOperator())) {
return false;
}
try {
String operator = rule.getOperator().toUpperCase();
String inputValue = value.trim().toLowerCase();
String ruleValue = StringUtils.trimToEmpty(rule.getOperatorRule());
// 1. 处理布尔值判断
if ("TRUE".equals(operator) || "FALSE".equals(operator)) {
return matchBooleanRule(inputValue, operator);
}
// 2. 如果operatorRule为空且不是布尔操作符则返回false
if (StringUtils.isBlank(ruleValue)) {
log.warn("规则比较值为空,但操作符不是布尔类型: operator={}", operator);
return false;
}
ruleValue = ruleValue.trim();
// 3. 尝试数值比较
if (isNumeric(inputValue) && isNumeric(ruleValue)) {
Double num1 = Double.parseDouble(inputValue);
Double num2 = Double.parseDouble(ruleValue);
return compareNumbers(num1, num2, operator);
}
// 4. 字符串比较
else {
return compareStrings(inputValue, ruleValue, operator);
}
} catch (Exception e) {
log.error("规则匹配异常: value={}, rule={}, error={}",
value, JSON.toJSONString(rule), e.getMessage());
return false;
}
}
/**
*
*/
private boolean compareStrings(String value, String ruleValue, String operator) {
switch (operator) {
case "EQ":
return value.equals(ruleValue);
case "NE":
return !value.equals(ruleValue);
case "GT":
return value.compareTo(ruleValue) > 0;
case "GE":
return value.compareTo(ruleValue) >= 0;
case "LT":
return value.compareTo(ruleValue) < 0;
case "LE":
return value.compareTo(ruleValue) <= 0;
default:
log.warn("不支持的操作符: {}", operator);
return false;
}
}
/**
*
*/
private boolean compareNumbers(Double value, Double ruleValue, String operator) {
switch (operator) {
case "EQ":
return Math.abs(value - ruleValue) < 0.000001; // 处理浮点数精度
case "NE":
return Math.abs(value - ruleValue) >= 0.000001;
case "GT":
return value > ruleValue;
case "GE":
return value >= ruleValue;
case "LT":
return value < ruleValue;
case "LE":
return value <= ruleValue;
default:
log.warn("不支持的操作符: {}", operator);
return false;
}
}
/**
*
*/
private boolean isNumeric(String str) {
if (StringUtils.isBlank(str)) {
return false;
}
try {
Double.parseDouble(str);
return true;
} catch (NumberFormatException e) {
return false;
}
}
/**
*
*/
private boolean matchBooleanRule(String value, String operator) {
// 常见布尔值表示
boolean booleanValue = parseBoolean(value);
if ("TRUE".equals(operator)) {
return booleanValue;
} else if ("FALSE".equals(operator)) {
return !booleanValue;
}
return false;
}
/**
*
* : true, false, 1, 0, yes, no, on, off
*/
private boolean parseBoolean(String value) {
if (StringUtils.isBlank(value)) {
return false;
}
String lowerValue = value.toLowerCase();
// 常见真值表示
if ("true".equals(lowerValue) ||
"1".equals(lowerValue) ||
"yes".equals(lowerValue) ||
"on".equals(lowerValue) ||
"是".equals(lowerValue) || // 中文支持
"成功".equals(lowerValue)) {
return true;
}
// 常见假值表示
if ("false".equals(lowerValue) ||
"0".equals(lowerValue) ||
"no".equals(lowerValue) ||
"off".equals(lowerValue) ||
"否".equals(lowerValue) || // 中文支持
"失败".equals(lowerValue)) {
return false;
}
// 尝试转换为布尔值
try {
return Boolean.parseBoolean(lowerValue);
} catch (Exception e) {
log.warn("无法解析为布尔值: {}", value);
return false;
}
}
}

@ -6,6 +6,7 @@ import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
import cn.iocoder.yudao.module.iot.controller.admin.device.vo.*;
import cn.iocoder.yudao.module.iot.controller.admin.devicecontactmodel.vo.DeviceContactModelPageReqVO;
import cn.iocoder.yudao.module.iot.controller.admin.devicewarinningrecord.vo.DeviceWarinningRecordPageReqVO;
import cn.iocoder.yudao.module.iot.dal.dataobject.device.DeviceAttributeDO;
import cn.iocoder.yudao.module.iot.dal.dataobject.device.DeviceDO;
import cn.iocoder.yudao.module.iot.dal.dataobject.devicecontactmodel.DeviceContactModelDO;
@ -13,6 +14,7 @@ import com.fasterxml.jackson.core.JsonProcessingException;
import org.eclipse.paho.client.mqttv3.MqttException;
import javax.validation.Valid;
import java.time.LocalDateTime;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
@ -134,7 +136,7 @@ public interface DeviceService {
Boolean scheduledStop(Long id);
DeviceOperationStatusRespVO getDeviceOperationalStatus();
DeviceOperationStatusRespVO getDeviceOperationalStatus(LocalDateTime startTime, LocalDateTime endTime);
List<Map<String, Object>> getMultiDeviceAttributes(Long goviewId);
@ -145,4 +147,12 @@ public interface DeviceService {
void updateDeviceEnabled(@Valid DeviceUpdateEnabledReqVO updateEnabledReqVO) throws MqttException;
DeviceDO getDeviceByMqttTopic(String topic);
List<DeviceRespVO> deviceList(@Valid DevicePageReqVO pageReqVO);
DeviceRunStatusStatsRespVO getDeviceRunStatusStats(Long customerId, Long orgNodeId);
DeviceRunStatusStatsRespVO getRunStatusStatsByCustomer(Long customerId);
List<CustomerDeviceStatusStatsRespVO> getStatusCountByCustomer();
}

@ -1,16 +1,21 @@
package cn.iocoder.yudao.module.iot.service.device;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.enums.DeviceConnectionStatusEnum;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.cus.dal.dataobject.management.ManagementDO;
import cn.iocoder.yudao.module.cus.dal.mysql.management.ManagementMapper;
import cn.iocoder.yudao.module.iot.controller.admin.device.dto.DeviceLatestRuleDTO;
import cn.iocoder.yudao.module.iot.controller.admin.device.enums.DeviceStatusEnum;
import cn.iocoder.yudao.module.iot.controller.admin.device.scheduled.scheduler.TaskSchedulerManager;
import cn.iocoder.yudao.module.iot.controller.admin.device.scheduled.utils.CronExpressionUtils;
import cn.iocoder.yudao.module.iot.controller.admin.device.utils.DataTypeParseUtil;
import cn.iocoder.yudao.module.iot.controller.admin.device.vo.*;
import cn.iocoder.yudao.module.iot.controller.admin.devicecontactmodel.vo.DeviceContactModelPageReqVO;
import cn.iocoder.yudao.module.iot.controller.admin.devicewarinningrecord.vo.DeviceWarinningRecordPageReqVO;
import cn.iocoder.yudao.module.iot.dal.dataobject.device.DeviceAttributeDO;
import cn.iocoder.yudao.module.iot.dal.dataobject.device.DeviceDO;
import cn.iocoder.yudao.module.iot.dal.dataobject.deviceattributetype.DeviceAttributeTypeDO;
@ -20,7 +25,9 @@ import cn.iocoder.yudao.module.iot.dal.dataobject.devicemodelattribute.DeviceMod
import cn.iocoder.yudao.module.iot.dal.dataobject.devicemodelrules.DeviceModelRulesDO;
import cn.iocoder.yudao.module.iot.dal.dataobject.deviceoperationrecord.DeviceOperationRecordDO;
import cn.iocoder.yudao.module.iot.dal.dataobject.devicepointrules.DevicePointRulesDO;
import cn.iocoder.yudao.module.iot.dal.dataobject.devicewarinningrecord.DeviceWarinningRecordDO;
import cn.iocoder.yudao.module.iot.dal.dataobject.gateway.GatewayDO;
import cn.iocoder.yudao.module.iot.dal.dataobject.orgnode.OrgNodeDO;
import cn.iocoder.yudao.module.iot.dal.mysql.device.DeviceAttributeMapper;
import cn.iocoder.yudao.module.iot.dal.mysql.device.DeviceMapper;
import cn.iocoder.yudao.module.iot.dal.mysql.deviceattributetype.DeviceAttributeTypeMapper;
@ -31,6 +38,8 @@ import cn.iocoder.yudao.module.iot.dal.mysql.devicemodelrules.DeviceModelRulesMa
import cn.iocoder.yudao.module.iot.dal.mysql.deviceoperationrecord.DeviceOperationRecordMapper;
import cn.iocoder.yudao.module.iot.dal.mysql.devicepointrules.DevicePointRulesMapper;
import cn.iocoder.yudao.module.iot.dal.mysql.gateway.GatewayMapper;
import cn.iocoder.yudao.module.iot.dal.mysql.orgnode.OrgNodeMapper;
import cn.iocoder.yudao.module.iot.dal.tdengine.IotDeviceMapper;
import cn.iocoder.yudao.module.iot.framework.mqtt.consumer.IMqttservice;
import com.alibaba.fastjson.JSON;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
@ -43,6 +52,7 @@ import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.eclipse.paho.client.mqttv3.MqttException;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Lazy;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@ -85,6 +95,9 @@ public class DeviceServiceImpl implements DeviceService {
@Resource
private DeviceContactModelMapper deviceContactModelMapper;
@Resource
private IotDeviceMapper iotDeviceMapper;
// @Resource
// private MqttDataRecordMapper mqttDataRecordMapper;
@Resource
@ -118,6 +131,12 @@ public class DeviceServiceImpl implements DeviceService {
@Resource
private GatewayMapper gatewayMapper;
@Resource
@Lazy
private OrgNodeMapper orgNodeMapper;
@Resource
private ManagementMapper cusManagementMapper;
@Override
@ -139,6 +158,9 @@ public class DeviceServiceImpl implements DeviceService {
throw exception(DEVICE_CODE_ALREADY_EXISTS);
}
if (StrUtil.isNotBlank(createReqVO.getDeviceCode())) {
tdengineService.validateTableName(createReqVO.getDeviceCode());
}
DeviceModelDO deviceModelDO = deviceModelMapper.selectById(createReqVO.getDeviceModelId());
@ -192,6 +214,13 @@ public class DeviceServiceImpl implements DeviceService {
List<DevicePointRulesDO> devicePointRulesDOList = new ArrayList<>();
DeviceModelDO deviceModelDO = deviceModelMapper.selectById(deviceModelId);
if(deviceModelDO == null){
DevicePointRulesDO devicePointRulesDO = new DevicePointRulesDO();
devicePointRulesDO.setIdentifier("RUNNING");
devicePointRulesDO.setFieldName("运行");
devicePointRulesDO.setDefaultValue("运行");
devicePointRulesDO.setDeviceId(deviceId);
devicePointRulesDOList.add(devicePointRulesDO);
devicePointRulesMapper.insert(devicePointRulesDO);
return;
}
List<DeviceModelRulesDO> deviceModelRulesDOList = deviceModelRulesMapper.selectList(Wrappers.<DeviceModelRulesDO>lambdaQuery()
@ -235,7 +264,7 @@ public class DeviceServiceImpl implements DeviceService {
// 校验存在
validateDeviceExists(id);
//是否有引用
validateReference(id);
// validateReference(id);
// // 删除
// deviceMapper.deleteById(id);
// 删除子表
@ -269,19 +298,49 @@ public class DeviceServiceImpl implements DeviceService {
@Override
public DeviceRespVO getDevice(Long id) {
DeviceDO deviceDO = deviceMapper.selectById(id);
if (deviceDO == null) {
return null; // 或者 throw exception(DEVICE_NOT_EXISTS);
}
DeviceRespVO deviceRespVO = BeanUtils.toBean(deviceDO, DeviceRespVO.class);
List<DevicePointRulesDO> devicePointRulesDOList = devicePointRulesMapper.selectList(Wrappers.<DevicePointRulesDO>lambdaQuery()
.eq(DevicePointRulesDO::getDeviceId, id));
//
// //设置点位规则
// if (!devicePointRulesDOList.isEmpty()){
//
// deviceRespVO.setPointRulesVOList(devicePointRulesDOList);
// }
Map<Long, LocalDateTime> latestTsMap = tdengineService.newSelectLatestTsBatch(Collections.singletonList(id));
return deviceRespVO;
List<DevicePointRulesDO> devicePointRulesDOList = devicePointRulesMapper.selectList(
Wrappers.<DevicePointRulesDO>lambdaQuery()
.eq(DevicePointRulesDO::getDeviceId, id)
);
// 如需返回点位规则再打开
// if (CollUtil.isNotEmpty(devicePointRulesDOList)) {
// deviceRespVO.setPointRulesVOList(devicePointRulesDOList);
// }
//设置采集时间
if(latestTsMap.get(id) != null){
deviceRespVO.setCollectionTime(latestTsMap.get(id));
}
// 补客户名称
if (deviceDO.getCustomerId() != null) {
ManagementDO customer = cusManagementMapper.selectById(deviceDO.getCustomerId());
if (customer != null) {
deviceRespVO.setCustomerName(customer.getCustomerName());
}
}
// 补组织名称(可能为空,表示直挂客户)
if (deviceDO.getOrgNodeId() != null && deviceDO.getOrgNodeId() > 0) {
OrgNodeDO orgNode = orgNodeMapper.selectById(deviceDO.getOrgNodeId());
if (orgNode != null) {
deviceRespVO.setOrgNodeName(orgNode.getName());
}
}
return deviceRespVO;
}
@Override
public DeviceDO getDeviceByName(String name) {
return deviceMapper.selectByName(name);
@ -1133,7 +1192,7 @@ public class DeviceServiceImpl implements DeviceService {
}
@Override
public DeviceOperationStatusRespVO getDeviceOperationalStatus() {
public DeviceOperationStatusRespVO getDeviceOperationalStatus(LocalDateTime startTime, LocalDateTime endTime) {
DeviceOperationStatusRespVO result = new DeviceOperationStatusRespVO();
@ -1185,6 +1244,11 @@ public class DeviceServiceImpl implements DeviceService {
// 计算故障率
calculateFaultRate(result);
// PageResult<DeviceWarinningRecordDO> deviceWarinningRecordDOPageResult = tdengineService.selectRunngingRecordPage(reqVO).getList();
Long warnningRecordcount = iotDeviceMapper.countWarningRecordByTime(startTime, endTime);
result.setWarningRecordCount(warnningRecordcount);
return result;
}
@ -1478,7 +1542,6 @@ public class DeviceServiceImpl implements DeviceService {
if (StringUtils.isBlank(deviceDO.getTopic())) {
throw exception(DEVICE_MQTT_TOPIC_EXIST);
}
//TODO 待优化
if (!"MQTT".equals(deviceDO.getProtocol())) {
throw exception(DEVICE_MQTT_TOPIC_EXIST);
@ -1501,6 +1564,67 @@ public class DeviceServiceImpl implements DeviceService {
return deviceMapper.selectOne(Wrappers.<DeviceDO>lambdaQuery().eq(DeviceDO::getTopic,topic));
}
@Override
public List<DeviceRespVO> deviceList(DevicePageReqVO pageReqVO) {
// 查询设备集合
List<DeviceDO> deviceDOS = deviceMapper.selectList(Wrappers.<DeviceDO>lambdaQuery()
.eq(DeviceDO::getCustomerId, pageReqVO.getCustomerId()));
List<Long> deviceIds = deviceDOS.stream()
.map(DeviceDO::getId)
.filter(Objects::nonNull)
.collect(Collectors.toList());
// 查询规则
List<String> ruleCodes = Arrays.stream(DeviceStatusEnum.values())
.map(DeviceStatusEnum::getCode)
.collect(Collectors.toList());
List<DeviceOperationRecordDO> operationRecords =
tdengineService.selectLatestByDeviceAndRuleMinimal(deviceIds, ruleCodes);
// 按 deviceId 分组,取最新一条
Map<Long, DeviceOperationRecordDO> latestRecordMap = operationRecords.stream()
.collect(Collectors.toMap(
DeviceOperationRecordDO::getDeviceId,
r -> r,
(r1, r2) -> r1.getCreateTime().isAfter(r2.getCreateTime()) ? r1 : r2
));
List<DeviceRespVO> deviceRespVOList = BeanUtils.toBean(deviceDOS, DeviceRespVO.class);
for (DeviceRespVO deviceRespVO : deviceRespVOList) {
Long deviceId = deviceRespVO.getId();
DeviceOperationRecordDO record = latestRecordMap.get(deviceId);
if (record != null) {
DeviceStatusEnum statusEnum = DeviceStatusEnum.getByCode(record.getRule());
deviceRespVO.setOperatingStatus(statusEnum != null
? statusEnum.getName()
: DeviceStatusEnum.OFFLINE.getName());
} else {
deviceRespVO.setOperatingStatus(DeviceStatusEnum.OFFLINE.getName());
}
}
// 按 status 过滤pageReqVO.status 传状态 code
if (StrUtil.isNotBlank(pageReqVO.getStatus())) {
DeviceStatusEnum queryStatus = DeviceStatusEnum.getByCode(pageReqVO.getStatus());
if (queryStatus != null) {
String targetStatusName = queryStatus.getName();
deviceRespVOList = deviceRespVOList.stream()
.filter(item -> Objects.equals(item.getOperatingStatus(), targetStatusName))
.collect(Collectors.toList());
} else {
// 传入非法状态时返回空
return Collections.emptyList();
}
}
return deviceRespVOList;
}
private void executeEnableUpdate(DeviceDO deviceDO, Boolean enabled) {
try {
@ -1596,4 +1720,262 @@ public class DeviceServiceImpl implements DeviceService {
return deviceMapper.deviceLedgerList();
}
@Override
public DeviceRunStatusStatsRespVO getDeviceRunStatusStats(Long customerId, Long orgNodeId) {
// 1) 先确定设备范围MySQL
List<Long> deviceIds;
if (orgNodeId != null) {
// 当前组织及子组织
List<OrgNodeDO> all = orgNodeMapper.selectListByCustomerByNodeId(orgNodeId);
if (CollUtil.isEmpty(all)) {
return emptyStats();
}
Map<Long, List<Long>> childrenMap = new HashMap<>();
for (OrgNodeDO n : all) {
childrenMap.computeIfAbsent(n.getParentId(), k -> new ArrayList<>()).add(n.getId());
}
Set<Long> subtreeIds = new HashSet<>();
Deque<Long> queue = new ArrayDeque<>();
queue.add(orgNodeId);
while (!queue.isEmpty()) {
Long cur = queue.poll();
if (!subtreeIds.add(cur)) {
continue;
}
List<Long> children = childrenMap.get(cur);
if (CollUtil.isNotEmpty(children)) {
queue.addAll(children);
}
}
List<DeviceDO> devices = deviceMapper.selectByOrgNodeIds(new ArrayList<>(subtreeIds));
deviceIds = devices.stream().map(DeviceDO::getId).collect(Collectors.toList());
} else if (customerId != null) {
List<DeviceDO> devices = deviceMapper.selectByCustomerId(customerId);
deviceIds = devices.stream().map(DeviceDO::getId).collect(Collectors.toList());
} else {
deviceIds = deviceMapper.getAllDeviceIds();
}
if (CollUtil.isEmpty(deviceIds)) {
return emptyStats();
}
// 2) tdengine 查每个设备最新一条 rule
List<DeviceLatestRuleDTO> latestRules = iotDeviceMapper.selectLatestRuleByDeviceIds(deviceIds);
// 3) 聚合统计
Map<Long, String> latestRuleMap = latestRules.stream()
.collect(Collectors.toMap(DeviceLatestRuleDTO::getDeviceId, DeviceLatestRuleDTO::getRule, (a, b) -> a));
DeviceRunStatusStatsRespVO resp = new DeviceRunStatusStatsRespVO();
resp.setTotalDeviceCount((long) deviceIds.size());
resp.setOfflineCount(0L);
resp.setRunningCount(0L);
resp.setStandbyCount(0L);
resp.setFaultStandbyCount(0L);
resp.setAlarmRunningCount(0L);
for (Long deviceId : deviceIds) {
String rule = latestRuleMap.get(deviceId);
if (rule == null) {
// 没有记录按离线
resp.setOfflineCount(resp.getOfflineCount() + 1);
continue;
}
DeviceStatusEnum status = DeviceStatusEnum.getByCode(rule);
if (status == null) {
resp.setOfflineCount(resp.getOfflineCount() + 1);
continue;
}
switch (status) {
case OFFLINE:
resp.setOfflineCount(resp.getOfflineCount() + 1);
break;
case RUNNING:
resp.setRunningCount(resp.getRunningCount() + 1);
break;
case STANDBY:
resp.setStandbyCount(resp.getStandbyCount() + 1);
break;
case FAULT_STANDBY:
resp.setFaultStandbyCount(resp.getFaultStandbyCount() + 1);
break;
case ALARM_RUNNING:
resp.setAlarmRunningCount(resp.getAlarmRunningCount() + 1);
break;
default:
resp.setOfflineCount(resp.getOfflineCount() + 1);
}
}
return resp;
}
private DeviceRunStatusStatsRespVO emptyStats() {
DeviceRunStatusStatsRespVO resp = new DeviceRunStatusStatsRespVO();
resp.setTotalDeviceCount(0L);
resp.setOfflineCount(0L);
resp.setRunningCount(0L);
resp.setStandbyCount(0L);
resp.setFaultStandbyCount(0L);
resp.setAlarmRunningCount(0L);
return resp;
}
@Override
public DeviceRunStatusStatsRespVO getRunStatusStatsByCustomer(Long customerId) {
List<DeviceDO> devices = deviceMapper.selectByCustomerId(customerId);
if (CollUtil.isEmpty(devices)) {
return emptyStats();
}
List<Long> deviceIds = devices.stream().map(DeviceDO::getId).collect(Collectors.toList());
List<DeviceLatestRuleDTO> latestRules = iotDeviceMapper.selectLatestRuleByDeviceIds(deviceIds);
Map<Long, String> latestRuleMap = latestRules.stream()
.collect(Collectors.toMap(DeviceLatestRuleDTO::getDeviceId, DeviceLatestRuleDTO::getRule, (a, b) -> a));
DeviceRunStatusStatsRespVO resp = new DeviceRunStatusStatsRespVO();
resp.setTotalDeviceCount((long) deviceIds.size());
resp.setOfflineCount(0L);
resp.setRunningCount(0L);
resp.setStandbyCount(0L);
resp.setFaultStandbyCount(0L);
resp.setAlarmRunningCount(0L);
for (Long deviceId : deviceIds) {
String rule = latestRuleMap.get(deviceId);
DeviceStatusEnum status = DeviceStatusEnum.getByCode(rule);
if (status == null) {
resp.setOfflineCount(resp.getOfflineCount() + 1);
continue;
}
switch (status) {
case OFFLINE:
resp.setOfflineCount(resp.getOfflineCount() + 1); break;
case RUNNING:
resp.setRunningCount(resp.getRunningCount() + 1); break;
case STANDBY:
resp.setStandbyCount(resp.getStandbyCount() + 1); break;
case FAULT_STANDBY:
resp.setFaultStandbyCount(resp.getFaultStandbyCount() + 1); break;
case ALARM_RUNNING:
resp.setAlarmRunningCount(resp.getAlarmRunningCount() + 1); break;
default:
resp.setOfflineCount(resp.getOfflineCount() + 1);
}
}
return resp;
}
@Override
public List<CustomerDeviceStatusStatsRespVO> getStatusCountByCustomer() {
// 1) 查所有客户
List<ManagementDO> customers = cusManagementMapper.selectList();
if (CollUtil.isEmpty(customers)) {
return Collections.emptyList();
}
Map<Long, String> customerNameMap = customers.stream()
.collect(Collectors.toMap(ManagementDO::getId, ManagementDO::getCustomerName, (a, b) -> a));
// 2) 查所有设备(含 customerId
List<DeviceDO> devices = deviceMapper.selectList();
if (CollUtil.isEmpty(devices)) {
// 没设备时,每个客户返回 0
return customers.stream()
.map(this::buildEmptyCustomerStats)
.collect(Collectors.toList());
}
List<Long> deviceIds = devices.stream().map(DeviceDO::getId).collect(Collectors.toList());
// 3) 查 tdengine 每设备最新 rule
List<DeviceLatestRuleDTO> latestRules = iotDeviceMapper.selectLatestRuleByDeviceIds(deviceIds);
Map<Long, String> latestRuleMap = latestRules.stream()
.collect(Collectors.toMap(DeviceLatestRuleDTO::getDeviceId, DeviceLatestRuleDTO::getRule, (a, b) -> a));
// 4) 初始化每个客户统计对象
Map<Long, CustomerDeviceStatusStatsRespVO> statsMap = new LinkedHashMap<>();
for (ManagementDO c : customers) {
statsMap.put(c.getId(), buildEmptyCustomerStats(c));
}
// 5) 按设备归属客户累加
for (DeviceDO d : devices) {
Long customerId = d.getCustomerId();
if (customerId == null) {
continue;
}
// 防止设备 customerId 在 customers 中不存在(脏数据兜底)
CustomerDeviceStatusStatsRespVO stats = statsMap.computeIfAbsent(customerId, k -> {
ManagementDO fallback = new ManagementDO();
fallback.setId(k);
fallback.setCustomerName(customerNameMap.get(k));
return buildEmptyCustomerStats(fallback);
});
stats.setTotalDeviceCount(stats.getTotalDeviceCount() + 1);
String rule = latestRuleMap.get(d.getId());
DeviceStatusEnum status = DeviceStatusEnum.getByCode(rule);
if (status == null) {
stats.setOfflineCount(stats.getOfflineCount() + 1);
continue;
}
switch (status) {
case OFFLINE:
stats.setOfflineCount(stats.getOfflineCount() + 1);
break;
case RUNNING:
stats.setRunningCount(stats.getRunningCount() + 1);
break;
case STANDBY:
stats.setStandbyCount(stats.getStandbyCount() + 1);
break;
case FAULT_STANDBY:
stats.setFaultStandbyCount(stats.getFaultStandbyCount() + 1);
break;
case ALARM_RUNNING:
stats.setAlarmRunningCount(stats.getAlarmRunningCount() + 1);
break;
default:
stats.setOfflineCount(stats.getOfflineCount() + 1);
}
}
return new ArrayList<>(statsMap.values());
}
private CustomerDeviceStatusStatsRespVO buildEmptyCustomerStats(ManagementDO customer) {
CustomerDeviceStatusStatsRespVO vo = new CustomerDeviceStatusStatsRespVO();
vo.setCustomerId(customer.getId());
vo.setCustomerName(customer.getCustomerName());
vo.setLongitude(customer.getLongitude());
vo.setLatitude(customer.getLatitude());
vo.setTotalDeviceCount(0L);
vo.setOfflineCount(0L);
vo.setRunningCount(0L);
vo.setStandbyCount(0L);
vo.setFaultStandbyCount(0L);
vo.setAlarmRunningCount(0L);
return vo;
}
}

@ -16,6 +16,7 @@ import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.DecoderException;
import org.apache.commons.codec.binary.Hex;
import org.apache.commons.lang3.StringUtils;
import org.apache.kafka.common.protocol.types.Field;
import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
@ -1259,6 +1260,52 @@ public class TDengineService {
}
}
/**
* TDengine
*/
public void validateTableName(String tableName) {
if (StrUtil.isBlank(tableName)) {
throw exception(DEVICE_MODEL_POINT_CODE_EXISTS, "表名不能为空");
}
// TDengine 表名规则验证
// 1. 不能是保留关键字
Set<String> reservedKeywords = new HashSet<>(Arrays.asList(
"value", "timestamp", "current", "database", "table", "user", "password",
"select", "insert", "update", "delete", "create", "drop", "alter",
"show", "describe", "use", "ts", "now", "current_timestamp"
));
if (reservedKeywords.contains(tableName.toLowerCase())) {
throw exception(DEVICE_MODEL_POINT_CODE_EXISTS,
"表名不能使用保留关键字: " + tableName);
}
// 2. 必须以字母开头
if (!Character.isLetter(tableName.charAt(0))) {
throw exception(DEVICE_MODEL_POINT_CODE_EXISTS,
"表名必须以字母开头: " + tableName);
}
// 3. 只能包含字母、数字、下划线
if (!tableName.matches("^[a-zA-Z][a-zA-Z0-9_]*$")) {
throw exception(DEVICE_MODEL_POINT_CODE_EXISTS,
"表名只能包含字母、数字和下划线: " + tableName);
}
// 4. 长度限制TDengine表名最大长度通常为192
if (tableName.length() > 192) {
throw exception(DEVICE_MODEL_POINT_CODE_EXISTS,
"表名长度不能超过192个字符: " + tableName);
}
// 5. 不能以下划线开头(与列名策略保持一致)
if (tableName.startsWith("_")) {
throw exception(DEVICE_MODEL_POINT_CODE_EXISTS,
"表名不能以下划线开头: " + tableName);
}
}
/**
* deviceId
@ -2236,6 +2283,8 @@ public class TDengineService {
/**
* TDengine
*/
@ -2324,21 +2373,20 @@ public class TDengineService {
}
// 创建时间范围
if (reqVO.getCreateTime() != null && reqVO.getCreateTime().length == 2) {
LocalDateTime startTime = reqVO.getCreateTime()[0];
LocalDateTime endTime = reqVO.getCreateTime()[1];
String startTime = reqVO.getStartTime();
String endTime = reqVO.getEndTime();
if (startTime != null) {
where.append(" AND create_time >= ? ");
params.add(Timestamp.valueOf(startTime));
}
if (startTime != null) {
where.append(" AND create_time >= ? ");
params.add(Timestamp.valueOf(startTime));
}
if (endTime != null) {
where.append(" AND create_time <= ? ");
params.add(Timestamp.valueOf(endTime));
}
if (endTime != null) {
where.append(" AND create_time <= ? ");
params.add(Timestamp.valueOf(endTime));
}
// 规则ID
if (reqVO.getRuleId() != null) {
where.append(" AND rule_id = ? ");

@ -23,6 +23,7 @@ import java.util.List;
import java.util.Random;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.DEVICE_MODEL_CODE_EXISTS;
import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.DEVICE_MODEL_NOT_EXISTS;
/**

@ -1,6 +1,7 @@
package cn.iocoder.yudao.module.iot.service.devicemodelattribute;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.exception.ServiceException;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.collection.MapUtils;
@ -63,6 +64,9 @@ import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.*;
@Resource
private ErpProductUnitService productUnitService;
@Resource
@Lazy
private TDengineService tDengineService;
@Override
public Long createDeviceModelAttribute(DeviceModelAttributeSaveReqVO createReqVO) {
@ -77,9 +81,15 @@ import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.*;
throw exception(DEVICE_MODEL_ATTRIBUTE_POTIN_CODE_EXISTS);
}
// 插入
// 插入
DeviceModelAttributeDO deviceModelAttribute = BeanUtils.toBean(createReqVO, DeviceModelAttributeDO.class);
// deviceModelAttribute.setTypeName(deviceAttributeTypeMapper.selectById(createReqVO.getAttributeCode()).getName());
//检查attributeCode是否符合TDengine列名规则
if (StrUtil.isNotBlank(createReqVO.getAttributeCode())) {
tDengineService.validateColumnName(createReqVO.getAttributeCode());
}
deviceModelAttributeMapper.insert(deviceModelAttribute);
// 返回
return deviceModelAttribute.getId();

@ -1,8 +1,7 @@
package cn.iocoder.yudao.module.iot.service.devicewarinningrecord;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.iot.controller.admin.devicewarinningrecord.vo.DeviceWarinningRecordPageReqVO;
import cn.iocoder.yudao.module.iot.controller.admin.devicewarinningrecord.vo.DeviceWarinningRecordSaveReqVO;
import cn.iocoder.yudao.module.iot.controller.admin.devicewarinningrecord.vo.*;
import cn.iocoder.yudao.module.iot.dal.dataobject.devicewarinningrecord.DeviceWarinningRecordDO;
import javax.validation.Valid;
@ -52,11 +51,16 @@ public interface DeviceWarinningRecordService {
* @param pageReqVO
* @return
*/
PageResult<DeviceWarinningRecordDO> getDeviceWarinningRecordPage(DeviceWarinningRecordPageReqVO pageReqVO);
PageResult<DeviceWarinningRecordPageRespVO> getDeviceWarinningRecordPage(DeviceWarinningRecordPageReqVO pageReqVO);
List<DeviceWarningListRespVO> getList(DeviceWarningListReqVO reqVO);
List<DeviceWarinningRecordDO> getList(Long id);
List<Map<String, Object>> getLastSevenHoursCount();
DeviceWarningCountRespVO getWarningCount(DeviceWarningCountReqVO reqVO);
DeviceWarningTrendRespVO getWarningTrend(DeviceWarningTrendReqVO reqVO);
}

@ -1,26 +1,32 @@
package cn.iocoder.yudao.module.iot.service.devicewarinningrecord;
import cn.hutool.core.collection.CollUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.iot.controller.admin.devicewarinningrecord.vo.DeviceWarinningRecordPageReqVO;
import cn.iocoder.yudao.module.iot.controller.admin.devicewarinningrecord.vo.DeviceWarinningRecordSaveReqVO;
import cn.iocoder.yudao.module.cus.dal.dataobject.management.ManagementDO;
import cn.iocoder.yudao.module.cus.dal.mysql.management.ManagementMapper;
import cn.iocoder.yudao.module.iot.dal.dataobject.devicewarinningrecord.WarningTrendPointRespDTO;
import cn.iocoder.yudao.module.iot.controller.admin.devicewarinningrecord.vo.*;
import cn.iocoder.yudao.module.iot.dal.dataobject.device.DeviceDO;
import cn.iocoder.yudao.module.iot.dal.dataobject.devicewarinningrecord.DeviceWarinningRecordDO;
import cn.iocoder.yudao.module.iot.dal.mysql.device.DeviceMapper;
import cn.iocoder.yudao.module.iot.dal.mysql.devicewarinningrecord.DeviceWarinningRecordMapper;
import cn.iocoder.yudao.module.iot.dal.tdengine.IotDeviceMapper;
import cn.iocoder.yudao.module.iot.service.device.TDengineService;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import javax.annotation.Resource;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.DEVICE_WARINNING_RECORD_NOT_EXISTS;
import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.*;
/**
* Service
@ -33,6 +39,14 @@ public class DeviceWarinningRecordServiceImpl implements DeviceWarinningRecordSe
@Resource
private DeviceWarinningRecordMapper deviceWarinningRecordMapper;
@Resource
private IotDeviceMapper iotDeviceMapper;
@Resource
@Lazy
private DeviceMapper deviceMapper;
@Resource
private ManagementMapper cusManagementMapper;
@Resource
private TDengineService tDengineService;
@ -76,24 +90,98 @@ public class DeviceWarinningRecordServiceImpl implements DeviceWarinningRecordSe
}
@Override
public PageResult<DeviceWarinningRecordDO> getDeviceWarinningRecordPage(DeviceWarinningRecordPageReqVO pageReqVO) {
// return deviceWarinningRecordMapper.selectPage(pageReqVO);
return tDengineService.selectRunngingRecordPage(pageReqVO);
public PageResult<DeviceWarinningRecordPageRespVO> getDeviceWarinningRecordPage(DeviceWarinningRecordPageReqVO pageReqVO) {
PageResult<DeviceWarinningRecordDO> page = tDengineService.selectRunngingRecordPage(pageReqVO);
if (CollUtil.isEmpty(page.getList())) {
return new PageResult<>(Collections.emptyList(), page.getTotal());
}
Set<Long> deviceIds = page.getList().stream()
.map(DeviceWarinningRecordDO::getDeviceId)
.filter(Objects::nonNull)
.collect(Collectors.toSet());
Map<Long, DeviceDO> deviceMap = CollUtil.isEmpty(deviceIds) ? Collections.emptyMap()
: deviceMapper.selectBatchIds(deviceIds).stream()
.collect(Collectors.toMap(DeviceDO::getId, Function.identity(), (a, b) -> a));
Set<Long> customerIds = deviceMap.values().stream()
.map(DeviceDO::getCustomerId)
.filter(Objects::nonNull)
.collect(Collectors.toSet());
Map<Long, ManagementDO> customerMap = CollUtil.isEmpty(customerIds) ? Collections.emptyMap()
: cusManagementMapper.selectBatchIds(customerIds).stream()
.collect(Collectors.toMap(ManagementDO::getId, Function.identity(), (a, b) -> a));
List<DeviceWarinningRecordPageRespVO> list = page.getList().stream().map(record -> {
DeviceWarinningRecordPageRespVO vo = BeanUtils.toBean(record, DeviceWarinningRecordPageRespVO.class);
DeviceDO device = deviceMap.get(record.getDeviceId());
if (device != null) {
vo.setDeviceName(device.getDeviceName());
vo.setDeviceCode(device.getDeviceCode()); // 按你项目真实字段改deviceCode/code/sn
ManagementDO customer = customerMap.get(device.getCustomerId());
if (customer != null) {
vo.setCustomerCode(customer.getCustomerCode()); // 按真实字段名调整
vo.setCustomerName(customer.getCustomerName());
}
}
return vo;
}).collect(Collectors.toList());
return new PageResult<>(list, page.getTotal());
}
@Override
public List<DeviceWarinningRecordDO> getList(Long id) {
// if (id == null) {
// return Collections.emptyList();
// }
// return deviceWarinningRecordMapper.selectList(
// Wrappers.<DeviceWarinningRecordDO>lambdaQuery()
// .eq(id != null, DeviceWarinningRecordDO::getDeviceId, id)
// .orderByDesc(DeviceWarinningRecordDO::getCreateTime)
// .last("LIMIT 100")// 限制 100 条
// );
return tDengineService.selectDeviceWarningList(id);
public List<DeviceWarningListRespVO> getList(DeviceWarningListReqVO reqVO) {
if (reqVO.getStartTime() == null || reqVO.getEndTime() == null) {
throw exception(DEVICE_WARNING_TIME_REQUIRED);
}
List<DeviceWarinningRecordDO> records = iotDeviceMapper.selectWarningRecordList(
reqVO.getDeviceId(),
reqVO.getStartTime(),
reqVO.getEndTime(),
reqVO.getAlarmLevel()
);
if (CollUtil.isEmpty(records)) {
return Collections.emptyList();
}
Set<Long> deviceIds = records.stream()
.map(DeviceWarinningRecordDO::getDeviceId)
.filter(Objects::nonNull)
.collect(Collectors.toSet());
Map<Long, DeviceDO> deviceMap = CollUtil.isEmpty(deviceIds) ? Collections.emptyMap()
: deviceMapper.selectBatchIds(deviceIds).stream()
.collect(Collectors.toMap(DeviceDO::getId, Function.identity(), (a, b) -> a));
Set<Long> customerIds = deviceMap.values().stream()
.map(DeviceDO::getCustomerId)
.filter(Objects::nonNull)
.collect(Collectors.toSet());
Map<Long, ManagementDO> customerMap = CollUtil.isEmpty(customerIds) ? Collections.emptyMap()
: cusManagementMapper.selectBatchIds(customerIds).stream()
.collect(Collectors.toMap(ManagementDO::getId, Function.identity(), (a, b) -> a));
return records.stream().map(record -> {
DeviceWarningListRespVO vo = BeanUtils.toBean(record, DeviceWarningListRespVO.class);
DeviceDO device = deviceMap.get(record.getDeviceId());
if (device != null) {
vo.setDeviceName(device.getDeviceName()); // 以主库设备名为准
ManagementDO customer = customerMap.get(device.getCustomerId());
if (customer != null) {
vo.setCustomerName(customer.getCustomerName());
}
}
return vo;
}).collect(Collectors.toList());
}
@Override
@ -142,4 +230,91 @@ public class DeviceWarinningRecordServiceImpl implements DeviceWarinningRecordSe
return result;
}
@Override
public DeviceWarningCountRespVO getWarningCount(DeviceWarningCountReqVO reqVO) {
if (reqVO.getStartTime() == null || reqVO.getEndTime() == null) {
throw exception(DEVICE_WARNING_TIME_REQUIRED);
}
DateTimeFormatter F = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
String start = reqVO.getStartTime().format(F);
String end = reqVO.getEndTime().format(F);
DeviceWarningCountRespVO resp = iotDeviceMapper.selectWarningCount(start, end);
if (resp == null) {
resp = new DeviceWarningCountRespVO();
resp.setTotalCount(0L);
resp.setNormalCount(0L);
resp.setTipCount(0L);
resp.setSeriousCount(0L);
return resp;
}
resp.setTotalCount(resp.getTotalCount() == null ? 0L : resp.getTotalCount());
resp.setNormalCount(resp.getNormalCount() == null ? 0L : resp.getNormalCount());
resp.setTipCount(resp.getTipCount() == null ? 0L : resp.getTipCount());
resp.setSeriousCount(resp.getSeriousCount() == null ? 0L : resp.getSeriousCount());
return resp;
}
@Override
public DeviceWarningTrendRespVO getWarningTrend(DeviceWarningTrendReqVO reqVO) {
LocalDateTime startTime = reqVO.getStartTime();
LocalDateTime endTime = reqVO.getEndTime();
if (startTime == null || endTime == null) {
throw exception(DEVICE_WARNING_TIME_REQUIRED);
}
if (startTime.isAfter(endTime)) {
throw exception(DEVICE_WARNING_TIME_RANGE_INVALID);
}
DateTimeFormatter F = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
String start = reqVO.getStartTime().format(F);
String end = reqVO.getEndTime().format(F);
boolean byHour = startTime.toLocalDate().equals(endTime.toLocalDate());
List<String> timePoints = new ArrayList<>();
Map<String, Long> bucket = new LinkedHashMap<>();
if (byHour) {
LocalDate date = startTime.toLocalDate();
for (int i = 0; i < 24; i++) {
String key = date + " " + String.format("%02d:00", i); // yyyy-MM-dd HH:00
timePoints.add(key);
bucket.put(key, 0L);
}
List<WarningTrendPointRespDTO> rows = iotDeviceMapper.selectWarningTrendByHour(start, end);
for (WarningTrendPointRespDTO row : rows) {
if (bucket.containsKey(row.getTimeKey())) {
bucket.put(row.getTimeKey(), row.getCount() == null ? 0L : row.getCount());
}
}
} else {
LocalDate d = startTime.toLocalDate();
LocalDate endDate = endTime.toLocalDate();
while (!d.isAfter(endDate)) {
String key = d.toString(); // yyyy-MM-dd
timePoints.add(key);
bucket.put(key, 0L);
d = d.plusDays(1);
}
List<WarningTrendPointRespDTO> rows = iotDeviceMapper.selectWarningTrendByDay(start, end);
for (WarningTrendPointRespDTO row : rows) {
if (bucket.containsKey(row.getTimeKey())) {
bucket.put(row.getTimeKey(), row.getCount() == null ? 0L : row.getCount());
}
}
}
DeviceWarningTrendRespVO resp = new DeviceWarningTrendRespVO();
resp.setTimePoints(timePoints);
resp.setCounts(new ArrayList<>(bucket.values()));
return resp;
}
}

@ -2,6 +2,8 @@ package cn.iocoder.yudao.module.iot.service.orgnode;
import java.util.*;
import javax.validation.*;
import cn.iocoder.yudao.module.iot.controller.admin.device.vo.DeviceSimpleRespVO;
import cn.iocoder.yudao.module.iot.controller.admin.orgnode.vo.*;
import cn.iocoder.yudao.module.iot.dal.dataobject.orgnode.OrgNodeDO;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
@ -61,4 +63,7 @@ public interface OrgNodeService {
List<OrgNodeTreeRespVO> getOrgTree(Long customerId);
List<DeviceSimpleRespVO> getDeviceListByNode(Long nodeId, Integer nodeType);
}

@ -2,12 +2,23 @@ package cn.iocoder.yudao.module.iot.service.orgnode;
import cn.hutool.core.collection.CollUtil;
import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder;
import cn.iocoder.yudao.module.cus.dal.dataobject.management.ManagementDO;
import cn.iocoder.yudao.module.cus.dal.mysql.management.ManagementMapper;
import cn.iocoder.yudao.module.iot.controller.admin.device.enums.DeviceStatusEnum;
import cn.iocoder.yudao.module.iot.controller.admin.device.vo.DeviceSimpleRespVO;
import cn.iocoder.yudao.module.iot.dal.dataobject.device.DeviceDO;
import cn.iocoder.yudao.module.iot.dal.dataobject.deviceoperationrecord.DeviceOperationRecordDO;
import cn.iocoder.yudao.module.iot.dal.mysql.device.DeviceMapper;
import cn.iocoder.yudao.module.iot.service.device.TDengineService;
import org.jetbrains.annotations.NotNull;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import org.springframework.validation.annotation.Validated;
import org.springframework.transaction.annotation.Transactional;
import java.util.*;
import java.util.stream.Collectors;
import cn.iocoder.yudao.module.iot.controller.admin.orgnode.vo.*;
import cn.iocoder.yudao.module.iot.dal.dataobject.orgnode.OrgNodeDO;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
@ -32,14 +43,38 @@ public class OrgNodeServiceImpl implements OrgNodeService {
@Resource
private OrgNodeMapper orgNodeMapper;
@Resource
private TDengineService tDengineService;
@Resource
private DeviceMapper deviceMapper;
@Resource
private ManagementMapper cusManagementMapper;
@Override
public Long createOrgNode(OrgNodeSaveReqVO createReqVO) {
// 插入
OrgNodeDO orgNode = BeanUtils.toBean(createReqVO, OrgNodeDO.class);
orgNodeMapper.insert(orgNode);
// 返回
// 1) parentId 兜底null 视为根下一级(挂客户)
if (orgNode.getParentId() == null) {
orgNode.setParentId(0L);
}
// 2) 根据 parentId 规范 customerId
if (orgNode.getParentId() > 0) {
OrgNodeDO parent = orgNodeMapper.selectById(orgNode.getParentId());
if (parent == null) {
throw exception(ORG_NODE_NOT_EXISTS);
}
// 子节点必须跟父节点同客户
orgNode.setCustomerId(parent.getCustomerId());
} else {
// parentId == 0必须明确属于哪个客户
if (orgNode.getCustomerId() == null) {
throw exception(ORG_NODE_CUSTOMER_ID_REQUIRED); // 你们项目没有这个错误码就换成已有的
}
}
orgNodeMapper.insert(orgNode);
return orgNode.getId();
}
@ -54,12 +89,24 @@ public class OrgNodeServiceImpl implements OrgNodeService {
@Override
public void deleteOrgNode(Long id) {
// 校验存在
// 1) 校验节点存在
validateOrgNodeExists(id);
// 删除
// 2) 校验是否有子节点
if (orgNodeMapper.existsByParentId(id)) {
throw exception(ORG_NODE_DELETE_HAS_CHILDREN);
}
// 3) 校验是否挂设备
if (deviceMapper.existsByOrgNodeId(id)) {
throw exception(ORG_NODE_DELETE_HAS_DEVICES);
}
// 4) 允许删除
orgNodeMapper.deleteById(id);
}
@Override
public void deleteOrgNodeListByIds(List<Long> ids) {
// 删除
@ -83,40 +130,239 @@ public class OrgNodeServiceImpl implements OrgNodeService {
return orgNodeMapper.selectPage(pageReqVO);
}
@Override
public List<OrgNodeTreeRespVO> getOrgTree(Long customerId) {
Long tenantId = TenantContextHolder.getRequiredTenantId();
List<OrgNodeDO> list = orgNodeMapper.selectListByTenantAndCustomer(tenantId, customerId);
if (list.isEmpty()) {
return Collections.emptyList();
// 1) customerId 可空:为空查全部客户;不为空查单个客户
List<ManagementDO> customers;
if (customerId == null) {
customers = cusManagementMapper.selectList();
} else {
ManagementDO customer = cusManagementMapper.selectById(customerId);
if (customer == null) {
return Collections.emptyList();
}
customers = Collections.singletonList(customer);
}
Map<Long, OrgNodeTreeRespVO> map = new HashMap<>(list.size());
for (OrgNodeDO item : list) {
OrgNodeTreeRespVO vo = new OrgNodeTreeRespVO();
vo.setId(item.getId());
vo.setParentId(item.getParentId());
vo.setNodeType(item.getNodeType()); // 1客户 2车间 3产线
vo.setName(item.getName());
vo.setChildren(new ArrayList<>());
map.put(item.getId(), vo);
if (customers.isEmpty()) {
return Collections.emptyList();
}
List<OrgNodeTreeRespVO> roots = new ArrayList<>();
for (OrgNodeDO item : list) {
OrgNodeTreeRespVO current = map.get(item.getId());
if (item.getParentId() == 0) {
roots.add(current);
} else {
OrgNodeTreeRespVO parent = map.get(item.getParentId());
if (parent != null) {
parent.getChildren().add(current);
List<OrgNodeTreeRespVO> roots = new ArrayList<>(customers.size());
// 2) 每个客户组一棵树
for (ManagementDO customer : customers) {
Long cid = customer.getId();
// 客户根
OrgNodeTreeRespVO customerRoot = new OrgNodeTreeRespVO();
customerRoot.setId(cid);
customerRoot.setParentId(0L);
customerRoot.setNodeType(1); // 客户
customerRoot.setName(customer.getCustomerName());
customerRoot.setOperateStatus(null);
customerRoot.setChildren(new ArrayList<>());
customerRoot.setNodeKey("C_" + cid);
customerRoot.setParentKey("0");
// 2.1 查组织节点(按 customer_id
List<OrgNodeDO> orgList = orgNodeMapper.selectListByCustomer(cid);
Map<Long, OrgNodeTreeRespVO> orgMap = new HashMap<>(orgList.size());
for (OrgNodeDO item : orgList) {
OrgNodeTreeRespVO vo = new OrgNodeTreeRespVO();
vo.setId(item.getId());
vo.setParentId(item.getParentId());
vo.setNodeType(item.getNodeType()); // 2车间 3产线
vo.setName(item.getName());
vo.setOperateStatus(null);
vo.setChildren(new ArrayList<>());
vo.setNodeKey("O_" + item.getId());
orgMap.put(item.getId(), vo);
}
// 2.2 组装组织树parentId=0 挂到客户根
for (OrgNodeDO item : orgList) {
OrgNodeTreeRespVO current = orgMap.get(item.getId());
if (Objects.equals(item.getParentId(), 0L)) {
current.setParentKey(customerRoot.getNodeKey()); // C_xxx
customerRoot.getChildren().add(current);
} else {
OrgNodeTreeRespVO parent = orgMap.get(item.getParentId());
if (parent != null) {
current.setParentKey(parent.getNodeKey()); // O_xxx
parent.getChildren().add(current);
} else {
// 脏数据兜底:挂客户根
current.setParentKey(customerRoot.getNodeKey());
customerRoot.getChildren().add(current);
}
}
}
// 2.3 挂设备节点 + operateStatus
// 这里请确保你的 SQL 能查到 org_node_id IS NULL / 0 的设备
List<DeviceDO> devices = deviceMapper.selectByCustomerAndOrgNodeIds(
cid,
orgList.stream().map(OrgNodeDO::getId).collect(Collectors.toList())
);
if (!devices.isEmpty()) {
List<Long> deviceIds = devices.stream().map(DeviceDO::getId).collect(Collectors.toList());
Map<Long, String> statusMap = tDengineService.getLatestDeviceStatusAlternative(deviceIds);
for (DeviceDO device : devices) {
Long orgNodeId = device.getOrgNodeId();
OrgNodeTreeRespVO deviceVo = new OrgNodeTreeRespVO();
deviceVo.setId(device.getId());
deviceVo.setParentId(orgNodeId == null ? 0L : orgNodeId);
deviceVo.setNodeType(4); // 设备
deviceVo.setName(device.getDeviceName());
deviceVo.setDeviceCode(device.getDeviceCode()); // 新增
deviceVo.setOperateStatus(statusMap.get(device.getId()));
deviceVo.setChildren(Collections.emptyList());
deviceVo.setNodeKey("D_" + device.getId());
// org_node_id 为空/0直接挂客户根
if (orgNodeId == null || orgNodeId == 0L) {
deviceVo.setParentKey(customerRoot.getNodeKey()); // C_xxx
customerRoot.getChildren().add(deviceVo);
continue;
}
// 否则挂组织节点
OrgNodeTreeRespVO parent = orgMap.get(orgNodeId);
if (parent != null) {
deviceVo.setParentKey(parent.getNodeKey()); // O_xxx
parent.getChildren().add(deviceVo);
} else {
// 脏数据兜底:挂客户根
deviceVo.setParentKey(customerRoot.getNodeKey());
customerRoot.getChildren().add(deviceVo);
}
}
}
roots.add(customerRoot);
}
return roots;
}
@Override
public List<DeviceSimpleRespVO> getDeviceListByNode(Long nodeId, Integer nodeType) {
if (nodeId == null || nodeType == null) {
return Collections.emptyList();
}
List<DeviceDO> devices = Collections.emptyList();
// 4=设备:直接返回自己
if (Objects.equals(nodeType, 4)) {
DeviceDO device = deviceMapper.selectById(nodeId);
if (device == null || Boolean.TRUE.equals(device.getDeleted())) {
return Collections.emptyList();
}
devices = Collections.singletonList(device);
}
// 1=客户:返回该客户全部设备
else if (Objects.equals(nodeType, 1)) {
devices = deviceMapper.selectByCustomerId(nodeId);
}
// 2/3=组织:返回当前节点及其子节点下全部设备
else if (Objects.equals(nodeType, 2) || Objects.equals(nodeType, 3)) {
List<OrgNodeDO> all = orgNodeMapper.selectListByCustomerByNodeId(nodeId);
if (CollUtil.isEmpty(all)) {
return Collections.emptyList();
}
// parentId -> childrenIds
Map<Long, List<Long>> childrenMap = new HashMap<>();
for (OrgNodeDO n : all) {
childrenMap.computeIfAbsent(n.getParentId(), k -> new ArrayList<>()).add(n.getId());
}
// BFS 收集子树(包含自己)
Set<Long> subtreeIds = new HashSet<>();
Deque<Long> queue = new ArrayDeque<>();
queue.add(nodeId);
while (!queue.isEmpty()) {
Long currentId = queue.poll();
if (!subtreeIds.add(currentId)) {
continue;
}
List<Long> children = childrenMap.get(currentId);
if (CollUtil.isNotEmpty(children)) {
queue.addAll(children);
}
}
if (CollUtil.isEmpty(subtreeIds)) {
return Collections.emptyList();
}
devices = deviceMapper.selectByOrgNodeIds(new ArrayList<>(subtreeIds));
}
// 其他 nodeType
else {
return Collections.emptyList();
}
if (CollUtil.isEmpty(devices)) {
return Collections.emptyList();
}
// 批量查运行状态(避免在 toSimpleVO 里逐条查)
List<Long> deviceIds = devices.stream().map(DeviceDO::getId).collect(Collectors.toList());
List<String> ruleCodes = Arrays.stream(DeviceStatusEnum.values())
.map(DeviceStatusEnum::getCode)
.collect(Collectors.toList());
List<DeviceOperationRecordDO> operationRecords =
tDengineService.selectLatestByDeviceAndRuleMinimal(deviceIds, ruleCodes);
Map<Long, DeviceOperationRecordDO> latestRecordMap = operationRecords.stream()
.collect(Collectors.toMap(
DeviceOperationRecordDO::getDeviceId,
r -> r,
(r1, r2) -> r1.getCreateTime().isAfter(r2.getCreateTime()) ? r1 : r2
));
//vo转换
return getDeviceSimpleRespVOS(devices, latestRecordMap);
}
private static List<DeviceSimpleRespVO> getDeviceSimpleRespVOS(List<DeviceDO> devices, Map<Long, DeviceOperationRecordDO> latestRecordMap) {
// 转 VO
List<DeviceSimpleRespVO> result = new ArrayList<>(devices.size());
for (DeviceDO d : devices) {
DeviceSimpleRespVO vo = new DeviceSimpleRespVO();
vo.setId(d.getId());
vo.setDeviceCode(d.getDeviceCode());
vo.setDeviceName(d.getDeviceName());
vo.setCustomerId(d.getCustomerId());
vo.setOrgNodeId(d.getOrgNodeId());
vo.setStatus(d.getStatus());
vo.setProtocol(d.getProtocol());
vo.setIsEnable(d.getIsEnable());
DeviceOperationRecordDO record = latestRecordMap.get(d.getId());
if (record != null) {
DeviceStatusEnum statusEnum = DeviceStatusEnum.getByCode(record.getRule());
vo.setOperatingStatus(statusEnum != null ? statusEnum.getName() : DeviceStatusEnum.OFFLINE.getName());
} else {
vo.setOperatingStatus(DeviceStatusEnum.OFFLINE.getName());
}
result.add(vo);
}
return result;
}
}

@ -200,8 +200,68 @@
<select id="getAllDeviceIds" resultType="java.lang.Long">
SELECT id
FROM besure.iot_device
FROM iot_device
WHERE deleted = 0
ORDER BY id
</select>
</mapper>
<select id="selectByTenantCustomerAndOrgNodeIds"
resultType="cn.iocoder.yudao.module.iot.dal.dataobject.device.DeviceDO">
SELECT *
FROM iot_device
WHERE deleted = 0
AND tenant_id = #{tenantId}
AND customer_id = #{customerId}
AND org_node_id IN
<foreach collection="orgNodeIds" item="id" open="(" separator="," close=")">
#{id}
</foreach>
</select>
<select id="selectByCustomerAndOrgNodeIds"
resultType="cn.iocoder.yudao.module.iot.dal.dataobject.device.DeviceDO">
SELECT *
FROM iot_device
WHERE deleted = 0
AND customer_id = #{customerId}
AND (
org_node_id IS NULL
OR org_node_id = 0
<if test="orgNodeIds != null and orgNodeIds.size() > 0">
OR org_node_id IN
<foreach collection="orgNodeIds" item="id" open="(" separator="," close=")">
#{id}
</foreach>
</if>
)
</select>
<select id="selectByCustomerId" resultType="cn.iocoder.yudao.module.iot.dal.dataobject.device.DeviceDO">
SELECT *
FROM iot_device
WHERE deleted = 0
AND customer_id = #{customerId}
</select>
<select id="selectByOrgNodeIds" resultType="cn.iocoder.yudao.module.iot.dal.dataobject.device.DeviceDO">
SELECT *
FROM iot_device
WHERE deleted = 0
AND org_node_id IN
<foreach collection="orgNodeIds" item="id" open="(" separator="," close=")">
#{id}
</foreach>
</select>
<select id="existsByOrgNodeId" resultType="boolean">
SELECT CASE WHEN COUNT(1) > 0 THEN TRUE ELSE FALSE END
FROM iot_device
WHERE deleted = 0
AND org_node_id = #{orgNodeId}
</select>
</mapper>

@ -21,4 +21,34 @@
</select>
<select id="selectListByCustomerByNodeId"
resultType="cn.iocoder.yudao.module.iot.dal.dataobject.orgnode.OrgNodeDO">
SELECT n.id, n.tenant_id, n.customer_id, n.parent_id, n.node_type, n.name, n.sort,
n.creator, n.create_time, n.updater, n.update_time, n.deleted
FROM iot_org_node n
INNER JOIN (
SELECT customer_id
FROM iot_org_node
WHERE id = #{nodeId}
AND deleted = b'0'
LIMIT 1
) t ON n.customer_id = t.customer_id
WHERE n.deleted = b'0'
ORDER BY n.sort ASC, n.id ASC
</select>
<select id="existsByParentId" resultType="boolean">
SELECT CASE WHEN COUNT(1) > 0 THEN TRUE ELSE FALSE END
FROM iot_org_node
WHERE deleted = b'0'
AND parent_id = #{parentId}
</select>
<select id="existsByOrgNodeId" resultType="boolean">
SELECT CASE WHEN COUNT(1) > 0 THEN TRUE ELSE FALSE END
FROM iot_device
WHERE deleted = 0
AND org_node_id = #{orgNodeId}
</select>
</mapper>

@ -0,0 +1,97 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="cn.iocoder.yudao.module.iot.dal.tdengine.IotDeviceMapper">
<!--
一般情况下,尽可能使用 Mapper 进行 CRUD 增删改查即可。
无法满足的场景,例如说多表关联查询,才使用 XML 编写 SQL。
代码生成器暂时只生成 Mapper XML 文件本身,更多推荐 MybatisX 快速开发插件来生成查询。
文档可见https://www.iocoder.cn/MyBatis/x-plugins/
-->
<select id="countWarningRecordByTime" resultType="java.lang.Long">
SELECT COUNT(*)
FROM besure_server.iot_device_warning_record
WHERE 1 = 1
<if test="startTime != null">
AND create_time <![CDATA[>=]]> #{startTime}
</if>
<if test="endTime != null">
AND create_time <![CDATA[<=]]> #{endTime}
</if>
</select>
<select id="selectWarningRecordList"
resultType="cn.iocoder.yudao.module.iot.dal.dataobject.devicewarinningrecord.DeviceWarinningRecordDO">
SELECT id, device_id, model_id, rule, alarm_level, address_value, rule_id,
device_name, model_name, rule_name, create_time
FROM besure_server.iot_device_warning_record
WHERE deleted = 0
AND create_time &gt;= #{startTime}
AND create_time &lt; #{endTime}
<if test="deviceId != null">
AND device_id = #{deviceId}
</if>
<if test="alarmLevel != null and alarmLevel != ''">
AND alarm_level = #{alarmLevel}
</if>
ORDER BY create_time DESC
LIMIT 1000
</select>
<!-- IotDeviceMapper.xml -->
<select id="selectWarningCount"
resultType="cn.iocoder.yudao.module.iot.controller.admin.devicewarinningrecord.vo.DeviceWarningCountRespVO">
SELECT
COUNT(1) AS totalCount,
SUM(CASE WHEN alarm_level = '1' THEN 1 ELSE 0 END) AS normalCount,
SUM(CASE WHEN alarm_level = '2' THEN 1 ELSE 0 END) AS tipCount,
SUM(CASE WHEN alarm_level = '0' THEN 1 ELSE 0 END) AS seriousCount
FROM besure_server.iot_device_warning_record
WHERE deleted = 0
AND create_time &gt;= #{startTime}
AND create_time &lt;= #{endTime}
</select>
<select id="selectWarningTrendByHour"
resultType="cn.iocoder.yudao.module.iot.dal.dataobject.devicewarinningrecord.WarningTrendPointRespDTO">
SELECT _wstart AS timeKey, COUNT(*) AS cnt
FROM besure_server.iot_device_warning_record
WHERE deleted = 0
AND create_time &gt;= #{startTime}
AND create_time &lt;= #{endTime}
INTERVAL(1h)
ORDER BY timeKey
</select>
<select id="selectWarningTrendByDay"
resultType="cn.iocoder.yudao.module.iot.dal.dataobject.devicewarinningrecord.WarningTrendPointRespDTO">
SELECT _wstart AS timeKey, COUNT(*) AS cnt
FROM besure_server.iot_device_warning_record
WHERE deleted = 0
AND create_time &gt;= #{startTime}
AND create_time &lt;= #{endTime}
INTERVAL(1d)
ORDER BY timeKey
</select>
<select id="selectLatestRuleByDeviceIds"
resultType="cn.iocoder.yudao.module.iot.controller.admin.device.dto.DeviceLatestRuleDTO">
SELECT device_id AS deviceId,
LAST(rule) AS rule
FROM besure_server.iot_device_operation_record
WHERE deleted = 0
<if test="deviceIds != null and deviceIds.size() > 0">
AND device_id IN
<foreach collection="deviceIds" item="id" open="(" separator="," close=")">
#{id}
</foreach>
</if>
PARTITION BY device_id
</select>
</mapper>

@ -1,12 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="cn.iocoder.yudao.module.iot.dal.tdengine.IotDevicePropertyMapper">
<!--
一般情况下,尽可能使用 Mapper 进行 CRUD 增删改查即可。
无法满足的场景,例如说多表关联查询,才使用 XML 编写 SQL。
代码生成器暂时只生成 Mapper XML 文件本身,更多推荐 MybatisX 快速开发插件来生成查询。
文档可见https://www.iocoder.cn/MyBatis/x-plugins/
-->
</mapper>

@ -48,31 +48,31 @@ spring:
datasource:
master:
name: besure-digital-center
url: jdbc:mysql://192.168.5.5:3307/${spring.datasource.dynamic.datasource.master.name}?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true
url: jdbc:mysql://ngsk.tech:3307/${spring.datasource.dynamic.datasource.master.name}?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true
username: root
password: ngsk0809
driver-class-name: com.mysql.cj.jdbc.Driver
#
# tdengine:
# name: tdengine
# url: jdbc:TAOS-RS://ngsk.tech:16042/besure_server?charset=UTF-8&locale=en_US.UTF-8
# username: root
# password: taosdata
# driver-class-name: com.taosdata.jdbc.rs.RestfulDriver
# druid: # TDengine专用配置
# initial-size: 1
# max-active: 5 # TDengine建议较小的连接池
# min-idle: 1
# max-wait: 30000 # 缩短等待时间
# time-between-eviction-runs-millis: 60000
# min-evictable-idle-time-millis: 300000
# validation-query: SELECT 1
# test-while-idle: true
# test-on-borrow: false
# test-on-return: false
# pool-prepared-statements: false # TDengine REST驱动不支持预处理语句
# max-pool-prepared-statement-per-connection-size: -1
# connection-properties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=1000
tdengine:
name: tdengine
url: jdbc:TAOS-RS://192.168.5.119:6042/besure_server?charset=UTF-8&locale=en_US.UTF-8
username: root
password: taosdata
driver-class-name: com.taosdata.jdbc.rs.RestfulDriver
druid: # TDengine专用配置
initial-size: 1
max-active: 5 # TDengine建议较小的连接池
min-idle: 1
max-wait: 30000 # 缩短等待时间
time-between-eviction-runs-millis: 60000
min-evictable-idle-time-millis: 300000
validation-query: SELECT 1
test-while-idle: true
test-on-borrow: false
test-on-return: false
pool-prepared-statements: false # TDengine REST驱动不支持预处理语句
max-pool-prepared-statement-per-connection-size: -1
connection-properties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=1000
# Redis 配置。Redisson 默认的配置足够使用,一般不需要进行调优
redis:
@ -86,7 +86,7 @@ spring:
# Quartz 配置项,对应 QuartzProperties 配置类
spring:
quartz:
auto-startup: false # 测试环境,需要开启 Job
auto-startup: true # 测试环境,需要开启 Job
scheduler-name: schedulerName # Scheduler 名字。默认为 schedulerName
job-store-type: jdbc # Job 存储器类型。默认为 memory 表示内存,可选 jdbc 使用数据库。
wait-for-jobs-to-complete-on-shutdown: true # 应用关闭时,是否等待定时任务执行完成。默认为 false ,建议设置为 true

Loading…
Cancel
Save