【同步】最新精简版本!
This commit is contained in:
parent
33de41b4da
commit
2bc0df6819
@ -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; // 该字段由后端拼接
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@ -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;
|
||||
|
||||
}
|
||||
@ -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;
|
||||
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -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 @puhui999:default 值,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() +
|
||||
'}';
|
||||
}
|
||||
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@ -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"));
|
||||
}
|
||||
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@ -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); // 应该返回直接标识符的值
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user