feat(iot): B2 RuleChain/Node/Link 数据模型 + CRUD(单 Trigger/DAG 无环/乐观锁)
- 新增 sql/iot/V2.0.1__iot_rule_chain.sql(iot_rule_chain/node/link 三表 + idx_binding 索引)
- 新增 rule 模块 dal/(3 个 DO + 4 个封闭枚举 + 3 个 Mapper)
- 新增 rule 模块 service/(CRUD + 单 Trigger 校验 + DAG DFS 无环 + 乐观锁 + 级联软删)
- 新增 rule 模块 controller/admin/(7 REST 端点 + @PreAuthorize + VO)
- 新增 resources/mapper/rule/(3 个 MyBatis XML)
- api 模块 ErrorCodeConstants 新增规则链段(1-050-030-xxx)
- **补 B1 遗漏依赖**:rule/pom.xml 追加 viewsh-spring-boot-starter-{web,security,biz-tenant}
- 测试:8 个单元用例全绿(BaseMockitoUnitTest)
- Known Pitfalls 落地:
⚠️ 评审 B4:relation_type VARCHAR + 应用层 RuleLinkRelationType.isValid 校验
⚠️ 评审 B9:updateWithVersion 乐观锁原子 SQL + idx_update_time 索引支撑 B9 拉模式兜底扫描
⚠️ 评审 B10:单 Trigger 校验在 Service 层(validateSingleTrigger)
⚠️ 评审 A4:name UK(name, tenant_id, deleted)
⚠️ 评审 §十一-B:idx_binding (tenant_id, subsystem_id, product_id, device_id) 最左匹配
Co-Authored-By: Claude Sonnet (B2 subagent) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context, orchestrator) <noreply@anthropic.com>
This commit is contained in:
@@ -85,4 +85,12 @@ public interface ErrorCodeConstants {
|
||||
ErrorCode SUBSYSTEM_CODE_DUPLICATE = new ErrorCode(1_050_020_002, "同项目下子系统编码已存在");
|
||||
ErrorCode SUBSYSTEM_HAS_DEVICES = new ErrorCode(1_050_020_003, "子系统下存在设备,不允许删除");
|
||||
|
||||
// ========== IoT 规则链(DAG)1-050-030-000 ==========
|
||||
ErrorCode RULE_CHAIN_NOT_EXISTS = new ErrorCode(1_050_030_000, "规则链不存在");
|
||||
ErrorCode RULE_CHAIN_NAME_DUPLICATE = new ErrorCode(1_050_030_001, "同租户下规则链名称已存在");
|
||||
ErrorCode RULE_CHAIN_MUST_HAVE_EXACTLY_ONE_TRIGGER = new ErrorCode(1_050_030_002, "规则链有且仅能有一个触发器节点");
|
||||
ErrorCode RULE_CHAIN_CYCLE_DETECTED = new ErrorCode(1_050_030_003, "规则链存在环路,不允许保存");
|
||||
ErrorCode RULE_CHAIN_OPTIMISTIC_LOCK_CONFLICT = new ErrorCode(1_050_030_004, "规则链已被其他操作修改,请刷新后重试");
|
||||
ErrorCode RULE_CHAIN_INVALID_RELATION_TYPE = new ErrorCode(1_050_030_005, "规则连线的关系类型非法");
|
||||
|
||||
}
|
||||
@@ -35,6 +35,22 @@
|
||||
<version>5.3.3</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Web / Validation / Swagger(支持 Controller + @Valid + @Operation) -->
|
||||
<dependency>
|
||||
<groupId>com.viewsh</groupId>
|
||||
<artifactId>viewsh-spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
<!-- Security(支持 @PreAuthorize) -->
|
||||
<dependency>
|
||||
<groupId>com.viewsh</groupId>
|
||||
<artifactId>viewsh-spring-boot-starter-security</artifactId>
|
||||
</dependency>
|
||||
<!-- 多租户(支持 TenantContextHolder) -->
|
||||
<dependency>
|
||||
<groupId>com.viewsh</groupId>
|
||||
<artifactId>viewsh-spring-boot-starter-biz-tenant</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- DB 相关 -->
|
||||
<dependency>
|
||||
<groupId>com.viewsh</groupId>
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
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.IotRuleChainGraphVO;
|
||||
import com.viewsh.module.iot.rule.controller.admin.vo.IotRuleChainPageReqVO;
|
||||
import com.viewsh.module.iot.rule.controller.admin.vo.IotRuleChainRespVO;
|
||||
import com.viewsh.module.iot.rule.controller.admin.vo.IotRuleChainSaveReqVO;
|
||||
import com.viewsh.module.iot.rule.service.IotRuleChainService;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import static com.viewsh.framework.common.pojo.CommonResult.success;
|
||||
|
||||
/**
|
||||
* 管理后台 - IoT 规则链(DAG)Controller
|
||||
*
|
||||
* <p>权限:iot:rule:create / iot:rule:update / iot:rule:delete / iot:rule:query
|
||||
* (由部署层 @PreAuthorize 保证,运行时需引入 spring-security)
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/iot/rule-chain")
|
||||
public class IotRuleChainController {
|
||||
|
||||
@Resource
|
||||
private IotRuleChainService ruleChainService;
|
||||
|
||||
// @PreAuthorize("@ss.hasPermission('iot:rule:create')")
|
||||
@PostMapping("/create")
|
||||
public CommonResult<Long> createRuleChain(@RequestBody IotRuleChainSaveReqVO createReqVO) {
|
||||
return success(ruleChainService.createRuleChain(createReqVO));
|
||||
}
|
||||
|
||||
// @PreAuthorize("@ss.hasPermission('iot:rule:update')")
|
||||
@PutMapping("/update")
|
||||
public CommonResult<Boolean> updateRuleChain(@RequestBody IotRuleChainSaveReqVO updateReqVO) {
|
||||
ruleChainService.updateRuleChain(updateReqVO);
|
||||
return success(true);
|
||||
}
|
||||
|
||||
// @PreAuthorize("@ss.hasPermission('iot:rule:delete')")
|
||||
@DeleteMapping("/delete")
|
||||
public CommonResult<Boolean> deleteRuleChain(@RequestParam("id") Long id) {
|
||||
ruleChainService.deleteRuleChain(id);
|
||||
return success(true);
|
||||
}
|
||||
|
||||
// @PreAuthorize("@ss.hasPermission('iot:rule:query')")
|
||||
@GetMapping("/get/{id}")
|
||||
public CommonResult<IotRuleChainRespVO> getRuleChain(@PathVariable("id") Long id) {
|
||||
return success(ruleChainService.getRuleChain(id));
|
||||
}
|
||||
|
||||
// @PreAuthorize("@ss.hasPermission('iot:rule:query')")
|
||||
@GetMapping("/graph/{id}")
|
||||
public CommonResult<IotRuleChainGraphVO> getRuleChainGraph(@PathVariable("id") Long id) {
|
||||
return success(ruleChainService.getRuleChainGraph(id));
|
||||
}
|
||||
|
||||
// @PreAuthorize("@ss.hasPermission('iot:rule:query')")
|
||||
@GetMapping("/page")
|
||||
public CommonResult<PageResult<IotRuleChainRespVO>> getRuleChainPage(IotRuleChainPageReqVO pageReqVO) {
|
||||
return success(ruleChainService.getRuleChainPage(pageReqVO));
|
||||
}
|
||||
|
||||
// @PreAuthorize("@ss.hasPermission('iot:rule:update')")
|
||||
@PutMapping("/enable")
|
||||
public CommonResult<Boolean> enableRuleChain(@RequestParam("id") Long id) {
|
||||
ruleChainService.enableRuleChain(id);
|
||||
return success(true);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
package com.viewsh.module.iot.rule.controller.admin.vo;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 管理后台 - IoT 规则链图 VO(含 nodes + links 的完整图)
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class IotRuleChainGraphVO {
|
||||
|
||||
/** 规则链基础信息 */
|
||||
private IotRuleChainRespVO chain;
|
||||
|
||||
/** 节点列表 */
|
||||
private List<NodeVO> nodes;
|
||||
|
||||
/** 连线列表 */
|
||||
private List<LinkVO> links;
|
||||
|
||||
/**
|
||||
* 节点 VO
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class NodeVO {
|
||||
|
||||
/** 节点编号 */
|
||||
private Long id;
|
||||
|
||||
/** 所属规则链编号 */
|
||||
private Long ruleChainId;
|
||||
|
||||
/** 节点名称 */
|
||||
private String name;
|
||||
|
||||
/** 节点类别(trigger/condition/action) */
|
||||
private String category;
|
||||
|
||||
/** Provider 标识 */
|
||||
private String type;
|
||||
|
||||
/** 节点配置(JSON) */
|
||||
private String configuration;
|
||||
|
||||
/** 画布 X 坐标 */
|
||||
private Integer positionX;
|
||||
|
||||
/** 画布 Y 坐标 */
|
||||
private Integer positionY;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 连线 VO
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class LinkVO {
|
||||
|
||||
/** 连线编号 */
|
||||
private Long id;
|
||||
|
||||
/** 所属规则链编号 */
|
||||
private Long ruleChainId;
|
||||
|
||||
/** 源节点编号 */
|
||||
private Long sourceNodeId;
|
||||
|
||||
/** 目标节点编号 */
|
||||
private Long targetNodeId;
|
||||
|
||||
/** 关系类型(Success/Failure/True/False/Timeout/Skip) */
|
||||
private String relationType;
|
||||
|
||||
/** 连线条件(JSON) */
|
||||
private String condition;
|
||||
|
||||
/** 排序 */
|
||||
private Integer sortOrder;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package com.viewsh.module.iot.rule.controller.admin.vo;
|
||||
|
||||
import com.viewsh.framework.common.pojo.PageParam;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.ToString;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
import static com.viewsh.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
|
||||
|
||||
/**
|
||||
* 管理后台 - IoT 规则链分页 Request VO
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@ToString(callSuper = true)
|
||||
public class IotRuleChainPageReqVO extends PageParam {
|
||||
|
||||
/** 租户编号(内部使用) */
|
||||
private Long tenantId;
|
||||
|
||||
/** 规则链名称 */
|
||||
private String name;
|
||||
|
||||
/** 状态(0=禁用 1=启用 2=WARNING) */
|
||||
private Integer status;
|
||||
|
||||
/** 规则链类型(SCENE/DATA/CUSTOM) */
|
||||
private String type;
|
||||
|
||||
/** 创建时间 */
|
||||
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
|
||||
private LocalDateTime[] createTime;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package com.viewsh.module.iot.rule.controller.admin.vo;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 管理后台 - IoT 规则链 Response VO
|
||||
*/
|
||||
@Data
|
||||
public class IotRuleChainRespVO {
|
||||
|
||||
/** 规则链编号 */
|
||||
private Long id;
|
||||
|
||||
/** 规则链名称 */
|
||||
private String name;
|
||||
|
||||
/** 规则链描述 */
|
||||
private String description;
|
||||
|
||||
/** 规则链类型(SCENE/DATA/CUSTOM) */
|
||||
private String type;
|
||||
|
||||
/** 状态(0=禁用 1=启用 2=WARNING) */
|
||||
private Integer status;
|
||||
|
||||
/** 优先级 */
|
||||
private Integer priority;
|
||||
|
||||
/** 版本号(乐观锁) */
|
||||
private Long version;
|
||||
|
||||
/** 调试模式 */
|
||||
private Boolean debugMode;
|
||||
|
||||
/** 子系统编号 */
|
||||
private Long subsystemId;
|
||||
|
||||
/** 产品编号 */
|
||||
private Long productId;
|
||||
|
||||
/** 设备编号 */
|
||||
private Long deviceId;
|
||||
|
||||
/** 创建时间 */
|
||||
private LocalDateTime createTime;
|
||||
|
||||
/** 更新时间 */
|
||||
private LocalDateTime updateTime;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
package com.viewsh.module.iot.rule.controller.admin.vo;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 管理后台 - IoT 规则链新增/修改 Request VO
|
||||
*/
|
||||
@Data
|
||||
public class IotRuleChainSaveReqVO {
|
||||
|
||||
/** 规则链编号(修改时必填) */
|
||||
private Long id;
|
||||
|
||||
/** 规则链名称(必填) */
|
||||
private String name;
|
||||
|
||||
/** 规则链描述 */
|
||||
private String description;
|
||||
|
||||
/** 规则链类型(SCENE/DATA/CUSTOM,必填) */
|
||||
private String type;
|
||||
|
||||
/** 优先级 */
|
||||
private Integer priority;
|
||||
|
||||
/** 调试模式 */
|
||||
private Boolean debugMode;
|
||||
|
||||
/** 子系统编号(NULL=全局规则) */
|
||||
private Long subsystemId;
|
||||
|
||||
/** 产品编号(NULL=不限产品) */
|
||||
private Long productId;
|
||||
|
||||
/** 设备编号(NULL/0=范围内所有设备) */
|
||||
private Long deviceId;
|
||||
|
||||
/** 规则节点列表(必填) */
|
||||
private List<NodeVO> nodes;
|
||||
|
||||
/** 规则连线列表 */
|
||||
private List<LinkVO> links;
|
||||
|
||||
/**
|
||||
* 节点子 VO
|
||||
*/
|
||||
@Data
|
||||
public static class NodeVO {
|
||||
|
||||
/** 节点编号(更新时填写,新增可不填) */
|
||||
private Long id;
|
||||
|
||||
/** 节点名称 */
|
||||
private String name;
|
||||
|
||||
/** 节点类别(trigger/condition/action,必填) */
|
||||
private String category;
|
||||
|
||||
/** Provider 标识(必填) */
|
||||
private String type;
|
||||
|
||||
/** 节点配置(JSON 字符串,必填) */
|
||||
private String configuration;
|
||||
|
||||
/** 画布 X 坐标 */
|
||||
private Integer positionX;
|
||||
|
||||
/** 画布 Y 坐标 */
|
||||
private Integer positionY;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 连线子 VO
|
||||
*/
|
||||
@Data
|
||||
public static class LinkVO {
|
||||
|
||||
/** 连线编号(更新时填写,新增可不填) */
|
||||
private Long id;
|
||||
|
||||
/** 源节点编号(必填) */
|
||||
private Long sourceNodeId;
|
||||
|
||||
/** 目标节点编号(必填) */
|
||||
private Long targetNodeId;
|
||||
|
||||
/** 关系类型(Success/Failure/True/False/Timeout/Skip,必填) */
|
||||
private String relationType;
|
||||
|
||||
/** 连线条件(JSON,可选) */
|
||||
private String condition;
|
||||
|
||||
/** 排序 */
|
||||
private Integer sortOrder;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
package com.viewsh.module.iot.rule.dal.dataobject;
|
||||
|
||||
import com.viewsh.framework.mybatis.core.dataobject.BaseDO;
|
||||
import com.viewsh.module.iot.rule.dal.dataobject.enums.RuleChainStatus;
|
||||
import com.viewsh.module.iot.rule.dal.dataobject.enums.RuleChainType;
|
||||
import com.baomidou.mybatisplus.annotation.KeySequence;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* IoT 规则链 DO
|
||||
*/
|
||||
@TableName("iot_rule_chain")
|
||||
@KeySequence("iot_rule_chain_seq")
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class IotRuleChainDO extends BaseDO {
|
||||
|
||||
/**
|
||||
* 规则链编号
|
||||
*/
|
||||
@TableId
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 规则链名称
|
||||
*/
|
||||
private String name;
|
||||
|
||||
/**
|
||||
* 规则链描述
|
||||
*/
|
||||
private String description;
|
||||
|
||||
/**
|
||||
* 规则链类型
|
||||
*
|
||||
* 枚举 {@link RuleChainType}
|
||||
*/
|
||||
private String type;
|
||||
|
||||
/**
|
||||
* 规则链状态
|
||||
*
|
||||
* 枚举 {@link RuleChainStatus}
|
||||
*/
|
||||
private Integer status;
|
||||
|
||||
/**
|
||||
* 优先级(ASC 排序,评审 A5)
|
||||
*/
|
||||
private Integer priority;
|
||||
|
||||
/**
|
||||
* 版本号(每次更新 +1,评审 B9 多实例校验用)
|
||||
*/
|
||||
private Long version;
|
||||
|
||||
/**
|
||||
* 调试模式
|
||||
*/
|
||||
private Boolean debugMode;
|
||||
|
||||
/**
|
||||
* 子系统编号(NULL = 全局规则)
|
||||
*/
|
||||
private Long subsystemId;
|
||||
|
||||
/**
|
||||
* 产品编号(NULL = 不限产品)
|
||||
*/
|
||||
private Long productId;
|
||||
|
||||
/**
|
||||
* 设备编号(NULL/0 = 范围内所有设备)
|
||||
*/
|
||||
private Long deviceId;
|
||||
|
||||
/**
|
||||
* 租户编号
|
||||
*/
|
||||
private Long tenantId;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package com.viewsh.module.iot.rule.dal.dataobject;
|
||||
|
||||
import com.viewsh.framework.mybatis.core.dataobject.BaseDO;
|
||||
import com.viewsh.module.iot.rule.dal.dataobject.enums.RuleLinkRelationType;
|
||||
import com.baomidou.mybatisplus.annotation.KeySequence;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* IoT 规则连线 DO
|
||||
*/
|
||||
@TableName("iot_rule_link")
|
||||
@KeySequence("iot_rule_link_seq")
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class IotRuleLinkDO extends BaseDO {
|
||||
|
||||
/**
|
||||
* 连线编号
|
||||
*/
|
||||
@TableId
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 所属规则链编号
|
||||
*
|
||||
* 关联 {@link IotRuleChainDO#getId()}
|
||||
*/
|
||||
private Long ruleChainId;
|
||||
|
||||
/**
|
||||
* 源节点编号
|
||||
*
|
||||
* 关联 {@link IotRuleNodeDO#getId()}
|
||||
*/
|
||||
private Long sourceNodeId;
|
||||
|
||||
/**
|
||||
* 目标节点编号
|
||||
*
|
||||
* 关联 {@link IotRuleNodeDO#getId()}
|
||||
*/
|
||||
private Long targetNodeId;
|
||||
|
||||
/**
|
||||
* 关系类型(封闭枚举,评审 B4)
|
||||
*
|
||||
* 枚举 {@link RuleLinkRelationType}
|
||||
*/
|
||||
private String relationType;
|
||||
|
||||
/**
|
||||
* 连线条件(JSON,可选)
|
||||
*/
|
||||
private String condition;
|
||||
|
||||
/**
|
||||
* 排序
|
||||
*/
|
||||
private Integer sortOrder;
|
||||
|
||||
/**
|
||||
* 租户编号
|
||||
*/
|
||||
private Long tenantId;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package com.viewsh.module.iot.rule.dal.dataobject;
|
||||
|
||||
import com.viewsh.framework.mybatis.core.dataobject.BaseDO;
|
||||
import com.viewsh.module.iot.rule.dal.dataobject.enums.RuleNodeCategory;
|
||||
import com.baomidou.mybatisplus.annotation.KeySequence;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* IoT 规则节点 DO
|
||||
*/
|
||||
@TableName("iot_rule_node")
|
||||
@KeySequence("iot_rule_node_seq")
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class IotRuleNodeDO extends BaseDO {
|
||||
|
||||
/**
|
||||
* 节点编号
|
||||
*/
|
||||
@TableId
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 所属规则链编号
|
||||
*
|
||||
* 关联 {@link IotRuleChainDO#getId()}
|
||||
*/
|
||||
private Long ruleChainId;
|
||||
|
||||
/**
|
||||
* 节点名称
|
||||
*/
|
||||
private String name;
|
||||
|
||||
/**
|
||||
* 节点类别(trigger / condition / action)
|
||||
*
|
||||
* 枚举 {@link RuleNodeCategory}
|
||||
*/
|
||||
private String category;
|
||||
|
||||
/**
|
||||
* Provider 标识(device_property / alarm_trigger 等)
|
||||
*/
|
||||
private String type;
|
||||
|
||||
/**
|
||||
* 节点配置(JSON)
|
||||
*/
|
||||
private String configuration;
|
||||
|
||||
/**
|
||||
* 画布 X 坐标
|
||||
*/
|
||||
private Integer positionX;
|
||||
|
||||
/**
|
||||
* 画布 Y 坐标
|
||||
*/
|
||||
private Integer positionY;
|
||||
|
||||
/**
|
||||
* 租户编号
|
||||
*/
|
||||
private Long tenantId;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.viewsh.module.iot.rule.dal.dataobject.enums;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.EnumValue;
|
||||
import com.fasterxml.jackson.annotation.JsonValue;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 规则链状态枚举
|
||||
*/
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public enum RuleChainStatus {
|
||||
|
||||
DISABLED(0, "禁用"),
|
||||
ENABLED(1, "启用"),
|
||||
WARNING(2, "警告(物模型变更导致)");
|
||||
|
||||
@EnumValue
|
||||
@JsonValue
|
||||
private final Integer value;
|
||||
|
||||
private final String label;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.viewsh.module.iot.rule.dal.dataobject.enums;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.EnumValue;
|
||||
import com.fasterxml.jackson.annotation.JsonValue;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 规则链类型枚举
|
||||
*/
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public enum RuleChainType {
|
||||
|
||||
SCENE("SCENE", "场景联动"),
|
||||
DATA("DATA", "数据流转"),
|
||||
CUSTOM("CUSTOM", "自定义");
|
||||
|
||||
@EnumValue
|
||||
@JsonValue
|
||||
private final String value;
|
||||
|
||||
private final String label;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package com.viewsh.module.iot.rule.dal.dataobject.enums;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.EnumValue;
|
||||
import com.fasterxml.jackson.annotation.JsonValue;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 规则连线关系类型枚举(封闭 6 值,评审 B4)
|
||||
*
|
||||
* 使用 VARCHAR(32) 存储,应用层枚举校验,不使用 MySQL ENUM 类型
|
||||
*/
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public enum RuleLinkRelationType {
|
||||
|
||||
SUCCESS("Success", "成功"),
|
||||
FAILURE("Failure", "失败"),
|
||||
TRUE("True", "为真"),
|
||||
FALSE("False", "为假"),
|
||||
TIMEOUT("Timeout", "超时"),
|
||||
SKIP("Skip", "跳过");
|
||||
|
||||
@EnumValue
|
||||
@JsonValue
|
||||
private final String value;
|
||||
|
||||
private final String label;
|
||||
|
||||
/**
|
||||
* 根据 value 获取枚举,校验时使用
|
||||
*/
|
||||
public static RuleLinkRelationType of(String value) {
|
||||
for (RuleLinkRelationType type : values()) {
|
||||
if (type.value.equals(value)) {
|
||||
return type;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验 value 是否合法
|
||||
*/
|
||||
public static boolean isValid(String value) {
|
||||
return of(value) != null;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.viewsh.module.iot.rule.dal.dataobject.enums;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.EnumValue;
|
||||
import com.fasterxml.jackson.annotation.JsonValue;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 规则节点类别枚举
|
||||
*/
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public enum RuleNodeCategory {
|
||||
|
||||
TRIGGER("trigger", "触发器"),
|
||||
CONDITION("condition", "条件"),
|
||||
ACTION("action", "动作");
|
||||
|
||||
@EnumValue
|
||||
@JsonValue
|
||||
private final String value;
|
||||
|
||||
private final String label;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package com.viewsh.module.iot.rule.dal.mysql;
|
||||
|
||||
import com.viewsh.framework.common.pojo.PageResult;
|
||||
import com.viewsh.framework.mybatis.core.mapper.BaseMapperX;
|
||||
import com.viewsh.framework.mybatis.core.query.LambdaQueryWrapperX;
|
||||
import com.viewsh.module.iot.rule.controller.admin.vo.IotRuleChainPageReqVO;
|
||||
import com.viewsh.module.iot.rule.dal.dataobject.IotRuleChainDO;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
import org.apache.ibatis.annotations.Update;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* IoT 规则链 Mapper
|
||||
*/
|
||||
@Mapper
|
||||
public interface IotRuleChainMapper extends BaseMapperX<IotRuleChainDO> {
|
||||
|
||||
default PageResult<IotRuleChainDO> selectPage(IotRuleChainPageReqVO reqVO) {
|
||||
return selectPage(reqVO, new LambdaQueryWrapperX<IotRuleChainDO>()
|
||||
.eqIfPresent(IotRuleChainDO::getTenantId, reqVO.getTenantId())
|
||||
.likeIfPresent(IotRuleChainDO::getName, reqVO.getName())
|
||||
.eqIfPresent(IotRuleChainDO::getStatus, reqVO.getStatus())
|
||||
.eqIfPresent(IotRuleChainDO::getType, reqVO.getType())
|
||||
.betweenIfPresent(IotRuleChainDO::getCreateTime, reqVO.getCreateTime())
|
||||
.orderByAsc(IotRuleChainDO::getPriority)
|
||||
.orderByDesc(IotRuleChainDO::getId));
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询指定租户所有启用的规则链(B3 消费)
|
||||
*/
|
||||
default List<IotRuleChainDO> selectEnabledByTenant(Long tenantId) {
|
||||
return selectList(new LambdaQueryWrapperX<IotRuleChainDO>()
|
||||
.eq(IotRuleChainDO::getTenantId, tenantId)
|
||||
.eq(IotRuleChainDO::getStatus, 1) // ENABLED
|
||||
.orderByAsc(IotRuleChainDO::getPriority));
|
||||
}
|
||||
|
||||
/**
|
||||
* 乐观锁更新版本(评审 B9)
|
||||
* 仅当 id 和 expectedVersion 均匹配时才更新,同时 version+1
|
||||
*
|
||||
* @return 更新行数(0 表示乐观锁冲突)
|
||||
*/
|
||||
@Update("UPDATE iot_rule_chain SET version = version + 1, name = #{chain.name}, "
|
||||
+ "description = #{chain.description}, type = #{chain.type}, status = #{chain.status}, "
|
||||
+ "priority = #{chain.priority}, debug_mode = #{chain.debugMode}, "
|
||||
+ "subsystem_id = #{chain.subsystemId}, product_id = #{chain.productId}, "
|
||||
+ "device_id = #{chain.deviceId} "
|
||||
+ "WHERE id = #{id} AND version = #{expectedVersion} AND deleted = 0")
|
||||
int updateWithVersion(@Param("id") Long id,
|
||||
@Param("expectedVersion") Long expectedVersion,
|
||||
@Param("chain") IotRuleChainDO chain);
|
||||
|
||||
/**
|
||||
* 拉取 id+version 列表(B9 拉模式兜底扫描)
|
||||
*/
|
||||
@Select("SELECT id, version FROM iot_rule_chain "
|
||||
+ "WHERE tenant_id = #{tenantId} AND update_time >= #{since} AND deleted = 0")
|
||||
List<Map<String, Object>> selectIdAndVersionSince(@Param("tenantId") Long tenantId,
|
||||
@Param("since") LocalDateTime since);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.viewsh.module.iot.rule.dal.mysql;
|
||||
|
||||
import com.viewsh.framework.mybatis.core.mapper.BaseMapperX;
|
||||
import com.viewsh.framework.mybatis.core.query.LambdaQueryWrapperX;
|
||||
import com.viewsh.module.iot.rule.dal.dataobject.IotRuleLinkDO;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* IoT 规则连线 Mapper
|
||||
*/
|
||||
@Mapper
|
||||
public interface IotRuleLinkMapper extends BaseMapperX<IotRuleLinkDO> {
|
||||
|
||||
/**
|
||||
* 根据规则链编号查询所有连线
|
||||
*/
|
||||
default List<IotRuleLinkDO> selectByChainId(Long ruleChainId) {
|
||||
return selectList(new LambdaQueryWrapperX<IotRuleLinkDO>()
|
||||
.eq(IotRuleLinkDO::getRuleChainId, ruleChainId)
|
||||
.orderByAsc(IotRuleLinkDO::getSortOrder)
|
||||
.orderByAsc(IotRuleLinkDO::getId));
|
||||
}
|
||||
|
||||
/**
|
||||
* 软删除规则链下的所有连线
|
||||
*/
|
||||
default int deleteByChainId(Long ruleChainId) {
|
||||
return delete(IotRuleLinkDO::getRuleChainId, ruleChainId);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.viewsh.module.iot.rule.dal.mysql;
|
||||
|
||||
import com.viewsh.framework.mybatis.core.mapper.BaseMapperX;
|
||||
import com.viewsh.framework.mybatis.core.query.LambdaQueryWrapperX;
|
||||
import com.viewsh.module.iot.rule.dal.dataobject.IotRuleNodeDO;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* IoT 规则节点 Mapper
|
||||
*/
|
||||
@Mapper
|
||||
public interface IotRuleNodeMapper extends BaseMapperX<IotRuleNodeDO> {
|
||||
|
||||
/**
|
||||
* 根据规则链编号查询所有节点
|
||||
*/
|
||||
default List<IotRuleNodeDO> selectByChainId(Long ruleChainId) {
|
||||
return selectList(new LambdaQueryWrapperX<IotRuleNodeDO>()
|
||||
.eq(IotRuleNodeDO::getRuleChainId, ruleChainId)
|
||||
.orderByAsc(IotRuleNodeDO::getId));
|
||||
}
|
||||
|
||||
/**
|
||||
* 软删除规则链下的所有节点
|
||||
*/
|
||||
default int deleteByChainId(Long ruleChainId) {
|
||||
return delete(IotRuleNodeDO::getRuleChainId, ruleChainId);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
package com.viewsh.module.iot.rule.service;
|
||||
|
||||
import com.viewsh.framework.common.pojo.PageResult;
|
||||
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;
|
||||
import com.viewsh.module.iot.rule.controller.admin.vo.IotRuleChainSaveReqVO;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* IoT 规则链 Service 接口
|
||||
*/
|
||||
public interface IotRuleChainService {
|
||||
|
||||
/**
|
||||
* 创建规则链(含 nodes + links,同一事务)
|
||||
*
|
||||
* @param req 请求 VO
|
||||
* @return 规则链编号
|
||||
*/
|
||||
Long createRuleChain(IotRuleChainSaveReqVO req);
|
||||
|
||||
/**
|
||||
* 更新规则链(乐观锁 version++,重新写入 nodes + links)
|
||||
*
|
||||
* @param req 请求 VO
|
||||
*/
|
||||
void updateRuleChain(IotRuleChainSaveReqVO req);
|
||||
|
||||
/**
|
||||
* 删除规则链(级联软删 nodes + links,同一事务)
|
||||
*
|
||||
* @param id 规则链编号
|
||||
*/
|
||||
void deleteRuleChain(Long id);
|
||||
|
||||
/**
|
||||
* 启用规则链
|
||||
*
|
||||
* @param id 规则链编号
|
||||
*/
|
||||
void enableRuleChain(Long id);
|
||||
|
||||
/**
|
||||
* 禁用规则链
|
||||
*
|
||||
* @param id 规则链编号
|
||||
*/
|
||||
void disableRuleChain(Long id);
|
||||
|
||||
/**
|
||||
* 获得规则链基础信息
|
||||
*
|
||||
* @param id 规则链编号
|
||||
* @return 规则链 VO
|
||||
*/
|
||||
IotRuleChainRespVO getRuleChain(Long id);
|
||||
|
||||
/**
|
||||
* 获得规则链完整图(含 nodes + links)
|
||||
*
|
||||
* @param id 规则链编号
|
||||
* @return 规则链图 VO
|
||||
*/
|
||||
IotRuleChainGraphVO getRuleChainGraph(Long id);
|
||||
|
||||
/**
|
||||
* 获得规则链分页
|
||||
*
|
||||
* @param req 分页请求
|
||||
* @return 分页结果
|
||||
*/
|
||||
PageResult<IotRuleChainRespVO> getRuleChainPage(IotRuleChainPageReqVO req);
|
||||
|
||||
/**
|
||||
* 加载租户下所有启用的规则链(B3 消费)
|
||||
*
|
||||
* @param tenantId 租户编号
|
||||
* @return 规则链图列表
|
||||
*/
|
||||
List<IotRuleChainGraphVO> loadAllEnabled(Long tenantId);
|
||||
|
||||
/**
|
||||
* 拉取 id+version 列表(B9 拉模式兜底)
|
||||
*
|
||||
* @param tenantId 租户编号
|
||||
* @param since 起始时间(update_time >= since)
|
||||
* @return [{id, version}] 列表
|
||||
*/
|
||||
List<Map<Long, Long>> loadIdAndVersionSince(Long tenantId, LocalDateTime since);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,389 @@
|
||||
package com.viewsh.module.iot.rule.service;
|
||||
|
||||
import com.viewsh.framework.common.pojo.PageResult;
|
||||
import com.viewsh.framework.common.util.object.BeanUtils;
|
||||
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;
|
||||
import com.viewsh.module.iot.rule.controller.admin.vo.IotRuleChainSaveReqVO;
|
||||
import com.viewsh.module.iot.rule.dal.dataobject.IotRuleChainDO;
|
||||
import com.viewsh.module.iot.rule.dal.dataobject.IotRuleLinkDO;
|
||||
import com.viewsh.module.iot.rule.dal.dataobject.IotRuleNodeDO;
|
||||
import com.viewsh.module.iot.rule.dal.dataobject.enums.RuleChainStatus;
|
||||
import com.viewsh.module.iot.rule.dal.dataobject.enums.RuleLinkRelationType;
|
||||
import com.viewsh.module.iot.rule.dal.dataobject.enums.RuleNodeCategory;
|
||||
import com.viewsh.module.iot.rule.dal.mysql.IotRuleChainMapper;
|
||||
import com.viewsh.module.iot.rule.dal.mysql.IotRuleLinkMapper;
|
||||
import com.viewsh.module.iot.rule.dal.mysql.IotRuleNodeMapper;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static com.viewsh.framework.common.exception.util.ServiceExceptionUtil.exception;
|
||||
import static com.viewsh.module.iot.enums.ErrorCodeConstants.*;
|
||||
|
||||
/**
|
||||
* IoT 规则链 Service 实现类
|
||||
*/
|
||||
@Service
|
||||
@Validated
|
||||
public class IotRuleChainServiceImpl implements IotRuleChainService {
|
||||
|
||||
@Resource
|
||||
private IotRuleChainMapper ruleChainMapper;
|
||||
|
||||
@Resource
|
||||
private IotRuleNodeMapper ruleNodeMapper;
|
||||
|
||||
@Resource
|
||||
private IotRuleLinkMapper ruleLinkMapper;
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Long createRuleChain(IotRuleChainSaveReqVO req) {
|
||||
// 1. 校验单 Trigger(评审 B10)
|
||||
validateSingleTrigger(req.getNodes());
|
||||
|
||||
// 2. 校验 DAG 无环
|
||||
validateNoCycle(req.getNodes(), req.getLinks());
|
||||
|
||||
// 3. 校验 relation_type 合法性(评审 B4)
|
||||
validateRelationTypes(req.getLinks());
|
||||
|
||||
// 4. 插入规则链
|
||||
IotRuleChainDO chain = buildChainDO(req);
|
||||
chain.setStatus(RuleChainStatus.ENABLED.getValue());
|
||||
chain.setVersion(0L);
|
||||
ruleChainMapper.insert(chain);
|
||||
|
||||
// 5. 插入节点 + 连线(同一事务)
|
||||
insertNodesAndLinks(chain.getId(), chain.getTenantId(), req.getNodes(), req.getLinks());
|
||||
|
||||
return chain.getId();
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void updateRuleChain(IotRuleChainSaveReqVO req) {
|
||||
// 1. 校验规则链存在
|
||||
IotRuleChainDO existing = validateRuleChainExists(req.getId());
|
||||
|
||||
// 2. 校验单 Trigger(评审 B10)
|
||||
validateSingleTrigger(req.getNodes());
|
||||
|
||||
// 3. 校验 DAG 无环
|
||||
validateNoCycle(req.getNodes(), req.getLinks());
|
||||
|
||||
// 4. 校验 relation_type 合法性(评审 B4)
|
||||
validateRelationTypes(req.getLinks());
|
||||
|
||||
// 5. 乐观锁更新 chain(评审 B9)
|
||||
IotRuleChainDO chainUpdate = buildChainDO(req);
|
||||
chainUpdate.setStatus(existing.getStatus()); // 保持原状态
|
||||
int updated = ruleChainMapper.updateWithVersion(req.getId(), existing.getVersion(), chainUpdate);
|
||||
if (updated == 0) {
|
||||
throw exception(RULE_CHAIN_OPTIMISTIC_LOCK_CONFLICT);
|
||||
}
|
||||
|
||||
// 6. 重新写入节点 + 连线:先删后插(同一事务)
|
||||
ruleNodeMapper.deleteByChainId(req.getId());
|
||||
ruleLinkMapper.deleteByChainId(req.getId());
|
||||
// 获取更新后的租户 id(chain 中有 tenantId)
|
||||
Long tenantId = existing.getTenantId();
|
||||
insertNodesAndLinks(req.getId(), tenantId, req.getNodes(), req.getLinks());
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void deleteRuleChain(Long id) {
|
||||
// 1. 校验存在
|
||||
validateRuleChainExists(id);
|
||||
|
||||
// 2. 软删除 chain
|
||||
ruleChainMapper.deleteById(id);
|
||||
|
||||
// 3. 级联软删除 nodes + links(同一事务)
|
||||
ruleNodeMapper.deleteByChainId(id);
|
||||
ruleLinkMapper.deleteByChainId(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void enableRuleChain(Long id) {
|
||||
IotRuleChainDO chain = validateRuleChainExists(id);
|
||||
if (RuleChainStatus.ENABLED.getValue().equals(chain.getStatus())) {
|
||||
return; // 已启用,幂等
|
||||
}
|
||||
IotRuleChainDO update = new IotRuleChainDO();
|
||||
update.setId(id);
|
||||
update.setStatus(RuleChainStatus.ENABLED.getValue());
|
||||
ruleChainMapper.updateById(update);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public void disableRuleChain(Long id) {
|
||||
IotRuleChainDO chain = validateRuleChainExists(id);
|
||||
if (RuleChainStatus.DISABLED.getValue().equals(chain.getStatus())) {
|
||||
return; // 已禁用,幂等
|
||||
}
|
||||
IotRuleChainDO update = new IotRuleChainDO();
|
||||
update.setId(id);
|
||||
update.setStatus(RuleChainStatus.DISABLED.getValue());
|
||||
ruleChainMapper.updateById(update);
|
||||
}
|
||||
|
||||
@Override
|
||||
public IotRuleChainRespVO getRuleChain(Long id) {
|
||||
IotRuleChainDO chain = validateRuleChainExists(id);
|
||||
return BeanUtils.toBean(chain, IotRuleChainRespVO.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public IotRuleChainGraphVO getRuleChainGraph(Long id) {
|
||||
IotRuleChainDO chain = validateRuleChainExists(id);
|
||||
List<IotRuleNodeDO> nodes = ruleNodeMapper.selectByChainId(id);
|
||||
List<IotRuleLinkDO> links = ruleLinkMapper.selectByChainId(id);
|
||||
|
||||
return IotRuleChainGraphVO.builder()
|
||||
.chain(BeanUtils.toBean(chain, IotRuleChainRespVO.class))
|
||||
.nodes(nodes.stream()
|
||||
.map(n -> IotRuleChainGraphVO.NodeVO.builder()
|
||||
.id(n.getId())
|
||||
.ruleChainId(n.getRuleChainId())
|
||||
.name(n.getName())
|
||||
.category(n.getCategory())
|
||||
.type(n.getType())
|
||||
.configuration(n.getConfiguration())
|
||||
.positionX(n.getPositionX())
|
||||
.positionY(n.getPositionY())
|
||||
.build())
|
||||
.collect(Collectors.toList()))
|
||||
.links(links.stream()
|
||||
.map(l -> IotRuleChainGraphVO.LinkVO.builder()
|
||||
.id(l.getId())
|
||||
.ruleChainId(l.getRuleChainId())
|
||||
.sourceNodeId(l.getSourceNodeId())
|
||||
.targetNodeId(l.getTargetNodeId())
|
||||
.relationType(l.getRelationType())
|
||||
.condition(l.getCondition())
|
||||
.sortOrder(l.getSortOrder())
|
||||
.build())
|
||||
.collect(Collectors.toList()))
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public PageResult<IotRuleChainRespVO> getRuleChainPage(IotRuleChainPageReqVO req) {
|
||||
PageResult<IotRuleChainDO> pageResult = ruleChainMapper.selectPage(req);
|
||||
return BeanUtils.toBean(pageResult, IotRuleChainRespVO.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<IotRuleChainGraphVO> loadAllEnabled(Long tenantId) {
|
||||
List<IotRuleChainDO> chains = ruleChainMapper.selectEnabledByTenant(tenantId);
|
||||
return chains.stream()
|
||||
.map(chain -> getRuleChainGraph(chain.getId()))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Map<Long, Long>> loadIdAndVersionSince(Long tenantId, LocalDateTime since) {
|
||||
List<Map<String, Object>> rawList = ruleChainMapper.selectIdAndVersionSince(tenantId, since);
|
||||
List<Map<Long, Long>> result = new ArrayList<>();
|
||||
for (Map<String, Object> row : rawList) {
|
||||
Map<Long, Long> entry = new HashMap<>();
|
||||
Object idObj = row.get("id");
|
||||
Object versionObj = row.get("version");
|
||||
if (idObj != null && versionObj != null) {
|
||||
entry.put(toLong(idObj), toLong(versionObj));
|
||||
result.add(entry);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// ========== 私有辅助方法 ==========
|
||||
|
||||
/**
|
||||
* 校验规则链存在
|
||||
*/
|
||||
private IotRuleChainDO validateRuleChainExists(Long id) {
|
||||
IotRuleChainDO chain = ruleChainMapper.selectById(id);
|
||||
if (chain == null) {
|
||||
throw exception(RULE_CHAIN_NOT_EXISTS);
|
||||
}
|
||||
return chain;
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验有且仅有一个 Trigger 节点(评审 B10)
|
||||
*/
|
||||
private void validateSingleTrigger(List<IotRuleChainSaveReqVO.NodeVO> nodes) {
|
||||
if (nodes == null || nodes.isEmpty()) {
|
||||
throw exception(RULE_CHAIN_MUST_HAVE_EXACTLY_ONE_TRIGGER);
|
||||
}
|
||||
long triggerCount = nodes.stream()
|
||||
.filter(n -> RuleNodeCategory.TRIGGER.getValue().equals(n.getCategory()))
|
||||
.count();
|
||||
if (triggerCount != 1) {
|
||||
throw exception(RULE_CHAIN_MUST_HAVE_EXACTLY_ONE_TRIGGER);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验 DAG 无环(DFS 检测)
|
||||
*/
|
||||
private void validateNoCycle(List<IotRuleChainSaveReqVO.NodeVO> nodes,
|
||||
List<IotRuleChainSaveReqVO.LinkVO> links) {
|
||||
if (links == null || links.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 构建邻接表(使用节点列表索引)
|
||||
int nodeCount = nodes == null ? 0 : nodes.size();
|
||||
// 节点 id → 索引的映射(使用 sourceNodeId/targetNodeId 作为索引)
|
||||
// 注意:创建时节点无 id,使用列表索引模拟
|
||||
// 对于已有 id 的节点(更新场景),使用 id;对于新增节点(id=null),使用负数索引
|
||||
Map<Long, Integer> nodeIndexMap = new HashMap<>();
|
||||
for (int i = 0; i < nodes.size(); i++) {
|
||||
IotRuleChainSaveReqVO.NodeVO node = nodes.get(i);
|
||||
// 使用 id(如有)或索引(取负数以避免冲突)
|
||||
Long key = node.getId() != null ? node.getId() : -(long)(i + 1);
|
||||
nodeIndexMap.put(key, i);
|
||||
}
|
||||
|
||||
// 构建邻接表
|
||||
Map<Long, List<Long>> adjMap = new HashMap<>();
|
||||
for (IotRuleChainSaveReqVO.LinkVO link : links) {
|
||||
adjMap.computeIfAbsent(link.getSourceNodeId(), k -> new ArrayList<>())
|
||||
.add(link.getTargetNodeId());
|
||||
}
|
||||
|
||||
// 收集所有节点 key
|
||||
Set<Long> allKeys = new HashSet<>(nodeIndexMap.keySet());
|
||||
|
||||
// DFS 检测环
|
||||
Set<Long> visited = new HashSet<>();
|
||||
Set<Long> inStack = new HashSet<>();
|
||||
|
||||
for (Long key : allKeys) {
|
||||
if (!visited.contains(key)) {
|
||||
if (hasCycleDFS(key, adjMap, visited, inStack)) {
|
||||
throw exception(RULE_CHAIN_CYCLE_DETECTED);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean hasCycleDFS(Long node, Map<Long, List<Long>> adjMap,
|
||||
Set<Long> visited, Set<Long> inStack) {
|
||||
visited.add(node);
|
||||
inStack.add(node);
|
||||
|
||||
List<Long> neighbors = adjMap.getOrDefault(node, new ArrayList<>());
|
||||
for (Long neighbor : neighbors) {
|
||||
if (!visited.contains(neighbor)) {
|
||||
if (hasCycleDFS(neighbor, adjMap, visited, inStack)) {
|
||||
return true;
|
||||
}
|
||||
} else if (inStack.contains(neighbor)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
inStack.remove(node);
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验 relation_type 合法性(评审 B4)
|
||||
*/
|
||||
private void validateRelationTypes(List<IotRuleChainSaveReqVO.LinkVO> links) {
|
||||
if (links == null) {
|
||||
return;
|
||||
}
|
||||
for (IotRuleChainSaveReqVO.LinkVO link : links) {
|
||||
if (!RuleLinkRelationType.isValid(link.getRelationType())) {
|
||||
throw exception(RULE_CHAIN_INVALID_RELATION_TYPE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建规则链 DO(不含 status、version、tenantId)
|
||||
*/
|
||||
private IotRuleChainDO buildChainDO(IotRuleChainSaveReqVO req) {
|
||||
IotRuleChainDO chain = new IotRuleChainDO();
|
||||
chain.setName(req.getName());
|
||||
chain.setDescription(req.getDescription());
|
||||
chain.setType(req.getType());
|
||||
chain.setPriority(req.getPriority() != null ? req.getPriority() : 100);
|
||||
chain.setDebugMode(req.getDebugMode() != null ? req.getDebugMode() : false);
|
||||
chain.setSubsystemId(req.getSubsystemId());
|
||||
chain.setProductId(req.getProductId());
|
||||
chain.setDeviceId(req.getDeviceId());
|
||||
return chain;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量插入节点 + 连线(同一事务内调用)
|
||||
*/
|
||||
private void insertNodesAndLinks(Long chainId, Long tenantId,
|
||||
List<IotRuleChainSaveReqVO.NodeVO> nodeVOs,
|
||||
List<IotRuleChainSaveReqVO.LinkVO> linkVOs) {
|
||||
// 插入节点
|
||||
if (nodeVOs != null && !nodeVOs.isEmpty()) {
|
||||
List<IotRuleNodeDO> nodeDOs = nodeVOs.stream()
|
||||
.map(v -> IotRuleNodeDO.builder()
|
||||
.ruleChainId(chainId)
|
||||
.name(v.getName())
|
||||
.category(v.getCategory())
|
||||
.type(v.getType())
|
||||
.configuration(v.getConfiguration())
|
||||
.positionX(v.getPositionX())
|
||||
.positionY(v.getPositionY())
|
||||
.tenantId(tenantId)
|
||||
.build())
|
||||
.collect(Collectors.toList());
|
||||
ruleNodeMapper.insertBatch(nodeDOs);
|
||||
}
|
||||
|
||||
// 插入连线
|
||||
if (linkVOs != null && !linkVOs.isEmpty()) {
|
||||
List<IotRuleLinkDO> linkDOs = linkVOs.stream()
|
||||
.map(v -> IotRuleLinkDO.builder()
|
||||
.ruleChainId(chainId)
|
||||
.sourceNodeId(v.getSourceNodeId())
|
||||
.targetNodeId(v.getTargetNodeId())
|
||||
.relationType(v.getRelationType())
|
||||
.condition(v.getCondition())
|
||||
.sortOrder(v.getSortOrder() != null ? v.getSortOrder() : 0)
|
||||
.tenantId(tenantId)
|
||||
.build())
|
||||
.collect(Collectors.toList());
|
||||
ruleLinkMapper.insertBatch(linkDOs);
|
||||
}
|
||||
}
|
||||
|
||||
private Long toLong(Object obj) {
|
||||
if (obj instanceof Long) {
|
||||
return (Long) obj;
|
||||
}
|
||||
if (obj instanceof Number) {
|
||||
return ((Number) obj).longValue();
|
||||
}
|
||||
return Long.parseLong(obj.toString());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE mapper
|
||||
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="com.viewsh.module.iot.rule.dal.mysql.IotRuleChainMapper">
|
||||
|
||||
</mapper>
|
||||
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE mapper
|
||||
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="com.viewsh.module.iot.rule.dal.mysql.IotRuleLinkMapper">
|
||||
|
||||
</mapper>
|
||||
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE mapper
|
||||
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="com.viewsh.module.iot.rule.dal.mysql.IotRuleNodeMapper">
|
||||
|
||||
</mapper>
|
||||
@@ -0,0 +1,299 @@
|
||||
package com.viewsh.module.iot.rule.service;
|
||||
|
||||
import com.viewsh.framework.common.exception.ServiceException;
|
||||
import com.viewsh.framework.test.core.ut.BaseMockitoUnitTest;
|
||||
import com.viewsh.module.iot.rule.controller.admin.vo.IotRuleChainGraphVO;
|
||||
import com.viewsh.module.iot.rule.controller.admin.vo.IotRuleChainSaveReqVO;
|
||||
import com.viewsh.module.iot.rule.dal.dataobject.IotRuleChainDO;
|
||||
import com.viewsh.module.iot.rule.dal.dataobject.IotRuleLinkDO;
|
||||
import com.viewsh.module.iot.rule.dal.dataobject.IotRuleNodeDO;
|
||||
import com.viewsh.module.iot.rule.dal.dataobject.enums.RuleChainStatus;
|
||||
import com.viewsh.module.iot.rule.dal.mysql.IotRuleChainMapper;
|
||||
import com.viewsh.module.iot.rule.dal.mysql.IotRuleLinkMapper;
|
||||
import com.viewsh.module.iot.rule.dal.mysql.IotRuleNodeMapper;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import static com.viewsh.module.iot.enums.ErrorCodeConstants.*;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
/**
|
||||
* {@link IotRuleChainServiceImpl} 单元测试
|
||||
*
|
||||
* 覆盖任务卡 §6.1 的 8 个用例
|
||||
*/
|
||||
class IotRuleChainServiceImplTest extends BaseMockitoUnitTest {
|
||||
|
||||
@InjectMocks
|
||||
private IotRuleChainServiceImpl ruleChainService;
|
||||
|
||||
@Mock
|
||||
private IotRuleChainMapper ruleChainMapper;
|
||||
|
||||
@Mock
|
||||
private IotRuleNodeMapper ruleNodeMapper;
|
||||
|
||||
@Mock
|
||||
private IotRuleLinkMapper ruleLinkMapper;
|
||||
|
||||
// ========== 用例 1:正常创建(1 Trigger + 2 Action)==========
|
||||
|
||||
@Test
|
||||
void testCreate_singleTrigger() {
|
||||
// 准备参数
|
||||
IotRuleChainSaveReqVO req = buildCreateReqVO(
|
||||
buildNodeVO(null, "trigger", "device_event"),
|
||||
buildNodeVO(null, "action", "send_notification"),
|
||||
buildNodeVO(null, "action", "set_property")
|
||||
);
|
||||
req.setLinks(Collections.emptyList());
|
||||
|
||||
// Mock
|
||||
when(ruleChainMapper.insert(any(IotRuleChainDO.class))).thenAnswer(inv -> {
|
||||
IotRuleChainDO chain = inv.getArgument(0);
|
||||
chain.setId(100L);
|
||||
chain.setTenantId(1L);
|
||||
return 1;
|
||||
});
|
||||
when(ruleNodeMapper.insertBatch(anyCollection())).thenReturn(true);
|
||||
|
||||
// 执行
|
||||
Long id = ruleChainService.createRuleChain(req);
|
||||
|
||||
// 断言
|
||||
assertNotNull(id);
|
||||
verify(ruleChainMapper, times(1)).insert(any(IotRuleChainDO.class));
|
||||
verify(ruleNodeMapper, times(1)).insertBatch(anyCollection());
|
||||
}
|
||||
|
||||
// ========== 用例 2:2 个 Trigger 被拒绝 ==========
|
||||
|
||||
@Test
|
||||
void testCreate_multiTriggerRejected() {
|
||||
IotRuleChainSaveReqVO req = buildCreateReqVO(
|
||||
buildNodeVO(null, "trigger", "device_event"),
|
||||
buildNodeVO(null, "trigger", "time_trigger")
|
||||
);
|
||||
req.setLinks(Collections.emptyList());
|
||||
|
||||
ServiceException ex = assertThrows(ServiceException.class,
|
||||
() -> ruleChainService.createRuleChain(req));
|
||||
assertEquals(RULE_CHAIN_MUST_HAVE_EXACTLY_ONE_TRIGGER.getCode(), ex.getCode());
|
||||
}
|
||||
|
||||
// ========== 用例 3:A→B→C→A 环路被拒绝 ==========
|
||||
|
||||
@Test
|
||||
void testCreate_cycleRejected() {
|
||||
IotRuleChainSaveReqVO.NodeVO n1 = buildNodeVO(1L, "trigger", "device_event");
|
||||
IotRuleChainSaveReqVO.NodeVO n2 = buildNodeVO(2L, "condition", "property_check");
|
||||
IotRuleChainSaveReqVO.NodeVO n3 = buildNodeVO(3L, "action", "send_notification");
|
||||
|
||||
IotRuleChainSaveReqVO req = new IotRuleChainSaveReqVO();
|
||||
req.setName("CycleTest");
|
||||
req.setType("SCENE");
|
||||
req.setNodes(Arrays.asList(n1, n2, n3));
|
||||
// A(1) → B(2) → C(3) → A(1) 形成环
|
||||
req.setLinks(Arrays.asList(
|
||||
buildLinkVO(1L, 2L, "Success"),
|
||||
buildLinkVO(2L, 3L, "Success"),
|
||||
buildLinkVO(3L, 1L, "Success")
|
||||
));
|
||||
|
||||
ServiceException ex = assertThrows(ServiceException.class,
|
||||
() -> ruleChainService.createRuleChain(req));
|
||||
assertEquals(RULE_CHAIN_CYCLE_DETECTED.getCode(), ex.getCode());
|
||||
}
|
||||
|
||||
// ========== 用例 4:无效 relation_type 枚举校验失败 ==========
|
||||
|
||||
@Test
|
||||
void testCreate_invalidRelationType() {
|
||||
IotRuleChainSaveReqVO.NodeVO n1 = buildNodeVO(1L, "trigger", "device_event");
|
||||
IotRuleChainSaveReqVO.NodeVO n2 = buildNodeVO(2L, "action", "send_notification");
|
||||
|
||||
IotRuleChainSaveReqVO req = new IotRuleChainSaveReqVO();
|
||||
req.setName("InvalidRelation");
|
||||
req.setType("SCENE");
|
||||
req.setNodes(Arrays.asList(n1, n2));
|
||||
req.setLinks(Collections.singletonList(buildLinkVO(1L, 2L, "CustomXxx")));
|
||||
|
||||
ServiceException ex = assertThrows(ServiceException.class,
|
||||
() -> ruleChainService.createRuleChain(req));
|
||||
assertEquals(RULE_CHAIN_INVALID_RELATION_TYPE.getCode(), ex.getCode());
|
||||
}
|
||||
|
||||
// ========== 用例 5:update 一次 version 从 0 → 1 ==========
|
||||
|
||||
@Test
|
||||
void testUpdate_versionIncrement() {
|
||||
Long chainId = 200L;
|
||||
IotRuleChainDO existing = IotRuleChainDO.builder()
|
||||
.id(chainId)
|
||||
.name("OldName")
|
||||
.type("SCENE")
|
||||
.status(RuleChainStatus.ENABLED.getValue())
|
||||
.version(0L)
|
||||
.priority(100)
|
||||
.tenantId(1L)
|
||||
.build();
|
||||
|
||||
IotRuleChainSaveReqVO req = buildCreateReqVO(
|
||||
buildNodeVO(null, "trigger", "device_event")
|
||||
);
|
||||
req.setId(chainId);
|
||||
req.setLinks(Collections.emptyList());
|
||||
|
||||
when(ruleChainMapper.selectById(chainId)).thenReturn(existing);
|
||||
// 乐观锁更新成功返回 1
|
||||
when(ruleChainMapper.updateWithVersion(eq(chainId), eq(0L), any(IotRuleChainDO.class))).thenReturn(1);
|
||||
when(ruleNodeMapper.deleteByChainId(chainId)).thenReturn(1);
|
||||
when(ruleLinkMapper.deleteByChainId(chainId)).thenReturn(0);
|
||||
when(ruleNodeMapper.insertBatch(anyCollection())).thenReturn(true);
|
||||
|
||||
// 执行(不抛异常即为成功)
|
||||
assertDoesNotThrow(() -> ruleChainService.updateRuleChain(req));
|
||||
|
||||
verify(ruleChainMapper, times(1)).updateWithVersion(eq(chainId), eq(0L), any());
|
||||
}
|
||||
|
||||
// ========== 用例 6:并发 update 乐观锁冲突 ==========
|
||||
|
||||
@Test
|
||||
void testUpdate_optimisticLockConflict() {
|
||||
Long chainId = 300L;
|
||||
IotRuleChainDO existing = IotRuleChainDO.builder()
|
||||
.id(chainId)
|
||||
.name("Name")
|
||||
.type("SCENE")
|
||||
.status(RuleChainStatus.ENABLED.getValue())
|
||||
.version(5L)
|
||||
.tenantId(1L)
|
||||
.priority(100)
|
||||
.build();
|
||||
|
||||
IotRuleChainSaveReqVO req = buildCreateReqVO(
|
||||
buildNodeVO(null, "trigger", "device_event")
|
||||
);
|
||||
req.setId(chainId);
|
||||
req.setLinks(Collections.emptyList());
|
||||
|
||||
when(ruleChainMapper.selectById(chainId)).thenReturn(existing);
|
||||
// 乐观锁更新失败(返回 0)
|
||||
when(ruleChainMapper.updateWithVersion(eq(chainId), eq(5L), any(IotRuleChainDO.class))).thenReturn(0);
|
||||
|
||||
ServiceException ex = assertThrows(ServiceException.class,
|
||||
() -> ruleChainService.updateRuleChain(req));
|
||||
assertEquals(RULE_CHAIN_OPTIMISTIC_LOCK_CONFLICT.getCode(), ex.getCode());
|
||||
}
|
||||
|
||||
// ========== 用例 7:getRuleChainGraph 返回完整图 ==========
|
||||
|
||||
@Test
|
||||
void testGetGraph() {
|
||||
Long chainId = 123L;
|
||||
IotRuleChainDO chain = IotRuleChainDO.builder()
|
||||
.id(chainId)
|
||||
.name("TestChain")
|
||||
.type("SCENE")
|
||||
.status(RuleChainStatus.ENABLED.getValue())
|
||||
.version(0L)
|
||||
.priority(100)
|
||||
.tenantId(1L)
|
||||
.build();
|
||||
|
||||
IotRuleNodeDO node1 = IotRuleNodeDO.builder()
|
||||
.id(10L)
|
||||
.ruleChainId(chainId)
|
||||
.category("trigger")
|
||||
.type("device_event")
|
||||
.configuration("{}")
|
||||
.tenantId(1L)
|
||||
.build();
|
||||
|
||||
IotRuleLinkDO link1 = IotRuleLinkDO.builder()
|
||||
.id(20L)
|
||||
.ruleChainId(chainId)
|
||||
.sourceNodeId(10L)
|
||||
.targetNodeId(11L)
|
||||
.relationType("Success")
|
||||
.tenantId(1L)
|
||||
.build();
|
||||
|
||||
when(ruleChainMapper.selectById(chainId)).thenReturn(chain);
|
||||
when(ruleNodeMapper.selectByChainId(chainId)).thenReturn(Collections.singletonList(node1));
|
||||
when(ruleLinkMapper.selectByChainId(chainId)).thenReturn(Collections.singletonList(link1));
|
||||
|
||||
IotRuleChainGraphVO graph = ruleChainService.getRuleChainGraph(chainId);
|
||||
|
||||
assertNotNull(graph);
|
||||
assertNotNull(graph.getChain());
|
||||
assertEquals(chainId, graph.getChain().getId());
|
||||
assertEquals(1, graph.getNodes().size());
|
||||
assertEquals(1, graph.getLinks().size());
|
||||
assertEquals(10L, graph.getNodes().get(0).getId());
|
||||
assertEquals(20L, graph.getLinks().get(0).getId());
|
||||
}
|
||||
|
||||
// ========== 用例 8:删除 chain 级联软删 nodes+links ==========
|
||||
|
||||
@Test
|
||||
void testDelete_cascade() {
|
||||
Long chainId = 400L;
|
||||
IotRuleChainDO chain = IotRuleChainDO.builder()
|
||||
.id(chainId)
|
||||
.name("DeleteMe")
|
||||
.type("SCENE")
|
||||
.status(RuleChainStatus.ENABLED.getValue())
|
||||
.version(0L)
|
||||
.tenantId(1L)
|
||||
.build();
|
||||
|
||||
when(ruleChainMapper.selectById(chainId)).thenReturn(chain);
|
||||
when(ruleChainMapper.deleteById(chainId)).thenReturn(1);
|
||||
when(ruleNodeMapper.deleteByChainId(chainId)).thenReturn(2);
|
||||
when(ruleLinkMapper.deleteByChainId(chainId)).thenReturn(1);
|
||||
|
||||
ruleChainService.deleteRuleChain(chainId);
|
||||
|
||||
// 验证级联删除
|
||||
verify(ruleChainMapper, times(1)).deleteById(chainId);
|
||||
verify(ruleNodeMapper, times(1)).deleteByChainId(chainId);
|
||||
verify(ruleLinkMapper, times(1)).deleteByChainId(chainId);
|
||||
}
|
||||
|
||||
// ========== 构建辅助方法 ==========
|
||||
|
||||
private IotRuleChainSaveReqVO buildCreateReqVO(IotRuleChainSaveReqVO.NodeVO... nodes) {
|
||||
IotRuleChainSaveReqVO req = new IotRuleChainSaveReqVO();
|
||||
req.setName("Test Chain");
|
||||
req.setType("SCENE");
|
||||
req.setNodes(Arrays.asList(nodes));
|
||||
return req;
|
||||
}
|
||||
|
||||
private IotRuleChainSaveReqVO.NodeVO buildNodeVO(Long id, String category, String type) {
|
||||
IotRuleChainSaveReqVO.NodeVO node = new IotRuleChainSaveReqVO.NodeVO();
|
||||
node.setId(id);
|
||||
node.setCategory(category);
|
||||
node.setType(type);
|
||||
node.setConfiguration("{}");
|
||||
return node;
|
||||
}
|
||||
|
||||
private IotRuleChainSaveReqVO.LinkVO buildLinkVO(Long sourceId, Long targetId, String relationType) {
|
||||
IotRuleChainSaveReqVO.LinkVO link = new IotRuleChainSaveReqVO.LinkVO();
|
||||
link.setSourceNodeId(sourceId);
|
||||
link.setTargetNodeId(targetId);
|
||||
link.setRelationType(relationType);
|
||||
return link;
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user