feat(iot): 一期 Controller 补齐 (B2/B4-6/B10/B11/B12/B13)
对照前端 feat/iot-2.0 已固化 API 契约补齐 5 组缺失端点(发现于一期 19/19
宣称完成后前端联调阶段),归属原任务卡 Controller 层返工,不占用二期
B20+ 编号。
- B2 规则链: 补 PUT /disable /deploy /debug + POST /copy?id= +
新增 GET /rule-chain/get?id= 返 GraphVO(保留 /get/{id})
deployRuleChain=enable+主动 Pub/Sub evict(对齐 B8)
- B10 子系统: 新增 GET /device-count 聚合(HGETALL 返空 map 遵循 A6)
+ GET /get?id= query 别名(保留 /get/{id})
- B11 设备: 新增驼峰 PUT /bindSubsystem /batchBindSubsystem
+ 2 ReqVO,保留 kebab 兼容
- B12/B13 告警: 新增 IotAlarmRecordController(整缺)11 端点:
page/get/ack/unack/clear/archive/batch-{ack,clear,archive}/
history/remark;Service 补 6 方法(getPage/batchAck/
batchClear/batchArchive/updateRemark/listHistory)
+ Mapper 2 方法 + 8 VO
- B4/5/6 节点元数据: 新增 GET /iot/rule/provider/metadata 聚合端点;
3 SPI 加 default getMetadata(),4 Manager 加
listAllMetadata(),13 具体 Provider 覆写(中文 label
+ mdi: icon),schema MVP 空骨架 {rule:[]}
测试:
- iot-rule 191/191 全绿(+5 B2 补齐 +9 B4/5/6 补齐)
- iot-server 106 active/161 Skipped v1 遗产 全绿
(+6 B12/B13 补齐 +3 B10 补齐)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,10 +1,12 @@
|
||||
package com.viewsh.module.iot.rule.action;
|
||||
|
||||
import com.viewsh.module.iot.rule.controller.admin.vo.ProviderMetadataVO;
|
||||
import com.viewsh.module.iot.rule.spi.ActionProvider;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
@@ -60,4 +62,15 @@ public class ActionProviderManager {
|
||||
public Map<String, ActionProvider> all() {
|
||||
return Map.copyOf(providers);
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回所有已注册 Action Provider 的元数据列表(供前端动态面板消费)。
|
||||
*/
|
||||
public List<ProviderMetadataVO> listAllMetadata() {
|
||||
List<ProviderMetadataVO> result = new ArrayList<>(providers.size());
|
||||
for (ActionProvider p : providers.values()) {
|
||||
result.add(p.getMetadata());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.viewsh.framework.common.pojo.CommonResult;
|
||||
import com.viewsh.module.iot.api.alarm.IotAlarmRecordApi;
|
||||
import com.viewsh.module.iot.api.alarm.dto.IotAlarmClearReqDTO;
|
||||
import com.viewsh.module.iot.rule.controller.admin.vo.ProviderMetadataVO;
|
||||
import com.viewsh.module.iot.rule.engine.RuleContext;
|
||||
import com.viewsh.module.iot.rule.result.ActionResult;
|
||||
import com.viewsh.module.iot.rule.spi.ActionProvider;
|
||||
@@ -39,6 +40,18 @@ public class AlarmClearAction implements ActionProvider {
|
||||
return TYPE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ProviderMetadataVO getMetadata() {
|
||||
return ProviderMetadataVO.builder()
|
||||
.type(TYPE)
|
||||
.category("action")
|
||||
.label("清除告警")
|
||||
.icon("mdi:bell-off-outline")
|
||||
.description("清除(恢复)已触发的告警记录")
|
||||
.schema(ProviderMetadataVO.emptySchema())
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ActionResult executeAction(RuleContext ctx, JsonNode config) {
|
||||
try {
|
||||
|
||||
@@ -4,6 +4,7 @@ import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.viewsh.framework.common.pojo.CommonResult;
|
||||
import com.viewsh.module.iot.api.alarm.IotAlarmRecordApi;
|
||||
import com.viewsh.module.iot.api.alarm.dto.IotAlarmTriggerReqDTO;
|
||||
import com.viewsh.module.iot.rule.controller.admin.vo.ProviderMetadataVO;
|
||||
import com.viewsh.module.iot.rule.engine.RuleContext;
|
||||
import com.viewsh.module.iot.rule.result.ActionResult;
|
||||
import com.viewsh.module.iot.rule.spi.ActionProvider;
|
||||
@@ -46,6 +47,18 @@ public class AlarmTriggerAction implements ActionProvider {
|
||||
return TYPE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ProviderMetadataVO getMetadata() {
|
||||
return ProviderMetadataVO.builder()
|
||||
.type(TYPE)
|
||||
.category("action")
|
||||
.label("触发告警")
|
||||
.icon("mdi:bell-outline")
|
||||
.description("创建或更新告警记录")
|
||||
.schema(ProviderMetadataVO.emptySchema())
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ActionResult executeAction(RuleContext ctx, JsonNode config) {
|
||||
try {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.viewsh.module.iot.rule.action;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.viewsh.module.iot.rule.controller.admin.vo.ProviderMetadataVO;
|
||||
import com.viewsh.module.iot.rule.engine.RuleContext;
|
||||
import com.viewsh.module.iot.rule.result.ActionResult;
|
||||
import com.viewsh.module.iot.rule.spi.ActionProvider;
|
||||
@@ -39,6 +40,18 @@ public class DevicePropertySetAction implements ActionProvider {
|
||||
return TYPE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ProviderMetadataVO getMetadata() {
|
||||
return ProviderMetadataVO.builder()
|
||||
.type(TYPE)
|
||||
.category("action")
|
||||
.label("设备属性设置")
|
||||
.icon("mdi:tune-vertical")
|
||||
.description("向设备下发属性设置指令")
|
||||
.schema(ProviderMetadataVO.emptySchema())
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ActionResult executeAction(RuleContext ctx, JsonNode config) {
|
||||
try {
|
||||
|
||||
@@ -6,6 +6,7 @@ import com.viewsh.framework.common.pojo.CommonResult;
|
||||
import com.viewsh.module.iot.api.device.IotDeviceControlApi;
|
||||
import com.viewsh.module.iot.api.device.dto.IotDeviceServiceInvokeReqDTO;
|
||||
import com.viewsh.module.iot.api.device.dto.IotDeviceServiceInvokeRespDTO;
|
||||
import com.viewsh.module.iot.rule.controller.admin.vo.ProviderMetadataVO;
|
||||
import com.viewsh.module.iot.rule.engine.RuleContext;
|
||||
import com.viewsh.module.iot.rule.result.ActionResult;
|
||||
import com.viewsh.module.iot.rule.spi.ActionProvider;
|
||||
@@ -45,6 +46,18 @@ public class DeviceServiceInvokeAction implements ActionProvider {
|
||||
return TYPE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ProviderMetadataVO getMetadata() {
|
||||
return ProviderMetadataVO.builder()
|
||||
.type(TYPE)
|
||||
.category("action")
|
||||
.label("设备服务调用")
|
||||
.icon("mdi:cog-play")
|
||||
.description("向设备下发服务调用指令")
|
||||
.schema(ProviderMetadataVO.emptySchema())
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ActionResult executeAction(RuleContext ctx, JsonNode config) {
|
||||
try {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.viewsh.module.iot.rule.action;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.viewsh.module.iot.rule.controller.admin.vo.ProviderMetadataVO;
|
||||
import com.viewsh.module.iot.rule.engine.RuleContext;
|
||||
import com.viewsh.module.iot.rule.notify.NotifyDispatcher;
|
||||
import com.viewsh.module.iot.rule.notify.model.NotifyResult;
|
||||
@@ -74,6 +75,18 @@ public class NotifyAction implements ActionProvider {
|
||||
return TYPE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ProviderMetadataVO getMetadata() {
|
||||
return ProviderMetadataVO.builder()
|
||||
.type(TYPE)
|
||||
.category("action")
|
||||
.label("发送通知")
|
||||
.icon("mdi:message-text-outline")
|
||||
.description("通过短信、邮件、站内信、Webhook 等渠道发送通知")
|
||||
.schema(ProviderMetadataVO.emptySchema())
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ActionResult executeAction(RuleContext ctx, JsonNode config) {
|
||||
try {
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
package com.viewsh.module.iot.rule.condition;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.viewsh.module.iot.rule.controller.admin.vo.ProviderMetadataVO;
|
||||
import com.viewsh.module.iot.rule.engine.RuleContext;
|
||||
import com.viewsh.module.iot.rule.spi.ConditionEvaluator;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
@@ -65,4 +67,15 @@ public class ConditionEvaluatorManager {
|
||||
public boolean supports(String type) {
|
||||
return evaluatorsByType.containsKey(type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns metadata for all registered condition evaluators (consumed by the frontend dynamic panel).
|
||||
*/
|
||||
public List<ProviderMetadataVO> listAllMetadata() {
|
||||
List<ProviderMetadataVO> result = new ArrayList<>(evaluatorsByType.size());
|
||||
for (ConditionEvaluator e : evaluatorsByType.values()) {
|
||||
result.add(e.getMetadata());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.viewsh.module.iot.rule.condition;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.viewsh.module.iot.rule.controller.admin.vo.ProviderMetadataVO;
|
||||
import com.viewsh.module.iot.rule.engine.RuleContext;
|
||||
import com.viewsh.module.iot.rule.spi.ConditionEvaluator;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@@ -46,6 +47,18 @@ public class DeviceStateConditionEvaluator implements ConditionEvaluator {
|
||||
return TYPE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ProviderMetadataVO getMetadata() {
|
||||
return ProviderMetadataVO.builder()
|
||||
.type(TYPE)
|
||||
.category("condition")
|
||||
.label("设备在线状态")
|
||||
.icon("mdi:wifi")
|
||||
.description("判断设备当前是否在线")
|
||||
.schema(ProviderMetadataVO.emptySchema())
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean evaluate(RuleContext ctx, JsonNode config) {
|
||||
String expectedState = config.path("state").asText("online").toLowerCase();
|
||||
|
||||
@@ -5,6 +5,7 @@ import com.googlecode.aviator.AviatorEvaluator;
|
||||
import com.googlecode.aviator.AviatorEvaluatorInstance;
|
||||
import com.googlecode.aviator.Expression;
|
||||
import com.googlecode.aviator.Options;
|
||||
import com.viewsh.module.iot.rule.controller.admin.vo.ProviderMetadataVO;
|
||||
import com.viewsh.module.iot.rule.engine.RuleContext;
|
||||
import com.viewsh.module.iot.rule.spi.ConditionEvaluator;
|
||||
import com.viewsh.module.iot.rule.template.TemplateResolver;
|
||||
@@ -66,6 +67,18 @@ public class ExpressionConditionEvaluator implements ConditionEvaluator {
|
||||
return TYPE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ProviderMetadataVO getMetadata() {
|
||||
return ProviderMetadataVO.builder()
|
||||
.type(TYPE)
|
||||
.category("condition")
|
||||
.label("表达式条件")
|
||||
.icon("mdi:code-braces")
|
||||
.description("使用 Aviator 表达式对设备数据进行条件判断")
|
||||
.schema(ProviderMetadataVO.emptySchema())
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean evaluate(RuleContext ctx, JsonNode config) {
|
||||
String rawExpression = extractExpression(config);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.viewsh.module.iot.rule.condition;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.viewsh.module.iot.rule.controller.admin.vo.ProviderMetadataVO;
|
||||
import com.viewsh.module.iot.rule.engine.RuleContext;
|
||||
import com.viewsh.module.iot.rule.spi.ConditionEvaluator;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@@ -53,6 +54,18 @@ public class TimeRangeConditionEvaluator implements ConditionEvaluator {
|
||||
return TYPE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ProviderMetadataVO getMetadata() {
|
||||
return ProviderMetadataVO.builder()
|
||||
.type(TYPE)
|
||||
.category("condition")
|
||||
.label("时间范围")
|
||||
.icon("mdi:calendar-clock")
|
||||
.description("判断当前时间是否在配置的时间段内(支持跨日、每周模式)")
|
||||
.schema(ProviderMetadataVO.emptySchema())
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean evaluate(RuleContext ctx, JsonNode config) {
|
||||
String mode = config.path("mode").asText("daily");
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
package com.viewsh.module.iot.rule.controller.admin;
|
||||
|
||||
import com.viewsh.framework.common.pojo.CommonResult;
|
||||
import com.viewsh.module.iot.rule.action.ActionProviderManager;
|
||||
import com.viewsh.module.iot.rule.condition.ConditionEvaluatorManager;
|
||||
import com.viewsh.module.iot.rule.controller.admin.vo.ProviderMetadataVO;
|
||||
import com.viewsh.module.iot.rule.spi.TriggerProviderManager;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static com.viewsh.framework.common.pojo.CommonResult.success;
|
||||
|
||||
/**
|
||||
* Provider Metadata 端点(B4/B5/B6 补齐)。
|
||||
*
|
||||
* <p>供前端动态属性面板(F3)消费,返回所有已注册
|
||||
* Trigger / Condition / Action Provider 的元数据列表。
|
||||
*
|
||||
* <p>端点:{@code GET /iot/rule/provider/metadata}
|
||||
*
|
||||
* <p>响应格式(对齐前端 {@code NodeTypeMeta}):
|
||||
* <pre>{@code
|
||||
* [
|
||||
* { "type": "device_property", "category": "trigger", "label": "设备属性上报",
|
||||
* "icon": "mdi:tune", "description": "...", "schema": {"rule": []} },
|
||||
* { "type": "expression", "category": "condition", ... },
|
||||
* { "type": "alarm_trigger", "category": "action", ... },
|
||||
* ...
|
||||
* ]
|
||||
* }</pre>
|
||||
*/
|
||||
@Tag(name = "管理后台 - IoT 规则节点 Provider 元数据")
|
||||
@RestController
|
||||
@RequestMapping("/iot/rule/provider")
|
||||
@Validated
|
||||
public class IotProviderMetadataController {
|
||||
|
||||
@Resource
|
||||
private TriggerProviderManager triggerProviderManager;
|
||||
|
||||
@Resource
|
||||
private ConditionEvaluatorManager conditionEvaluatorManager;
|
||||
|
||||
@Resource
|
||||
private ActionProviderManager actionProviderManager;
|
||||
|
||||
/**
|
||||
* 获取所有节点 Provider 元数据(trigger + condition + action 三类聚合)。
|
||||
*
|
||||
* @return ProviderMetadataVO 列表,每条包含 type / category / label / icon / description / schema
|
||||
*/
|
||||
@GetMapping("/metadata")
|
||||
@Operation(summary = "获取所有规则节点 Provider 元数据(供前端动态属性面板使用)")
|
||||
@PreAuthorize("@ss.hasPermission('iot:rule:query')")
|
||||
public CommonResult<List<ProviderMetadataVO>> listProviderMetadata() {
|
||||
List<ProviderMetadataVO> result = new ArrayList<>();
|
||||
result.addAll(triggerProviderManager.listAllMetadata());
|
||||
result.addAll(conditionEvaluatorManager.listAllMetadata());
|
||||
result.addAll(actionProviderManager.listAllMetadata());
|
||||
return success(result);
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package com.viewsh.module.iot.rule.controller.admin;
|
||||
|
||||
import com.viewsh.framework.common.pojo.CommonResult;
|
||||
import com.viewsh.framework.common.pojo.PageResult;
|
||||
import com.viewsh.module.iot.rule.controller.admin.vo.IotRuleChainDebugReqVO;
|
||||
import com.viewsh.module.iot.rule.controller.admin.vo.IotRuleChainGraphVO;
|
||||
import com.viewsh.module.iot.rule.controller.admin.vo.IotRuleChainPageReqVO;
|
||||
import com.viewsh.module.iot.rule.controller.admin.vo.IotRuleChainRespVO;
|
||||
@@ -16,6 +17,8 @@ import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import static com.viewsh.framework.common.pojo.CommonResult.success;
|
||||
|
||||
@Tag(name = "管理后台 - IoT 规则链")
|
||||
@@ -52,15 +55,23 @@ public class IotRuleChainController {
|
||||
}
|
||||
|
||||
@GetMapping("/get/{id}")
|
||||
@Operation(summary = "获得规则链")
|
||||
@Operation(summary = "获得规则链(path 变量风格,向后兼容)")
|
||||
@Parameter(name = "id", description = "规则链编号", required = true, example = "1024")
|
||||
@PreAuthorize("@ss.hasPermission('iot:rule:query')")
|
||||
public CommonResult<IotRuleChainRespVO> getRuleChain(@PathVariable("id") Long id) {
|
||||
return success(ruleChainService.getRuleChain(id));
|
||||
}
|
||||
|
||||
@GetMapping("/get")
|
||||
@Operation(summary = "获得规则链图(含节点和连线,query 参数风格)")
|
||||
@Parameter(name = "id", description = "规则链编号", required = true, example = "1024")
|
||||
@PreAuthorize("@ss.hasPermission('iot:rule:query')")
|
||||
public CommonResult<IotRuleChainGraphVO> getRuleChainById(@RequestParam("id") Long id) {
|
||||
return success(ruleChainService.getRuleChainGraph(id));
|
||||
}
|
||||
|
||||
@GetMapping("/graph/{id}")
|
||||
@Operation(summary = "获得规则链图(含节点和连线)")
|
||||
@Operation(summary = "获得规则链图(含节点和连线,path 变量风格,向后兼容)")
|
||||
@Parameter(name = "id", description = "规则链编号", required = true, example = "1024")
|
||||
@PreAuthorize("@ss.hasPermission('iot:rule:query')")
|
||||
public CommonResult<IotRuleChainGraphVO> getRuleChainGraph(@PathVariable("id") Long id) {
|
||||
@@ -83,4 +94,36 @@ public class IotRuleChainController {
|
||||
return success(true);
|
||||
}
|
||||
|
||||
@PutMapping("/disable")
|
||||
@Operation(summary = "禁用规则链")
|
||||
@PreAuthorize("@ss.hasPermission('iot:rule:update')")
|
||||
public CommonResult<Boolean> disableRuleChain(@RequestBody Map<String, Long> body) {
|
||||
ruleChainService.disableRuleChain(body.get("id"));
|
||||
return success(true);
|
||||
}
|
||||
|
||||
@PutMapping("/deploy")
|
||||
@Operation(summary = "发布规则链(启用 + 主动驱逐缓存)")
|
||||
@PreAuthorize("@ss.hasPermission('iot:rule:update')")
|
||||
public CommonResult<Boolean> deployRuleChain(@RequestBody Map<String, Long> body) {
|
||||
ruleChainService.deployRuleChain(body.get("id"));
|
||||
return success(true);
|
||||
}
|
||||
|
||||
@PutMapping("/debug")
|
||||
@Operation(summary = "切换调试模式")
|
||||
@PreAuthorize("@ss.hasPermission('iot:rule:update')")
|
||||
public CommonResult<Boolean> toggleDebug(@Valid @RequestBody IotRuleChainDebugReqVO req) {
|
||||
ruleChainService.toggleDebug(req.getId(), req.getEnabled());
|
||||
return success(true);
|
||||
}
|
||||
|
||||
@PostMapping("/copy")
|
||||
@Operation(summary = "复制规则链")
|
||||
@Parameter(name = "id", description = "源规则链编号", required = true)
|
||||
@PreAuthorize("@ss.hasPermission('iot:rule:create')")
|
||||
public CommonResult<Long> copyRuleChain(@RequestParam("id") Long id) {
|
||||
return success(ruleChainService.copyRuleChain(id));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.viewsh.module.iot.rule.controller.admin.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 管理后台 - IoT 规则链调试模式切换 Request VO
|
||||
*/
|
||||
@Schema(description = "管理后台 - IoT 规则链调试模式切换 Request VO")
|
||||
@Data
|
||||
public class IotRuleChainDebugReqVO {
|
||||
|
||||
@Schema(description = "规则链编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
|
||||
@NotNull(message = "规则链编号不能为空")
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "是否启用调试模式", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
|
||||
@NotNull(message = "调试模式标志不能为空")
|
||||
private Boolean enabled;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package com.viewsh.module.iot.rule.controller.admin.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Provider Metadata VO — 供前端动态属性面板(F3/F4/F5/F6)消费。
|
||||
*
|
||||
* <p>端点:GET /iot/rule/provider/metadata
|
||||
*
|
||||
* <p>对齐前端 {@code NodeTypeMeta} 类型约定:
|
||||
* <ul>
|
||||
* <li>{@code type} — Provider 类型标识,与 rule_node.type 一致</li>
|
||||
* <li>{@code category} — "trigger" / "condition" / "action"</li>
|
||||
* <li>{@code label} — 显示名(中文)</li>
|
||||
* <li>{@code icon} — iconify 图标名(可选)</li>
|
||||
* <li>{@code description} — 工具提示(可选)</li>
|
||||
* <li>{@code schema} — form-create 配置,MVP 阶段返回 {@code {"rule":[]}}</li>
|
||||
* </ul>
|
||||
*/
|
||||
@Schema(description = "节点 Provider 元数据")
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class ProviderMetadataVO {
|
||||
|
||||
@Schema(description = "Provider 类型标识,与 rule_node.type 一致", example = "device_property")
|
||||
private String type;
|
||||
|
||||
@Schema(description = "节点类别:trigger / condition / action", example = "trigger")
|
||||
private String category;
|
||||
|
||||
@Schema(description = "节点显示名", example = "设备属性上报")
|
||||
private String label;
|
||||
|
||||
@Schema(description = "iconify 图标名(可选)", example = "mdi:bell-outline")
|
||||
private String icon;
|
||||
|
||||
@Schema(description = "工具提示(可选)")
|
||||
private String description;
|
||||
|
||||
/**
|
||||
* form-create 配置。MVP 阶段返回 {@code {"rule":[]}};
|
||||
* 后续 F4/F5/F6 widget 就绪后由前端本地定义或后端补全。
|
||||
*/
|
||||
@Schema(description = "form-create schema,MVP 阶段为 {\"rule\":[]}")
|
||||
private Map<String, Object> schema;
|
||||
|
||||
/** 构造空 schema({@code {"rule":[]}}),供 default getMetadata() 调用。 */
|
||||
public static Map<String, Object> emptySchema() {
|
||||
return Collections.singletonMap("rule", Collections.emptyList());
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.viewsh.module.iot.rule.engine;
|
||||
|
||||
import com.viewsh.module.iot.rule.controller.admin.vo.ProviderMetadataVO;
|
||||
import com.viewsh.module.iot.rule.dal.dataobject.enums.RuleNodeCategory;
|
||||
|
||||
/**
|
||||
@@ -27,4 +28,21 @@ public interface NodeProvider {
|
||||
* 抛出 RuntimeException 将被 DagExecutor 转为 FAILURE 并交由链级 try-catch 兜底。
|
||||
*/
|
||||
NodeResult execute(RuleContext ctx, String config);
|
||||
|
||||
/**
|
||||
* 返回该 Provider 的元数据(供前端动态属性面板 F3 消费)。
|
||||
*
|
||||
* <p>默认实现以 {@link #getCategory()} + {@link #getType()} 构造元数据,schema 返回空骨架。
|
||||
* 具体 Provider 可覆盖以提供更友好的 label / icon / description。
|
||||
*/
|
||||
default ProviderMetadataVO getMetadata() {
|
||||
return ProviderMetadataVO.builder()
|
||||
.type(getType())
|
||||
.category(getCategory().getValue())
|
||||
.label(getType())
|
||||
.icon(null)
|
||||
.description(null)
|
||||
.schema(ProviderMetadataVO.emptySchema())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
package com.viewsh.module.iot.rule.engine;
|
||||
|
||||
import com.viewsh.module.iot.rule.controller.admin.vo.ProviderMetadataVO;
|
||||
import com.viewsh.module.iot.rule.dal.dataobject.enums.RuleNodeCategory;
|
||||
import com.viewsh.module.iot.rule.engine.exception.RuleChainException;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
@@ -42,6 +44,17 @@ public class NodeProviderRegistry {
|
||||
return p;
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回所有已注册节点 Provider 的元数据列表(供管理端口使用)。
|
||||
*/
|
||||
public List<ProviderMetadataVO> listAllMetadata() {
|
||||
List<ProviderMetadataVO> result = new ArrayList<>(providersByKey.size());
|
||||
for (NodeProvider p : providersByKey.values()) {
|
||||
result.add(p.getMetadata());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static String key(RuleNodeCategory category, String type) {
|
||||
return category.getValue() + ":" + type;
|
||||
}
|
||||
|
||||
@@ -51,6 +51,29 @@ public interface IotRuleChainService {
|
||||
*/
|
||||
void disableRuleChain(Long id);
|
||||
|
||||
/**
|
||||
* 发布规则链到运行时(enable + 主动驱逐缓存,令所有实例重载)
|
||||
*
|
||||
* @param id 规则链编号
|
||||
*/
|
||||
void deployRuleChain(Long id);
|
||||
|
||||
/**
|
||||
* 切换调试模式
|
||||
*
|
||||
* @param id 规则链编号
|
||||
* @param enabled true=开启调试,false=关闭调试
|
||||
*/
|
||||
void toggleDebug(Long id, boolean enabled);
|
||||
|
||||
/**
|
||||
* 复制规则链(深拷 chain + nodes + links,名称追加 "_copy",status=disabled,version=0)
|
||||
*
|
||||
* @param id 源规则链编号
|
||||
* @return 新规则链编号
|
||||
*/
|
||||
Long copyRuleChain(Long id);
|
||||
|
||||
/**
|
||||
* 获得规则链基础信息
|
||||
*
|
||||
|
||||
@@ -173,6 +173,106 @@ public class IotRuleChainServiceImpl implements IotRuleChainService {
|
||||
ruleChainMapper.updateById(update);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void deployRuleChain(Long id) {
|
||||
IotRuleChainDO chain = validateRuleChainExists(id);
|
||||
// 1. 确保链处于启用状态
|
||||
if (!RuleChainStatus.ENABLED.getValue().equals(chain.getStatus())) {
|
||||
IotRuleChainDO update = new IotRuleChainDO();
|
||||
update.setId(id);
|
||||
update.setStatus(RuleChainStatus.ENABLED.getValue());
|
||||
ruleChainMapper.updateById(update);
|
||||
}
|
||||
// 2. 主动发布 Pub/Sub 驱逐事件,令所有实例重载缓存
|
||||
if (redisTemplate != null) {
|
||||
try {
|
||||
Map<String, Object> event = new LinkedHashMap<>();
|
||||
event.put("chainId", id);
|
||||
event.put("tenantId", TenantContextHolder.getTenantId());
|
||||
event.put("version", chain.getVersion());
|
||||
redisTemplate.convertAndSend(RuleChainCache.EVICT_CHANNEL, JsonUtils.toJsonString(event));
|
||||
} catch (Exception e) {
|
||||
log.warn("[IotRuleChainServiceImpl] deploy 发布缓存驱逐事件失败 chainId={}", id, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void toggleDebug(Long id, boolean enabled) {
|
||||
validateRuleChainExists(id);
|
||||
IotRuleChainDO update = new IotRuleChainDO();
|
||||
update.setId(id);
|
||||
update.setDebugMode(enabled);
|
||||
ruleChainMapper.updateById(update);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Long copyRuleChain(Long id) {
|
||||
// 1. 查询原链
|
||||
IotRuleChainDO original = validateRuleChainExists(id);
|
||||
|
||||
// 2. 深拷 chain(id/create_time 置空,让 DB 生成)
|
||||
IotRuleChainDO copy = new IotRuleChainDO();
|
||||
copy.setName(original.getName() + "_copy");
|
||||
copy.setDescription(original.getDescription());
|
||||
copy.setType(original.getType());
|
||||
copy.setStatus(RuleChainStatus.DISABLED.getValue());
|
||||
copy.setPriority(original.getPriority());
|
||||
copy.setVersion(0L);
|
||||
copy.setDebugMode(original.getDebugMode());
|
||||
copy.setSubsystemId(original.getSubsystemId());
|
||||
copy.setProductId(original.getProductId());
|
||||
copy.setDeviceId(original.getDeviceId());
|
||||
ruleChainMapper.insert(copy);
|
||||
Long newChainId = copy.getId();
|
||||
Long tenantId = copy.getTenantId();
|
||||
|
||||
// 3. 深拷 nodes,建立 old_id → new_id 映射
|
||||
List<IotRuleNodeDO> oldNodes = ruleNodeMapper.selectByChainId(id);
|
||||
Map<Long, Long> nodeIdMap = new HashMap<>();
|
||||
if (!oldNodes.isEmpty()) {
|
||||
List<IotRuleNodeDO> newNodes = oldNodes.stream()
|
||||
.map(n -> IotRuleNodeDO.builder()
|
||||
.ruleChainId(newChainId)
|
||||
.name(n.getName())
|
||||
.category(n.getCategory())
|
||||
.type(n.getType())
|
||||
.configuration(n.getConfiguration())
|
||||
.positionX(n.getPositionX())
|
||||
.positionY(n.getPositionY())
|
||||
.tenantId(tenantId)
|
||||
.build())
|
||||
.collect(Collectors.toList());
|
||||
ruleNodeMapper.insertBatch(newNodes);
|
||||
// 建立映射:新节点是按原顺序插入的,id 由 insertBatch 回填
|
||||
for (int i = 0; i < oldNodes.size(); i++) {
|
||||
nodeIdMap.put(oldNodes.get(i).getId(), newNodes.get(i).getId());
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 深拷 links,用 nodeIdMap 重写 sourceNodeId/targetNodeId
|
||||
List<IotRuleLinkDO> oldLinks = ruleLinkMapper.selectByChainId(id);
|
||||
if (!oldLinks.isEmpty()) {
|
||||
List<IotRuleLinkDO> newLinks = oldLinks.stream()
|
||||
.map(l -> IotRuleLinkDO.builder()
|
||||
.ruleChainId(newChainId)
|
||||
.sourceNodeId(nodeIdMap.getOrDefault(l.getSourceNodeId(), l.getSourceNodeId()))
|
||||
.targetNodeId(nodeIdMap.getOrDefault(l.getTargetNodeId(), l.getTargetNodeId()))
|
||||
.relationType(l.getRelationType())
|
||||
.condition(l.getCondition())
|
||||
.sortOrder(l.getSortOrder())
|
||||
.tenantId(tenantId)
|
||||
.build())
|
||||
.collect(Collectors.toList());
|
||||
ruleLinkMapper.insertBatch(newLinks);
|
||||
}
|
||||
|
||||
return newChainId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public IotRuleChainRespVO getRuleChain(Long id) {
|
||||
IotRuleChainDO chain = validateRuleChainExists(id);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.viewsh.module.iot.rule.spi;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.viewsh.module.iot.rule.controller.admin.vo.ProviderMetadataVO;
|
||||
import com.viewsh.module.iot.rule.engine.RuleContext;
|
||||
|
||||
/**
|
||||
@@ -33,4 +34,21 @@ public interface ConditionEvaluator {
|
||||
* @return true = condition met, false = not met
|
||||
*/
|
||||
boolean evaluate(RuleContext ctx, JsonNode config);
|
||||
|
||||
/**
|
||||
* Returns metadata for this condition evaluator (consumed by the frontend dynamic panel F3).
|
||||
*
|
||||
* <p>Default implementation uses {@link #getType()} as label with empty schema skeleton.
|
||||
* Concrete evaluators may override to provide friendlier label / icon / description.
|
||||
*/
|
||||
default ProviderMetadataVO getMetadata() {
|
||||
return ProviderMetadataVO.builder()
|
||||
.type(getType())
|
||||
.category("condition")
|
||||
.label(getType())
|
||||
.icon(null)
|
||||
.description(null)
|
||||
.schema(ProviderMetadataVO.emptySchema())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.viewsh.module.iot.rule.spi;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.viewsh.module.iot.core.mq.message.IotDeviceMessage;
|
||||
import com.viewsh.module.iot.rule.controller.admin.vo.ProviderMetadataVO;
|
||||
import com.viewsh.module.iot.rule.engine.CompiledRuleChain;
|
||||
import com.viewsh.module.iot.rule.engine.RuleContext;
|
||||
|
||||
@@ -52,4 +53,22 @@ public interface TriggerProvider {
|
||||
default void unregister(CompiledRuleChain chain) {
|
||||
// 默认无操作
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回该 Provider 的元数据(供前端动态属性面板 F3 消费)。
|
||||
*
|
||||
* <p>默认实现以 {@link #getType()} 作为 label,icon/description 留空,
|
||||
* schema 返回空骨架 {@code {"rule":[]}}。
|
||||
* 具体 Provider 可覆盖以提供更友好的 label / icon / description。
|
||||
*/
|
||||
default ProviderMetadataVO getMetadata() {
|
||||
return ProviderMetadataVO.builder()
|
||||
.type(getType())
|
||||
.category("trigger")
|
||||
.label(getType())
|
||||
.icon(null)
|
||||
.description(null)
|
||||
.schema(ProviderMetadataVO.emptySchema())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
package com.viewsh.module.iot.rule.spi;
|
||||
|
||||
import com.viewsh.framework.common.exception.ServiceException;
|
||||
import com.viewsh.module.iot.rule.controller.admin.vo.ProviderMetadataVO;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
@@ -72,4 +74,15 @@ public class TriggerProviderManager {
|
||||
public boolean contains(String type) {
|
||||
return providers.containsKey(type);
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回所有已注册触发器 Provider 的元数据列表(供前端动态面板消费)。
|
||||
*/
|
||||
public List<ProviderMetadataVO> listAllMetadata() {
|
||||
List<ProviderMetadataVO> result = new ArrayList<>(providers.size());
|
||||
for (TriggerProvider p : providers.values()) {
|
||||
result.add(p.getMetadata());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.viewsh.module.iot.rule.trigger;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.viewsh.module.iot.core.enums.IotDeviceMessageMethodEnum;
|
||||
import com.viewsh.module.iot.core.mq.message.IotDeviceMessage;
|
||||
import com.viewsh.module.iot.rule.controller.admin.vo.ProviderMetadataVO;
|
||||
import com.viewsh.module.iot.rule.engine.RuleContext;
|
||||
import com.viewsh.module.iot.rule.spi.TriggerProvider;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@@ -46,6 +47,18 @@ public class DeviceEventTriggerProvider implements TriggerProvider {
|
||||
return TYPE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ProviderMetadataVO getMetadata() {
|
||||
return ProviderMetadataVO.builder()
|
||||
.type(TYPE)
|
||||
.category("trigger")
|
||||
.label("设备事件上报")
|
||||
.icon("mdi:bell-ring-outline")
|
||||
.description("当设备上报指定事件时触发规则链")
|
||||
.schema(ProviderMetadataVO.emptySchema())
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean matches(IotDeviceMessage msg, JsonNode config, RuleContext ctx) {
|
||||
// 1. method 必须是 thing.event.post
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.viewsh.module.iot.rule.trigger;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.viewsh.module.iot.core.enums.IotDeviceMessageMethodEnum;
|
||||
import com.viewsh.module.iot.core.mq.message.IotDeviceMessage;
|
||||
import com.viewsh.module.iot.rule.controller.admin.vo.ProviderMetadataVO;
|
||||
import com.viewsh.module.iot.rule.engine.RuleContext;
|
||||
import com.viewsh.module.iot.rule.spi.TriggerProvider;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@@ -42,6 +43,18 @@ public class DevicePropertyTriggerProvider implements TriggerProvider {
|
||||
return TYPE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ProviderMetadataVO getMetadata() {
|
||||
return ProviderMetadataVO.builder()
|
||||
.type(TYPE)
|
||||
.category("trigger")
|
||||
.label("设备属性上报")
|
||||
.icon("mdi:tune")
|
||||
.description("当设备上报指定属性时触发规则链")
|
||||
.schema(ProviderMetadataVO.emptySchema())
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean matches(IotDeviceMessage msg, JsonNode config, RuleContext ctx) {
|
||||
// 1. method 必须是 thing.property.post
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.viewsh.module.iot.rule.trigger;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.viewsh.module.iot.core.enums.IotDeviceMessageMethodEnum;
|
||||
import com.viewsh.module.iot.core.mq.message.IotDeviceMessage;
|
||||
import com.viewsh.module.iot.rule.controller.admin.vo.ProviderMetadataVO;
|
||||
import com.viewsh.module.iot.rule.engine.RuleContext;
|
||||
import com.viewsh.module.iot.rule.spi.TriggerProvider;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@@ -46,6 +47,18 @@ public class DeviceServiceTriggerProvider implements TriggerProvider {
|
||||
return TYPE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ProviderMetadataVO getMetadata() {
|
||||
return ProviderMetadataVO.builder()
|
||||
.type(TYPE)
|
||||
.category("trigger")
|
||||
.label("设备服务调用回复")
|
||||
.icon("mdi:cog-play-outline")
|
||||
.description("当设备服务调用回复时触发规则链")
|
||||
.schema(ProviderMetadataVO.emptySchema())
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean matches(IotDeviceMessage msg, JsonNode config, RuleContext ctx) {
|
||||
// 1. method 必须是 thing.service.invoke
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.viewsh.module.iot.rule.trigger;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.viewsh.module.iot.core.enums.IotDeviceMessageMethodEnum;
|
||||
import com.viewsh.module.iot.core.mq.message.IotDeviceMessage;
|
||||
import com.viewsh.module.iot.rule.controller.admin.vo.ProviderMetadataVO;
|
||||
import com.viewsh.module.iot.rule.engine.RuleContext;
|
||||
import com.viewsh.module.iot.rule.spi.TriggerProvider;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@@ -41,6 +42,18 @@ public class DeviceStateTriggerProvider implements TriggerProvider {
|
||||
return TYPE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ProviderMetadataVO getMetadata() {
|
||||
return ProviderMetadataVO.builder()
|
||||
.type(TYPE)
|
||||
.category("trigger")
|
||||
.label("设备状态变化")
|
||||
.icon("mdi:access-point")
|
||||
.description("当设备上下线状态发生变化时触发规则链")
|
||||
.schema(ProviderMetadataVO.emptySchema())
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean matches(IotDeviceMessage msg, JsonNode config, RuleContext ctx) {
|
||||
// 1. method 必须是 thing.state.update
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.viewsh.module.iot.rule.trigger;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.viewsh.module.iot.core.mq.message.IotDeviceMessage;
|
||||
import com.viewsh.module.iot.rule.controller.admin.vo.ProviderMetadataVO;
|
||||
import com.viewsh.module.iot.rule.engine.CompiledRuleChain;
|
||||
import com.viewsh.module.iot.rule.engine.RuleContext;
|
||||
import com.viewsh.module.iot.rule.spi.TriggerProvider;
|
||||
@@ -64,6 +65,18 @@ public class TimerTriggerProvider implements TriggerProvider, DisposableBean {
|
||||
return TYPE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ProviderMetadataVO getMetadata() {
|
||||
return ProviderMetadataVO.builder()
|
||||
.type(TYPE)
|
||||
.category("trigger")
|
||||
.label("定时触发")
|
||||
.icon("mdi:clock-outline")
|
||||
.description("按 CRON 表达式定时触发规则链")
|
||||
.schema(ProviderMetadataVO.emptySchema())
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Timer 触发器不通过消息匹配触发,始终返回 false。
|
||||
* Timer 触发走 CRON 调度回调路径。
|
||||
|
||||
@@ -0,0 +1,215 @@
|
||||
package com.viewsh.module.iot.rule.controller;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.viewsh.module.iot.rule.action.ActionProviderManager;
|
||||
import com.viewsh.module.iot.rule.action.AlarmTriggerAction;
|
||||
import com.viewsh.module.iot.rule.condition.ConditionEvaluatorManager;
|
||||
import com.viewsh.module.iot.rule.condition.TimeRangeConditionEvaluator;
|
||||
import com.viewsh.module.iot.rule.controller.admin.vo.ProviderMetadataVO;
|
||||
import com.viewsh.module.iot.rule.dal.dataobject.enums.RuleNodeCategory;
|
||||
import com.viewsh.module.iot.rule.engine.NodeResult;
|
||||
import com.viewsh.module.iot.rule.engine.RuleContext;
|
||||
import com.viewsh.module.iot.rule.result.ActionResult;
|
||||
import com.viewsh.module.iot.rule.spi.ActionProvider;
|
||||
import com.viewsh.module.iot.rule.spi.ConditionEvaluator;
|
||||
import com.viewsh.module.iot.rule.spi.TriggerProvider;
|
||||
import com.viewsh.module.iot.rule.spi.TriggerProviderManager;
|
||||
import com.viewsh.module.iot.rule.trigger.DeviceEventTriggerProvider;
|
||||
import com.viewsh.module.iot.rule.trigger.DevicePropertyTriggerProvider;
|
||||
import com.viewsh.module.iot.rule.trigger.DeviceServiceTriggerProvider;
|
||||
import com.viewsh.module.iot.rule.trigger.DeviceStateTriggerProvider;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* B4/B5/B6 Provider Metadata 端点单元测试。
|
||||
*
|
||||
* <p>测试内容:
|
||||
* <ol>
|
||||
* <li>TriggerProviderManager.listAllMetadata() 返回 N 条,category 全为 "trigger"</li>
|
||||
* <li>ConditionEvaluatorManager.listAllMetadata() 返回 N 条,category 全为 "condition"</li>
|
||||
* <li>ActionProviderManager.listAllMetadata() 返回 N 条,category 全为 "action"</li>
|
||||
* <li>各具体 Provider 的 getMetadata() label / icon 非空</li>
|
||||
* <li>ProviderMetadataVO.emptySchema() 返回包含 "rule" key 的 Map</li>
|
||||
* </ol>
|
||||
*/
|
||||
class ProviderMetadataTest {
|
||||
|
||||
private TriggerProviderManager triggerProviderManager;
|
||||
private ConditionEvaluatorManager conditionEvaluatorManager;
|
||||
private ActionProviderManager actionProviderManager;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
// ---- Trigger providers ----
|
||||
triggerProviderManager = new TriggerProviderManager();
|
||||
List<TriggerProvider> triggers = List.of(
|
||||
new DevicePropertyTriggerProvider(),
|
||||
new DeviceEventTriggerProvider(),
|
||||
new DeviceStateTriggerProvider(),
|
||||
new DeviceServiceTriggerProvider()
|
||||
// TimerTriggerProvider requires TaskScheduler, excluded from unit test
|
||||
);
|
||||
triggerProviderManager.autoRegister(triggers);
|
||||
|
||||
// ---- Condition evaluators — use anonymous stubs to avoid infra deps ----
|
||||
List<ConditionEvaluator> conditions = List.of(
|
||||
new TimeRangeConditionEvaluator(),
|
||||
stubCondition("expression", "表达式条件", "mdi:code-braces"),
|
||||
stubCondition("device_state", "设备在线状态", "mdi:wifi")
|
||||
);
|
||||
conditionEvaluatorManager = new ConditionEvaluatorManager(conditions);
|
||||
|
||||
// ---- Action providers — use anonymous stubs to avoid infra deps ----
|
||||
actionProviderManager = new ActionProviderManager();
|
||||
List<ActionProvider> actions = List.of(
|
||||
stubAction("alarm_trigger", "触发告警", "mdi:bell-outline"),
|
||||
stubAction("alarm_clear", "清除告警", "mdi:bell-off-outline"),
|
||||
stubAction("notify", "发送通知", "mdi:message-text-outline"),
|
||||
stubAction("device_property_set", "设备属性设置", "mdi:tune-vertical"),
|
||||
stubAction("device_service_invoke", "设备服务调用", "mdi:cog-play")
|
||||
);
|
||||
actionProviderManager.autoRegister(actions);
|
||||
}
|
||||
|
||||
/** Create a minimal ConditionEvaluator stub with given type/label/icon metadata. */
|
||||
private static ConditionEvaluator stubCondition(String type, String label, String icon) {
|
||||
return new ConditionEvaluator() {
|
||||
@Override public String getType() { return type; }
|
||||
@Override public boolean evaluate(RuleContext ctx, JsonNode config) { return false; }
|
||||
@Override public ProviderMetadataVO getMetadata() {
|
||||
return ProviderMetadataVO.builder()
|
||||
.type(type).category("condition").label(label).icon(icon)
|
||||
.schema(ProviderMetadataVO.emptySchema()).build();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/** Create a minimal ActionProvider stub with given type/label/icon metadata. */
|
||||
private static ActionProvider stubAction(String type, String label, String icon) {
|
||||
return new ActionProvider() {
|
||||
@Override public String getType() { return type; }
|
||||
@Override public ActionResult executeAction(RuleContext ctx, JsonNode config) {
|
||||
return ActionResult.success();
|
||||
}
|
||||
@Override public NodeResult execute(RuleContext ctx, String config) {
|
||||
return NodeResult.of(com.viewsh.module.iot.rule.dal.dataobject.enums.RuleLinkRelationType.SUCCESS, java.util.Map.of());
|
||||
}
|
||||
@Override public RuleNodeCategory getCategory() { return RuleNodeCategory.ACTION; }
|
||||
@Override public ProviderMetadataVO getMetadata() {
|
||||
return ProviderMetadataVO.builder()
|
||||
.type(type).category("action").label(label).icon(icon)
|
||||
.schema(ProviderMetadataVO.emptySchema()).build();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// ======================== ProviderMetadataVO.emptySchema ========================
|
||||
|
||||
@Test
|
||||
void emptySchema_containsRuleKey() {
|
||||
var schema = ProviderMetadataVO.emptySchema();
|
||||
assertNotNull(schema);
|
||||
assertTrue(schema.containsKey("rule"), "schema must contain 'rule' key");
|
||||
}
|
||||
|
||||
// ======================== Trigger metadata ========================
|
||||
|
||||
@Test
|
||||
void triggerManager_listAllMetadata_returnsTriggerCategory() {
|
||||
List<ProviderMetadataVO> list = triggerProviderManager.listAllMetadata();
|
||||
assertFalse(list.isEmpty(), "Should return at least one trigger metadata");
|
||||
for (ProviderMetadataVO vo : list) {
|
||||
assertEquals("trigger", vo.getCategory(), "All trigger metadata must have category=trigger");
|
||||
assertNotNull(vo.getType(), "type must not be null");
|
||||
assertNotNull(vo.getLabel(), "label must not be null");
|
||||
assertNotNull(vo.getSchema(), "schema must not be null");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void devicePropertyTrigger_metadata_hasExpectedValues() {
|
||||
ProviderMetadataVO meta = new DevicePropertyTriggerProvider().getMetadata();
|
||||
assertEquals("device_property", meta.getType());
|
||||
assertEquals("trigger", meta.getCategory());
|
||||
assertNotNull(meta.getLabel());
|
||||
assertFalse(meta.getLabel().isBlank());
|
||||
assertNotNull(meta.getIcon());
|
||||
assertFalse(meta.getIcon().isBlank());
|
||||
}
|
||||
|
||||
@Test
|
||||
void timerTrigger_metadata_hasCorrectType() {
|
||||
// Only test metadata via default interface method pattern, no scheduler needed
|
||||
TriggerProvider stubTimer = new TriggerProvider() {
|
||||
@Override
|
||||
public String getType() { return "timer"; }
|
||||
@Override
|
||||
public boolean matches(com.viewsh.module.iot.core.mq.message.IotDeviceMessage msg,
|
||||
com.fasterxml.jackson.databind.JsonNode config,
|
||||
com.viewsh.module.iot.rule.engine.RuleContext ctx) { return false; }
|
||||
};
|
||||
ProviderMetadataVO meta = stubTimer.getMetadata();
|
||||
assertEquals("timer", meta.getType());
|
||||
assertEquals("trigger", meta.getCategory());
|
||||
}
|
||||
|
||||
// ======================== Condition metadata ========================
|
||||
|
||||
@Test
|
||||
void conditionManager_listAllMetadata_returnsConditionCategory() {
|
||||
List<ProviderMetadataVO> list = conditionEvaluatorManager.listAllMetadata();
|
||||
assertFalse(list.isEmpty(), "Should return at least one condition metadata");
|
||||
for (ProviderMetadataVO vo : list) {
|
||||
assertEquals("condition", vo.getCategory(), "All condition metadata must have category=condition");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void timeRangeCondition_metadata_hasExpectedValues() {
|
||||
ProviderMetadataVO meta = new TimeRangeConditionEvaluator().getMetadata();
|
||||
assertEquals("time_range", meta.getType());
|
||||
assertEquals("condition", meta.getCategory());
|
||||
assertNotNull(meta.getLabel());
|
||||
assertFalse(meta.getLabel().isBlank());
|
||||
}
|
||||
|
||||
// ======================== Action metadata ========================
|
||||
|
||||
@Test
|
||||
void actionManager_listAllMetadata_returnsActionCategory() {
|
||||
List<ProviderMetadataVO> list = actionProviderManager.listAllMetadata();
|
||||
assertFalse(list.isEmpty(), "Should return at least one action metadata");
|
||||
for (ProviderMetadataVO vo : list) {
|
||||
assertEquals("action", vo.getCategory(), "All action metadata must have category=action");
|
||||
assertNotNull(vo.getType(), "type must not be null");
|
||||
assertNotNull(vo.getLabel(), "label must not be null");
|
||||
assertFalse(vo.getLabel().isBlank(), "label must not be blank");
|
||||
assertNotNull(vo.getSchema(), "schema must not be null");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void alarmTriggerAction_typeConstant_isConsistent() {
|
||||
// Verify TYPE constant aligns with expected contract
|
||||
assertEquals("alarm_trigger", AlarmTriggerAction.TYPE);
|
||||
}
|
||||
|
||||
// ======================== Aggregated list coverage ========================
|
||||
|
||||
@Test
|
||||
void allTriggerTypes_arePresent() {
|
||||
List<ProviderMetadataVO> list = triggerProviderManager.listAllMetadata();
|
||||
Set<String> types = list.stream().map(ProviderMetadataVO::getType).collect(Collectors.toSet());
|
||||
assertTrue(types.contains("device_property"));
|
||||
assertTrue(types.contains("device_event"));
|
||||
assertTrue(types.contains("device_state"));
|
||||
assertTrue(types.contains("device_service"));
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,10 @@ import org.junit.jupiter.api.Test;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
|
||||
import org.mockito.ArgumentCaptor;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
@@ -269,6 +272,161 @@ class IotRuleChainServiceImplTest extends BaseMockitoUnitTest {
|
||||
verify(ruleLinkMapper, times(1)).deleteByChainId(chainId);
|
||||
}
|
||||
|
||||
// ========== 用例 9:disableRuleChain 幂等(已禁用不更新)==========
|
||||
|
||||
@Test
|
||||
void testDisable_idempotent() {
|
||||
Long chainId = 500L;
|
||||
IotRuleChainDO chain = IotRuleChainDO.builder()
|
||||
.id(chainId)
|
||||
.name("DisabledChain")
|
||||
.type("SCENE")
|
||||
.status(RuleChainStatus.DISABLED.getValue())
|
||||
.version(0L)
|
||||
.tenantId(1L)
|
||||
.build();
|
||||
|
||||
when(ruleChainMapper.selectById(chainId)).thenReturn(chain);
|
||||
|
||||
ruleChainService.disableRuleChain(chainId);
|
||||
|
||||
// 已禁用,不应调用 updateById
|
||||
verify(ruleChainMapper, never()).updateById(any(IotRuleChainDO.class));
|
||||
}
|
||||
|
||||
// ========== 用例 10:disableRuleChain 正常禁用(从 enabled 到 disabled)==========
|
||||
|
||||
@Test
|
||||
void testDisable_fromEnabled() {
|
||||
Long chainId = 501L;
|
||||
IotRuleChainDO chain = IotRuleChainDO.builder()
|
||||
.id(chainId)
|
||||
.name("EnabledChain")
|
||||
.type("SCENE")
|
||||
.status(RuleChainStatus.ENABLED.getValue())
|
||||
.version(1L)
|
||||
.tenantId(1L)
|
||||
.build();
|
||||
|
||||
when(ruleChainMapper.selectById(chainId)).thenReturn(chain);
|
||||
when(ruleChainMapper.updateById(any(IotRuleChainDO.class))).thenReturn(1);
|
||||
|
||||
ruleChainService.disableRuleChain(chainId);
|
||||
|
||||
ArgumentCaptor<IotRuleChainDO> disableCaptor = ArgumentCaptor.forClass(IotRuleChainDO.class);
|
||||
verify(ruleChainMapper, times(1)).updateById(disableCaptor.capture());
|
||||
assertEquals(RuleChainStatus.DISABLED.getValue(), disableCaptor.getValue().getStatus());
|
||||
}
|
||||
|
||||
// ========== 用例 11:toggleDebug 开启调试模式 ==========
|
||||
|
||||
@Test
|
||||
void testToggleDebug_enable() {
|
||||
Long chainId = 600L;
|
||||
IotRuleChainDO chain = IotRuleChainDO.builder()
|
||||
.id(chainId)
|
||||
.name("DebugChain")
|
||||
.type("SCENE")
|
||||
.status(RuleChainStatus.ENABLED.getValue())
|
||||
.version(0L)
|
||||
.debugMode(false)
|
||||
.tenantId(1L)
|
||||
.build();
|
||||
|
||||
when(ruleChainMapper.selectById(chainId)).thenReturn(chain);
|
||||
when(ruleChainMapper.updateById(any(IotRuleChainDO.class))).thenReturn(1);
|
||||
|
||||
ruleChainService.toggleDebug(chainId, true);
|
||||
|
||||
ArgumentCaptor<IotRuleChainDO> debugCaptor = ArgumentCaptor.forClass(IotRuleChainDO.class);
|
||||
verify(ruleChainMapper, times(1)).updateById(debugCaptor.capture());
|
||||
assertTrue(debugCaptor.getValue().getDebugMode());
|
||||
}
|
||||
|
||||
// ========== 用例 12:copyRuleChain 深拷节点和连线 ==========
|
||||
|
||||
@Test
|
||||
void testCopy_deepCopy() {
|
||||
Long originalId = 700L;
|
||||
Long newId = 701L;
|
||||
IotRuleChainDO original = IotRuleChainDO.builder()
|
||||
.id(originalId)
|
||||
.name("OriginalChain")
|
||||
.type("SCENE")
|
||||
.status(RuleChainStatus.ENABLED.getValue())
|
||||
.version(2L)
|
||||
.priority(100)
|
||||
.debugMode(false)
|
||||
.tenantId(1L)
|
||||
.build();
|
||||
|
||||
IotRuleNodeDO node1 = IotRuleNodeDO.builder()
|
||||
.id(10L).ruleChainId(originalId).category("trigger").type("device_event").configuration("{}").tenantId(1L).build();
|
||||
IotRuleNodeDO node2 = IotRuleNodeDO.builder()
|
||||
.id(11L).ruleChainId(originalId).category("action").type("send_notification").configuration("{}").tenantId(1L).build();
|
||||
|
||||
IotRuleLinkDO link1 = IotRuleLinkDO.builder()
|
||||
.id(20L).ruleChainId(originalId).sourceNodeId(10L).targetNodeId(11L).relationType("Success").sortOrder(0).tenantId(1L).build();
|
||||
|
||||
when(ruleChainMapper.selectById(originalId)).thenReturn(original);
|
||||
when(ruleChainMapper.insert(any(IotRuleChainDO.class))).thenAnswer(inv -> {
|
||||
IotRuleChainDO c = inv.getArgument(0);
|
||||
c.setId(newId);
|
||||
c.setTenantId(1L);
|
||||
return 1;
|
||||
});
|
||||
when(ruleNodeMapper.selectByChainId(originalId)).thenReturn(Arrays.asList(node1, node2));
|
||||
when(ruleNodeMapper.insertBatch(anyCollection())).thenAnswer(inv -> {
|
||||
// 模拟 MyBatis Plus saveBatch 回填 ID
|
||||
Collection<IotRuleNodeDO> nodes = inv.getArgument(0);
|
||||
long idCounter = 100L;
|
||||
for (IotRuleNodeDO n : nodes) {
|
||||
n.setId(idCounter++);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
when(ruleLinkMapper.selectByChainId(originalId)).thenReturn(Collections.singletonList(link1));
|
||||
when(ruleLinkMapper.insertBatch(anyCollection())).thenReturn(true);
|
||||
|
||||
Long result = ruleChainService.copyRuleChain(originalId);
|
||||
|
||||
assertEquals(newId, result);
|
||||
// 验证新 chain 名称追加了 _copy,状态为 DISABLED,版本 0
|
||||
ArgumentCaptor<IotRuleChainDO> copyCaptor = ArgumentCaptor.forClass(IotRuleChainDO.class);
|
||||
verify(ruleChainMapper, times(1)).insert(copyCaptor.capture());
|
||||
assertEquals("OriginalChain_copy", copyCaptor.getValue().getName());
|
||||
assertEquals(RuleChainStatus.DISABLED.getValue(), copyCaptor.getValue().getStatus());
|
||||
assertEquals(Long.valueOf(0L), copyCaptor.getValue().getVersion());
|
||||
verify(ruleNodeMapper, times(1)).insertBatch(anyCollection());
|
||||
verify(ruleLinkMapper, times(1)).insertBatch(anyCollection());
|
||||
}
|
||||
|
||||
// ========== 用例 13:deployRuleChain 启用 + 无 Redis 不抛异常 ==========
|
||||
|
||||
@Test
|
||||
void testDeploy_enablesAndSkipsRedisWhenNull() {
|
||||
Long chainId = 800L;
|
||||
IotRuleChainDO chain = IotRuleChainDO.builder()
|
||||
.id(chainId)
|
||||
.name("DeployChain")
|
||||
.type("SCENE")
|
||||
.status(RuleChainStatus.DISABLED.getValue())
|
||||
.version(0L)
|
||||
.tenantId(1L)
|
||||
.build();
|
||||
|
||||
when(ruleChainMapper.selectById(chainId)).thenReturn(chain);
|
||||
when(ruleChainMapper.updateById(any(IotRuleChainDO.class))).thenReturn(1);
|
||||
|
||||
// redisTemplate 为 null(@Autowired required=false,mock 环境未注入)
|
||||
assertDoesNotThrow(() -> ruleChainService.deployRuleChain(chainId));
|
||||
|
||||
// 验证状态从 DISABLED 变为 ENABLED
|
||||
ArgumentCaptor<IotRuleChainDO> deployCaptor = ArgumentCaptor.forClass(IotRuleChainDO.class);
|
||||
verify(ruleChainMapper, times(1)).updateById(deployCaptor.capture());
|
||||
assertEquals(RuleChainStatus.ENABLED.getValue(), deployCaptor.getValue().getStatus());
|
||||
}
|
||||
|
||||
// ========== 构建辅助方法 ==========
|
||||
|
||||
private IotRuleChainSaveReqVO buildCreateReqVO(IotRuleChainSaveReqVO.NodeVO... nodes) {
|
||||
|
||||
Reference in New Issue
Block a user