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:
66
sql/iot/V2.0.1__iot_rule_chain.sql
Normal file
66
sql/iot/V2.0.1__iot_rule_chain.sql
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
-- IoT 规则引擎三张核心表
|
||||||
|
-- [B2] RuleChain/Node/Link 数据模型
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS iot_rule_chain (
|
||||||
|
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||||
|
name VARCHAR(128) NOT NULL COMMENT '规则链名称',
|
||||||
|
description TEXT COMMENT '规则链描述',
|
||||||
|
type VARCHAR(32) NOT NULL COMMENT 'SCENE / DATA / CUSTOM',
|
||||||
|
status TINYINT NOT NULL DEFAULT 1 COMMENT '0=禁用 1=启用 2=WARNING(物模型变更导致)',
|
||||||
|
priority INT NOT NULL DEFAULT 100 COMMENT 'ASC 排序,评审 A5',
|
||||||
|
version BIGINT NOT NULL DEFAULT 0 COMMENT '变更时+1,评审 B9 多实例校验用',
|
||||||
|
debug_mode TINYINT NOT NULL DEFAULT 0 COMMENT '调试模式',
|
||||||
|
|
||||||
|
-- 三层绑定(评审 §十一-B)
|
||||||
|
subsystem_id BIGINT COMMENT 'NULL = 全局规则',
|
||||||
|
product_id BIGINT COMMENT 'NULL = 不限产品',
|
||||||
|
device_id BIGINT COMMENT 'NULL/0 = 范围内所有设备',
|
||||||
|
|
||||||
|
tenant_id BIGINT NOT NULL COMMENT '租户编号',
|
||||||
|
creator VARCHAR(64) COMMENT '创建者',
|
||||||
|
create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||||
|
updater VARCHAR(64) COMMENT '更新者',
|
||||||
|
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||||
|
deleted BIT NOT NULL DEFAULT b'0' COMMENT '是否删除',
|
||||||
|
|
||||||
|
INDEX idx_binding (tenant_id, subsystem_id, product_id, device_id),
|
||||||
|
INDEX idx_status (tenant_id, status, priority),
|
||||||
|
INDEX idx_update_time (update_time)
|
||||||
|
) COMMENT = '规则链';
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS iot_rule_node (
|
||||||
|
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||||
|
rule_chain_id BIGINT NOT NULL COMMENT '所属规则链编号',
|
||||||
|
name VARCHAR(128) COMMENT '节点名称',
|
||||||
|
category VARCHAR(32) NOT NULL COMMENT 'trigger / condition / action',
|
||||||
|
type VARCHAR(64) NOT NULL COMMENT 'Provider 标识(device_property / alarm_trigger 等)',
|
||||||
|
configuration JSON NOT NULL COMMENT '节点配置',
|
||||||
|
position_x INT COMMENT '画布 X 坐标',
|
||||||
|
position_y INT COMMENT '画布 Y 坐标',
|
||||||
|
tenant_id BIGINT NOT NULL COMMENT '租户编号',
|
||||||
|
creator VARCHAR(64) COMMENT '创建者',
|
||||||
|
create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||||
|
updater VARCHAR(64) COMMENT '更新者',
|
||||||
|
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||||
|
deleted BIT NOT NULL DEFAULT b'0' COMMENT '是否删除',
|
||||||
|
|
||||||
|
INDEX idx_chain (rule_chain_id, deleted)
|
||||||
|
) COMMENT = '规则节点';
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS iot_rule_link (
|
||||||
|
id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||||
|
rule_chain_id BIGINT NOT NULL COMMENT '所属规则链编号',
|
||||||
|
source_node_id BIGINT NOT NULL COMMENT '源节点编号',
|
||||||
|
target_node_id BIGINT NOT NULL COMMENT '目标节点编号',
|
||||||
|
relation_type VARCHAR(32) NOT NULL COMMENT '【封闭枚举 评审 B4】Success/Failure/True/False/Timeout/Skip',
|
||||||
|
condition JSON COMMENT '连线条件(可选)',
|
||||||
|
sort_order INT DEFAULT 0 COMMENT '排序',
|
||||||
|
tenant_id BIGINT NOT NULL COMMENT '租户编号',
|
||||||
|
creator VARCHAR(64) COMMENT '创建者',
|
||||||
|
create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||||
|
updater VARCHAR(64) COMMENT '更新者',
|
||||||
|
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||||
|
deleted BIT NOT NULL DEFAULT b'0' COMMENT '是否删除',
|
||||||
|
|
||||||
|
INDEX idx_source (rule_chain_id, source_node_id, deleted)
|
||||||
|
) COMMENT = '规则连线';
|
||||||
@@ -85,4 +85,12 @@ public interface ErrorCodeConstants {
|
|||||||
ErrorCode SUBSYSTEM_CODE_DUPLICATE = new ErrorCode(1_050_020_002, "同项目下子系统编码已存在");
|
ErrorCode SUBSYSTEM_CODE_DUPLICATE = new ErrorCode(1_050_020_002, "同项目下子系统编码已存在");
|
||||||
ErrorCode SUBSYSTEM_HAS_DEVICES = new ErrorCode(1_050_020_003, "子系统下存在设备,不允许删除");
|
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>
|
<version>5.3.3</version>
|
||||||
</dependency>
|
</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 相关 -->
|
<!-- DB 相关 -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.viewsh</groupId>
|
<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