reactor:【IoT 物联网】移除 script 脚本,简化逻辑复杂度
This commit is contained in:
parent
02c3aa748b
commit
cf52a16f6c
|
@ -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>
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
||||
}
|
|
@ -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;
|
||||
|
||||
}
|
|
@ -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;
|
||||
|
||||
}
|
|
@ -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;
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
||||
}
|
|
@ -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") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 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;
|
||||
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);";
|
||||
}
|
||||
}
|
|
@ -1,4 +0,0 @@
|
|||
/**
|
||||
* IoT 脚本模块,提供脚本引擎、执行环境和沙箱功能,支持 JavaScript 脚本的执行
|
||||
*/
|
||||
package cn.iocoder.yudao.module.iot.script;
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
|
@ -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 脚本类型(如 js、groovy 等)
|
||||
* @param script 脚本内容
|
||||
* @param context 脚本上下文
|
||||
* @return 脚本执行结果
|
||||
*/
|
||||
Object executeScript(String scriptType, String script, ScriptContext context);
|
||||
|
||||
/**
|
||||
* 执行脚本
|
||||
*
|
||||
* @param scriptType 脚本类型(如 js、groovy 等)
|
||||
* @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);
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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:
|
||||
|
|
Loading…
Reference in New Issue