reactor:【IoT 物联网】移除 script 脚本,简化逻辑复杂度

This commit is contained in:
YunaiV 2025-05-31 10:02:01 +08:00
parent 02c3aa748b
commit cf52a16f6c
31 changed files with 7 additions and 2815 deletions

View File

@ -103,39 +103,6 @@
<optional>true</optional>
</dependency>
<!-- JavaScript 引擎 - 使用 GraalJS 替代 Nashorn -->
<!-- TODO @haohao得考虑下jdk8 可能不支持 graalvm后续哈【优先级低】 -->
<dependency>
<groupId>org.graalvm.sdk</groupId>
<artifactId>graal-sdk</artifactId>
<version>22.3.0</version>
</dependency>
<dependency>
<groupId>org.graalvm.js</groupId>
<artifactId>js</artifactId>
<version>22.3.0</version>
</dependency>
<dependency>
<groupId>org.graalvm.js</groupId>
<artifactId>js-scriptengine</artifactId>
<version>22.3.0</version>
</dependency>
<!-- TODO @芋艿:合理注释 -->
<!-- IoT 数据桥梁的执行器所需消息队列。如果您只需要使用 rocketmq 那么则注释掉其它消息队列即可 -->
<!-- <dependency>-->
<!-- <groupId>org.apache.rocketmq</groupId>-->
<!-- <artifactId>rocketmq-spring-boot-starter</artifactId>-->
<!-- </dependency>-->
<!-- <dependency>-->
<!-- <groupId>org.springframework.kafka</groupId>-->
<!-- <artifactId>spring-kafka</artifactId>-->
<!-- </dependency>-->
<!-- <dependency>-->
<!-- <groupId>org.springframework.boot</groupId>-->
<!-- <artifactId>spring-boot-starter-amqp</artifactId>-->
<!-- </dependency>-->
<!-- IoT 网络组件:接收来自设备的上行数据 -->
<!-- <dependency>-->
<!-- <groupId>cn.iocoder.boot</groupId>-->
@ -147,13 +114,6 @@
<!-- <artifactId>yudao-module-iot-net-component-emqx</artifactId>-->
<!-- <version>${revision}</version>-->
<!-- </dependency>-->
<!-- 脚本相关 -->
<!-- <dependency>-->
<!-- <groupId>cn.iocoder.boot</groupId>-->
<!-- <artifactId>yudao-module-iot-script</artifactId>-->
<!-- <version>${revision}</version>-->
<!-- </dependency>-->
</dependencies>
</project>

View File

@ -1,61 +0,0 @@
package cn.iocoder.yudao.module.iot;
import cn.hutool.script.ScriptUtil;
import javax.script.Bindings;
import javax.script.ScriptEngine;
import javax.script.ScriptException;
/**
* TODO 芋艿测试脚本的接入
*/
public class ScriptTest {
public static void main2(String[] args) {
// 创建一个 Groovy 脚本引擎
ScriptEngine engine = ScriptUtil.createGroovyEngine();
// 创建绑定参数
Bindings bindings = engine.createBindings();
bindings.put("name", "Alice");
bindings.put("age", 30);
// 定义一个稍微复杂的 Groovy 脚本
String script = "def greeting = 'Hello, ' + name + '!';\n" +
"def ageInFiveYears = age + 5;\n" +
"def message = greeting + ' In five years, you will be ' + ageInFiveYears + ' years old.';\n" +
"return message.toUpperCase();\n";
try {
// 执行脚本并获取结果
Object result = engine.eval(script, bindings);
System.out.println(result); // 输出: HELLO, ALICE! IN FIVE YEARS, YOU WILL BE 35 YEARS OLD.
} catch (ScriptException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
// 创建一个 JavaScript 脚本引擎
ScriptEngine jsEngine = ScriptUtil.createJsEngine();
// 创建绑定参数
Bindings jsBindings = jsEngine.createBindings();
jsBindings.put("name", "Bob");
jsBindings.put("age", 25);
// 定义一个简单的 JavaScript 脚本
String jsScript = "var greeting = 'Hello, ' + name + '!';\n" +
"var ageInTenYears = age + 10;\n" +
"var message = greeting + ' In ten years, you will be ' + ageInTenYears + ' years old.';\n" +
"message.toUpperCase();\n";
try {
// 执行脚本并获取结果
Object jsResult = jsEngine.eval(jsScript, jsBindings);
System.out.println(jsResult); // 输出: HELLO, BOB! IN TEN YEARS, YOU WILL BE 35 YEARS OLD.
} catch (ScriptException e) {
e.printStackTrace();
}
}
}

View File

@ -1,127 +0,0 @@
package cn.iocoder.yudao.module.iot.controller.admin.product;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
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.product.vo.script.*;
import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductScriptDO;
import cn.iocoder.yudao.module.iot.script.example.ProductScriptSamples;
import cn.iocoder.yudao.module.iot.service.product.IotProductScriptService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
@Tag(name = "管理后台 - IoT 产品脚本信息")
@RestController
@RequestMapping("/iot/product-script")
@Validated
public class IotProductScriptController {
@Resource
private IotProductScriptService productScriptService;
@Resource
private ProductScriptSamples scriptSamples;
@PostMapping("/create")
@Operation(summary = "创建产品脚本")
@PreAuthorize("@ss.hasPermission('iot:product-script:create')")
public CommonResult<Long> createProductScript(@Valid @RequestBody IotProductScriptSaveReqVO createReqVO) {
return success(productScriptService.createProductScript(createReqVO));
}
@PutMapping("/update")
@Operation(summary = "更新产品脚本")
@PreAuthorize("@ss.hasPermission('iot:product-script:update')")
public CommonResult<Boolean> updateProductScript(@Valid @RequestBody IotProductScriptSaveReqVO updateReqVO) {
productScriptService.updateProductScript(updateReqVO);
return success(true);
}
@DeleteMapping("/delete")
@Operation(summary = "删除产品脚本")
@Parameter(name = "id", description = "编号", required = true)
@PreAuthorize("@ss.hasPermission('iot:product-script:delete')")
public CommonResult<Boolean> deleteProductScript(@RequestParam("id") Long id) {
productScriptService.deleteProductScript(id);
return success(true);
}
@GetMapping("/get")
@Operation(summary = "获得产品脚本详情")
@Parameter(name = "id", description = "编号", required = true, example = "1024")
@PreAuthorize("@ss.hasPermission('iot:product-script:query')")
public CommonResult<IotProductScriptRespVO> getProductScript(@RequestParam("id") Long id) {
IotProductScriptDO productScript = productScriptService.getProductScript(id);
return success(BeanUtils.toBean(productScript, IotProductScriptRespVO.class));
}
@GetMapping("/list-by-product")
@Operation(summary = "获得产品的脚本列表")
@Parameter(name = "productId", description = "产品编号", required = true, example = "1024")
@PreAuthorize("@ss.hasPermission('iot:product-script:query')")
public CommonResult<List<IotProductScriptRespVO>> getProductScriptListByProductId(
@RequestParam("productId") Long productId) {
List<IotProductScriptDO> list = productScriptService.getProductScriptListByProductId(productId);
return success(BeanUtils.toBean(list, IotProductScriptRespVO.class));
}
@GetMapping("/page")
@Operation(summary = "获得产品脚本分页")
@PreAuthorize("@ss.hasPermission('iot:product-script:query')")
public CommonResult<PageResult<IotProductScriptRespVO>> getProductScriptPage(
@Valid IotProductScriptPageReqVO pageReqVO) {
PageResult<IotProductScriptDO> pageResult = productScriptService.getProductScriptPage(pageReqVO);
return success(BeanUtils.toBean(pageResult, IotProductScriptRespVO.class));
}
@PostMapping("/test")
@Operation(summary = "测试产品脚本")
@PreAuthorize("@ss.hasPermission('iot:product-script:test')")
public CommonResult<IotProductScriptTestRespVO> testProductScript(
@Valid @RequestBody IotProductScriptTestReqVO testReqVO) {
return success(productScriptService.testProductScript(testReqVO));
}
@PutMapping("/update-status")
@Operation(summary = "更新产品脚本状态")
@PreAuthorize("@ss.hasPermission('iot:product-script:update')")
public CommonResult<Boolean> updateProductScriptStatus(
@Valid @RequestBody IotProductScriptUpdateStatusReqVO updateStatusReqVO) {
productScriptService.updateProductScriptStatus(updateStatusReqVO.getId(), updateStatusReqVO.getStatus());
return success(true);
}
@GetMapping("/sample")
@Operation(summary = "获取示例脚本")
@Parameter(name = "type", description = "脚本类型(1=属性解析, 2=事件解析, 3=命令编码)", required = true, example = "1")
@PreAuthorize("@ss.hasPermission('iot:product-script:query')")
public CommonResult<String> getSampleScript(@RequestParam("type") Integer type) {
String sample;
// TODO @haohao要不枚举下
switch (type) {
case 1:
sample = scriptSamples.getPropertyParserSample();
break;
case 2:
sample = scriptSamples.getEventParserSample();
break;
case 3:
sample = scriptSamples.getCommandEncoderSample();
break;
default:
// TODO @haohao不支持返回 error 会不会好点哈例如说参数不正确
sample = "// 不支持的脚本类型";
}
return success(sample);
}
}

View File

@ -1,53 +0,0 @@
package cn.iocoder.yudao.module.iot.controller.admin.product.vo.script;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import cn.iocoder.yudao.framework.common.validation.InEnum;
import cn.iocoder.yudao.module.iot.enums.product.IotProductScriptLanguageEnum;
import cn.iocoder.yudao.module.iot.enums.product.IotProductScriptStatusEnum;
import cn.iocoder.yudao.module.iot.enums.product.IotProductScriptTypeEnum;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDateTime;
import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
@Schema(description = "管理后台 - IoT 产品脚本信息分页 Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class IotProductScriptPageReqVO extends PageParam {
@Schema(description = "产品ID", example = "28277")
private Long productId;
@Schema(description = "产品唯一标识符")
private String productKey;
@Schema(description = "脚本类型", example = "1")
@InEnum(IotProductScriptTypeEnum.class)
private Integer scriptType;
@Schema(description = "脚本语言")
@InEnum(IotProductScriptLanguageEnum.class)
private String scriptLanguage;
@Schema(description = "状态", example = "0")
@InEnum(IotProductScriptStatusEnum.class)
private Integer status;
@Schema(description = "备注说明", example = "你说的对")
private String remark;
@Schema(description = "最后测试时间")
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
private LocalDateTime[] lastTestTime;
@Schema(description = "创建时间")
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
private LocalDateTime[] createTime;
}

View File

@ -1,63 +0,0 @@
package cn.iocoder.yudao.module.iot.controller.admin.product.vo.script;
import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
import com.alibaba.excel.annotation.ExcelProperty;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
@Schema(description = "管理后台 - IoT 产品脚本信息 Response VO")
@Data
@ExcelIgnoreUnannotated
public class IotProductScriptRespVO {
@Schema(description = "主键", requiredMode = Schema.RequiredMode.REQUIRED, example = "26565")
@ExcelProperty("主键")
private Long id;
@Schema(description = "产品ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "28277")
@ExcelProperty("产品ID")
private Long productId;
@Schema(description = "产品唯一标识符", requiredMode = Schema.RequiredMode.REQUIRED)
@ExcelProperty("产品唯一标识符")
private String productKey;
@Schema(description = "脚本类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
@ExcelProperty("脚本类型")
private Integer scriptType;
@Schema(description = "脚本内容", requiredMode = Schema.RequiredMode.REQUIRED)
@ExcelProperty("脚本内容")
private String scriptContent;
@Schema(description = "脚本语言", requiredMode = Schema.RequiredMode.REQUIRED)
@ExcelProperty("脚本语言")
private String scriptLanguage;
@Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
@ExcelProperty("状态")
private Integer status;
@Schema(description = "备注说明", example = "你说的对")
@ExcelProperty("备注说明")
private String remark;
@Schema(description = "最后测试时间")
@ExcelProperty("最后测试时间")
private LocalDateTime lastTestTime;
@Schema(description = "最后测试结果(0=失败 1=成功)")
@ExcelProperty("最后测试结果(0=失败 1=成功)")
private Integer lastTestResult;
@Schema(description = "脚本版本号", requiredMode = Schema.RequiredMode.REQUIRED)
@ExcelProperty("脚本版本号")
private Integer version;
@Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
@ExcelProperty("创建时间")
private LocalDateTime createTime;
}

View File

@ -1,49 +0,0 @@
package cn.iocoder.yudao.module.iot.controller.admin.product.vo.script;
import cn.iocoder.yudao.framework.common.validation.InEnum;
import cn.iocoder.yudao.module.iot.enums.product.IotProductScriptLanguageEnum;
import cn.iocoder.yudao.module.iot.enums.product.IotProductScriptStatusEnum;
import cn.iocoder.yudao.module.iot.enums.product.IotProductScriptTypeEnum;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
@Schema(description = "管理后台 - IoT 产品脚本信息新增/修改 Request VO")
@Data
public class IotProductScriptSaveReqVO {
@Schema(description = "主键", requiredMode = Schema.RequiredMode.REQUIRED, example = "26565")
private Long id;
@Schema(description = "产品ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "28277")
@NotNull(message = "产品ID不能为空")
private Long productId;
@Schema(description = "产品唯一标识符", requiredMode = Schema.RequiredMode.REQUIRED)
@NotEmpty(message = "产品唯一标识符不能为空")
private String productKey;
@Schema(description = "脚本类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
@NotNull(message = "脚本类型不能为空")
@InEnum(IotProductScriptTypeEnum.class)
private Integer scriptType;
@Schema(description = "脚本内容", requiredMode = Schema.RequiredMode.REQUIRED)
@NotEmpty(message = "脚本内容不能为空")
private String scriptContent;
@Schema(description = "脚本语言", requiredMode = Schema.RequiredMode.REQUIRED)
@NotEmpty(message = "脚本语言不能为空")
@InEnum(IotProductScriptLanguageEnum.class)
private String scriptLanguage;
@Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
@NotNull(message = "状态不能为空")
@InEnum(IotProductScriptStatusEnum.class)
private Integer status;
@Schema(description = "备注说明", example = "你说的对")
private String remark;
}

View File

@ -1,38 +0,0 @@
package cn.iocoder.yudao.module.iot.controller.admin.product.vo.script;
import cn.iocoder.yudao.framework.common.validation.InEnum;
import cn.iocoder.yudao.module.iot.enums.product.IotProductScriptTypeEnum;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
@Schema(description = "管理后台 - IoT 产品脚本测试 Request VO")
@Data
public class IotProductScriptTestReqVO {
@Schema(description = "脚本ID如果已保存脚本则传入", example = "1024")
private Long id;
@Schema(description = "产品ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048")
@NotNull(message = "产品ID不能为空")
private Long productId;
@Schema(description = "脚本类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
@NotNull(message = "脚本类型不能为空")
@InEnum(value = IotProductScriptTypeEnum.class)
private Integer scriptType;
@Schema(description = "脚本内容", requiredMode = Schema.RequiredMode.REQUIRED)
@NotEmpty(message = "脚本内容不能为空")
private String scriptContent;
@Schema(description = "脚本语言", requiredMode = Schema.RequiredMode.REQUIRED, example = "javascript")
@NotEmpty(message = "脚本语言不能为空")
private String scriptLanguage;
@Schema(description = "测试输入数据", requiredMode = Schema.RequiredMode.REQUIRED)
@NotEmpty(message = "测试输入数据不能为空")
private String testInput;
}

View File

@ -1,39 +0,0 @@
package cn.iocoder.yudao.module.iot.controller.admin.product.vo.script;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Schema(description = "管理后台 - IoT 产品脚本测试 Response VO")
@Data
public class IotProductScriptTestRespVO {
@Schema(description = "测试是否成功", requiredMode = Schema.RequiredMode.REQUIRED)
private Boolean success;
@Schema(description = "测试结果输出")
private Object output;
@Schema(description = "错误消息,失败时返回")
private String errorMessage;
@Schema(description = "执行耗时(毫秒)")
private Long executionTimeMs;
// 静态工厂方法 - 成功
public static IotProductScriptTestRespVO success(Object output, Long executionTimeMs) {
IotProductScriptTestRespVO respVO = new IotProductScriptTestRespVO();
respVO.setSuccess(true);
respVO.setOutput(output);
respVO.setExecutionTimeMs(executionTimeMs);
return respVO;
}
// 静态工厂方法 - 失败
public static IotProductScriptTestRespVO error(String errorMessage, Long executionTimeMs) {
IotProductScriptTestRespVO respVO = new IotProductScriptTestRespVO();
respVO.setSuccess(false);
respVO.setErrorMessage(errorMessage);
respVO.setExecutionTimeMs(executionTimeMs);
return respVO;
}
}

View File

@ -1,22 +0,0 @@
package cn.iocoder.yudao.module.iot.controller.admin.product.vo.script;
import cn.iocoder.yudao.framework.common.validation.InEnum;
import cn.iocoder.yudao.module.iot.enums.product.IotProductScriptStatusEnum;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
@Schema(description = "管理后台 - IoT 产品脚本状态更新 Request VO")
@Data
public class IotProductScriptUpdateStatusReqVO {
@Schema(description = "脚本ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
@NotNull(message = "脚本ID不能为空")
private Long id;
@Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
@NotNull(message = "状态不能为空")
@InEnum(IotProductScriptStatusEnum.class)
private Integer status;
}

View File

@ -1,73 +0,0 @@
package cn.iocoder.yudao.module.iot.dal.dataobject.product;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.*;
import java.time.LocalDateTime;
// TODO @haohao类似阿里云的脚本貌似是一个这个可以简化么微信讨论哈类似阿里云貌似是加了个 topic
/**
* IoT 产品脚本信息 DO
*
* @author 芋道源码
*/
@TableName("iot_product_script")
@KeySequence("iot_product_script_seq") // 用于 OraclePostgreSQLKingbaseDB2H2 数据库的主键自增如果是 MySQL 等数据库可不写
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class IotProductScriptDO extends BaseDO {
/**
* 主键
*/
@TableId
private Long id;
/**
* 产品ID
*/
private Long productId;
/**
* 产品唯一标识符
*/
private String productKey;
/**
* 脚本类型(property_parser=属性解析,event_parser=事件解析,command_encoder=命令编码)
*/
private String scriptType;
/**
* 脚本内容
*/
private String scriptContent;
/**
* 脚本语言
*/
private String scriptLanguage;
/**
* 状态(0=禁用 1=启用)
*/
private Integer status;
/**
* 备注说明
*/
private String remark;
/**
* 最后测试时间
*/
private LocalDateTime lastTestTime;
/**
* 最后测试结果(0=失败 1=成功)
*/
private Integer lastTestResult;
/**
* 脚本版本号
*/
private Integer version;
}

View File

@ -1,31 +0,0 @@
package cn.iocoder.yudao.module.iot.dal.mysql.product;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.iot.controller.admin.product.vo.script.IotProductScriptPageReqVO;
import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductScriptDO;
import org.apache.ibatis.annotations.Mapper;
/**
* IoT 产品脚本信息 Mapper
*
* @author 芋道源码
*/
@Mapper
public interface IotProductScriptMapper extends BaseMapperX<IotProductScriptDO> {
default PageResult<IotProductScriptDO> selectPage(IotProductScriptPageReqVO reqVO) {
return selectPage(reqVO, new LambdaQueryWrapperX<IotProductScriptDO>()
.eqIfPresent(IotProductScriptDO::getProductId, reqVO.getProductId())
.eqIfPresent(IotProductScriptDO::getProductKey, reqVO.getProductKey())
.eqIfPresent(IotProductScriptDO::getScriptType, reqVO.getScriptType())
.eqIfPresent(IotProductScriptDO::getScriptLanguage, reqVO.getScriptLanguage())
.eqIfPresent(IotProductScriptDO::getStatus, reqVO.getStatus())
.eqIfPresent(IotProductScriptDO::getRemark, reqVO.getRemark())
.betweenIfPresent(IotProductScriptDO::getLastTestTime, reqVO.getLastTestTime())
.betweenIfPresent(IotProductScriptDO::getCreateTime, reqVO.getCreateTime())
.orderByDesc(IotProductScriptDO::getId));
}
}

View File

@ -1,113 +0,0 @@
package cn.iocoder.yudao.module.iot.script;
import cn.hutool.core.map.MapUtil;
import cn.iocoder.yudao.module.iot.script.context.DefaultScriptContext;
import cn.iocoder.yudao.module.iot.script.context.ScriptContext;
import cn.iocoder.yudao.module.iot.script.service.ScriptService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.Map;
// TODO @haohao挪到 test 目录下
/**
* 脚本使用示例类
*/
@Slf4j
@Component
public class ScriptExample {
@Autowired
private ScriptService scriptService;
/**
* 执行简单的 JavaScript 脚本
*
* @return 执行结果
*/
public Object executeSimpleScript() {
// 简单的脚本内容
String script = "var result = a + b; result;";
// 创建参数
Map<String, Object> params = MapUtil.newHashMap();
params.put("a", 10);
params.put("b", 20);
// 执行脚本
return scriptService.executeJavaScript(script, params);
}
/**
* 执行包含函数的 JavaScript 脚本
*
* @return 执行结果
*/
public Object executeScriptWithFunction() {
// 包含函数的脚本内容
String script = "function calc(x, y) { return x * y; } calc(a, b);";
// 创建上下文
ScriptContext context = new DefaultScriptContext();
context.setParameter("a", 5);
context.setParameter("b", 6);
// 执行脚本
return scriptService.executeJavaScript(script, context);
}
/**
* 执行包含工具类使用的脚本
*
* @return 执行结果
*/
public Object executeScriptWithUtils() {
// 使用工具类的脚本内容
String script = "var data = {name: 'test', value: 123}; utils.toJson(data);";
// 执行脚本
return scriptService.executeJavaScript(script, MapUtil.newHashMap());
}
/**
* 执行包含日志输出的脚本
*
* @return 执行结果
*/
public Object executeScriptWithLogging() {
// 包含日志输出的脚本内容
String script = "log.info('脚本开始执行...'); " +
"var result = a + b; " +
"log.info('计算结果: ' + result); " +
"result;";
// 创建参数
Map<String, Object> params = MapUtil.newHashMap();
params.put("a", 100);
params.put("b", 200);
// 执行脚本
return scriptService.executeJavaScript(script, params);
}
/**
* 演示脚本安全性验证
*
* @return 是否安全
*/
public boolean validateScriptSecurity() {
// 安全的脚本
String safeScript = "var x = 10; var y = 20; x + y;";
boolean safeResult = scriptService.validateScript("js", safeScript);
// 不安全的脚本
String unsafeScript = "java.lang.System.exit(0);";
boolean unsafeResult = scriptService.validateScript("js", unsafeScript);
log.info("安全脚本验证结果: {}", safeResult);
log.info("不安全脚本验证结果: {}", unsafeResult);
return safeResult && !unsafeResult;
}
}

View File

@ -1,24 +0,0 @@
package cn.iocoder.yudao.module.iot.script.config;
import cn.iocoder.yudao.module.iot.script.engine.ScriptEngineFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
/**
* 脚本模块配置类
*/
@Configuration
public class ScriptConfiguration {
/**
* 创建脚本引擎工厂
*
* @return 脚本引擎工厂
*/
@Bean
@Primary
public ScriptEngineFactory scriptEngineFactory() {
return new ScriptEngineFactory();
}
}

View File

@ -1,46 +0,0 @@
package cn.iocoder.yudao.module.iot.script.context;
import cn.hutool.core.map.MapUtil;
import java.util.Map;
/**
* 默认脚本上下文实现
*/
public class DefaultScriptContext implements ScriptContext {
/**
* 上下文参数
*/
private final Map<String, Object> parameters = MapUtil.newHashMap();
/**
* 上下文函数
*/
private final Map<String, Object> functions = MapUtil.newHashMap();
@Override
public Map<String, Object> getParameters() {
return parameters;
}
@Override
public Map<String, Object> getFunctions() {
return functions;
}
@Override
public void setParameter(String key, Object value) {
parameters.put(key, value);
}
@Override
public Object getParameter(String key) {
return parameters.get(key);
}
@Override
public void registerFunction(String name, Object function) {
functions.put(name, function);
}
}

View File

@ -1,92 +0,0 @@
package cn.iocoder.yudao.module.iot.script.context;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.StrUtil;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import java.util.Map;
/**
* 设备脚本上下文提供设备相关的上下文信息
*/
@Slf4j
public class DeviceScriptContext extends DefaultScriptContext {
/**
* 产品 Key
*/
@Getter
private String productKey;
/**
* 设备名称
*/
@Getter
private String deviceName;
/**
* 设备属性数据缓存
*/
private Map<String, Object> properties;
/**
* 使用产品 Key 和设备名称初始化上下文
*
* @param productKey 产品 Key
* @param deviceName 设备名称可以为 null
* @return 当前上下文实例用于链式调用
*/
public DeviceScriptContext withDeviceInfo(String productKey, String deviceName) {
this.productKey = productKey;
this.deviceName = deviceName;
// 添加到参数中便于脚本访问
setParameter("productKey", productKey);
if (StrUtil.isNotEmpty(deviceName)) {
setParameter("deviceName", deviceName);
}
return this;
}
/**
* 设置设备属性数据
*
* @param properties 属性数据
* @return 当前上下文实例用于链式调用
*/
public DeviceScriptContext withProperties(Map<String, Object> properties) {
this.properties = properties;
if (MapUtil.isNotEmpty(properties)) {
setParameter("properties", properties);
}
return this;
}
/**
* 获取设备属性值
*
* @param key 属性标识符
* @return 属性值
*/
public Object getProperty(String key) {
if (MapUtil.isEmpty(properties)) {
return null;
}
return properties.get(key);
}
/**
* 设置设备属性值
*
* @param key 属性标识符
* @param value 属性值
*/
public void setProperty(String key, Object value) {
if (this.properties == null) {
this.properties = MapUtil.newHashMap();
setParameter("properties", this.properties);
}
this.properties.put(key, value);
}
}

View File

@ -1,47 +0,0 @@
package cn.iocoder.yudao.module.iot.script.context;
import java.util.Map;
/**
* 脚本上下文接口定义脚本执行所需的上下文环境
*/
public interface ScriptContext {
/**
* 获取上下文参数
*
* @return 上下文参数
*/
Map<String, Object> getParameters();
/**
* 获取上下文函数
*
* @return 上下文函数
*/
Map<String, Object> getFunctions();
/**
* 设置上下文参数
*
* @param key 参数名
* @param value 参数值
*/
void setParameter(String key, Object value);
/**
* 获取上下文参数
*
* @param key 参数名
* @return 参数值
*/
Object getParameter(String key);
/**
* 注册函数
*
* @param name 函数名称
* @param function 函数对象
*/
void registerFunction(String name, Object function);
}

View File

@ -1,49 +0,0 @@
package cn.iocoder.yudao.module.iot.script.engine;
import cn.iocoder.yudao.module.iot.script.context.ScriptContext;
import cn.iocoder.yudao.module.iot.script.sandbox.ScriptSandbox;
import lombok.extern.slf4j.Slf4j;
/**
* 抽象脚本引擎提供脚本引擎的基本框架
*/
@Slf4j
public abstract class AbstractScriptEngine implements ScriptEngine {
/**
* 脚本沙箱用于提供安全执行环境
*/
protected final ScriptSandbox sandbox;
/**
* 构造函数
*
* @param sandbox 脚本沙箱
*/
protected AbstractScriptEngine(ScriptSandbox sandbox) {
this.sandbox = sandbox;
}
@Override
public Object execute(String script, ScriptContext context) {
try {
// 执行前验证脚本安全性
sandbox.validate(script);
// 执行脚本
return doExecute(script, context);
} catch (Exception e) {
log.error("执行脚本出错:{}", e.getMessage(), e);
throw new RuntimeException("脚本执行失败:" + e.getMessage(), e);
}
}
/**
* 执行脚本的具体实现
*
* @param script 脚本内容
* @param context 脚本上下文
* @return 脚本执行结果
* @throws Exception 执行异常
*/
protected abstract Object doExecute(String script, ScriptContext context) throws Exception;
}

View File

@ -1,348 +0,0 @@
package cn.iocoder.yudao.module.iot.script.engine;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.module.iot.script.context.ScriptContext;
import cn.iocoder.yudao.module.iot.script.sandbox.ScriptSandbox;
import cn.iocoder.yudao.module.iot.script.util.ScriptUtils;
import lombok.extern.slf4j.Slf4j;
import org.graalvm.polyglot.*;
import java.nio.file.Path;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
/**
* JavaScript 脚本引擎实现基于 GraalJS Context API
*/
@Slf4j
public class JsScriptEngine extends AbstractScriptEngine implements AutoCloseable {
/**
* JavaScript 引擎类型
*/
public static final String TYPE = "js";
/**
* 脚本语言类型
*/
private static final String LANGUAGE_ID = "js";
/**
* GraalJS 上下文
*/
private final Context context;
/**
* 脚本源代码缓存
*/
private final Map<String, Source> sourceCache = new ConcurrentHashMap<>();
/**
* 脚本缓存的最大数量
*/
private static final int MAX_CACHE_SIZE = 1000;
/**
* 构造函数
*
* @param sandbox JavaScript 沙箱
*/
public JsScriptEngine(ScriptSandbox sandbox) {
super(sandbox);
// 创建安全的主机访问配置
HostAccess hostAccess = HostAccess.newBuilder()
.allowPublicAccess(true) // 允许访问公共方法和字段
.allowArrayAccess(true) // 允许数组访问
.allowListAccess(true) // 允许 List 访问
.allowMapAccess(true) // 允许 Map 访问
.build();
// 创建隔离的临时目录路径
// TODO @haohao貌似没用到
Path tempDirectory = Path.of(System.getProperty("java.io.tmpdir"), "graaljs-" + IdUtil.fastSimpleUUID());
// 初始化 GraalJS 上下文
this.context = Context.newBuilder(LANGUAGE_ID)
.allowHostAccess(hostAccess) // 使用安全的主机访问配置
.allowHostClassLookup(className -> false) // 禁止查找 Java
.allowIO(false) // 禁止文件 IO
.allowNativeAccess(false) // 禁止本地访问
.allowCreateThread(false) // 禁止创建线程
.allowEnvironmentAccess(EnvironmentAccess.NONE) // 禁止环境变量访问
.allowExperimentalOptions(false) // 禁止实验性选项
.option("js.ecmascript-version", "2021") // 使用最新的 ECMAScript 标准
.option("js.foreign-object-prototype", "false") // 禁用外部对象原型
.option("js.nashorn-compat", "false") // 关闭 Nashorn 兼容模式以获得更好性能
.build();
}
@Override
protected Object doExecute(String script, ScriptContext context) throws Exception {
if (StrUtil.isBlank(script)) {
return null;
}
try {
// 绑定上下文变量
bindContextVariables(context);
// 从缓存获取或创建脚本源
Source source = getOrCreateSource(script);
// 执行脚本并捕获结果添加超时控制
// TODO @haohao通过线程池 + future 会好点
Value result;
Thread executionThread = Thread.currentThread();
Thread watchdogThread = new Thread(() -> {
try {
// 等待 5
TimeUnit.SECONDS.sleep(5);
// 如果执行线程还在运行中断它
if (executionThread.isAlive()) {
log.warn("脚本执行超时,强制中断");
executionThread.interrupt();
}
} catch (InterruptedException ignored) {
// 忽略中断
}
});
watchdogThread.setDaemon(true);
watchdogThread.start();
try {
result = this.context.eval(source);
} finally {
watchdogThread.interrupt(); // 确保看门狗线程停止
}
// 转换结果为 Java 对象
return convertResultToJava(result);
} catch (PolyglotException e) {
handleScriptException(e, script);
throw e;
}
}
/**
* 绑定上下文变量
*
* @param context 脚本上下文
*/
private void bindContextVariables(ScriptContext context) {
Value bindings = this.context.getBindings(LANGUAGE_ID);
// 添加上下文参数
if (MapUtil.isNotEmpty(context.getParameters())) {
context.getParameters().forEach(bindings::putMember);
}
// 添加上下文函数
if (MapUtil.isNotEmpty(context.getFunctions())) {
context.getFunctions().forEach(bindings::putMember);
}
// 添加工具类
bindings.putMember("utils", ScriptUtils.getInstance());
// 添加日志对象
bindings.putMember("log", log);
// 添加控制台输出限制并重定向到日志
AtomicReference<StringBuilder> consoleBuffer = new AtomicReference<>(new StringBuilder());
Value console = this.context.eval(LANGUAGE_ID, "({\n" +
" log: function(msg) { _consoleLog(msg, 'INFO'); },\n" +
" info: function(msg) { _consoleLog(msg, 'INFO'); },\n" +
" warn: function(msg) { _consoleLog(msg, 'WARN'); },\n" +
" error: function(msg) { _consoleLog(msg, 'ERROR'); }\n" +
"})");
bindings.putMember("console", console);
bindings.putMember("_consoleLog", (java.util.function.BiConsumer<String, String>) (message, level) -> {
String formattedMsg = String.valueOf(message);
switch (level) {
case "INFO":
log.info("Script console: {}", formattedMsg);
break;
case "WARN":
log.warn("Script console: {}", formattedMsg);
break;
case "ERROR":
log.error("Script console: {}", formattedMsg);
break;
default:
log.info("Script console: {}", formattedMsg);
}
// 将输出添加到缓冲区
StringBuilder buffer = consoleBuffer.get();
if (buffer.length() > 10000) {
buffer = new StringBuilder();
consoleBuffer.set(buffer);
}
buffer.append(formattedMsg).append("\n");
});
}
/**
* 从缓存中获取或创建脚本源
*
* @param script 脚本内容
* @return 脚本源
*/
private Source getOrCreateSource(String script) {
// 如果缓存太大清理部分缓存
if (sourceCache.size() > MAX_CACHE_SIZE) {
int itemsToRemove = (int) (MAX_CACHE_SIZE * 0.2); // 清理 20% 的缓存
sourceCache.keySet().stream()
.limit(itemsToRemove)
.toList()
.forEach(sourceCache::remove);
}
// 使用脚本的哈希码作为缓存键
String cacheKey = String.valueOf(script.hashCode());
return sourceCache.computeIfAbsent(cacheKey, key -> {
try {
return Source.newBuilder(LANGUAGE_ID, script, "script-" + key + ".js").cached(true).build();
} catch (Exception e) {
log.error("创建脚本源失败: {}", e.getMessage(), e);
throw new RuntimeException("创建脚本源失败: " + e.getMessage(), e);
}
});
}
/**
* GraalJS 结果转换为 Java 对象
*
* @param result GraalJS 执行结果
* @return Java 对象
*/
private Object convertResultToJava(Value result) {
if (result == null || result.isNull()) {
return null;
}
if (result.isString()) {
return result.asString();
}
if (result.isNumber()) {
if (result.fitsInInt()) {
return result.asInt();
}
if (result.fitsInLong()) {
return result.asLong();
}
if (result.fitsInFloat()) {
return result.asFloat();
}
if (result.fitsInDouble()) {
return result.asDouble();
}
}
if (result.isBoolean()) {
return result.asBoolean();
}
if (result.hasArrayElements()) {
int size = (int) result.getArraySize();
Object[] array = new Object[size];
for (int i = 0; i < size; i++) {
array[i] = convertResultToJava(result.getArrayElement(i));
}
return array;
}
if (result.hasMembers()) {
Map<String, Object> map = MapUtil.newHashMap();
for (String key : result.getMemberKeys()) {
map.put(key, convertResultToJava(result.getMember(key)));
}
return map;
}
if (result.isHostObject()) {
return result.asHostObject();
}
// 默认情况下尝试转换为字符串
return result.toString();
}
/**
* 处理脚本执行异常
*
* @param e 多语言异常
* @param script 原始脚本
*/
private void handleScriptException(PolyglotException e, String script) {
if (e.isCancelled()) {
log.error("脚本执行被取消,可能超出资源限制");
} else if (e.isHostException()) {
Throwable hostException = e.asHostException();
log.error("脚本执行时发生 Java 异常: {}", hostException.getMessage(), hostException);
} else if (e.isGuestException()) {
if (e.getSourceLocation() != null) {
log.error("脚本执行错误: {} 位于行 {},列 {}",
e.getMessage(),
e.getSourceLocation().getStartLine(),
e.getSourceLocation().getStartColumn());
// 尝试显示错误位置上下文
try {
String[] lines = script.split("\n");
int lineNumber = e.getSourceLocation().getStartLine();
if (lineNumber > 0 && lineNumber <= lines.length) {
int contextStart = Math.max(1, lineNumber - 2);
int contextEnd = Math.min(lines.length, lineNumber + 2);
StringBuilder context = new StringBuilder();
for (int i = contextStart; i <= contextEnd; i++) {
if (i == lineNumber) {
context.append("> "); // 标记错误行
} else {
context.append(" ");
}
context.append(i).append(": ").append(lines[i - 1]).append("\n");
}
log.error("脚本上下文:\n{}", context);
}
} catch (Exception ignored) {
// 忽略上下文显示失败
}
} else {
log.error("脚本执行错误: {}", e.getMessage());
}
} else {
log.error("脚本执行时发生未知错误: {}", e.getMessage(), e);
}
}
@Override
public String getType() {
return TYPE;
}
@Override
public void close() {
try {
// 清除脚本缓存
sourceCache.clear();
// 关闭 GraalJS 上下文释放资源
context.close(true);
} catch (Exception e) {
log.warn("关闭 GraalJS 引擎时发生错误: {}", e.getMessage());
}
}
}

View File

@ -1,25 +0,0 @@
package cn.iocoder.yudao.module.iot.script.engine;
import cn.iocoder.yudao.module.iot.script.context.ScriptContext;
/**
* 脚本引擎接口定义脚本执行的核心功能
*/
public interface ScriptEngine {
/**
* 执行脚本
*
* @param script 脚本内容
* @param context 脚本上下文
* @return 脚本执行结果
*/
Object execute(String script, ScriptContext context);
/**
* 获取脚本引擎类型
*
* @return 脚本引擎类型
*/
String getType();
}

View File

@ -1,85 +0,0 @@
package cn.iocoder.yudao.module.iot.script.engine;
import cn.iocoder.yudao.module.iot.script.sandbox.JsSandbox;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.stereotype.Component;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* 脚本引擎工厂用于创建和缓存不同类型的脚本引擎支持资源生命周期管理
*/
@Slf4j
@Component
public class ScriptEngineFactory implements DisposableBean {
/**
* 脚本引擎缓存
*/
private final Map<String, ScriptEngine> engines = new ConcurrentHashMap<>();
/**
* 获取脚本引擎
*
* @param type 脚本类型
* @return 脚本引擎
*/
public ScriptEngine getEngine(String type) {
// 从缓存中获取引擎
return engines.computeIfAbsent(type, this::createEngine);
}
/**
* 创建脚本引擎
*
* @param type 脚本类型
* @return 脚本引擎
*/
private ScriptEngine createEngine(String type) {
try {
if (JsScriptEngine.TYPE.equals(type)) {
log.info("创建 GraalJS 脚本引擎");
return new JsScriptEngine(new JsSandbox());
}
log.warn("不支持的脚本类型: {}", type);
return null;
} catch (Exception e) {
log.error("创建脚本引擎 [{}] 失败: {}", type, e.getMessage(), e);
throw new RuntimeException("创建脚本引擎失败: " + e.getMessage(), e);
}
}
/**
* 释放指定类型的引擎资源
*
* @param type 脚本类型
*/
public void releaseEngine(String type) {
ScriptEngine engine = engines.remove(type);
if (engine instanceof AutoCloseable) {
try {
((AutoCloseable) engine).close();
log.info("已释放脚本引擎资源: {}", type);
} catch (Exception e) {
log.warn("释放脚本引擎 [{}] 资源时发生错误: {}", type, e.getMessage());
}
}
}
/**
* 清理所有引擎资源
*/
public void releaseAllEngines() {
engines.keySet().forEach(this::releaseEngine);
log.info("已清理所有脚本引擎资源");
}
@Override
public void destroy() {
log.info("应用关闭,释放所有脚本引擎资源...");
releaseAllEngines();
}
}

View File

@ -1,209 +0,0 @@
package cn.iocoder.yudao.module.iot.script.example;
import cn.hutool.core.map.MapUtil;
import cn.iocoder.yudao.module.iot.script.context.DefaultScriptContext;
import cn.iocoder.yudao.module.iot.script.context.ScriptContext;
import cn.iocoder.yudao.module.iot.script.service.ScriptService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
// TODO @haohao搞到 test 里面哈
/**
* GraalJS 脚本引擎示例
* <p>
* 展示了如何使用 GraalJS 脚本引擎的各种功能
*/
@Slf4j
@Component
public class GraalJsExample {
@Autowired
private ScriptService scriptService;
/**
* 执行简单的 JavaScript 脚本
*
* @return 执行结果
*/
public Object executeSimpleScript() {
// 简单的脚本内容
String script = "var result = a + b; result;";
// 创建参数
Map<String, Object> params = MapUtil.newHashMap();
params.put("a", 10);
params.put("b", 20);
// 执行脚本
return scriptService.executeJavaScript(script, params);
}
/**
* 执行现代 JavaScript 语法ES6+
*
* @return 执行结果
*/
public Object executeModernJavaScript() {
// 使用现代 JavaScript 语法
String script = "// 使用箭头函数\n" +
"const add = (a, b) => a + b;\n" +
"\n" +
"// 使用解构赋值\n" +
"const {c, d} = params;\n" +
"\n" +
"// 使用模板字符串\n" +
"const result = `计算结果: ${add(c, d)}`;\n" +
"\n" +
"// 使用可选链操作符\n" +
"const value = params?.e?.value ?? 'default';\n" +
"\n" +
"// 返回一个对象\n" +
"({\n" +
" sum: add(c, d),\n" +
" message: result,\n" +
" defaultValue: value\n" +
"})";
// 创建参数
Map<String, Object> params = MapUtil.newHashMap();
params.put("params", MapUtil.builder()
.put("c", 30)
.put("d", 40)
.build());
// 执行脚本
return scriptService.executeJavaScript(script, params);
}
/**
* 执行带错误处理的脚本
*
* @return 执行结果
*/
public Object executeWithErrorHandling() {
// 包含错误处理的脚本
String script = "try {\n" +
" // 故意制造错误\n" +
" if (!nonExistentVar) {\n" +
" throw new Error('手动抛出的错误');\n" +
" }\n" +
"} catch (error) {\n" +
" console.error('捕获到错误: ' + error.message);\n" +
" return { success: false, error: error.message };\n" +
"}\n" +
"\n" +
"return { success: true, data: 'No error' };";
// 执行脚本
return scriptService.executeJavaScript(script, MapUtil.newHashMap());
}
/**
* 演示超时控制
*
* @return 执行结果
*/
public Object executeWithTimeout() {
// 这个脚本会导致无限循环
String script = "// 无限循环\n" +
"var counter = 0;\n" +
"while(true) {\n" +
" counter++;\n" +
" if (counter % 1000000 === 0) {\n" +
" console.log('Still running: ' + counter);\n" +
" }\n" +
"}\n" +
"return counter;";
// 使用 CompletableFuture 和超时控制
CompletableFuture<Object> future = CompletableFuture.supplyAsync(() -> {
try {
return scriptService.executeJavaScript(script, MapUtil.newHashMap());
} catch (Exception e) {
log.error("脚本执行失败: {}", e.getMessage());
return "执行失败: " + e.getMessage();
}
});
try {
// 等待结果最多 10
return future.get(10, TimeUnit.SECONDS);
} catch (InterruptedException | ExecutionException e) {
return "执行异常: " + e.getMessage();
} catch (TimeoutException e) {
future.cancel(true);
return "执行超时,已强制终止";
}
}
/**
* 演示 JSON 处理
*
* @return 执行结果
*/
public Object executeJsonProcessing() {
// JSON 处理示例
String script = "// 解析传入的 JSON\n" +
"var data = JSON.parse(jsonString);\n" +
"\n" +
"// 处理 JSON 数据\n" +
"var result = {\n" +
" name: data.name.toUpperCase(),\n" +
" age: data.age + 1,\n" +
" address: data.address || 'Unknown',\n" +
" tags: [...data.tags, 'processed'],\n" +
" timestamp: Date.now()\n" +
"};\n" +
"\n" +
"// 转换回 JSON 字符串\n" +
"JSON.stringify(result);";
// 创建上下文
ScriptContext context = new DefaultScriptContext();
context.setParameter("jsonString",
"{\"name\":\"test user\",\"age\":25,\"tags\":[\"tag1\",\"tag2\"]}");
// 执行脚本
return scriptService.executeJavaScript(script, context);
}
/**
* 演示数据转换
*
* @return 执行结果
*/
public Object executeDataConversion() {
// 数据转换和处理示例
String script = "// 使用 utils 工具类进行数据转换\n" +
"var stringValue = utils.isEmpty(input) ? \"默认值\" : input;\n" +
"var numberValue = utils.convert(stringValue, \"number\");\n" +
"\n" +
"// 创建一个复杂数据结构\n" +
"var result = {\n" +
" original: input,\n" +
" stringValue: stringValue,\n" +
" numberValue: numberValue,\n" +
" booleanValue: Boolean(numberValue),\n" +
" isValid: utils.isNotEmpty(input)\n" +
"};\n" +
"\n" +
"// 记录处理结果\n" +
"log.info(\"处理结果: \" + utils.toJson(result));\n" +
"\n" +
"return result;";
// 创建参数
Map<String, Object> params = MapUtil.newHashMap();
params.put("input", "42");
// 执行脚本
return scriptService.executeJavaScript(script, params);
}
}

View File

@ -1,174 +0,0 @@
package cn.iocoder.yudao.module.iot.script.example;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
/**
* 产品脚本示例类提供各种类型的产品脚本示例代码
*/
@Slf4j
@Component
public class ProductScriptSamples {
/**
* 获取属性解析脚本示例
*
* @return 属性解析脚本示例代码
*/
public String getPropertyParserSample() {
return "/**\n" +
" * 属性上报数据解析脚本示例\n" +
" * @param input 设备上报的原始数据\n" +
" * @param productKey 产品标识\n" +
" * @param method 方法类型,固定为 property\n" +
" * @param properties 当前设备的属性数据\n" +
" * @return 解析后的属性数据\n" +
" */\n" +
"function parseProperty(input, productKey) {\n" +
" // 记录日志\n" +
" console.log('开始解析属性数据: ' + input);\n" +
" \n" +
" try {\n" +
" // 假设上报的是 JSON 字符串\n" +
" var data = JSON.parse(input);\n" +
" \n" +
" // 构建属性数据结构\n" +
" var result = {\n" +
" // 属性上报的时间戳,毫秒级\n" +
" timestamp: data.timestamp || Date.now(),\n" +
" // 属性数据\n" +
" params: {}\n" +
" };\n" +
" \n" +
" // 处理属性值\n" +
" if (data.temperature) {\n" +
" result.params.temperature = parseFloat(data.temperature);\n" +
" }\n" +
" \n" +
" if (data.humidity) {\n" +
" result.params.humidity = parseFloat(data.humidity);\n" +
" }\n" +
" \n" +
" console.log('属性解析结果: ' + JSON.stringify(result));\n" +
" return result;\n" +
" } catch (error) {\n" +
" console.error('解析属性数据失败: ' + error.message);\n" +
" throw new Error('解析失败: ' + error.message);\n" +
" }\n" +
"};\n" +
"\n" +
"// 执行解析\n" +
"parseProperty(input, productKey);";
}
/**
* 获取事件解析脚本示例
*
* @return 事件解析脚本示例代码
*/
public String getEventParserSample() {
return "/**\n" +
" * 事件数据解析脚本示例\n" +
" * @param input 设备上报的原始数据\n" +
" * @param productKey 产品标识\n" +
" * @param method 方法类型,固定为 event\n" +
" * @param identifier 事件标识符\n" +
" * @return 解析后的事件数据\n" +
" */\n" +
"function parseEvent(input, productKey, identifier) {\n" +
" // 记录日志\n" +
" console.log('开始解析事件数据: ' + input);\n" +
" console.log('事件标识符: ' + identifier);\n" +
" \n" +
" try {\n" +
" // 假设上报的是 JSON 字符串\n" +
" var data = JSON.parse(input);\n" +
" \n" +
" // 构建事件数据结构\n" +
" var result = {\n" +
" // 事件标识符\n" +
" identifier: identifier || 'alert',\n" +
" // 事件上报的时间戳,毫秒级\n" +
" timestamp: data.timestamp || Date.now(),\n" +
" // 事件参数\n" +
" params: {}\n" +
" };\n" +
" \n" +
" // 根据不同事件类型处理参数\n" +
" if (result.identifier === 'alert') {\n" +
" result.params.level = data.level || 'info';\n" +
" result.params.message = data.message || '';\n" +
" } else if (result.identifier === 'error') {\n" +
" result.params.code = data.code || 0;\n" +
" result.params.message = data.message || '';\n" +
" }\n" +
" \n" +
" console.log('事件解析结果: ' + JSON.stringify(result));\n" +
" return result;\n" +
" } catch (error) {\n" +
" console.error('解析事件数据失败: ' + error.message);\n" +
" throw new Error('解析失败: ' + error.message);\n" +
" }\n" +
"};\n" +
"\n" +
"// 执行解析\n" +
"parseEvent(input, productKey, identifier);";
}
/**
* 获取命令编码脚本示例
*
* @return 命令编码脚本示例代码
*/
public String getCommandEncoderSample() {
return "/**\n" +
" * 命令数据编码脚本示例\n" +
" * @param input 平台下发的命令数据\n" +
" * @param productKey 产品标识\n" +
" * @param method 方法类型,固定为 command\n" +
" * @param cmdParams 命令参数\n" +
" * @return 编码后的命令数据\n" +
" */\n" +
"function encodeCommand(input, productKey, cmdParams) {\n" +
" // 记录日志\n" +
" console.log('开始编码命令数据: ' + input);\n" +
" console.log('命令参数: ' + JSON.stringify(cmdParams));\n" +
" \n" +
" try {\n" +
" // 输入可能是 JSON 字符串或对象\n" +
" var data = typeof input === 'string' ? JSON.parse(input) : input;\n" +
" \n" +
" // 获取命令名称和值\n" +
" var cmdName = cmdParams.cmdName || '';\n" +
" var cmdValue = cmdParams.cmdValue;\n" +
" \n" +
" // 构建设备可识别的命令格式\n" +
" var result = {\n" +
" cmd: cmdName,\n" +
" value: cmdValue,\n" +
" timestamp: Date.now()\n" +
" };\n" +
" \n" +
" // 根据不同命令类型构建参数\n" +
" if (cmdName === 'setValue') {\n" +
" // 无需额外处理\n" +
" } else if (cmdName === 'control') {\n" +
" result.mode = data.mode || 'auto';\n" +
" result.action = data.action || 'start';\n" +
" }\n" +
" \n" +
" // 转换为设备能识别的格式(此处以 JSON 字符串为例)\n" +
" var encodedResult = JSON.stringify(result);\n" +
" \n" +
" console.log('命令编码结果: ' + encodedResult);\n" +
" return encodedResult;\n" +
" } catch (error) {\n" +
" console.error('编码命令数据失败: ' + error.message);\n" +
" throw new Error('编码失败: ' + error.message);\n" +
" }\n" +
"};\n" +
"\n" +
"// 执行编码\n" +
"encodeCommand(input, productKey, cmdParams);";
}
}

View File

@ -1,4 +0,0 @@
/**
* IoT 脚本模块提供脚本引擎执行环境和沙箱功能支持 JavaScript 脚本的执行
*/
package cn.iocoder.yudao.module.iot.script;

View File

@ -1,330 +0,0 @@
package cn.iocoder.yudao.module.iot.script.sandbox;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import lombok.extern.slf4j.Slf4j;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* JavaScript 沙箱实现提供脚本安全性验证
*/
@Slf4j
public class JsSandbox implements ScriptSandbox {
/**
* JavaScript 沙箱类型
*/
public static final String TYPE = "js";
/**
* 不安全的关键字列表
*/
private final List<String> unsafeKeywords = new ArrayList<>();
/**
* 可能导致高资源消耗的关键字
*/
private final List<String> highResourceKeywords = new ArrayList<>();
/**
* 不安全的包/类访问模式
*/
private final List<Pattern> unsafePatterns = new ArrayList<>();
/**
* 递归或循环嵌套深度检测模式
*/
private final List<Pattern> recursionPatterns = new ArrayList<>();
/**
* 允许的脚本最大长度字节
*/
private static final int MAX_SCRIPT_LENGTH = 100 * 1024; // 100KB
/**
* 脚本安全验证超时时间毫秒
*/
private static final long VALIDATION_TIMEOUT = 1000; // 1秒
/**
* 构造函数初始化不安全的关键字和模式
*/
public JsSandbox() {
// 初始化 Java 相关的不安全关键字
// TODO @haohao可以使用 addAll
Arrays.asList(
"java.lang.System",
"java.io",
"java.net",
"java.nio",
"java.security",
"java.rmi",
"java.lang.reflect",
"java.sql",
"javax.sql",
"javax.naming",
"javax.script",
"javax.tools",
"org.omg",
"org.graalvm.polyglot",
"sun.",
"javafx.",
"Packages.",
"com.sun.",
"com.oracle.").forEach(unsafeKeywords::add);
// GraalJS 特有的不安全关键字
Arrays.asList(
"Polyglot.import",
"Polyglot.eval",
"Java.type",
"allowHostAccess",
"allowNativeAccess",
"allowIO",
"allowHostClassLoading",
"allowAllAccess",
"allowExperimentalOptions",
"Context.Builder",
"Context.create",
"Context.getCurrent",
"Context.newBuilder",
"__proto__",
"__defineGetter__",
"__defineSetter__",
"__lookupGetter__",
"__lookupSetter__",
"__noSuchMethod__",
"constructor.constructor",
"Object.constructor").forEach(unsafeKeywords::add);
// 可能导致高资源消耗的关键字
Arrays.asList(
"while(true)",
"for(;;)",
"do{",
"BigInt",
"Promise.all",
"setTimeout",
"setInterval",
"new Array(",
"Array(",
"new ArrayBuffer(",
".repeat(",
".forEach(",
".map(",
".reduce(").forEach(highResourceKeywords::add);
// 初始化不安全的模式
// 系统访问和进程执行
unsafePatterns.add(Pattern.compile("java\\.lang\\.Runtime"));
unsafePatterns.add(Pattern.compile("java\\.lang\\.ProcessBuilder"));
unsafePatterns.add(Pattern.compile("java\\.lang\\.reflect"));
// 特殊对象和操作
unsafePatterns.add(Pattern.compile("Packages"));
unsafePatterns.add(Pattern.compile("JavaImporter"));
unsafePatterns.add(Pattern.compile("load\\s*\\("));
unsafePatterns.add(Pattern.compile("loadWithNewGlobal\\s*\\("));
unsafePatterns.add(Pattern.compile("exit\\s*\\("));
unsafePatterns.add(Pattern.compile("quit\\s*\\("));
unsafePatterns.add(Pattern.compile("eval\\s*\\("));
// GraalJS 特有的不安全模式
unsafePatterns.add(Pattern.compile("Polyglot\\."));
unsafePatterns.add(Pattern.compile("Java\\.type\\s*\\("));
unsafePatterns.add(Pattern.compile("Context\\."));
unsafePatterns.add(Pattern.compile("Engine\\."));
// 原型污染检测
unsafePatterns.add(Pattern.compile("(?:Object|Array|String|Number|Boolean|Function|RegExp|Date)\\.prototype"));
unsafePatterns.add(Pattern.compile("\\['constructor'\\]"));
unsafePatterns.add(Pattern.compile("\\[\"constructor\"\\]"));
unsafePatterns.add(Pattern.compile("\\['__proto__'\\]"));
unsafePatterns.add(Pattern.compile("\\[\"__proto__\"\\]"));
// 检测可能导致无限递归或循环的模式
recursionPatterns.add(Pattern.compile("for\\s*\\([^\\)]*\\)\\s*\\{[^\\}]*for\\s*\\(")); // 嵌套循环
recursionPatterns.add(Pattern.compile("while\\s*\\([^\\)]*\\)\\s*\\{[^\\}]*while\\s*\\(")); // 嵌套 while
recursionPatterns.add(Pattern.compile("function\\s+[a-zA-Z0-9_$]+\\s*\\([^\\)]*\\)\\s*\\{[^\\}]*\\1\\s*\\(")); // 递归函数调用
}
@Override
public boolean validate(String script) {
if (StrUtil.isBlank(script)) {
return true;
}
// 检查脚本长度
if (script.length() > MAX_SCRIPT_LENGTH) {
log.warn("脚本长度超过限制: {} > {}", script.length(), MAX_SCRIPT_LENGTH);
return false;
}
// 使用超时机制进行验证
final boolean[] result = {true};
Thread validationThread = new Thread(() -> {
// 检查不安全的关键字
if (containsUnsafeKeywords(script)) {
result[0] = false;
return;
}
// 检查不安全的模式
if (matchesUnsafePatterns(script)) {
result[0] = false;
return;
}
// 检查可能导致高资源消耗的构造
if (containsHighResourcePatterns(script)) {
log.warn("脚本包含可能导致高资源消耗的构造,需要注意");
// 不直接拒绝而是记录警告
}
// 分析脚本复杂度
analyzeScriptComplexity(script);
});
validationThread.start();
try {
validationThread.join(VALIDATION_TIMEOUT);
if (validationThread.isAlive()) {
validationThread.interrupt();
log.warn("脚本安全验证超时");
return false;
}
} catch (InterruptedException e) {
log.warn("脚本安全验证被中断");
return false;
}
return result[0];
}
@Override
public String getType() {
return TYPE;
}
/**
* 检查脚本是否包含不安全的关键字
*
* @param script 脚本内容
* @return 是否包含不安全的关键字
*/
private boolean containsUnsafeKeywords(String script) {
if (CollUtil.isEmpty(unsafeKeywords)) {
return false;
}
for (String keyword : unsafeKeywords) {
if (script.contains(keyword)) {
log.warn("脚本包含不安全的关键字: {}", keyword);
return true;
}
}
return false;
}
/**
* 检查脚本是否匹配不安全的模式
*
* @param script 脚本内容
* @return 是否匹配不安全的模式
*/
private boolean matchesUnsafePatterns(String script) {
if (CollUtil.isEmpty(unsafePatterns)) {
return false;
}
for (Pattern pattern : unsafePatterns) {
Matcher matcher = pattern.matcher(script);
if (matcher.find()) {
log.warn("脚本匹配到不安全的模式: {}", pattern.pattern());
return true;
}
}
return false;
}
/**
* 检查脚本是否包含可能导致高资源消耗的模式
*
* @param script 脚本内容
* @return 是否包含高资源消耗模式
*/
private boolean containsHighResourcePatterns(String script) {
if (CollUtil.isEmpty(highResourceKeywords)) {
return false;
}
boolean result = false;
for (String pattern : highResourceKeywords) {
if (script.contains(pattern)) {
log.warn("脚本包含高资源消耗模式: {}", pattern);
result = true;
}
}
// 还要检查递归或嵌套循环模式
for (Pattern pattern : recursionPatterns) {
Matcher matcher = pattern.matcher(script);
if (matcher.find()) {
log.warn("脚本包含嵌套循环或递归调用: {}", pattern.pattern());
result = true;
}
}
return result;
}
/**
* 分析脚本复杂度
*
* @param script 脚本内容
*/
private void analyzeScriptComplexity(String script) {
// 计算循环和条件语句的数量
int forCount = countOccurrences(script, "for(");
forCount += countOccurrences(script, "for (");
int whileCount = countOccurrences(script, "while(");
whileCount += countOccurrences(script, "while (");
int doWhileCount = countOccurrences(script, "do{");
doWhileCount += countOccurrences(script, "do {");
int funcCount = countOccurrences(script, "function");
// 记录复杂度评估
if (forCount + whileCount + doWhileCount > 10) {
log.warn("脚本循环结构过多: for={}, while={}, do-while={}", forCount, whileCount, doWhileCount);
}
if (funcCount > 20) {
log.warn("脚本函数定义过多: {}", funcCount);
}
}
/**
* 计算字符串出现次数
*
* @param source 源字符串
* @param substring 子字符串
* @return 出现次数
*/
private int countOccurrences(String source, String substring) {
int count = 0;
int index = 0;
while ((index = source.indexOf(substring, index)) != -1) {
count++;
index += substring.length();
}
return count;
}
}

View File

@ -1,22 +0,0 @@
package cn.iocoder.yudao.module.iot.script.sandbox;
/**
* 脚本沙箱接口提供脚本安全性验证
*/
public interface ScriptSandbox {
/**
* 验证脚本内容是否安全
*
* @param script 脚本内容
* @return 脚本是否安全
*/
boolean validate(String script);
/**
* 获取沙箱类型
*
* @return 沙箱类型
*/
String getType();
}

View File

@ -1,58 +0,0 @@
package cn.iocoder.yudao.module.iot.script.service;
import cn.iocoder.yudao.module.iot.script.context.ScriptContext;
import java.util.Map;
/**
* 脚本服务接口定义脚本执行的核心功能
*/
public interface ScriptService {
/**
* 执行脚本
*
* @param scriptType 脚本类型 jsgroovy
* @param script 脚本内容
* @param context 脚本上下文
* @return 脚本执行结果
*/
Object executeScript(String scriptType, String script, ScriptContext context);
/**
* 执行脚本
*
* @param scriptType 脚本类型 jsgroovy
* @param script 脚本内容
* @param params 脚本参数
* @return 脚本执行结果
*/
Object executeScript(String scriptType, String script, Map<String, Object> params);
/**
* 执行 JavaScript 脚本
*
* @param script 脚本内容
* @param context 脚本上下文
* @return 脚本执行结果
*/
Object executeJavaScript(String script, ScriptContext context);
/**
* 执行 JavaScript 脚本
*
* @param script 脚本内容
* @param params 脚本参数
* @return 脚本执行结果
*/
Object executeJavaScript(String script, Map<String, Object> params);
/**
* 验证脚本内容是否安全
*
* @param scriptType 脚本类型
* @param script 脚本内容
* @return 脚本是否安全
*/
boolean validateScript(String scriptType, String script);
}

View File

@ -1,111 +0,0 @@
package cn.iocoder.yudao.module.iot.script.service;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.module.iot.script.context.DefaultScriptContext;
import cn.iocoder.yudao.module.iot.script.context.ScriptContext;
import cn.iocoder.yudao.module.iot.script.engine.JsScriptEngine;
import cn.iocoder.yudao.module.iot.script.engine.ScriptEngine;
import cn.iocoder.yudao.module.iot.script.engine.ScriptEngineFactory;
import cn.iocoder.yudao.module.iot.script.sandbox.JsSandbox;
import cn.iocoder.yudao.module.iot.script.sandbox.ScriptSandbox;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.Map;
/**
* 脚本服务实现类
*/
@Slf4j
@Service
public class ScriptServiceImpl implements ScriptService {
@Autowired
private ScriptEngineFactory engineFactory;
@Override
public Object executeScript(String scriptType, String script, ScriptContext context) {
if (StrUtil.isBlank(scriptType) || StrUtil.isBlank(script)) {
return null;
}
ScriptEngine engine = engineFactory.getEngine(scriptType);
if (engine == null) {
log.error("找不到脚本引擎: {}", scriptType);
throw new RuntimeException("不支持的脚本类型: " + scriptType);
}
try {
return engine.execute(script, context);
} catch (Exception e) {
// TODO @haohao最好打印一些参数下面类似的也是
log.error("执行脚本失败: {}", e.getMessage(), e);
throw new RuntimeException("执行脚本失败: " + e.getMessage(), e);
}
}
@Override
public Object executeScript(String scriptType, String script, Map<String, Object> params) {
ScriptContext context = createContext(params);
return executeScript(scriptType, script, context);
}
@Override
public Object executeJavaScript(String script, ScriptContext context) {
return executeScript(JsScriptEngine.TYPE, script, context);
}
@Override
public Object executeJavaScript(String script, Map<String, Object> params) {
return executeScript(JsScriptEngine.TYPE, script, params);
}
@Override
public boolean validateScript(String scriptType, String script) {
if (StrUtil.isBlank(scriptType) || StrUtil.isBlank(script)) {
return true;
}
ScriptSandbox sandbox = getSandbox(scriptType);
if (sandbox == null) {
log.warn("找不到对应的脚本沙箱: {}", scriptType);
return false;
}
try {
return sandbox.validate(script);
} catch (Exception e) {
log.error("验证脚本安全性失败: {}", e.getMessage(), e);
return false;
}
}
/**
* 根据脚本类型获取对应的沙箱实现
*
* @param scriptType 脚本类型
* @return 沙箱实现
*/
private ScriptSandbox getSandbox(String scriptType) {
if (JsScriptEngine.TYPE.equals(scriptType)) {
return new JsSandbox();
}
return null;
}
/**
* 根据参数创建脚本上下文
*
* @param params 参数
* @return 脚本上下文
*/
private ScriptContext createContext(Map<String, Object> params) {
ScriptContext context = new DefaultScriptContext();
if (MapUtil.isNotEmpty(params)) {
params.forEach(context::setParameter);
}
return context;
}
}

View File

@ -1,159 +0,0 @@
package cn.iocoder.yudao.module.iot.script.util;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import lombok.extern.slf4j.Slf4j;
import java.util.Map;
/**
* 脚本工具类提供给脚本执行环境使用的工具方法
*/
@Slf4j
public class ScriptUtils {
/**
* 单例实例
*/
private static final ScriptUtils INSTANCE = new ScriptUtils();
/**
* 获取单例实例
*
* @return 工具类实例
*/
public static ScriptUtils getInstance() {
return INSTANCE;
}
// TODO @haohao使用 lombok 简化掉
private ScriptUtils() {
// 私有构造函数
}
/**
* 字符串是否为空
*
* @param str 字符串
* @return 是否为空
*/
public boolean isEmpty(String str) {
return StrUtil.isEmpty(str);
}
/**
* 字符串是否不为空
*
* @param str 字符串
* @return 是否不为空
*/
public boolean isNotEmpty(String str) {
return StrUtil.isNotEmpty(str);
}
/**
* 将对象转为 JSON 字符串
*
* @param obj 对象
* @return JSON 字符串
*/
public String toJson(Object obj) {
return JSONUtil.toJsonStr(obj);
}
/**
* JSON 字符串转为 Map
*
* @param json JSON 字符串
* @return Map 对象
*/
public Map<String, Object> parseJson(String json) {
if (StrUtil.isEmpty(json)) {
return MapUtil.newHashMap();
}
try {
return JSONUtil.toBean(json, Map.class);
} catch (Exception e) {
log.error("JSON 解析失败: {}", json, e);
return MapUtil.newHashMap();
}
}
/**
* 类型转换
*
* @param value
* @param type 目标类型
* @param <T> 泛型
* @return 转换后的值
*/
public <T> T convert(Object value, Class<T> type) {
return Convert.convert(type, value);
}
/**
* Map 中获取值
*
* @param map Map 对象
* @param key
* @return
*/
public Object get(Map<String, Object> map, String key) {
return MapUtil.get(map, key, Object.class);
}
/**
* Map 中获取字符串
*
* @param map Map 对象
* @param key
* @return 字符串值
*/
public String getString(Map<String, Object> map, String key) {
return MapUtil.getStr(map, key);
}
/**
* Map 中获取整数
*
* @param map Map 对象
* @param key
* @return 整数值
*/
public Integer getInt(Map<String, Object> map, String key) {
return MapUtil.getInt(map, key);
}
/**
* Map 中获取布尔值
*
* @param map Map 对象
* @param key
* @return 布尔值
*/
public Boolean getBool(Map<String, Object> map, String key) {
return MapUtil.getBool(map, key);
}
/**
* Map 中获取双精度浮点数
*
* @param map Map 对象
* @param key
* @return 双精度浮点数值
*/
public Double getDouble(Map<String, Object> map, String key) {
return MapUtil.getDouble(map, key);
}
/**
* 获取当前时间戳毫秒
*
* @return 时间戳
*/
public long currentTimeMillis() {
return System.currentTimeMillis();
}
}

View File

@ -1,82 +0,0 @@
package cn.iocoder.yudao.module.iot.service.product;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.iot.controller.admin.product.vo.script.IotProductScriptPageReqVO;
import cn.iocoder.yudao.module.iot.controller.admin.product.vo.script.IotProductScriptSaveReqVO;
import cn.iocoder.yudao.module.iot.controller.admin.product.vo.script.IotProductScriptTestReqVO;
import cn.iocoder.yudao.module.iot.controller.admin.product.vo.script.IotProductScriptTestRespVO;
import cn.iocoder.yudao.module.iot.dal.dataobject.product.IotProductScriptDO;
import jakarta.validation.Valid;
import java.util.List;
/**
* IoT 产品脚本信息 Service 接口
*
* @author 芋道源码
*/
public interface IotProductScriptService {
/**
* 创建IoT 产品脚本信息
*
* @param createReqVO 创建信息
* @return 编号
*/
Long createProductScript(@Valid IotProductScriptSaveReqVO createReqVO);
/**
* 更新IoT 产品脚本信息
*
* @param updateReqVO 更新信息
*/
void updateProductScript(@Valid IotProductScriptSaveReqVO updateReqVO);
/**
* 删除IoT 产品脚本信息
*
* @param id 编号
*/
void deleteProductScript(Long id);
/**
* 获得IoT 产品脚本信息
*
* @param id 编号
* @return IoT 产品脚本信息
*/
IotProductScriptDO getProductScript(Long id);
/**
* 获得IoT 产品脚本信息分页
*
* @param pageReqVO 分页查询
* @return IoT 产品脚本信息分页
*/
PageResult<IotProductScriptDO> getProductScriptPage(IotProductScriptPageReqVO pageReqVO);
/**
* 获取产品的脚本列表
*
* @param productId 产品ID
* @return 脚本列表
*/
List<IotProductScriptDO> getProductScriptListByProductId(Long productId);
/**
* 测试产品脚本
*
* @param testReqVO 测试请求
* @return 测试结果
*/
IotProductScriptTestRespVO testProductScript(@Valid IotProductScriptTestReqVO testReqVO);
/**
* 更新产品脚本状态
*
* @param id 脚本ID
* @param status 状态
*/
void updateProductScriptStatus(Long id, Integer status);
}

View File

@ -1,234 +0,0 @@
package cn.iocoder.yudao.module.iot.service.product;
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.product.vo.script.IotProductScriptPageReqVO;
import cn.iocoder.yudao.module.iot.controller.admin.product.vo.script.IotProductScriptSaveReqVO;
import cn.iocoder.yudao.module.iot.controller.admin.product.vo.script.IotProductScriptTestReqVO;
import cn.iocoder.yudao.module.iot.controller.admin.product.vo.script.IotProductScriptTestRespVO;
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.script.context.DeviceScriptContext;
import cn.iocoder.yudao.module.iot.script.service.ScriptService;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.PRODUCT_NOT_EXISTS;
import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.PRODUCT_SCRIPT_NOT_EXISTS;
// TODO @芋艿后续再 review
/**
* IoT 产品脚本信息 Service 实现类
*
* @author 芋道源码
*/
@Service
@Validated
@Slf4j
public class IotProductScriptServiceImpl implements IotProductScriptService {
@Resource
private IotProductScriptMapper productScriptMapper;
@Resource
private IotProductService productService;
@Resource
private ScriptService scriptService;
@Override
public Long createProductScript(IotProductScriptSaveReqVO createReqVO) {
// 验证产品是否存在
validateProductExists(createReqVO.getProductId());
// 插入
IotProductScriptDO productScript = BeanUtils.toBean(createReqVO, IotProductScriptDO.class);
// 初始化版本为1
productScript.setVersion(1);
// 初始化测试相关字段
productScript.setLastTestResult(null);
productScript.setLastTestTime(null);
productScriptMapper.insert(productScript);
// 返回
return productScript.getId();
}
@Override
public void updateProductScript(IotProductScriptSaveReqVO updateReqVO) {
// 校验存在
validateProductScriptExists(updateReqVO.getId());
// 获取旧的记录保留版本号和测试信息
IotProductScriptDO oldScript = getProductScript(updateReqVO.getId());
// 更新
IotProductScriptDO updateObj = BeanUtils.toBean(updateReqVO, IotProductScriptDO.class);
// 更新版本号
updateObj.setVersion(oldScript.getVersion() + 1);
// 保留测试相关信息
updateObj.setLastTestTime(oldScript.getLastTestTime());
updateObj.setLastTestResult(oldScript.getLastTestResult());
productScriptMapper.updateById(updateObj);
}
@Override
public void deleteProductScript(Long id) {
// 校验存在
validateProductScriptExists(id);
// 删除
productScriptMapper.deleteById(id);
}
private void validateProductScriptExists(Long id) {
if (productScriptMapper.selectById(id) == null) {
throw exception(PRODUCT_SCRIPT_NOT_EXISTS);
}
}
private void validateProductExists(Long productId) {
IotProductDO product = productService.getProduct(productId);
if (product == null) {
throw exception(PRODUCT_NOT_EXISTS);
}
}
@Override
public IotProductScriptDO getProductScript(Long id) {
return productScriptMapper.selectById(id);
}
@Override
public PageResult<IotProductScriptDO> getProductScriptPage(IotProductScriptPageReqVO pageReqVO) {
return productScriptMapper.selectPage(pageReqVO);
}
@Override
public List<IotProductScriptDO> getProductScriptListByProductId(Long productId) {
return productScriptMapper.selectList(new LambdaQueryWrapper<IotProductScriptDO>()
.eq(IotProductScriptDO::getProductId, productId)
.orderByDesc(IotProductScriptDO::getId));
}
@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());
}
// 创建测试上下文
IotProductDO product = productService.getProduct(testReqVO.getProductId());
DeviceScriptContext context = new DeviceScriptContext();
// 设置设备上下文使用产品信息测试时无具体设备
context.withDeviceInfo(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");
// 添加一些模拟的属性数据
Map<String, Object> properties = new HashMap<>();
properties.put("temp", 25.5);
properties.put("humidity", 60);
context.withProperties(properties);
break;
case 2: // EVENT_PARSER
params.put("method", "event");
params.put("identifier", "default");
// 添加事件数据
Map<String, Object> eventParams = new HashMap<>();
eventParams.put("timestamp", System.currentTimeMillis());
params.put("eventParams", eventParams);
break;
case 3: // COMMAND_ENCODER
params.put("method", "command");
// 添加命令参数
Map<String, Object> cmdParams = new HashMap<>();
cmdParams.put("cmdName", "setValue");
cmdParams.put("cmdValue", 100);
params.put("cmdParams", cmdParams);
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
public void updateProductScriptStatus(Long id, Integer status) {
// 校验存在
validateProductScriptExists(id);
// 更新状态
IotProductScriptDO updateObj = new IotProductScriptDO();
updateObj.setId(id);
updateObj.setStatus(status);
productScriptMapper.updateById(updateObj);
}
}

View File

@ -69,13 +69,13 @@ spring:
url: jdbc:mysql://127.0.0.1:3306/ruoyi-vue-pro?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&rewriteBatchedStatements=true&nullCatalogMeansCurrent=true
username: root
password: 123456
# tdengine: # IoT 数据库(需要 IoT 物联网再开启噢!)
# url: jdbc:TAOS-RS://127.0.0.1:6041/ruoyi_vue_pro
# driver-class-name: com.taosdata.jdbc.rs.RestfulDriver
# username: root
# password: taosdata
# druid:
# validation-query: SELECT SERVER_STATUS() # TDengine 数据源的有效性检查 SQL
tdengine: # IoT 数据库(需要 IoT 物联网再开启噢!)
url: jdbc:TAOS-RS://127.0.0.1:6041/ruoyi_vue_pro
driver-class-name: com.taosdata.jdbc.rs.RestfulDriver
username: root
password: taosdata
druid:
validation-query: SELECT SERVER_STATUS() # TDengine 数据源的有效性检查 SQL
# Redis 配置。Redisson 默认的配置足够使用,一般不需要进行调优
data: