Переглянути джерело

Merge branch 'master' of http://47.111.81.118:3000/uskycloud/usky-modules into fu-dev

fuyuchuan 1 день тому
батько
коміт
799b3b3c18
100 змінених файлів з 4603 додано та 179 видалено
  1. 2 1
      pom.xml
  2. 2 0
      service-eg/service-eg-api/src/main/java/com/usky/eg/RemoteEgService.java
  3. 5 1
      service-eg/service-eg-api/src/main/java/com/usky/eg/factory/RemoteEgFallbackFactory.java
  4. 25 3
      service-eg/service-eg-biz/src/main/java/com/usky/eg/controller/web/EgDeviceController.java
  5. 41 0
      service-eg/service-eg-biz/src/main/java/com/usky/eg/controller/web/EgDeviceHeartbeatController.java
  6. 11 13
      service-eg/service-eg-biz/src/main/java/com/usky/eg/domain/EgDevice.java
  7. 84 0
      service-eg/service-eg-biz/src/main/java/com/usky/eg/domain/EgDeviceHeartbeat.java
  8. 34 0
      service-eg/service-eg-biz/src/main/java/com/usky/eg/domain/EgDevicePersonBind.java
  9. 1 1
      service-eg/service-eg-biz/src/main/java/com/usky/eg/domain/EgRecord.java
  10. 98 0
      service-eg/service-eg-biz/src/main/java/com/usky/eg/domain/SysPerson.java
  11. 18 0
      service-eg/service-eg-biz/src/main/java/com/usky/eg/mapper/EgDeviceHeartbeatMapper.java
  12. 44 0
      service-eg/service-eg-biz/src/main/java/com/usky/eg/mapper/EgDevicePersonBindMapper.java
  13. 22 0
      service-eg/service-eg-biz/src/main/java/com/usky/eg/mapper/SysPersonMapper.java
  14. 19 0
      service-eg/service-eg-biz/src/main/java/com/usky/eg/service/EgDeviceHeartbeatService.java
  15. 15 2
      service-eg/service-eg-biz/src/main/java/com/usky/eg/service/EgDeviceService.java
  16. 91 0
      service-eg/service-eg-biz/src/main/java/com/usky/eg/service/impl/EgDeviceHeartbeatServiceImpl.java
  17. 425 133
      service-eg/service-eg-biz/src/main/java/com/usky/eg/service/impl/EgDeviceServiceImpl.java
  18. 17 10
      service-eg/service-eg-biz/src/main/java/com/usky/eg/service/impl/EgRecordServiceImpl.java
  19. 21 0
      service-eg/service-eg-biz/src/main/java/com/usky/eg/service/vo/EgDeviceBindFacePersonQueryVO.java
  20. 21 0
      service-eg/service-eg-biz/src/main/java/com/usky/eg/service/vo/EgDeviceBindFacePersonVO.java
  21. 9 0
      service-eg/service-eg-biz/src/main/java/com/usky/eg/service/vo/EgDeviceRequestVO.java
  22. 20 0
      service-eg/service-eg-biz/src/main/resources/mapper/eg/EgDeviceHeartbeatMapper.xml
  23. 1 2
      service-eg/service-eg-biz/src/main/resources/mapper/eg/EgDeviceMapper.xml
  24. 62 0
      service-eg/service-eg-biz/src/main/resources/mapper/eg/EgDevicePersonBindMapper.xml
  25. 32 0
      service-eg/service-eg-biz/src/main/resources/mapper/eg/SysPersonMapper.xml
  26. 2 1
      service-iot/service-iot-biz/src/main/java/com/usky/iot/service/impl/DmpDeviceInfoServiceImpl.java
  27. 6 0
      service-job/pom.xml
  28. 9 0
      service-job/src/main/java/com/ruoyi/job/task/RyTask.java
  29. 0 10
      service-meeting/service-meeting-biz/src/main/java/com/usky/meeting/domain/EgDevice.java
  30. 0 2
      service-meeting/service-meeting-biz/src/main/resources/mapper/meeting/EgDeviceMapper.xml
  31. 17 0
      service-rule/pom.xml
  32. 26 0
      service-rule/service-rule-api/pom.xml
  33. 101 0
      service-rule/service-rule-biz/pom.xml
  34. 44 0
      service-rule/service-rule-biz/src/main/java/com/usky/rule/RuoYiSystemApplication.java
  35. 37 0
      service-rule/service-rule-biz/src/main/java/com/usky/rule/cache/DeviceAcqTriggerCooldownCache.java
  36. 113 0
      service-rule/service-rule-biz/src/main/java/com/usky/rule/cache/DeviceTriggerIncludeMinuteCache.java
  37. 32 0
      service-rule/service-rule-biz/src/main/java/com/usky/rule/cache/RuleEngineCache.java
  38. 141 0
      service-rule/service-rule-biz/src/main/java/com/usky/rule/config/CronTaskManager.java
  39. 22 0
      service-rule/service-rule-biz/src/main/java/com/usky/rule/constant/DateTimeConstants.java
  40. 36 0
      service-rule/service-rule-biz/src/main/java/com/usky/rule/constant/RegExpConstants.java
  41. 108 0
      service-rule/service-rule-biz/src/main/java/com/usky/rule/controller/MybatisGeneratorUtils.java
  42. 21 0
      service-rule/service-rule-biz/src/main/java/com/usky/rule/controller/web/BaseBuildController.java
  43. 21 0
      service-rule/service-rule-biz/src/main/java/com/usky/rule/controller/web/BaseSpaceController.java
  44. 21 0
      service-rule/service-rule-biz/src/main/java/com/usky/rule/controller/web/DmpDeviceController.java
  45. 21 0
      service-rule/service-rule-biz/src/main/java/com/usky/rule/controller/web/DmpProductAttributeController.java
  46. 21 0
      service-rule/service-rule-biz/src/main/java/com/usky/rule/controller/web/DmpProductCommandController.java
  47. 21 0
      service-rule/service-rule-biz/src/main/java/com/usky/rule/controller/web/DmpProductController.java
  48. 116 0
      service-rule/service-rule-biz/src/main/java/com/usky/rule/controller/web/RuleEngineController.java
  49. 53 0
      service-rule/service-rule-biz/src/main/java/com/usky/rule/controller/web/RuleEngineLogController.java
  50. 172 0
      service-rule/service-rule-biz/src/main/java/com/usky/rule/crons/TriggerCronTask.java
  51. 212 0
      service-rule/service-rule-biz/src/main/java/com/usky/rule/domain/BaseBuild.java
  52. 91 0
      service-rule/service-rule-biz/src/main/java/com/usky/rule/domain/BaseSpace.java
  53. 156 0
      service-rule/service-rule-biz/src/main/java/com/usky/rule/domain/DmpDevice.java
  54. 166 0
      service-rule/service-rule-biz/src/main/java/com/usky/rule/domain/DmpProduct.java
  55. 142 0
      service-rule/service-rule-biz/src/main/java/com/usky/rule/domain/DmpProductAttribute.java
  56. 112 0
      service-rule/service-rule-biz/src/main/java/com/usky/rule/domain/DmpProductCommand.java
  57. 40 0
      service-rule/service-rule-biz/src/main/java/com/usky/rule/domain/RuleEngine.java
  58. 32 0
      service-rule/service-rule-biz/src/main/java/com/usky/rule/domain/RuleEngineCondition.java
  59. 28 0
      service-rule/service-rule-biz/src/main/java/com/usky/rule/domain/RuleEngineCron.java
  60. 32 0
      service-rule/service-rule-biz/src/main/java/com/usky/rule/domain/RuleEngineDevice.java
  61. 40 0
      service-rule/service-rule-biz/src/main/java/com/usky/rule/domain/RuleEngineLog.java
  62. 62 0
      service-rule/service-rule-biz/src/main/java/com/usky/rule/engine/RuleConditionEvaluator.java
  63. 30 0
      service-rule/service-rule-biz/src/main/java/com/usky/rule/enums/ActionTypeEnum.java
  64. 28 0
      service-rule/service-rule-biz/src/main/java/com/usky/rule/enums/ConstraintTypeEnum.java
  65. 29 0
      service-rule/service-rule-biz/src/main/java/com/usky/rule/enums/TimeTypeEnum.java
  66. 27 0
      service-rule/service-rule-biz/src/main/java/com/usky/rule/enums/TriggerTypeEnum.java
  67. 22 0
      service-rule/service-rule-biz/src/main/java/com/usky/rule/enums/TriggerValueTypeEnum.java
  68. 25 0
      service-rule/service-rule-biz/src/main/java/com/usky/rule/exception/BizException.java
  69. 44 0
      service-rule/service-rule-biz/src/main/java/com/usky/rule/jobs/CommonJob.java
  70. 310 0
      service-rule/service-rule-biz/src/main/java/com/usky/rule/jobs/ConsumptionJob.java
  71. 93 0
      service-rule/service-rule-biz/src/main/java/com/usky/rule/listeners/CommonListener.java
  72. 16 0
      service-rule/service-rule-biz/src/main/java/com/usky/rule/mapper/BaseBuildMapper.java
  73. 16 0
      service-rule/service-rule-biz/src/main/java/com/usky/rule/mapper/BaseSpaceMapper.java
  74. 16 0
      service-rule/service-rule-biz/src/main/java/com/usky/rule/mapper/DmpDeviceMapper.java
  75. 16 0
      service-rule/service-rule-biz/src/main/java/com/usky/rule/mapper/DmpProductAttributeMapper.java
  76. 16 0
      service-rule/service-rule-biz/src/main/java/com/usky/rule/mapper/DmpProductCommandMapper.java
  77. 16 0
      service-rule/service-rule-biz/src/main/java/com/usky/rule/mapper/DmpProductMapper.java
  78. 14 0
      service-rule/service-rule-biz/src/main/java/com/usky/rule/mapper/RuleEngineConditionMapper.java
  79. 18 0
      service-rule/service-rule-biz/src/main/java/com/usky/rule/mapper/RuleEngineCronMapper.java
  80. 22 0
      service-rule/service-rule-biz/src/main/java/com/usky/rule/mapper/RuleEngineDeviceMapper.java
  81. 15 0
      service-rule/service-rule-biz/src/main/java/com/usky/rule/mapper/RuleEngineLogMapper.java
  82. 18 0
      service-rule/service-rule-biz/src/main/java/com/usky/rule/mapper/RuleEngineMapper.java
  83. 38 0
      service-rule/service-rule-biz/src/main/java/com/usky/rule/mq/RuleReportConsumer.java
  84. 23 0
      service-rule/service-rule-biz/src/main/java/com/usky/rule/mq/vo/DeviceReportPayload.java
  85. 78 0
      service-rule/service-rule-biz/src/main/java/com/usky/rule/schedule/RuleEngineCronScheduler.java
  86. 16 0
      service-rule/service-rule-biz/src/main/java/com/usky/rule/service/BaseBuildService.java
  87. 16 0
      service-rule/service-rule-biz/src/main/java/com/usky/rule/service/BaseSpaceService.java
  88. 16 0
      service-rule/service-rule-biz/src/main/java/com/usky/rule/service/DmpDeviceService.java
  89. 16 0
      service-rule/service-rule-biz/src/main/java/com/usky/rule/service/DmpProductAttributeService.java
  90. 16 0
      service-rule/service-rule-biz/src/main/java/com/usky/rule/service/DmpProductCommandService.java
  91. 16 0
      service-rule/service-rule-biz/src/main/java/com/usky/rule/service/DmpProductService.java
  92. 13 0
      service-rule/service-rule-biz/src/main/java/com/usky/rule/service/RuleEngineConditionService.java
  93. 15 0
      service-rule/service-rule-biz/src/main/java/com/usky/rule/service/RuleEngineCronService.java
  94. 13 0
      service-rule/service-rule-biz/src/main/java/com/usky/rule/service/RuleEngineDetailService.java
  95. 11 0
      service-rule/service-rule-biz/src/main/java/com/usky/rule/service/RuleEngineDeviceService.java
  96. 15 0
      service-rule/service-rule-biz/src/main/java/com/usky/rule/service/RuleEngineLogService.java
  97. 56 0
      service-rule/service-rule-biz/src/main/java/com/usky/rule/service/RuleEngineService.java
  98. 20 0
      service-rule/service-rule-biz/src/main/java/com/usky/rule/service/impl/BaseBuildServiceImpl.java
  99. 20 0
      service-rule/service-rule-biz/src/main/java/com/usky/rule/service/impl/BaseSpaceServiceImpl.java
  100. 20 0
      service-rule/service-rule-biz/src/main/java/com/usky/rule/service/impl/DmpDeviceServiceImpl.java

+ 2 - 1
pom.xml

@@ -64,7 +64,6 @@
 
         <module>service-agbox</module>
 
-
         <module>service-job</module>
 
 
@@ -88,6 +87,8 @@
 
         <module>service-tsdb</module>
 
+        <module>service-rule</module>
+
         <!--    <module>service-data</module>-->
 
         <module>service-sas</module>

+ 2 - 0
service-eg/service-eg-api/src/main/java/com/usky/eg/RemoteEgService.java

@@ -13,4 +13,6 @@ import java.util.Map;
 @FeignClient(contextId = "remoteEgService", value = "service-eg", fallbackFactory = RemoteEgFallbackFactory.class)
 public interface RemoteEgService {
 
+    @GetMapping("/egDeviceStatus")
+    void egDeviceStatus();
 }

+ 5 - 1
service-eg/service-eg-api/src/main/java/com/usky/eg/factory/RemoteEgFallbackFactory.java

@@ -1,5 +1,6 @@
 package com.usky.eg.factory;
 
+import com.usky.common.core.exception.FeignBadRequestException;
 import com.usky.eg.RemoteEgService;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -23,7 +24,10 @@ public class RemoteEgFallbackFactory implements FallbackFactory<RemoteEgService>
     {
         log.error("用户服务调用失败:{}", throwable.getMessage());
         return new RemoteEgService() {
-
+            @Override
+            public void egDeviceStatus() {
+                throw new FeignBadRequestException(500,"同步门禁设备状态异常"+throwable.getMessage());
+            }
         };
     }
 }

+ 25 - 3
service-eg/service-eg-biz/src/main/java/com/usky/eg/controller/web/EgDeviceController.java

@@ -7,6 +7,8 @@ import com.usky.common.log.annotation.Log;
 import com.usky.common.log.enums.BusinessType;
 import com.usky.eg.domain.EgDevice;
 import com.usky.eg.service.EgDeviceService;
+import com.usky.eg.service.vo.EgDeviceBindFacePersonQueryVO;
+import com.usky.eg.service.vo.EgDeviceBindFacePersonVO;
 import com.usky.eg.service.vo.EgDeviceRequestVO;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.web.bind.annotation.*;
@@ -32,6 +34,11 @@ public class EgDeviceController {
         return ApiResult.success(egDeviceService.page(requestVO));
     }
 
+    @PostMapping("/bindFacePerson/page")
+    public ApiResult<CommonPage<EgDeviceBindFacePersonVO>> pageBindFacePersons(@RequestBody EgDeviceBindFacePersonQueryVO queryVO) {
+        return ApiResult.success(egDeviceService.pageBindFacePersons(queryVO));
+    }
+
     @PostMapping("wePage")
     public ApiResult<CommonPage<EgDevice>> wePage(@RequestBody EgDeviceRequestVO requestVO){
         return ApiResult.success(egDeviceService.wePage(requestVO));
@@ -81,6 +88,20 @@ public class EgDeviceController {
         return ApiResult.success();
     }
 
+    /**
+     * 设备绑定人员
+     * @param params 包含 deviceId, personIds, isLoginNotify 的参数对象
+     * @return
+     */
+    @PostMapping("/bindPerson")
+    public ApiResult<Void> bindPerson(@RequestBody Map<String, Object> params){
+        Integer deviceId = (Integer) params.get("deviceId");
+        String personIds = (String) params.get("personIds");
+        Integer isLoginNotify = params.get("isLoginNotify") != null ? (Integer) params.get("isLoginNotify") : 0;
+        egDeviceService.bindPerson(deviceId, personIds, isLoginNotify);
+        return ApiResult.success();
+    }
+
     /**
      * 下发设备控制命令
      */
@@ -90,12 +111,13 @@ public class EgDeviceController {
                                                  @RequestParam("deviceUuid") String deviceUuid,
                                                  @RequestParam("commandCode") String commandCode,
                                                  @RequestParam("commandValue") String commandValue,
-                                                 @RequestParam(value = "domain",required = false) String domain,
                                                  @RequestParam(value = "userId",required = false) Long userId,
                                                  @RequestParam(value = "userName",required = false) String userName,
                                                  @RequestParam(value = "categoryType",required = false , defaultValue = "1") Integer categoryType,
-                                                 @RequestParam(value = "gatewayUuid",required = false) String gatewayUuid){
-        return ApiResult.success(egDeviceService.control(productCode,deviceUuid,commandCode,commandValue,domain,userId,userName,categoryType,gatewayUuid));
+                                                 @RequestParam(value = "gatewayUuid",required = false) String gatewayUuid,
+                                                 @RequestParam(value = "skipCheck",required = false, defaultValue = "false") Boolean skipCheck,
+                                                 @RequestParam(value = "passType",required = false) Integer passType){
+        return ApiResult.success(egDeviceService.control(productCode,deviceUuid,commandCode,commandValue,userId,userName,categoryType,gatewayUuid,skipCheck,passType));
     }
 
 }

+ 41 - 0
service-eg/service-eg-biz/src/main/java/com/usky/eg/controller/web/EgDeviceHeartbeatController.java

@@ -0,0 +1,41 @@
+package com.usky.eg.controller.web;
+
+
+import com.usky.common.core.bean.ApiResult;
+import com.usky.eg.domain.EgDeviceHeartbeat;
+import com.usky.eg.service.EgDeviceHeartbeatService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ * <p>
+ * 门禁设备心跳表 前端控制器
+ * </p>
+ *
+ * @author fhs
+ * @since 2026-02-03
+ */
+@RestController
+@RequestMapping("/egDeviceHeartbeat")
+public class EgDeviceHeartbeatController {
+    private static final Logger log = LoggerFactory.getLogger(EgDeviceHeartbeatController.class);
+    @Autowired
+    private EgDeviceHeartbeatService heartbeatService;
+
+    @PostMapping("/escalation")
+    public ApiResult<Void> heartbeatEscalation(@RequestBody EgDeviceHeartbeat heartbeat) {
+        try {
+            heartbeatService.heartbeatEscalation(heartbeat);
+        } catch (Exception e) {
+            log.error("设备心跳异常", e);
+            return ApiResult.error("设备心跳入库异常!");
+        }
+        return ApiResult.success();
+    }
+}

+ 11 - 13
service-eg/service-eg-biz/src/main/java/com/usky/eg/domain/EgDevice.java

@@ -6,7 +6,6 @@ import com.baomidou.mybatisplus.annotation.TableField;
 import com.baomidou.mybatisplus.annotation.TableId;
 import java.time.LocalDateTime;
 import java.io.Serializable;
-import java.util.List;
 
 import lombok.Data;
 import lombok.EqualsAndHashCode;
@@ -62,16 +61,6 @@ public class EgDevice implements Serializable {
      */
     private String deviceIp;
 
-    /**
-     * 端口
-     */
-    private Integer devicePort;
-
-    /**
-     * 门禁号
-     */
-    private String egNumber;
-
     /**
      * 绑定人脸信息
      */
@@ -108,6 +97,12 @@ public class EgDevice implements Serializable {
      */
     private Integer tenantId;
 
+    /**
+     * 绑定人员信息(从 eg_device_person_bind 表汇总的 person_id 列表,以逗号分隔)
+     */
+    @TableField("bind_person")
+    private String bindPerson;
+
     /**
      * 屏保
      */
@@ -134,10 +129,13 @@ public class EgDevice implements Serializable {
     private String workStatus;
 
     /**
+     * 设备code
+     */
+    private String deviceCode;
 
     /**
-     * 用户人脸信息记录
+     * 设备状态;1:在线,0:离线(从心跳表获取,不映射到数据库)
      */
     @TableField(exist = false)
-    private List<MeetingFace> meetingFaceList;
+    private Integer deviceStatus;
 }

+ 84 - 0
service-eg/service-eg-biz/src/main/java/com/usky/eg/domain/EgDeviceHeartbeat.java

@@ -0,0 +1,84 @@
+package com.usky.eg.domain;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+
+import java.time.LocalDateTime;
+import java.io.Serializable;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * <p>
+ * 门禁设备心跳表
+ * </p>
+ *
+ * @author fhs
+ * @since 2026-02-03
+ */
+@Data
+@EqualsAndHashCode(callSuper = false)
+public class EgDeviceHeartbeat implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 门禁设备心跳表主键ID
+     */
+    @TableId(value = "id", type = IdType.AUTO)
+    private Integer id;
+
+    /**
+     * 设备code
+     */
+    private String deviceCode;
+
+    /**
+     * 设备ip地址
+     */
+    private String ipAddr;
+
+    /**
+     * 设备mac地址
+     */
+    private String macAddr;
+
+    /**
+     * 设备类型(1.会议屏 2.信息发布屏 3.综合屏)
+     */
+    private Boolean deviceType;
+
+    /**
+     * 创建日期
+     */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private LocalDateTime createTime;
+
+    /**
+     * 设备型号
+     */
+    private String model;
+
+    /**
+     *  设备厂商
+     */
+    private String manuFacturer;
+
+    /**
+     * 设备版本号
+     */
+    private String version;
+
+    /**
+     * 设备sdk
+     */
+    private String sdk;
+
+    /**
+     * 设备状态;1:在线,0:离线
+     */
+    private Integer deviceStatus;
+
+}

+ 34 - 0
service-eg/service-eg-biz/src/main/java/com/usky/eg/domain/EgDevicePersonBind.java

@@ -0,0 +1,34 @@
+package com.usky.eg.domain;
+
+import java.io.Serializable;
+
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * <p>
+ * 门禁设备人员绑定表 eg_device_person_bind
+ * </p>
+ */
+@Data
+@EqualsAndHashCode(callSuper = false)
+public class EgDevicePersonBind implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 设备主键ID
+     */
+    private Integer deviceId;
+
+    /**
+     * 人员ID
+     */
+    private Integer personId;
+
+    /**
+     * 是否打开登录通知(1 表示是,0 表示否,默认0)
+     */
+    private Integer isLoginNotify;
+}
+

+ 1 - 1
service-eg/service-eg-biz/src/main/java/com/usky/eg/domain/EgRecord.java

@@ -46,7 +46,7 @@ public class EgRecord implements Serializable {
     private Integer egDeviceId;
 
     /**
-     * 通行方式(1、人脸 2、刷卡 3、手机)
+     * 通行方式(0、其它 1、人脸 2、刷卡 3、手机 4、电脑)
      */
     private Integer passType;
 

+ 98 - 0
service-eg/service-eg-biz/src/main/java/com/usky/eg/domain/SysPerson.java

@@ -0,0 +1,98 @@
+package com.usky.eg.domain;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.io.Serializable;
+import java.time.LocalDateTime;
+
+/**
+ * <p>
+ * 人员信息表
+ * </p>
+ *
+ * @author zyj
+ * @since 2024-11-27
+ */
+@Data
+@EqualsAndHashCode(callSuper = false)
+@TableName("sys_person")
+public class SysPerson implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    @TableId(value = "id", type = IdType.AUTO)
+    private Integer id;
+
+    /** 姓名 */
+    private String fullName;
+
+    /** 年龄 */
+    private Integer age;
+
+    /** 性别;1男、2女 */
+    private Integer gender;
+
+    /** 家庭住址 */
+    private String address;
+
+    /** 文化程度 */
+    private Integer educationDegree;
+
+    /** 身份证号 */
+    private String idNumber;
+
+    /** 联系方式 */
+    private String linkPhone;
+
+    /** 岗位ID */
+    private Long postId;
+
+    /** 部门ID */
+    private Long deptId;
+
+    /** 入职时间 */
+    private LocalDateTime entryTime;
+
+    /** 证书1 */
+    private String certificateUrl1;
+
+    /** 证书2 */
+    private String certificateUrl2;
+
+    /** 证书3 */
+    private String certificateUrl3;
+
+    /** 创建人 */
+    private String creator;
+
+    /** 创建时间 */
+    private LocalDateTime createTime;
+
+    /** 更新人 */
+    private String updatePerson;
+
+    /** 更新时间 */
+    private LocalDateTime updateTime;
+
+    /** 图片数据(base64编码) */
+    private String faceBase;
+
+    /** 验证次数(默认0) */
+    private Integer vefNum;
+
+    /** 人脸名称 */
+    private String faceName;
+
+    /** 人脸备注 */
+    private String remark;
+
+    /** 人脸状态(0=可用,1=不可用) */
+    private Byte faceStatus;
+
+    /** 卡号 */
+    private String cardNum;
+}

+ 18 - 0
service-eg/service-eg-biz/src/main/java/com/usky/eg/mapper/EgDeviceHeartbeatMapper.java

@@ -0,0 +1,18 @@
+package com.usky.eg.mapper;
+
+import com.usky.eg.domain.EgDeviceHeartbeat;
+import com.usky.common.mybatis.core.CrudMapper;
+import org.springframework.stereotype.Repository;
+
+/**
+ * <p>
+ * 门禁设备心跳表 Mapper 接口
+ * </p>
+ *
+ * @author fhs
+ * @since 2026-02-03
+ */
+@Repository
+public interface EgDeviceHeartbeatMapper extends CrudMapper<EgDeviceHeartbeat> {
+
+}

+ 44 - 0
service-eg/service-eg-biz/src/main/java/com/usky/eg/mapper/EgDevicePersonBindMapper.java

@@ -0,0 +1,44 @@
+package com.usky.eg.mapper;
+
+import com.usky.eg.domain.EgDevicePersonBind;
+import org.apache.ibatis.annotations.Param;
+import org.springframework.stereotype.Repository;
+
+import java.util.List;
+
+@Repository
+public interface EgDevicePersonBindMapper {
+
+    /**
+     * 根据多个设备ID查询绑定关系
+     */
+    List<EgDevicePersonBind> selectByDeviceIds(@Param("deviceIds") List<Integer> deviceIds);
+
+    /**
+     * 统计某个设备下的绑定人数
+     */
+    Integer countByDeviceId(@Param("deviceId") Integer deviceId);
+
+    /**
+     * 根据用户ID和设备ID统计绑定关系
+     * 通过 sys_user_person -> sys_person -> eg_device_person_bind 进行关联
+     */
+    Integer countByUserIdAndDeviceId(@Param("userId") Long userId, @Param("deviceId") Integer deviceId);
+
+    /**
+     * 根据用户ID查询其绑定的设备ID列表
+     * 通过 sys_user_person -> sys_person -> eg_device_person_bind 进行关联
+     */
+    List<Integer> selectDeviceIdsByUserId(@Param("userId") Long userId);
+
+    /**
+     * 根据设备ID删除绑定关系
+     */
+    void deleteByDeviceId(@Param("deviceId") Integer deviceId);
+
+    /**
+     * 新增绑定关系
+     */
+    void insert(EgDevicePersonBind bind);
+}
+

+ 22 - 0
service-eg/service-eg-biz/src/main/java/com/usky/eg/mapper/SysPersonMapper.java

@@ -0,0 +1,22 @@
+package com.usky.eg.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.usky.eg.domain.SysPerson;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+import org.apache.ibatis.annotations.Select;
+
+import java.util.List;
+
+/**
+ * 人员信息表 Mapper
+ */
+@Mapper
+public interface SysPersonMapper extends BaseMapper<SysPerson> {
+
+    /**
+     * 查询所有有效的人脸数据(face_base不为空且face_status=0)
+     */
+    @Select("SELECT id, full_name, face_base, face_name, card_num FROM sys_person WHERE face_base IS NOT NULL AND face_base != '' AND face_status = 0")
+    List<SysPerson> selectAllValidFace();
+}

+ 19 - 0
service-eg/service-eg-biz/src/main/java/com/usky/eg/service/EgDeviceHeartbeatService.java

@@ -0,0 +1,19 @@
+package com.usky.eg.service;
+
+import com.usky.eg.domain.EgDeviceHeartbeat;
+import com.usky.common.mybatis.core.CrudService;
+
+/**
+ * <p>
+ * 门禁设备心跳表 服务类
+ * </p>
+ *
+ * @author fhs
+ * @since 2026-02-03
+ */
+public interface EgDeviceHeartbeatService extends CrudService<EgDeviceHeartbeat> {
+
+    void heartbeatEscalation(EgDeviceHeartbeat heartbeat);
+
+    void updateEgDeviceStatus();
+}

+ 15 - 2
service-eg/service-eg-biz/src/main/java/com/usky/eg/service/EgDeviceService.java

@@ -3,8 +3,9 @@ package com.usky.eg.service;
 import com.usky.common.core.bean.CommonPage;
 import com.usky.eg.domain.EgDevice;
 import com.usky.common.mybatis.core.CrudService;
+import com.usky.eg.service.vo.EgDeviceBindFacePersonQueryVO;
+import com.usky.eg.service.vo.EgDeviceBindFacePersonVO;
 import com.usky.eg.service.vo.EgDeviceRequestVO;
-import org.springframework.web.bind.annotation.RequestBody;
 
 import java.util.Map;
 
@@ -33,5 +34,17 @@ public interface EgDeviceService extends CrudService<EgDevice> {
 
     boolean checkDeviceNameUnique(EgDevice egDevice);
 
-    Map<String,Object> control(String productCode, String deviceUuid, String commandCode, String commandValue, String domain, Long userId, String userName, Integer categoryType, String gatewayUuid);
+    Map<String,Object> control(String productCode, String deviceUuid, String commandCode, String commandValue, Long userId, String userName, Integer categoryType, String gatewayUuid, Boolean skipCheck, Integer passType);
+
+    /**
+     * 设备与人员绑定
+     *
+     * @param deviceId      设备主键ID
+     * @param personIds     人员ID,多个以逗号分隔
+     * @param isLoginNotify 是否打开登录通知(1 表示是,0 表示否)
+     */
+    void bindPerson(Integer deviceId, String personIds, Integer isLoginNotify);
+
+    /** 绑定表人员分页(可选姓名、人脸状态) */
+    CommonPage<EgDeviceBindFacePersonVO> pageBindFacePersons(EgDeviceBindFacePersonQueryVO queryVO);
 }

+ 91 - 0
service-eg/service-eg-biz/src/main/java/com/usky/eg/service/impl/EgDeviceHeartbeatServiceImpl.java

@@ -0,0 +1,91 @@
+package com.usky.eg.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
+import com.usky.common.core.exception.BusinessException;
+import com.usky.eg.domain.EgDeviceHeartbeat;
+import com.usky.eg.mapper.EgDeviceHeartbeatMapper;
+import com.usky.eg.service.EgDeviceHeartbeatService;
+import com.usky.common.mybatis.core.AbstractCrudService;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+/**
+ * <p>
+ * 门禁设备心跳表 服务实现类
+ * </p>
+ *
+ * @author fhs
+ * @since 2026-02-03
+ */
+@Service
+public class EgDeviceHeartbeatServiceImpl extends AbstractCrudService<EgDeviceHeartbeatMapper, EgDeviceHeartbeat> implements EgDeviceHeartbeatService {
+
+    @Transactional(rollbackFor = Exception.class)
+    @Override
+    public void heartbeatEscalation(EgDeviceHeartbeat heartbeat) {
+        if (StringUtils.isBlank(heartbeat.getDeviceCode())) {
+            throw new BusinessException("设备编码不能为空!");
+        }
+        if (StringUtils.isBlank(heartbeat.getIpAddr())) {
+            throw new BusinessException("设备IP不能为空!");
+        }
+        if (heartbeat.getCreateTime() == null) {
+            heartbeat.setCreateTime(LocalDateTime.now());
+        }
+        if (heartbeat.getDeviceType() == null) {
+            throw new BusinessException("设备类型不能为空!");
+        }
+
+        // 查询是否存在该设备的心跳记录
+        LambdaQueryWrapper<EgDeviceHeartbeat> queryWrapper = Wrappers.lambdaQuery();
+        queryWrapper.eq(EgDeviceHeartbeat::getDeviceCode, heartbeat.getDeviceCode());
+        EgDeviceHeartbeat existingHeartbeat = baseMapper.selectOne(queryWrapper);
+        
+        if (existingHeartbeat != null) {
+            // 如果存在,更新记录
+            heartbeat.setId(existingHeartbeat.getId());
+            baseMapper.updateById(heartbeat);
+        } else {
+            // 如果不存在,插入新记录
+            baseMapper.insert(heartbeat);
+        }
+    }
+
+    /**
+     * 更新设备状态定时任务服务接口
+     * 从心跳表中获取设备状态并同步更新
+     */
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void updateEgDeviceStatus() {
+        try {
+            LocalDateTime now = LocalDateTime.now();
+            
+            // 1. 查询所有当前状态为在线的心跳记录
+            LambdaQueryWrapper<EgDeviceHeartbeat> heartbeatQueryWrapper = Wrappers.lambdaQuery();
+            heartbeatQueryWrapper.eq(EgDeviceHeartbeat::getDeviceStatus, 1);
+            List<EgDeviceHeartbeat> heartbeatList = baseMapper.selectList(heartbeatQueryWrapper);
+
+            // 2. 遍历在线记录,若心跳时间超过5分钟则更新为离线
+            for (EgDeviceHeartbeat heartbeat : heartbeatList) {
+                LocalDateTime heartbeatTime = heartbeat.getCreateTime();
+                if (heartbeatTime != null && !heartbeatTime.isAfter(now.minusMinutes(5))) {
+                    // 心跳超时,更新心跳表中的状态为离线
+                    LambdaUpdateWrapper<EgDeviceHeartbeat> updateWrapper = Wrappers.lambdaUpdate();
+                    updateWrapper.set(EgDeviceHeartbeat::getDeviceStatus, 0)
+                            .eq(EgDeviceHeartbeat::getId, heartbeat.getId());
+                    baseMapper.update(null, updateWrapper);
+                }
+            }
+        } catch (Exception e) {
+            // 定时任务失败不影响主流程,记录日志即可
+            throw new BusinessException("更新设备状态失败:" + (e.getMessage() != null ? e.getMessage() : "未知错误"));
+        }
+    }
+}

+ 425 - 133
service-eg/service-eg-biz/src/main/java/com/usky/eg/service/impl/EgDeviceServiceImpl.java

@@ -13,22 +13,28 @@ import com.usky.common.core.exception.BusinessException;
 import com.usky.common.core.util.UUIDUtils;
 import com.usky.common.security.utils.SecurityUtils;
 import com.usky.eg.domain.EgDevice;
-import com.usky.eg.domain.MeetingFace;
-import com.usky.eg.domain.MeetingFaceDevice;
+import com.usky.eg.domain.EgDeviceHeartbeat;
+import com.usky.eg.domain.EgDevicePersonBind;
+import com.usky.eg.domain.EgRecord;
+import com.usky.eg.domain.SysPerson;
 import com.usky.eg.mapper.EgDeviceMapper;
-import com.usky.eg.mapper.MeetingFaceDeviceMapper;
-import com.usky.eg.mapper.MeetingFaceMapper;
+import com.usky.eg.mapper.EgDeviceHeartbeatMapper;
+import com.usky.eg.mapper.EgDevicePersonBindMapper;
+import com.usky.eg.mapper.EgRecordMapper;
+import com.usky.eg.mapper.SysPersonMapper;
 import com.usky.eg.service.EgDeviceService;
 import com.usky.common.mybatis.core.AbstractCrudService;
+import com.usky.eg.service.vo.EgDeviceBindFacePersonQueryVO;
+import com.usky.eg.service.vo.EgDeviceBindFacePersonVO;
 import com.usky.eg.service.vo.EgDeviceRequestVO;
 import com.usky.iot.RemoteIotTaskService;
 import com.usky.transfer.RemoteTransferService;
-import io.swagger.models.auth.In;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 
 import java.time.LocalDateTime;
 import java.util.*;
+import java.util.concurrent.CompletableFuture;
 
 /**
  * <p>
@@ -40,25 +46,35 @@ import java.util.*;
  */
 @Service
 public class EgDeviceServiceImpl extends AbstractCrudService<EgDeviceMapper, EgDevice> implements EgDeviceService {
-    @Autowired
-    private MeetingFaceDeviceMapper meetingFaceDeviceMapper;
-    @Autowired
-    private MeetingFaceMapper meetingFaceMapper;
     @Autowired
     private EgDeviceMapper egDeviceMapper;
     @Autowired
+    private EgDeviceHeartbeatMapper egDeviceHeartbeatMapper;
+    @Autowired
     private RemoteIotTaskService remoteIotTaskService;
-
     @Autowired
     private RemoteTransferService remoteTransferService;
+    @Autowired
+    private EgDevicePersonBindMapper egDevicePersonBindMapper;
+    @Autowired
+    private EgRecordMapper egRecordMapper;
+    @Autowired
+    private SysPersonMapper sysPersonMapper;
 
     @Override
     public CommonPage<EgDevice> page(EgDeviceRequestVO requestVO){
         IPage<EgDevice> page = new Page<>(requestVO.getCurrent(),requestVO.getSize());
-        Integer tenantId ;
+        Integer tenantId;
 
-        if(StringUtils.isNotBlank(requestVO.getDomain())){
-            tenantId = egDeviceMapper.sysTenantId(requestVO.getDomain());
+        if(StringUtils.isNotBlank(requestVO.getDeviceCode())){
+            LambdaQueryWrapper<EgDevice> tempQueryWrapper = new LambdaQueryWrapper<>();
+            tempQueryWrapper.eq(EgDevice::getDeviceCode, requestVO.getDeviceCode());
+            EgDevice tempDevice = egDeviceMapper.selectOne(tempQueryWrapper);
+            if(tempDevice == null){
+                // 未查询到数据,返回空数组
+                return new CommonPage<>(new ArrayList<>(), 0L, requestVO.getSize(), requestVO.getCurrent());
+            }
+            tenantId = tempDevice.getTenantId();
         }else{
             tenantId = SecurityUtils.getTenantId();
         }
@@ -69,34 +85,70 @@ public class EgDeviceServiceImpl extends AbstractCrudService<EgDeviceMapper, EgD
                 .eq(null != requestVO.getServiceStatus(),EgDevice::getServiceStatus,requestVO.getServiceStatus())
                 .eq(null != requestVO.getId(),EgDevice::getId,requestVO.getId())
                 .eq(null != requestVO.getDeviceUuid(),EgDevice::getDeviceUuid, requestVO.getDeviceUuid())
+                .eq(StringUtils.isNotBlank(requestVO.getDeviceCode()),EgDevice::getDeviceCode, requestVO.getDeviceCode())
                 .eq(EgDevice::getTenantId,tenantId)
                 .orderByDesc(EgDevice::getId);
         page = this.page(page,queryWrapper);
+
         if(!page.getRecords().isEmpty()){
+            // 检查设备心跳
+            List<String> validDeviceCodes = new ArrayList<>();
+            Map<String, EgDevice> deviceCodeMap = new HashMap<>();
+            for (EgDevice device : page.getRecords()) {
+                String deviceCode = device.getDeviceCode();
+                if (StringUtils.isNotBlank(deviceCode)) {
+                    validDeviceCodes.add(deviceCode);
+                    deviceCodeMap.put(deviceCode, device);
+                } else {
+                    // deviceCode为空,直接设置为离线
+                    device.setDeviceStatus(0);
+                }
+            }
 
-            LambdaQueryWrapper<MeetingFace> meetingFaceQuery = Wrappers.lambdaQuery();
-            meetingFaceQuery.select(MeetingFace::getFid,MeetingFace::getCreateTime,MeetingFace::getVefNum,MeetingFace::getFaceName,MeetingFace::getRemark,MeetingFace::getFaceStatus,MeetingFace::getCardNum,MeetingFace::getBindDevice,MeetingFace::getDeptId,MeetingFace::getTenantId,MeetingFace::getUserId)
-                    .eq(MeetingFace::getTenantId,tenantId);
-            List<MeetingFace> meetingAllFaceList = meetingFaceMapper.selectList(meetingFaceQuery);
-
-            for (int i = 0; i < page.getRecords().size(); i++) {
-                if(Objects.nonNull(page.getRecords().get(i).getBindFace()) ||StringUtils.isNotBlank(page.getRecords().get(i).getBindFace())){
-                    String[] fidListStr = page.getRecords().get(i).getBindFace().split(",");
-                    Integer[] fidList = Arrays.stream(fidListStr).map(Integer::parseInt).toArray(Integer[]::new);
-
-                    List<MeetingFace> meetingFaceList = new ArrayList<>();
-                    for (int j = 0; j < fidList.length; j++) {
-                        for (int k = 0; k < meetingAllFaceList.size(); k++) {
-                            if(fidList[j] == meetingAllFaceList.get(k).getFid()){
-                                meetingFaceList.add(meetingAllFaceList.get(k));
-                                break;
+            // 批量查询心跳数据
+            if (!validDeviceCodes.isEmpty()) {
+                LambdaQueryWrapper<EgDeviceHeartbeat> heartbeatQueryWrapper = Wrappers.lambdaQuery();
+                heartbeatQueryWrapper.in(EgDeviceHeartbeat::getDeviceCode, validDeviceCodes)
+                        .orderByDesc(EgDeviceHeartbeat::getCreateTime);
+                List<EgDeviceHeartbeat> heartbeatList = egDeviceHeartbeatMapper.selectList(heartbeatQueryWrapper);
+                
+                // 构建deviceCode到最新心跳记录的映射,并同时设置deviceStatus
+                Map<String, EgDeviceHeartbeat> deviceHeartbeatMap = new HashMap<>();
+                for (EgDeviceHeartbeat heartbeat : heartbeatList) {
+                    String deviceCode = heartbeat.getDeviceCode();
+                    if (StringUtils.isNotBlank(deviceCode)) {
+                        // 如果已存在,保留最新的(因为已按createTime降序排列)
+                        if (deviceHeartbeatMap.putIfAbsent(deviceCode, heartbeat) == null) {
+                            // 首次遇到该deviceCode,从心跳表直接获取deviceStatus并设置到EgDevice
+                            EgDevice device = deviceCodeMap.get(deviceCode);
+                            if (device != null && heartbeat.getDeviceStatus() != null) {
+                                device.setDeviceStatus(heartbeat.getDeviceStatus());
                             }
                         }
                     }
-                    page.getRecords().get(i).setMeetingFaceList(meetingFaceList);
                 }
-
+                
+                // 如果未找到对应的设备心跳记录,则设置DeviceStatus为0(离线)
+                for (String deviceCode : validDeviceCodes) {
+                    if (!deviceHeartbeatMap.containsKey(deviceCode)) {
+                        EgDevice device = deviceCodeMap.get(deviceCode);
+                        if (device != null) {
+                            device.setDeviceStatus(0);
+                        }
+                    }
+                }
+            } else {
+                // 如果没有有效的deviceCode,将所有设备的deviceStatus设置为0
+                for (EgDevice device : page.getRecords()) {
+                    device.setDeviceStatus(0);
+                }
             }
+
+        }
+
+        // 查询并填充绑定人员信息(来自 eg_device_person_bind,person_id 逗号分隔后写入 bindPerson 字段)
+        if (!page.getRecords().isEmpty()) {
+            fillBindPerson(page.getRecords());
         }
 
         return new CommonPage<>(page.getRecords(),page.getTotal(),requestVO.getSize(),requestVO.getCurrent());
@@ -105,86 +157,150 @@ public class EgDeviceServiceImpl extends AbstractCrudService<EgDeviceMapper, EgD
     @Override
     public CommonPage<EgDevice> wePage(EgDeviceRequestVO requestVO){
         long userId = SecurityUtils.getUserId();
-        //人员设备权限校验,校验通过,可以下发命令控制设备
-        Integer fid = baseMapper.getMeetingFaceData(userId);
-//        if(fid == null){
-//            throw new BusinessException("人脸卡号信息未注册");
-//        }
-        Integer[] deviceFid = baseMapper.getMeetingFaceDeviceList(fid);
-//        if(deviceFid.length == 0){
-//            throw new BusinessException("人员未绑定设备,请检查");
-//        }
+        // 通过 sys_user_person -> sys_person -> eg_device_person_bind -> eg_device 查询当前用户绑定的设备ID列表
+        List<Integer> deviceIds = egDevicePersonBindMapper.selectDeviceIdsByUserId(userId);
+        if (CollectionUtils.isEmpty(deviceIds)) {
+            // 未绑定任何设备,返回空分页
+            return new CommonPage<>(new ArrayList<>(), 0L, requestVO.getSize(), requestVO.getCurrent());
+        }
 
         IPage<EgDevice> page = new Page<>(requestVO.getCurrent(),requestVO.getSize());
-        if(deviceFid.length > 0){
-            LambdaQueryWrapper<EgDevice> queryWrapper = Wrappers.lambdaQuery();
-            queryWrapper.like(StringUtils.isNotBlank(requestVO.getDeviceName()),EgDevice::getDeviceName,requestVO.getDeviceName())
-                    .like(StringUtils.isNotBlank(requestVO.getInstallAddress()),EgDevice::getInstallAddress,requestVO.getInstallAddress())
-                    .eq(null != requestVO.getServiceStatus(),EgDevice::getServiceStatus,requestVO.getServiceStatus())
-                    .eq(null != requestVO.getId(),EgDevice::getId,requestVO.getId())
-                    .in(EgDevice::getId,deviceFid)
-                    .eq(EgDevice::getTenantId,SecurityUtils.getTenantId())
-                    .orderByDesc(EgDevice::getId);
-            page = this.page(page,queryWrapper);
-        }
+        LambdaQueryWrapper<EgDevice> queryWrapper = Wrappers.lambdaQuery();
+        queryWrapper.like(StringUtils.isNotBlank(requestVO.getDeviceName()),EgDevice::getDeviceName,requestVO.getDeviceName())
+                .like(StringUtils.isNotBlank(requestVO.getInstallAddress()),EgDevice::getInstallAddress,requestVO.getInstallAddress())
+                .eq(null != requestVO.getServiceStatus(),EgDevice::getServiceStatus,requestVO.getServiceStatus())
+                .eq(null != requestVO.getId(),EgDevice::getId,requestVO.getId())
+                .in(EgDevice::getId, deviceIds)
+                .eq(EgDevice::getTenantId,SecurityUtils.getTenantId())
+                .orderByDesc(EgDevice::getId);
+        page = this.page(page,queryWrapper);
 
+        // 查询并填充绑定人员信息
+        if (!page.getRecords().isEmpty()) {
+            fillBindPerson(page.getRecords());
+        }
 
         return new CommonPage<>(page.getRecords(),page.getTotal(),requestVO.getSize(),requestVO.getCurrent());
     }
 
     @Override
-    public void add(EgDevice egDevice){
-        if(checkNameUnique(egDevice)){
-            throw new BusinessException("新增门禁门号设备'"+egDevice.getDeviceId()+","+egDevice.getEgNumber()+"'失败,设备已存在");
+    public void add(EgDevice egDevice) {
+        LocalDateTime now = LocalDateTime.now();
+        
+        // 1. 校验设备名称唯一性
+        if (checkDeviceNameUnique(egDevice)) {
+            throw new BusinessException("新增门禁设备'" + egDevice.getDeviceName() + "'失败,设备已存在");
         }
-        if(checkDeviceNameUnique(egDevice)){
-            throw new BusinessException("新增门禁设备'"+egDevice.getDeviceName()+"'失败,设备已存在");
+
+        // 2. 校验设备IP是否已被绑定
+        if (StringUtils.isNotBlank(egDevice.getDeviceIp())) {
+            LambdaQueryWrapper<EgDevice> ipCheckWrapper = Wrappers.lambdaQuery();
+            ipCheckWrapper.eq(EgDevice::getDeviceIp, egDevice.getDeviceIp())
+                    .eq(EgDevice::getTenantId, SecurityUtils.getTenantId());
+            EgDevice existingDeviceByIp = this.getOne(ipCheckWrapper);
+            if (existingDeviceByIp != null) {
+                throw new BusinessException("设备IP'" + egDevice.getDeviceIp() + "'已被设备'" 
+                        + existingDeviceByIp.getDeviceName() + "'绑定,请更换IP或检查设备配置!");
+            }
+        }
+
+        // 3. 若未传deviceCode但传了deviceIp,尝试从心跳表中获取deviceCode
+        if (StringUtils.isBlank(egDevice.getDeviceCode()) && StringUtils.isNotBlank(egDevice.getDeviceIp())) {
+            LambdaQueryWrapper<EgDeviceHeartbeat> heartbeatCheckWrapper = Wrappers.lambdaQuery();
+            heartbeatCheckWrapper.eq(EgDeviceHeartbeat::getIpAddr, egDevice.getDeviceIp())
+                    .eq(EgDeviceHeartbeat::getDeviceStatus, 1);
+            EgDeviceHeartbeat heartbeat = egDeviceHeartbeatMapper.selectOne(heartbeatCheckWrapper);
+            if (heartbeat != null && StringUtils.isNotBlank(heartbeat.getDeviceCode())) {
+                egDevice.setDeviceCode(heartbeat.getDeviceCode());
+            }
         }
 
+        // 4. 校验设备编码是否已被绑定
+        if (StringUtils.isNotBlank(egDevice.getDeviceCode())) {
+            LambdaQueryWrapper<EgDevice> codeCheckWrapper = Wrappers.lambdaQuery();
+            codeCheckWrapper.eq(EgDevice::getDeviceCode, egDevice.getDeviceCode())
+                    .eq(EgDevice::getTenantId, SecurityUtils.getTenantId());
+            EgDevice existingDeviceByCode = this.getOne(codeCheckWrapper);
+            if (existingDeviceByCode != null) {
+                throw new BusinessException("设备编码'" + egDevice.getDeviceCode() + "'已被设备'" 
+                        + existingDeviceByCode.getDeviceName() + "'绑定,请更换设备编码或检查设备配置!");
+            }
+        }
+
+        // 5. 设置设备基本信息
         egDevice.setDeviceUuid(UUIDUtils.uuid());
         egDevice.setCreateBy(SecurityUtils.getUsername());
-        egDevice.setCreateTime(LocalDateTime.now());
+        egDevice.setCreateTime(now);
         egDevice.setTenantId(SecurityUtils.getTenantId());
+        egDevice.setOpenMode("人脸");
+        egDevice.setWorkStatus("4");
 
+        // 6. 保存设备
         this.save(egDevice);
 
-        String[] fids = new String[0];
-        if(Objects.nonNull(egDevice.getBindFace()) || StringUtils.isNotBlank(egDevice.getBindFace())){
-            fids = egDevice.getBindFace().split(",");
-        }
-        if(fids.length > 0){
-            for (int i = 0; i < fids.length; i++) {
-                egDeviceMapper.insertMeetingFaceDevice(Integer.parseInt(fids[i]),egDevice.getId());
-            }
+        // 7. 异步调用远程服务添加设备信息,静默处理异常,不影响设备保存成功
+        String deviceUuid = egDevice.getDeviceUuid();
+        String deviceName = egDevice.getDeviceName();
+        if (StringUtils.isNotBlank(deviceUuid) && StringUtils.isNotBlank(deviceName)) {
+            String installAddress = StringUtils.isNotBlank(egDevice.getInstallAddress()) 
+                    ? egDevice.getInstallAddress() 
+                    : "";
+            Integer serviceStatus = egDevice.getServiceStatus() != null 
+                    ? egDevice.getServiceStatus() 
+                    : 1;
+            
+            CompletableFuture.runAsync(() -> {
+                try {
+                    remoteIotTaskService.addDeviceInfo("502_USKY", deviceUuid, deviceUuid, 
+                            deviceName, installAddress, serviceStatus);
+                } catch (Exception e) {
+                    // 远程服务调用失败,静默处理,不影响设备保存成功
+                }
+            });
         }
-
-        remoteIotTaskService.addDeviceInfo("502_USKY", egDevice.getDeviceUuid(),egDevice.getDeviceId()+egDevice.getEgNumber(),egDevice.getDeviceName(),egDevice.getInstallAddress(),egDevice.getServiceStatus());
     }
 
     @Override
     public void update(EgDevice egDevice) {
-
-        String[] fids = new String[0];
-        if(Objects.nonNull(egDevice.getBindFace()) || StringUtils.isNotBlank(egDevice.getBindFace())){
-            fids = egDevice.getBindFace().split(",");
-
-            egDeviceMapper.deleteMeetingFaceDevice(egDevice.getId());
-        }else{
-            EgDevice one = this.getById(egDevice.getId());
-            egDevice.setBindFace(one.getBindFace());
+        if(checkDeviceNameUnique(egDevice)){
+            throw new BusinessException("修改门禁设备'"+egDevice.getDeviceName()+"'失败,设备名称已存在");
         }
-        if(fids.length > 0){
-            for (int i = 0; i < fids.length; i++) {
-                egDeviceMapper.insertMeetingFaceDevice(Integer.parseInt(fids[i]),egDevice.getId());
+
+        // 校验设备IP是否已被其他设备绑定(排除当前设备)
+        if (StringUtils.isNotBlank(egDevice.getDeviceIp())) {
+            LambdaQueryWrapper<EgDevice> ipCheckWrapper = Wrappers.lambdaQuery();
+            ipCheckWrapper.eq(EgDevice::getDeviceIp, egDevice.getDeviceIp())
+                    .eq(EgDevice::getTenantId, SecurityUtils.getTenantId())
+                    .ne(EgDevice::getId, egDevice.getId());
+            EgDevice existingDeviceByIp = this.getOne(ipCheckWrapper);
+            if (existingDeviceByIp != null) {
+                throw new BusinessException("设备IP'" + egDevice.getDeviceIp() + "'已被设备'" 
+                        + existingDeviceByIp.getDeviceName() + "'绑定,请更换IP或检查设备配置!");
             }
         }
 
-
-        if(checkNameUnique(egDevice)){
-            throw new BusinessException("修改门禁门号设备'"+egDevice.getDeviceId()+","+egDevice.getEgNumber()+"'失败,设备已存在");
+        // 若未传deviceCode但传了deviceIp,尝试从心跳表中获取deviceCode
+        if (StringUtils.isBlank(egDevice.getDeviceCode()) && StringUtils.isNotBlank(egDevice.getDeviceIp())) {
+            LambdaQueryWrapper<EgDeviceHeartbeat> heartbeatCheckWrapper = Wrappers.lambdaQuery();
+            heartbeatCheckWrapper.eq(EgDeviceHeartbeat::getIpAddr, egDevice.getDeviceIp())
+                    .eq(EgDeviceHeartbeat::getDeviceStatus, 1);
+            EgDeviceHeartbeat heartbeat = egDeviceHeartbeatMapper.selectOne(heartbeatCheckWrapper);
+            if (heartbeat != null && StringUtils.isNotBlank(heartbeat.getDeviceCode())) {
+                egDevice.setDeviceCode(heartbeat.getDeviceCode());
+            }
         }
-        if(checkDeviceNameUnique(egDevice)){
-            throw new BusinessException("新增门禁设备'"+egDevice.getDeviceName()+"'失败,设备已存在");
+
+        // 校验设备编码是否已被其他设备绑定(排除当前设备)
+        if (StringUtils.isNotBlank(egDevice.getDeviceCode())) {
+            LambdaQueryWrapper<EgDevice> codeCheckWrapper = Wrappers.lambdaQuery();
+            codeCheckWrapper.eq(EgDevice::getDeviceCode, egDevice.getDeviceCode())
+                    .eq(EgDevice::getTenantId, SecurityUtils.getTenantId())
+                    .ne(EgDevice::getId, egDevice.getId());
+            EgDevice existingDeviceByCode = this.getOne(codeCheckWrapper);
+            if (existingDeviceByCode != null) {
+                throw new BusinessException("设备编码'" + egDevice.getDeviceCode() + "'已被设备'" 
+                        + existingDeviceByCode.getDeviceName() + "'绑定,请更换设备编码或检查设备配置!");
+            }
         }
 
         egDevice.setUpdateBy(SecurityUtils.getUsername());
@@ -199,9 +315,7 @@ public class EgDeviceServiceImpl extends AbstractCrudService<EgDeviceMapper, EgD
         EgDevice one = this.getById(egDevice.getId());
         egDevice.setBindFace(one.getBindFace());
 
-        if(checkNameUnique(egDevice)){
-            throw new BusinessException("更新门禁设备附加功能'"+egDevice.getDeviceId()+"'失败,设备已存在");
-        }
+
         if(checkDeviceNameUnique(egDevice)){
             throw new BusinessException("新增门禁设备'"+egDevice.getDeviceName()+"'失败,设备已存在");
         }
@@ -217,16 +331,72 @@ public class EgDeviceServiceImpl extends AbstractCrudService<EgDeviceMapper, EgD
         EgDevice egDevice = this.getById(id);
         Optional.ofNullable(egDevice).orElseThrow(() -> new BusinessException("门禁设备信息不存在"));
 
-        LambdaQueryWrapper<MeetingFaceDevice> queryWrapper = Wrappers.lambdaQuery();
-        queryWrapper.eq(MeetingFaceDevice::getDeviceId,id);
-        Integer count = meetingFaceDeviceMapper.selectCount(queryWrapper);
-        if(count > 0){
+        // 校验 eg_device_person_bind 是否存在绑定人员
+        Integer personBindCount = egDevicePersonBindMapper.countByDeviceId(id);
+        if (personBindCount != null && personBindCount > 0) {
             throw new BusinessException("已绑定人员不能删除");
         }
 
         this.removeById(id);
 
-        remoteIotTaskService.deleteDeviceInfo(egDevice.getDeviceUuid());
+        // 异步调用远程服务删除设备信息,静默处理异常,不影响设备删除
+        String deviceUuid = egDevice.getDeviceUuid();
+        if (StringUtils.isNotBlank(deviceUuid)) {
+            CompletableFuture.runAsync(() -> {
+                try {
+                    remoteIotTaskService.deleteDeviceInfo(deviceUuid);
+                } catch (Exception e) {
+                    // 远程服务调用失败,静默处理,不影响设备删除
+                }
+            });
+        }
+    }
+
+    /**
+     * 根据设备ID集合查询 eg_device_person_bind,将 person_id 按逗号拼接后写入设备的 bindPerson 字段
+     */
+    private void fillBindPerson(List<EgDevice> devices) {
+        if (CollectionUtils.isEmpty(devices)) {
+            return;
+        }
+
+        List<Integer> deviceIds = new ArrayList<>();
+        for (EgDevice device : devices) {
+            if (device != null && device.getId() != null) {
+                deviceIds.add(device.getId());
+            }
+        }
+
+        if (deviceIds.isEmpty()) {
+            return;
+        }
+
+        List<EgDevicePersonBind> bindList = egDevicePersonBindMapper.selectByDeviceIds(deviceIds);
+        if (CollectionUtils.isEmpty(bindList)) {
+            return;
+        }
+
+        Map<Integer, List<String>> devicePersonMap = new HashMap<>();
+        for (EgDevicePersonBind bind : bindList) {
+            if (bind.getDeviceId() == null || bind.getPersonId() == null) {
+                continue;
+            }
+            devicePersonMap
+                    .computeIfAbsent(bind.getDeviceId(), k -> new ArrayList<>())
+                    .add(String.valueOf(bind.getPersonId()));
+        }
+
+        for (EgDevice device : devices) {
+            if (device == null || device.getId() == null) {
+                continue;
+            }
+            List<String> personIds = devicePersonMap.get(device.getId());
+            if (personIds != null && !personIds.isEmpty()) {
+                device.setBindPerson(String.join(",", personIds));
+            } else {
+                device.setBindPerson(null);
+            }
+        }
     }
 
     @Override
@@ -234,7 +404,6 @@ public class EgDeviceServiceImpl extends AbstractCrudService<EgDeviceMapper, EgD
         Integer id = null == egDevice.getId() ? -1 : egDevice.getId();
         LambdaQueryWrapper<EgDevice> queryWrapper = Wrappers.lambdaQuery();
         queryWrapper.eq(EgDevice::getDeviceId,egDevice.getDeviceId())
-                .eq(EgDevice::getEgNumber,egDevice.getEgNumber())
                 .eq(EgDevice::getTenantId, SecurityUtils.getTenantId());
         EgDevice one = this.getOne(queryWrapper);
         return null != one && !Objects.equals(one.getId(),id);
@@ -251,56 +420,179 @@ public class EgDeviceServiceImpl extends AbstractCrudService<EgDeviceMapper, EgD
     }
 
     @Override
-    public Map<String,Object> control(String productCode, String deviceUuid, String commandCode, String commandValue, String domain, Long userId, String userName, Integer categoryType, String gatewayUuid){
-        Integer tenantId;
-        long commandUserId;
-        String commandUserName;
-        if(StringUtils.isNotBlank(domain)){
-            tenantId = baseMapper.sysTenantId(domain);
-            commandUserId = userId;
-            commandUserName = userName;
-        }else{
-            tenantId = SecurityUtils.getTenantId();
-            commandUserId = SecurityUtils.getUserId();
-            commandUserName = SecurityUtils.getUsername();
+    public Map<String,Object> control(String productCode, String deviceUuid, String commandCode, String commandValue, Long userId, String userName, Integer categoryType, String gatewayUuid, Boolean skipCheck, Integer passType){
+        // 1. 查询设备
+        EgDevice device = this.getOne(Wrappers.<EgDevice>lambdaQuery()
+                .select(EgDevice::getId, EgDevice::getTenantId)
+                .eq(EgDevice::getDeviceUuid, deviceUuid));
+        if (device == null) {
+            throw new BusinessException("设备未注册,请先注册");
         }
 
-        //人员设备权限校验,校验通过,可以下发命令控制设备
-        Integer fid = baseMapper.getMeetingFaceData(commandUserId);
-        if(fid == null){
-            throw new BusinessException("人脸卡号信息未注册");
-        }
-        Integer[] deviceFid = baseMapper.getMeetingFaceDeviceList(fid);
-        if(deviceFid.length == 0){
-            throw new BusinessException("人员未绑定设备,请检查");
-        }
+        Integer tenantId = device.getTenantId();
+        long commandUserId = userId != null ? userId : SecurityUtils.getUserId();
+        String commandUserName = userName != null ? userName : SecurityUtils.getUsername();
 
-        LambdaQueryWrapper<EgDevice> queryWrapper = Wrappers.lambdaQuery();
-        queryWrapper.select(EgDevice::getId)
-                .eq(EgDevice::getDeviceUuid,deviceUuid);
-        EgDevice one = this.getOne(queryWrapper);
-        if(one != null){
-            boolean exist = Arrays.asList(deviceFid).contains(one.getId());
-            if(!exist){
+        // 2. 权限校验
+        if (!Boolean.TRUE.equals(skipCheck)) {
+            Integer bindCount = egDevicePersonBindMapper.countByUserIdAndDeviceId(commandUserId, device.getId());
+            if (bindCount == null || bindCount == 0) {
+                saveRecord(device.getId(), tenantId, commandUserName, passType, "失败:暂无权限");
                 throw new BusinessException("暂无权限");
             }
-        }else{
-            throw new BusinessException("设备未注册,请先注册");
         }
 
-
+        // 3. 构建下发命令参数
+        Map<String,Object> params = new HashMap<>();
+        params.put("commandCode", commandCode);
+        params.put("commandValue", commandValue);
         Map<String,Object> map = new HashMap<>();
-        map.put("method","control");
+        map.put("method", "control");
         map.put("deviceUuid", deviceUuid);
-        Map<String,Object> map1 = new HashMap<>();
-        map1.put("commandCode",commandCode);
-        map1.put("commandValue",commandValue);
-        map.put("params",map1);
+        map.put("params", params);
+
+        // 4. 下发命令并记录结果
+        String targetUuid = categoryType == 3 ? gatewayUuid : deviceUuid;
+        Map<String,Object> result = null;
+        String passResult = "下发命令失败";
+        try {
+            result = remoteTransferService.deviceControl(productCode, targetUuid, JSON.toJSONString(map), tenantId, commandUserId, commandUserName);
+            if (result == null || !Integer.valueOf(200).equals(result.get("code"))) {
+                passResult = result != null && result.get("message") != null ? result.get("message").toString() : "下发命令失败";
+                throw new BusinessException(passResult);
+            }
+            passResult = result.get("message") != null ? result.get("message").toString() : "下发命令成功";
+            return result;
+        } finally {
+            saveRecord(device.getId(), tenantId, commandUserName, passType, passResult);
+        }
+    }
 
-        if(categoryType == 3){
-            return remoteTransferService.deviceControl(productCode, gatewayUuid, JSON.toJSONString(map), tenantId, commandUserId, commandUserName);
-        }else{
-            return remoteTransferService.deviceControl(productCode, deviceUuid, JSON.toJSONString(map), tenantId, commandUserId, commandUserName);
+    /**
+     * 插入通行记录,失败不影响主流程
+     */
+    private void saveRecord(Integer deviceId, Integer tenantId, String userName, Integer passType, String passResult) {
+        try {
+            EgRecord record = new EgRecord();
+            record.setEgDeviceId(deviceId);
+            record.setTenantId(tenantId);
+            record.setPassTime(LocalDateTime.now());
+            record.setCreateTime(LocalDateTime.now());
+            record.setPassResult(passResult);
+            record.setUserName(userName);
+            record.setPassType(passType);
+            egRecordMapper.insert(record);
+        } catch (Exception ignored) {}
+    }
+
+    @Override
+    public CommonPage<EgDeviceBindFacePersonVO> pageBindFacePersons(EgDeviceBindFacePersonQueryVO queryVO) {
+        Integer cur = queryVO.getCurrent();
+        Integer sz = queryVO.getSize();
+        IPage<SysPerson> page = new Page<>(cur, sz);
+
+        List<Integer> deviceIds = new ArrayList<>();
+        if (StringUtils.isNotBlank(queryVO.getDeviceCode())) {
+            EgDevice tempDevice = egDeviceMapper.selectOne(Wrappers.<EgDevice>lambdaQuery()
+                    .eq(EgDevice::getDeviceCode, queryVO.getDeviceCode()));
+            if (tempDevice == null) {
+                return new CommonPage<>(new ArrayList<>(), 0L, sz, cur);
+            }
+            EgDevice device = egDeviceMapper.selectOne(Wrappers.<EgDevice>lambdaQuery()
+                    .eq(EgDevice::getDeviceCode, queryVO.getDeviceCode())
+                    .eq(EgDevice::getTenantId, tempDevice.getTenantId()));
+            if (device == null || device.getId() == null) {
+                return new CommonPage<>(new ArrayList<>(), 0L, sz, cur);
+            }
+            deviceIds.add(device.getId());
+        } else {
+            List<EgDevice> devices = egDeviceMapper.selectList(Wrappers.<EgDevice>lambdaQuery()
+                    .eq(EgDevice::getTenantId, SecurityUtils.getTenantId()));
+            if (!CollectionUtils.isEmpty(devices)) {
+                for (EgDevice d : devices) {
+                    if (d != null && d.getId() != null) {
+                        deviceIds.add(d.getId());
+                    }
+                }
+            }
+        }
+
+        if (deviceIds.isEmpty()) {
+            return new CommonPage<>(new ArrayList<>(), 0L, sz, cur);
+        }
+
+        List<EgDevicePersonBind> bindList = egDevicePersonBindMapper.selectByDeviceIds(deviceIds);
+        LinkedHashSet<Integer> personIdSet = new LinkedHashSet<>();
+        if (!CollectionUtils.isEmpty(bindList)) {
+            for (EgDevicePersonBind b : bindList) {
+                if (b.getPersonId() != null) {
+                    personIdSet.add(b.getPersonId());
+                }
+            }
+        }
+        if (personIdSet.isEmpty()) {
+            return new CommonPage<>(new ArrayList<>(), 0L, sz, cur);
+        }
+        List<Integer> personIds = new ArrayList<>(personIdSet);
+
+        LambdaQueryWrapper<SysPerson> personWrapper = Wrappers.lambdaQuery();
+        personWrapper.in(SysPerson::getId, personIds);
+        if (StringUtils.isNotBlank(queryVO.getFullName())) {
+            personWrapper.like(SysPerson::getFullName, queryVO.getFullName());
+        }
+        Integer faceStatusFilter = queryVO.getFaceStatus();
+        if (faceStatusFilter != null) {
+            personWrapper.eq(SysPerson::getFaceStatus, faceStatusFilter.byteValue());
+        }
+        personWrapper.orderByAsc(SysPerson::getId);
+        page = sysPersonMapper.selectPage(page, personWrapper);
+
+        List<SysPerson> records = page.getRecords();
+        List<EgDeviceBindFacePersonVO> rows = new ArrayList<>(records.size());
+        for (SysPerson p : records) {
+            EgDeviceBindFacePersonVO vo = new EgDeviceBindFacePersonVO();
+            vo.setFullName(p.getFullName());
+            vo.setAge(p.getAge());
+            vo.setGender(p.getGender());
+            vo.setLinkPhone(p.getLinkPhone());
+            vo.setFaceBase(p.getFaceBase());
+            vo.setVefNum(p.getVefNum());
+            vo.setFaceStatus(p.getFaceStatus());
+            vo.setCardNum(p.getCardNum());
+            rows.add(vo);
+        }
+        return new CommonPage<>(rows, page.getTotal(), sz, cur);
+    }
+
+    @Override
+    public void bindPerson(Integer deviceId, String personIds, Integer isLoginNotify) {
+        if (deviceId == null) {
+            throw new BusinessException("设备ID不能为空");
+        }
+
+        // 先校验设备是否存在
+        EgDevice device = this.getById(deviceId);
+        Optional.ofNullable(device).orElseThrow(() -> new BusinessException("门禁设备信息不存在"));
+
+        // 先清空原有绑定关系
+        egDevicePersonBindMapper.deleteByDeviceId(deviceId);
+
+        // 为空则视为解绑所有人员
+        if (!StringUtils.isNotBlank(personIds)) {
+            return;
+        }
+
+        String[] personIdArr = personIds.split(",");
+        int loginNotify = (isLoginNotify == null) ? 0 : isLoginNotify;
+        for (String personIdStr : personIdArr) {
+            if (!StringUtils.isNotBlank(personIdStr)) {
+                continue;
+            }
+            EgDevicePersonBind bind = new EgDevicePersonBind();
+            bind.setDeviceId(deviceId);
+            bind.setPersonId(Integer.parseInt(personIdStr.trim()));
+            bind.setIsLoginNotify(loginNotify);
+            egDevicePersonBindMapper.insert(bind);
         }
     }
 }

+ 17 - 10
service-eg/service-eg-biz/src/main/java/com/usky/eg/service/impl/EgRecordServiceImpl.java

@@ -47,23 +47,31 @@ public class EgRecordServiceImpl extends AbstractCrudService<EgRecordMapper, EgR
     @Override
     public void add(EgRecord egRecord){
         Integer tenantId;
-        if(StringUtils.isNotBlank(egRecord.getDomain())){
-            tenantId = egDeviceMapper.sysTenantId(egRecord.getDomain());
-        }else{
-            tenantId = SecurityUtils.getTenantId();
-        }
-
+        
+        // 通过deviceUuid查询设备获取tenantId
         LambdaQueryWrapper<EgDevice> queryWrapper = Wrappers.lambdaQuery();
         if(StringUtils.isBlank(egRecord.getDeviceUuid())){
             throw new BusinessException("设备Uuid不能为空");
         }
-        queryWrapper.eq(EgDevice::getDeviceUuid,egRecord.getDeviceUuid());
+        queryWrapper.select(EgDevice::getId, EgDevice::getTenantId)
+                .eq(EgDevice::getDeviceUuid,egRecord.getDeviceUuid());
         EgDevice one = egDeviceService.getOne(queryWrapper);
+        if(one == null){
+            throw new BusinessException("设备未注册,请先注册");
+        }
+        tenantId = one.getTenantId();
         egRecord.setEgDeviceId(one.getId());
-
         egRecord.setCreateTime(LocalDateTime.now());
         egRecord.setTenantId(tenantId);
-        egRecord.setDeptId(SecurityUtils.getLoginUser().getSysUser().getDeptId().intValue());
+        
+        // 安全获取部门ID
+        Integer deptId = null;
+        if(SecurityUtils.getLoginUser() != null 
+                && SecurityUtils.getLoginUser().getSysUser() != null 
+                && SecurityUtils.getLoginUser().getSysUser().getDeptId() != null){
+            deptId = SecurityUtils.getLoginUser().getSysUser().getDeptId().intValue();
+        }
+        egRecord.setDeptId(deptId);
 
         this.save(egRecord);
     }
@@ -114,7 +122,6 @@ public class EgRecordServiceImpl extends AbstractCrudService<EgRecordMapper, EgR
             }
         }
 
-
         return new CommonPage<>(page.getRecords(),page.getTotal(),requestVO.getSize(),requestVO.getCurrent());
     }
 }

+ 21 - 0
service-eg/service-eg-biz/src/main/java/com/usky/eg/service/vo/EgDeviceBindFacePersonQueryVO.java

@@ -0,0 +1,21 @@
+package com.usky.eg.service.vo;
+
+import lombok.Data;
+
+import java.io.Serializable;
+
+/** 设备绑定人员分页查询参数 */
+@Data
+public class EgDeviceBindFacePersonQueryVO implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    private Integer current;
+    private Integer size;
+    /** 设备编码,空则当前租户全部设备 */
+    private String deviceCode;
+    /** 姓名模糊,可空 */
+    private String fullName;
+    /** 人脸状态 0/1,可空 */
+    private Integer faceStatus;
+}

+ 21 - 0
service-eg/service-eg-biz/src/main/java/com/usky/eg/service/vo/EgDeviceBindFacePersonVO.java

@@ -0,0 +1,21 @@
+package com.usky.eg.service.vo;
+
+import lombok.Data;
+
+import java.io.Serializable;
+
+/** 绑定人员列表项 */
+@Data
+public class EgDeviceBindFacePersonVO implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    private String fullName;
+    private Integer age;
+    private Integer gender;
+    private String linkPhone;
+    private String faceBase;
+    private Integer vefNum;
+    private Byte faceStatus;
+    private String cardNum;
+}

+ 9 - 0
service-eg/service-eg-biz/src/main/java/com/usky/eg/service/vo/EgDeviceRequestVO.java

@@ -45,4 +45,13 @@ public class EgDeviceRequestVO implements Serializable {
      * 设备uuid
      */
     private String deviceUuid;
+
+    /**
+     * 设备编码
+     */
+    private String deviceCode;
+    /**
+     * 设备状态
+     */
+    private Integer deviceStatus;
 }

+ 20 - 0
service-eg/service-eg-biz/src/main/resources/mapper/eg/EgDeviceHeartbeatMapper.xml

@@ -0,0 +1,20 @@
+<?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.usky.eg.mapper.EgDeviceHeartbeatMapper">
+
+    <!-- 通用查询映射结果 -->
+    <resultMap id="BaseResultMap" type="com.usky.eg.domain.EgDeviceHeartbeat">
+        <id column="id" property="id"/>
+        <result column="device_code" property="deviceCode"/>
+        <result column="ip_addr" property="ipAddr"/>
+        <result column="mac_addr" property="macAddr"/>
+        <result column="device_type" property="deviceType"/>
+        <result column="create_time" property="createTime"/>
+        <result column="model" property="model"/>
+        <result column="manu_facturer"  property="manuFacturer"/>
+        <result column="version" property="version"/>
+        <result column="sdk" property="sdk"/>
+        <result column="device_status" property="deviceStatus"/>
+    </resultMap>
+
+</mapper>

+ 1 - 2
service-eg/service-eg-biz/src/main/resources/mapper/eg/EgDeviceMapper.xml

@@ -11,9 +11,8 @@
         <result column="install_address" property="installAddress" />
         <result column="service_status" property="serviceStatus" />
         <result column="device_ip" property="deviceIp" />
-        <result column="device_port" property="devicePort" />
-        <result column="eg_number" property="egNumber" />
         <result column="bind_face" property="bindFace" />
+        <result column="bind_person" property="bindPerson" />
         <result column="create_by" property="createBy" />
         <result column="update_by" property="updateBy" />
         <result column="create_time" property="createTime" />

+ 62 - 0
service-eg/service-eg-biz/src/main/resources/mapper/eg/EgDevicePersonBindMapper.xml

@@ -0,0 +1,62 @@
+<?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.usky.eg.mapper.EgDevicePersonBindMapper">
+
+    <resultMap id="BaseResultMap" type="com.usky.eg.domain.EgDevicePersonBind">
+        <result column="device_id" property="deviceId" />
+        <result column="person_id" property="personId" />
+        <result column="is_login_notify" property="isLoginNotify" />
+    </resultMap>
+
+    <select id="selectByDeviceIds" resultMap="BaseResultMap">
+        select device_id, person_id, is_login_notify
+        from eg_device_person_bind
+        <where>
+            <if test="deviceIds != null and deviceIds.size() > 0">
+                and device_id in
+                <foreach collection="deviceIds" item="id" open="(" separator="," close=")">
+                    #{id}
+                </foreach>
+            </if>
+        </where>
+    </select>
+
+    <select id="countByDeviceId" resultType="java.lang.Integer">
+        select count(1)
+        from eg_device_person_bind
+        where device_id = #{deviceId}
+    </select>
+
+    <!--
+        通过 sys_user_person 关联 sys_person,最终关联到 eg_device_person_bind,判断某个用户是否对某设备有绑定关系
+        假定字段:sys_user_person(user_id, person_id),sys_person(id),eg_device_person_bind(person_id, device_id)
+    -->
+    <select id="countByUserIdAndDeviceId" resultType="java.lang.Integer">
+        select count(1)
+        from eg_device_person_bind b
+                 join sys_person p on b.person_id = p.id
+                 join sys_user_person up on up.person_id = p.id
+        where up.user_id = #{userId}
+          and b.device_id = #{deviceId}
+    </select>
+
+    <!-- 根据用户ID查询其绑定的设备ID列表 -->
+    <select id="selectDeviceIdsByUserId" resultType="java.lang.Integer">
+        select distinct b.device_id
+        from eg_device_person_bind b
+                 join sys_person p on b.person_id = p.id
+                 join sys_user_person up on up.person_id = p.id
+        where up.user_id = #{userId}
+    </select>
+
+    <delete id="deleteByDeviceId">
+        delete from eg_device_person_bind
+        where device_id = #{deviceId}
+    </delete>
+
+    <insert id="insert" parameterType="com.usky.eg.domain.EgDevicePersonBind">
+        insert into eg_device_person_bind (device_id, person_id, is_login_notify)
+        values (#{deviceId}, #{personId}, #{isLoginNotify})
+    </insert>
+
+</mapper>

+ 32 - 0
service-eg/service-eg-biz/src/main/resources/mapper/eg/SysPersonMapper.xml

@@ -0,0 +1,32 @@
+<?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.usky.eg.mapper.SysPersonMapper">
+
+    <resultMap id="BaseResultMap" type="com.usky.eg.domain.SysPerson">
+        <id column="id" property="id"/>
+        <result column="full_name" property="fullName"/>
+        <result column="age" property="age"/>
+        <result column="gender" property="gender"/>
+        <result column="address" property="address"/>
+        <result column="education_degree" property="educationDegree"/>
+        <result column="id_number" property="idNumber"/>
+        <result column="link_phone" property="linkPhone"/>
+        <result column="post_id" property="postId"/>
+        <result column="dept_id" property="deptId"/>
+        <result column="entry_time" property="entryTime"/>
+        <result column="certificate_url1" property="certificateUrl1"/>
+        <result column="certificate_url2" property="certificateUrl2"/>
+        <result column="certificate_url3" property="certificateUrl3"/>
+        <result column="creator" property="creator"/>
+        <result column="create_time" property="createTime"/>
+        <result column="update_person" property="updatePerson"/>
+        <result column="update_time" property="updateTime"/>
+        <result column="face_base" property="faceBase"/>
+        <result column="vef_num" property="vefNum"/>
+        <result column="face_name" property="faceName"/>
+        <result column="remark" property="remark"/>
+        <result column="face_status" property="faceStatus"/>
+        <result column="card_num" property="cardNum"/>
+    </resultMap>
+
+</mapper>

+ 2 - 1
service-iot/service-iot-biz/src/main/java/com/usky/iot/service/impl/DmpDeviceInfoServiceImpl.java

@@ -611,7 +611,8 @@ public class DmpDeviceInfoServiceImpl extends AbstractCrudService<DmpDeviceInfoM
 
     @Override
     public void updateDeviceStatus(LastInnerQueryVO queryVO) {
-        List<LastInnerResultVO> list = remoteTsdbProxyService.queryLastDeviceData(queryVO);
+        ApiResult<List<LastInnerResultVO>> lastApi = remoteTsdbProxyService.queryLastDeviceData(queryVO);
+        List<LastInnerResultVO> list = lastApi != null ? lastApi.getData() : null;
         if (CollectionUtils.isNotEmpty(list)) {
             for (int i = 0; i < list.size(); i++) {
                 if (Objects.nonNull(list.get(i).getMetrics())) {

+ 6 - 0
service-job/pom.xml

@@ -88,6 +88,12 @@
             <version>0.0.1</version>
             <scope>compile</scope>
         </dependency>
+        <dependency>
+            <groupId>com.usky</groupId>
+            <artifactId>service-eg-api</artifactId>
+            <version>0.0.1</version>
+            <scope>compile</scope>
+        </dependency>
     </dependencies>
 
     <build>

+ 9 - 0
service-job/src/main/java/com/ruoyi/job/task/RyTask.java

@@ -3,6 +3,7 @@ package com.ruoyi.job.task;
 import com.usky.cdi.AlarmDataSyncTaskService;
 import com.usky.cdi.RemotecdiTaskService;
 import com.usky.common.core.utils.StringUtils;
+import com.usky.eg.RemoteEgService;
 import com.usky.meeting.RemoteMeetingService;
 import com.usky.ems.RemoteEmsTaskService;
 import com.usky.fire.RemoteFireService;
@@ -39,6 +40,8 @@ public class RyTask {
     @Autowired
     private RemoteEmsTaskService remoteEmsTaskService;
 
+    @Autowired
+    private RemoteEgService remoteEgService;
 
     public void ryMultipleParams(String s, Boolean b, Long l, Double d, Integer i) {
         System.out.println(StringUtils.format("执行多参方法: 字符串类型{},布尔类型{},长整型{},浮点型{},整形{}", s, b, l, d, i));
@@ -121,4 +124,10 @@ public class RyTask {
         remoteEmsTaskService.sendEnergyHeartbeat();
     }
 
+    // 门禁设备心跳状态
+    public void egDeviceStatus() {
+        System.out.println("egDeviceStatus start......");
+        remoteEgService.egDeviceStatus();
+    }
+
 }

+ 0 - 10
service-meeting/service-meeting-biz/src/main/java/com/usky/meeting/domain/EgDevice.java

@@ -57,16 +57,6 @@ public class EgDevice implements Serializable {
      */
     private String deviceIp;
 
-    /**
-     * 端口
-     */
-    private Integer devicePort;
-
-    /**
-     * 门禁号
-     */
-    private String egNumber;
-
     /**
      * 创建者
      */

+ 0 - 2
service-meeting/service-meeting-biz/src/main/resources/mapper/meeting/EgDeviceMapper.xml

@@ -11,8 +11,6 @@
         <result column="install_address" property="installAddress" />
         <result column="service_status" property="serviceStatus" />
         <result column="device_ip" property="deviceIp" />
-        <result column="device_port" property="devicePort" />
-        <result column="eg_number" property="egNumber" />
         <result column="create_by" property="createBy" />
         <result column="update_by" property="updateBy" />
         <result column="create_time" property="createTime" />

+ 17 - 0
service-rule/pom.xml

@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <artifactId>usky-modules</artifactId>
+        <groupId>com.usky</groupId>
+        <version>0.0.1</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>service-rule</artifactId>
+    <packaging>pom</packaging>
+    <modules>
+        <module>service-rule-biz</module>
+        <module>service-rule-api</module>
+    </modules>
+</project>

+ 26 - 0
service-rule/service-rule-api/pom.xml

@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <artifactId>service-rule</artifactId>
+        <groupId>com.usky</groupId>
+        <version>0.0.1</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>service-rule-api</artifactId>
+    <dependencies>
+        <dependency>
+            <groupId>org.springframework.cloud</groupId>
+            <artifactId>spring-cloud-starter-openfeign</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>com.usky</groupId>
+            <artifactId>usky-common-core</artifactId>
+        </dependency>
+    </dependencies>
+    <build>
+        <finalName>${project.artifactId}</finalName>
+    </build>
+</project>

+ 101 - 0
service-rule/service-rule-biz/pom.xml

@@ -0,0 +1,101 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <artifactId>service-rule</artifactId>
+        <groupId>com.usky</groupId>
+        <version>0.0.1</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>service-rule-biz</artifactId>
+    <dependencies>
+        <dependency>
+            <groupId>com.usky</groupId>
+            <artifactId>common-cloud-starter</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>com.usky</groupId>
+            <artifactId>service-rule-api</artifactId>
+            <version>0.0.1</version>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-test</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>com.github.pagehelper</groupId>
+            <artifactId>pagehelper-spring-boot-starter</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>com.usky</groupId>
+            <artifactId>ruoyi-common-swagger</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>com.alibaba</groupId>
+            <artifactId>fastjson</artifactId>
+            <version>1.2.28</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.rocketmq</groupId>
+            <artifactId>rocketmq-spring-boot-starter</artifactId>
+            <version>2.1.1</version>
+        </dependency>
+        <dependency>
+            <groupId>org.mybatis.spring.boot</groupId>
+            <artifactId>mybatis-spring-boot-starter</artifactId>
+            <version>2.3.0</version>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-quartz</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.cache2k</groupId>
+            <artifactId>cache2k-core</artifactId>
+            <version>2.6.1.Final</version> <!-- 稳定版,兼容JDK8+ -->
+        </dependency>
+        <dependency>
+            <groupId>org.jetbrains</groupId>
+            <artifactId>annotations</artifactId>
+            <version>13.0</version>
+            <scope>compile</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>com.usky</groupId>
+            <artifactId>service-tsdb-api</artifactId>
+            <version>0.0.1</version>
+        </dependency>
+        <dependency>
+            <groupId>com.usky</groupId>
+            <artifactId>service-transfer-api</artifactId>
+            <version>0.0.1</version>
+        </dependency>
+        <!-- FastJSON2(高性能 JSON 解析) -->
+        <dependency>
+            <groupId>com.alibaba.fastjson2</groupId>
+            <artifactId>fastjson2</artifactId>
+            <version>2.0.48</version>
+        </dependency>
+    </dependencies>
+    <build>
+        <finalName>${project.artifactId}</finalName>
+        <plugins>
+            <plugin>
+                <groupId>org.springframework.boot</groupId>
+                <artifactId>spring-boot-maven-plugin</artifactId>
+                <version>2.2.6.RELEASE</version>
+                <executions>
+                    <execution>
+                        <goals>
+                            <goal>repackage</goal>
+                        </goals>
+                    </execution>
+                </executions>
+            </plugin>
+        </plugins>
+    </build>
+</project>

+ 44 - 0
service-rule/service-rule-biz/src/main/java/com/usky/rule/RuoYiSystemApplication.java

@@ -0,0 +1,44 @@
+package com.usky.rule;
+
+import com.ruoyi.common.swagger.annotation.EnableCustomSwagger2;
+import org.mybatis.spring.annotation.MapperScan;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.cloud.openfeign.EnableFeignClients;
+import org.springframework.context.ConfigurableApplicationContext;
+import org.springframework.context.annotation.ComponentScan;
+import org.springframework.core.env.Environment;
+import org.springframework.scheduling.annotation.EnableScheduling;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+
+/**
+ * 数据规则引擎服务(对标 leo-rule-engine + MQTT 上报链路,RocketMQ 订阅触发)
+ */
+@EnableCustomSwagger2
+@EnableFeignClients(basePackages = "com.usky")
+@EnableScheduling
+@MapperScan("com.usky.rule.mapper")
+@ComponentScan("com.usky")
+@SpringBootApplication
+public class RuoYiSystemApplication {
+
+    private static final Logger LOGGER = LoggerFactory.getLogger(RuoYiSystemApplication.class);
+
+    public static void main(String[] args) throws UnknownHostException {
+        ConfigurableApplicationContext application = SpringApplication.run(RuoYiSystemApplication.class, args);
+        Environment env = application.getEnvironment();
+        String ip = InetAddress.getLocalHost().getHostAddress();
+        String port = env.getProperty("server.port");
+        String path = env.getProperty("server.servlet.context-path");
+        LOGGER.info("\n----------------------------------------------------------\n\t" +
+                "Application is running! Access URLs:\n\t" +
+                "Local: \t\thttp://localhost:" + port + (path == null ? "" : path) + "/\n\t" +
+                "External: \thttp://" + ip + ":" + port + (path == null ? "" : path) + "/\n\t" +
+                "Api: \t\thttp://" + ip + ":" + port + (path == null ? "" : path) + "/swagger-ui/index.html\n\t" +
+                "----------------------------------------------------------");
+    }
+}

+ 37 - 0
service-rule/service-rule-biz/src/main/java/com/usky/rule/cache/DeviceAcqTriggerCooldownCache.java

@@ -0,0 +1,37 @@
+package com.usky.rule.cache;
+
+import com.usky.common.redis.core.RedisHelper;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import java.util.concurrent.TimeUnit;
+
+/**
+ * 设备采集触发规则成功执行动作后的冷却窗口,避免短时间内频繁上报反复执行同一规则。
+ */
+@Component
+public class DeviceAcqTriggerCooldownCache {
+
+    private static final String KEY_PREFIX = "ruleEngine:deviceAcqCooldown:";
+
+    @Autowired
+    private RedisHelper redisHelper;
+
+    public boolean isInCooldown(Long ruleEngineId, String deviceId) {
+        return redisHelper.hasKey(buildKey(ruleEngineId, deviceId));
+    }
+
+    /**
+     * 执行成功后调用,在 {@code cooldownSeconds} 秒内同规则同设备不再触发动作。
+     */
+    public void startCooldown(Long ruleEngineId, String deviceId, long cooldownSeconds) {
+        if (cooldownSeconds <= 0) {
+            return;
+        }
+        redisHelper.set(buildKey(ruleEngineId, deviceId), "1", cooldownSeconds, TimeUnit.SECONDS);
+    }
+
+    private static String buildKey(Long ruleEngineId, String deviceId) {
+        return KEY_PREFIX + ruleEngineId + ":" + deviceId;
+    }
+}

+ 113 - 0
service-rule/service-rule-biz/src/main/java/com/usky/rule/cache/DeviceTriggerIncludeMinuteCache.java

@@ -0,0 +1,113 @@
+package com.usky.rule.cache;
+
+import com.usky.common.redis.core.RedisHelper;
+import com.usky.rule.util.JsonUtil;
+import com.usky.rule.vo.ConditionExpression;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * 设备触发“包含分钟”条件缓存,使用 RedisHelper 存储。
+ * 单条条件过期时间 1 天;按 ruleEngineId 可批量删除。
+ */
+@Component
+public class DeviceTriggerIncludeMinuteCache {
+
+    private static final String KEY_PREFIX = "ruleEngine:condition:";
+    private static final String INDEX_KEY_PREFIX = "ruleEngine:keys:";
+    private static final long EXPIRE_DAYS = 1L;
+
+    @Autowired
+    private RedisHelper redisHelper;
+
+    public ConditionExpression getConditions(Long ruleEngineId, String device, String identifier, String expression) {
+        String key = buildConditionKey(ruleEngineId, device, identifier, expression);
+        if (!redisHelper.hasKey(key)) {
+            return null;
+        }
+        Object val = redisHelper.get(key);
+        if (val == null) {
+            return null;
+        }
+        return JsonUtil.toObject(val.toString(), ConditionExpression.class);
+    }
+
+    public void setCondition(Long ruleEngineId, String device, String identifier, ConditionExpression condition) {
+        String key = buildConditionKey(ruleEngineId, device, identifier, condition.getExpression());
+        redisHelper.set(key, JsonUtil.toJson(condition), EXPIRE_DAYS, TimeUnit.DAYS);
+        addToIndex(ruleEngineId, key);
+    }
+
+    public void removeCondition(Long ruleEngineId, String device, String identifier, String expression) {
+        String key = buildConditionKey(ruleEngineId, device, identifier, expression);
+        redisHelper.delete(key);
+        removeFromIndex(ruleEngineId, key);
+    }
+
+    public void removeConditions(Long ruleEngineId, String device, String identifier, List<String> expressionList) {
+        if (expressionList == null || expressionList.isEmpty()) {
+            return;
+        }
+        for (String expression : expressionList) {
+            removeCondition(ruleEngineId, device, identifier, expression);
+        }
+    }
+
+    public void deleteConditions(Long ruleEngineId) {
+        String indexKey = INDEX_KEY_PREFIX + ruleEngineId;
+        if (!redisHelper.hasKey(indexKey)) {
+            return;
+        }
+        Object val = redisHelper.get(indexKey);
+        if (val != null) {
+            List<String> keys = JsonUtil.parseJsonArray(val.toString(), String.class);
+            if (keys != null) {
+                for (String key : keys) {
+                    redisHelper.delete(key);
+                }
+            }
+        }
+        redisHelper.delete(indexKey);
+    }
+
+    private static String buildConditionKey(Long ruleEngineId, String device, String identifier, String expression) {
+        return KEY_PREFIX + ruleEngineId + ":" + device + ":" + identifier + ":" + expression;
+    }
+
+    private void addToIndex(Long ruleEngineId, String conditionKey) {
+        String indexKey = INDEX_KEY_PREFIX + ruleEngineId;
+        List<String> keys = getIndexKeys(indexKey);
+        if (!keys.contains(conditionKey)) {
+            keys.add(conditionKey);
+            redisHelper.set(indexKey, JsonUtil.toJson(keys));
+        }
+    }
+
+    private void removeFromIndex(Long ruleEngineId, String conditionKey) {
+        String indexKey = INDEX_KEY_PREFIX + ruleEngineId;
+        List<String> keys = getIndexKeys(indexKey);
+        if (keys.remove(conditionKey)) {
+            if (keys.isEmpty()) {
+                redisHelper.delete(indexKey);
+            } else {
+                redisHelper.set(indexKey, JsonUtil.toJson(keys));
+            }
+        }
+    }
+
+    private List<String> getIndexKeys(String indexKey) {
+        if (!redisHelper.hasKey(indexKey)) {
+            return new ArrayList<>();
+        }
+        Object val = redisHelper.get(indexKey);
+        if (val == null) {
+            return new ArrayList<>();
+        }
+        List<String> list = JsonUtil.parseJsonArray(val.toString(), String.class);
+        return list != null ? list : new ArrayList<>();
+    }
+}

+ 32 - 0
service-rule/service-rule-biz/src/main/java/com/usky/rule/cache/RuleEngineCache.java

@@ -0,0 +1,32 @@
+package com.usky.rule.cache;
+
+import com.usky.rule.vo.trigger.DeviceTrigger;
+import com.usky.rule.vo.trigger.SpaceTrigger;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+import org.cache2k.Cache;
+import org.cache2k.Cache2kBuilder;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+public class RuleEngineCache {
+    public RuleEngineCache() {
+    }
+
+    @Bean(
+            name = {"consumptionTriggerCache"}
+    )
+    public Cache<Long, List<DeviceTrigger>> consumptionTriggerCache() {
+        return (new Cache2kBuilder<Long, List<DeviceTrigger>>() {
+        }).name("consumptionTriggerCache").eternal(true).entryCapacity(100000L).build();
+    }
+
+    @Bean(
+            name = {"spaceTriggerCache"}
+    )
+    public Cache<Long, List<SpaceTrigger>> spaceTriggerCache() {
+        return (new Cache2kBuilder<Long, List<SpaceTrigger>>() {
+        }).name("spaceTriggerCache").eternal(false).expireAfterWrite(1L, TimeUnit.HOURS).entryCapacity(100000L).build();
+    }
+}

+ 141 - 0
service-rule/service-rule-biz/src/main/java/com/usky/rule/config/CronTaskManager.java

@@ -0,0 +1,141 @@
+package com.usky.rule.config;
+
+import com.usky.rule.util.RuleEngineUtil;
+import com.usky.rule.vo.action.RuleEngineAction;
+import com.usky.rule.vo.constraint.CronConstraint;
+import com.usky.rule.vo.constraint.DeviceConstraint;
+import com.usky.rule.jobs.ConsumptionJob;
+import com.usky.rule.listeners.CommonListener;
+import com.usky.rule.subscribe.TriggerDeviceUtil;
+
+import java.util.ArrayList;
+import java.util.List;
+import org.quartz.CronScheduleBuilder;
+import org.quartz.CronTrigger;
+import org.quartz.Job;
+import org.quartz.JobBuilder;
+import org.quartz.JobDataMap;
+import org.quartz.JobDetail;
+import org.quartz.JobKey;
+import org.quartz.Scheduler;
+import org.quartz.SchedulerException;
+import org.quartz.TriggerBuilder;
+import org.quartz.TriggerKey;
+import org.quartz.impl.matchers.GroupMatcher;
+import org.springframework.stereotype.Component;
+
+@Component
+public class CronTaskManager {
+    private Scheduler scheduler;
+    private TriggerDeviceUtil triggerDeviceUtil;
+
+    public void addJob(String jobName, String jobGroup, String cronExpression, Class<? extends Job> jobClass, List<CronConstraint> cronConstraintList, List<DeviceConstraint> deviceConstraints, List<RuleEngineAction> actions, Long ruleEngineId, Long projectId, Long spaceId) {
+        JobDataMap jobDataMap = new JobDataMap();
+        jobDataMap.put("actions", actions);
+        jobDataMap.put("ruleEngineId", ruleEngineId);
+        jobDataMap.put("projectId", projectId);
+        jobDataMap.put("spaceId", spaceId);
+        JobDetail jobDetail = JobBuilder.newJob(jobClass).withIdentity(jobName, jobGroup).usingJobData(jobDataMap).build();
+        CronTrigger cronTrigger = (CronTrigger)TriggerBuilder.newTrigger().withIdentity(jobName, jobGroup).withSchedule(CronScheduleBuilder.cronSchedule(cronExpression)).build();
+
+        try {
+            if (cronConstraintList != null || deviceConstraints != null) {
+                CommonListener jobListener = new CommonListener(this.getListenerName(ruleEngineId), this.triggerDeviceUtil, cronConstraintList, deviceConstraints);
+                this.scheduler.getListenerManager().addJobListener(jobListener, GroupMatcher.jobGroupEquals(jobGroup));
+            }
+
+            this.scheduler.scheduleJob(jobDetail, cronTrigger);
+        } catch (SchedulerException e) {
+            throw new RuntimeException(e.getMessage());
+        }
+    }
+
+    public String getListenerName(Long ruleEngineId) {
+        return "ruleEngineListener-" + ruleEngineId;
+    }
+
+    public void updateJob(String jobName, String jobGroup, String cronExpression) throws SchedulerException {
+        TriggerKey triggerKey = TriggerKey.triggerKey(jobName, jobGroup);
+        CronTrigger cronTrigger = (CronTrigger)this.scheduler.getTrigger(triggerKey);
+        if (cronTrigger == null) {
+            throw new SchedulerException("定时任务不存在");
+        } else {
+            String oldCronExpression = cronTrigger.getCronExpression();
+            if (!oldCronExpression.equalsIgnoreCase(cronExpression)) {
+                CronScheduleBuilder cronScheduleBuilder = CronScheduleBuilder.cronSchedule(cronExpression);
+                cronTrigger = (CronTrigger)cronTrigger.getTriggerBuilder().withIdentity(triggerKey).withSchedule(cronScheduleBuilder).build();
+                this.scheduler.rescheduleJob(triggerKey, cronTrigger);
+            }
+
+        }
+    }
+
+    public void deleteJob(String jobName, String jobGroup) throws SchedulerException {
+        JobKey jobKey = JobKey.jobKey(jobName, jobGroup);
+        if (this.scheduler.checkExists(jobKey)) {
+            this.scheduler.deleteJob(jobKey);
+        }
+
+    }
+
+    public boolean deleteAllJobsInJobGroup(Long ruleEngineId) {
+        String jobGroup = RuleEngineUtil.getJobGroup(ruleEngineId);
+
+        try {
+            List<JobKey> jobKeys = new ArrayList(this.scheduler.getJobKeys(GroupMatcher.jobGroupEquals(jobGroup)));
+            if (!jobKeys.isEmpty()) {
+                this.scheduler.deleteJobs(jobKeys);
+                this.scheduler.getListenerManager().removeJobListenerMatcher(this.getListenerName(ruleEngineId), GroupMatcher.jobGroupEquals(jobGroup));
+            }
+
+            return true;
+        } catch (SchedulerException var4) {
+            return false;
+        }
+    }
+
+    public void pauseAllJobs() throws SchedulerException {
+        this.scheduler.pauseAll();
+    }
+
+    public void resumeAllJobs() throws SchedulerException {
+        this.scheduler.resumeAll();
+    }
+
+    public void resumePausedJobs() throws SchedulerException {
+        for(String groupName : this.scheduler.getPausedTriggerGroups()) {
+            for(JobKey jobKey : this.scheduler.getJobKeys(GroupMatcher.groupEquals(groupName))) {
+                this.scheduler.resumeJob(jobKey);
+            }
+        }
+
+    }
+
+    public void performConsumptionTask() {
+        JobKey consumptionKey = JobKey.jobKey("consumption", "device");
+        try {
+            if (this.scheduler.checkExists(consumptionKey)) {
+                return;
+            }
+        } catch (SchedulerException e) {
+            throw new RuntimeException(e);
+        }
+
+        JobDetail consumptionJob = JobBuilder.newJob(ConsumptionJob.class).withIdentity(consumptionKey).build();
+        CronTrigger consumptionTrigger = (CronTrigger)TriggerBuilder.newTrigger().forJob(consumptionJob).withIdentity("consumptionTrigger").withSchedule(CronScheduleBuilder.cronSchedule("0 0 * * * ?")).build();
+//        JobDetail spaceJob = JobBuilder.newJob(SpaceJob.class).withIdentity("space").build();
+//        CronTrigger spaceTrigger = (CronTrigger)TriggerBuilder.newTrigger().forJob(spaceJob).withIdentity("spaceTrigger", "space").withSchedule(CronScheduleBuilder.cronSchedule("0 0 * * * ?")).build();
+
+        try {
+            this.scheduler.scheduleJob(consumptionJob, consumptionTrigger);
+//            this.scheduler.scheduleJob(spaceJob, spaceTrigger);
+        } catch (SchedulerException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    public CronTaskManager(final Scheduler scheduler, final TriggerDeviceUtil triggerDeviceUtil) {
+        this.scheduler = scheduler;
+        this.triggerDeviceUtil = triggerDeviceUtil;
+    }
+}

+ 22 - 0
service-rule/service-rule-biz/src/main/java/com/usky/rule/constant/DateTimeConstants.java

@@ -0,0 +1,22 @@
+package com.usky.rule.constant;
+
+import com.usky.rule.util.DateTimeUtil;
+import java.time.format.DateTimeFormatter;
+
+public interface DateTimeConstants {
+    String PATTERN_NUMERIC_DATETIME = "yyyyMMddHHmmss";
+    String PATTERN_DATETIME = "yyyy-MM-dd HH:mm:ss";
+    String PATTERN_DATE = "yyyy-MM-dd";
+    String PATTERN_TIME = "HH:mm:ss";
+    DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").withZone(DateTimeUtil.getZoneId());
+    DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd").withZone(DateTimeUtil.getZoneId());
+    DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("HH:mm:ss").withZone(DateTimeUtil.getZoneId());
+    DateTimeFormatter DATE_TIME_HOUR_FORMATTER_ZH_CN = DateTimeFormatter.ofPattern("yyyy年MM月dd日 HH时").withZone(DateTimeUtil.getZoneId());
+    DateTimeFormatter DATE_TIME_DAY_FORMATTER_ZH_CN = DateTimeFormatter.ofPattern("yyyy年MM月dd日").withZone(DateTimeUtil.getZoneId());
+    DateTimeFormatter DATE_TIME_MONTH_FORMATTER_ZH_CN = DateTimeFormatter.ofPattern("yyyy年MM月").withZone(DateTimeUtil.getZoneId());
+    DateTimeFormatter DATE_TIME_YEAR_FORMATTER_ZH_CN = DateTimeFormatter.ofPattern("yyyy年").withZone(DateTimeUtil.getZoneId());
+    DateTimeFormatter SIMPLE_DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMddHHmmss").withZone(DateTimeUtil.getZoneId());
+    DateTimeFormatter SIMPLE_DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMdd").withZone(DateTimeUtil.getZoneId());
+    DateTimeFormatter SIMPLE_TIME_FORMATTER = DateTimeFormatter.ofPattern("HHmmss").withZone(DateTimeUtil.getZoneId());
+    DateTimeFormatter DB_DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS").withZone(DateTimeUtil.getZoneId());
+}

+ 36 - 0
service-rule/service-rule-biz/src/main/java/com/usky/rule/constant/RegExpConstants.java

@@ -0,0 +1,36 @@
+package com.usky.rule.constant;
+
+import java.util.regex.Pattern;
+
+public interface RegExpConstants {
+    String DATE_TIME = "^(?:(?!0000)[0-9]{4}-(?:(?:0[1-9]|1[0-2])-(?:0[1-9]|1[0-9]|2[0-8])|(?:0[13-9]|1[0-2])-(?:29|30)|(?:0[13578]|1[02])-31)|(?:[0-9]{2}(?:0[48]|[2468][048]|[13579][26])|(?:0[48]|[2468][048]|[13579][26])00)-02-29)\\s+([01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9]$";
+    Pattern DATE_TIME_PATTERN = Pattern.compile("^(?:(?!0000)[0-9]{4}-(?:(?:0[1-9]|1[0-2])-(?:0[1-9]|1[0-9]|2[0-8])|(?:0[13-9]|1[0-2])-(?:29|30)|(?:0[13578]|1[02])-31)|(?:[0-9]{2}(?:0[48]|[2468][048]|[13579][26])|(?:0[48]|[2468][048]|[13579][26])00)-02-29)\\s+([01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9]$");
+    String DATE = "^(?:(?!0000)[0-9]{4}-(?:(?:0[1-9]|1[0-2])-(?:0[1-9]|1[0-9]|2[0-8])|(?:0[13-9]|1[0-2])-(?:29|30)|(?:0[13578]|1[02])-31)|(?:[0-9]{2}(?:0[48]|[2468][048]|[13579][26])|(?:0[48]|[2468][048]|[13579][26])00)-02-29)$";
+    Pattern DATE_PATTERN = Pattern.compile("^(?:(?!0000)[0-9]{4}-(?:(?:0[1-9]|1[0-2])-(?:0[1-9]|1[0-9]|2[0-8])|(?:0[13-9]|1[0-2])-(?:29|30)|(?:0[13578]|1[02])-31)|(?:[0-9]{2}(?:0[48]|[2468][048]|[13579][26])|(?:0[48]|[2468][048]|[13579][26])00)-02-29)$");
+    String TIME = "^([01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9]$";
+    Pattern TIME_PATTERN = Pattern.compile("^([01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9]$");
+    String SIMPLE_DATE_TIME = "^(?:(?!0000)[0-9]{4}(?:(?:0[1-9]|1[0-2])(?:0[1-9]|1[0-9]|2[0-8])|(?:0[13-9]|1[0-2])(?:29|30)|(?:0[13578]|1[02])31)|(?:[0-9]{2}(?:0[48]|[2468][048]|[13579][26])|(?:0[48]|[2468][048]|[13579][26])00)0229)([01][0-9]|2[0-3])[0-5][0-9][0-5][0-9]$";
+    Pattern SIMPLE_DATE_TIME_PATTERN = Pattern.compile("^(?:(?!0000)[0-9]{4}(?:(?:0[1-9]|1[0-2])(?:0[1-9]|1[0-9]|2[0-8])|(?:0[13-9]|1[0-2])(?:29|30)|(?:0[13578]|1[02])31)|(?:[0-9]{2}(?:0[48]|[2468][048]|[13579][26])|(?:0[48]|[2468][048]|[13579][26])00)0229)([01][0-9]|2[0-3])[0-5][0-9][0-5][0-9]$");
+    String SIMPLE_DATE = "^(?:(?!0000)[0-9]{4}(?:(?:0[1-9]|1[0-2])(?:0[1-9]|1[0-9]|2[0-8])|(?:0[13-9]|1[0-2])(?:29|30)|(?:0[13578]|1[02])31)|(?:[0-9]{2}(?:0[48]|[2468][048]|[13579][26])|(?:0[48]|[2468][048]|[13579][26])00)0229)$";
+    Pattern SIMPLE_DATE_PATTERN = Pattern.compile("^(?:(?!0000)[0-9]{4}(?:(?:0[1-9]|1[0-2])(?:0[1-9]|1[0-9]|2[0-8])|(?:0[13-9]|1[0-2])(?:29|30)|(?:0[13578]|1[02])31)|(?:[0-9]{2}(?:0[48]|[2468][048]|[13579][26])|(?:0[48]|[2468][048]|[13579][26])00)0229)$");
+    String SIMPLE_TIME = "^([01][0-9]|2[0-3])[0-5][0-9][0-5][0-9]$";
+    Pattern SIMPLE_TIME_PATTERN = Pattern.compile("^([01][0-9]|2[0-3])[0-5][0-9][0-5][0-9]$");
+    String IP = "^[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}$";
+    Pattern IP_PATTERN = Pattern.compile("^[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}$");
+    String DEVICE_ID = "^[0-9a-zA-Z]{13}$";
+    Pattern DEVICE_ID_PATTERN = Pattern.compile("^[0-9a-zA-Z]{13}$");
+    String GATEWAY_ID = "^[0-9a-zA-Z]{13}$";
+    Pattern GATEWAY_ID_PATTERN = Pattern.compile("^[0-9a-zA-Z]{13}$");
+    String PRODUCT_IDENTIFIER = "^[0-9A-Z]{7}$";
+    Pattern PRODUCT_IDENTIFIER_PATTERN = Pattern.compile("^[0-9A-Z]{7}$");
+    String GATEWAY_SECRET = "^(?![0-9]+$)[0-9A-Za-z]{16}$";
+    Pattern GATEWAY_SECRET_PATTERN = Pattern.compile("^(?![0-9]+$)[0-9A-Za-z]{16}$");
+    String PRODUCT_TEMPLATE_IDENTIFIER = "[0-9a-zA-Z]{18}$";
+    String USERNAME = "^(?![0-9]+$)[0-9A-Za-z]{4,20}$";
+    String isNumeric = "-?\\d+(\\.\\d+)?";
+    String isInteger = "-?\\d+";
+    String PASSWORD = "^(?:(?=.*\\d)(?=.*[a-zA-Z])|(?=.*\\d)(?=.*[^a-zA-Z\\d])|(?=.*[a-zA-Z])(?=.*[^a-zA-Z\\d]))[a-zA-Z\\d\\S]{8,18}$";
+    String PHONE = "^[1]\\d{10}$";
+    String HOUR = "^(2[0-3]|[0-1]?\\d)$";
+    String MONTH = "^(2[0-8]|[0-1]?[1-9])$";
+}

+ 108 - 0
service-rule/service-rule-biz/src/main/java/com/usky/rule/controller/MybatisGeneratorUtils.java

@@ -0,0 +1,108 @@
+package com.usky.rule.controller;//package com.usky.iot.controller;//package com.usky.dm.controller.web.business;//package com.usky.dm.controller.web;
+
+
+import com.baomidou.mybatisplus.core.toolkit.StringPool;
+import com.baomidou.mybatisplus.generator.AutoGenerator;
+import com.baomidou.mybatisplus.generator.InjectionConfig;
+import com.baomidou.mybatisplus.generator.config.*;
+import com.baomidou.mybatisplus.generator.config.po.TableInfo;
+import com.baomidou.mybatisplus.generator.config.rules.NamingStrategy;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * @author yq
+ * @date 2021/7/6 11:42
+ */
+public class MybatisGeneratorUtils {
+    public static void main(String[] args) {
+
+            shell("service-rule","service-rule-biz");
+    }
+
+    private static void shell(String parentName,String model) {
+
+        AutoGenerator mpg = new AutoGenerator();
+        //1、全局配置
+        GlobalConfig gc = new GlobalConfig();
+//        File file = new File(model);
+//        String path = file.getAbsolutePath();
+        String projectPath = System.getProperty("user.dir");
+        projectPath+="/"+parentName;
+        projectPath+="/"+model;
+        gc.setOutputDir(projectPath+ "/src/main/java");  //生成路径(一般都是生成在此项目的src/main/java下面)
+        //修改为自己的名字
+        gc.setAuthor("zyj"); //设置作者
+        gc.setOpen(false);
+        gc.setFileOverride(true); //第二次生成会把第一次生成的覆盖掉
+        gc.setServiceName("%sService"); //生成的service接口名字首字母是否为I,这样设置就没有
+        gc.setBaseResultMap(true); //生成resultMap
+        mpg.setGlobalConfig(gc);
+
+        //2、数据源配置
+        //修改数据源
+        DataSourceConfig dsc = new DataSourceConfig();
+        dsc.setUrl("jdbc:mysql://192.168.10.165:3306/usky-cloud?useUnicode=true&serverTimezone=GMT&useSSL=false&characterEncoding=utf8");
+        dsc.setDriverName("com.mysql.jdbc.Driver");
+        dsc.setUsername("root");
+        dsc.setPassword("yt123456");
+        mpg.setDataSource(dsc);
+
+        // 3、包配置
+        PackageConfig pc = new PackageConfig();
+        pc.setParent("com.usky.rule");
+        pc.setController("controller.web");
+        pc.setEntity("domain");
+        pc.setMapper("mapper");
+        pc.setService("service");
+        pc.setServiceImpl("service.impl");
+//        pc.setXml("mapper.demo");
+        //pc.setModuleName("test");
+        mpg.setPackageInfo(pc);
+
+        // 4、策略配置
+        StrategyConfig strategy = new StrategyConfig();
+        strategy.setNaming(NamingStrategy.underline_to_camel);
+        strategy.setColumnNaming(NamingStrategy.underline_to_camel);
+        strategy.setSuperMapperClass("com.usky.common.mybatis.core.CrudMapper");
+        strategy.setSuperServiceClass("com.usky.common.mybatis.core.CrudService");
+        strategy.setSuperServiceImplClass("com.usky.common.mybatis.core.AbstractCrudService");
+        // strategy.setTablePrefix("t_"); // 表名前缀
+        strategy.setEntityLombokModel(true); //使用lombok
+        //修改自己想要生成的表
+        strategy.setInclude("base_space");  // 逆向工程使用的表   如果要生成多个,这里可以传入String[]
+        mpg.setStrategy(strategy);
+
+        // 关闭默认 xml 生成,调整生成 至 根目录
+        //修改对应的模块名称
+        TemplateConfig tc = new TemplateConfig();
+        // 自定义配置
+        InjectionConfig cfg = new InjectionConfig() {
+            @Override
+            public void initMap() {
+                // to do nothing
+            }
+        };
+        //如果模板引擎是 velocity
+        String templatePath = "/templates/mapper.xml.vm";
+        // 自定义输出配置
+        List<FileOutConfig> focList = new ArrayList<>();
+        // 自定义配置会被优先输出
+        String finalProjectPath = projectPath;
+        focList.add(new FileOutConfig(templatePath) {
+            @Override
+            public String outputFile(TableInfo tableInfo) {
+                // 自定义输出文件名 , 如果你 Entity 设置了前后缀、此处注意 xml 的名称会跟着发生变化!!
+                return finalProjectPath + "/src/main/resources/mapper/rule" + "/"
+                        + tableInfo.getEntityName() + "Mapper" + StringPool.DOT_XML;
+            }
+        });
+        cfg.setFileOutConfigList(focList);
+        mpg.setCfg(cfg);
+        tc.setXml(null);
+        mpg.setTemplate(tc);
+        //5、执行
+        mpg.execute();
+    }
+}

+ 21 - 0
service-rule/service-rule-biz/src/main/java/com/usky/rule/controller/web/BaseBuildController.java

@@ -0,0 +1,21 @@
+package com.usky.rule.controller.web;
+
+
+import org.springframework.web.bind.annotation.RequestMapping;
+
+import org.springframework.stereotype.Controller;
+
+/**
+ * <p>
+ * 建筑信息 前端控制器
+ * </p>
+ *
+ * @author zyj
+ * @since 2026-03-17
+ */
+@Controller
+@RequestMapping("/baseBuild")
+public class BaseBuildController {
+
+}
+

+ 21 - 0
service-rule/service-rule-biz/src/main/java/com/usky/rule/controller/web/BaseSpaceController.java

@@ -0,0 +1,21 @@
+package com.usky.rule.controller.web;
+
+
+import org.springframework.web.bind.annotation.RequestMapping;
+
+import org.springframework.stereotype.Controller;
+
+/**
+ * <p>
+ * 空间 前端控制器
+ * </p>
+ *
+ * @author zyj
+ * @since 2026-03-17
+ */
+@Controller
+@RequestMapping("/baseSpace")
+public class BaseSpaceController {
+
+}
+

+ 21 - 0
service-rule/service-rule-biz/src/main/java/com/usky/rule/controller/web/DmpDeviceController.java

@@ -0,0 +1,21 @@
+package com.usky.rule.controller.web;
+
+
+import org.springframework.web.bind.annotation.RequestMapping;
+
+import org.springframework.stereotype.Controller;
+
+/**
+ * <p>
+ * 设备信息表 前端控制器
+ * </p>
+ *
+ * @author zyj
+ * @since 2026-03-17
+ */
+@Controller
+@RequestMapping("/dmpDevice")
+public class DmpDeviceController {
+
+}
+

+ 21 - 0
service-rule/service-rule-biz/src/main/java/com/usky/rule/controller/web/DmpProductAttributeController.java

@@ -0,0 +1,21 @@
+package com.usky.rule.controller.web;
+
+
+import org.springframework.web.bind.annotation.RequestMapping;
+
+import org.springframework.stereotype.Controller;
+
+/**
+ * <p>
+ * 产品属性表 前端控制器
+ * </p>
+ *
+ * @author zyj
+ * @since 2026-03-17
+ */
+@Controller
+@RequestMapping("/dmpProductAttribute")
+public class DmpProductAttributeController {
+
+}
+

+ 21 - 0
service-rule/service-rule-biz/src/main/java/com/usky/rule/controller/web/DmpProductCommandController.java

@@ -0,0 +1,21 @@
+package com.usky.rule.controller.web;
+
+
+import org.springframework.web.bind.annotation.RequestMapping;
+
+import org.springframework.stereotype.Controller;
+
+/**
+ * <p>
+ * 产品命令表 前端控制器
+ * </p>
+ *
+ * @author zyj
+ * @since 2026-03-20
+ */
+@Controller
+@RequestMapping("/dmpProductCommand")
+public class DmpProductCommandController {
+
+}
+

+ 21 - 0
service-rule/service-rule-biz/src/main/java/com/usky/rule/controller/web/DmpProductController.java

@@ -0,0 +1,21 @@
+package com.usky.rule.controller.web;
+
+
+import org.springframework.web.bind.annotation.RequestMapping;
+
+import org.springframework.stereotype.Controller;
+
+/**
+ * <p>
+ * 产品信息表 前端控制器
+ * </p>
+ *
+ * @author zyj
+ * @since 2026-03-17
+ */
+@Controller
+@RequestMapping("/dmpProduct")
+public class DmpProductController {
+
+}
+

+ 116 - 0
service-rule/service-rule-biz/src/main/java/com/usky/rule/controller/web/RuleEngineController.java

@@ -0,0 +1,116 @@
+package com.usky.rule.controller.web;
+
+import com.usky.common.core.bean.ApiResult;
+import com.usky.common.core.bean.CommonPage;
+import com.usky.common.log.annotation.Log;
+import com.usky.common.log.enums.BusinessType;
+import com.usky.common.security.utils.SecurityUtils;
+import com.usky.rule.domain.RuleEngine;
+import com.usky.rule.domain.RuleEngineCondition;
+import com.usky.rule.service.RuleEngineConditionService;
+import com.usky.rule.service.RuleEngineService;
+import com.usky.rule.vo.RuleEngineConfigDTO;
+import com.usky.rule.vo.RuleEngineDTO;
+import com.usky.rule.vo.RuleEnginePageRequest;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import io.swagger.annotations.ApiParam;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+/**
+ * 数据规则引擎界面 API(表 rule_engine)
+ * 含:配置规则、条件下拉、触发
+ */
+@Api(tags = "数据规则引擎")
+@RestController
+@RequestMapping("/ruleEngine")
+public class RuleEngineController {
+    private final RuleEngineService ruleEngineService;
+    private final RuleEngineConditionService ruleEngineConditionService;
+
+    public RuleEngineController(RuleEngineService ruleEngineService,
+                               RuleEngineConditionService ruleEngineConditionService) {
+        this.ruleEngineService = ruleEngineService;
+        this.ruleEngineConditionService = ruleEngineConditionService;
+    }
+
+    @ApiOperation("新增规则引擎")
+    @PostMapping
+    public ApiResult<Void> add(@RequestBody RuleEngine ruleEngine) {
+        ruleEngineService.add(ruleEngine);
+        return ApiResult.success();
+    }
+
+    @ApiOperation("修改规则引擎")
+    @PutMapping
+    public ApiResult<Void> edit(@RequestBody RuleEngine ruleEngine) {
+        ruleEngineService.update(ruleEngine);
+        return ApiResult.success();
+    }
+
+    @ApiOperation("删除规则引擎")
+    @DeleteMapping("/{id}")
+    public ApiResult<Void> remove(@PathVariable("id") Long id) {
+        ruleEngineService.remove(id);
+        return ApiResult.success();
+    }
+
+    @ApiOperation("规则引擎详情")
+    @GetMapping("/{id}")
+    public ApiResult<RuleEngine> detail(@PathVariable("id") Long id) {
+        return ApiResult.success(ruleEngineService.getById(id));
+    }
+
+    @ApiOperation("规则引擎分页")
+    @PostMapping("/page")
+    public ApiResult<CommonPage<RuleEngine>> page(@RequestBody RuleEnginePageRequest request) {
+        return ApiResult.success(ruleEngineService.pageList(request));
+    }
+
+    @ApiOperation("查询多条数据(条件同分页,不分页,对应 leo list 接口)")
+    @PostMapping("/list")
+    public ApiResult<List<RuleEngine>> list(@RequestBody RuleEnginePageRequest request) {
+        return ApiResult.success(ruleEngineService.list(request));
+    }
+
+    @ApiOperation("规则启停")
+    @PutMapping("/{id}/status")
+    public ApiResult<Void> status(@PathVariable("id") Long id, @RequestParam("status") Integer status) {
+        ruleEngineService.updateStatus(id, status);
+        return ApiResult.success();
+    }
+
+
+    // --------------- 配置规则、条件下拉、触发 ---------------
+
+    @ApiOperation("配置规则")
+    @PostMapping("/ruleEngineConfig")
+    public ApiResult<Void> ruleEngineConfig(@RequestBody RuleEngineDTO dto) {
+        ruleEngineService.ruleEngineConfig(dto);
+        return ApiResult.success();
+    }
+
+    @ApiOperation("条件下拉")
+    @GetMapping("/conditions")
+    public ApiResult<List<RuleEngineCondition>> conditions(
+            @ApiParam(value = "类型:1 触发条件,2 约束条件,不传则返回全部") @RequestParam(required = false) Integer type) {
+        List<RuleEngineCondition> list = ruleEngineConditionService.listConditions(type);
+        return ApiResult.success(list);
+    }
+
+    @ApiOperation("手动触发规则")
+    @PostMapping("/{id}/manualTrigger")
+    public ApiResult<Void> manualTrigger(@PathVariable("id") Long id) {
+        ruleEngineService.manualTrigger(id);
+        return ApiResult.success();
+    }
+
+    @ApiOperation("获取规则完整配置")
+    @GetMapping("/{id}/getEngineConfig")
+    public ApiResult<RuleEngineConfigDTO> getEngineConfig(@PathVariable("id") Long id) {
+        RuleEngineConfigDTO config = ruleEngineService.getEngineConfig(id);
+        return ApiResult.success(config);
+    }
+}

+ 53 - 0
service-rule/service-rule-biz/src/main/java/com/usky/rule/controller/web/RuleEngineLogController.java

@@ -0,0 +1,53 @@
+package com.usky.rule.controller.web;
+
+import com.usky.common.core.bean.ApiResult;
+import com.usky.common.core.bean.CommonPage;
+import com.usky.common.log.annotation.Log;
+import com.usky.common.log.enums.BusinessType;
+import com.usky.common.security.utils.SecurityUtils;
+import com.usky.rule.domain.RuleEngineLog;
+import com.usky.rule.service.RuleEngineLogService;
+import com.usky.rule.vo.RuleEngineContent;
+import com.usky.rule.vo.RuleEngineLogPageRequest;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import org.springframework.web.bind.annotation.*;
+
+/**
+ * 数据规则日志界面 API(表 rule_engine_log)
+ */
+@Api(tags = "数据规则日志")
+@RestController
+@RequestMapping("/ruleEngineLog")
+public class RuleEngineLogController {
+    private final RuleEngineLogService ruleEngineLogService;
+
+    public RuleEngineLogController(RuleEngineLogService ruleEngineLogService) {
+        this.ruleEngineLogService = ruleEngineLogService;
+    }
+
+    @ApiOperation("规则执行日志分页")
+    @PostMapping("/page")
+    public ApiResult<CommonPage<RuleEngineLog>> page(@RequestBody RuleEngineLogPageRequest request) {
+        if (request.getTenantId() == null) {
+            try {
+                request.setTenantId(SecurityUtils.getTenantId());
+            } catch (Exception ignored) {
+            }
+        }
+        return ApiResult.success(ruleEngineLogService.pageList(request));
+    }
+
+    @ApiOperation("规则执行日志详情")
+    @GetMapping("/{id}")
+    public ApiResult<RuleEngineContent> detail(@PathVariable("id") Long id) {
+        return ApiResult.success(ruleEngineLogService.getById(id));
+    }
+
+    @ApiOperation("删除规则执行日志")
+    @DeleteMapping("/{id}")
+    public ApiResult<Void> remove(@PathVariable("id") Long id) {
+        ruleEngineLogService.remove(id);
+        return ApiResult.success();
+    }
+}

+ 172 - 0
service-rule/service-rule-biz/src/main/java/com/usky/rule/crons/TriggerCronTask.java

@@ -0,0 +1,172 @@
+package com.usky.rule.crons;
+
+import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
+import com.usky.rule.domain.RuleEngine;
+import com.usky.rule.service.RuleEngineCronService;
+import com.usky.rule.service.RuleEngineService;
+import com.usky.rule.util.CronUtil;
+import com.usky.rule.util.JsonUtil;
+import com.usky.rule.util.RuleEngineUtil;
+import com.usky.rule.vo.RuleEngineCronVO;
+import com.usky.rule.vo.RuleEngineDetail;
+import com.usky.rule.vo.action.RuleEngineAction;
+import com.usky.rule.vo.constraint.CronConstraint;
+import com.usky.rule.vo.constraint.DeviceConstraint;
+import com.usky.rule.vo.trigger.CronTrigger;
+import com.usky.rule.vo.trigger.DeviceTrigger;
+import com.usky.rule.vo.RuleEnginePageRequest;
+import com.usky.rule.vo.trigger.SpaceTrigger;
+import com.usky.rule.config.CronTaskManager;
+import com.usky.rule.jobs.CommonJob;
+
+import java.util.List;
+import java.util.Map;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+import org.apache.commons.lang3.StringUtils;
+import org.cache2k.Cache;
+import org.jetbrains.annotations.NotNull;
+import org.springframework.beans.BeansException;
+import org.springframework.beans.factory.DisposableBean;
+import org.springframework.beans.factory.InitializingBean;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.ApplicationContextAware;
+import org.springframework.context.SmartLifecycle;
+import org.springframework.stereotype.Component;
+
+@Component
+public class TriggerCronTask implements ApplicationContextAware, InitializingBean, DisposableBean, SmartLifecycle {
+    private RuleEngineService ruleEngineService;
+    private RuleEngineCronService ruleEngineCronService;
+    private CronTaskManager cronTaskManager;
+    private Cache<Long, List<DeviceTrigger>> consumptionTriggerCache;
+    private Cache<Long, List<SpaceTrigger>> spaceTriggerCache;
+
+    public void start() {
+        List<RuleEngine> enabledRuleEngineList = this.getAllEnabledRuleEngine();
+        enabledRuleEngineList = (List)enabledRuleEngineList.stream().filter((ruleEnginex) -> StringUtils.isNotBlank(ruleEnginex.getDetail())).collect(Collectors.toList());
+        if (!enabledRuleEngineList.isEmpty()) {
+            for(RuleEngine ruleEngine : enabledRuleEngineList) {
+                RuleEngineDetail ruleEngineDetail = (RuleEngineDetail)JsonUtil.toObject(ruleEngine.getDetail(), RuleEngineDetail.class);
+                List<CronTrigger> cronTriggers = this.ruleEngineService.getCronTriggers(ruleEngineDetail.getTriggers());
+                if (cronTriggers != null) {
+                    cronTriggers = (List)cronTriggers.stream().filter((cronTrigger) -> CronUtil.isCronMatched(cronTrigger.getCron())).collect(Collectors.toList());
+                    if (!cronTriggers.isEmpty()) {
+                        this.processCronTask(ruleEngine.getId(), ruleEngine.getProjectId(), ruleEngine.getSpaceId(), ruleEngineDetail, cronTriggers);
+                    }
+                }
+
+                this.setDeviceConsumptionCache(ruleEngine.getId(), ruleEngineDetail);
+            }
+        }
+        this.cronTaskManager.performConsumptionTask();
+    }
+
+    private void startCronTask() {
+        List<RuleEngineCronVO> ruleEngineCronList = this.ruleEngineCronService.getTurnedOnCron();
+        Function<RuleEngineCronVO, String> ruleEngineCronFunction = (vo) -> vo.getRuleEngineId() + ":" + vo.getProjectId() + ":" + vo.getSpaceId();
+        Map<String, List<CronTrigger>> ruleEngineCronMap = (Map)ruleEngineCronList.stream().filter((cronTrigger) -> CronUtil.isCronMatched(cronTrigger.getCron())).collect(Collectors.groupingBy(ruleEngineCronFunction, Collectors.mapping((vo) -> new CronTrigger(vo.getCron()), Collectors.toList())));
+        if (CollectionUtils.isNotEmpty(ruleEngineCronMap)) {
+            ruleEngineCronMap.forEach((identifier, list) -> {
+                String[] idAndProjectId = identifier.split(":");
+                Long ruleEngineId = Long.valueOf(idAndProjectId[0]);
+                Long projectId = Long.valueOf(idAndProjectId[1]);
+                Long spaceId = Long.valueOf(idAndProjectId[2]);
+                String detail = this.ruleEngineService.getDetail(Long.valueOf(idAndProjectId[0]));
+                if (StringUtils.isNotEmpty(detail)) {
+                    RuleEngineDetail ruleEngineDetail = (RuleEngineDetail)JsonUtil.toObject(detail, RuleEngineDetail.class);
+                    this.processCronTask(ruleEngineId, projectId, spaceId, ruleEngineDetail, list);
+                }
+
+            });
+        }
+
+    }
+
+    /**
+     * 与库里的规则详情对齐能耗类设备触发器。去掉/改掉 consumption 时必须 remove,否则 ConsumptionJob 仍用旧缓存。
+     */
+    public void setDeviceConsumptionCache(Long ruleEngineId, RuleEngineDetail ruleEngineDetail) {
+        if (ruleEngineId == null) {
+            return;
+        }
+        List<DeviceTrigger> deviceTriggers = this.ruleEngineService.getDeviceTriggers(ruleEngineDetail.getTriggers());
+        if (deviceTriggers == null || deviceTriggers.isEmpty()) {
+            this.consumptionTriggerCache.remove(ruleEngineId);
+            return;
+        }
+        List<DeviceTrigger> consumptionTriggers = deviceTriggers.stream()
+                .filter((t) -> "consumption".equals(t.getMethod()))
+                .collect(Collectors.toList());
+        if (consumptionTriggers.isEmpty()) {
+            this.consumptionTriggerCache.remove(ruleEngineId);
+        } else {
+            this.consumptionTriggerCache.put(ruleEngineId, consumptionTriggers);
+        }
+
+//        List<SpaceTrigger> spaceTriggers = this.ruleEngineService.getSpaceTriggers(ruleEngineDetail.getTriggers());
+//        if (deviceTriggers != null && !spaceTriggers.isEmpty()) {
+//            this.spaceTriggerCache.put(ruleEngineId, spaceTriggers);
+//        }
+
+    }
+
+    public void removeDeviceConsumptionCache(Long ruleEngineId) {
+        if (ruleEngineId != null) {
+            this.consumptionTriggerCache.remove(ruleEngineId);
+        }
+    }
+
+    private void processCronTask(Long id, Long projectId, Long spaceId, RuleEngineDetail engineDetail, List<CronTrigger> cronTriggers) {
+        List<CronConstraint> cronConstraints = this.ruleEngineService.getCronConstraints(engineDetail.getConstraints());
+        List<DeviceConstraint> deviceConstraints = this.ruleEngineService.getDeviceConstraints(engineDetail.getConstraints());
+        List<RuleEngineAction> actions = this.ruleEngineService.getActions(engineDetail.getActions());
+        if (actions != null && !actions.isEmpty()) {
+            addCronJob(id, projectId, spaceId, cronTriggers, cronConstraints, deviceConstraints, actions, this.cronTaskManager);
+        }
+
+    }
+
+    public static void addCronJob(Long ruleEngineId, Long projectId, Long spaceId, List<CronTrigger> cronTriggers, List<CronConstraint> cronConstraints, List<DeviceConstraint> deviceConstraints, List<RuleEngineAction> actions, CronTaskManager cronTaskManager) {
+        String jobGroup = RuleEngineUtil.getJobGroup(ruleEngineId);
+
+        for(int i = 0; i < cronTriggers.size(); ++i) {
+            CronTrigger cronTrigger = (CronTrigger)cronTriggers.get(i);
+            String jobName = RuleEngineUtil.getTriggerCronJobName(i);
+            cronTaskManager.addJob(jobName, jobGroup, cronTrigger.getCron(), CommonJob.class, cronConstraints, deviceConstraints, actions, ruleEngineId, projectId, spaceId);
+        }
+
+    }
+
+    /** 查询所有已启用规则(status=1),使用 list(RuleEnginePageRequest) 替代 MyBatis-Plus list(Wrapper) */
+    public List<RuleEngine> getAllEnabledRuleEngine() {
+        RuleEnginePageRequest request = new RuleEnginePageRequest();
+        request.setStatus(1);
+        return this.ruleEngineService.list(request);
+    }
+
+    public void destroy() throws Exception {
+    }
+
+    public void afterPropertiesSet() throws Exception {
+    }
+
+    public void setApplicationContext(@NotNull ApplicationContext applicationContext) throws BeansException {
+    }
+
+    public void stop() {
+    }
+
+    public boolean isRunning() {
+        return false;
+    }
+
+    public TriggerCronTask(final RuleEngineService ruleEngineService, final RuleEngineCronService ruleEngineCronService, final CronTaskManager cronTaskManager, final Cache<Long, List<DeviceTrigger>> consumptionTriggerCache, final Cache<Long, List<SpaceTrigger>> spaceTriggerCache) {
+        this.ruleEngineService = ruleEngineService;
+        this.ruleEngineCronService = ruleEngineCronService;
+        this.cronTaskManager = cronTaskManager;
+        this.consumptionTriggerCache = consumptionTriggerCache;
+        this.spaceTriggerCache = spaceTriggerCache;
+    }
+}

+ 212 - 0
service-rule/service-rule-biz/src/main/java/com/usky/rule/domain/BaseBuild.java

@@ -0,0 +1,212 @@
+package com.usky.rule.domain;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import java.time.LocalDate;
+import com.baomidou.mybatisplus.annotation.TableId;
+import java.time.LocalDateTime;
+import java.io.Serializable;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * <p>
+ * 建筑信息
+ * </p>
+ *
+ * @author zyj
+ * @since 2026-03-17
+ */
+@Data
+@EqualsAndHashCode(callSuper = false)
+public class BaseBuild implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 主键
+     */
+    @TableId(value = "id", type = IdType.AUTO)
+    private Integer id;
+
+    /**
+     * 建筑编号
+     */
+    private String buildNum;
+
+    /**
+     * 建筑名称
+     */
+    private String buildName;
+
+    /**
+     * 详细地址
+     */
+    private String address;
+
+    /**
+     * 模型地址
+     */
+    private String modelAddress;
+
+    /**
+     * 地上楼层
+     */
+    private Integer aboveFloor;
+
+    /**
+     * 地下楼层
+     */
+    private Integer underFloor;
+
+    /**
+     * 建筑面积
+     */
+    private Double buildArea;
+
+    /**
+     * 占地面积
+     */
+    private Double coverArea;
+
+    /**
+     * 耐火等级
+     */
+    private Integer fireRating;
+
+    /**
+     * 使用性质
+     */
+    private Integer useCharacter;
+
+    /**
+     * 建筑结构
+     */
+    private Integer buildStructure;
+
+    /**
+     * 建筑高度
+     */
+    private Double buildHigh;
+
+    /**
+     * 建筑高度分类
+     */
+    private Integer highType;
+
+    /**
+     * 竣工年份
+     */
+    private LocalDate completeYear;
+
+    /**
+     * 安全责任人
+     */
+    private String safePerson;
+
+    /**
+     * 安全管理人
+     */
+    private String managePerson;
+
+    /**
+     * 火灾危险性
+     */
+    private Integer fireRisk;
+
+    /**
+     * 消防控制室位置
+     */
+    private String fireControlRoom;
+
+    /**
+     * 建筑立面图
+     */
+    private String buildInside;
+
+    /**
+     * 建筑平面图
+     */
+    private String buildPlan;
+
+    /**
+     * 设施ID
+     */
+    private Integer facilityId;
+
+    /**
+     * BIM地址
+     */
+    private String bimUrl;
+
+    /**
+     * 联系人电话
+     */
+    private String contactPhone;
+
+    /**
+     * 建筑备注
+     */
+    private String buildDesc;
+
+    /**
+     * 单元数量
+     */
+    private Integer unitCount;
+
+    /**
+     * 创建时间
+     */
+    private LocalDateTime createTime;
+
+    /**
+     * 更新时间
+     */
+    private LocalDateTime updateTime;
+
+    /**
+     * 更新人
+     */
+    private String updateBy;
+
+    /**
+     * 创建人
+     */
+    private String createBy;
+
+    /**
+     * 删除标识
+     */
+    private Integer deleteFlag;
+
+    /**
+     * 地下空间
+     */
+    private Double underSpace;
+
+    /**
+     * 防火涂层(0、无 1、有)
+     */
+    private Integer fireproofCoat;
+
+    /**
+     * 组织机构ID
+     */
+    private Integer deptId;
+
+    /**
+     * 租户ID
+     */
+    private Integer tenantId;
+
+    /**
+     * 经度(当设施类型为点时使用该字段)
+     */
+    private String longitude;
+
+    /**
+     * 纬度(当设施类型为点时使用该字段)
+     */
+    private String latitude;
+
+
+}

+ 91 - 0
service-rule/service-rule-biz/src/main/java/com/usky/rule/domain/BaseSpace.java

@@ -0,0 +1,91 @@
+package com.usky.rule.domain;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import java.time.LocalDateTime;
+import java.io.Serializable;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * <p>
+ * 空间
+ * </p>
+ *
+ * @author zyj
+ * @since 2026-03-17
+ */
+@Data
+@EqualsAndHashCode(callSuper = false)
+public class BaseSpace implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 自增ID
+     */
+    @TableId(value = "id", type = IdType.AUTO)
+    private Long id;
+
+    /**
+     * 名称
+     */
+    private String name;
+
+    /**
+     * 父节点ID
+     */
+    private Long parentId;
+
+    /**
+     * 空间类型 1:项目 2:区域 3:建筑 4:楼层 5:房间
+     */
+    private Integer type;
+
+    /**
+     * 根节点ID
+     */
+    private Long rootId;
+
+    /**
+     * 节点路径
+     */
+    private String path;
+
+    /**
+     * 节点路径名称
+     */
+    private String pathName;
+
+    /**
+     * 深度
+     */
+    private Integer deep;
+
+    /**
+     * 更新人
+     */
+    private Long updatedBy;
+
+    /**
+     * 记录更新时间
+     */
+    private LocalDateTime updateTime;
+
+    /**
+     * 创建人
+     */
+    private Long createdBy;
+
+    /**
+     * 记录创建时间
+     */
+    private LocalDateTime createTime;
+
+    /**
+     * 租户号
+     */
+    private Integer tenantId;
+
+
+}

+ 156 - 0
service-rule/service-rule-biz/src/main/java/com/usky/rule/domain/DmpDevice.java

@@ -0,0 +1,156 @@
+package com.usky.rule.domain;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import java.time.LocalDateTime;
+import java.io.Serializable;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * <p>
+ * 设备信息表
+ * </p>
+ *
+ * @author zyj
+ * @since 2026-03-17
+ */
+@Data
+@EqualsAndHashCode(callSuper = false)
+public class DmpDevice implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 主键id
+     */
+    @TableId(value = "id", type = IdType.AUTO)
+    private Integer id;
+
+    /**
+     * 设备ID;设备注册时系统自动生成一个唯一编号
+     */
+    private String deviceId;
+
+    /**
+     * 设备名称
+     */
+    private String deviceName;
+
+    /**
+     * 设备类型(501、监控系统  502、门禁系统  503、梯控系统  504、机房系统  509、环境系统  510、照明系统)
+     */
+    private Integer deviceType;
+
+    /**
+     * 产品ID
+     */
+    private Integer productId;
+
+    /**
+     * 物联网卡号
+     */
+    private String simCode;
+
+    /**
+     * 国际移动用户识别码
+     */
+    private String imsiCode;
+
+    /**
+     * 自动订阅标识(0:否,1:是)
+     */
+    private Integer subscribeFlag;
+
+    /**
+     * 节点类型
+     */
+    private Integer nodeType;
+
+    /**
+     * 分组id
+     */
+    private Integer groupId;
+
+    /**
+     * 删除标识
+     */
+    private Integer deleteFlag;
+
+    /**
+     * 创建人
+     */
+    private String createdBy;
+
+    /**
+     * 创建时间
+     */
+    private LocalDateTime createdTime;
+
+    /**
+     * 更新人
+     */
+    private String updatedBy;
+
+    /**
+     * 更新时间
+     */
+    private LocalDateTime updatedTime;
+
+    /**
+     * 租户号
+     */
+    private Integer tenantId;
+
+    /**
+     * 单位编号
+     */
+    private String companyCode;
+
+    /**
+     * 安装位置
+     */
+    private String installAddress;
+
+    /**
+     * 业务状态;1:未激活,2:已激活,3:禁用
+     */
+    private Integer serviceStatus;
+
+    /**
+     * 产品编码
+     */
+    private String productCode;
+
+    /**
+     * 设备uuid
+     */
+    private String deviceUuid;
+
+    /**
+     * 经度
+     */
+    private String longitude;
+
+    /**
+     * 纬度
+     */
+    private String latitude;
+
+    /**
+     * 设备所属类型(1、普通设备  2、网关设备  3、网关子设备)
+     */
+    private Integer categoryType;
+
+    /**
+     * 所属网关
+     */
+    private String gatewayUuid;
+
+    /**
+     * 建筑ID
+     */
+    private Integer buildId;
+
+
+}

+ 166 - 0
service-rule/service-rule-biz/src/main/java/com/usky/rule/domain/DmpProduct.java

@@ -0,0 +1,166 @@
+package com.usky.rule.domain;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import java.time.LocalDateTime;
+import java.io.Serializable;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * <p>
+ * 产品信息表
+ * </p>
+ *
+ * @author zyj
+ * @since 2026-03-17
+ */
+@Data
+@EqualsAndHashCode(callSuper = false)
+public class DmpProduct implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 主键id
+     */
+    @TableId(value = "id", type = IdType.AUTO)
+    private Integer id;
+
+    /**
+     * 产品名称
+     */
+    private String productName;
+
+    /**
+     * 接入方式(1、设备直连  2、网关接入)
+     */
+    private Integer accessMode;
+
+    /**
+     * 网络类型(1、WIFI  2、移动蜂窝数据 3、NB-IoT 4、以太网)
+     */
+    private Integer networkType;
+
+    /**
+     * 设备类型(501、监控系统  502、门禁系统  503、梯控系统  504、机房系统  509、环境系统  510、照明系统)
+     */
+    private Integer deviceType;
+
+    /**
+     * 通信协议(1、MQTT  2、TCP设备直连 3、HTTP)
+     */
+    private Integer comProtocol;
+
+    /**
+     * 认证方式
+     */
+    private String authMode;
+
+    /**
+     * 设备型号
+     */
+    private String deviceModel;
+
+    /**
+     * 产品描述
+     */
+    private String productDescribe;
+
+    /**
+     * 厂家名称
+     */
+    private String factoryName;
+
+    /**
+     * 厂家联系人
+     */
+    private String factoryPerson;
+
+    /**
+     * 厂家联系电话
+     */
+    private String factoryPhone;
+
+    /**
+     * 资质证书1
+     */
+    private String certificateUrl1;
+
+    /**
+     * 资质证书2
+     */
+    private String certificateUrl2;
+
+    /**
+     * 资质证书3
+     */
+    private String certificateUrl3;
+
+    /**
+     * 协议文档
+     */
+    private String agreementUrl;
+
+    /**
+     * 删除标识
+     */
+    private Integer deleteFlag;
+
+    /**
+     * 创建人
+     */
+    private String createdBy;
+
+    /**
+     * 创建时间
+     */
+    private LocalDateTime createdTime;
+
+    /**
+     * 更新人
+     */
+    private String updatedBy;
+
+    /**
+     * 更新时间
+     */
+    private LocalDateTime updatedTime;
+
+    /**
+     * 租户号
+     */
+    private Integer tenantId;
+
+    /**
+     * 产品编码
+     */
+    private String productCode;
+
+    /**
+     * 项目ID
+     */
+    private Long projectId;
+
+    /**
+     * 产品模板ID
+     */
+    private Long productTemplateId;
+
+    /**
+     * 产品模板编码
+     */
+    private String productTemplateCode;
+
+    /**
+     * 产品分类ID
+     */
+    private Long productCategoryId;
+
+    /**
+     * 产品厂商ID
+     */
+    private Long productManufacturerId;
+
+
+}

+ 142 - 0
service-rule/service-rule-biz/src/main/java/com/usky/rule/domain/DmpProductAttribute.java

@@ -0,0 +1,142 @@
+package com.usky.rule.domain;
+
+import java.math.BigDecimal;
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import java.time.LocalDateTime;
+import java.io.Serializable;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * <p>
+ * 产品属性表
+ * </p>
+ *
+ * @author zyj
+ * @since 2026-03-17
+ */
+@Data
+@EqualsAndHashCode(callSuper = false)
+public class DmpProductAttribute implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 主键id
+     */
+    @TableId(value = "id", type = IdType.AUTO)
+    private Integer id;
+
+    /**
+     * 产品ID
+     */
+    private Integer productId;
+
+    /**
+     * 属性名称
+     */
+    private String attributeName;
+
+    /**
+     * 属性标识
+     */
+    private String attributeCode;
+
+    /**
+     * 对应端口/属性ID
+     */
+    private Integer attributePort;
+
+    /**
+     * 属性类型;1:必选,2:可选
+     */
+    private Integer attributeType;
+
+    /**
+     * 数据类型(1、数值型 2、字符型 3、bool型)
+     */
+    private Integer dataType;
+
+    /**
+     * 绑定状态;1:已绑定,2:未绑定
+     */
+    private Integer bindStatus;
+
+    /**
+     * 长度
+     */
+    private Integer attributeLength;
+
+    /**
+     * 单位
+     */
+    private String attributeUnit;
+
+    /**
+     * 最大值
+     */
+    private BigDecimal maximum;
+
+    /**
+     * 最小值
+     */
+    private BigDecimal minimum;
+
+    /**
+     * 时间格式
+     */
+    private String timeFormat;
+
+    /**
+     * 布尔值false
+     */
+    private String boolFalse;
+
+    /**
+     * 布尔值true
+     */
+    private String boolTrue;
+
+    /**
+     * 删除标识;0:未删除,1:已删除
+     */
+    private Integer deleteFlag;
+
+    /**
+     * 描述
+     */
+    private String attributeDescribe;
+
+    /**
+     * 创建人
+     */
+    private String createdBy;
+
+    /**
+     * 创建时间
+     */
+    private LocalDateTime createdTime;
+
+    /**
+     * 更新人
+     */
+    private String updatedBy;
+
+    /**
+     * 更新时间
+     */
+    private LocalDateTime updatedTime;
+
+    /**
+     * 租户号
+     */
+    private Integer tenantId;
+
+    /**
+     * 属性字典
+     */
+    private String attributeDict;
+
+
+}

+ 112 - 0
service-rule/service-rule-biz/src/main/java/com/usky/rule/domain/DmpProductCommand.java

@@ -0,0 +1,112 @@
+package com.usky.rule.domain;
+
+import java.math.BigDecimal;
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import java.time.LocalDateTime;
+import java.io.Serializable;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * <p>
+ * 产品命令表
+ * </p>
+ *
+ * @author zyj
+ * @since 2026-03-20
+ */
+@Data
+@EqualsAndHashCode(callSuper = false)
+public class DmpProductCommand implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 主键id
+     */
+    @TableId(value = "id", type = IdType.AUTO)
+    private Integer id;
+
+    /**
+     * 产品编码
+     */
+    private String productCode;
+
+    /**
+     * 命令编码
+     */
+    private String commandCode;
+
+    /**
+     * 命令名称
+     */
+    private String commandName;
+
+    /**
+     * 数据类型(1、状态量 2、模拟量)
+     */
+    private Integer dataType;
+
+    /**
+     * 单位
+     */
+    private String commandUnit;
+
+    /**
+     * 最大值
+     */
+    private BigDecimal maximum;
+
+    /**
+     * 最小值
+     */
+    private BigDecimal minimum;
+
+    /**
+     * 命令字典
+     */
+    private String commandDict;
+
+    /**
+     * 命令描述
+     */
+    private String commandDescribe;
+
+    /**
+     * 备注
+     */
+    private String remark;
+
+    /**
+     * 删除标识;0:未删除,1:已删除
+     */
+    private Integer deleteFlag;
+
+    /**
+     * 创建人
+     */
+    private String createdBy;
+
+    /**
+     * 创建时间
+     */
+    private LocalDateTime createdTime;
+
+    /**
+     * 更新人
+     */
+    private String updatedBy;
+
+    /**
+     * 更新时间
+     */
+    private LocalDateTime updatedTime;
+
+    /**
+     * 租户号
+     */
+    private Integer tenantId;
+
+
+}

+ 40 - 0
service-rule/service-rule-biz/src/main/java/com/usky/rule/domain/RuleEngine.java

@@ -0,0 +1,40 @@
+package com.usky.rule.domain;
+
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import lombok.Data;
+import retrofit2.http.Field;
+
+import java.io.Serializable;
+import java.time.LocalDateTime;
+import java.util.Date;
+
+/**
+ * 对应表 rule_engine(数据规则引擎)
+ */
+@Data
+public class RuleEngine implements Serializable {
+    private Long id;
+    private Long projectId;
+    private String name;
+    private Long spaceId;
+    /** 规则状态 1:启用 0:停用 */
+    private Integer status;
+    private String descr;
+    /** JSON:规则详情(条件、动作等与前端约定) */
+    private String detail;
+    private String updatedBy;
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private LocalDateTime updateTime;
+    private String createdBy;
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private LocalDateTime createTime;
+
+    /**
+     * 租户号
+     */
+    private Integer tenantId;
+
+    @TableField(exist = false)
+    private String spaceName;
+}

+ 32 - 0
service-rule/service-rule-biz/src/main/java/com/usky/rule/domain/RuleEngineCondition.java

@@ -0,0 +1,32 @@
+package com.usky.rule.domain;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+import java.util.Date;
+
+/**
+ * 规则引擎条件字典(表 rule_engine_condition),用于条件下拉
+ * type 1:触发条件 2:约束条件
+ */
+@Data
+public class RuleEngineCondition {
+    private Long id;
+    private String optionalCondition;
+    private String expression;
+    private String descr;
+    /** 类型 1:触发条件 2:约束条件 */
+    private Integer type;
+    private String updatedBy;
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private LocalDateTime updateTime;
+    private String createdBy;
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private LocalDateTime createTime;
+
+    /**
+     * 租户号
+     */
+    private Integer tenantId;
+}

+ 28 - 0
service-rule/service-rule-biz/src/main/java/com/usky/rule/domain/RuleEngineCron.java

@@ -0,0 +1,28 @@
+package com.usky.rule.domain;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+import java.util.Date;
+
+/**
+ * 规则引擎 CRON(表 rule_engine_cron)
+ */
+@Data
+public class RuleEngineCron {
+    private Long id;
+    private Long ruleEngineId;
+    private String cron;
+    private String updatedBy;
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private LocalDateTime updateTime;
+    private String createdBy;
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private LocalDateTime createTime;
+
+    /**
+     * 租户号
+     */
+    private Integer tenantId;
+}

+ 32 - 0
service-rule/service-rule-biz/src/main/java/com/usky/rule/domain/RuleEngineDevice.java

@@ -0,0 +1,32 @@
+package com.usky.rule.domain;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+import java.util.Date;
+
+/**
+ * 对应表 rule_engine_device(规则绑定设备+属性标识)
+ */
+@Data
+public class RuleEngineDevice {
+    private Long id;
+    private String deviceId;
+    /** 设备 UUID(与 device_id 并存,上报/外部系统常以 UUID 标识设备) */
+    private String deviceUuid;
+    private String identifier;
+    private Long ruleEngineId;
+    private Long productId;
+    private String updatedBy;
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private LocalDateTime updateTime;
+    private String createdBy;
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private LocalDateTime createTime;
+
+    /**
+     * 租户号
+     */
+    private Integer tenantId;
+}

+ 40 - 0
service-rule/service-rule-biz/src/main/java/com/usky/rule/domain/RuleEngineLog.java

@@ -0,0 +1,40 @@
+package com.usky.rule.domain;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+import java.util.Date;
+
+/**
+ * 对应表 rule_engine_log(规则执行日志)
+ */
+@Data
+public class RuleEngineLog {
+    private Long id;
+    private Long projectId;
+    private Long ruleEngineId;
+    private String ruleEngineName;
+    /** 自动触发 0:否 1:是 */
+    private Byte autoTrigger;
+    /** 触发类型 device/space/cron */
+    private String triggerType;
+    /** 执行动作 deviceControl/alarmEvent/workOrder */
+    private String action;
+    /** 日志数据 JSON */
+    private String detail;
+    private String content;
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private LocalDateTime time;
+    private String updatedBy;
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private LocalDateTime updateTime;
+    private String createdBy;
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private LocalDateTime createTime;
+
+    /**
+     * 租户号
+     */
+    private Integer tenantId;
+}

+ 62 - 0
service-rule/service-rule-biz/src/main/java/com/usky/rule/engine/RuleConditionEvaluator.java

@@ -0,0 +1,62 @@
+package com.usky.rule.engine;
+
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONObject;
+
+/**
+ * 规则 detail JSON 条件评估(简化版):支持 threshold 比较。
+ * 与 leo-rule-engine 中按 identifier + 表达式匹配的思路一致。
+ */
+public final class RuleConditionEvaluator {
+
+    private RuleConditionEvaluator() {
+    }
+
+    /**
+     * @param detailJson rule_engine.detail
+     * @param value      当前属性值
+     */
+    public static boolean evaluate(String detailJson, Object value) {
+        if (detailJson == null || detailJson.isEmpty()) {
+            return false;
+        }
+        try {
+            JSONObject root = JSON.parseObject(detailJson);
+            if (root.containsKey("threshold")) {
+                double v = toDouble(value);
+                double th = root.getDoubleValue("threshold");
+                String op = root.getString("op");
+                if (op == null) {
+                    op = ">=";
+                }
+                switch (op) {
+                    case ">":
+                        return v > th;
+                    case ">=":
+                        return v >= th;
+                    case "<":
+                        return v < th;
+                    case "<=":
+                        return v <= th;
+                    case "==":
+                        return Double.compare(v, th) == 0;
+                    default:
+                        return v >= th;
+                }
+            }
+            return root.getBooleanValue("match");
+        } catch (Exception e) {
+            return false;
+        }
+    }
+
+    private static double toDouble(Object value) {
+        if (value == null) {
+            return Double.NaN;
+        }
+        if (value instanceof Number) {
+            return ((Number) value).doubleValue();
+        }
+        return Double.parseDouble(String.valueOf(value));
+    }
+}

+ 30 - 0
service-rule/service-rule-biz/src/main/java/com/usky/rule/enums/ActionTypeEnum.java

@@ -0,0 +1,30 @@
+package com.usky.rule.enums;
+
+import com.usky.rule.exception.BizException;
+import java.util.Objects;
+
+public enum ActionTypeEnum {
+    ALARM_EVENT("alarmEvent"),
+    WORK_ORDER("workOrder"),
+    DEVICE_CONTROL("deviceControl");
+
+    private final String type;
+
+    private ActionTypeEnum(String type) {
+        this.type = type;
+    }
+
+    public static ActionTypeEnum getTypeEnum(String type) {
+        for(ActionTypeEnum typeEnum : values()) {
+            if (Objects.equals(typeEnum.getType(), type)) {
+                return typeEnum;
+            }
+        }
+
+        throw new BizException("不支持该类型的执行动作");
+    }
+
+    public String getType() {
+        return this.type;
+    }
+}

+ 28 - 0
service-rule/service-rule-biz/src/main/java/com/usky/rule/enums/ConstraintTypeEnum.java

@@ -0,0 +1,28 @@
+package com.usky.rule.enums;
+
+import java.util.Objects;
+
+public enum ConstraintTypeEnum {
+    CRON("cron"),
+    DEVICE("device");
+
+    private final String type;
+
+    private ConstraintTypeEnum(String type) {
+        this.type = type;
+    }
+
+    public static ConstraintTypeEnum getTypeEnum(String type) {
+        for(ConstraintTypeEnum typeEnum : values()) {
+            if (Objects.equals(typeEnum.getType(), type)) {
+                return typeEnum;
+            }
+        }
+
+        return null;
+    }
+
+    public String getType() {
+        return this.type;
+    }
+}

+ 29 - 0
service-rule/service-rule-biz/src/main/java/com/usky/rule/enums/TimeTypeEnum.java

@@ -0,0 +1,29 @@
+package com.usky.rule.enums;
+
+public enum TimeTypeEnum {
+    HOUR("hour"),
+    DAY("day"),
+    WEEK("week"),
+    MONTH("month"),
+    YEAR("year");
+
+    private final String identifier;
+
+    public String getIdentifier() {
+        return this.identifier;
+    }
+
+    private TimeTypeEnum(String identifier) {
+        this.identifier = identifier;
+    }
+
+    public static TimeTypeEnum get(String identifier) {
+        for(TimeTypeEnum timeTypeEnum : values()) {
+            if (timeTypeEnum.getIdentifier().equals(identifier)) {
+                return timeTypeEnum;
+            }
+        }
+
+        throw new IllegalArgumentException("不支持的时间类型");
+    }
+}

+ 27 - 0
service-rule/service-rule-biz/src/main/java/com/usky/rule/enums/TriggerTypeEnum.java

@@ -0,0 +1,27 @@
+package com.usky.rule.enums;
+
+public enum TriggerTypeEnum {
+    CRON("cron"),
+    SPACE("space"),
+    DEVICE("device");
+
+    private final String type;
+
+    private TriggerTypeEnum(String type) {
+        this.type = type;
+    }
+
+    public static TriggerTypeEnum getTypeEnum(String type) {
+        for(TriggerTypeEnum typeEnum : values()) {
+            if (typeEnum.getType().equals(type)) {
+                return typeEnum;
+            }
+        }
+
+        return null;
+    }
+
+    public String getType() {
+        return this.type;
+    }
+}

+ 22 - 0
service-rule/service-rule-biz/src/main/java/com/usky/rule/enums/TriggerValueTypeEnum.java

@@ -0,0 +1,22 @@
+package com.usky.rule.enums;
+
+public enum TriggerValueTypeEnum {
+    ACQ("采集值", "acq"),
+    CONSUMPTION("用量", "consumption");
+
+    private final String name;
+    private final String value;
+
+    private TriggerValueTypeEnum(String name, String value) {
+        this.name = name;
+        this.value = value;
+    }
+
+    public String getName() {
+        return this.name;
+    }
+
+    public String getValue() {
+        return this.value;
+    }
+}

+ 25 - 0
service-rule/service-rule-biz/src/main/java/com/usky/rule/exception/BizException.java

@@ -0,0 +1,25 @@
+package com.usky.rule.exception;
+
+import org.springframework.http.HttpStatus;
+
+public class BizException extends RuntimeException {
+    private final String code;
+
+    public BizException(String message) {
+        super(message);
+        this.code = "200001";
+    }
+
+    public BizException(String code, String message) {
+        super(message);
+        this.code = code;
+    }
+
+    public String getCode() {
+        return this.code;
+    }
+
+    public HttpStatus getHttpStatus() {
+        return HttpStatus.OK;
+    }
+}

+ 44 - 0
service-rule/service-rule-biz/src/main/java/com/usky/rule/jobs/CommonJob.java

@@ -0,0 +1,44 @@
+package com.usky.rule.jobs;
+
+import com.usky.rule.enums.TriggerTypeEnum;
+import com.usky.rule.vo.action.RuleEngineAction;
+import com.usky.rule.vo.log.RuleEngineDetailLog;
+import com.usky.rule.listeners.CommonListener;
+import com.usky.rule.util.RuleEngineUtil;
+import java.time.LocalDateTime;
+import java.util.ArrayList;
+import java.util.List;
+import javax.annotation.Resource;
+
+import lombok.extern.slf4j.Slf4j;
+import org.quartz.CronTrigger;
+import org.quartz.DisallowConcurrentExecution;
+import org.quartz.Job;
+import org.quartz.JobDataMap;
+import org.quartz.JobExecutionContext;
+
+@DisallowConcurrentExecution
+@Slf4j
+public class CommonJob implements Job {
+    @Resource
+    private RuleEngineUtil ruleEngineUtil;
+
+    public CommonJob() {
+    }
+
+    public void execute(JobExecutionContext context) {
+        JobDataMap dataMap = context.getJobDetail().getJobDataMap();
+        Long ruleEngineId = (Long)dataMap.get("ruleEngineId");
+        Long projectId = (Long)dataMap.get("projectId");
+        Long spaceId = (Long)dataMap.get("spaceId");
+        List<RuleEngineAction> actions = (List)dataMap.get("actions");
+        RuleEngineDetailLog detail = (RuleEngineDetailLog)context.get("detail");
+        if (detail == null) {
+            detail = CommonListener.initRuleEngineDetailLog((CronTrigger)context.getTrigger(), LocalDateTime.now());
+            detail.setConstraints(new ArrayList());
+        }
+        log.info("commonJob start ruleEngineId: {}, detail: {}, spaceId: {}", ruleEngineId, detail, spaceId);
+
+        this.ruleEngineUtil.performMultipleDevicesControl(ruleEngineId, true, TriggerTypeEnum.CRON.getType(), projectId, spaceId, actions, detail);
+    }
+}

+ 310 - 0
service-rule/service-rule-biz/src/main/java/com/usky/rule/jobs/ConsumptionJob.java

@@ -0,0 +1,310 @@
+package com.usky.rule.jobs;
+
+
+import com.usky.common.core.bean.ApiResult;
+import com.usky.common.security.utils.SecurityUtils;
+import com.usky.demo.RemoteTsdbProxyService;
+import com.usky.demo.domain.HistorysInnerRequestVO;
+import com.usky.demo.domain.HistorysInnerResultVO;
+import com.usky.demo.domain.MetricVO;
+import com.usky.rule.domain.RuleEngine;
+import com.usky.rule.enums.TimeTypeEnum;
+import com.usky.rule.enums.TriggerTypeEnum;
+import com.usky.rule.enums.TriggerValueTypeEnum;
+import com.usky.rule.util.JsonUtil;
+import com.usky.rule.vo.DataPointVO;
+import com.usky.rule.vo.Condition;
+import com.usky.rule.vo.Expression;
+import com.usky.rule.vo.RuleEngineDetail;
+import com.usky.rule.vo.TimeRange;
+import com.usky.rule.vo.action.RuleEngineAction;
+import com.usky.rule.vo.constraint.DeviceConstraint;
+import com.usky.rule.vo.log.RuleEngineDetailLog;
+import com.usky.rule.vo.trigger.DeviceTrigger;
+import com.usky.rule.vo.visualization.SimpleVO;
+import com.usky.rule.subscribe.TriggerDeviceUtil;
+import com.usky.rule.util.RuleEngineUtil;
+//import com.usky.rule.DeviceService;
+import com.usky.rule.service.RuleEngineService;
+import java.math.BigDecimal;
+import java.time.DayOfWeek;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.time.temporal.ChronoUnit;
+import java.util.*;
+import javax.annotation.Resource;
+
+import lombok.extern.slf4j.Slf4j;
+import org.cache2k.Cache;
+import org.cache2k.CacheEntry;
+import org.quartz.DisallowConcurrentExecution;
+import org.quartz.Job;
+import org.quartz.JobExecutionContext;
+import org.quartz.JobExecutionException;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.util.Assert;
+import org.springframework.util.CollectionUtils;
+
+@DisallowConcurrentExecution
+@Slf4j
+public class ConsumptionJob implements Job {
+
+    private static final DateTimeFormatter CONSUMPTION_TIME_DISPLAY = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
+    @Resource
+    private Cache<Long, List<DeviceTrigger>> consumptionTriggerCache;
+    @Resource
+    private TriggerDeviceUtil triggerDeviceUtil;
+    @Resource
+    private RuleEngineUtil ruleEngineUtil;
+
+    @Autowired
+    private RemoteTsdbProxyService remoteTsdbProxyService;
+    @Resource
+    private RuleEngineService ruleEngineService;
+
+    public ConsumptionJob() {
+    }
+
+    public void execute(JobExecutionContext context) throws JobExecutionException {
+        String[] times = new String[2];
+        LocalDateTime now = LocalDateTime.now();
+
+        for(CacheEntry<Long, List<DeviceTrigger>> entry : this.consumptionTriggerCache.entries()) {
+            log.info("consumptionTriggerCache: {}, now: {}", entry.getKey(), now);
+            Long engineId = (Long)entry.getKey();
+            RuleEngine ruleEngine = (RuleEngine)this.ruleEngineService.getById(engineId);
+            log.info("ruleEngine: {}", ruleEngine);
+            if (ruleEngine == null || ruleEngine.getStatus() == null || ruleEngine.getStatus() != 1) {
+                continue;
+            }
+            Long spaceId = ruleEngine.getSpaceId();
+            RuleEngineDetail ruleEngineDetail = (RuleEngineDetail) JsonUtil.toObject(ruleEngine.getDetail(), RuleEngineDetail.class);
+            List<RuleEngineAction> actions = this.ruleEngineService.getActions(ruleEngineDetail.getActions());
+            if (!actions.isEmpty()) {
+                RuleEngineDetailLog ruleEngineDetailLog = new RuleEngineDetailLog();
+                boolean spaceTriggerAction = false;
+
+                label77:
+                for(DeviceTrigger trigger : entry.getValue()) {
+                    if (!CollectionUtils.isEmpty(trigger.getDevices())) {
+                        label75:
+                        for(SimpleVO device : trigger.getDevices()) {
+                            List<Condition> meetTriggerConditionList = new ArrayList();
+                            List<Condition> triggerConditions = trigger.getConditions();
+                            StringBuilder boolConstraintExp = new StringBuilder();
+                            Map<String, String> valueMap = new HashMap();
+                            boolConstraintExp.append("(");
+
+                            for(int i = 0; i < triggerConditions.size(); ++i) {
+                                Condition triggerCondition = (Condition)triggerConditions.get(i);
+                                String identifier = triggerCondition.getIdentifier();
+                                Expression expression = triggerCondition.getExpression();
+                                TimeRange timeRange = triggerCondition.getTimeRange();
+                                String valueCondition = triggerCondition.getCondition();
+                                String operator = triggerCondition.getOperator();
+                                if (i != 0 && preAssertFalse(boolConstraintExp, operator)) {
+                                    continue label75;
+                                }
+
+                                initStartTimeAndEndTime(now, times, timeRange);
+
+                                HistorysInnerRequestVO requestVO = new HistorysInnerRequestVO();
+                                requestVO.setDeviceuuid(Collections.singletonList(device.getDeviceUuid()));
+                                requestVO.setMetrics(Collections.singletonList(identifier.toLowerCase()));
+                                requestVO.setStartTime(times[0]);
+                                requestVO.setEndTime(times[1]);
+                                ApiResult<List<HistorysInnerResultVO>> historyApi = remoteTsdbProxyService.queryHistoryDeviceData(requestVO);
+                                List<HistorysInnerResultVO> result = historyApi != null && historyApi.getData() != null
+                                        ? historyApi.getData()
+                                        : Collections.emptyList();
+                                List<DataPointVO> dataPointVOList = findDataPointsForMetric(result, device.getDeviceUuid(), identifier.toLowerCase());
+                                if (dataPointVOList.isEmpty()) {
+                                    boolConstraintExp.append(false);
+                                } else {
+                                    DataPointVO first = dataPointVOList.get(0);
+                                    DataPointVO last = dataPointVOList.get(dataPointVOList.size() - 1);
+                                    if (first.getValue() == null || last.getValue() == null) {
+                                        boolConstraintExp.append(false);
+                                    } else {
+                                        BigDecimal currValue = last.getValue().subtract(first.getValue());
+                                        boolean meetCondition = TriggerDeviceUtil.isMeetConsumptionCondition(currValue, valueCondition, expression.getX());
+                                        if (meetCondition) {
+                                            meetTriggerConditionList.add(triggerCondition);
+                                            valueMap.put(identifier, currValue.toString());
+                                        }
+
+                                        boolConstraintExp.append(meetCondition);
+                                    }
+                                }
+                            }
+
+                            spaceTriggerAction = TriggerDeviceUtil.getBooleanExpressionValue(boolConstraintExp + ")");
+                            if (spaceTriggerAction) {
+                                this.triggerDeviceUtil.setTriggerLog(now, ruleEngineDetailLog, device.getId(), device.getName(), TriggerValueTypeEnum.CONSUMPTION.getValue(), TriggerTypeEnum.DEVICE, meetTriggerConditionList, valueMap);
+                                break label77;
+                            }
+                        }
+                    }
+                }
+
+                if (spaceTriggerAction) {
+                    log.info("consumptionJob spaceTriggerAction true");
+                    boolean cronOk = this.triggerDeviceUtil.meetCronConstraintAction(
+                            this.ruleEngineService.getCronConstraints(ruleEngineDetail.getConstraints()),
+                            ruleEngineDetailLog,
+                            now);
+                    List<DeviceConstraint> deviceConstraints = this.ruleEngineService.getDeviceConstraints(ruleEngineDetail.getConstraints());
+                    boolean deviceOk = this.triggerDeviceUtil.meetConstraintAction(deviceConstraints, ruleEngineDetailLog);
+                    if (cronOk && deviceOk) {
+
+                        log.info("ConsumptionJob constraints satisfied engineId: {}, actions: {}", engineId, actions);
+                        this.ruleEngineUtil.performMultipleDevicesControl(engineId, true, TriggerTypeEnum.DEVICE.getType(), ruleEngine.getProjectId(), spaceId, actions, ruleEngineDetailLog);
+                    }
+                }
+            }
+        }
+
+    }
+
+    public static void initStartTimeAndEndTime(LocalDateTime now, String[] times, TimeRange timeRange) {
+        Integer start = timeRange.getStart();
+        Integer end = timeRange.getEnd();
+        Assert.notNull(start, "start不能为空");
+        Assert.notNull(end, "end不能为空");
+        Assert.isTrue(end > start, "结束时间必须大于起始时间");
+
+        LocalDateTime startTime;
+        LocalDateTime endTime;
+
+        TimeTypeEnum typeEnum = TimeTypeEnum.get(timeRange.getType());
+        Assert.notNull(typeEnum, "不支持的时间类型");
+
+        // 老版本传统 switch 格式
+        switch (typeEnum) {
+            case HOUR:
+                // 支持跨天小时 22~26
+                LocalDateTime todayZeroHour = now.truncatedTo(ChronoUnit.DAYS);
+                startTime = todayZeroHour.withHour(start);
+
+                if (end >= 24) {
+                    endTime = todayZeroHour.plusDays(1).withHour(end - 24);
+                } else {
+                    endTime = todayZeroHour.withHour(end);
+                }
+
+                if (now.isBefore(startTime)) {
+                    startTime = startTime.minusDays(1);
+                    endTime = endTime.minusDays(1);
+                }
+                break;
+            case DAY:
+                // 支持按当月实际天数跨月:start=5, end=33
+                LocalDateTime todayZeroDay = now.truncatedTo(ChronoUnit.DAYS);
+                startTime = todayZeroDay.withDayOfMonth(start);
+                // 结束时间 = 开始时间 + (end-1)天
+                endTime = startTime.plusDays(end - 1);
+
+                if (now.isBefore(startTime)) {
+                    startTime = startTime.minusMonths(1);
+                    endTime = endTime.minusMonths(1);
+                }
+                break;
+            case WEEK:
+                LocalDateTime todayZeroWeek = now.truncatedTo(ChronoUnit.DAYS);
+
+                // 开始时间:本周 星期start(1=周一)
+                DayOfWeek todayWeek = todayZeroWeek.getDayOfWeek();
+                int weekVal = todayWeek.getValue();
+                startTime = todayZeroWeek.plusDays((long) start - weekVal);
+
+                // 结束时间:从开始时间 直接往后加 (end - start) 天
+                // 支持 end > 7,自动跨周、跨N周
+                endTime = startTime.plusDays(end - start);
+
+                // 如果还没到当前周期 → 取上一周
+                if (now.isBefore(startTime)) {
+                    startTime = startTime.minusWeeks(1);
+                    endTime = endTime.minusWeeks(1);
+                }
+                break;
+            default:
+                throw new IllegalArgumentException("不支持的时间类型: " + typeEnum);
+        }
+
+        times[0] = startTime.format(CONSUMPTION_TIME_DISPLAY);
+        times[1] = endTime.format(CONSUMPTION_TIME_DISPLAY);
+        log.debug("initStartTimeAndEndTime type={} startTime={} endTime={}", typeEnum, times[0], times[1]);
+    }
+
+    public static boolean preAssertFalse(StringBuilder boolConstraintExp, String operator) {
+        if (TriggerDeviceUtil.checkOperator(operator)) {
+            throw new RuntimeException("operator的值必须是 && or ||");
+        } else {
+            boolConstraintExp.append(" ").append(operator).append(" ");
+            return !TriggerDeviceUtil.getBooleanExpressionValue(boolConstraintExp + "true)");
+        }
+    }
+
+    private static List<DataPointVO> findDataPointsForMetric(List<HistorysInnerResultVO> historyList, String deviceUuid, String metricIdentifier) {
+        if (CollectionUtils.isEmpty(historyList)) {
+            return Collections.emptyList();
+        }
+        HistorysInnerResultVO row = null;
+        for (HistorysInnerResultVO vo : historyList) {
+            if (vo != null && deviceUuid != null && deviceUuid.equals(vo.getDeviceuuid())) {
+                row = vo;
+                break;
+            }
+        }
+        if (row == null) {
+            row = historyList.get(0);
+        }
+        List<MetricVO> metrics = row.getMetrics();
+        if (CollectionUtils.isEmpty(metrics)) {
+            return Collections.emptyList();
+        }
+        for (MetricVO m : metrics) {
+            if (m != null && metricIdentifier != null && metricIdentifier.equals(m.getMetric())) {
+                return metricItemsToDataPoints(m.getMetricItems());
+            }
+        }
+        return Collections.emptyList();
+    }
+
+    private static List<DataPointVO> metricItemsToDataPoints(List<Map<String, Object>> metricItems) {
+        if (CollectionUtils.isEmpty(metricItems)) {
+            return Collections.emptyList();
+        }
+        List<DataPointVO> out = new ArrayList<>(metricItems.size());
+        for (Map<String, Object> item : metricItems) {
+            if (item == null) {
+                continue;
+            }
+            Object ts = item.get("timestamp");
+            Object val = item.get("value");
+            if (ts == null || val == null) {
+                continue;
+            }
+            BigDecimal num = toBigDecimal(val);
+            if (num == null) {
+                continue;
+            }
+            out.add(new DataPointVO(ts.toString(), num));
+        }
+        return out;
+    }
+
+    private static BigDecimal toBigDecimal(Object val) {
+        if (val instanceof BigDecimal) {
+            return (BigDecimal) val;
+        }
+        if (val instanceof Number) {
+            return BigDecimal.valueOf(((Number) val).doubleValue());
+        }
+        try {
+            return new BigDecimal(val.toString().trim());
+        } catch (Exception e) {
+            return null;
+        }
+    }
+}

+ 93 - 0
service-rule/service-rule-biz/src/main/java/com/usky/rule/listeners/CommonListener.java

@@ -0,0 +1,93 @@
+package com.usky.rule.listeners;
+
+import com.usky.rule.util.CronUtil;
+import com.usky.rule.util.DateTimeUtil;
+import com.usky.rule.enums.TriggerTypeEnum;
+import com.usky.rule.vo.constraint.CronConstraint;
+import com.usky.rule.vo.constraint.DeviceConstraint;
+import com.usky.rule.vo.log.BaseLog;
+import com.usky.rule.vo.log.CronTriggerLog;
+import com.usky.rule.vo.log.RuleEngineDetailLog;
+import com.usky.rule.subscribe.TriggerDeviceUtil;
+import java.time.LocalDateTime;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+import org.quartz.CronTrigger;
+import org.quartz.JobExecutionContext;
+import org.quartz.JobExecutionException;
+import org.quartz.JobListener;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class CommonListener implements JobListener {
+    private static final Logger LOGGER = LoggerFactory.getLogger(CommonListener.class);
+    private final String name;
+    private final TriggerDeviceUtil triggerDeviceUtil;
+    private final List<CronConstraint> cronConstraintList;
+    private final List<DeviceConstraint> deviceConstraintList;
+
+    public CommonListener(String name, TriggerDeviceUtil triggerDeviceUtil, List<CronConstraint> cronConstraintList, List<DeviceConstraint> deviceConstraints) {
+        this.name = name;
+        this.triggerDeviceUtil = triggerDeviceUtil;
+        this.cronConstraintList = cronConstraintList;
+        this.deviceConstraintList = deviceConstraints;
+    }
+
+    public String getName() {
+        return this.name;
+    }
+
+    public void jobToBeExecuted(JobExecutionContext context) {
+        LocalDateTime now = LocalDateTime.now();
+        List<BaseLog> constraintLogs = new ArrayList();
+        RuleEngineDetailLog ruleEngineDetailLog = initRuleEngineDetailLog((CronTrigger)context.getTrigger(), now);
+        ruleEngineDetailLog.setConstraints(constraintLogs);
+        LOGGER.info("Job {} is about to be executed", context.getJobDetail().getKey());
+        if (this.cronConstraintList != null) {
+            Date evalAt = DateTimeUtil.localDateTimeToDate(now);
+            for (CronConstraint cronConstraint : this.cronConstraintList) {
+                if (!CronUtil.isCronSatisfiedBy(cronConstraint.getCron(), evalAt)) {
+                    throw new RuntimeException(" cron condition not met, aborting job execution.");
+                }
+
+                CronTriggerLog cronConstraintLog = new CronTriggerLog();
+                cronConstraintLog.setCronExp(cronConstraint.getCron());
+                cronConstraintLog.setTime(DateTimeUtil.format(now));
+                BaseLog cronBaseLog = new BaseLog();
+                cronBaseLog.setDetail(cronConstraintLog);
+                cronBaseLog.setType(TriggerTypeEnum.CRON.getType());
+                constraintLogs.add(cronBaseLog);
+            }
+        }
+
+        if (this.deviceConstraintList != null && !this.triggerDeviceUtil.meetConstraintAction(this.deviceConstraintList, ruleEngineDetailLog)) {
+            throw new RuntimeException("device constraint condition not met, aborting job execution.");
+        } else {
+            context.put("detail", ruleEngineDetailLog);
+        }
+    }
+
+    public static RuleEngineDetailLog initRuleEngineDetailLog(CronTrigger trigger, LocalDateTime now) {
+        RuleEngineDetailLog ruleEngineDetailLog = new RuleEngineDetailLog();
+        List<BaseLog> triggerLogs = new ArrayList();
+        ruleEngineDetailLog.setTriggers(triggerLogs);
+        String cronExpression = trigger.getCronExpression();
+        CronTriggerLog cronTriggerLog = new CronTriggerLog();
+        cronTriggerLog.setCronExp(cronExpression);
+        cronTriggerLog.setTime(DateTimeUtil.format(now));
+        BaseLog baseLog = new BaseLog();
+        baseLog.setDetail(cronTriggerLog);
+        baseLog.setType(TriggerTypeEnum.CRON.getType());
+        triggerLogs.add(baseLog);
+        return ruleEngineDetailLog;
+    }
+
+    public void jobExecutionVetoed(JobExecutionContext context) {
+        LOGGER.info("Job {} was vetoed from being executed", context.getJobDetail().getKey());
+    }
+
+    public void jobWasExecuted(JobExecutionContext context, JobExecutionException jobException) {
+        LOGGER.info("Job {} was executed", context.getJobDetail().getKey());
+    }
+}

+ 16 - 0
service-rule/service-rule-biz/src/main/java/com/usky/rule/mapper/BaseBuildMapper.java

@@ -0,0 +1,16 @@
+package com.usky.rule.mapper;
+
+import com.usky.rule.domain.BaseBuild;
+import com.usky.common.mybatis.core.CrudMapper;
+
+/**
+ * <p>
+ * 建筑信息 Mapper 接口
+ * </p>
+ *
+ * @author zyj
+ * @since 2026-03-17
+ */
+public interface BaseBuildMapper extends CrudMapper<BaseBuild> {
+
+}

+ 16 - 0
service-rule/service-rule-biz/src/main/java/com/usky/rule/mapper/BaseSpaceMapper.java

@@ -0,0 +1,16 @@
+package com.usky.rule.mapper;
+
+import com.usky.rule.domain.BaseSpace;
+import com.usky.common.mybatis.core.CrudMapper;
+
+/**
+ * <p>
+ * 空间 Mapper 接口
+ * </p>
+ *
+ * @author zyj
+ * @since 2026-03-17
+ */
+public interface BaseSpaceMapper extends CrudMapper<BaseSpace> {
+
+}

+ 16 - 0
service-rule/service-rule-biz/src/main/java/com/usky/rule/mapper/DmpDeviceMapper.java

@@ -0,0 +1,16 @@
+package com.usky.rule.mapper;
+
+import com.usky.rule.domain.DmpDevice;
+import com.usky.common.mybatis.core.CrudMapper;
+
+/**
+ * <p>
+ * 设备信息表 Mapper 接口
+ * </p>
+ *
+ * @author zyj
+ * @since 2026-03-17
+ */
+public interface DmpDeviceMapper extends CrudMapper<DmpDevice> {
+
+}

+ 16 - 0
service-rule/service-rule-biz/src/main/java/com/usky/rule/mapper/DmpProductAttributeMapper.java

@@ -0,0 +1,16 @@
+package com.usky.rule.mapper;
+
+import com.usky.rule.domain.DmpProductAttribute;
+import com.usky.common.mybatis.core.CrudMapper;
+
+/**
+ * <p>
+ * 产品属性表 Mapper 接口
+ * </p>
+ *
+ * @author zyj
+ * @since 2026-03-17
+ */
+public interface DmpProductAttributeMapper extends CrudMapper<DmpProductAttribute> {
+
+}

+ 16 - 0
service-rule/service-rule-biz/src/main/java/com/usky/rule/mapper/DmpProductCommandMapper.java

@@ -0,0 +1,16 @@
+package com.usky.rule.mapper;
+
+import com.usky.rule.domain.DmpProductCommand;
+import com.usky.common.mybatis.core.CrudMapper;
+
+/**
+ * <p>
+ * 产品命令表 Mapper 接口
+ * </p>
+ *
+ * @author zyj
+ * @since 2026-03-20
+ */
+public interface DmpProductCommandMapper extends CrudMapper<DmpProductCommand> {
+
+}

+ 16 - 0
service-rule/service-rule-biz/src/main/java/com/usky/rule/mapper/DmpProductMapper.java

@@ -0,0 +1,16 @@
+package com.usky.rule.mapper;
+
+import com.usky.rule.domain.DmpProduct;
+import com.usky.common.mybatis.core.CrudMapper;
+
+/**
+ * <p>
+ * 产品信息表 Mapper 接口
+ * </p>
+ *
+ * @author zyj
+ * @since 2026-03-17
+ */
+public interface DmpProductMapper extends CrudMapper<DmpProduct> {
+
+}

+ 14 - 0
service-rule/service-rule-biz/src/main/java/com/usky/rule/mapper/RuleEngineConditionMapper.java

@@ -0,0 +1,14 @@
+package com.usky.rule.mapper;
+
+import com.usky.rule.domain.RuleEngineCondition;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+/**
+ * 规则条件字典,供条件下拉;tenantId 为空时不按租户过滤
+ */
+public interface RuleEngineConditionMapper {
+    List<RuleEngineCondition> selectAll();
+    List<RuleEngineCondition> selectByType(@Param("type") Integer type);
+}

+ 18 - 0
service-rule/service-rule-biz/src/main/java/com/usky/rule/mapper/RuleEngineCronMapper.java

@@ -0,0 +1,18 @@
+package com.usky.rule.mapper;
+
+import com.usky.rule.domain.RuleEngineCron;
+import com.usky.rule.vo.RuleEngineCronVO;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+public interface RuleEngineCronMapper {
+    int insert(RuleEngineCron row);
+    int deleteByRuleEngineId(@Param("ruleEngineId") Long ruleEngineId);
+    List<RuleEngineCron> selectByRuleEngineId(@Param("ruleEngineId") Long ruleEngineId);
+
+    /**
+     * 查询所有已启用规则及其 cron(status=1),供定时任务调度使用(对应 leo getTurnedOnCron)
+     */
+    List<RuleEngineCronVO> getTurnedOnCron();
+}

+ 22 - 0
service-rule/service-rule-biz/src/main/java/com/usky/rule/mapper/RuleEngineDeviceMapper.java

@@ -0,0 +1,22 @@
+package com.usky.rule.mapper;
+
+import com.usky.rule.domain.RuleEngineDevice;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+public interface RuleEngineDeviceMapper {
+    /** tenantId 为空时不按租户过滤 */
+    List<RuleEngineDevice> selectByDeviceAndIdentifier(@Param("deviceId") String deviceId,
+                                                        @Param("identifier") String identifier,
+                                                        @Param("ruleEngineId") Long ruleEngineId,
+                                                        @Param("tenantId") Integer tenantId);
+    int insert(RuleEngineDevice row);
+    int deleteByRuleEngineId(@Param("ruleEngineId") Long ruleEngineId);
+    List<RuleEngineDevice> selectByRuleEngineId(@Param("ruleEngineId") Long ruleEngineId);
+    /** 按设备 ID 查询关联的规则绑定(供设备上报触发使用) */
+    List<RuleEngineDevice> selectByDeviceId(@Param("deviceId") String deviceId);
+
+    /** 按设备 UUID 查询关联的规则绑定 */
+    List<RuleEngineDevice> selectByDeviceUuid(@Param("deviceUuid") String deviceUuid);
+}

+ 15 - 0
service-rule/service-rule-biz/src/main/java/com/usky/rule/mapper/RuleEngineLogMapper.java

@@ -0,0 +1,15 @@
+package com.usky.rule.mapper;
+
+import com.usky.common.mybatis.core.CrudMapper;
+import com.usky.rule.domain.RuleEngineLog;
+import com.usky.rule.vo.RuleEngineLogPageRequest;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+public interface RuleEngineLogMapper  extends CrudMapper<RuleEngineLog> {
+    int insert(RuleEngineLog row);
+    int updateById(RuleEngineLog row);
+    int deleteById(@Param("id") Long id);
+    RuleEngineLog selectById(@Param("id") Long id);
+}

+ 18 - 0
service-rule/service-rule-biz/src/main/java/com/usky/rule/mapper/RuleEngineMapper.java

@@ -0,0 +1,18 @@
+package com.usky.rule.mapper;
+
+import com.usky.common.mybatis.core.CrudMapper;
+import com.usky.rule.domain.RuleEngine;
+import com.usky.rule.vo.RuleEnginePageRequest;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+public interface RuleEngineMapper extends CrudMapper<RuleEngine> {
+    int deleteById(@Param("id") Long id);
+    RuleEngine selectById(@Param("id") Long id, @Param("tenantId") Integer tenantId);
+    int updateStatus(@Param("id") Long id, @Param("status") Integer status);
+    /** 按项目启用规则,供 MQ 消费侧加载;tenantId 为空时不按租户过滤 */
+    List<RuleEngine> selectEnabledByProjectId(@Param("projectId") Long projectId, @Param("tenantId") Integer tenantId);
+    /** 查询某空间下的直接子空间 id(用于在应用层递归求所有子孙 id,兼容 MySQL 5.7) */
+    List<Long> selectDirectChildrenSpaceIds(Long parentId);
+}

+ 38 - 0
service-rule/service-rule-biz/src/main/java/com/usky/rule/mq/RuleReportConsumer.java

@@ -0,0 +1,38 @@
+package com.usky.rule.mq;
+
+import com.alibaba.fastjson.JSON;
+import com.usky.rule.mq.vo.DeviceReportPayload;
+import com.usky.rule.subscribe.TriggerDeviceUtil;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
+import org.apache.rocketmq.spring.core.RocketMQListener;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+
+/**
+ * 订阅 RocketMQ 设备上报 topic,触发数据规则(对标 MQTTUtil 订阅后解析并投递规则引擎)。
+ * 配置:rocketmq.consumer.group / topic 与 bootstrap.yml 或 Nacos 中一致。
+ */
+@Slf4j
+@Component
+@RocketMQMessageListener(
+        consumerGroup = "${rocketmq.consumer.group}",
+        topic = "${rocketmq.consumer.topic}"
+)
+public class RuleReportConsumer implements RocketMQListener<String> {
+
+    private final TriggerDeviceUtil triggerDeviceUtil;
+
+    @Value("${spring.application.name:service-rule}")
+    private String appName;
+
+    public RuleReportConsumer(TriggerDeviceUtil triggerDeviceUtil) {
+        this.triggerDeviceUtil = triggerDeviceUtil;
+    }
+
+    @Override
+    public void onMessage(String message) {
+        log.debug("[{}] RuleReportConsumer received: {}", appName, message);
+        triggerDeviceUtil.processMessage(message);
+    }
+}

+ 23 - 0
service-rule/service-rule-biz/src/main/java/com/usky/rule/mq/vo/DeviceReportPayload.java

@@ -0,0 +1,23 @@
+package com.usky.rule.mq.vo;
+
+import lombok.Data;
+
+/**
+ * RocketMQ 设备上报消息体(与 MQTT 上报入 MQ 后结构对齐,供规则触发)。
+ * MQTTUtil 侧多为 topic 订阅后解析 JSON;此处统一为 MQ 字符串反序列化。
+ */
+@Data
+public class DeviceReportPayload {
+    /** 项目 ID */
+    private Long projectId;
+    /** 设备 ID(业务 device_id) */
+    private String deviceId;
+    /** 产品 ID */
+    private Long productId;
+    /** 属性标识,对应 rule_engine_device.identifier */
+    private String identifier;
+    /** 上报值(数值/字符串由 detail 内条件决定) */
+    private Object value;
+    /** 毫秒时间戳,可选 */
+    private Long ts;
+}

+ 78 - 0
service-rule/service-rule-biz/src/main/java/com/usky/rule/schedule/RuleEngineCronScheduler.java

@@ -0,0 +1,78 @@
+package com.usky.rule.schedule;
+
+import com.usky.rule.mapper.RuleEngineCronMapper;
+import com.usky.rule.service.RuleEngineService;
+import com.usky.rule.vo.RuleEngineCronVO;
+import lombok.extern.slf4j.Slf4j;
+import org.quartz.CronExpression;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+
+import java.util.Date;
+import java.util.List;
+
+/**
+ * 数据规则引擎定时任务调度器(参考能耗 leo-rule-engine)。
+ * 按分钟扫描已启用规则的 cron 配置(getTurnedOnCron),若当前时间满足 cron 则触发规则并写 rule_engine_log(trigger_type=cron)。
+ */
+@Slf4j
+@Component
+@ConditionalOnProperty(name = "rule-engine.cron-schedule.enabled", havingValue = "true", matchIfMissing = true)
+public class RuleEngineCronScheduler {
+
+    private final RuleEngineCronMapper ruleEngineCronMapper;
+    private final RuleEngineService ruleEngineService;
+
+    public RuleEngineCronScheduler(RuleEngineCronMapper ruleEngineCronMapper,
+                                   RuleEngineService ruleEngineService) {
+        this.ruleEngineCronMapper = ruleEngineCronMapper;
+        this.ruleEngineService = ruleEngineService;
+    }
+
+    /**
+     * 每分钟整秒执行一次,拉取所有已启用规则及其 cron,匹配当前时间则触发(对应 leo Quartz + getTurnedOnCron)
+     */
+    @Scheduled(cron = "0 * * * * ?")
+    public void runCronRules() {
+        List<RuleEngineCronVO> list;
+        try {
+            list = ruleEngineCronMapper.getTurnedOnCron();
+        } catch (Exception e) {
+            log.warn("getTurnedOnCron error", e);
+            return;
+        }
+        if (list == null || list.isEmpty()) {
+            return;
+        }
+        Date now = new Date();
+        for (RuleEngineCronVO vo : list) {
+            if (vo.getRuleEngineId() == null || vo.getCron() == null || vo.getCron().trim().isEmpty()) {
+                continue;
+            }
+            try {
+                String cronStr = normalizeCron(vo.getCron().trim());
+                CronExpression expression = new CronExpression(cronStr);
+                if (expression.isSatisfiedBy(now)) {
+                    ruleEngineService.triggerByCron(vo.getRuleEngineId());
+                    log.debug("rule cron triggered ruleEngineId={} cron={}", vo.getRuleEngineId(), vo.getCron());
+                }
+            } catch (Exception e) {
+                log.warn("cron parse or trigger error ruleEngineId={} cron={}", vo.getRuleEngineId(), vo.getCron(), e);
+            }
+        }
+    }
+
+    /** Quartz 为 6 段(秒 分 时 日 月 周);若为 5 段(分 时 日 月 周)则前补秒 0 */
+    private static String normalizeCron(String cron) {
+        if (cron == null) {
+            return "0 0 * * * ?";
+        }
+        String s = cron.trim();
+        int n = s.split("\\s+").length;
+        if (n == 5) {
+            return "0 " + s;
+        }
+        return s;
+    }
+}

+ 16 - 0
service-rule/service-rule-biz/src/main/java/com/usky/rule/service/BaseBuildService.java

@@ -0,0 +1,16 @@
+package com.usky.rule.service;
+
+import com.usky.rule.domain.BaseBuild;
+import com.usky.common.mybatis.core.CrudService;
+
+/**
+ * <p>
+ * 建筑信息 服务类
+ * </p>
+ *
+ * @author zyj
+ * @since 2026-03-17
+ */
+public interface BaseBuildService extends CrudService<BaseBuild> {
+
+}

+ 16 - 0
service-rule/service-rule-biz/src/main/java/com/usky/rule/service/BaseSpaceService.java

@@ -0,0 +1,16 @@
+package com.usky.rule.service;
+
+import com.usky.rule.domain.BaseSpace;
+import com.usky.common.mybatis.core.CrudService;
+
+/**
+ * <p>
+ * 空间 服务类
+ * </p>
+ *
+ * @author zyj
+ * @since 2026-03-17
+ */
+public interface BaseSpaceService extends CrudService<BaseSpace> {
+
+}

+ 16 - 0
service-rule/service-rule-biz/src/main/java/com/usky/rule/service/DmpDeviceService.java

@@ -0,0 +1,16 @@
+package com.usky.rule.service;
+
+import com.usky.rule.domain.DmpDevice;
+import com.usky.common.mybatis.core.CrudService;
+
+/**
+ * <p>
+ * 设备信息表 服务类
+ * </p>
+ *
+ * @author zyj
+ * @since 2026-03-17
+ */
+public interface DmpDeviceService extends CrudService<DmpDevice> {
+
+}

+ 16 - 0
service-rule/service-rule-biz/src/main/java/com/usky/rule/service/DmpProductAttributeService.java

@@ -0,0 +1,16 @@
+package com.usky.rule.service;
+
+import com.usky.rule.domain.DmpProductAttribute;
+import com.usky.common.mybatis.core.CrudService;
+
+/**
+ * <p>
+ * 产品属性表 服务类
+ * </p>
+ *
+ * @author zyj
+ * @since 2026-03-17
+ */
+public interface DmpProductAttributeService extends CrudService<DmpProductAttribute> {
+
+}

+ 16 - 0
service-rule/service-rule-biz/src/main/java/com/usky/rule/service/DmpProductCommandService.java

@@ -0,0 +1,16 @@
+package com.usky.rule.service;
+
+import com.usky.rule.domain.DmpProductCommand;
+import com.usky.common.mybatis.core.CrudService;
+
+/**
+ * <p>
+ * 产品命令表 服务类
+ * </p>
+ *
+ * @author zyj
+ * @since 2026-03-20
+ */
+public interface DmpProductCommandService extends CrudService<DmpProductCommand> {
+
+}

+ 16 - 0
service-rule/service-rule-biz/src/main/java/com/usky/rule/service/DmpProductService.java

@@ -0,0 +1,16 @@
+package com.usky.rule.service;
+
+import com.usky.rule.domain.DmpProduct;
+import com.usky.common.mybatis.core.CrudService;
+
+/**
+ * <p>
+ * 产品信息表 服务类
+ * </p>
+ *
+ * @author zyj
+ * @since 2026-03-17
+ */
+public interface DmpProductService extends CrudService<DmpProduct> {
+
+}

+ 13 - 0
service-rule/service-rule-biz/src/main/java/com/usky/rule/service/RuleEngineConditionService.java

@@ -0,0 +1,13 @@
+package com.usky.rule.service;
+
+import com.usky.rule.domain.RuleEngineCondition;
+
+import java.util.List;
+
+/**
+ * 规则条件字典,供条件下拉(规则引擎触发条件/约束条件)
+ */
+public interface RuleEngineConditionService {
+    /** 查询全部条件,或按类型:1 触发条件 2 约束条件,null 表示全部 */
+    List<RuleEngineCondition> listConditions(Integer type);
+}

+ 15 - 0
service-rule/service-rule-biz/src/main/java/com/usky/rule/service/RuleEngineCronService.java

@@ -0,0 +1,15 @@
+package com.usky.rule.service;
+
+import com.usky.rule.vo.RuleEngineCronVO;
+
+import java.util.List;
+
+/**
+ * 规则引擎 Cron 配置服务(对应 leo RuleEngineCronService,供 TriggerCronTask 等使用)
+ */
+public interface RuleEngineCronService {
+    /**
+     * 查询所有已启用规则及其 cron(status=1)
+     */
+    List<RuleEngineCronVO> getTurnedOnCron();
+}

+ 13 - 0
service-rule/service-rule-biz/src/main/java/com/usky/rule/service/RuleEngineDetailService.java

@@ -0,0 +1,13 @@
+package com.usky.rule.service;
+
+import com.usky.rule.vo.RuleEngineDTO;
+
+public interface RuleEngineDetailService {
+    Boolean save(RuleEngineDTO dto);
+
+    Boolean update(RuleEngineDTO dto);
+
+    Boolean remove(Long id);
+
+    Boolean manualPerformDeviceControl(Long id);
+}

+ 11 - 0
service-rule/service-rule-biz/src/main/java/com/usky/rule/service/RuleEngineDeviceService.java

@@ -0,0 +1,11 @@
+package com.usky.rule.service;
+
+import com.usky.rule.vo.RuleEngineDeviceVO;
+
+import java.util.List;
+
+/**
+ * 规则引擎设备绑定服务(对应 leo RuleEngineDeviceService,供 TriggerDeviceUtil 等使用)
+ */
+public interface RuleEngineDeviceService {
+}

+ 15 - 0
service-rule/service-rule-biz/src/main/java/com/usky/rule/service/RuleEngineLogService.java

@@ -0,0 +1,15 @@
+package com.usky.rule.service;
+
+import com.usky.common.core.bean.CommonPage;
+import com.usky.common.mybatis.core.CrudService;
+import com.usky.rule.domain.RuleEngineLog;
+import com.usky.rule.vo.RuleEngineContent;
+import com.usky.rule.vo.RuleEngineLogPageRequest;
+
+public interface RuleEngineLogService  extends CrudService<RuleEngineLog> {
+    void add(RuleEngineLog log);
+    void update(RuleEngineLog log);
+    void remove(Long id);
+    RuleEngineContent getById(Long id);
+    CommonPage<RuleEngineLog> pageList(RuleEngineLogPageRequest request);
+}

+ 56 - 0
service-rule/service-rule-biz/src/main/java/com/usky/rule/service/RuleEngineService.java

@@ -0,0 +1,56 @@
+package com.usky.rule.service;
+
+import com.usky.common.core.bean.CommonPage;
+import com.usky.rule.domain.RuleEngine;
+import com.usky.rule.vo.RuleEngineConfigDTO;
+import com.usky.rule.vo.RuleEngineDTO;
+import com.usky.rule.vo.RuleEngineDetail;
+import com.usky.rule.vo.RuleEnginePageRequest;
+import com.usky.rule.vo.action.RuleEngineAction;
+import com.usky.rule.vo.constraint.CronConstraint;
+import com.usky.rule.vo.constraint.DeviceConstraint;
+import com.usky.rule.vo.trigger.CronTrigger;
+import com.usky.rule.vo.trigger.DeviceTrigger;
+import com.usky.rule.vo.trigger.SpaceTrigger;
+
+import java.util.List;
+
+public interface RuleEngineService {
+    void add(RuleEngine ruleEngine);
+    void update(RuleEngine ruleEngine);
+    void remove(Long id);
+    RuleEngine getById(Long id);
+    CommonPage<RuleEngine> pageList(RuleEnginePageRequest request);
+    /** 查询多条数据(条件同分页,不分页),对应 leo RuleEngineController.list */
+    List<RuleEngine> list(RuleEnginePageRequest request);
+    void updateStatus(Long id, Integer status);
+    List<RuleEngine> listEnabledByProjectId(Long projectId);
+
+    /** 配置规则:保存规则主体 + cron 列表 + 设备绑定(新增或更新) */
+    void ruleEngineConfig(RuleEngineDTO dto);
+
+    /** 获取规则完整配置(规则 + cron 列表 + 设备列表) */
+    RuleEngineConfigDTO getEngineConfig(Long ruleEngineId);
+
+    /** 手动触发规则:写 rule_engine_log,triggerType=manual,autoTrigger=0 */
+    void manualTrigger(Long ruleEngineId);
+
+    /** 定时触发规则:写 rule_engine_log,triggerType=cron,autoTrigger=1(供定时任务调度调用) */
+    void triggerByCron(Long ruleEngineId);
+
+    List<CronTrigger> getCronTriggers(List<RuleEngineDetail.CommonVO> commonVOList);
+
+    List<DeviceTrigger> getDeviceTriggers(List<RuleEngineDetail.CommonVO> commonVOList);
+
+    List<SpaceTrigger> getSpaceTriggers(List<RuleEngineDetail.CommonVO> commonVOList);
+
+    List<CronConstraint> getCronConstraints(List<RuleEngineDetail.CommonVO> commonVOList);
+
+    List<DeviceConstraint> getDeviceConstraints(List<RuleEngineDetail.CommonVO> commonVOList);
+
+    List<RuleEngineAction> getActions(List<RuleEngineDetail.CommonVO> commonVOList);
+
+    String getName(Long ruleEngineId);
+
+    String getDetail(Long ruleEngineId);
+}

+ 20 - 0
service-rule/service-rule-biz/src/main/java/com/usky/rule/service/impl/BaseBuildServiceImpl.java

@@ -0,0 +1,20 @@
+package com.usky.rule.service.impl;
+
+import com.usky.rule.domain.BaseBuild;
+import com.usky.rule.mapper.BaseBuildMapper;
+import com.usky.rule.service.BaseBuildService;
+import com.usky.common.mybatis.core.AbstractCrudService;
+import org.springframework.stereotype.Service;
+
+/**
+ * <p>
+ * 建筑信息 服务实现类
+ * </p>
+ *
+ * @author zyj
+ * @since 2026-03-17
+ */
+@Service
+public class BaseBuildServiceImpl extends AbstractCrudService<BaseBuildMapper, BaseBuild> implements BaseBuildService {
+
+}

+ 20 - 0
service-rule/service-rule-biz/src/main/java/com/usky/rule/service/impl/BaseSpaceServiceImpl.java

@@ -0,0 +1,20 @@
+package com.usky.rule.service.impl;
+
+import com.usky.rule.domain.BaseSpace;
+import com.usky.rule.mapper.BaseSpaceMapper;
+import com.usky.rule.service.BaseSpaceService;
+import com.usky.common.mybatis.core.AbstractCrudService;
+import org.springframework.stereotype.Service;
+
+/**
+ * <p>
+ * 空间 服务实现类
+ * </p>
+ *
+ * @author zyj
+ * @since 2026-03-17
+ */
+@Service
+public class BaseSpaceServiceImpl extends AbstractCrudService<BaseSpaceMapper, BaseSpace> implements BaseSpaceService {
+
+}

+ 20 - 0
service-rule/service-rule-biz/src/main/java/com/usky/rule/service/impl/DmpDeviceServiceImpl.java

@@ -0,0 +1,20 @@
+package com.usky.rule.service.impl;
+
+import com.usky.rule.domain.DmpDevice;
+import com.usky.rule.mapper.DmpDeviceMapper;
+import com.usky.rule.service.DmpDeviceService;
+import com.usky.common.mybatis.core.AbstractCrudService;
+import org.springframework.stereotype.Service;
+
+/**
+ * <p>
+ * 设备信息表 服务实现类
+ * </p>
+ *
+ * @author zyj
+ * @since 2026-03-17
+ */
+@Service
+public class DmpDeviceServiceImpl extends AbstractCrudService<DmpDeviceMapper, DmpDevice> implements DmpDeviceService {
+
+}

Деякі файли не було показано, через те що забагато файлів було змінено