【功能优化】IoT: 重构脚本服务实现,启用脚本执行和测试逻辑,更新日志记录方式

This commit is contained in:
安浩浩 2025-03-24 18:11:33 +08:00
parent a9dc654b36
commit 0ae893272b
9 changed files with 132 additions and 137 deletions

View File

@ -70,11 +70,11 @@
</dependency>
<!-- 脚本插件相关 -->
<!--<dependency>
<dependency>
<groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-module-iot-plugin-script</artifactId>
<version>${revision}</version>
</dependency>-->
</dependency>
<!-- 消息队列相关 -->
<dependency>

View File

@ -9,6 +9,8 @@ import cn.iocoder.yudao.module.iot.controller.admin.product.vo.script.IotProduct
import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductDO;
import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductScriptDO;
import cn.iocoder.yudao.module.iot.dal.mysql.product.IotProductScriptMapper;
import cn.iocoder.yudao.module.iot.plugin.script.context.PluginScriptContext;
import cn.iocoder.yudao.module.iot.plugin.script.service.ScriptService;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
@ -40,8 +42,8 @@ public class IotProductScriptServiceImpl implements IotProductScriptService {
@Resource
private IotProductService productService;
// @Resource
// private ScriptService scriptService;
@Resource
private ScriptService scriptService;
@Override
public Long createProductScript(IotProductScriptSaveReqVO createReqVO) {
@ -118,90 +120,89 @@ public class IotProductScriptServiceImpl implements IotProductScriptService {
@Override
public IotProductScriptTestRespVO testProductScript(IotProductScriptTestReqVO testReqVO) {
// long startTime = System.currentTimeMillis();
//
// try {
// // 验证产品是否存在
// validateProductExists(testReqVO.getProductId());
//
// // 根据ID获取已保存的脚本如果有
// IotProductScriptDO existingScript = null;
// if (testReqVO.getId() != null) {
// existingScript = getProductScript(testReqVO.getId());
// }
//
// // 创建测试上下文
// PluginScriptContext context = new PluginScriptContext();
// IotProductDO product = productService.getProduct(testReqVO.getProductId());
//
// // 设置设备上下文使用产品信息没有具体设备
// context.withDeviceContext(product.getProductKey(), null);
//
// // 设置输入参数
// Map<String, Object> params = new HashMap<>();
// params.put("input", testReqVO.getTestInput());
// params.put("productKey", product.getProductKey());
// params.put("scriptType", testReqVO.getScriptType());
//
// // 根据脚本类型设置特定参数
// switch (testReqVO.getScriptType()) {
// case 1: // PROPERTY_PARSER
// params.put("method", "property");
// break;
// case 2: // EVENT_PARSER
// params.put("method", "event");
// params.put("identifier", "default");
// break;
// case 3: // COMMAND_ENCODER
// params.put("method", "command");
// break;
// default:
// // 默认不添加额外参数
// }
//
// // 添加所有参数到上下文
// for (Map.Entry<String, Object> entry : params.entrySet()) {
// context.setParameter(entry.getKey(), entry.getValue());
// }
//
// // 执行脚本
// Object result = scriptService.executeScript(
// testReqVO.getScriptLanguage(),
// testReqVO.getScriptContent(),
// context);
//
// // 更新测试结果如果是已保存的脚本
// if (existingScript != null) {
// IotProductScriptDO updateObj = new IotProductScriptDO();
// updateObj.setId(existingScript.getId());
// updateObj.setLastTestTime(LocalDateTime.now());
// updateObj.setLastTestResult(1); // 1表示成功
// productScriptMapper.updateById(updateObj);
// }
//
// long executionTime = System.currentTimeMillis() - startTime;
// return IotProductScriptTestRespVO.success(result, executionTime);
//
// } catch (Exception e) {
// log.error("[testProductScript][测试脚本异常]", e);
//
// // 如果是已保存的脚本更新测试失败状态
// if (testReqVO.getId() != null) {
// try {
// IotProductScriptDO updateObj = new IotProductScriptDO();
// updateObj.setId(testReqVO.getId());
// updateObj.setLastTestTime(LocalDateTime.now());
// updateObj.setLastTestResult(0); // 0表示失败
// productScriptMapper.updateById(updateObj);
// } catch (Exception ex) {
// log.error("[testProductScript][更新脚本测试结果异常]", ex);
// }
// }
//
// long executionTime = System.currentTimeMillis() - startTime;
// return IotProductScriptTestRespVO.error(e.getMessage(), executionTime);
// }
return null;
long startTime = System.currentTimeMillis();
try {
// 验证产品是否存在
validateProductExists(testReqVO.getProductId());
// 根据ID获取已保存的脚本如果有
IotProductScriptDO existingScript = null;
if (testReqVO.getId() != null) {
existingScript = getProductScript(testReqVO.getId());
}
// 创建测试上下文
PluginScriptContext context = new PluginScriptContext();
IotProductDO product = productService.getProduct(testReqVO.getProductId());
// 设置设备上下文使用产品信息没有具体设备
context.withDeviceContext(product.getProductKey(), null);
// 设置输入参数
Map<String, Object> params = new HashMap<>();
params.put("input", testReqVO.getTestInput());
params.put("productKey", product.getProductKey());
params.put("scriptType", testReqVO.getScriptType());
// 根据脚本类型设置特定参数
switch (testReqVO.getScriptType()) {
case 1: // PROPERTY_PARSER
params.put("method", "property");
break;
case 2: // EVENT_PARSER
params.put("method", "event");
params.put("identifier", "default");
break;
case 3: // COMMAND_ENCODER
params.put("method", "command");
break;
default:
// 默认不添加额外参数
}
// 添加所有参数到上下文
for (Map.Entry<String, Object> entry : params.entrySet()) {
context.setParameter(entry.getKey(), entry.getValue());
}
// 执行脚本
Object result = scriptService.executeScript(
testReqVO.getScriptLanguage(),
testReqVO.getScriptContent(),
context);
// 更新测试结果如果是已保存的脚本
if (existingScript != null) {
IotProductScriptDO updateObj = new IotProductScriptDO();
updateObj.setId(existingScript.getId());
updateObj.setLastTestTime(LocalDateTime.now());
updateObj.setLastTestResult(1); // 1表示成功
productScriptMapper.updateById(updateObj);
}
long executionTime = System.currentTimeMillis() - startTime;
return IotProductScriptTestRespVO.success(result, executionTime);
} catch (Exception e) {
log.error("[testProductScript][测试脚本异常]", e);
// 如果是已保存的脚本更新测试失败状态
if (testReqVO.getId() != null) {
try {
IotProductScriptDO updateObj = new IotProductScriptDO();
updateObj.setId(testReqVO.getId());
updateObj.setLastTestTime(LocalDateTime.now());
updateObj.setLastTestResult(0); // 0表示失败
productScriptMapper.updateById(updateObj);
} catch (Exception ex) {
log.error("[testProductScript][更新脚本测试结果异常]", ex);
}
}
long executionTime = System.currentTimeMillis() - startTime;
return IotProductScriptTestRespVO.error(e.getMessage(), executionTime);
}
}
@Override

View File

@ -19,7 +19,7 @@
<!-- 引入公共模块 -->
<dependency>
<groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-module-iot-plugin-common</artifactId>
<artifactId>yudao-module-iot-api</artifactId>
<version>${revision}</version>
</dependency>

View File

@ -2,8 +2,7 @@ package cn.iocoder.yudao.module.iot.plugin.script;
import cn.iocoder.yudao.module.iot.plugin.script.context.PluginScriptContext;
import cn.iocoder.yudao.module.iot.plugin.script.service.ScriptService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@ -14,8 +13,8 @@ import java.util.Map;
* 脚本使用示例类
*/
@Component
@Slf4j
public class ScriptExample {
private static final Logger logger = LoggerFactory.getLogger(ScriptExample.class);
@Autowired
private ScriptService scriptService;
@ -31,7 +30,7 @@ public class ScriptExample {
params.put("b", 20);
Object result = scriptService.executeJavaScript(script, params);
logger.info("脚本执行结果: {}", result);
log.info("脚本执行结果: {}", result);
}
/**
@ -73,17 +72,17 @@ public class ScriptExample {
Object result = scriptService.executeJavaScript(script, context);
if (result != null) {
// 处理结果
logger.info("设备数据处理结果: {}", result);
log.info("设备数据处理结果: {}", result);
// 安全地将结果转换为Map
if (result instanceof Map) {
return (Map<String, Object>) result;
} else {
logger.warn("脚本返回结果类型不是Map: {}", result.getClass().getName());
log.warn("脚本返回结果类型不是Map: {}", result.getClass().getName());
}
}
} catch (Exception e) {
logger.error("处理设备数据失败: {}", e.getMessage());
log.error("处理设备数据失败: {}", e.getMessage());
}
return new HashMap<>();
@ -121,10 +120,10 @@ public class ScriptExample {
if (result instanceof String) {
return (String) result;
} else if (result != null) {
logger.warn("脚本返回结果类型不是String: {}", result.getClass().getName());
log.warn("脚本返回结果类型不是String: {}", result.getClass().getName());
}
} catch (Exception e) {
logger.error("生成设备命令失败: {}", e.getMessage());
log.error("生成设备命令失败: {}", e.getMessage());
}
return null;

View File

@ -4,8 +4,7 @@ import cn.hutool.core.map.MapUtil;
import cn.iocoder.yudao.module.iot.plugin.script.context.ScriptContext;
import cn.iocoder.yudao.module.iot.plugin.script.sandbox.JsSandbox;
import cn.iocoder.yudao.module.iot.plugin.script.util.ScriptUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import lombok.extern.slf4j.Slf4j;
import javax.script.*;
import java.util.Map;
@ -16,8 +15,8 @@ import java.util.concurrent.ConcurrentHashMap;
* JavaScript脚本引擎实现
* 使用JSR-223 Nashorn脚本引擎
*/
@Slf4j
public class JsScriptEngine extends AbstractScriptEngine {
private static final Logger logger = LoggerFactory.getLogger(JsScriptEngine.class);
/**
* 默认脚本执行超时时间毫秒
@ -46,7 +45,7 @@ public class JsScriptEngine extends AbstractScriptEngine {
@Override
public void init() {
logger.info("初始化JavaScript脚本引擎");
log.info("初始化JavaScript脚本引擎");
// 创建脚本引擎管理器
engineManager = new ScriptEngineManager();
@ -54,7 +53,7 @@ public class JsScriptEngine extends AbstractScriptEngine {
// 获取JavaScript引擎
engine = engineManager.getEngineByName(JS_ENGINE_NAME);
if (engine == null) {
logger.error("无法创建JavaScript引擎尝试使用JavaScript名称获取");
log.error("无法创建JavaScript引擎尝试使用JavaScript名称获取");
engine = engineManager.getEngineByName("JavaScript");
}
@ -62,7 +61,7 @@ public class JsScriptEngine extends AbstractScriptEngine {
throw new IllegalStateException("无法创建JavaScript引擎请检查环境配置");
}
logger.info("成功创建JavaScript引擎: {}", engine.getClass().getName());
log.info("成功创建JavaScript引擎: {}", engine.getClass().getName());
// 默认使用JS沙箱
if (sandbox == null) {
@ -100,7 +99,7 @@ public class JsScriptEngine extends AbstractScriptEngine {
// 执行脚本
return engine.eval(script, bindings);
} catch (ScriptException e) {
logger.error("执行JavaScript脚本异常: {}", e.getMessage());
log.error("执行JavaScript脚本异常: {}", e.getMessage());
throw new RuntimeException("脚本执行异常: " + e.getMessage(), e);
}
};
@ -109,7 +108,7 @@ public class JsScriptEngine extends AbstractScriptEngine {
// 使用超时执行器执行脚本
return ScriptUtils.executeWithTimeout(task, DEFAULT_TIMEOUT_MS);
} catch (Exception e) {
logger.error("执行JavaScript脚本错误: {}", e.getMessage());
log.error("执行JavaScript脚本错误: {}", e.getMessage());
throw new RuntimeException("脚本执行失败: " + e.getMessage(), e);
}
}
@ -137,7 +136,7 @@ public class JsScriptEngine extends AbstractScriptEngine {
// 执行脚本
return engine.eval(script, bindings);
} catch (ScriptException e) {
logger.error("执行JavaScript脚本异常: {}", e.getMessage());
log.error("执行JavaScript脚本异常: {}", e.getMessage());
throw new RuntimeException("脚本执行异常: " + e.getMessage(), e);
}
};
@ -146,14 +145,14 @@ public class JsScriptEngine extends AbstractScriptEngine {
// 使用超时执行器执行脚本
return ScriptUtils.executeWithTimeout(task, DEFAULT_TIMEOUT_MS);
} catch (Exception e) {
logger.error("执行JavaScript脚本错误: {}", e.getMessage());
log.error("执行JavaScript脚本错误: {}", e.getMessage());
throw new RuntimeException("脚本执行失败: " + e.getMessage(), e);
}
}
@Override
public void destroy() {
logger.info("销毁JavaScript脚本引擎");
log.info("销毁JavaScript脚本引擎");
cachedScripts.clear();
engine = null;
engineManager = null;

View File

@ -1,15 +1,14 @@
package cn.iocoder.yudao.module.iot.plugin.script.engine;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
/**
* 脚本引擎工厂用于创建不同类型的脚本引擎
*/
@Component
@Slf4j
public class ScriptEngineFactory {
private static final Logger logger = LoggerFactory.getLogger(ScriptEngineFactory.class);
/**
* 创建JavaScript脚本引擎
@ -17,7 +16,7 @@ public class ScriptEngineFactory {
* @return JavaScript脚本引擎
*/
public JsScriptEngine createJsEngine() {
logger.debug("创建JavaScript脚本引擎");
log.debug("创建JavaScript脚本引擎");
return new JsScriptEngine();
}

View File

@ -1,7 +1,6 @@
package cn.iocoder.yudao.module.iot.plugin.script.sandbox;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import lombok.extern.slf4j.Slf4j;
import javax.script.ScriptEngine;
import java.util.Arrays;
@ -12,8 +11,8 @@ import java.util.regex.Pattern;
/**
* JavaScript脚本沙箱限制脚本的执行权限
*/
@Slf4j
public class JsSandbox implements ScriptSandbox {
private static final Logger logger = LoggerFactory.getLogger(JsSandbox.class);
/**
* 禁止使用的关键字
@ -59,10 +58,10 @@ public class JsSandbox implements ScriptSandbox {
engine.eval("var Packages = undefined;");
// 增强安全控制可以在这里添加
logger.debug("已应用JavaScript安全沙箱限制");
log.debug("已应用JavaScript安全沙箱限制");
} catch (Exception e) {
logger.warn("应用JavaScript沙箱限制失败: {}", e.getMessage());
log.warn("应用JavaScript沙箱限制失败: {}", e.getMessage());
}
}
@ -75,20 +74,20 @@ public class JsSandbox implements ScriptSandbox {
// 检查禁止的关键字
for (String keyword : FORBIDDEN_KEYWORDS) {
if (script.contains(keyword)) {
logger.warn("脚本包含禁止使用的关键字: {}", keyword);
log.warn("脚本包含禁止使用的关键字: {}", keyword);
return false;
}
}
// 使用正则表达式检查更复杂的模式
if (FORBIDDEN_PATTERN.matcher(script).find()) {
logger.warn("脚本包含禁止使用的模式");
log.warn("脚本包含禁止使用的模式");
return false;
}
// 脚本长度限制
if (script.length() > 1024 * 100) { // 限制100KB
logger.warn("脚本太大,超过了限制");
log.warn("脚本太大,超过了限制");
return false;
}

View File

@ -6,13 +6,12 @@ import cn.iocoder.yudao.module.iot.plugin.script.engine.AbstractScriptEngine;
import cn.iocoder.yudao.module.iot.plugin.script.engine.ScriptEngineFactory;
import cn.iocoder.yudao.module.iot.plugin.script.sandbox.JsSandbox;
import cn.iocoder.yudao.module.iot.plugin.script.sandbox.ScriptSandbox;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import javax.annotation.Resource;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@ -20,10 +19,10 @@ import java.util.concurrent.ConcurrentHashMap;
* 脚本服务实现类
*/
@Service
@Slf4j
public class ScriptServiceImpl implements ScriptService {
private static final Logger logger = LoggerFactory.getLogger(ScriptServiceImpl.class);
@Autowired
@Resource
private ScriptEngineFactory engineFactory;
/**
@ -50,7 +49,7 @@ public class ScriptServiceImpl implements ScriptService {
try {
engine.destroy();
} catch (Exception e) {
logger.error("销毁脚本引擎失败", e);
log.error("销毁脚本引擎失败", e);
}
}
engineCache.clear();
@ -75,7 +74,7 @@ public class ScriptServiceImpl implements ScriptService {
// 执行脚本
return engine.execute(script, context);
} catch (Exception e) {
logger.error("执行脚本失败: {}", e.getMessage());
log.error("执行脚本失败: {}", e.getMessage());
throw new RuntimeException("执行脚本失败: " + e.getMessage(), e);
}
}
@ -101,7 +100,7 @@ public class ScriptServiceImpl implements ScriptService {
public boolean validateScript(String scriptType, String script) {
ScriptSandbox sandbox = sandboxCache.get(scriptType.toLowerCase());
if (sandbox == null) {
logger.warn("找不到脚本类型[{}]对应的沙箱使用默认JS沙箱", scriptType);
log.warn("找不到脚本类型[{}]对应的沙箱使用默认JS沙箱", scriptType);
sandbox = new JsSandbox();
sandboxCache.put(scriptType.toLowerCase(), sandbox);
}

View File

@ -1,8 +1,7 @@
package cn.iocoder.yudao.module.iot.plugin.script.util;
import cn.hutool.json.JSONUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import lombok.extern.slf4j.Slf4j;
import java.util.Map;
import java.util.concurrent.*;
@ -10,8 +9,8 @@ import java.util.concurrent.*;
/**
* 脚本工具类提供执行脚本的辅助方法
*/
@Slf4j
public class ScriptUtils {
private static final Logger logger = LoggerFactory.getLogger(ScriptUtils.class);
/**
* 默认脚本执行超时时间毫秒
@ -90,7 +89,7 @@ public class ScriptUtils {
// 使用hutool的JSONUtil工具类解析JSON
return JSONUtil.toBean(json, Map.class);
} catch (Exception e) {
logger.error("解析JSON失败: {}", e.getMessage());
log.error("解析JSON失败: {}", e.getMessage());
return null;
}
}
@ -114,12 +113,12 @@ public class ScriptUtils {
try {
return Integer.parseInt((String) obj);
} catch (NumberFormatException e) {
logger.debug("无法将字符串转换为整数: {}", obj);
log.debug("无法将字符串转换为整数: {}", obj);
return null;
}
}
logger.debug("无法将对象转换为整数: {}", obj.getClass().getName());
log.debug("无法将对象转换为整数: {}", obj.getClass().getName());
return null;
}
@ -142,12 +141,12 @@ public class ScriptUtils {
try {
return Double.parseDouble((String) obj);
} catch (NumberFormatException e) {
logger.debug("无法将字符串转换为双精度浮点数: {}", obj);
log.debug("无法将字符串转换为双精度浮点数: {}", obj);
return null;
}
}
logger.debug("无法将对象转换为双精度浮点数: {}", obj.getClass().getName());
log.debug("无法将对象转换为双精度浮点数: {}", obj.getClass().getName());
return null;
}