【同步】最新精简版本!

This commit is contained in:
YunaiV 2025-10-12 15:33:30 +08:00
parent 33de41b4da
commit 2bc0df6819
11 changed files with 0 additions and 1230 deletions

View File

@ -1,42 +0,0 @@
package cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.List;
@Schema(description = "管理后台 - 流程实例的打印数据 Response VO")
@Data
public class BpmProcessPrintDataRespVO {
@Schema(description = "流程实例数据")
private BpmProcessInstanceRespVO processInstance;
@Schema(description = "是否开启自定义打印模板", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
private Boolean printTemplateEnable;
@Schema(description = "自定义打印模板 HTML")
private String printTemplateHtml;
@Schema(description = "审批任务列表")
private List<Task> tasks;
@Schema(description = "流程任务")
@Data
public static class Task {
@Schema(description = "流程任务的编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
private String id;
@Schema(description = "任务名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋道")
private String name;
@Schema(description = "签名 URL", example = "https://www.iocoder.cn/sign.png")
private String signPicUrl;
@Schema(description = "任务描述", requiredMode = Schema.RequiredMode.REQUIRED)
private String description; // 该字段由后端拼接
}
}

View File

@ -1,63 +0,0 @@
package cn.iocoder.yudao.module.iot.dal.dataobject.rule.config;
import lombok.Data;
/**
* IoT TCP 配置 {@link IotAbstractDataSinkConfig} 实现类
*
* @author HUIHUI
*/
@Data
public class IotDataSinkTcpConfig extends IotAbstractDataSinkConfig {
/**
* TCP 服务器地址
*/
private String host;
/**
* TCP 服务器端口
*/
private Integer port;
/**
* 连接超时时间毫秒
*/
private Integer connectTimeoutMs = 5000;
/**
* 读取超时时间毫秒
*/
private Integer readTimeoutMs = 10000;
/**
* 是否启用 SSL
*/
private Boolean ssl = false;
/**
* SSL 证书路径 ssl=true 时需要
*/
private String sslCertPath;
/**
* 数据格式JSON BINARY
*/
private String dataFormat = "JSON";
/**
* 心跳间隔时间毫秒0 表示不启用心跳
*/
private Long heartbeatIntervalMs = 30000L;
/**
* 重连间隔时间毫秒
*/
private Long reconnectIntervalMs = 5000L;
/**
* 最大重连次数
*/
private Integer maxReconnectAttempts = 3;
}

View File

@ -1,87 +0,0 @@
package cn.iocoder.yudao.module.iot.dal.dataobject.rule.config;
import lombok.Data;
/**
* IoT WebSocket 配置 {@link IotAbstractDataSinkConfig} 实现类
* <p>
* 配置设备消息通过 WebSocket 协议发送到外部 WebSocket 服务器
* 支持 WebSocket (ws://) WebSocket Secure (wss://) 连接
*
* @author HUIHUI
*/
@Data
public class IotDataSinkWebSocketConfig extends IotAbstractDataSinkConfig {
/**
* WebSocket 服务器地址
* 例如ws://localhost:8080/ws wss://example.com/ws
*/
private String serverUrl;
/**
* 连接超时时间毫秒
*/
private Integer connectTimeoutMs = 5000;
/**
* 发送超时时间毫秒
*/
private Integer sendTimeoutMs = 10000;
/**
* 心跳间隔时间毫秒0 表示不启用心跳
*/
private Long heartbeatIntervalMs = 30000L;
/**
* 心跳消息内容JSON 格式
*/
private String heartbeatMessage = "{\"type\":\"heartbeat\"}";
/**
* 子协议列表逗号分隔
*/
private String subprotocols;
/**
* 自定义请求头JSON 格式
*/
private String customHeaders;
/**
* 是否启用 SSL 证书验证仅对 wss:// 生效
*/
private Boolean verifySslCert = true;
/**
* 数据格式JSON TEXT
*/
private String dataFormat = "JSON";
/**
* 重连间隔时间毫秒
*/
private Long reconnectIntervalMs = 5000L;
/**
* 最大重连次数
*/
private Integer maxReconnectAttempts = 3;
/**
* 是否启用压缩
*/
private Boolean enableCompression = false;
/**
* 消息发送重试次数
*/
private Integer sendRetryCount = 1;
/**
* 消息发送重试间隔毫秒
*/
private Long sendRetryIntervalMs = 1000L;
}

View File

@ -1,91 +0,0 @@
package cn.iocoder.yudao.module.iot.service.rule.data.action;
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
import cn.iocoder.yudao.module.iot.dal.dataobject.rule.config.IotDataSinkTcpConfig;
import cn.iocoder.yudao.module.iot.enums.rule.IotDataSinkTypeEnum;
import cn.iocoder.yudao.module.iot.service.rule.data.action.tcp.IotTcpClient;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.time.Duration;
/**
* TCP {@link IotDataRuleAction} 实现类
* <p>
* 负责将设备消息发送到外部 TCP 服务器
* 支持普通 TCP SSL TCP 连接支持 JSON BINARY 数据格式
* 使用连接池管理 TCP 连接提高性能和资源利用率
*
* @author HUIHUI
*/
@Component
@Slf4j
public class IotTcpDataRuleAction extends
IotDataRuleCacheableAction<IotDataSinkTcpConfig, IotTcpClient> {
private static final Duration CONNECT_TIMEOUT = Duration.ofSeconds(5);
private static final Duration SEND_TIMEOUT = Duration.ofSeconds(10);
@Override
public Integer getType() {
return IotDataSinkTypeEnum.TCP.getType();
}
@Override
protected IotTcpClient initProducer(IotDataSinkTcpConfig config) throws Exception {
// 1.1 参数校验
if (config.getHost() == null || config.getHost().trim().isEmpty()) {
throw new IllegalArgumentException("TCP 服务器地址不能为空");
}
if (config.getPort() == null || config.getPort() <= 0 || config.getPort() > 65535) {
throw new IllegalArgumentException("TCP 服务器端口无效");
}
// 2.1 创建 TCP 客户端
IotTcpClient tcpClient = new IotTcpClient(
config.getHost(),
config.getPort(),
config.getConnectTimeoutMs(),
config.getReadTimeoutMs(),
config.getSsl(),
config.getSslCertPath(),
config.getDataFormat()
);
// 2.2 连接服务器
tcpClient.connect();
log.info("[initProducer][TCP 客户端创建并连接成功,服务器: {}:{}SSL: {},数据格式: {}]",
config.getHost(), config.getPort(), config.getSsl(), config.getDataFormat());
return tcpClient;
}
@Override
protected void closeProducer(IotTcpClient producer) throws Exception {
if (producer != null) {
producer.close();
}
}
@Override
protected void execute(IotDeviceMessage message, IotDataSinkTcpConfig config) throws Exception {
try {
// 1.1 获取或创建 TCP 客户端
IotTcpClient tcpClient = getProducer(config);
// 1.2 检查连接状态如果断开则重新连接
if (!tcpClient.isConnected()) {
log.warn("[execute][TCP 连接已断开,尝试重新连接,服务器: {}:{}]", config.getHost(), config.getPort());
tcpClient.connect();
}
// 2.1 发送消息并等待结果
tcpClient.sendMessage(message);
// 2.2 记录发送成功日志
log.info("[execute][message({}) config({}) 发送成功TCP 服务器: {}:{}]",
message, config, config.getHost(), config.getPort());
} catch (Exception e) {
log.error("[execute][message({}) config({}) 发送失败TCP 服务器: {}:{}]",
message, config, config.getHost(), config.getPort(), e);
throw e;
}
}
}

View File

@ -1,184 +0,0 @@
package cn.iocoder.yudao.module.iot.service.rule.data.action.tcp;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
import lombok.extern.slf4j.Slf4j;
import javax.net.ssl.SSLSocketFactory;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* IoT TCP 客户端
* <p>
* 负责与外部 TCP 服务器建立连接并发送设备消息
* 支持 JSON BINARY 两种数据格式支持 SSL 加密连接
*
* @author HUIHUI
*/
@Slf4j
public class IotTcpClient {
private final String host;
private final Integer port;
private final Integer connectTimeoutMs;
private final Integer readTimeoutMs;
private final Boolean ssl;
private final String sslCertPath;
private final String dataFormat;
private Socket socket;
private OutputStream outputStream;
private BufferedReader reader;
private final AtomicBoolean connected = new AtomicBoolean(false);
// TODO @puhui999default IotDataSinkTcpConfig.java 枚举起来哈
public IotTcpClient(String host, Integer port, Integer connectTimeoutMs, Integer readTimeoutMs,
Boolean ssl, String sslCertPath, String dataFormat) {
this.host = host;
this.port = port;
this.connectTimeoutMs = connectTimeoutMs != null ? connectTimeoutMs : 5000;
this.readTimeoutMs = readTimeoutMs != null ? readTimeoutMs : 10000;
this.ssl = ssl != null ? ssl : false;
this.sslCertPath = sslCertPath;
this.dataFormat = dataFormat != null ? dataFormat : "JSON";
}
/**
* 连接到 TCP 服务器
*/
public void connect() throws Exception {
if (connected.get()) {
log.warn("[connect][TCP 客户端已经连接,无需重复连接]");
return;
}
try {
if (ssl) {
// SSL 连接
SSLSocketFactory sslSocketFactory = (SSLSocketFactory) SSLSocketFactory.getDefault();
socket = sslSocketFactory.createSocket();
} else {
// 普通连接
socket = new Socket();
}
// 连接服务器
socket.connect(new InetSocketAddress(host, port), connectTimeoutMs);
socket.setSoTimeout(readTimeoutMs);
// 获取输入输出流
outputStream = socket.getOutputStream();
reader = new BufferedReader(new InputStreamReader(socket.getInputStream(), StandardCharsets.UTF_8));
// 更新状态
connected.set(true);
log.info("[connect][TCP 客户端连接成功,服务器地址: {}:{}]", host, port);
} catch (Exception e) {
close();
log.error("[connect][TCP 客户端连接失败,服务器地址: {}:{}]", host, port, e);
throw e;
}
}
/**
* 发送设备消息
*
* @param message 设备消息
* @throws Exception 发送异常
*/
public void sendMessage(IotDeviceMessage message) throws Exception {
if (!connected.get()) {
throw new IllegalStateException("TCP 客户端未连接");
}
try {
// TODO @puhui999枚举值
String messageData;
if ("JSON".equalsIgnoreCase(dataFormat)) {
// JSON 格式
messageData = JsonUtils.toJsonString(message);
} else {
// BINARY 格式这里简化为字符串实际可能需要自定义二进制协议
messageData = message.toString();
}
// 发送消息
outputStream.write(messageData.getBytes(StandardCharsets.UTF_8));
outputStream.write('\n'); // 添加换行符作为消息分隔符
outputStream.flush();
log.debug("[sendMessage][发送消息成功,设备 ID: {},消息长度: {}]",
message.getDeviceId(), messageData.length());
} catch (Exception e) {
log.error("[sendMessage][发送消息失败,设备 ID: {}]", message.getDeviceId(), e);
throw e;
}
}
/**
* 关闭连接
*/
public void close() {
if (!connected.get()) {
return;
}
try {
// 关闭资源
if (reader != null) {
try {
reader.close();
} catch (IOException e) {
log.warn("[close][关闭输入流失败]", e);
}
}
if (outputStream != null) {
try {
outputStream.close();
} catch (IOException e) {
log.warn("[close][关闭输出流失败]", e);
}
}
if (socket != null) {
try {
socket.close();
} catch (IOException e) {
log.warn("[close][关闭 Socket 失败]", e);
}
}
// 更新状态
connected.set(false);
log.info("[close][TCP 客户端连接已关闭,服务器地址: {}:{}]", host, port);
} catch (Exception e) {
log.error("[close][关闭 TCP 客户端连接异常]", e);
}
}
/**
* 检查连接状态
*
* @return 是否已连接
*/
public boolean isConnected() {
return connected.get() && socket != null && !socket.isClosed();
}
@Override
public String toString() {
return "IotTcpClient{" +
"host='" + host + '\'' +
", port=" + port +
", ssl=" + ssl +
", dataFormat='" + dataFormat + '\'' +
", connected=" + connected.get() +
'}';
}
}

View File

@ -1,148 +0,0 @@
package cn.iocoder.yudao.module.iot.service.rule.scene.action;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum;
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO;
import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO;
import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleActionTypeEnum;
import cn.iocoder.yudao.module.iot.service.device.IotDeviceService;
import cn.iocoder.yudao.module.iot.service.device.message.IotDeviceMessageService;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* IoT 设备服务调用的 {@link IotSceneRuleAction} 实现类
*
* @author HUIHUI
*/
@Component
@Slf4j
public class IotDeviceServiceInvokeSceneRuleAction implements IotSceneRuleAction {
@Resource
private IotDeviceService deviceService;
@Resource
private IotDeviceMessageService deviceMessageService;
@Override
public void execute(IotDeviceMessage message,
IotSceneRuleDO rule, IotSceneRuleDO.Action actionConfig) {
// 1. 参数校验
if (actionConfig.getDeviceId() == null) {
log.error("[execute][规则场景({}) 动作配置({}) 设备编号不能为空]", rule.getId(), actionConfig);
return;
}
if (StrUtil.isEmpty(actionConfig.getIdentifier())) {
log.error("[execute][规则场景({}) 动作配置({}) 服务标识符不能为空]", rule.getId(), actionConfig);
return;
}
// 2. 判断是否为全部设备
if (IotDeviceDO.DEVICE_ID_ALL.equals(actionConfig.getDeviceId())) {
executeForAllDevices(message, rule, actionConfig);
} else {
executeForSingleDevice(message, rule, actionConfig);
}
}
/**
* 为单个设备执行服务调用
*/
private void executeForSingleDevice(IotDeviceMessage message,
IotSceneRuleDO rule, IotSceneRuleDO.Action actionConfig) {
// 1. 获取设备信息
IotDeviceDO device = deviceService.getDeviceFromCache(actionConfig.getDeviceId());
if (device == null) {
log.error("[executeForSingleDevice][规则场景({}) 动作配置({}) 对应的设备({}) 不存在]",
rule.getId(), actionConfig, actionConfig.getDeviceId());
return;
}
// 2. 执行服务调用
executeServiceInvokeForDevice(rule, actionConfig, device);
}
/**
* 为产品下的所有设备执行服务调用
*/
private void executeForAllDevices(IotDeviceMessage message,
IotSceneRuleDO rule, IotSceneRuleDO.Action actionConfig) {
// 1. 参数校验
if (actionConfig.getProductId() == null) {
log.error("[executeForAllDevices][规则场景({}) 动作配置({}) 产品编号不能为空]", rule.getId(), actionConfig);
return;
}
// 2. 获取产品下的所有设备
List<IotDeviceDO> devices = deviceService.getDeviceListByProductId(actionConfig.getProductId());
if (CollUtil.isEmpty(devices)) {
log.warn("[executeForAllDevices][规则场景({}) 动作配置({}) 产品({}) 下没有设备]",
rule.getId(), actionConfig, actionConfig.getProductId());
return;
}
// 3. 遍历所有设备执行服务调用
for (IotDeviceDO device : devices) {
executeServiceInvokeForDevice(rule, actionConfig, device);
}
}
/**
* 为指定设备执行服务调用
*/
private void executeServiceInvokeForDevice(IotSceneRuleDO rule, IotSceneRuleDO.Action actionConfig, IotDeviceDO device) {
// 1. 构建服务调用消息
IotDeviceMessage downstreamMessage = buildServiceInvokeMessage(actionConfig, device);
if (downstreamMessage == null) {
log.error("[executeServiceInvokeForDevice][规则场景({}) 动作配置({}) 设备({}) 构建服务调用消息失败]",
rule.getId(), actionConfig, device.getId());
return;
}
// 2. 发送设备消息
try {
IotDeviceMessage result = deviceMessageService.sendDeviceMessage(downstreamMessage, device);
log.info("[executeServiceInvokeForDevice][规则场景({}) 动作配置({}) 设备({}) 服务调用消息({}) 发送成功]",
rule.getId(), actionConfig, device.getId(), result.getId());
} catch (Exception e) {
log.error("[executeServiceInvokeForDevice][规则场景({}) 动作配置({}) 设备({}) 服务调用消息发送失败]",
rule.getId(), actionConfig, device.getId(), e);
}
}
/**
* 构建服务调用消息
*
* @param actionConfig 动作配置
* @param device 设备信息
* @return 设备消息
*/
private IotDeviceMessage buildServiceInvokeMessage(IotSceneRuleDO.Action actionConfig, IotDeviceDO device) {
try {
// 服务调用参数格式: {"identifier": "serviceId", "params": {...}}
Object params = MapUtil.builder()
.put("identifier", actionConfig.getIdentifier())
.put("params", actionConfig.getParams() != null ? actionConfig.getParams() : Collections.emptyMap())
.build();
return IotDeviceMessage.requestOf(IotDeviceMessageMethodEnum.SERVICE_INVOKE.getMethod(), params);
} catch (Exception e) {
log.error("[buildServiceInvokeMessage][构建服务调用消息异常]", e);
return null;
}
}
@Override
public IotSceneRuleActionTypeEnum getType() {
return IotSceneRuleActionTypeEnum.DEVICE_SERVICE_INVOKE;
}
}

View File

@ -1,154 +0,0 @@
package cn.iocoder.yudao.module.iot.service.rule.scene.timer;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO;
import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum;
import cn.iocoder.yudao.module.iot.framework.job.core.IotSchedulerManager;
import cn.iocoder.yudao.module.iot.job.rule.IotSceneRuleJob;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.quartz.SchedulerException;
import org.springframework.stereotype.Component;
import java.util.List;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.filterList;
/**
* IoT 场景规则定时触发器处理器负责管理定时触发器的注册更新删除等操作
*
* @author HUIHUI
*/
@Component
@Slf4j
public class IotSceneRuleTimerHandler {
@Resource(name = "iotSchedulerManager")
private IotSchedulerManager schedulerManager;
/**
* 注册场景规则的定时触发器
*
* @param sceneRule 场景规则
*/
public void registerTimerTriggers(IotSceneRuleDO sceneRule) {
// 1. 过滤出定时触发器
if (sceneRule == null || CollUtil.isEmpty(sceneRule.getTriggers())) {
return;
}
List<IotSceneRuleDO.Trigger> timerTriggers = filterList(sceneRule.getTriggers(),
trigger -> ObjUtil.equals(trigger.getType(), IotSceneRuleTriggerTypeEnum.TIMER.getType()));
if (CollUtil.isEmpty(timerTriggers)) {
return;
}
// 2. 注册每个定时触发器
timerTriggers.forEach(trigger -> registerSingleTimerTrigger(sceneRule, trigger));
}
/**
* 更新场景规则的定时触发器
*
* @param sceneRule 场景规则
*/
public void updateTimerTriggers(IotSceneRuleDO sceneRule) {
if (sceneRule == null) {
return;
}
// 1. 先删除旧的定时任务
unregisterTimerTriggers(sceneRule.getId());
// 2.1 如果场景规则已禁用则不重新注册
if (CommonStatusEnum.isDisable(sceneRule.getStatus())) {
log.info("[updateTimerTriggers][场景规则({}) 已禁用,不注册定时触发器]", sceneRule.getId());
return;
}
// 2.2 重新注册定时触发器
registerTimerTriggers(sceneRule);
}
/**
* 注销场景规则的定时触发器
*
* @param sceneRuleId 场景规则 ID
*/
public void unregisterTimerTriggers(Long sceneRuleId) {
if (sceneRuleId == null) {
return;
}
String jobName = buildJobName(sceneRuleId);
try {
schedulerManager.deleteJob(jobName);
log.info("[unregisterTimerTriggers][场景规则({}) 定时触发器注销成功]", sceneRuleId);
} catch (SchedulerException e) {
log.error("[unregisterTimerTriggers][场景规则({}) 定时触发器注销失败]", sceneRuleId, e);
}
}
/**
* 暂停场景规则的定时触发器
*
* @param sceneRuleId 场景规则 ID
*/
public void pauseTimerTriggers(Long sceneRuleId) {
if (sceneRuleId == null) {
return;
}
String jobName = buildJobName(sceneRuleId);
try {
schedulerManager.pauseJob(jobName);
log.info("[pauseTimerTriggers][场景规则({}) 定时触发器暂停成功]", sceneRuleId);
} catch (SchedulerException e) {
log.error("[pauseTimerTriggers][场景规则({}) 定时触发器暂停失败]", sceneRuleId, e);
}
}
/**
* 注册单个定时触发器
*
* @param sceneRule 场景规则
* @param trigger 定时触发器配置
*/
private void registerSingleTimerTrigger(IotSceneRuleDO sceneRule, IotSceneRuleDO.Trigger trigger) {
// 1. 参数校验
if (StrUtil.isBlank(trigger.getCronExpression())) {
log.error("[registerSingleTimerTrigger][场景规则({}) 定时触发器缺少 CRON 表达式]", sceneRule.getId());
return;
}
try {
// 2.1 构建任务名称和数据
String jobName = buildJobName(sceneRule.getId());
// 2.2 注册定时任务
schedulerManager.addOrUpdateJob(
IotSceneRuleJob.class,
jobName,
trigger.getCronExpression(),
IotSceneRuleJob.buildJobDataMap(sceneRule.getId())
);
log.info("[registerSingleTimerTrigger][场景规则({}) 定时触发器注册成功CRON: {}]",
sceneRule.getId(), trigger.getCronExpression());
} catch (SchedulerException e) {
log.error("[registerSingleTimerTrigger][场景规则({}) 定时触发器注册失败CRON: {}]",
sceneRule.getId(), trigger.getCronExpression(), e);
}
}
/**
* 构建任务名称
*
* @param sceneRuleId 场景规则 ID
* @return 任务名称
*/
private String buildJobName(Long sceneRuleId) {
return "iot_scene_rule_timer_" + sceneRuleId;
}
}

View File

@ -1,162 +0,0 @@
package cn.iocoder.yudao.module.iot.service.rule.data.action;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
import cn.iocoder.yudao.module.iot.dal.dataobject.rule.config.IotDataSinkTcpConfig;
import cn.iocoder.yudao.module.iot.service.rule.data.action.tcp.IotTcpClient;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
/**
* {@link IotTcpDataRuleAction} 的单元测试
*
* @author HUIHUI
*/
class IotTcpDataRuleActionTest {
private IotTcpDataRuleAction tcpDataRuleAction;
@Mock
private IotTcpClient mockTcpClient;
@BeforeEach
public void setUp() {
MockitoAnnotations.openMocks(this);
tcpDataRuleAction = new IotTcpDataRuleAction();
}
@Test
public void testGetType() {
// 准备参数
Integer expectedType = 2; // 数据接收类型枚举中 TCP 类型的值
// 调用方法
Integer actualType = tcpDataRuleAction.getType();
// 断言结果
assertEquals(expectedType, actualType);
}
// TODO @puhui999_ 后面是小写哈单测的命名规则
@Test
public void testInitProducer_Success() throws Exception {
// 准备参数
IotDataSinkTcpConfig config = new IotDataSinkTcpConfig();
config.setHost("localhost");
config.setPort(8080);
config.setDataFormat("JSON");
config.setSsl(false);
// 调用方法 & 断言结果
// 此测试需要实际的 TCP 连接在单元测试中可能不可用
// 目前我们只验证配置是否有效
assertNotNull(config.getHost());
assertTrue(config.getPort() > 0 && config.getPort() <= 65535);
}
@Test
public void testInitProducer_InvalidHost() {
// 准备参数
IotDataSinkTcpConfig config = new IotDataSinkTcpConfig();
config.setHost("");
config.setPort(8080);
// 调用方法 & 断言结果
IotTcpDataRuleAction action = new IotTcpDataRuleAction();
// 测试验证逻辑通常在 initProducer 方法中
assertThrows(IllegalArgumentException.class, () -> {
if (config.getHost() == null || config.getHost().trim().isEmpty()) {
throw new IllegalArgumentException("TCP 服务器地址不能为空");
}
});
}
@Test
public void testInitProducer_InvalidPort() {
// 准备参数
IotDataSinkTcpConfig config = new IotDataSinkTcpConfig();
config.setHost("localhost");
config.setPort(-1);
// 调用方法 & 断言结果
assertThrows(IllegalArgumentException.class, () -> {
if (config.getPort() == null || config.getPort() <= 0 || config.getPort() > 65535) {
throw new IllegalArgumentException("TCP 服务器端口无效");
}
});
}
@Test
public void testCloseProducer() throws Exception {
// 准备参数
IotTcpClient client = mock(IotTcpClient.class);
// 调用方法
tcpDataRuleAction.closeProducer(client);
// 断言结果
verify(client, times(1)).close();
}
@Test
public void testExecute_WithValidConfig() {
// 准备参数
IotDeviceMessage message = IotDeviceMessage.requestOf("thing.property.report",
"{\"temperature\": 25.5, \"humidity\": 60}");
IotDataSinkTcpConfig config = new IotDataSinkTcpConfig();
config.setHost("localhost");
config.setPort(8080);
config.setDataFormat("JSON");
// 调用方法 & 断言结果
// 通常这需要实际的 TCP 连接
// 在单元测试中我们只验证输入参数
assertNotNull(message);
assertNotNull(config);
assertNotNull(config.getHost());
assertTrue(config.getPort() > 0);
}
@Test
public void testConfig_DefaultValues() {
// 准备参数
IotDataSinkTcpConfig config = new IotDataSinkTcpConfig();
// 调用方法 & 断言结果
// 验证默认值
assertEquals("JSON", config.getDataFormat());
assertEquals(5000, config.getConnectTimeoutMs());
assertEquals(10000, config.getReadTimeoutMs());
assertEquals(false, config.getSsl());
assertEquals(30000L, config.getHeartbeatIntervalMs());
assertEquals(5000L, config.getReconnectIntervalMs());
assertEquals(3, config.getMaxReconnectAttempts());
}
@Test
public void testMessageSerialization() {
// 准备参数
IotDeviceMessage message = IotDeviceMessage.builder()
.deviceId(123L)
.method("thing.property.report")
.params("{\"temperature\": 25.5}")
.build();
// 调用方法
String json = JsonUtils.toJsonString(message);
// 断言结果
assertNotNull(json);
assertTrue(json.contains("\"deviceId\":123"));
assertTrue(json.contains("\"method\":\"thing.property.report\""));
assertTrue(json.contains("\"temperature\":25.5"));
}
}

View File

@ -1,32 +0,0 @@
package cn.iocoder.yudao.module.iot.service.rule.scene.matcher;
import cn.hutool.extra.spring.SpringUtil;
import cn.iocoder.yudao.framework.common.util.spring.SpringExpressionUtils;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
/**
* Matcher 测试基类
* 提供通用的 Spring 测试配置
*
* @author HUIHUI
*/
@SpringJUnitConfig
public abstract class IotBaseConditionMatcherTest {
/**
* 注入一下 SpringUtil解析 EL 表达式时需要
* {@link SpringExpressionUtils#parseExpression}
*/
@Configuration
static class TestConfig {
@Bean
public SpringUtil springUtil() {
return new SpringUtil();
}
}
}

View File

@ -1,126 +0,0 @@
package cn.iocoder.yudao.module.iot.service.rule.scene.timer;
import cn.hutool.core.collection.ListUtil;
import cn.iocoder.yudao.module.iot.dal.dataobject.rule.IotSceneRuleDO;
import cn.iocoder.yudao.module.iot.enums.rule.IotSceneRuleTriggerTypeEnum;
import cn.iocoder.yudao.module.iot.framework.job.core.IotSchedulerManager;
import cn.iocoder.yudao.module.iot.job.rule.IotSceneRuleJob;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.quartz.SchedulerException;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.*;
/**
* {@link IotSceneRuleTimerHandler} 的单元测试类
*
* @author HUIHUI
*/
@ExtendWith(MockitoExtension.class)
public class IotSceneRuleTimerHandlerTest {
@Mock
private IotSchedulerManager schedulerManager;
@InjectMocks
private IotSceneRuleTimerHandler timerHandler;
@BeforeEach
void setUp() {
// 重置 Mock 对象
reset(schedulerManager);
}
@Test
public void testRegisterTimerTriggers_success() throws SchedulerException {
// 准备参数
Long sceneRuleId = 1L;
IotSceneRuleDO sceneRule = new IotSceneRuleDO();
sceneRule.setId(sceneRuleId);
sceneRule.setStatus(0); // 0 表示启用
// 创建定时触发器
IotSceneRuleDO.Trigger timerTrigger = new IotSceneRuleDO.Trigger();
timerTrigger.setType(IotSceneRuleTriggerTypeEnum.TIMER.getType());
timerTrigger.setCronExpression("0 0 12 * * ?"); // 每天中午12点
sceneRule.setTriggers(ListUtil.toList(timerTrigger));
// 调用
timerHandler.registerTimerTriggers(sceneRule);
// 验证
verify(schedulerManager, times(1)).addOrUpdateJob(
eq(IotSceneRuleJob.class),
eq("iot_scene_rule_timer_" + sceneRuleId),
eq("0 0 12 * * ?"),
eq(IotSceneRuleJob.buildJobDataMap(sceneRuleId))
);
}
@Test
public void testRegisterTimerTriggers_noTimerTrigger() throws SchedulerException {
// 准备参数 - 没有定时触发器
IotSceneRuleDO sceneRule = new IotSceneRuleDO();
sceneRule.setStatus(0); // 0 表示启用
// 创建非定时触发器
IotSceneRuleDO.Trigger deviceTrigger = new IotSceneRuleDO.Trigger();
deviceTrigger.setType(IotSceneRuleTriggerTypeEnum.DEVICE_PROPERTY_POST.getType());
sceneRule.setTriggers(ListUtil.toList(deviceTrigger));
// 调用
timerHandler.registerTimerTriggers(sceneRule);
// 验证 - 不应该调用调度器
verify(schedulerManager, never()).addOrUpdateJob(any(), any(), any(), any());
}
@Test
public void testRegisterTimerTriggers_emptyCronExpression() throws SchedulerException {
// 准备参数 - CRON 表达式为空
Long sceneRuleId = 2L;
IotSceneRuleDO sceneRule = new IotSceneRuleDO();
sceneRule.setId(sceneRuleId);
sceneRule.setStatus(0); // 0 表示启用
// 创建定时触发器但没有 CRON 表达式
IotSceneRuleDO.Trigger timerTrigger = new IotSceneRuleDO.Trigger();
timerTrigger.setType(IotSceneRuleTriggerTypeEnum.TIMER.getType());
timerTrigger.setCronExpression(""); // 空的 CRON 表达式
sceneRule.setTriggers(ListUtil.toList(timerTrigger));
// 调用
timerHandler.registerTimerTriggers(sceneRule);
// 验证 - 不应该调用调度器
verify(schedulerManager, never()).addOrUpdateJob(any(), any(), any(), any());
}
@Test
public void testUnregisterTimerTriggers_success() throws SchedulerException {
// 准备参数
Long sceneRuleId = 3L;
// 调用
timerHandler.unregisterTimerTriggers(sceneRuleId);
// 验证
verify(schedulerManager, times(1)).deleteJob("iot_scene_rule_timer_" + sceneRuleId);
}
@Test
public void testPauseTimerTriggers_success() throws SchedulerException {
// 准备参数
Long sceneRuleId = 4L;
// 调用
timerHandler.pauseTimerTriggers(sceneRuleId);
// 验证
verify(schedulerManager, times(1)).pauseJob("iot_scene_rule_timer_" + sceneRuleId);
}
}

View File

@ -1,141 +0,0 @@
package cn.iocoder.yudao.module.iot.core.util;
import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage;
import org.junit.jupiter.api.Test;
import java.util.HashMap;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
/**
* {@link IotDeviceMessageUtils} 的单元测试
*
* @author HUIHUI
*/
public class IotDeviceMessageUtilsTest {
@Test
public void testExtractPropertyValue_directValue() {
// 测试直接值 Map
IotDeviceMessage message = new IotDeviceMessage();
message.setParams(25.5);
Object result = IotDeviceMessageUtils.extractPropertyValue(message, "temperature");
assertEquals(25.5, result);
}
@Test
public void testExtractPropertyValue_directIdentifier() {
// 测试直接通过标识符获取
IotDeviceMessage message = new IotDeviceMessage();
Map<String, Object> params = new HashMap<>();
params.put("temperature", 25.5);
message.setParams(params);
Object result = IotDeviceMessageUtils.extractPropertyValue(message, "temperature");
assertEquals(25.5, result);
}
@Test
public void testExtractPropertyValue_propertiesStructure() {
// 测试 properties 结构
IotDeviceMessage message = new IotDeviceMessage();
Map<String, Object> properties = new HashMap<>();
properties.put("temperature", 25.5);
properties.put("humidity", 60);
Map<String, Object> params = new HashMap<>();
params.put("properties", properties);
message.setParams(params);
Object result = IotDeviceMessageUtils.extractPropertyValue(message, "temperature");
assertEquals(25.5, result);
}
@Test
public void testExtractPropertyValue_dataStructure() {
// 测试 data 结构
IotDeviceMessage message = new IotDeviceMessage();
Map<String, Object> data = new HashMap<>();
data.put("temperature", 25.5);
Map<String, Object> params = new HashMap<>();
params.put("data", data);
message.setParams(params);
Object result = IotDeviceMessageUtils.extractPropertyValue(message, "temperature");
assertEquals(25.5, result);
}
@Test
public void testExtractPropertyValue_valueField() {
// 测试 value 字段
IotDeviceMessage message = new IotDeviceMessage();
Map<String, Object> params = new HashMap<>();
params.put("identifier", "temperature");
params.put("value", 25.5);
message.setParams(params);
Object result = IotDeviceMessageUtils.extractPropertyValue(message, "temperature");
assertEquals(25.5, result);
}
@Test
public void testExtractPropertyValue_singleValueMap() {
// 测试单值 Map包含 identifier 和一个值
IotDeviceMessage message = new IotDeviceMessage();
Map<String, Object> params = new HashMap<>();
params.put("identifier", "temperature");
params.put("actualValue", 25.5);
message.setParams(params);
Object result = IotDeviceMessageUtils.extractPropertyValue(message, "temperature");
assertEquals(25.5, result);
}
@Test
public void testExtractPropertyValue_notFound() {
// 测试未找到属性值
IotDeviceMessage message = new IotDeviceMessage();
Map<String, Object> params = new HashMap<>();
params.put("humidity", 60);
message.setParams(params);
Object result = IotDeviceMessageUtils.extractPropertyValue(message, "temperature");
assertNull(result);
}
@Test
public void testExtractPropertyValue_nullParams() {
// 测试 params null
IotDeviceMessage message = new IotDeviceMessage();
message.setParams(null);
Object result = IotDeviceMessageUtils.extractPropertyValue(message, "temperature");
assertNull(result);
}
@Test
public void testExtractPropertyValue_priorityOrder() {
// 测试优先级顺序直接标识符 > properties > data > value
IotDeviceMessage message = new IotDeviceMessage();
Map<String, Object> properties = new HashMap<>();
properties.put("temperature", 20.0);
Map<String, Object> data = new HashMap<>();
data.put("temperature", 30.0);
Map<String, Object> params = new HashMap<>();
params.put("temperature", 25.5); // 最高优先级
params.put("properties", properties);
params.put("data", data);
params.put("value", 40.0);
message.setParams(params);
Object result = IotDeviceMessageUtils.extractPropertyValue(message, "temperature");
assertEquals(25.5, result); // 应该返回直接标识符的值
}
}