From f408d72159040815ee1a983ca345387659ee4f82 Mon Sep 17 00:00:00 2001 From: bestfeng1020 <31398465+bestfeng1020@users.noreply.github.com> Date: Fri, 21 Oct 2022 19:06:52 +0800 Subject: [PATCH] =?UTF-8?q?=E5=90=8C=E6=AD=A52.0=E7=9B=B8=E5=85=B3?= =?UTF-8?q?=E5=8A=9F=E8=83=BD=E4=BB=A3=E7=A0=81=20(#206)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../community/doc/QueryConditionOnly.java | 27 + .../micrometer/MeterRegistryCustomizer.java | 19 + .../micrometer/MeterRegistrySettings.java | 23 + .../community/web/ErrorControllerAdvice.java | 12 + .../common-component/messages_en.properties | 3 +- .../common-component/messages_zh.properties | 2 +- .../notify-component/notify-voice/pom.xml | 28 + .../voice/VoiceNotifierConfiguration.java | 19 + .../community/notify/voice/VoiceProvider.java | 20 + .../voice/aliyun/AliyunNotifierProvider.java | 80 ++ .../voice/aliyun/AliyunVoiceNotifier.java | 116 +++ .../voice/aliyun/AliyunVoiceTemplate.java | 68 ++ .../voice/VoiceNotifierConfigurationTest.java | 20 + .../aliyun/AliyunNotifierProviderTest.java | 90 ++ .../voice/aliyun/AliyunVoiceNotifierTest.java | 69 ++ .../notify-component/notify-webhook/pom.xml | 29 + .../notify/webhook/WebHookProvider.java | 21 + .../webhook/http/HttpWebHookNotifier.java | 96 +++ .../http/HttpWebHookNotifierProvider.java | 62 ++ .../webhook/http/HttpWebHookProperties.java | 32 + .../webhook/http/HttpWebHookTemplate.java | 108 +++ .../webhook/http/HttpWebHookTemplateTest.java | 49 ++ jetlinks-components/notify-component/pom.xml | 2 + .../configuration/ThingsConfiguration.java | 4 +- ...ot.autoconfigure.AutoConfiguration.imports | 1 + .../community/auth/web/MenuController.java | 7 +- .../DeviceManagerConfiguration.java | 46 +- .../device/entity/DeviceLatestData.java | 34 + .../entity/DeviceMetadataMappingEntity.java | 111 +++ .../device/entity/ProtocolSupportEntity.java | 27 +- .../DeviceInstanceImportExportEntity.java | 2 +- .../community/device/enums/DeviceFeature.java | 15 +- .../device/events/DeviceDeployedEvent.java | 16 + .../device/events/DeviceUnregisterEvent.java | 20 + .../handler/DeviceProductDeployHandler.java | 67 +- .../message/DeviceMessageMeasurement.java | 79 +- .../DeviceMessageMeasurementProvider.java | 3 +- .../device/relation/DeviceObjectProvider.java | 107 +++ .../device/response/DeviceDeployResult.java | 17 +- .../device/response/DeviceDetail.java | 50 ++ .../DefaultDeviceConfigMetadataManager.java | 13 +- .../DefaultDeviceConfigMetadataSupplier.java | 60 +- .../service/DeviceConfigMetadataManager.java | 7 +- .../service/DeviceEntityEventHandler.java | 133 +++ .../device/service/DeviceProductHandler.java | 60 ++ .../service/LocalDeviceInstanceService.java | 769 +++++++++++++----- .../service/LocalDeviceProductService.java | 12 + .../data/DatabaseDeviceLatestDataService.java | 651 +++++++++++++++ .../service/data/DeviceDataService.java | 4 + .../service/data/DeviceLatestDataService.java | 138 ++++ .../data/NonDeviceLatestDataService.java | 54 ++ .../device/service/data/StorageConstants.java | 38 + .../StorageDeviceConfigMetadataSupplier.java | 94 +++ .../spi/DeviceConfigMetadataSupplier.java | 11 + .../device/web/DeviceInstanceController.java | 131 ++- .../device/web/DeviceProductController.java | 112 ++- .../web/excel/PropertyMetadataExcelInfo.java | 321 ++++++++ .../web/excel/PropertyMetadataWrapper.java | 56 ++ .../device-manager/messages_zh.properties | 8 +- .../service/NetworkEntityEventHandler.java | 113 +++ .../ProtocolDataReferenceProvider.java | 47 ++ jetlinks-manager/notify-manager/pom.xml | 13 + .../notify/manager/subscriber/Notify.java | 10 + .../subscriber/SubscriberProvider.java | 6 + .../subscriber/providers/AlarmProvider.java | 125 +++ .../providers/DeviceAlarmProvider.java | 74 -- .../manager/web/NotifierController.java | 44 +- .../web/NotifierTemplateController.java | 82 +- .../manager/web/response/TemplateInfo.java | 28 + .../RuleEngineManagerConfiguration.java | 16 + .../measurement/AlarmDashboardDefinition.java | 21 + .../measurement/AlarmObjectDefinition.java | 21 + .../AlarmRecordMeasurementProvider.java | 51 ++ .../AlarmRecordRankMeasurement.java | 135 +++ .../AlarmRecordTrendMeasurement.java | 101 +++ .../measurement/AlarmTimeSeriesMetric.java | 23 + .../rule/engine/scene/DeviceTrigger.java | 66 +- .../rule/engine/scene/SceneActions.java | 21 + .../engine/scene/SceneConditionAction.java | 25 + .../rule/engine/scene/SceneRule.java | 231 +++++- .../scene/SceneTaskExecutorProvider.java | 5 +- .../engine/web/AlarmConfigController.java | 8 +- .../rule/engine/web/SceneController.java | 2 + .../src/main/resources/application-dev.yml | 191 +++++ 84 files changed, 5282 insertions(+), 450 deletions(-) create mode 100755 jetlinks-components/common-component/src/main/java/org/jetlinks/community/doc/QueryConditionOnly.java create mode 100644 jetlinks-components/common-component/src/main/java/org/jetlinks/community/micrometer/MeterRegistryCustomizer.java create mode 100644 jetlinks-components/common-component/src/main/java/org/jetlinks/community/micrometer/MeterRegistrySettings.java create mode 100644 jetlinks-components/notify-component/notify-voice/pom.xml create mode 100755 jetlinks-components/notify-component/notify-voice/src/main/java/org/jetlinks/community/notify/voice/VoiceNotifierConfiguration.java create mode 100755 jetlinks-components/notify-component/notify-voice/src/main/java/org/jetlinks/community/notify/voice/VoiceProvider.java create mode 100755 jetlinks-components/notify-component/notify-voice/src/main/java/org/jetlinks/community/notify/voice/aliyun/AliyunNotifierProvider.java create mode 100755 jetlinks-components/notify-component/notify-voice/src/main/java/org/jetlinks/community/notify/voice/aliyun/AliyunVoiceNotifier.java create mode 100755 jetlinks-components/notify-component/notify-voice/src/main/java/org/jetlinks/community/notify/voice/aliyun/AliyunVoiceTemplate.java create mode 100644 jetlinks-components/notify-component/notify-voice/src/test/java/org/jetlinks/community/notify/voice/VoiceNotifierConfigurationTest.java create mode 100644 jetlinks-components/notify-component/notify-voice/src/test/java/org/jetlinks/community/notify/voice/aliyun/AliyunNotifierProviderTest.java create mode 100644 jetlinks-components/notify-component/notify-voice/src/test/java/org/jetlinks/community/notify/voice/aliyun/AliyunVoiceNotifierTest.java create mode 100644 jetlinks-components/notify-component/notify-webhook/pom.xml create mode 100644 jetlinks-components/notify-component/notify-webhook/src/main/java/org/jetlinks/community/notify/webhook/WebHookProvider.java create mode 100644 jetlinks-components/notify-component/notify-webhook/src/main/java/org/jetlinks/community/notify/webhook/http/HttpWebHookNotifier.java create mode 100644 jetlinks-components/notify-component/notify-webhook/src/main/java/org/jetlinks/community/notify/webhook/http/HttpWebHookNotifierProvider.java create mode 100644 jetlinks-components/notify-component/notify-webhook/src/main/java/org/jetlinks/community/notify/webhook/http/HttpWebHookProperties.java create mode 100644 jetlinks-components/notify-component/notify-webhook/src/main/java/org/jetlinks/community/notify/webhook/http/HttpWebHookTemplate.java create mode 100644 jetlinks-components/notify-component/notify-webhook/src/test/java/org/jetlinks/community/notify/webhook/http/HttpWebHookTemplateTest.java create mode 100644 jetlinks-components/things-component/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports create mode 100755 jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/entity/DeviceLatestData.java create mode 100644 jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/entity/DeviceMetadataMappingEntity.java create mode 100755 jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/events/DeviceDeployedEvent.java create mode 100644 jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/events/DeviceUnregisterEvent.java create mode 100644 jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/relation/DeviceObjectProvider.java create mode 100644 jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/service/DeviceEntityEventHandler.java create mode 100644 jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/service/DeviceProductHandler.java mode change 100644 => 100755 jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/service/LocalDeviceInstanceService.java create mode 100755 jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/service/data/DatabaseDeviceLatestDataService.java create mode 100755 jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/service/data/DeviceLatestDataService.java create mode 100644 jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/service/data/NonDeviceLatestDataService.java create mode 100755 jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/service/data/StorageConstants.java create mode 100755 jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/service/data/StorageDeviceConfigMetadataSupplier.java create mode 100644 jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/web/excel/PropertyMetadataExcelInfo.java create mode 100644 jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/web/excel/PropertyMetadataWrapper.java create mode 100644 jetlinks-manager/network-manager/src/main/java/org/jetlinks/community/network/manager/service/NetworkEntityEventHandler.java create mode 100644 jetlinks-manager/network-manager/src/main/java/org/jetlinks/community/network/manager/service/ProtocolDataReferenceProvider.java create mode 100755 jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/subscriber/providers/AlarmProvider.java delete mode 100644 jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/subscriber/providers/DeviceAlarmProvider.java mode change 100644 => 100755 jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/web/NotifierController.java create mode 100644 jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/web/response/TemplateInfo.java create mode 100644 jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/measurement/AlarmDashboardDefinition.java create mode 100755 jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/measurement/AlarmObjectDefinition.java create mode 100644 jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/measurement/AlarmRecordMeasurementProvider.java create mode 100644 jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/measurement/AlarmRecordRankMeasurement.java create mode 100644 jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/measurement/AlarmRecordTrendMeasurement.java create mode 100755 jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/measurement/AlarmTimeSeriesMetric.java create mode 100644 jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/scene/SceneActions.java create mode 100644 jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/scene/SceneConditionAction.java create mode 100644 jetlinks-standalone/src/main/resources/application-dev.yml diff --git a/jetlinks-components/common-component/src/main/java/org/jetlinks/community/doc/QueryConditionOnly.java b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/doc/QueryConditionOnly.java new file mode 100755 index 00000000..5b3349f7 --- /dev/null +++ b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/doc/QueryConditionOnly.java @@ -0,0 +1,27 @@ +package org.jetlinks.community.doc; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; +import org.hswebframework.ezorm.core.param.Term; + +import java.util.List; + +/** + * 文档专用,描述仅有查询功能的动态查询参数 + * + * @author zhouhao + * @since 1.5 + * @see org.hswebframework.web.api.crud.entity.QueryParamEntity + */ +@Getter +@Setter +public class QueryConditionOnly { + + @Schema(description = "where条件表达式,与terms参数不能共存.语法: name = 张三 and age > 16") + private String where; + + @Schema(description = "查询条件集合") + private List terms; + +} diff --git a/jetlinks-components/common-component/src/main/java/org/jetlinks/community/micrometer/MeterRegistryCustomizer.java b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/micrometer/MeterRegistryCustomizer.java new file mode 100644 index 00000000..27c88f02 --- /dev/null +++ b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/micrometer/MeterRegistryCustomizer.java @@ -0,0 +1,19 @@ +package org.jetlinks.community.micrometer; + +/** + * 监控指标自定义注册接口,用于对指标进行自定义,如添加指标标签等操作 + * + * @author zhouhao + * @since 1.11 + */ +public interface MeterRegistryCustomizer { + + /** + * 在指标首次初始化时调用,可以通过判断metric进行自定义标签 + * + * @param metric 指标 + * @param settings 自定义设置 + */ + void custom(String metric, MeterRegistrySettings settings); + +} diff --git a/jetlinks-components/common-component/src/main/java/org/jetlinks/community/micrometer/MeterRegistrySettings.java b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/micrometer/MeterRegistrySettings.java new file mode 100644 index 00000000..8cfc7796 --- /dev/null +++ b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/micrometer/MeterRegistrySettings.java @@ -0,0 +1,23 @@ +package org.jetlinks.community.micrometer; + +import org.jetlinks.core.metadata.DataType; + +import javax.annotation.Nonnull; + +/** + * 指标注册配置信息 + * + * @author zhouhao + * @since 1.11 + */ +public interface MeterRegistrySettings { + + /** + * 给指标添加标签,用于自定义标签类型.在相应指标实现中会根据类型对数据进行存储 + * + * @param tag 标签key + * @param type 类型 + */ + void addTag(@Nonnull String tag, @Nonnull DataType type); + +} diff --git a/jetlinks-components/common-component/src/main/java/org/jetlinks/community/web/ErrorControllerAdvice.java b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/web/ErrorControllerAdvice.java index 982cdc9b..8181c5b1 100755 --- a/jetlinks-components/common-component/src/main/java/org/jetlinks/community/web/ErrorControllerAdvice.java +++ b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/web/ErrorControllerAdvice.java @@ -6,6 +6,8 @@ import org.hswebframework.ezorm.rdb.exception.DuplicateKeyException; import org.hswebframework.ezorm.rdb.metadata.RDBColumnMetadata; import org.hswebframework.web.crud.web.ResponseMessage; import org.hswebframework.web.i18n.LocaleUtils; +import org.jetlinks.community.reference.DataReferenceInfo; +import org.jetlinks.community.reference.DataReferencedException; import org.jetlinks.core.enums.ErrorCode; import org.jetlinks.core.exception.DeviceOperationException; import org.springframework.core.Ordered; @@ -102,5 +104,15 @@ public class ErrorControllerAdvice { .body(ResponseMessage.error(status.value(), e.getCode().name().toLowerCase(), msg)) ); } + @ExceptionHandler + public Mono>> handleException(DataReferencedException e) { + return e + .getLocalizedMessageReactive() + .map(msg -> { + return ResponseMessage + .>error(400,"error.data.referenced", msg) + .result(e.getReferenceList()); + }); + } } diff --git a/jetlinks-components/common-component/src/main/resources/i18n/common-component/messages_en.properties b/jetlinks-components/common-component/src/main/resources/i18n/common-component/messages_en.properties index 60520654..356138e0 100644 --- a/jetlinks-components/common-component/src/main/resources/i18n/common-component/messages_en.properties +++ b/jetlinks-components/common-component/src/main/resources/i18n/common-component/messages_en.properties @@ -1,3 +1,4 @@ message.device_message_handing=Message sent to device, processing... -error.duplicate_key_detail=Duplicate Data:{0} \ No newline at end of file +error.duplicate_key_detail=Duplicate Data:{0} +error.data.referenced=The data has been used elsewhere \ No newline at end of file diff --git a/jetlinks-components/common-component/src/main/resources/i18n/common-component/messages_zh.properties b/jetlinks-components/common-component/src/main/resources/i18n/common-component/messages_zh.properties index 2b536872..9f9400e1 100644 --- a/jetlinks-components/common-component/src/main/resources/i18n/common-component/messages_zh.properties +++ b/jetlinks-components/common-component/src/main/resources/i18n/common-component/messages_zh.properties @@ -1,3 +1,3 @@ message.device_message_handing=消息已发往设备,处理中... - +error.data.referenced=数据已经被其他地方使用 error.duplicate_key_detail=重复的数据:{0} diff --git a/jetlinks-components/notify-component/notify-voice/pom.xml b/jetlinks-components/notify-component/notify-voice/pom.xml new file mode 100644 index 00000000..6b0941e3 --- /dev/null +++ b/jetlinks-components/notify-component/notify-voice/pom.xml @@ -0,0 +1,28 @@ + + + + notify-component + org.jetlinks.community + 2.0.0-SNAPSHOT + + 4.0.0 + + notify-voice + + + + com.aliyun + aliyun-java-sdk-core + 4.5.2 + + + ${project.groupId} + notify-core + ${project.version} + + + + + \ No newline at end of file diff --git a/jetlinks-components/notify-component/notify-voice/src/main/java/org/jetlinks/community/notify/voice/VoiceNotifierConfiguration.java b/jetlinks-components/notify-component/notify-voice/src/main/java/org/jetlinks/community/notify/voice/VoiceNotifierConfiguration.java new file mode 100755 index 00000000..45eeb701 --- /dev/null +++ b/jetlinks-components/notify-component/notify-voice/src/main/java/org/jetlinks/community/notify/voice/VoiceNotifierConfiguration.java @@ -0,0 +1,19 @@ +package org.jetlinks.community.notify.voice; + +import org.jetlinks.community.notify.template.TemplateManager; +import org.jetlinks.community.notify.voice.aliyun.AliyunNotifierProvider; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class VoiceNotifierConfiguration { + + + @Bean + @ConditionalOnBean(TemplateManager.class) + public AliyunNotifierProvider aliyunNotifierProvider(TemplateManager templateManager) { + return new AliyunNotifierProvider(templateManager); + } + +} diff --git a/jetlinks-components/notify-component/notify-voice/src/main/java/org/jetlinks/community/notify/voice/VoiceProvider.java b/jetlinks-components/notify-component/notify-voice/src/main/java/org/jetlinks/community/notify/voice/VoiceProvider.java new file mode 100755 index 00000000..4526f39a --- /dev/null +++ b/jetlinks-components/notify-component/notify-voice/src/main/java/org/jetlinks/community/notify/voice/VoiceProvider.java @@ -0,0 +1,20 @@ +package org.jetlinks.community.notify.voice; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.jetlinks.community.notify.Provider; + +@Getter +@AllArgsConstructor +public enum VoiceProvider implements Provider { + + aliyun("阿里云") + ; + + private final String name; + + @Override + public String getId() { + return name(); + } +} diff --git a/jetlinks-components/notify-component/notify-voice/src/main/java/org/jetlinks/community/notify/voice/aliyun/AliyunNotifierProvider.java b/jetlinks-components/notify-component/notify-voice/src/main/java/org/jetlinks/community/notify/voice/aliyun/AliyunNotifierProvider.java new file mode 100755 index 00000000..62b3a660 --- /dev/null +++ b/jetlinks-components/notify-component/notify-voice/src/main/java/org/jetlinks/community/notify/voice/aliyun/AliyunNotifierProvider.java @@ -0,0 +1,80 @@ +package org.jetlinks.community.notify.voice.aliyun; + +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.hswebframework.web.i18n.LocaleUtils; +import org.jetlinks.community.notify.*; +import org.jetlinks.core.metadata.ConfigMetadata; +import org.jetlinks.core.metadata.DefaultConfigMetadata; +import org.jetlinks.core.metadata.types.IntType; +import org.jetlinks.core.metadata.types.StringType; +import org.jetlinks.community.notify.template.TemplateManager; +import org.jetlinks.community.notify.template.TemplateProperties; +import org.jetlinks.community.notify.template.TemplateProvider; +import org.jetlinks.community.notify.voice.VoiceProvider; +import reactor.core.publisher.Mono; + +import javax.annotation.Nonnull; + +/** + * + * 阿里云语音通知服务 + * + * + * @author zhouhao + * @since 1.0 + */ +@Slf4j +@AllArgsConstructor +public class AliyunNotifierProvider implements NotifierProvider, TemplateProvider { + + private TemplateManager templateManager; + + @Nonnull + @Override + public Provider getProvider() { + return VoiceProvider.aliyun; + } + + public static final DefaultConfigMetadata templateConfig = new DefaultConfigMetadata("阿里云语音模版", + "https://help.aliyun.com/document_detail/114035.html?spm=a2c4g.11186623.6.561.3d1b3c2dGMXAmk") + .add("ttsCode", "模版ID", "ttsCode", new StringType()) + .add("calledShowNumbers", "被叫显号", "", new StringType()) + .add("CalledNumber", "被叫号码", "", new StringType()) + .add("PlayTimes", "播放次数", "", new IntType()); + + public static final DefaultConfigMetadata notifierConfig = new DefaultConfigMetadata("阿里云通知配置", + "https://help.aliyun.com/document_detail/114035.html?spm=a2c4g.11186623.6.561.3d1b3c2dGMXAmk") + .add("regionId", "regionId", "regionId", new StringType()) + .add("accessKeyId", "accessKeyId", "", new StringType()) + .add("secret", "secret", "", new StringType()); + + @Override + public ConfigMetadata getTemplateConfigMetadata() { + return templateConfig; + } + + @Override + public ConfigMetadata getNotifierConfigMetadata() { + return notifierConfig; + } + + @Override + public Mono createTemplate(TemplateProperties properties) { + return Mono.fromCallable(() -> new AliyunVoiceTemplate().with(properties).validate()) + .as(LocaleUtils::transform); + } + + @Nonnull + @Override + public NotifyType getType() { + return DefaultNotifyType.voice; + } + + @Nonnull + @Override + public Mono createNotifier(@Nonnull NotifierProperties properties) { + return Mono.fromSupplier(() -> new AliyunVoiceNotifier(properties, templateManager)) + .as(LocaleUtils::transform); + } +} diff --git a/jetlinks-components/notify-component/notify-voice/src/main/java/org/jetlinks/community/notify/voice/aliyun/AliyunVoiceNotifier.java b/jetlinks-components/notify-component/notify-voice/src/main/java/org/jetlinks/community/notify/voice/aliyun/AliyunVoiceNotifier.java new file mode 100755 index 00000000..8ab60813 --- /dev/null +++ b/jetlinks-components/notify-component/notify-voice/src/main/java/org/jetlinks/community/notify/voice/aliyun/AliyunVoiceNotifier.java @@ -0,0 +1,116 @@ +package org.jetlinks.community.notify.voice.aliyun; + +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; +import com.aliyuncs.CommonRequest; +import com.aliyuncs.CommonResponse; +import com.aliyuncs.DefaultAcsClient; +import com.aliyuncs.IAcsClient; +import com.aliyuncs.http.MethodType; +import com.aliyuncs.profile.DefaultProfile; +import com.aliyuncs.profile.IClientProfile; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import org.hswebframework.web.exception.BusinessException; +import org.hswebframework.web.logger.ReactiveLogger; +import org.jetlinks.community.notify.*; +import org.jetlinks.community.notify.template.TemplateManager; +import org.jetlinks.community.notify.voice.VoiceProvider; +import org.jetlinks.core.Values; +import reactor.core.publisher.Mono; +import reactor.core.scheduler.Schedulers; + +import javax.annotation.Nonnull; +import java.util.Map; +import java.util.Objects; + +@Slf4j +public class AliyunVoiceNotifier extends AbstractNotifier { + + private final IAcsClient client; + private final int connectTimeout = 1000; + private final int readTimeout = 5000; + + @Getter + private String notifierId; + + private String domain = "dyvmsapi.aliyuncs.com"; + private String regionId = "cn-hangzhou"; + + public AliyunVoiceNotifier(NotifierProperties profile, TemplateManager templateManager) { + super(templateManager); + Map config = profile.getConfiguration(); + DefaultProfile defaultProfile = DefaultProfile.getProfile( + this.regionId = (String) Objects.requireNonNull(config.get("regionId"), "regionId不能为空"), + (String) Objects.requireNonNull(config.get("accessKeyId"), "accessKeyId不能为空"), + (String) Objects.requireNonNull(config.get("secret"), "secret不能为空") + ); + this.client = new DefaultAcsClient(defaultProfile); + this.domain = (String) config.getOrDefault("domain", "dyvmsapi.aliyuncs.com"); + this.notifierId = profile.getId(); + } + + public AliyunVoiceNotifier(IClientProfile profile, TemplateManager templateManager) { + this(new DefaultAcsClient(profile), templateManager); + } + + public AliyunVoiceNotifier(IAcsClient client, TemplateManager templateManager) { + super(templateManager); + this.client = client; + } + + @Override + @Nonnull + public NotifyType getType() { + return DefaultNotifyType.voice; + } + + @Nonnull + @Override + public Provider getProvider() { + return VoiceProvider.aliyun; + } + + @Override + @Nonnull + public Mono send(@Nonnull AliyunVoiceTemplate template, @Nonnull Values context) { + + return Mono.defer(() -> { + try { + CommonRequest request = new CommonRequest(); + request.setSysMethod(MethodType.POST); + request.setSysDomain(domain); + request.setSysVersion("2017-05-25"); + request.setSysAction("SingleCallByTts"); + request.setSysConnectTimeout(connectTimeout); + request.setSysReadTimeout(readTimeout); + request.putQueryParameter("RegionId", regionId); + request.putQueryParameter("CalledShowNumber", template.getCalledShowNumbers()); + request.putQueryParameter("CalledNumber", template.getCalledNumber(context.getAllValues())); + request.putQueryParameter("TtsCode", template.getTtsCode()); + request.putQueryParameter("PlayTimes", String.valueOf(template.getPlayTimes())); + request.putQueryParameter("TtsParam", template.createTtsParam(context.getAllValues())); + + CommonResponse response = client.getCommonResponse(request); + + log.info("发起语音通知完成 {}:{}", response.getHttpResponse().getStatus(), response.getData()); + + JSONObject json = JSON.parseObject(response.getData()); + if (!"ok".equalsIgnoreCase(json.getString("Code"))) { + return Mono.error(new BusinessException(json.getString("Message"), json.getString("Code"))); + } + } catch (Exception e) { + return Mono.error(e); + } + return Mono.empty(); + }).doOnEach(ReactiveLogger.onError(err -> { + log.info("发起语音通知失败", err); + })).subscribeOn(Schedulers.boundedElastic()); + } + + @Override + @Nonnull + public Mono close() { + return Mono.fromRunnable(client::shutdown); + } +} diff --git a/jetlinks-components/notify-component/notify-voice/src/main/java/org/jetlinks/community/notify/voice/aliyun/AliyunVoiceTemplate.java b/jetlinks-components/notify-component/notify-voice/src/main/java/org/jetlinks/community/notify/voice/aliyun/AliyunVoiceTemplate.java new file mode 100755 index 00000000..634f2caf --- /dev/null +++ b/jetlinks-components/notify-component/notify-voice/src/main/java/org/jetlinks/community/notify/voice/aliyun/AliyunVoiceTemplate.java @@ -0,0 +1,68 @@ +package org.jetlinks.community.notify.voice.aliyun; + +import com.alibaba.fastjson.JSON; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; +import org.jetlinks.community.notify.template.AbstractTemplate; +import org.jetlinks.community.notify.template.Template; +import org.jetlinks.community.notify.template.VariableDefinition; +import org.springframework.util.StringUtils; + +import javax.annotation.Nonnull; +import javax.validation.constraints.NotBlank; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +/** + * 阿里云语音验证码通知模版 + *

+ * https://help.aliyun.com/document_detail/114035.html?spm=a2c4g.11186623.6.561.3d1b3c2dGMXAmk + */ +@Getter +@Setter +public class AliyunVoiceTemplate extends AbstractTemplate { + public static final String CALLED_NUMBER_KEY = "calledNumber"; + + @Schema(description = "通知模版ID") + @NotBlank(message = "[ttsCode]不能为空") + private String ttsCode; + + private String calledShowNumbers; + + @NotBlank(message = "[calledNumber]不能为空") + private String calledNumber; + + @Schema(description = "通知播放次数") + private int playTimes = 1; + + private Map ttsParam; + + public String createTtsParam(Map ctx) { + + return JSON.toJSONString(ctx); + } + + public String getCalledNumber(Map ctx) { + return get(CALLED_NUMBER_KEY, ctx, this::getCalledNumber); + } + + @Nonnull + @Override + protected List getEmbeddedVariables() { + //指定了固定的收信人 + if (StringUtils.hasText(calledNumber)) { + return Collections.emptyList(); + } + return Collections.singletonList( + VariableDefinition + .builder() + .id(CALLED_NUMBER_KEY) + .name("收信人") + .description("收信人手机号码") + .required(true) + .build() + ); + } +} diff --git a/jetlinks-components/notify-component/notify-voice/src/test/java/org/jetlinks/community/notify/voice/VoiceNotifierConfigurationTest.java b/jetlinks-components/notify-component/notify-voice/src/test/java/org/jetlinks/community/notify/voice/VoiceNotifierConfigurationTest.java new file mode 100644 index 00000000..f655a988 --- /dev/null +++ b/jetlinks-components/notify-component/notify-voice/src/test/java/org/jetlinks/community/notify/voice/VoiceNotifierConfigurationTest.java @@ -0,0 +1,20 @@ +package org.jetlinks.community.notify.voice; + +import org.jetlinks.community.notify.voice.aliyun.AliyunNotifierProvider; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +/** + * 输入描述. + * + * @author zhangji + * @version 1.11 2022/2/7 + */ +public class VoiceNotifierConfigurationTest { + @Test + void test() { + VoiceNotifierConfiguration configuration = new VoiceNotifierConfiguration(); + AliyunNotifierProvider provider = configuration.aliyunNotifierProvider(null); + Assertions.assertNotNull(provider); + } +} diff --git a/jetlinks-components/notify-component/notify-voice/src/test/java/org/jetlinks/community/notify/voice/aliyun/AliyunNotifierProviderTest.java b/jetlinks-components/notify-component/notify-voice/src/test/java/org/jetlinks/community/notify/voice/aliyun/AliyunNotifierProviderTest.java new file mode 100644 index 00000000..c3547bd6 --- /dev/null +++ b/jetlinks-components/notify-component/notify-voice/src/test/java/org/jetlinks/community/notify/voice/aliyun/AliyunNotifierProviderTest.java @@ -0,0 +1,90 @@ +package org.jetlinks.community.notify.voice.aliyun; + +import com.alibaba.fastjson.JSONObject; +import org.jetlinks.core.metadata.ConfigMetadata; +import org.jetlinks.community.notify.DefaultNotifyType; +import org.jetlinks.community.notify.NotifierProperties; +import org.jetlinks.community.notify.template.TemplateProperties; +import org.jetlinks.community.notify.voice.VoiceProvider; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import reactor.test.StepVerifier; + +import java.util.HashMap; +import java.util.Map; + +/** + * 输入描述. + * + * @author zhangji + * @version 1.11 2022/2/7 + */ +public class AliyunNotifierProviderTest { + private AliyunNotifierProvider provider; + private TemplateProperties templateProperties; + private NotifierProperties notifierProperties; + + private static final String TTS_CODE = "ttsCode"; + private static final String NOTIFIER_ID = "notifier_id"; + + @BeforeEach + void init() { + provider = new AliyunNotifierProvider(null); + + templateProperties = new TemplateProperties(); + templateProperties.setId("test"); + templateProperties.setType(DefaultNotifyType.voice.getId()); + templateProperties.setProvider(VoiceProvider.aliyun.getId()); + AliyunVoiceTemplate aliyunVoiceTemplate = new AliyunVoiceTemplate(); + aliyunVoiceTemplate.setTtsCode(TTS_CODE); + aliyunVoiceTemplate.setCalledNumber("calledNumber"); + templateProperties.setTemplate((JSONObject)JSONObject.toJSON(aliyunVoiceTemplate)); + + notifierProperties = new NotifierProperties(); + notifierProperties.setId(NOTIFIER_ID); + notifierProperties.setType(DefaultNotifyType.voice.getId()); + notifierProperties.setProvider(VoiceProvider.aliyun.getId()); + Map config = new HashMap<>(); + config.put("regionId", "regionId"); + config.put("accessKeyId", "accessKeyId"); + config.put("secret", "secret"); + notifierProperties.setConfiguration(config); + } + + @Test + void test() { + Assertions.assertEquals(VoiceProvider.aliyun, provider.getProvider()); + Assertions.assertEquals(DefaultNotifyType.voice, provider.getType()); + } + + @Test + void getTemplateConfigMetadata() { + ConfigMetadata templateConfig = provider.getTemplateConfigMetadata(); + Assertions.assertNotNull(templateConfig); + Assertions.assertEquals("阿里云语音模版", templateConfig.getName()); + } + + @Test + void getNotifierConfigMetadata() { + ConfigMetadata notifierConfig = provider.getNotifierConfigMetadata(); + Assertions.assertNotNull(notifierConfig); + Assertions.assertEquals("阿里云通知配置", notifierConfig.getName()); + } + + @Test + void createTemplate() { + provider.createTemplate(templateProperties) + .as(StepVerifier::create) + .expectNextMatches(template -> template.getTtsCode().equals(TTS_CODE)) + .verifyComplete(); + } + + @Test + void createNotifier() { + provider.createNotifier(notifierProperties) + .as(StepVerifier::create) + .expectNextMatches(notifier -> notifier.getNotifierId().equals(NOTIFIER_ID)) + .verifyComplete(); + } +} diff --git a/jetlinks-components/notify-component/notify-voice/src/test/java/org/jetlinks/community/notify/voice/aliyun/AliyunVoiceNotifierTest.java b/jetlinks-components/notify-component/notify-voice/src/test/java/org/jetlinks/community/notify/voice/aliyun/AliyunVoiceNotifierTest.java new file mode 100644 index 00000000..df881f24 --- /dev/null +++ b/jetlinks-components/notify-component/notify-voice/src/test/java/org/jetlinks/community/notify/voice/aliyun/AliyunVoiceNotifierTest.java @@ -0,0 +1,69 @@ +package org.jetlinks.community.notify.voice.aliyun; + +import com.aliyuncs.CommonRequest; +import com.aliyuncs.CommonResponse; +import com.aliyuncs.IAcsClient; +import com.aliyuncs.exceptions.ClientException; +import com.aliyuncs.http.HttpResponse; +import org.jetlinks.core.Values; +import org.jetlinks.community.notify.DefaultNotifyType; +import org.jetlinks.community.notify.voice.VoiceProvider; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.springframework.http.HttpStatus; +import reactor.test.StepVerifier; + +import java.util.HashMap; + +/** + * 输入描述. + * + * @author zhangji + * @version 1.11 2022/2/7 + */ +public class AliyunVoiceNotifierTest { + private IAcsClient client; + private AliyunVoiceNotifier notifier; + + private static final String TTS_CODE = "tts_code"; + + @BeforeEach + void init() throws ClientException { + client = Mockito.mock(IAcsClient.class); + CommonResponse response = new CommonResponse(); + HttpResponse httpResponse = new HttpResponse(); + httpResponse.setStatus(HttpStatus.OK.value()); + response.setHttpResponse(httpResponse); + response.setData("{\"Code\":\"ok\"}"); + Mockito.when(client.getCommonResponse(Mockito.any(CommonRequest.class))).thenReturn(response); + + notifier = new AliyunVoiceNotifier(client, null); + } + + @Test + void test() { + Assertions.assertNotNull(notifier); + Assertions.assertEquals(DefaultNotifyType.voice, notifier.getType()); + Assertions.assertEquals(VoiceProvider.aliyun, notifier.getProvider()); + } + + @Test + void send() { + AliyunVoiceTemplate template = new AliyunVoiceTemplate(); + template.setTtsCode(TTS_CODE); + template.setCalledNumber("calledNumber"); + + notifier.send(template, Values.of(new HashMap<>())) + .as(StepVerifier::create) + .verifyComplete(); + } + + @Test + void close() { + notifier.close() + .as(StepVerifier::create) + .verifyComplete(); + } +} diff --git a/jetlinks-components/notify-component/notify-webhook/pom.xml b/jetlinks-components/notify-component/notify-webhook/pom.xml new file mode 100644 index 00000000..da2dea6e --- /dev/null +++ b/jetlinks-components/notify-component/notify-webhook/pom.xml @@ -0,0 +1,29 @@ + + + + notify-component + org.jetlinks.community + 2.0.0-SNAPSHOT + + 4.0.0 + + notify-webhook + + + + org.jetlinks.community + notify-core + ${project.version} + + + org.springframework + spring-webflux + + + + + + + \ No newline at end of file diff --git a/jetlinks-components/notify-component/notify-webhook/src/main/java/org/jetlinks/community/notify/webhook/WebHookProvider.java b/jetlinks-components/notify-component/notify-webhook/src/main/java/org/jetlinks/community/notify/webhook/WebHookProvider.java new file mode 100644 index 00000000..cf17b847 --- /dev/null +++ b/jetlinks-components/notify-component/notify-webhook/src/main/java/org/jetlinks/community/notify/webhook/WebHookProvider.java @@ -0,0 +1,21 @@ +package org.jetlinks.community.notify.webhook; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.jetlinks.community.notify.Provider; + +@Getter +@AllArgsConstructor +public enum WebHookProvider implements Provider { + + http("HTTP") + ; + + private final String name; + + @Override + public String getId() { + return name(); + } +} + diff --git a/jetlinks-components/notify-component/notify-webhook/src/main/java/org/jetlinks/community/notify/webhook/http/HttpWebHookNotifier.java b/jetlinks-components/notify-component/notify-webhook/src/main/java/org/jetlinks/community/notify/webhook/http/HttpWebHookNotifier.java new file mode 100644 index 00000000..08b93549 --- /dev/null +++ b/jetlinks-components/notify-component/notify-webhook/src/main/java/org/jetlinks/community/notify/webhook/http/HttpWebHookNotifier.java @@ -0,0 +1,96 @@ +package org.jetlinks.community.notify.webhook.http; + +import org.apache.commons.collections.CollectionUtils; +import org.jetlinks.community.notify.AbstractNotifier; +import org.jetlinks.community.notify.DefaultNotifyType; +import org.jetlinks.community.notify.NotifyType; +import org.jetlinks.community.notify.Provider; +import org.jetlinks.community.notify.template.TemplateManager; +import org.jetlinks.community.notify.webhook.WebHookProvider; +import org.jetlinks.core.Values; +import org.springframework.http.HttpMethod; +import org.springframework.util.StringUtils; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.core.publisher.Mono; + +import javax.annotation.Nonnull; + +public class HttpWebHookNotifier extends AbstractNotifier { + private final String id; + + private final WebClient webClient; + + private final HttpWebHookProperties properties; + + public HttpWebHookNotifier(String id, + HttpWebHookProperties properties, + WebClient webClient, + TemplateManager templateManager) { + super(templateManager); + this.id = id; + this.properties = properties; + this.webClient = webClient; + } + + @Override + public String getNotifierId() { + return id; + } + + @Nonnull + @Override + public NotifyType getType() { + return DefaultNotifyType.webhook; + } + + @Nonnull + @Override + public Provider getProvider() { + return WebHookProvider.http; + } + + @Nonnull + @Override + public Mono send(@Nonnull HttpWebHookTemplate template, + @Nonnull Values context) { + HttpMethod method = template.getMethod(); + WebClient.RequestBodyUriSpec bodyUriSpec = webClient + .method(template.getMethod()); + + if (StringUtils.hasText(template.getUrl())) { + bodyUriSpec.uri(template.getUrl()); + } + if (method == HttpMethod.POST + || method == HttpMethod.PUT + || method == HttpMethod.PATCH) { + String body = template.resolveBody(context); + if (null != body) { + bodyUriSpec.bodyValue(body); + } + } + + bodyUriSpec.headers(headers -> { + if (CollectionUtils.isNotEmpty(properties.getHeaders())) { + for (HttpWebHookProperties.Header header : properties.getHeaders()) { + headers.add(header.getKey(), header.getValue()); + } + } + + if (CollectionUtils.isNotEmpty(template.getHeaders())) { + for (HttpWebHookProperties.Header header : template.getHeaders()) { + headers.add(header.getKey(), header.getValue()); + } + } + }); + + return bodyUriSpec + .retrieve() + .bodyToMono(Void.class); + } + + @Nonnull + @Override + public Mono close() { + return Mono.empty(); + } +} diff --git a/jetlinks-components/notify-component/notify-webhook/src/main/java/org/jetlinks/community/notify/webhook/http/HttpWebHookNotifierProvider.java b/jetlinks-components/notify-component/notify-webhook/src/main/java/org/jetlinks/community/notify/webhook/http/HttpWebHookNotifierProvider.java new file mode 100644 index 00000000..07466a1b --- /dev/null +++ b/jetlinks-components/notify-component/notify-webhook/src/main/java/org/jetlinks/community/notify/webhook/http/HttpWebHookNotifierProvider.java @@ -0,0 +1,62 @@ +package org.jetlinks.community.notify.webhook.http; + +import lombok.AllArgsConstructor; +import org.hswebframework.web.bean.FastBeanCopier; +import org.hswebframework.web.i18n.LocaleUtils; +import org.hswebframework.web.validator.ValidatorUtils; +import org.jetlinks.community.notify.*; +import org.jetlinks.community.notify.template.Template; +import org.jetlinks.community.notify.template.TemplateManager; +import org.jetlinks.community.notify.template.TemplateProperties; +import org.jetlinks.community.notify.template.TemplateProvider; +import org.jetlinks.community.notify.webhook.WebHookProvider; +import org.springframework.stereotype.Component; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.core.publisher.Mono; + +import javax.annotation.Nonnull; + +@Component +@AllArgsConstructor +public class HttpWebHookNotifierProvider implements NotifierProvider, TemplateProvider { + + private final TemplateManager templateManager; + + private final WebClient.Builder builder; + + @Nonnull + @Override + public NotifyType getType() { + return DefaultNotifyType.webhook; + } + + @Nonnull + @Override + public Provider getProvider() { + return WebHookProvider.http; + } + + @Override + public Mono createTemplate(TemplateProperties properties) { + return Mono.just(new HttpWebHookTemplate().with(properties).validate()) + .as(LocaleUtils::transform); + } + + @Nonnull + @Override + public Mono> createNotifier(@Nonnull NotifierProperties properties) { + + HttpWebHookProperties hookProperties = FastBeanCopier.copy(properties.getConfiguration(),new HttpWebHookProperties()); + ValidatorUtils.tryValidate(hookProperties); + + WebClient.Builder client = builder.clone(); + + client.baseUrl(hookProperties.getUrl()); + + return Mono.just(new HttpWebHookNotifier(properties.getId(), + hookProperties, + client.build(), + templateManager)) + .as(LocaleUtils::transform); + } +} diff --git a/jetlinks-components/notify-component/notify-webhook/src/main/java/org/jetlinks/community/notify/webhook/http/HttpWebHookProperties.java b/jetlinks-components/notify-component/notify-webhook/src/main/java/org/jetlinks/community/notify/webhook/http/HttpWebHookProperties.java new file mode 100644 index 00000000..3ff612fd --- /dev/null +++ b/jetlinks-components/notify-component/notify-webhook/src/main/java/org/jetlinks/community/notify/webhook/http/HttpWebHookProperties.java @@ -0,0 +1,32 @@ +package org.jetlinks.community.notify.webhook.http; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; +import org.hibernate.validator.constraints.URL; + +import javax.validation.constraints.NotBlank; +import java.util.List; + +@Getter +@Setter +public class HttpWebHookProperties { + @Schema(description = "请求根地址,如: https://host/api") + @NotBlank + @URL + private String url; + + @Schema(description = "请求头") + private List

headers; + + //todo 认证方式 + + + @Getter + @Setter + public static class Header { + private String key; + + private String value; + } +} diff --git a/jetlinks-components/notify-component/notify-webhook/src/main/java/org/jetlinks/community/notify/webhook/http/HttpWebHookTemplate.java b/jetlinks-components/notify-component/notify-webhook/src/main/java/org/jetlinks/community/notify/webhook/http/HttpWebHookTemplate.java new file mode 100644 index 00000000..954bdcbb --- /dev/null +++ b/jetlinks-components/notify-component/notify-webhook/src/main/java/org/jetlinks/community/notify/webhook/http/HttpWebHookTemplate.java @@ -0,0 +1,108 @@ +package org.jetlinks.community.notify.webhook.http; + +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONException; +import com.google.common.collect.Maps; +import io.swagger.v3.oas.annotations.Hidden; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; +import org.jetlinks.community.notify.template.AbstractTemplate; +import org.jetlinks.core.Values; +import org.springframework.http.HttpMethod; +import org.springframework.util.ObjectUtils; +import org.springframework.util.StringUtils; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +@Getter +@Setter +public class HttpWebHookTemplate extends AbstractTemplate { + + @Schema(description = "请求地址") + private String url = ""; + + @Schema(description = "请求头") + private List headers; + + @Schema(description = "请求方式,默认POST") + private HttpMethod method = HttpMethod.POST; + + @Schema(description = "使用上下文作为请求体") + private boolean contextAsBody; + + @Schema(description = "自定义请求体") + private String body; + + @Hidden + private Boolean bodyIsJson; + + //todo 增加认证类型, oauth2等 + + public String resolveBody(Values context) { + if (!StringUtils.hasText(body)) { + return body; + } + if (contextAsBody) { + return JSON.toJSONString(context.getAllValues()); + } + Map contextVal = context.getAllValues(); + + try { + if (bodyIsJson == null || bodyIsJson) { + //解析为json再填充变量,防止变量值中含有",{等字符时导致json格式错误 + String _body = JSON.toJSONString( + resolveBody(JSON.parse(body), contextVal) + ); + bodyIsJson = true; + return _body; + } + } catch (JSONException ignore) { + + } + bodyIsJson = false; + return render(body, contextVal); + } + + + private Object resolveBody(Object val, Map context) { + //字符串,支持变量:${var} + if (val instanceof String) { + return render(String.valueOf(val), context); + } + if (val instanceof Map) { + return resolveBody(((Map) val), context); + } + if (val instanceof List) { + return resolveBody(((List) val), context); + } + return val; + } + + private Map resolveBody(Map obj, Map context) { + Map val = Maps.newLinkedHashMapWithExpectedSize(obj.size()); + + for (Map.Entry entry : obj.entrySet()) { + Object key = resolveBody(entry.getKey(), context); + //空key,忽略 + if (ObjectUtils.isEmpty(key)) { + continue; + } + val.put(key, resolveBody(entry.getValue(), context)); + } + return val; + } + + private List resolveBody(List obj, Map context) { + List array = new ArrayList<>(obj.size()); + for (Object val : obj) { + array.add(resolveBody(val, context)); + } + return array; + } + + + +} diff --git a/jetlinks-components/notify-component/notify-webhook/src/test/java/org/jetlinks/community/notify/webhook/http/HttpWebHookTemplateTest.java b/jetlinks-components/notify-component/notify-webhook/src/test/java/org/jetlinks/community/notify/webhook/http/HttpWebHookTemplateTest.java new file mode 100644 index 00000000..cd4ef53b --- /dev/null +++ b/jetlinks-components/notify-component/notify-webhook/src/test/java/org/jetlinks/community/notify/webhook/http/HttpWebHookTemplateTest.java @@ -0,0 +1,49 @@ +package org.jetlinks.community.notify.webhook.http; + +import org.jetlinks.community.notify.webhook.http.HttpWebHookTemplate; +import org.jetlinks.core.Values; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.springframework.context.annotation.Bean; + +import java.util.Collections; + +class HttpWebHookTemplateTest { + + + @Test + void testResolveBody() { + HttpWebHookTemplate template = new HttpWebHookTemplate(); + template.setBody("{\"name\":\"${deviceName}\"}"); + + String body = template.resolveBody(Values.of(Collections.singletonMap("deviceName", "Test"))); + System.out.println(body); + Assertions.assertEquals( + "{\"name\":\"Test\"}", + body); + + } + + + @Test + void testResolveArrayBody() { + HttpWebHookTemplate template = new HttpWebHookTemplate(); + template.setBody("[{\"name\":\"${deviceName}\"}]"); + + String body = template.resolveBody(Values.of(Collections.singletonMap("deviceName", "Test"))); + System.out.println(body); + Assertions.assertEquals("[{\"name\":\"Test\"}]", body); + + } + + @Test + void testResolvePlainBody() { + HttpWebHookTemplate template = new HttpWebHookTemplate(); + template.setBody("\"${deviceName}\""); + + String body = template.resolveBody(Values.of(Collections.singletonMap("deviceName", "Test"))); + System.out.println(body); + Assertions.assertEquals("\"Test\"", body); + + } +} \ No newline at end of file diff --git a/jetlinks-components/notify-component/pom.xml b/jetlinks-components/notify-component/pom.xml index fc02b32b..d397371b 100644 --- a/jetlinks-components/notify-component/pom.xml +++ b/jetlinks-components/notify-component/pom.xml @@ -18,6 +18,8 @@ notify-email notify-wechat notify-dingtalk + notify-voice + notify-webhook diff --git a/jetlinks-components/things-component/src/main/java/org/jetlinks/community/things/configuration/ThingsConfiguration.java b/jetlinks-components/things-component/src/main/java/org/jetlinks/community/things/configuration/ThingsConfiguration.java index d7d94e37..c9622977 100644 --- a/jetlinks-components/things-component/src/main/java/org/jetlinks/community/things/configuration/ThingsConfiguration.java +++ b/jetlinks-components/things-component/src/main/java/org/jetlinks/community/things/configuration/ThingsConfiguration.java @@ -7,12 +7,12 @@ import org.jetlinks.core.device.DeviceRegistry; import org.jetlinks.core.event.EventBus; import org.jetlinks.core.things.ThingsRegistry; import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; -@Configuration +@AutoConfiguration @Generated public class ThingsConfiguration { diff --git a/jetlinks-components/things-component/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/jetlinks-components/things-component/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 00000000..7ecda38b --- /dev/null +++ b/jetlinks-components/things-component/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +org.jetlinks.community.things.configuration.ThingsConfiguration \ No newline at end of file diff --git a/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/web/MenuController.java b/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/web/MenuController.java index c86a2348..871bb85c 100755 --- a/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/web/MenuController.java +++ b/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/web/MenuController.java @@ -247,15 +247,16 @@ public class MenuController implements ReactiveServiceCrudController saveAll(@RequestBody Flux menus) { + @Operation(summary = "保存一个应用下的全量数据", description = "先应用下全部删除旧数据,再新增数据") + public Mono saveOwnerAll(@PathVariable String owner, @RequestBody Flux menus) { return this .getService() .createDelete() .where(MenuEntity::getStatus, 1) + .and(MenuEntity::getOwner, owner) .execute() .then( this.save(menus) diff --git a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/configuration/DeviceManagerConfiguration.java b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/configuration/DeviceManagerConfiguration.java index 1036020d..dcd4d287 100644 --- a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/configuration/DeviceManagerConfiguration.java +++ b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/configuration/DeviceManagerConfiguration.java @@ -1,25 +1,29 @@ package org.jetlinks.community.device.configuration; import org.hswebframework.ezorm.rdb.mapping.ReactiveRepository; +import org.hswebframework.ezorm.rdb.operator.DatabaseOperator; +import org.jetlinks.community.buffer.BufferProperties; import org.jetlinks.community.device.entity.DeviceInstanceEntity; import org.jetlinks.community.device.function.ReactorQLDeviceSelectorBuilder; import org.jetlinks.community.device.function.RelationDeviceSelectorProvider; import org.jetlinks.community.device.message.DeviceMessageConnector; import org.jetlinks.community.device.message.writer.TimeSeriesMessageWriterConnector; -import org.jetlinks.community.device.service.data.DeviceDataService; -import org.jetlinks.community.device.service.data.DeviceDataStorageProperties; +import org.jetlinks.community.device.service.data.*; import org.jetlinks.community.rule.engine.executor.DeviceSelectorBuilder; import org.jetlinks.community.rule.engine.executor.device.DeviceSelectorProvider; import org.jetlinks.core.device.DeviceRegistry; import org.jetlinks.core.device.session.DeviceSessionManager; import org.jetlinks.core.event.EventBus; import org.jetlinks.core.server.MessageHandler; -import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import java.time.Duration; + @Configuration @EnableConfigurationProperties(DeviceDataStorageProperties.class) public class DeviceManagerConfiguration { @@ -51,4 +55,40 @@ public class DeviceManagerConfiguration { } + @Configuration + @ConditionalOnProperty(prefix = "jetlinks.device.storage", name = "enable-last-data-in-db", havingValue = "true") + static class DeviceLatestDataServiceConfiguration { + + @Bean + @ConfigurationProperties(prefix = "jetlinks.device.storage.latest.buffer") + public BufferProperties deviceLatestDataServiceBufferProperties() { + BufferProperties bufferProperties = new BufferProperties(); + bufferProperties.setFilePath("./data/device-latest-data-buffer"); + bufferProperties.setSize(1000); + bufferProperties.setParallelism(1); + bufferProperties.setTimeout(Duration.ofSeconds(1)); + return bufferProperties; + } + + @Bean(destroyMethod = "destroy") + public DatabaseDeviceLatestDataService deviceLatestDataService(DatabaseOperator databaseOperator) { + return new DatabaseDeviceLatestDataService(databaseOperator, + deviceLatestDataServiceBufferProperties()); + } + + } + + @Bean + @ConditionalOnProperty( + prefix = "jetlinks.device.storage", + name = "enable-last-data-in-db", + havingValue = "false", + matchIfMissing = true) + @ConditionalOnMissingBean(DeviceLatestDataService.class) + public DeviceLatestDataService deviceLatestDataService() { + return new NonDeviceLatestDataService(); + } + + + } diff --git a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/entity/DeviceLatestData.java b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/entity/DeviceLatestData.java new file mode 100755 index 00000000..015d0e8a --- /dev/null +++ b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/entity/DeviceLatestData.java @@ -0,0 +1,34 @@ +package org.jetlinks.community.device.entity; + +import io.swagger.v3.oas.annotations.media.Schema; + +import java.util.HashMap; +import java.util.Map; + +public class DeviceLatestData extends HashMap { + + public DeviceLatestData(int initialCapacity, float loadFactor) { + super(initialCapacity, loadFactor); + } + + public DeviceLatestData(int initialCapacity) { + super(initialCapacity); + } + + public DeviceLatestData() { + } + + public DeviceLatestData(Map m) { + super(m); + } + + @Schema(description = "设备ID") + public String getDeviceId(){ + return (String)get("deviceId"); + } + + @Schema(description = "设备名称") + public String getDeviceName(){ + return (String)get("deviceName"); + } +} diff --git a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/entity/DeviceMetadataMappingEntity.java b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/entity/DeviceMetadataMappingEntity.java new file mode 100644 index 00000000..ecc5d145 --- /dev/null +++ b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/entity/DeviceMetadataMappingEntity.java @@ -0,0 +1,111 @@ +package org.jetlinks.community.device.entity; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; +import org.hswebframework.ezorm.rdb.mapping.annotation.ColumnType; +import org.hswebframework.ezorm.rdb.mapping.annotation.DefaultValue; +import org.hswebframework.ezorm.rdb.mapping.annotation.EnumCodec; +import org.hswebframework.ezorm.rdb.mapping.annotation.JsonCodec; +import org.hswebframework.web.api.crud.entity.GenericEntity; +import org.hswebframework.web.api.crud.entity.RecordCreationEntity; +import org.hswebframework.web.crud.annotation.EnableEntityEvent; +import org.hswebframework.web.crud.generator.Generators; +import org.hswebframework.web.utils.DigestUtils; +import org.hswebframework.web.validator.CreateGroup; +import org.jetlinks.core.things.ThingMetadataType; +import org.springframework.util.StringUtils; + +import javax.persistence.Column; +import javax.persistence.Index; +import javax.persistence.Table; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import java.sql.JDBCType; +import java.util.Map; + +@Getter +@Setter +@Table(name = "dev_metadata_mapping", indexes = { + @Index(name = "idx_dev_mmp_did", columnList = "device_id"), + @Index(name = "idx_dev_mmp_pid", columnList = "product_id") +}) +@Schema(description = "设备物模型映射") +@EnableEntityEvent +public class DeviceMetadataMappingEntity extends GenericEntity implements RecordCreationEntity { + + @Schema(description = "产品ID") + @Column(length = 64, nullable = false, updatable = false) + @NotBlank(groups = CreateGroup.class) + private String productId; + + @Schema(description = "设备ID,为空时表示映射对产品下所有设备生效") + @Column(length = 64, updatable = false) + @NotBlank + private String deviceId; + + @Schema(description = "物模型类型,如:property") + @Column(length = 32, nullable = false, updatable = false) + @DefaultValue("property") + @NotNull(groups = CreateGroup.class) + @EnumCodec + @ColumnType(javaType = String.class) + private ThingMetadataType metadataType; + + @Schema(description = "物模型ID,如:属性ID") + @Column(length = 64, nullable = false, updatable = false) + @NotBlank(groups = CreateGroup.class) + private String metadataId; + + @Schema(description = "原始物模型ID") + @Column(length = 64, nullable = false) + @NotBlank(groups = CreateGroup.class) + private String originalId; + + @Schema(description = "其他配置") + @Column + @JsonCodec + @ColumnType(jdbcType = JDBCType.LONGVARCHAR) + private Map others; + + @Schema(description = "说明") + @Column(length = 512) + private String description; + + @Schema(description = "创建者ID", accessMode = Schema.AccessMode.READ_ONLY) + @Column(length = 64, updatable = false) + private String creatorId; + + @Schema(description = "创建时间", accessMode = Schema.AccessMode.READ_ONLY) + @Column(updatable = false) + @DefaultValue(generator = Generators.CURRENT_TIME) + private Long createTime; + + @Override + public String getId() { + if (super.getId() == null) { + generateId(); + } + return super.getId(); + } + + public void generateId() { + if (StringUtils.hasText(deviceId)) { + setId( + generateIdByDevice(deviceId, metadataType, metadataId) + ); + } else if (StringUtils.hasText(productId)) { + setId( + generateIdByProduct(productId, metadataType, metadataId) + ); + } + } + + public static String generateIdByProduct(String productId, ThingMetadataType type, String metadataId) { + return DigestUtils.md5Hex(String.join(":", "product", productId, type.name(), metadataId)); + } + + public static String generateIdByDevice(String deviceId, ThingMetadataType type, String metadataId) { + return DigestUtils.md5Hex(String.join(":", "device", deviceId, type.name(), metadataId)); + } +} diff --git a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/entity/ProtocolSupportEntity.java b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/entity/ProtocolSupportEntity.java index f0305c15..ac182792 100644 --- a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/entity/ProtocolSupportEntity.java +++ b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/entity/ProtocolSupportEntity.java @@ -1,10 +1,15 @@ package org.jetlinks.community.device.entity; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Getter; import lombok.Setter; import org.hswebframework.ezorm.rdb.mapping.annotation.ColumnType; +import org.hswebframework.ezorm.rdb.mapping.annotation.DefaultValue; import org.hswebframework.ezorm.rdb.mapping.annotation.JsonCodec; import org.hswebframework.web.api.crud.entity.GenericEntity; +import org.hswebframework.web.api.crud.entity.RecordCreationEntity; +import org.hswebframework.web.crud.annotation.EnableEntityEvent; +import org.hswebframework.web.crud.generator.Generators; import org.jetlinks.supports.protocol.management.ProtocolSupportDefinition; import javax.persistence.Column; @@ -15,7 +20,8 @@ import java.util.Map; @Getter @Setter @Table(name = "dev_protocol") -public class ProtocolSupportEntity extends GenericEntity { +@EnableEntityEvent +public class ProtocolSupportEntity extends GenericEntity implements RecordCreationEntity { @Column private String name; @@ -27,8 +33,27 @@ public class ProtocolSupportEntity extends GenericEntity { private String type; @Column + @Schema(description = "状态,1启用,0禁用") + @DefaultValue("1") private Byte state; + + @Column(updatable = false) + @Schema( + description = "创建者ID(只读)" + , accessMode = Schema.AccessMode.READ_ONLY + ) + private String creatorId; + + @Column(updatable = false) + @DefaultValue(generator = Generators.CURRENT_TIME) + @Schema( + description = "创建时间(只读)" + , accessMode = Schema.AccessMode.READ_ONLY + ) + private Long createTime; + + @Column @ColumnType(jdbcType = JDBCType.CLOB) @JsonCodec diff --git a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/entity/excel/DeviceInstanceImportExportEntity.java b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/entity/excel/DeviceInstanceImportExportEntity.java index 13464cc3..4833b1d5 100644 --- a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/entity/excel/DeviceInstanceImportExportEntity.java +++ b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/entity/excel/DeviceInstanceImportExportEntity.java @@ -12,7 +12,7 @@ public class DeviceInstanceImportExportEntity { @ExcelProperty("设备名称") private String name; - @ExcelProperty("设备型号") + @ExcelProperty("产品名称") private String productName; @ExcelProperty("描述") diff --git a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/enums/DeviceFeature.java b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/enums/DeviceFeature.java index 7baacad9..4848580e 100644 --- a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/enums/DeviceFeature.java +++ b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/enums/DeviceFeature.java @@ -1,14 +1,16 @@ package org.jetlinks.community.device.enums; import lombok.AllArgsConstructor; +import lombok.Generated; import lombok.Getter; import org.hswebframework.web.dict.Dict; import org.hswebframework.web.dict.EnumDict; +import org.jetlinks.core.metadata.Feature; @Dict("device-feature") @Getter @AllArgsConstructor -public enum DeviceFeature implements EnumDict { +public enum DeviceFeature implements EnumDict , Feature { selfManageState("子设备自己管理状态") @@ -16,7 +18,18 @@ public enum DeviceFeature implements EnumDict { private final String text; @Override + @Generated public String getValue() { return name(); } + + @Override + public String getId() { + return getValue(); + } + + @Override + public String getName() { + return text; + } } diff --git a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/events/DeviceDeployedEvent.java b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/events/DeviceDeployedEvent.java new file mode 100755 index 00000000..754696c0 --- /dev/null +++ b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/events/DeviceDeployedEvent.java @@ -0,0 +1,16 @@ +package org.jetlinks.community.device.events; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.hswebframework.web.event.DefaultAsyncEvent; +import org.jetlinks.community.device.entity.DeviceInstanceEntity; + +import java.util.List; + +@Getter +@AllArgsConstructor(staticName = "of") +public class DeviceDeployedEvent extends DefaultAsyncEvent { + + private final List devices; + +} diff --git a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/events/DeviceUnregisterEvent.java b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/events/DeviceUnregisterEvent.java new file mode 100644 index 00000000..aec9f4f3 --- /dev/null +++ b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/events/DeviceUnregisterEvent.java @@ -0,0 +1,20 @@ +package org.jetlinks.community.device.events; + +import lombok.AllArgsConstructor; +import lombok.Generated; +import lombok.Getter; +import lombok.Setter; +import org.hswebframework.web.event.DefaultAsyncEvent; +import org.jetlinks.community.device.entity.DeviceInstanceEntity; + +import java.util.List; + +@Getter +@Setter +@AllArgsConstructor(staticName = "of") +@Generated +public class DeviceUnregisterEvent extends DefaultAsyncEvent { + + private List devices; + +} diff --git a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/events/handler/DeviceProductDeployHandler.java b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/events/handler/DeviceProductDeployHandler.java index aed45b1c..66ad306d 100644 --- a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/events/handler/DeviceProductDeployHandler.java +++ b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/events/handler/DeviceProductDeployHandler.java @@ -4,6 +4,9 @@ import lombok.extern.slf4j.Slf4j; import org.jetlinks.community.device.events.DeviceProductDeployEvent; import org.jetlinks.community.device.service.LocalDeviceProductService; import org.jetlinks.community.device.service.data.DeviceDataService; +import org.jetlinks.community.device.service.data.DeviceLatestDataService; +import org.jetlinks.core.event.EventBus; +import org.jetlinks.core.event.Subscription; import org.jetlinks.core.metadata.DeviceMetadataCodec; import org.jetlinks.supports.official.JetLinksDeviceMetadataCodec; import org.springframework.beans.factory.annotation.Autowired; @@ -11,8 +14,12 @@ import org.springframework.boot.CommandLineRunner; import org.springframework.context.event.EventListener; import org.springframework.core.annotation.Order; import org.springframework.stereotype.Component; +import reactor.core.Disposable; +import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import javax.annotation.PreDestroy; + /** * 处理设备产品发布事件 * @@ -31,33 +38,83 @@ public class DeviceProductDeployHandler implements CommandLineRunner { private final DeviceDataService dataService; + private final DeviceLatestDataService latestDataService; + private final EventBus eventBus; + + private final Disposable disposable; @Autowired public DeviceProductDeployHandler(LocalDeviceProductService productService, - DeviceDataService dataService) { + DeviceDataService dataService, + EventBus eventBus, + DeviceLatestDataService latestDataService) { this.productService = productService; this.dataService = dataService; + this.eventBus = eventBus; + this.latestDataService = latestDataService; + //监听其他服务器上的物模型变更 + disposable = eventBus + .subscribe(Subscription + .builder() + .subscriberId("product-metadata-upgrade") + .topics("/_sys/product-upgrade") + .justBroker() + .build(), String.class) + .flatMap(id -> this + .reloadMetadata(id) + .onErrorResume((err) -> { + log.warn("handle product upgrade event error", err); + return Mono.empty(); + })) + .subscribe(); } + @PreDestroy + public void shutdown() { + disposable.dispose(); + } @EventListener public void handlerEvent(DeviceProductDeployEvent event) { event.async( this .doRegisterMetadata(event.getId(), event.getMetadata()) + .then( + eventBus.publish("/_sys/product-upgrade", event.getId()) + ) ); } - private Mono doRegisterMetadata(String productId, String metadataString) { + protected Mono reloadMetadata(String productId) { + return productService + .findById(productId) + .flatMap(product -> doReloadMetadata(productId, product.getMetadata())) + .then(); + } + + protected Mono doReloadMetadata(String productId, String metadataString) { return codec .decode(metadataString) - .flatMap(metadata -> dataService.registerMetadata(productId, metadata)); + .flatMap(metadata -> Flux + .mergeDelayError(2, + dataService.reloadMetadata(productId, metadata), + latestDataService.reloadMetadata(productId, metadata)) + .then()); + } + + protected Mono doRegisterMetadata(String productId, String metadataString) { + return codec + .decode(metadataString) + .flatMap(metadata -> Flux + .mergeDelayError(2, + dataService.registerMetadata(productId, metadata), + latestDataService.upgradeMetadata(productId, metadata)) + .then()); } @Override public void run(String... args) { - //启动时发布物模型 productService .createQuery() .fetch() @@ -65,7 +122,7 @@ public class DeviceProductDeployHandler implements CommandLineRunner { .flatMap(deviceProductEntity -> this .doRegisterMetadata(deviceProductEntity.getId(), deviceProductEntity.getMetadata()) .onErrorResume(err -> { - log.warn("register product metadata error", err); + log.warn("register product [{}] metadata error", deviceProductEntity.getId(), err); return Mono.empty(); }) ) diff --git a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/measurements/message/DeviceMessageMeasurement.java b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/measurements/message/DeviceMessageMeasurement.java index 9af81953..7857c7f3 100644 --- a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/measurements/message/DeviceMessageMeasurement.java +++ b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/measurements/message/DeviceMessageMeasurement.java @@ -1,5 +1,6 @@ package org.jetlinks.community.device.measurements.message; +import lombok.Generated; import org.jetlinks.community.Interval; import org.jetlinks.community.dashboard.*; import org.jetlinks.community.dashboard.supports.StaticMeasurement; @@ -33,14 +34,11 @@ class DeviceMessageMeasurement extends StaticMeasurement { private final TimeSeriesManager timeSeriesManager; - private final DeviceRegistry deviceRegistry; static MeasurementDefinition definition = MeasurementDefinition.of("quantity", "设备消息量"); public DeviceMessageMeasurement(EventBus eventBus, - DeviceRegistry registry, TimeSeriesManager timeSeriesManager) { super(definition); - this.deviceRegistry = registry; this.eventBus = eventBus; this.timeSeriesManager = timeSeriesManager; addDimension(new RealTimeMessageDimension()); @@ -53,21 +51,25 @@ class DeviceMessageMeasurement extends StaticMeasurement { class RealTimeMessageDimension implements MeasurementDimension { @Override + @Generated public DimensionDefinition getDefinition() { return CommonDimensionDefinition.realTime; } @Override + @Generated public DataType getValueType() { return IntType.GLOBAL; } @Override + @Generated public ConfigMetadata getParams() { return realTimeConfigMetadata; } @Override + @Generated public boolean isRealTime() { return true; } @@ -86,7 +88,6 @@ class DeviceMessageMeasurement extends StaticMeasurement { } } - static ConfigMetadata historyConfigMetadata = new DefaultConfigMetadata() .add("productId", "设备型号", "", new StringType()) .add("time", "周期", "例如: 1h,10m,30s", new StringType()) @@ -100,78 +101,62 @@ class DeviceMessageMeasurement extends StaticMeasurement { @Override + @Generated public DimensionDefinition getDefinition() { return CommonDimensionDefinition.agg; } @Override + @Generated public DataType getValueType() { return IntType.GLOBAL; } @Override + @Generated public ConfigMetadata getParams() { return historyConfigMetadata; } @Override + @Generated public boolean isRealTime() { return false; } - private AggregationQueryParam createQueryParam(MeasurementParameter parameter) { - return AggregationQueryParam.of() -// .sum("count") - .groupBy( - parameter.getInterval("time").orElse(Interval.ofHours(1)), - parameter.getString("format").orElse("MM月dd日 HH时")) -// .filter(query -> -// query -// .where("name", "message-count") -// .is("productId", parameter.getString("productId").orElse(null)) -// .is("msgType", parameter.getString("msgType").orElse(null)) -// ) + public AggregationQueryParam createQueryParam(MeasurementParameter parameter) { + return AggregationQueryParam + .of() + .sum("count") + .groupBy(parameter.getInterval("interval", parameter.getInterval("time", null)), + parameter.getString("format").orElse("MM月dd日 HH时")) + .filter(query -> query + .where("name", "message-count") + .is("productId", parameter.getString("productId").orElse(null)) + ) .limit(parameter.getInt("limit").orElse(1)) - .from(parameter.getDate("from").orElseGet(() -> Date.from(LocalDateTime.now().plusDays(-1).atZone(ZoneId.systemDefault()).toInstant()))) + .from(parameter + .getDate("from") + .orElseGet(() -> Date + .from(LocalDateTime + .now() + .plusDays(-1) + .atZone(ZoneId.systemDefault()) + .toInstant()))) .to(parameter.getDate("to").orElse(new Date())); } - private Mono getProductMetrics(List productIdList) { - return Flux - .fromIterable(productIdList) - .flatMap(id -> deviceRegistry - .getProduct(id) - .flatMap(DeviceProductOperator::getMetadata) - .onErrorResume(err -> Mono.empty()) - .flatMapMany(metadata -> Flux.fromIterable(metadata.getEvents()) - .map(event -> DeviceTimeSeriesMetric.deviceEventMetric(id, event.getId()))) - .concatWithValues(DeviceTimeSeriesMetric.devicePropertyMetric(id))) - .collectList() - .map(list -> list.toArray(new TimeSeriesMetric[0])); - } - @Override public Flux getValue(MeasurementParameter parameter) { + AggregationQueryParam param = createQueryParam(parameter); - return AggregationQueryParam.of() - .sum("count") - .groupBy( - parameter.getInterval("time").orElse(Interval.ofHours(1)), - parameter.getString("format").orElse("MM月dd日 HH时")) - .filter(query -> - query.where("name", "message-count") - .is("productId", parameter.getString("productId").orElse(null)) - .is("msgType", parameter.getString("msgType").orElse(null)) - ) - .limit(parameter.getInt("limit").orElse(1)) - .from(parameter.getDate("from").orElseGet(() -> Date.from(LocalDateTime.now().plusDays(-1).atZone(ZoneId.systemDefault()).toInstant()))) - .to(parameter.getDate("to").orElse(new Date())) + return Flux.defer(() -> param .execute(timeSeriesManager.getService(DeviceTimeSeriesMetric.deviceMetrics())::aggregation) .index((index, data) -> SimpleMeasurementValue.of( - data.getInt("count").orElse(0), + data.getLong("count",0), data.getString("time").orElse(""), - index)) - .sort(); + index))) + .take(param.getLimit()); } } diff --git a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/measurements/message/DeviceMessageMeasurementProvider.java b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/measurements/message/DeviceMessageMeasurementProvider.java index aae4abd9..917f60c5 100644 --- a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/measurements/message/DeviceMessageMeasurementProvider.java +++ b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/measurements/message/DeviceMessageMeasurementProvider.java @@ -21,14 +21,13 @@ public class DeviceMessageMeasurementProvider extends StaticMeasurementProvider public DeviceMessageMeasurementProvider(EventBus eventBus, MeterRegistryManager registryManager, - DeviceRegistry deviceRegistry, TimeSeriesManager timeSeriesManager) { super(DeviceDashboardDefinition.instance, DeviceObjectDefinition.message); registry = registryManager.getMeterRegister(DeviceTimeSeriesMetric.deviceMetrics().getId(), "target", "msgType", "productId"); - addMeasurement(new DeviceMessageMeasurement(eventBus, deviceRegistry, timeSeriesManager)); + addMeasurement(new DeviceMessageMeasurement(eventBus, timeSeriesManager)); } diff --git a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/relation/DeviceObjectProvider.java b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/relation/DeviceObjectProvider.java new file mode 100644 index 00000000..36bc9bc7 --- /dev/null +++ b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/relation/DeviceObjectProvider.java @@ -0,0 +1,107 @@ +package org.jetlinks.community.device.relation; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; +import org.jetlinks.core.device.DeviceConfigKey; +import org.jetlinks.core.device.DeviceOperator; +import org.jetlinks.core.device.DeviceRegistry; +import org.jetlinks.core.message.DeviceDataManager; +import org.jetlinks.core.things.relation.ObjectType; +import org.jetlinks.core.things.relation.PropertyOperation; +import org.jetlinks.community.PropertyConstants; +import org.jetlinks.community.device.service.LocalDeviceInstanceService; +import org.jetlinks.community.relation.RelationObjectProvider; +import org.jetlinks.community.relation.impl.SimpleObjectType; +import org.jetlinks.community.relation.impl.property.PropertyOperationStrategy; +import org.springframework.stereotype.Component; +import reactor.core.publisher.Mono; + +@Getter +@Setter +@AllArgsConstructor +@Component +public class DeviceObjectProvider implements RelationObjectProvider { + + private final DeviceDataManager deviceDataManager; + + private final LocalDeviceInstanceService instanceService; + + private final DeviceRegistry registry; + + @Override + public String getTypeId() { + return RelationObjectProvider.TYPE_DEVICE; + } + + @Override + public Mono getType() { + return Mono.just(new SimpleObjectType(getTypeId(), "设备", "设备")); + } + + @Override + public PropertyOperation properties(String id) { + return PropertyOperationStrategy + .composite( + PropertyOperationStrategy + .simple(registry.getDevice(id), + strategy -> strategy + .addMapper("id", DeviceOperator::getDeviceId) + .addAsyncMapper(PropertyConstants.deviceName, DeviceOperator::getSelfConfig) + .addAsyncMapper(PropertyConstants.productName, DeviceOperator::getSelfConfig) + .addAsyncMapper(PropertyConstants.productId, DeviceOperator::getSelfConfig) + .addAsyncMapper(DeviceConfigKey.deviceType, DeviceOperator::getSelfConfig) + .addAsyncMapper(DeviceConfigKey.parentGatewayId, DeviceOperator::getSelfConfig) + .addAsyncMapper(DeviceConfigKey.firstPropertyTime, DeviceOperator::getSelfConfig)), + PropertyOperationStrategy + .detect(strategy -> { + // dev@device.property.temp + // dev@device.property.temp.timestamp + strategy + .addOperation("property", + key -> getDeviceProperty(id, key)) + // dev@device.tag.tagKey + .addOperation("tag", + key -> deviceDataManager + .getTags(id, key) + .map(DeviceDataManager.TagValue::getValue) + .singleOrEmpty() + ) + // dev@device.config.key + .addOperation("config", + key -> registry + .getDevice(id) + .flatMap(device -> device.getConfig(key)) + ); + }) + ); + } + + protected Mono getDeviceProperty(String deviceId, String property) { + if (!property.contains(".")) { + return this + .getPropertyValue(deviceId, property) + .map(DeviceDataManager.PropertyValue::getValue); + } + String[] arr = property.split("[.]"); + String propertyKey = arr[0]; + String valueType = arr[1]; + Mono propertyValueMono = this.getPropertyValue(deviceId, propertyKey); + + if ("timestamp".equals(valueType)) { + return propertyValueMono + .map(DeviceDataManager.PropertyValue::getTimestamp); + } + if ("state".equals(valueType)) { + return propertyValueMono + .mapNotNull(DeviceDataManager.PropertyValue::getState); + } + return propertyValueMono + .map(DeviceDataManager.PropertyValue::getValue); + + } + + Mono getPropertyValue(String deviceId, String property) { + return deviceDataManager.getLastProperty(deviceId, property); + } +} diff --git a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/response/DeviceDeployResult.java b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/response/DeviceDeployResult.java index f81a1f4d..4d78dfd8 100644 --- a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/response/DeviceDeployResult.java +++ b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/response/DeviceDeployResult.java @@ -1,9 +1,6 @@ package org.jetlinks.community.device.response; -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; +import lombok.*; @Getter @Setter @@ -17,11 +14,19 @@ public class DeviceDeployResult { private String message; + //导致错误的源头 + private Object source; + + //导致错误的操作 + private String operation; + + @Generated public static DeviceDeployResult success(int total) { - return new DeviceDeployResult(total, true, null); + return new DeviceDeployResult(total, true, null, null, null); } + @Generated public static DeviceDeployResult error(String message) { - return new DeviceDeployResult(0, false, message); + return new DeviceDeployResult(0, false, message, null, null); } } diff --git a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/response/DeviceDetail.java b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/response/DeviceDetail.java index 3b4ca5d6..8c384a2c 100644 --- a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/response/DeviceDetail.java +++ b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/response/DeviceDetail.java @@ -7,12 +7,19 @@ import org.apache.commons.collections4.MapUtils; import org.jetlinks.community.device.entity.DeviceInstanceEntity; import org.jetlinks.community.device.entity.DeviceProductEntity; import org.jetlinks.community.device.entity.DeviceTagEntity; +import org.jetlinks.community.device.enums.DeviceFeature; import org.jetlinks.community.device.enums.DeviceState; import org.jetlinks.community.device.enums.DeviceType; +import org.jetlinks.community.relation.service.response.RelatedInfo; +import org.jetlinks.core.ProtocolSupport; import org.jetlinks.core.Values; +import org.jetlinks.core.device.DeviceConfigKey; import org.jetlinks.core.device.DeviceOperator; +import org.jetlinks.core.device.DeviceProductOperator; import org.jetlinks.core.metadata.ConfigPropertyMetadata; import org.jetlinks.core.metadata.DeviceMetadata; +import org.jetlinks.core.metadata.Feature; +import org.jetlinks.core.metadata.SimpleFeature; import org.jetlinks.supports.official.JetLinksDeviceMetadataCodec; import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; @@ -126,6 +133,13 @@ public class DeviceDetail { @Schema(description = "设备描述") private String description; + @Schema(description = "关系信息") + private List relations; + + @Schema(description = "设备特性") + private List features = new ArrayList<>(); + + public DeviceDetail notActive() { state = DeviceState.notActive; @@ -209,6 +223,11 @@ public class DeviceDetail { return this; } + public DeviceDetail withRelation(List relations){ + this.relations=relations; + return this; + } + public DeviceDetail with(DeviceProductEntity productEntity) { if (productEntity == null) { return this; @@ -237,6 +256,9 @@ public class DeviceDetail { setOrgId(device.getOrgId()); setParentId(device.getParentId()); setDescription(device.getDescribe()); + if (device.getFeatures() != null) { + withFeatures(Arrays.asList(device.getFeatures())); + } Optional.ofNullable(device.getRegistryTime()) .ifPresent(this::setRegisterTime); @@ -267,4 +289,32 @@ public class DeviceDetail { return this; } + public DeviceDetail withFeatures(Collection features) { + for (Feature feature : features) { + this.features.add(new SimpleFeature(feature.getId(), feature.getName())); + } + return this; + } + + public Mono with(DeviceProductOperator product) { + return Mono + .zip( + product + .getProtocol() + .mapNotNull(ProtocolSupport::getName) + .defaultIfEmpty(""), + product + .getConfig(DeviceConfigKey.metadata) + .defaultIfEmpty("")) + .doOnNext(tp2 -> { + setProtocolName(tp2.getT1()); + //物模型以产品缓存里的为准 + if (!this.independentMetadata && StringUtils.hasText(tp2.getT2())) { + setMetadata(tp2.getT2()); + } + }) + .thenReturn(this); + } + + } diff --git a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/service/DefaultDeviceConfigMetadataManager.java b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/service/DefaultDeviceConfigMetadataManager.java index ef93564b..d038291b 100644 --- a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/service/DefaultDeviceConfigMetadataManager.java +++ b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/service/DefaultDeviceConfigMetadataManager.java @@ -1,10 +1,7 @@ package org.jetlinks.community.device.service; import org.jetlinks.community.device.spi.DeviceConfigMetadataSupplier; -import org.jetlinks.core.metadata.ConfigMetadata; -import org.jetlinks.core.metadata.ConfigScope; -import org.jetlinks.core.metadata.DeviceConfigScope; -import org.jetlinks.core.metadata.DeviceMetadataType; +import org.jetlinks.core.metadata.*; import org.springframework.beans.factory.config.BeanPostProcessor; import org.springframework.stereotype.Component; import org.springframework.util.CollectionUtils; @@ -72,4 +69,12 @@ public class DefaultDeviceConfigMetadataManager implements DeviceConfigMetadataM } return bean; } + + @Override + public Flux getProductFeatures(String productId) { + return Flux + .fromIterable(suppliers) + .flatMap(supplier -> supplier.getProductFeatures(productId)) + .distinct(Feature::getId); + } } diff --git a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/service/DefaultDeviceConfigMetadataSupplier.java b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/service/DefaultDeviceConfigMetadataSupplier.java index 03dc3b1a..c32a16e7 100644 --- a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/service/DefaultDeviceConfigMetadataSupplier.java +++ b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/service/DefaultDeviceConfigMetadataSupplier.java @@ -5,18 +5,21 @@ import org.hswebframework.web.exception.BusinessException; import org.jetlinks.community.device.entity.DeviceInstanceEntity; import org.jetlinks.community.device.entity.DeviceProductEntity; import org.jetlinks.community.device.spi.DeviceConfigMetadataSupplier; +import org.jetlinks.core.ProtocolSupport; import org.jetlinks.core.ProtocolSupports; import org.jetlinks.core.message.codec.Transport; import org.jetlinks.core.message.codec.Transports; import org.jetlinks.core.metadata.ConfigMetadata; import org.jetlinks.core.metadata.DeviceConfigScope; import org.jetlinks.core.metadata.DeviceMetadataType; +import org.jetlinks.core.metadata.Feature; import org.springframework.stereotype.Component; import org.springframework.util.Assert; import org.springframework.util.StringUtils; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import java.util.function.BiFunction; import java.util.function.Function; @Component @@ -63,32 +66,16 @@ public class DefaultDeviceConfigMetadataSupplier implements DeviceConfigMetadata .filter(metadata -> metadata.hasScope(DeviceConfigScope.product)); } - @Override - @SuppressWarnings("all") public Flux getMetadataExpandsConfig(String productId, DeviceMetadataType metadataType, String metadataId, String typeId) { - Assert.hasText(productId, "productId can not be empty"); - Assert.notNull(metadataType, "metadataType can not be empty"); + Assert.hasText(productId, "message.productId_cannot_be_empty"); + Assert.notNull(metadataType, "message.metadataType_cannot_be_empty"); - return productService - .createQuery() - .select(DeviceProductEntity::getMessageProtocol, DeviceProductEntity::getTransportProtocol) - .where(DeviceInstanceEntity::getId, productId) - .fetchOne() - .flatMap(product -> { - return Mono - .zip( - //消息协议 - protocolSupports.getProtocol(product.getMessageProtocol()), - //传输协议 - Mono.justOrEmpty(product.getTransportEnum(Transports.get())), - (protocol, transport) -> { - return protocol.getMetadataExpandsConfig(transport, metadataType, metadataId, typeId); - } - ); - }) + return this + .computeDeviceProtocol(productId, (protocol, transport) -> + protocol.getMetadataExpandsConfig(transport, metadataType, metadataId, typeId)) .flatMapMany(Function.identity()); } @@ -101,4 +88,35 @@ public class DefaultDeviceConfigMetadataSupplier implements DeviceConfigMetadata .onErrorMap(e -> new BusinessException("error.unable_to_load_protocol_by_access_id", 404, product.getMessageProtocol())) .flatMap(support -> support.getConfigMetadata(Transport.of(product.getTransportProtocol())))); } + + @Override + public Flux getProductFeatures(String productId) { + Assert.hasText(productId, "message.productId_cannot_be_empty"); + return this + .computeDeviceProtocol(productId, ProtocolSupport::getFeatures) + .flatMapMany(Function.identity()); + } + + + @SuppressWarnings("all") + protected Mono computeDeviceProtocol(String productId, BiFunction computer) { + return productService + .createQuery() + .select(DeviceProductEntity::getMessageProtocol, DeviceProductEntity::getTransportProtocol) + .where(DeviceInstanceEntity::getId, productId) + .fetchOne() + .flatMap(product -> { + return Mono + .zip( + //消息协议 + Mono.justOrEmpty(product.getMessageProtocol()) + .flatMap(protocolSupports::getProtocol) + .onErrorMap(e -> new BusinessException("error.unable_to_load_protocol_by_access_id", 404, product.getMessageProtocol())), + //传输协议 + Mono.justOrEmpty(product.getTransportProtocol()) + .map(Transport::of), + computer + ); + }); + } } diff --git a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/service/DeviceConfigMetadataManager.java b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/service/DeviceConfigMetadataManager.java index 723f0940..aa79a612 100644 --- a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/service/DeviceConfigMetadataManager.java +++ b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/service/DeviceConfigMetadataManager.java @@ -4,10 +4,7 @@ import org.jetlinks.community.device.entity.DeviceInstanceEntity; import org.jetlinks.community.device.entity.DeviceProductEntity; import org.jetlinks.community.device.spi.DeviceConfigMetadataSupplier; import org.jetlinks.core.message.codec.Transport; -import org.jetlinks.core.metadata.ConfigMetadata; -import org.jetlinks.core.metadata.ConfigPropertyMetadata; -import org.jetlinks.core.metadata.ConfigScope; -import org.jetlinks.core.metadata.DeviceMetadataType; +import org.jetlinks.core.metadata.*; import reactor.core.publisher.Flux; @@ -78,4 +75,6 @@ public interface DeviceConfigMetadataManager { String typeId, ConfigScope... scopes); + Flux getProductFeatures(String productId); + } diff --git a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/service/DeviceEntityEventHandler.java b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/service/DeviceEntityEventHandler.java new file mode 100644 index 00000000..8ab7c4a0 --- /dev/null +++ b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/service/DeviceEntityEventHandler.java @@ -0,0 +1,133 @@ +package org.jetlinks.community.device.service; + +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.hswebframework.web.crud.events.EntityDeletedEvent; +import org.hswebframework.web.crud.events.EntityModifyEvent; +import org.hswebframework.web.crud.events.EntityPrepareCreateEvent; +import org.hswebframework.web.crud.events.EntitySavedEvent; +import org.hswebframework.web.exception.BusinessException; +import org.jetlinks.core.ProtocolSupports; +import org.jetlinks.core.device.DeviceRegistry; +import org.jetlinks.core.message.codec.Transport; +import org.jetlinks.community.PropertyConstants; +import org.jetlinks.community.device.entity.DeviceCategoryEntity; +import org.jetlinks.community.device.entity.DeviceInstanceEntity; +import org.jetlinks.community.device.entity.DeviceProductEntity; +import org.jetlinks.supports.official.JetLinksDeviceMetadataCodec; +import org.springframework.context.event.EventListener; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.util.Map; +import java.util.Objects; +import java.util.function.Function; +import java.util.stream.Collectors; + +@Component +@AllArgsConstructor +@Slf4j +public class DeviceEntityEventHandler { + + private final LocalDeviceProductService productService; + + private final DeviceRegistry registry; + + private final ProtocolSupports supports; + + @EventListener + public void handleDeviceEvent(EntitySavedEvent event) { + //保存设备时,自动更新注册中心里的名称 + event.first( + Flux.fromIterable(event.getEntity()) + .filter(device -> StringUtils.hasText(device.getName())) + .flatMap(device -> registry + .getDevice(device.getId()) + .flatMap(deviceOperator -> deviceOperator.setConfig(PropertyConstants.deviceName, device.getName()))) + ); + } + + @EventListener + public void handleDeviceEvent(EntityModifyEvent event) { + Map olds = event + .getBefore() + .stream() + .filter(device -> StringUtils.hasText(device.getId())) + .collect(Collectors.toMap(DeviceInstanceEntity::getId, Function.identity())); + + //更新设备时,自动更新注册中心里的名称 + event.first( + Flux.fromIterable(event.getAfter()) + .filter(device -> { + DeviceInstanceEntity old = olds.get(device.getId()); + return old != null && !Objects.equals(device.getName(), old.getName()); + }) + .flatMap(device -> registry + .getDevice(device.getId()) + .flatMap(deviceOperator -> deviceOperator.setConfig(PropertyConstants.deviceName, device.getName()))) + ); + + } + + @EventListener + public void handleProductDefaultMetadata(EntityPrepareCreateEvent event) { + event.async( + Flux.fromIterable(event.getEntity()) + .flatMap(product -> { + //新建产品时自动填充默认物模型 + if (product.getMetadata() == null && + StringUtils.hasText(product.getMessageProtocol()) && + StringUtils.hasText(product.getTransportProtocol())) { + return supports + .getProtocol(product.getMessageProtocol()) + .flatMapMany(support -> support + .getDefaultMetadata(Transport.of(product.getTransportProtocol())) + .flatMap(JetLinksDeviceMetadataCodec.getInstance()::encode) + .doOnNext(product::setMetadata)) + .onErrorResume(err -> { + log.warn("auto set product[{}] default metadata error", product.getName(), err); + return Mono.empty(); + }); + } + return Mono.empty(); + }) + ); + } + + @EventListener + public void handleCategoryDelete(EntityDeletedEvent event) { + //禁止删除有产品使用的分类 + event.async( + productService + .createQuery() + .in(DeviceProductEntity::getClassifiedId, event + .getEntity() + .stream() + .map(DeviceCategoryEntity::getId) + .collect(Collectors.toList())) + .count() + .doOnNext(i -> { + if (i > 0) { + throw new BusinessException("error.device_category_has_bean_use_by_product"); + } + }) + ); + + } + + //修改产品分类时,同步修改产品分类名称 + @EventListener + public void handleCategorySave(EntitySavedEvent event) { + event.async( + Flux.fromIterable(event.getEntity()) + .flatMap(category -> productService + .createUpdate() + .set(DeviceProductEntity::getClassifiedName, category.getName()) + .where(DeviceProductEntity::getClassifiedId, category.getId()) + .execute() + .then()) + ); + } +} diff --git a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/service/DeviceProductHandler.java b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/service/DeviceProductHandler.java new file mode 100644 index 00000000..70bf4341 --- /dev/null +++ b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/service/DeviceProductHandler.java @@ -0,0 +1,60 @@ +package org.jetlinks.community.device.service; + +import lombok.AllArgsConstructor; +import org.hswebframework.web.bean.FastBeanCopier; +import org.hswebframework.web.crud.events.EntityModifyEvent; +import org.hswebframework.web.crud.events.EntitySavedEvent; +import org.jetlinks.core.device.DeviceRegistry; +import org.jetlinks.community.device.entity.DeviceProductEntity; +import org.jetlinks.community.device.events.DeviceProductDeployEvent; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.event.EventListener; +import org.springframework.stereotype.Component; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.util.List; + +/** + * @author bestfeng + */ +@AllArgsConstructor +@Component +public class DeviceProductHandler { + + private final LocalDeviceProductService productService; + + private final DeviceRegistry deviceRegistry; + + private final ApplicationEventPublisher eventPublisher; + + @EventListener + public void handleProductSaveEvent(EntitySavedEvent event) { + event.async( + applyProductConfig(event.getEntity()) + ); + } + + @EventListener + public void handleProductSaveEvent(EntityModifyEvent event) { + event.async( + applyProductConfig(event.getBefore()) + ); + } + + //已发布状态的产品配置更新后,重新应用配置 + private Mono applyProductConfig(List entities) { + return Flux + .fromIterable(entities) + .map(DeviceProductEntity::getId) + .as(productService::findById) + .filter(product -> product.getState() == 1) + .flatMap(product -> deviceRegistry + .register(product.toProductInfo()) + .flatMap(i -> FastBeanCopier + .copy(product, new DeviceProductDeployEvent()) + .publish(eventPublisher)) + ) + .then(); + } +} diff --git a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/service/LocalDeviceInstanceService.java b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/service/LocalDeviceInstanceService.java old mode 100644 new mode 100755 index eb186007..07f83661 --- a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/service/LocalDeviceInstanceService.java +++ b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/service/LocalDeviceInstanceService.java @@ -7,36 +7,51 @@ import org.apache.commons.collections4.MapUtils; import org.hswebframework.ezorm.rdb.mapping.ReactiveRepository; import org.hswebframework.ezorm.rdb.mapping.ReactiveUpdate; import org.hswebframework.ezorm.rdb.mapping.defaults.SaveResult; -import org.hswebframework.ezorm.rdb.operator.dml.Terms; +import org.hswebframework.web.api.crud.entity.PagerResult; +import org.hswebframework.web.api.crud.entity.QueryParamEntity; import org.hswebframework.web.crud.events.EntityDeletedEvent; import org.hswebframework.web.crud.events.EntityEventHelper; import org.hswebframework.web.crud.service.GenericReactiveCrudService; import org.hswebframework.web.exception.BusinessException; +import org.hswebframework.web.exception.I18nSupportException; +import org.hswebframework.web.exception.TraceSourceException; +import org.hswebframework.web.i18n.LocaleUtils; import org.hswebframework.web.id.IDGenerator; -import org.jetlinks.community.device.entity.*; -import org.jetlinks.community.device.enums.DeviceFeature; -import org.jetlinks.community.device.enums.DeviceState; -import org.jetlinks.community.device.response.DeviceDeployResult; import org.jetlinks.community.device.response.DeviceDetail; -import org.jetlinks.community.utils.ErrorUtils; import org.jetlinks.core.device.DeviceConfigKey; import org.jetlinks.core.device.DeviceOperator; +import org.jetlinks.core.device.DeviceProductOperator; import org.jetlinks.core.device.DeviceRegistry; import org.jetlinks.core.enums.ErrorCode; import org.jetlinks.core.exception.DeviceOperationException; import org.jetlinks.core.message.DeviceMessageReply; import org.jetlinks.core.message.FunctionInvokeMessageSender; +import org.jetlinks.core.message.ReadPropertyMessageSender; import org.jetlinks.core.message.WritePropertyMessageSender; +import org.jetlinks.core.message.codec.Transport; import org.jetlinks.core.message.function.FunctionInvokeMessageReply; import org.jetlinks.core.message.property.ReadPropertyMessageReply; import org.jetlinks.core.message.property.WritePropertyMessageReply; import org.jetlinks.core.metadata.ConfigMetadata; -import org.jetlinks.core.metadata.PropertyMetadata; -import org.jetlinks.core.metadata.types.StringType; +import org.jetlinks.core.metadata.DeviceMetadata; +import org.jetlinks.core.metadata.MergeOption; import org.jetlinks.core.utils.CyclicDependencyChecker; +import org.jetlinks.community.device.entity.*; +import org.jetlinks.community.device.enums.DeviceState; +import org.jetlinks.community.device.events.DeviceDeployedEvent; +import org.jetlinks.community.device.events.DeviceUnregisterEvent; +import org.jetlinks.community.device.response.DeviceDeployResult; +import org.jetlinks.community.relation.RelationObjectProvider; +import org.jetlinks.community.relation.service.RelationService; +import org.jetlinks.community.relation.service.response.RelatedInfo; +import org.jetlinks.community.utils.ErrorUtils; +import org.jetlinks.reactor.ql.utils.CastUtils; +import org.jetlinks.supports.official.JetLinksDeviceMetadataCodec; import org.reactivestreams.Publisher; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.event.EventListener; import org.springframework.stereotype.Service; +import org.springframework.transaction.reactive.TransactionalOperator; import org.springframework.util.StringUtils; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -46,6 +61,7 @@ import reactor.util.function.Tuple3; import reactor.util.function.Tuples; import java.util.*; +import java.util.concurrent.ConcurrentHashMap; import java.util.function.Function; import java.util.stream.Collectors; @@ -57,31 +73,54 @@ public class LocalDeviceInstanceService extends GenericReactiveCrudService tagRepository; + + private final ApplicationEventPublisher eventPublisher; + private final DeviceConfigMetadataManager metadataManager; - @SuppressWarnings("all") - private final ReactiveRepository tagRepository; + private final RelationService relationService; + + private final TransactionalOperator transactionalOperator; public LocalDeviceInstanceService(DeviceRegistry registry, LocalDeviceProductService deviceProductService, - DeviceConfigMetadataManager metadataManager, @SuppressWarnings("all") - ReactiveRepository tagRepository) { + ReactiveRepository tagRepository, + ApplicationEventPublisher eventPublisher, + DeviceConfigMetadataManager metadataManager, + RelationService relationService, + TransactionalOperator transactionalOperator) { this.registry = registry; this.deviceProductService = deviceProductService; - this.metadataManager = metadataManager; this.tagRepository = tagRepository; + this.eventPublisher = eventPublisher; + this.metadataManager = metadataManager; + this.relationService = relationService; + this.transactionalOperator = transactionalOperator; } - @Override public Mono save(Publisher entityPublisher) { - return Flux.from(entityPublisher) - .doOnNext(instance -> instance.setState(null)) - .as(super::save); + return Flux + .from(entityPublisher) + .flatMap(instance -> { + instance.setState(null); + if (StringUtils.isEmpty(instance.getId())) { + return handleCreateBefore(instance); + } + return registry + .getDevice(instance.getId()) + .flatMap(DeviceOperator::getState) + .map(DeviceState::of) + .onErrorReturn(DeviceState.offline) + .defaultIfEmpty(DeviceState.notActive) + .doOnNext(instance::setState) + .thenReturn(instance); + }) + .as(super::save); } - /** * 重置设备配置 * @@ -105,19 +144,24 @@ public class LocalDeviceInstanceService extends GenericReactiveCrudService opts.removeConfigs(product.getConfiguration().keySet())) - .then(); + return registry + .getDevice(deviceId) + .flatMap(opts -> opts.removeConfigs(product.getConfiguration().keySet())) + .then(); } return Mono.empty(); - }).then( - //更新数据库 - createUpdate() - .set(device::getConfiguration) - .where(device::getId) - .execute() + }) + .then( + Mono.defer(() -> { + //更新数据库 + return createUpdate() + .when(device.getConfiguration() != null, update -> update.set(device::getConfiguration)) + .when(device.getConfiguration() == null, update -> update.setNull(DeviceInstanceEntity::getConfiguration)) + .where(device::getId) + .execute(); + }) ) - .thenReturn(device.getConfiguration()); + .then(Mono.fromSupplier(device::getConfiguration)); }) .defaultIfEmpty(Collections.emptyMap()) ; @@ -132,22 +176,48 @@ public class LocalDeviceInstanceService extends GenericReactiveCrudService deploy(String id) { return findById(id) .flux() - .as(this::deploy) + .as(flux -> deploy(flux, Mono::error)) .singleOrEmpty(); } + /** - * 批量发布设备到设备注册中心 + * 批量发布设备到设备注册中心,并异常返回空 * * @param flux 设备实例流 * @return 发布数量 */ public Flux deploy(Flux flux) { + return this + .deploy(flux, err -> Mono.empty()); +// .contextWrite(TraceSourceException.deepTraceContext()); + } + + /** + * 批量发布设备到设备注册中心并指定异常 + * + * @param flux 设备实例流 + * @return 发布数量 + */ + public Flux deploy(Flux flux, Function> fallback) { + //设备回滚 key: deviceId value: 操作 + Map> rollback = new ConcurrentHashMap<>(); + return flux + //添加回滚操作,用于再触发DeviceDeployedEvent事件执行失败时进行回滚. + .flatMap(device -> registry + .getDevice(device.getId()) + .switchIfEmpty(Mono.fromRunnable(() -> { + //设备之前没有注册的回滚操作(注销) + rollback.put(device.getId(), registry.unregisterDevice(device.getId())); + })) + .thenReturn(device)) + //发布到注册中心 .flatMap(instance -> registry .register(instance.toDeviceInfo()) .flatMap(deviceOperator -> deviceOperator - .getState() + .checkState()//激活时检查设备状态 + .onErrorReturn(org.jetlinks.core.device.DeviceState.offline) .flatMap(r -> { if (r.equals(org.jetlinks.core.device.DeviceState.unknown) || r.equals(org.jetlinks.core.device.DeviceState.noActive)) { @@ -157,12 +227,14 @@ public class LocalDeviceInstanceService extends GenericReactiveCrudService success ? Mono.just(deviceOperator) : Mono.empty()) - ) - .thenReturn(instance)) - .buffer(50) + .flatMap(success -> success ? Mono.just(deviceOperator) : Mono.empty())) + .thenReturn(instance) + //激活失败,忽略错误,继续处理其他设备 + .onErrorResume(e -> fallback.apply(e).then(Mono.empty())) + ) + .buffer(200)//每200条数据批量更新 .publishOn(Schedulers.single()) - .flatMap(all -> Flux + .concatMap(all -> Flux .fromIterable(all) .groupBy(DeviceInstanceEntity::getState) .flatMap(group -> group @@ -171,30 +243,38 @@ public class LocalDeviceInstanceService extends GenericReactiveCrudService createUpdate() .where() .set(DeviceInstanceEntity::getState, group.key()) - .set(DeviceInstanceEntity::getRegistryTime, new Date()) + .set(DeviceInstanceEntity::getRegistryTime, System.currentTimeMillis()) .in(DeviceInstanceEntity::getId, list) + .is(DeviceInstanceEntity::getState, DeviceState.notActive) .execute() - .map(r -> DeviceDeployResult.success(list.size())) - .onErrorResume(err -> Mono.just(DeviceDeployResult.error(err.getMessage())))))) + .map(r -> DeviceDeployResult.success(list.size())))) + //推送激活事件 + .flatMap(res -> DeviceDeployedEvent.of(all).publish(eventPublisher).thenReturn(res)) + //传递国际化上下文 + .as(LocaleUtils::transform) + .as(transactionalOperator::transactional) + .onErrorResume(err -> Flux + .fromIterable(all) + .mapNotNull(device -> rollback.get(device.getId())) + .flatMap(Function.identity()) + .then( + Mono.zip( + I18nSupportException.tryGetLocalizedMessageReactive(err), + TraceSourceException.tryGetOperationLocalizedReactive(err).defaultIfEmpty(""), + (msg, opt) -> new DeviceDeployResult(all.size(), + false, + msg, + TraceSourceException.tryGetSource(err), + opt)) + ) + .flatMap(res -> fallback.apply(err).thenReturn(res)) + ) + ) + //激活时不触发事件,单独处理DeviceDeployedEvent + .as(EntityEventHelper::setDoNotFireEvent) ; } - /** - * 取消发布(取消激活),取消后,设备无法再连接到服务. 注册中心也无法再获取到该设备信息. - * - * @param id 设备ID - * @return 取消结果 - */ - public Mono cancelDeploy(String id) { - return findById(Mono.just(id)) - .flatMap(product -> registry - .unregisterDevice(id) - .then(createUpdate() - .set(DeviceInstanceEntity::getState, DeviceState.notActive.getValue()) - .where(DeviceInstanceEntity::getId, id) - .execute())); - } - /** * 注销设备,取消后,设备无法再连接到服务. 注册中心也无法再获取到该设备信息. * @@ -202,13 +282,9 @@ public class LocalDeviceInstanceService extends GenericReactiveCrudService unregisterDevice(String id) { - return this.findById(Mono.just(id)) - .flatMap(device -> registry - .unregisterDevice(id) - .then(createUpdate() - .set(DeviceInstanceEntity::getState, DeviceState.notActive.getValue()) - .where(DeviceInstanceEntity::getId, id) - .execute())); + return this + .unregisterDevice(Mono.just(id)) + .thenReturn(1); } /** @@ -218,25 +294,187 @@ public class LocalDeviceInstanceService extends GenericReactiveCrudService unregisterDevice(Publisher ids) { - return Flux.from(ids) - .flatMap(id -> registry.unregisterDevice(id).thenReturn(id)) + return Flux + .from(ids) + .buffer(200) + //先修改状态 + .flatMap(list -> this + .findById(list) + .collectList() + .flatMap(devices -> DeviceUnregisterEvent.of(devices).publish(eventPublisher)) + .then(this + .createUpdate() + .set(DeviceInstanceEntity::getState, DeviceState.notActive.getValue()) + .where().in(DeviceInstanceEntity::getId, list) + .execute() + .thenReturn(list) + )) + .flatMapIterable(Function.identity()) + //再注销 + .flatMap(id -> registry + .getDevice(id) + .flatMap(DeviceOperator::disconnect) + .onErrorResume(err -> Mono.empty()) + .then(registry.unregisterDevice(id)) + .onErrorResume(err -> Mono.empty()) + .thenReturn(id)) + .count() + .map(Long::intValue) + //注销不触发事件,单独处理DeviceDeployedEvent + .as(EntityEventHelper::setDoNotFireEvent); + } + + @Override + public Mono deleteById(Publisher idPublisher) { + return Flux.from(idPublisher) .collectList() - .flatMap(list -> createUpdate() - .set(DeviceInstanceEntity::getState, DeviceState.notActive.getValue()) - .where().in(DeviceInstanceEntity::getId, list) + .flatMap(list -> createDelete() + .where() + .in(DeviceInstanceEntity::getId, list) + .and(DeviceInstanceEntity::getState, DeviceState.notActive) .execute()); } - protected Mono createDeviceDetail(DeviceProductEntity product, - DeviceInstanceEntity device, - List tags) { + private boolean hasContext(QueryParamEntity param, String key) { + return param + .getContext(key) + .map(CastUtils::castBoolean) + .orElse(true); + } + + //分页查询设备详情列表 + public Mono> queryDeviceDetail(QueryParamEntity entity) { + + return this + .queryPager(entity) + .filter(e -> CollectionUtils.isNotEmpty(e.getData())) + .flatMap(result -> this + .convertDeviceInstanceToDetail(result.getData(), + hasContext(entity, "includeTags"), + hasContext(entity, "includeBind"), + hasContext(entity, "includeRelations"), + hasContext(entity, "includeFirmwareInfos")) + .collectList() + .map(detailList -> PagerResult.of(result.getTotal(), detailList, entity))) + .defaultIfEmpty(PagerResult.empty()); + } + + //查询设备详情列表 + public Flux queryDeviceDetailList(QueryParamEntity entity) { + return this + .query(entity) + .collectList() + .flatMapMany(list -> this + .convertDeviceInstanceToDetail(list, + hasContext(entity, "includeTags"), + hasContext(entity, "includeBind"), + hasContext(entity, "includeRelations"), + hasContext(entity, "includeFirmwareInfos"))); + } + + private Mono>> queryDeviceTagGroup(Collection deviceIdList) { + return tagRepository + .createQuery() + .where() + .in(DeviceTagEntity::getDeviceId, deviceIdList) + .fetch() + .collect(Collectors.groupingBy(DeviceTagEntity::getDeviceId)) + .defaultIfEmpty(Collections.emptyMap()); + } + + private Flux convertDeviceInstanceToDetail(List instanceList, + boolean includeTag, + boolean includeBinds, + boolean includeRelations, + boolean includeFirmwareInfos) { + if (CollectionUtils.isEmpty(instanceList)) { + return Flux.empty(); + } + List deviceIdList = new ArrayList<>(instanceList.size()); + //按设备产品分组 + Map> productGroup = instanceList + .stream() + .peek(device -> deviceIdList.add(device.getId())) + .collect(Collectors.groupingBy(DeviceInstanceEntity::getProductId)); + //标签 + Mono>> tags = includeTag + ? this.queryDeviceTagGroup(deviceIdList) + : Mono.just(Collections.emptyMap()); + + //关系信息 + Mono>> relations = includeRelations ? relationService + .getRelationInfo(RelationObjectProvider.TYPE_DEVICE, deviceIdList) + .collect(Collectors.groupingBy(RelatedInfo::getObjectId)) + .defaultIfEmpty(Collections.emptyMap()) + : Mono.just(Collections.emptyMap()); + - DeviceDetail detail = new DeviceDetail().with(product).with(device).with(tags); return Mono .zip( + //T1:查询出所有设备的产品信息 + deviceProductService + .findById(productGroup.keySet()) + .collect(Collectors.toMap(DeviceProductEntity::getId, Function.identity())), + //T2:查询出标签并按设备ID分组 + tags, + //T3: 关系信息 + relations + ) + .flatMapMany(tp5 -> Flux + //遍历设备,将设备信息转为详情. + .fromIterable(instanceList) + .flatMap(instance -> this + .createDeviceDetail( + // 设备 + instance + //产品 + , tp5.getT1().get(instance.getProductId()) + //标签 + , tp5.getT2().get(instance.getId()) + //关系信息 + , tp5.getT3().get(instance.getId()) + ) + )) + //createDeviceDetail是异步操作,可能导致顺序错乱.进行重新排序. + .sort(Comparator.comparingInt(detail -> deviceIdList.indexOf(detail.getId()))) + ; + } + + private Mono createDeviceDetail(DeviceInstanceEntity device, + DeviceProductEntity product, + List tags, + List relations) { + if (product == null) { + log.warn("device [{}] product [{}] does not exists", device.getId(), device.getProductId()); + return Mono.empty(); + } + DeviceDetail detail = new DeviceDetail() + .with(product) + .with(device) + .with(tags) + .withRelation(relations); + + return Mono + .zip( + //产品注册信息 + registry + .getProduct(product.getId()), + //feature信息 + metadataManager + .getProductFeatures(product.getId()) + .collectList()) + .flatMap(t2 -> { + //填充产品中feature信息 + detail.withFeatures(t2.getT2()); + //填充注册中心里的产品信息 + return detail.with(t2.getT1()); + }) + .then(Mono.zip( //设备信息 registry .getDevice(device.getId()) + //先刷新配置缓存 + .flatMap(operator -> operator.refreshAllConfig().thenReturn(operator)) .flatMap(operator -> operator //检查设备的真实状态,可能出现设备已经离线,但是数据库状态未及时更新的. .checkState() @@ -255,11 +493,11 @@ public class LocalDeviceInstanceService extends GenericReactiveCrudService detail + .with(tp2.getT1(), tp2.getT2())) .switchIfEmpty( Mono.defer(() -> { //如果设备注册中心里没有设备信息,并且数据库里的状态不是未激活. @@ -277,39 +515,168 @@ public class LocalDeviceInstanceService extends GenericReactiveCrudService getDeviceDetail(String deviceId) { + return this .findById(deviceId) - .zipWhen(device -> deviceProductService.findById(device.getProductId()))//合并型号 - .zipWith(tagRepository - .createQuery() - .where(DeviceTagEntity::getDeviceId, deviceId) - .fetch() - .collectList() - .defaultIfEmpty(Collections.emptyList()) //合并标签 - , (left, right) -> Tuples.of(left.getT2(), left.getT1(), right)) - .flatMap(tp3 -> createDeviceDetail(tp3.getT1(), tp3.getT2(), tp3.getT3())); + .map(Collections::singletonList) + .flatMapMany(list -> convertDeviceInstanceToDetail(list, true, true, true, true)) + .next(); } public Mono getDeviceState(String deviceId) { - return registry - .getDevice(deviceId) - .flatMap(DeviceOperator::checkState) - .flatMap(state -> { - DeviceState deviceState = DeviceState.of(state); - return this - .createUpdate() - .set(DeviceInstanceEntity::getState, deviceState) - .where(DeviceInstanceEntity::getId, deviceId) - .execute() - .thenReturn(deviceState); - }) - .defaultIfEmpty(DeviceState.notActive); + return registry.getDevice(deviceId) + .flatMap(DeviceOperator::checkState) + .flatMap(state -> { + DeviceState deviceState = DeviceState.of(state); + return createUpdate() + .set(DeviceInstanceEntity::getState, deviceState) + .where(DeviceInstanceEntity::getId, deviceId) + .execute() + .thenReturn(deviceState); + }) + .defaultIfEmpty(DeviceState.notActive); } + //获取设备属性 + @SneakyThrows + public Mono> readProperty(String deviceId, + String property) { + return registry + .getDevice(deviceId) + .switchIfEmpty(ErrorUtils.notFound("error.device_not_found_or_not_activated")) + .map(DeviceOperator::messageSender)//发送消息到设备 + .map(sender -> sender.readProperty(property).messageId(IDGenerator.SNOW_FLAKE_STRING.generate())) + .flatMapMany(ReadPropertyMessageSender::send) + .flatMap(mapReply(ReadPropertyMessageReply::getProperties)) + .reduceWith(LinkedHashMap::new, (main, map) -> { + main.putAll(map); + return main; + }); + + } + + //获取标准设备属性 + @SneakyThrows + public Mono readAndConvertProperty(String deviceId, + String property) { + return registry + .getDevice(deviceId) + .switchIfEmpty(ErrorUtils.notFound("error.device_not_found_or_not_activated")) + .flatMap(deviceOperator -> deviceOperator + .messageSender() + .readProperty(property) + .messageId(IDGenerator.SNOW_FLAKE_STRING.generate()) + .send() + .flatMap(mapReply(ReadPropertyMessageReply::getProperties)) + .reduceWith(LinkedHashMap::new, (main, map) -> { + main.putAll(map); + return main; + }) + .flatMap(map -> { + Object value = map.get(property); + return deviceOperator + .getMetadata() + .map(deviceMetadata -> DeviceProperty.of(value, deviceMetadata.getPropertyOrNull(property))); + })); + + } + + //设置设备属性 + @SneakyThrows + public Mono> writeProperties(String deviceId, + Map properties) { + + return registry + .getDevice(deviceId) + .switchIfEmpty(ErrorUtils.notFound("error.device_not_found_or_not_activated")) + .flatMap(operator -> operator + .messageSender() + .writeProperty() + .messageId(IDGenerator.SNOW_FLAKE_STRING.generate()) + .write(properties) + .validate() + ) + .flatMapMany(WritePropertyMessageSender::send) + .flatMap(mapReply(WritePropertyMessageReply::getProperties)) + .reduceWith(LinkedHashMap::new, (main, map) -> { + main.putAll(map); + return main; + }); + } + + //设备功能调用 + @SneakyThrows + public Flux invokeFunction(String deviceId, + String functionId, + Map properties) { + return invokeFunction(deviceId, functionId, properties, true); + } + + //设备功能调用 + @SneakyThrows + public Flux invokeFunction(String deviceId, + String functionId, + Map properties, + boolean convertReply) { + return registry + .getDevice(deviceId) + .switchIfEmpty(ErrorUtils.notFound("error.device_not_found_or_not_activated")) + .flatMap(operator -> operator + .messageSender() + .invokeFunction(functionId) + .messageId(IDGenerator.SNOW_FLAKE_STRING.generate()) + .setParameter(properties) + .validate() + ) + .flatMapMany(FunctionInvokeMessageSender::send) + .flatMap(convertReply ? mapReply(FunctionInvokeMessageReply::getOutput) : Mono::just); + + + } + + //获取设备所有属性 + @SneakyThrows + public Mono> readProperties(String deviceId, List properties) { + + return registry.getDevice(deviceId) + .switchIfEmpty(ErrorUtils.notFound("error.device_not_found_or_not_activated")) + .map(DeviceOperator::messageSender) + .flatMapMany((sender) -> sender.readProperty() + .read(properties) + .messageId(IDGenerator.SNOW_FLAKE_STRING.generate()) + .send()) + .flatMap(mapReply(ReadPropertyMessageReply::getProperties)) + .reduceWith(LinkedHashMap::new, (main, map) -> { + main.putAll(map); + return main; + }); + } + + private static Function> mapReply(Function function) { + return reply -> { + if (ErrorCode.REQUEST_HANDLING.name().equals(reply.getCode())) { + throw new DeviceOperationException(ErrorCode.REQUEST_HANDLING, reply.getMessage()); + } + if (!reply.isSuccess()) { + if (StringUtils.isEmpty(reply.getMessage())) { + throw new BusinessException("error.reply_is_error"); + } + throw new BusinessException(reply.getMessage(), reply.getCode()); + } + return Mono.justOrEmpty(function.apply(reply)); + }; + } + + /** + * 批量同步设备状态 + * + * @param batch 设备状态ID流 + * @param force 是否强制获取设备状态,强制获取会去设备连接到服务器检查设备是否真实在线 + * @return 同步数量 + */ public Flux> syncStateBatch(Flux> batch, boolean force) { return batch @@ -363,92 +730,108 @@ public class LocalDeviceInstanceService extends GenericReactiveCrudService Function> mapReply(Function function) { - return reply -> { - if (ErrorCode.REQUEST_HANDLING.name().equals(reply.getCode())) { - throw new DeviceOperationException(ErrorCode.REQUEST_HANDLING, reply.getMessage()); - } - if (!reply.isSuccess()) { - throw new BusinessException(reply.getMessage(), reply.getCode()); - } - return Mono.justOrEmpty(function.apply(reply)); - }; - } - //获取标准设备属性 - @SneakyThrows - public Mono readAndConvertProperty(String deviceId, - String property) { - return registry - .getDevice(deviceId) - .switchIfEmpty(ErrorUtils.notFound("设备不存在")) - .flatMap(deviceOperator -> deviceOperator - .messageSender() - .readProperty(property) - .messageId(IDGenerator.SNOW_FLAKE_STRING.generate()) - .send() - .flatMap(mapReply(ReadPropertyMessageReply::getProperties)) - .reduceWith(LinkedHashMap::new, (main, map) -> { - main.putAll(map); - return main; - }) - .flatMap(map -> { - Object value = map.get(property); - return deviceOperator - .getMetadata() - .map(deviceMetadata -> deviceMetadata - .getProperty(property) - .map(PropertyMetadata::getValueType) - .orElse(new StringType())) - .map(dataType -> DevicePropertiesEntity - .builder() - .deviceId(deviceId) - .productId(property) - .build() - .withValue(dataType, value)); - })); + public Mono mergeMetadata(String deviceId, DeviceMetadata metadata, MergeOption... options) { - } - - //设置设备属性 - @SneakyThrows - public Mono> writeProperties(String deviceId, - Map properties) { - - return registry - .getDevice(deviceId) - .switchIfEmpty(ErrorUtils.notFound("设备不存在")) - .map(operator -> operator - .messageSender() - .writeProperty() - .messageId(IDGenerator.SNOW_FLAKE_STRING.generate()) - .write(properties) + return Mono + .zip(this.findById(deviceId) + .flatMap(device -> { + if (StringUtils.hasText(device.getDeriveMetadata())) { + return Mono.just(device.getDeriveMetadata()); + } else { + return deviceProductService + .findById(device.getProductId()) + .map(DeviceProductEntity::getMetadata); + } + }) + .flatMap(JetLinksDeviceMetadataCodec.getInstance()::decode), + Mono.just(metadata), + (older, newer) -> older.merge(newer, options) ) - .flatMapMany(WritePropertyMessageSender::send) - .flatMap(mapReply(WritePropertyMessageReply::getProperties)) - .reduceWith(LinkedHashMap::new, (main, map) -> { - main.putAll(map); - return main; - }); + .flatMap(JetLinksDeviceMetadataCodec.getInstance()::encode) + .flatMap(newMetadata -> createUpdate() + .set(DeviceInstanceEntity::getDeriveMetadata, newMetadata) + .where(DeviceInstanceEntity::getId, deviceId) + .execute() + .then( + registry + .getDevice(deviceId) + .flatMap(device -> device.updateMetadata(newMetadata)) + )) + .then(); } - //设备功能调用 - @SneakyThrows - public Flux invokeFunction(String deviceId, - String functionId, - Map properties) { - return registry - .getDevice(deviceId) - .switchIfEmpty(ErrorUtils.notFound("设备不存在")) - .flatMap(operator -> operator - .messageSender() - .invokeFunction(functionId) - .messageId(IDGenerator.SNOW_FLAKE_STRING.generate()) - .setParameter(properties) - .validate() + public Flux queryDeviceTag(String deviceId, String... tags) { + return tagRepository + .createQuery() + .where(DeviceTagEntity::getDeviceId, deviceId) + .when(tags.length > 0, q -> q.in(DeviceTagEntity::getKey, Arrays.asList(tags))) + .fetch(); + } + + //删除设备时,删除设备标签 + @EventListener + public void handleDeviceDelete(EntityDeletedEvent event) { + event.async( + Flux.concat( + Flux + .fromIterable(event.getEntity()) + .flatMap(device -> registry + .unregisterDevice(device.getId()) + .onErrorResume(err -> Mono.empty()) + ) + .then(), + tagRepository + .createDelete() + .where() + .in(DeviceTagEntity::getDeviceId, event + .getEntity() + .stream() + .map(DeviceInstanceEntity::getId) + .collect(Collectors.toSet())) + .execute() ) - .flatMapMany(FunctionInvokeMessageSender::send) - .flatMap(mapReply(FunctionInvokeMessageReply::getOutput)); + ); + } + + @Override + public Mono insert(DeviceInstanceEntity data) { + return this + .handleCreateBefore(data) + .flatMap(super::insert); + } + + @Override + public Mono insert(Publisher entityPublisher) { + return super.insert(Flux.from(entityPublisher).flatMap(this::handleCreateBefore)); + } + + @Override + public Mono insertBatch(Publisher> entityPublisher) { + return Flux.from(entityPublisher) + .flatMapIterable(Function.identity()) + .as(this::insert); + } + + private Mono handleCreateBefore(DeviceInstanceEntity instanceEntity) { + return Mono + .zip( + deviceProductService.findById(instanceEntity.getProductId()), + registry + .getProduct(instanceEntity.getProductId()) + .flatMap(DeviceProductOperator::getProtocol), + (product, protocol) -> protocol.doBeforeDeviceCreate(Transport.of(product.getTransportProtocol()), instanceEntity + .toDeviceInfo()) + ) + .flatMap(Function.identity()) + .doOnNext(info -> { + if (StringUtils.isEmpty(instanceEntity.getId())) { + instanceEntity.setId(info.getId()); + } + instanceEntity.mergeConfiguration(info.getConfiguration()); + }) + .thenReturn(instanceEntity); + } private final CyclicDependencyChecker checker = CyclicDependencyChecker @@ -495,34 +878,4 @@ public class LocalDeviceInstanceService extends GenericReactiveCrudService deletedHandle(Flux devices) { - return devices.filter(device -> !StringUtils.isEmpty(device.getParentId())) - .groupBy(DeviceInstanceEntity::getParentId) - .flatMap(group -> { - String parentId = group.key(); - return group.flatMap(child -> registry.getDevice(child.getId()) - .flatMap(device -> device.removeConfig(DeviceConfigKey.parentGatewayId.getKey()).thenReturn(device)) - ) - .as(childrenDeviceOp -> registry.getDevice(parentId) - .flatMap(gwOperator -> gwOperator.getProtocol() - .flatMap(protocolSupport -> protocolSupport.onChildUnbind(gwOperator, childrenDeviceOp)) - ) - ); - }) - // 取消激活 - .thenMany( - devices.filter(device -> device.getState() != DeviceState.notActive) - .flatMap(device -> registry.unregisterDevice(device.getId())) - ); - } - - @EventListener - public void deletedHandle(EntityDeletedEvent event) { - event.async( - this.deletedHandle(Flux.fromIterable(event.getEntity())).then() - ); - } } diff --git a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/service/LocalDeviceProductService.java b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/service/LocalDeviceProductService.java index c162c395..51285ec0 100644 --- a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/service/LocalDeviceProductService.java +++ b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/service/LocalDeviceProductService.java @@ -4,6 +4,7 @@ import lombok.extern.slf4j.Slf4j; import org.hswebframework.ezorm.rdb.mapping.ReactiveRepository; import org.hswebframework.web.bean.FastBeanCopier; import org.hswebframework.web.crud.service.GenericReactiveCrudService; +import org.hswebframework.web.exception.BusinessException; import org.jetlinks.community.device.entity.DeviceInstanceEntity; import org.jetlinks.community.device.entity.DeviceProductEntity; import org.jetlinks.community.device.enums.DeviceProductState; @@ -13,6 +14,7 @@ import org.reactivestreams.Publisher; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Service; +import org.springframework.util.Assert; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -29,10 +31,13 @@ public class LocalDeviceProductService extends GenericReactiveCrudService instanceRepository; + public Mono deploy(String id) { return findById(Mono.just(id)) + .doOnNext(this::validateDeviceProduct) .flatMap(product -> registry .register(product.toProductInfo()) + .onErrorMap(e -> new BusinessException("error.unable_to_load_protocol_by_access_id", 404, product.getMessageProtocol())) .then( createUpdate() .set(DeviceProductEntity::getState, DeviceProductState.registered.getValue()) @@ -46,6 +51,13 @@ public class LocalDeviceProductService extends GenericReactiveCrudService cancelDeploy(String id) { return createUpdate() diff --git a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/service/data/DatabaseDeviceLatestDataService.java b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/service/data/DatabaseDeviceLatestDataService.java new file mode 100755 index 00000000..a05f2c0b --- /dev/null +++ b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/service/data/DatabaseDeviceLatestDataService.java @@ -0,0 +1,651 @@ +package org.jetlinks.community.device.service.data; + +import com.google.common.collect.Maps; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import org.hswebframework.ezorm.core.ValueCodec; +import org.hswebframework.ezorm.rdb.codec.ClobValueCodec; +import org.hswebframework.ezorm.rdb.codec.DateTimeCodec; +import org.hswebframework.ezorm.rdb.codec.JsonValueCodec; +import org.hswebframework.ezorm.rdb.codec.NumberValueCodec; +import org.hswebframework.ezorm.rdb.executor.wrapper.ResultWrappers; +import org.hswebframework.ezorm.rdb.mapping.ReactiveRepository; +import org.hswebframework.ezorm.rdb.mapping.defaults.record.Record; +import org.hswebframework.ezorm.rdb.metadata.RDBColumnMetadata; +import org.hswebframework.ezorm.rdb.metadata.RDBSchemaMetadata; +import org.hswebframework.ezorm.rdb.metadata.RDBTableMetadata; +import org.hswebframework.ezorm.rdb.operator.DatabaseOperator; +import org.hswebframework.ezorm.rdb.operator.ddl.TableBuilder; +import org.hswebframework.ezorm.rdb.operator.dml.SelectColumnSupplier; +import org.hswebframework.ezorm.rdb.operator.dml.query.Selects; +import org.hswebframework.web.api.crud.entity.QueryParamEntity; +import org.hswebframework.web.exception.ValidationException; +import org.jetlinks.core.event.Subscription; +import org.jetlinks.core.message.DeviceMessage; +import org.jetlinks.core.message.event.EventMessage; +import org.jetlinks.core.metadata.DataType; +import org.jetlinks.core.metadata.DeviceMetadata; +import org.jetlinks.core.metadata.EventMetadata; +import org.jetlinks.core.metadata.PropertyMetadata; +import org.jetlinks.core.metadata.types.*; +import org.jetlinks.core.utils.Reactors; +import org.jetlinks.core.utils.SerializeUtils; +import org.jetlinks.core.utils.StringBuilderUtils; +import org.jetlinks.community.ConfigMetadataConstants; +import org.jetlinks.community.buffer.BufferProperties; +import org.jetlinks.community.buffer.BufferSettings; +import org.jetlinks.community.buffer.PersistenceBuffer; +import org.jetlinks.community.device.entity.DeviceLatestData; +import org.jetlinks.community.gateway.DeviceMessageUtils; +import org.jetlinks.community.gateway.annotation.Subscribe; +import org.jetlinks.community.timeseries.query.Aggregation; +import org.jetlinks.community.timeseries.query.AggregationColumn; +import org.jetlinks.reactor.ql.utils.CastUtils; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.CollectionUtils; +import org.springframework.util.StringUtils; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.core.scheduler.Schedulers; +import reactor.math.MathFlux; +import reactor.util.function.Tuple2; +import reactor.util.function.Tuples; + +import java.io.Externalizable; +import java.io.IOException; +import java.io.ObjectInput; +import java.io.ObjectOutput; +import java.sql.JDBCType; +import java.time.Duration; +import java.util.*; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * 设备最新数据服务,用于保存设备最新的相关数据到关系型数据库中,可以使用动态条件进行查询相关数据 + * + * @author zhouhao + * @since 1.5.0 + */ +@Slf4j +public class DatabaseDeviceLatestDataService implements DeviceLatestDataService { + + private final DatabaseOperator databaseOperator; + + private final BufferProperties buffer; + + private PersistenceBuffer writer; + + public DatabaseDeviceLatestDataService(DatabaseOperator databaseOperator, BufferProperties properties) { + this.databaseOperator = databaseOperator; + this.buffer = properties; + init(); + } + + public static String getLatestTableTableName(String productId) { + return StringBuilderUtils.buildString(productId, (p, b) -> { + b.append("dev_lst_"); + for (char c : productId.toCharArray()) { + if (c == '-' || c == '.') { + b.append('_'); + } else { + b.append(Character.toLowerCase(c)); + } + } + }); + } + + private String getEventColumn(String event, String property) { + return event + "_" + property; + } + + private Mono doWrite(Flux flux) { + return flux + .groupBy(Buffer::getTable, Integer.MAX_VALUE) + .concatMap(group -> group + .groupBy(Buffer::getDeviceId, Integer.MAX_VALUE) + .flatMap(sameDevice -> sameDevice.reduce(Buffer::merge)) + .buffer(200) + //批量更新 + .flatMap(sameTableData -> { + Buffer first = sameTableData.get(0); + List> data = sameTableData + .stream() + .map(Buffer::getProperties) + .collect(Collectors.toList()); + return this + .doUpdateLatestData(first.table, data) + .onErrorResume((err) -> { + log.error("save device latest data error", err); + return Mono.empty(); + }); + })) + .then(Reactors.ALWAYS_FALSE); + + } + + public void init() { + + writer = new PersistenceBuffer<>( + BufferSettings.create("./data/buffer", buffer), + Buffer::new, + this::doWrite) + .name("device-latest-data") + //最大缓冲10万条数据 + .settings(setting -> setting.bufferSize(10_0000)); + + writer.start(); + + } + + public void destroy() { + writer.dispose(); + } + + static GeoCodec geoCodec = new GeoCodec(); + + static StringCodec stringCodec = new StringCodec(); + + static class GeoCodec implements ValueCodec { + + @Override + public String encode(Object value) { + return String.valueOf(value); + } + + @Override + public GeoPoint decode(Object data) { + return GeoPoint.of(data); + } + } + + static class StringCodec implements ValueCodec { + + @Override + public String encode(Object value) { + return String.valueOf(value); + } + + @Override + public String decode(Object data) { + return String.valueOf(data); + } + } + + Class getJavaType(DataType dataType) { + if (null == dataType) { + return Map.class; + } + switch (dataType.getType()) { + case IntType.ID: + return Integer.class; + case LongType.ID: + return Long.class; + case FloatType.ID: + return Float.class; + case DoubleType.ID: + return Double.class; + case BooleanType.ID: + return Boolean.class; + case DateTimeType.ID: + return Date.class; + case ArrayType.ID: + return List.class; + case GeoType.ID: + case ObjectType.ID: + return Map.class; + default: + return String.class; + } + } + + RDBColumnMetadata convertColumn(PropertyMetadata metadata) { + RDBColumnMetadata column = new RDBColumnMetadata(); + column.setName(metadata.getId()); + column.setComment(metadata.getName()); + DataType type = metadata.getValueType(); + if (type instanceof NumberType) { + column.setLength(32); + column.setPrecision(32); + if (type instanceof DoubleType) { + column.setScale(Optional.ofNullable(((DoubleType) type).getScale()).orElse(2)); + column.setValueCodec(new NumberValueCodec(Double.class)); + column.setJdbcType(JDBCType.NUMERIC, Double.class); + } else if (type instanceof FloatType) { + column.setScale(Optional.ofNullable(((FloatType) type).getScale()).orElse(2)); + column.setValueCodec(new NumberValueCodec(Float.class)); + column.setJdbcType(JDBCType.NUMERIC, Float.class); + } else if (type instanceof LongType) { + column.setValueCodec(new NumberValueCodec(Long.class)); + column.setJdbcType(JDBCType.NUMERIC, Long.class); + } else { + column.setValueCodec(new NumberValueCodec(IntType.class)); + column.setJdbcType(JDBCType.NUMERIC, Integer.class); + } + } else if (type instanceof ObjectType) { + column.setJdbcType(JDBCType.CLOB, String.class); + column.setValueCodec(JsonValueCodec.of(Map.class)); + } else if (type instanceof ArrayType) { + column.setJdbcType(JDBCType.CLOB, String.class); + ArrayType arrayType = ((ArrayType) type); + column.setValueCodec(JsonValueCodec.ofCollection(ArrayList.class, getJavaType(arrayType.getElementType()))); + } else if (type instanceof DateTimeType) { + column.setJdbcType(JDBCType.TIMESTAMP, Long.class); + String format = ((DateTimeType) type).getFormat(); + if (DateTimeType.TIMESTAMP_FORMAT.equals(format)) { + format = "yyyy-MM-dd HH:mm:ss"; + } + column.setValueCodec(new DateTimeCodec(format, Long.class)); + } else if (type instanceof GeoType) { + column.setJdbcType(JDBCType.VARCHAR, String.class); + column.setValueCodec(geoCodec); + column.setLength(128); + } else if (type instanceof EnumType) { + column.setJdbcType(JDBCType.VARCHAR, String.class); + column.setValueCodec(stringCodec); + column.setLength(64); + } else { + int len = type + .getExpand(ConfigMetadataConstants.maxLength.getKey()) + .filter(o -> !StringUtils.isEmpty(o)) + .map(CastUtils::castNumber) + .map(Number::intValue) + .orElse(255); + if (len > 2048) { + column.setJdbcType(JDBCType.LONGVARBINARY, String.class); + column.setValueCodec(ClobValueCodec.INSTANCE); + } else { + column.setJdbcType(JDBCType.VARCHAR, String.class); + column.setLength(len); + column.setValueCodec(stringCodec); + } + } + + return column; + } + + + public Mono reloadMetadata(String productId, DeviceMetadata metadata) { + return Mono + .defer(() -> { + String tableName = getLatestTableTableName(productId); + log.debug("reload product[{}] metadata,table name:[{}] ", productId, tableName); + RDBSchemaMetadata schema = databaseOperator.getMetadata() + .getCurrentSchema(); + + RDBTableMetadata table = schema.newTable(tableName); + + RDBColumnMetadata id = table.newColumn(); + id.setName("id"); + id.setLength(64); + id.setPrimaryKey(true); + id.setJdbcType(JDBCType.VARCHAR, String.class); + table.addColumn(id); + + RDBColumnMetadata deviceName = table.newColumn(); + deviceName.setLength(128); + deviceName.setName("device_name"); + deviceName.setAlias("deviceName"); + deviceName.setJdbcType(JDBCType.VARCHAR, String.class); + table.addColumn(deviceName); + + for (PropertyMetadata property : metadata.getProperties()) { + table.addColumn(convertColumn(property)); + } + for (EventMetadata event : metadata.getEvents()) { + DataType type = event.getType(); + if (type instanceof ObjectType) { + for (PropertyMetadata property : ((ObjectType) type).getProperties()) { + RDBColumnMetadata column = convertColumn(property); + column.setName(getEventColumn(event.getId(), property.getId())); + table.addColumn(column); + } + } + } + + return schema + .getTableReactive(tableName, false) + .doOnNext(oldTable -> oldTable.replace(table)) + .switchIfEmpty(Mono.fromRunnable(() -> schema.addTable(table))) + .then(); + }); + } + + @Transactional(propagation = Propagation.NEVER) + public Mono upgradeMetadata(String productId, DeviceMetadata metadata, boolean ddl) { + return Mono + .defer(() -> { + String tableName = getLatestTableTableName(productId); + log.debug("upgrade product[{}] metadata,table name:[{}] ", productId, tableName); + TableBuilder builder = databaseOperator + .ddl() + .createOrAlter(tableName) + .addColumn("id").primaryKey().varchar(64).commit() + .addColumn("device_name").alias("deviceName").varchar(128).notNull().commit() + .merge(true) + .allowAlter(ddl); + + for (PropertyMetadata property : metadata.getProperties()) { + builder.addColumn(convertColumn(property)); + } + for (EventMetadata event : metadata.getEvents()) { + DataType type = event.getType(); + if (type instanceof ObjectType) { + for (PropertyMetadata property : ((ObjectType) type).getProperties()) { + RDBColumnMetadata column = convertColumn(property); + column.setName(getEventColumn(event.getId(), property.getId())); + builder.addColumn(column); + } + } + } + return builder + .commit() + .reactive() + .subscribeOn(Schedulers.boundedElastic()) + .then(); + }); + } + + public Mono upgradeMetadata(String productId, DeviceMetadata metadata) { + return upgradeMetadata(productId, metadata, true); + } + + @Subscribe(topics = "/device/**", features = Subscription.Feature.local) + public void save(DeviceMessage message) { + try { + Map properties = DeviceMessageUtils + .tryGetProperties(message) + .orElseGet(() -> { + //事件 + if (message instanceof EventMessage) { + Object data = ((EventMessage) message).getData(); + String event = ((EventMessage) message).getEvent(); + if (data instanceof Map) { + Map mapValue = (Map) data; + Map val = Maps.newHashMapWithExpectedSize(mapValue.size()); + ((Map) data).forEach((k, v) -> val.put(getEventColumn(event, String.valueOf(k)), v)); + return val; + } + return Collections.singletonMap(getEventColumn(event, "value"), data); + } + return null; + }); + if (CollectionUtils.isEmpty(properties)) { + return; + } + String productId = message.getHeader("productId").map(String::valueOf).orElse("null"); + String deviceName = message.getHeader("deviceName").map(String::valueOf).orElse(message.getDeviceId()); + String tableName = getLatestTableTableName(productId); + Map prob = new HashMap<>(properties); + prob.put("id", message.getDeviceId()); + prob.put("deviceName", deviceName); + + Buffer buffer = Buffer.of(tableName, message.getDeviceId(), deviceName, prob, message.getTimestamp()); + writer.write(buffer); + } catch (Exception e) { + log.error(e.getMessage(), e); + } + } + + @Getter + private static class Buffer implements Externalizable { + //有效期 + private final static long expires = Duration.ofSeconds(30).toMillis(); + + private String table; + + private String deviceId; + + private String deviceName; + + private Map properties; + + private long timestamp; + + + public Buffer() { + } + + public boolean isEffective() { + return System.currentTimeMillis() - timestamp < expires; + } + + public static Buffer of(String table, + String deviceId, + String deviceName, + Map properties, + long timestamp) { + Buffer buffer = new Buffer(); + buffer.table = table; + buffer.deviceId = deviceId; + buffer.deviceName = deviceName; + buffer.properties = properties; + buffer.timestamp = timestamp; + return buffer; + } + + public Buffer merge(Buffer buffer) { + + //以比较新的数据为准 + if (buffer.timestamp > this.timestamp) { + return buffer.merge(this); + } + //合并 + buffer.properties.forEach(properties::putIfAbsent); + return this; + } + + int size() { + return properties == null ? 0 : properties.size(); + } + + @Override + public void writeExternal(ObjectOutput out) throws IOException { + out.writeUTF(table); + out.writeUTF(deviceId); + out.writeUTF(deviceName); + out.writeLong(timestamp); + SerializeUtils.writeObject(properties, out); + } + + @Override + @SuppressWarnings("all") + public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { + table = in.readUTF(); + deviceId = in.readUTF(); + deviceName = in.readUTF(); + timestamp = in.readLong(); + properties = (Map) SerializeUtils.readObject(in); + } + } + + @Transactional(propagation = Propagation.REQUIRES_NEW) + public Mono doUpdateLatestData(String table, + List> properties) { + return databaseOperator + .getMetadata() + .getCurrentSchema() + .getTableReactive(table, false) + .flatMap(ignore -> { + //没有deviceName,说明可能在同步表结构的时候发生了错误。 + if (!ignore.getColumn("deviceName").isPresent()) { + log.warn("设备最新数据表[{}]结构错误", table); + return Mono.empty(); + } + return databaseOperator + .dml() + .upsert(table) + .ignoreUpdate("id") + .values(properties) + .execute() + .reactive() + .then(); + }); + } + + public ReactiveRepository getRepository(String productId) { + return databaseOperator + .dml() + .createReactiveRepository(getLatestTableTableName(productId)); + } + + @Override + public Flux query(String productId, QueryParamEntity param) { + return getRepository(productId) + .createQuery() + .setParam(param) + .fetch() + .map(DeviceLatestData::new); + } + + @Override + public Mono queryDeviceData(String productId, String deviceId) { + return getRepository(productId) + .findById(deviceId) + .map(DeviceLatestData::new); + } + + @Override + public Mono count(String productId, QueryParamEntity param) { + return getRepository(productId) + .createQuery() + .setParam(param) + .count(); + } + + private SelectColumnSupplier createAggColumn(AggregationColumn column) { + switch (column.getAggregation()) { + case COUNT: + return Selects.count(column.getProperty()).as(column.getAlias()); + case AVG: + return Selects.avg(column.getProperty()).as(column.getAlias()); + case MAX: + return Selects.max(column.getProperty()).as(column.getAlias()); + case MIN: + return Selects.min(column.getProperty()).as(column.getAlias()); + case SUM: + return Selects.sum(column.getProperty()).as(column.getAlias()); + default: + throw new UnsupportedOperationException("unsupported agg:" + column.getAggregation()); + } + } + + private SelectColumnSupplier[] createAggColumns(List columns) { + return columns + .stream() + .map(this::createAggColumn) + .toArray(SelectColumnSupplier[]::new); + } + + @Override + public Mono> aggregation(String productId, + List columns, + QueryParamEntity paramEntity) { + if (CollectionUtils.isEmpty(columns)) { + return Mono.error(new ValidationException("columns", "error.aggregate_column_cannot_be_empty")); + } + String table = getLatestTableTableName(productId); + + return databaseOperator + .getMetadata() + .getTableReactive(table) + .flatMap(tableMetadata -> + { + List illegals = new ArrayList<>(); + + List columnList = columns + .stream() + .filter(column -> { + if (tableMetadata + .getColumn(column.getProperty()) + .isPresent()) { + return true; + } + illegals.add(column.getProperty()); + return false; + }) + .collect(Collectors.toList()); + if (CollectionUtils.isEmpty(columnList)) { + return Mono.error(new ValidationException("columns", "error.invalid_product_attribute_or_event", productId, illegals)); + } + return databaseOperator + .dml() + .query(table) + .select(createAggColumns(columnList)) + .setParam(paramEntity.clone().noPaging()) + .fetch(ResultWrappers.map()) + .reactive() + .take(1) + .singleOrEmpty() + .doOnNext(map -> { + for (AggregationColumn column : columns) { + map.putIfAbsent(column.getAlias(), 0); + } + }) + //表不存在 + .onErrorReturn(e -> StringUtils.hasText(e.getMessage()) && e + .getMessage() + .contains("doesn't exist "), Collections.emptyMap()); + } + ); + + } + + @Override + public Flux> aggregation(Flux param, + boolean merge) { + Flux cached = param.cache(); + return cached + .flatMap(request -> this + .aggregation(request.getProductId(), request.getColumns(), request.getQuery()) + .doOnNext(map -> { + if (!merge) { + map.put("productId", request.getProductId()); + } + })) + .as(flux -> { + if (!merge) { + return flux; + } + //合并所有产品的字段到一条数据中,合并时,使用第一个聚合字段使用的聚合类型 + return cached + .take(1) + .flatMapIterable(QueryLatestDataRequest::getColumns) + .collectMap(AggregationColumn::getAlias, agg -> aggMappers.getOrDefault(agg.getAggregation(), sum)) + .flatMap(mappers -> flux + .flatMapIterable(Map::entrySet) + .groupBy(Map.Entry::getKey, Integer.MAX_VALUE) + .flatMap(group -> mappers + .getOrDefault(group.key(), sum) + .apply(group.map(Map.Entry::getValue)) + .map(val -> Tuples.of(String.valueOf(group.key()), (Object) val))) + .collectMap(Tuple2::getT1, Tuple2::getT2)).flux(); + }); + } + + + static Map, Mono>> aggMappers = new HashMap<>(); + + static Function, Mono> avg = flux -> MathFlux.averageDouble(flux + .map(CastUtils::castNumber) + .map(Number::doubleValue)); + static Function, Mono> max = flux -> MathFlux.max(flux + .map(CastUtils::castNumber) + .map(Number::doubleValue)); + static Function, Mono> min = flux -> MathFlux.min(flux + .map(CastUtils::castNumber) + .map(Number::doubleValue)); + static Function, Mono> sum = flux -> MathFlux.sumDouble(flux + .map(CastUtils::castNumber) + .map(Number::doubleValue)); + + static { + aggMappers.put(Aggregation.AVG, avg); + aggMappers.put(Aggregation.MAX, max); + aggMappers.put(Aggregation.MIN, min); + aggMappers.put(Aggregation.SUM, sum); + aggMappers.put(Aggregation.COUNT, sum); + } + +} diff --git a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/service/data/DeviceDataService.java b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/service/data/DeviceDataService.java index c881b17b..e96b3053 100644 --- a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/service/data/DeviceDataService.java +++ b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/service/data/DeviceDataService.java @@ -11,6 +11,7 @@ import org.jetlinks.community.device.entity.DeviceOperationLogEntity; import org.jetlinks.community.device.entity.DeviceProperty; import org.jetlinks.community.timeseries.query.Aggregation; import org.jetlinks.community.timeseries.query.AggregationData; +import org.jetlinks.core.config.ConfigKey; import org.jetlinks.core.message.DeviceMessage; import org.jetlinks.core.metadata.DeviceMetadata; import org.jetlinks.core.metadata.EventMetadata; @@ -33,6 +34,9 @@ import java.util.Map; */ public interface DeviceDataService { + + ConfigKey STORE_POLICY_CONFIG_KEY = ConfigKey.of("storePolicy", "存储策略", String.class); + /** * 注册设备物模型信息 * diff --git a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/service/data/DeviceLatestDataService.java b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/service/data/DeviceLatestDataService.java new file mode 100755 index 00000000..a205fe8f --- /dev/null +++ b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/service/data/DeviceLatestDataService.java @@ -0,0 +1,138 @@ +package org.jetlinks.community.device.service.data; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; +import org.hswebframework.web.api.crud.entity.PagerResult; +import org.hswebframework.web.api.crud.entity.QueryParamEntity; +import org.jetlinks.core.message.DeviceMessage; +import org.jetlinks.core.metadata.DeviceMetadata; +import org.jetlinks.community.device.entity.DeviceLatestData; +import org.jetlinks.community.doc.QueryConditionOnly; +import org.jetlinks.community.timeseries.query.AggregationColumn; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import java.util.List; +import java.util.Map; + +/** + * 设备最新数据服务,用于保存设备最新的相关数据到关系型数据库中,可以使用动态条件进行查询相关数据 + * + * @author zhouhao + * @since 1.5.0 + */ +public interface DeviceLatestDataService { + + /** + * 根据物模型更新产品表结构信息 + * + * @param productId 产品ID + * @param metadata 物模型 + * @return void + */ + Mono upgradeMetadata(String productId, DeviceMetadata metadata); + + /** + * 重新加载物模型信息 + * + * @param productId 产品ID + * @param metadata 物模型 + * @return void + */ + Mono reloadMetadata(String productId, DeviceMetadata metadata); + + /** + * 保存消息数据 + * + * @param message 设备消息 + */ + void save(DeviceMessage message); + + /** + * 根据产品ID 查询最新的数据 + * + * @param productId 产品ID + * @param param 查询参数 + * @return 数据列表 + */ + Flux query(String productId, QueryParamEntity param); + + /** + * 查询设备最新属性数据 + * + * @param productId 产品ID + * @param deviceId 设备ID + * @return 属性数据 + */ + Mono queryDeviceData(String productId, String deviceId); + + /** + * 根据产品ID查询数量 + * + * @param productId 产品ID + * @param param 参数 + * @return 查询数量 + */ + Mono count(String productId, QueryParamEntity param); + + /** + * 根据产品ID分页查询数据 + * + * @param productId 产品ID + * @param param 查询条件参数 + * @return 分页结果数据 + */ + default Mono> queryPager(String productId, QueryParamEntity param) { + return Mono + .zip( + query(productId, param).collectList(), + count(productId, param), + (data, total) -> PagerResult.of(total, data, param) + ) + .defaultIfEmpty(PagerResult.empty()); + } + + /** + * 根据产品ID聚合查询数据 + * + * @param productId 产品ID + * @param columns 聚合列 + * @param paramEntity 查询条件参数 + * @return 聚合结果 + */ + Mono> aggregation(String productId, + List columns, + QueryParamEntity paramEntity); + + /** + * 聚合查询多个产品下设备最新的数据 + * + * @param param 参数 + * @param merge 是否将所有数据合并在一起 + * @return 查询结果 + */ + Flux> aggregation(Flux param, + boolean merge); + + + @Getter + @Setter + class QueryProductLatestDataRequest extends QueryLatestDataRequest { + @NotBlank + @Schema(defaultValue = "产品ID") + private String productId; + } + + @Getter + @Setter + class QueryLatestDataRequest { + @NotNull + private List columns; + + @Schema(implementation = QueryConditionOnly.class) + private QueryParamEntity query = QueryParamEntity.of(); + } +} diff --git a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/service/data/NonDeviceLatestDataService.java b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/service/data/NonDeviceLatestDataService.java new file mode 100644 index 00000000..195394c6 --- /dev/null +++ b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/service/data/NonDeviceLatestDataService.java @@ -0,0 +1,54 @@ +package org.jetlinks.community.device.service.data; + +import org.hswebframework.web.api.crud.entity.QueryParamEntity; +import org.jetlinks.core.message.DeviceMessage; +import org.jetlinks.core.metadata.DeviceMetadata; +import org.jetlinks.community.device.entity.DeviceLatestData; +import org.jetlinks.community.timeseries.query.AggregationColumn; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.util.List; +import java.util.Map; + +public class NonDeviceLatestDataService implements DeviceLatestDataService { + @Override + public Mono upgradeMetadata(String productId, DeviceMetadata metadata) { + return Mono.empty(); + } + + @Override + public Mono reloadMetadata(String productId, DeviceMetadata metadata) { + return Mono.empty(); + } + + @Override + public void save(DeviceMessage message) { + + } + + @Override + public Flux query(String productId, QueryParamEntity param) { + return Flux.empty(); + } + + @Override + public Mono queryDeviceData(String productId, String deviceId) { + return Mono.empty(); + } + + @Override + public Mono count(String productId, QueryParamEntity param) { + return Mono.empty(); + } + + @Override + public Mono> aggregation(String productId, List columns, QueryParamEntity paramEntity) { + return Mono.empty(); + } + + @Override + public Flux> aggregation(Flux param, boolean merge) { + return Flux.empty(); + } +} diff --git a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/service/data/StorageConstants.java b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/service/data/StorageConstants.java new file mode 100755 index 00000000..97f4df90 --- /dev/null +++ b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/service/data/StorageConstants.java @@ -0,0 +1,38 @@ +package org.jetlinks.community.device.service.data; + +import org.jetlinks.core.metadata.PropertyMetadata; + +public interface StorageConstants { + String storePolicyConfigKey = "storePolicy"; + + String propertyStorageType = "storageType"; + String propertyStorageTypeJson = "json-string"; + String propertyStorageTypeIgnore = "ignore"; + + /** + * 判断属性是否使用json字符串来存储 + * + * @param metadata 属性物模型 + * @return 是否使用json字符串存储 + */ + static boolean propertyIsJsonStringStorage(PropertyMetadata metadata) { + return metadata + .getExpand(propertyStorageType) + .map(propertyStorageTypeJson::equals) + .orElse(false); + } + + /** + * 判断属性是否忽略存储 + * + * @param metadata 属性物模型 + * @return 属性是否忽略存储 + */ + static boolean propertyIsIgnoreStorage(PropertyMetadata metadata) { + return metadata + .getExpand(propertyStorageType) + .map(propertyStorageTypeIgnore::equals) + .orElse(false); + } + +} diff --git a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/service/data/StorageDeviceConfigMetadataSupplier.java b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/service/data/StorageDeviceConfigMetadataSupplier.java new file mode 100755 index 00000000..6a15553d --- /dev/null +++ b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/service/data/StorageDeviceConfigMetadataSupplier.java @@ -0,0 +1,94 @@ +package org.jetlinks.community.device.service.data; + +import lombok.AllArgsConstructor; +import org.jetlinks.core.Value; +import org.jetlinks.core.device.DeviceRegistry; +import org.jetlinks.core.metadata.*; +import org.jetlinks.core.metadata.types.ArrayType; +import org.jetlinks.core.metadata.types.EnumType; +import org.jetlinks.core.metadata.types.ObjectType; +import org.jetlinks.community.device.spi.DeviceConfigMetadataSupplier; +import org.jetlinks.community.things.data.ThingsDataRepositoryStrategies; +import org.jetlinks.community.things.data.ThingsDataRepositoryStrategy; +import org.springframework.stereotype.Component; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +@Component +@AllArgsConstructor +public class StorageDeviceConfigMetadataSupplier implements DeviceConfigMetadataSupplier { + private final DeviceRegistry registry; + + private final DeviceDataStorageProperties properties; + + private final ConfigMetadata objectConf = new DefaultConfigMetadata("存储配置", "") + .scope(DeviceConfigScope.product) + .add(StorageConstants.propertyStorageType, "存储方式", new EnumType() + .addElement(EnumType.Element.of("direct", "直接存储", "直接存储上报的数据")) + .addElement(EnumType.Element.of(StorageConstants.propertyStorageTypeIgnore, "不存储", "不存储此属性值")) + .addElement(EnumType.Element.of(StorageConstants.propertyStorageTypeJson, "JSON字符", "将数据序列话为JSON字符串进行存储")) + ); + + private final ConfigMetadata anotherConf = new DefaultConfigMetadata("存储配置", "") + .scope(DeviceConfigScope.product) + .add(StorageConstants.propertyStorageType, "存储方式", new EnumType() + .addElement(EnumType.Element.of("direct", "存储", "将上报的属性值保存到配置到存储策略中")) + .addElement(EnumType.Element.of(StorageConstants.propertyStorageTypeIgnore, "不存储", "不存储此属性值")) + ); + + + @Override + public Flux getDeviceConfigMetadata(String deviceId) { + return Flux.empty(); + } + + @Override + public Flux getDeviceConfigMetadataByProductId(String productId) { + return Flux.empty(); + } + + @Override + public Flux getProductConfigMetadata(String productId) { + return Flux.empty(); + } + + @Override + public Flux getProductFeatures(String productId) { + return registry + .getProduct(productId) + .flatMap(prod -> prod.getConfig(DeviceDataService.STORE_POLICY_CONFIG_KEY)) + .defaultIfEmpty(properties.getDefaultPolicy()) + .flatMap(this::getStoragePolicy) + .flatMapMany(strategy -> strategy + .opsForSave(ThingsDataRepositoryStrategy.OperationsContext.DEFAULT) + .getFeatures()); + } + + private Mono getStoragePolicy(String policy) { + return Mono.justOrEmpty(ThingsDataRepositoryStrategies.getStrategy(policy)); + } + + @Override + public Flux getMetadataExpandsConfig(String productId, + DeviceMetadataType metadataType, + String metadataId, + String typeId) { + if (metadataType == DeviceMetadataType.property) { + if ((ObjectType.ID.equals(typeId) || ArrayType.ID.equals(typeId))) { + return registry + .getProduct(productId) + .flatMap(prod -> prod + .getConfig(StorageConstants.storePolicyConfigKey) + .map(Value::asString)) + .defaultIfEmpty(properties.getDefaultPolicy()) + .filter(policy -> policy.startsWith("default-")) + .map(ignore -> objectConf) + .flux(); + } + return Flux.just(anotherConf); + } + + return Flux.empty(); + + } +} diff --git a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/spi/DeviceConfigMetadataSupplier.java b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/spi/DeviceConfigMetadataSupplier.java index 35c50974..6e650098 100644 --- a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/spi/DeviceConfigMetadataSupplier.java +++ b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/spi/DeviceConfigMetadataSupplier.java @@ -1,7 +1,9 @@ package org.jetlinks.community.device.spi; +import lombok.Generated; import org.jetlinks.core.metadata.ConfigMetadata; import org.jetlinks.core.metadata.DeviceMetadataType; +import org.jetlinks.core.metadata.Feature; import reactor.core.publisher.Flux; /** @@ -38,4 +40,13 @@ public interface DeviceConfigMetadataSupplier { String typeId) { return Flux.empty(); } + + + /** + * @see org.jetlinks.community.device.service.DeviceConfigMetadataManager#getProductFeatures(String) + */ + @Generated + default Flux getProductFeatures(String productId){ + return Flux.empty(); + } } diff --git a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/web/DeviceInstanceController.java b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/web/DeviceInstanceController.java index e4241e81..cfb33486 100644 --- a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/web/DeviceInstanceController.java +++ b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/web/DeviceInstanceController.java @@ -6,6 +6,7 @@ import io.swagger.v3.oas.annotations.tags.Tag; import lombok.Getter; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections4.CollectionUtils; import org.hswebframework.ezorm.rdb.exception.DuplicateKeyException; import org.hswebframework.ezorm.rdb.mapping.ReactiveRepository; import org.hswebframework.ezorm.rdb.mapping.defaults.SaveResult; @@ -22,6 +23,7 @@ import org.hswebframework.web.crud.web.reactive.ReactiveServiceCrudController; import org.hswebframework.web.exception.BusinessException; import org.hswebframework.web.exception.NotFoundException; import org.hswebframework.web.exception.ValidationException; +import org.hswebframework.web.i18n.LocaleUtils; import org.hswebframework.web.id.IDGenerator; import org.jetlinks.community.device.entity.*; import org.jetlinks.community.device.enums.DeviceState; @@ -34,10 +36,16 @@ import org.jetlinks.community.device.service.LocalDeviceProductService; import org.jetlinks.community.device.service.data.DeviceDataService; import org.jetlinks.community.device.web.excel.DeviceExcelInfo; import org.jetlinks.community.device.web.excel.DeviceWrapper; +import org.jetlinks.community.device.web.excel.PropertyMetadataExcelInfo; +import org.jetlinks.community.device.web.excel.PropertyMetadataWrapper; import org.jetlinks.community.device.web.request.AggRequest; import org.jetlinks.community.io.excel.ImportExportService; import org.jetlinks.community.io.utils.FileUtils; +import org.jetlinks.community.relation.RelationObjectProvider; +import org.jetlinks.community.relation.service.RelationService; +import org.jetlinks.community.relation.service.request.SaveRelationRequest; import org.jetlinks.community.timeseries.query.AggregationData; +import org.jetlinks.community.web.response.ValidationResult; import org.jetlinks.core.Values; import org.jetlinks.core.device.*; import org.jetlinks.core.device.manager.DeviceBindHolder; @@ -48,6 +56,7 @@ import org.jetlinks.core.message.Message; import org.jetlinks.core.message.MessageType; import org.jetlinks.core.message.RepayableDeviceMessage; import org.jetlinks.core.metadata.*; +import org.jetlinks.supports.official.JetLinksDeviceMetadataCodec; import org.springframework.core.io.buffer.DataBufferFactory; import org.springframework.core.io.buffer.DefaultDataBufferFactory; import org.springframework.data.util.Lazy; @@ -73,7 +82,7 @@ import java.util.Map; import java.util.function.Function; import java.util.stream.Collectors; -import static org.hswebframework.reactor.excel.ReactorExcel.*; +import static org.hswebframework.reactor.excel.ReactorExcel.read; @RestController @RequestMapping({"/device-instance", "/device/instance"}) @@ -99,6 +108,8 @@ public class DeviceInstanceController implements private final DeviceConfigMetadataManager metadataManager; + private final RelationService relationService; + @SuppressWarnings("all") public DeviceInstanceController(LocalDeviceInstanceService service, DeviceRegistry registry, @@ -106,7 +117,8 @@ public class DeviceInstanceController implements ImportExportService importExportService, ReactiveRepository tagRepository, DeviceDataService deviceDataService, - DeviceConfigMetadataManager metadataManager) { + DeviceConfigMetadataManager metadataManager, + RelationService relationService) { this.service = service; this.registry = registry; this.productService = productService; @@ -114,6 +126,7 @@ public class DeviceInstanceController implements this.tagRepository = tagRepository; this.deviceDataService = deviceDataService; this.metadataManager = metadataManager; + this.relationService = relationService; } @@ -125,6 +138,17 @@ public class DeviceInstanceController implements return service.getDeviceDetail(id); } + //读取设备属性 + @PostMapping("/{deviceId:.+}/properties/_read") + @QueryAction + @Operation(summary = "发送读取属性指令到设备", description = "请求示例: [\"属性ID\"]") + public Mono readProperties(@PathVariable @Parameter(description = "设备ID") String deviceId, + @RequestBody Mono> properties) { + return properties.flatMap(props -> service.readProperties(deviceId, props)); + } + + + //获取设备详情 @GetMapping("/{id:.+}/config-metadata") @QueryAction @@ -314,12 +338,22 @@ public class DeviceInstanceController implements //查询设备日志 @GetMapping("/{deviceId:.+}/logs") @QueryAction - @QueryOperation(summary = "查询设备日志数据") + @QueryOperation(summary = "(GET)查询设备日志数据") public Mono> queryDeviceLog(@PathVariable @Parameter(description = "设备ID") String deviceId, @Parameter(hidden = true) QueryParamEntity entity) { return deviceDataService.queryDeviceMessageLog(deviceId, entity); } + //查询设备日志 + @PostMapping("/{deviceId:.+}/logs") + @QueryAction + @Operation(summary = "(POST)查询设备日志数据") + public Mono> queryDeviceLog(@PathVariable @Parameter(description = "设备ID") String deviceId, + @RequestBody @Parameter(hidden = true) Mono queryParam) { + return queryParam.flatMap(param -> deviceDataService.queryDeviceMessageLog(deviceId, param)); + } + + //删除标签 @DeleteMapping("/{deviceId}/tag/{tagId:.+}") @SaveAction @@ -366,8 +400,7 @@ public class DeviceInstanceController implements * 批量激活设备 * * @param idList ID列表 - * @return 被注销的数量 - * @since 1.1 + * @return 被激活的数量 */ @PutMapping("/batch/_deploy") @SaveAction @@ -844,4 +877,92 @@ public class DeviceInstanceController implements } + @GetMapping("/{id:.+}/exists") + @QueryAction + @Operation(summary = "验证设备ID是否存在") + public Mono deviceIdValidate(@PathVariable @Parameter(description = "设备ID") String id) { + return service.findById(id) + .hasElement(); + } + + @GetMapping("/id/_validate") + @QueryAction + @Operation(summary = "验证设备ID是否合法") + public Mono deviceIdValidate2(@RequestParam @Parameter(description = "设备ID") String id) { + return LocaleUtils.currentReactive() + .flatMap(locale -> { + DeviceInstanceEntity entity = new DeviceInstanceEntity(); + entity.setId(id); + entity.validateId(); + + return service.findById(id) + .map(device -> ValidationResult.error( + LocaleUtils.resolveMessage("error.device_ID_already_exists", locale))) + .defaultIfEmpty(ValidationResult.success()); + }) + .onErrorResume(ValidationException.class, e -> Mono.just(e.getI18nCode()) + .map(ValidationResult::error)); + } + + + //解析文件为属性物模型 + @PostMapping(value = "/{productId}/property-metadata/import") + @SaveAction + @Operation(summary = "解析文件为属性物模型") + public Mono importPropertyMetadata(@PathVariable @Parameter(description = "产品ID") String productId, + @RequestParam @Parameter(description = "文件地址,支持csv,xlsx文件格式") String fileUrl) { + return metadataManager + .getMetadataExpandsConfig(productId, DeviceMetadataType.property, "*", "*", DeviceConfigScope.device) + .collectList() + .map(PropertyMetadataWrapper::new) + //解析数据并转为物模型 + .flatMap(wrapper -> importExportService + .getInputStream(fileUrl) + .flatMapMany(inputStream -> read(inputStream, FileUtils.getExtension(fileUrl), wrapper)) + .map(PropertyMetadataExcelInfo::toMetadata) + .collectList()) + .filter(CollectionUtils::isNotEmpty) + .map(list -> { + SimpleDeviceMetadata metadata = new SimpleDeviceMetadata(); + list.forEach(metadata::addProperty); + return JetLinksDeviceMetadataCodec.getInstance().doEncode(metadata); + }); + } + + //获取物模型属性导入模块 + @GetMapping("/{deviceId}/property-metadata/template.{format}") + @QueryAction + @Operation(summary = "下载设备物模型属性导入模块") + public Mono downloadMetadataExportTemplate(@PathVariable @Parameter(description = "设备ID") String deviceId, + ServerHttpResponse response, + @PathVariable @Parameter(description = "文件格式,支持csv,xlsx") String format) throws IOException { + response.getHeaders().set(HttpHeaders.CONTENT_DISPOSITION, + "attachment; filename=".concat(URLEncoder.encode("物模型导入模块." + format, StandardCharsets.UTF_8 + .displayName()))); + + return metadataManager + .getMetadataExpandsConfig(deviceId, DeviceMetadataType.property, "*", "*", DeviceConfigScope.device) + .collectList() + .map(PropertyMetadataExcelInfo::getTemplateHeaderMapping) + .flatMapMany(headers -> + ReactorExcel.writer(format) + .headers(headers) + .converter(DeviceExcelInfo::toMap) + .writeBuffer(Flux.empty())) + .doOnError(err -> log.error(err.getMessage(), err)) + .map(bufferFactory::wrap) + .as(response::writeWith) + ; + } + + + @PatchMapping("/{deviceId}/relations") + @Operation(summary = "保存设备的关系信息") + @SaveAction + public Mono saveRelation(@PathVariable String deviceId, + @RequestBody Flux requestFlux) { + return relationService.saveRelated(RelationObjectProvider.TYPE_DEVICE, deviceId, requestFlux); + } + + } diff --git a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/web/DeviceProductController.java b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/web/DeviceProductController.java index 6c4dc4c9..2869b1cb 100644 --- a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/web/DeviceProductController.java +++ b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/web/DeviceProductController.java @@ -7,34 +7,51 @@ import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections4.CollectionUtils; +import org.hswebframework.reactor.excel.ReactorExcel; import org.hswebframework.web.authorization.annotation.QueryAction; import org.hswebframework.web.authorization.annotation.Resource; import org.hswebframework.web.authorization.annotation.SaveAction; import org.hswebframework.web.crud.web.reactive.ReactiveServiceCrudController; +import org.hswebframework.web.exception.ValidationException; +import org.hswebframework.web.i18n.LocaleUtils; import org.jetlinks.community.device.entity.DeviceProductEntity; import org.jetlinks.community.device.service.DeviceConfigMetadataManager; import org.jetlinks.community.device.service.LocalDeviceProductService; import org.jetlinks.community.device.service.data.DeviceDataService; +import org.jetlinks.community.device.web.excel.PropertyMetadataExcelInfo; +import org.jetlinks.community.device.web.excel.PropertyMetadataWrapper; import org.jetlinks.community.device.web.request.AggRequest; +import org.jetlinks.community.io.excel.ImportExportService; +import org.jetlinks.community.io.utils.FileUtils; import org.jetlinks.community.things.data.ThingsDataRepositoryStrategy; import org.jetlinks.community.timeseries.query.AggregationData; -import org.jetlinks.core.metadata.ConfigMetadata; -import org.jetlinks.core.metadata.DeviceConfigScope; -import org.jetlinks.core.metadata.DeviceMetadataCodec; -import org.jetlinks.core.metadata.DeviceMetadataType; +import org.jetlinks.community.web.response.ValidationResult; +import org.jetlinks.core.metadata.*; import org.jetlinks.supports.official.JetLinksDeviceMetadataCodec; import org.springframework.beans.factory.ObjectProvider; +import org.springframework.core.io.buffer.DataBufferFactory; +import org.springframework.core.io.buffer.DefaultDataBufferFactory; +import org.springframework.http.HttpHeaders; +import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.web.bind.annotation.*; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import java.io.IOException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; import java.util.List; import java.util.Map; +import static org.hswebframework.reactor.excel.ReactorExcel.read; + @RestController @RequestMapping({"/device-product","/device/product"}) @Resource(id = "device-product", name = "设备产品") @Tag(name = "设备产品接口") +@Slf4j public class DeviceProductController implements ReactiveServiceCrudController { private final LocalDeviceProductService productService; @@ -48,16 +65,23 @@ public class DeviceProductController implements ReactiveServiceCrudController policies, DeviceDataService deviceDataService, DeviceConfigMetadataManager configMetadataManager, - ObjectProvider metadataCodecs) { + ObjectProvider metadataCodecs, + ImportExportService importExportService) { this.productService = productService; this.policies = policies; this.deviceDataService = deviceDataService; this.configMetadataManager = configMetadataManager; this.metadataCodecs = metadataCodecs; + this.importExportService = importExportService; } @Override @@ -177,4 +201,82 @@ public class DeviceProductController implements ReactiveServiceCrudController deviceIdValidate(@PathVariable @Parameter(description = "产品ID") String id) { + return productService.findById(id) + .hasElement(); + } + + @GetMapping("/id/_validate") + @QueryAction + @Operation(summary = "验证产品ID是否合法") + public Mono deviceIdValidate2(@RequestParam @Parameter(description = "产品ID") String id) { + return LocaleUtils.currentReactive() + .flatMap(locale -> { + DeviceProductEntity entity = new DeviceProductEntity(); + entity.setId(id); + entity.validateId(); + + return productService.findById(id) + .map(product -> ValidationResult.error( + LocaleUtils.resolveMessage("error.product_ID_already_exists", locale))) + .defaultIfEmpty(ValidationResult.success()); + }) + .onErrorResume(ValidationException.class, e -> Mono.just(e.getI18nCode()) + .map(ValidationResult::error)); + } + + + //获取产品物模型属性导入模块 + @GetMapping("/{productId}/property-metadata/template.{format}") + @QueryAction + @Operation(summary = "下载产品物模型属性导入模块") + public Mono downloadExportPropertyMetadataTemplate(@PathVariable @Parameter(description = "产品ID") String productId, + ServerHttpResponse response, + @PathVariable @Parameter(description = "文件格式,支持csv,xlsx") String format) throws IOException { + response.getHeaders().set(HttpHeaders.CONTENT_DISPOSITION, + "attachment; filename=".concat(URLEncoder.encode("物模型导入模块." + format, StandardCharsets.UTF_8 + .displayName()))); + + return configMetadataManager + .getMetadataExpandsConfig(productId, DeviceMetadataType.property, "*", "*", DeviceConfigScope.product) + .collectList() + .map(PropertyMetadataExcelInfo::getTemplateHeaderMapping) + .flatMapMany(headers -> ReactorExcel + .writer(format) + .headers(headers) + .converter(PropertyMetadataExcelInfo::toMap) + .writeBuffer(PropertyMetadataExcelInfo.getTemplateContentMapping())) + .doOnError(err -> log.error(err.getMessage(), err)) + .map(bufferFactory::wrap) + .as(response::writeWith) + ; + } + + //解析文件为属性物模型 + @PostMapping(value = "/{productId}/property-metadata/import") + @SaveAction + @Operation(summary = "解析文件为属性物模型") + public Mono importPropertyMetadata(@PathVariable @Parameter(description = "产品ID") String productId, + @RequestParam @Parameter(description = "文件地址,支持csv,xlsx文件格式") String fileUrl) { + return configMetadataManager + .getMetadataExpandsConfig(productId, DeviceMetadataType.property, "*", "*", DeviceConfigScope.product) + .collectList() + .map(PropertyMetadataWrapper::new) + //解析数据并转为物模型 + .flatMap(wrapper -> importExportService + .getInputStream(fileUrl) + .flatMapMany(inputStream -> read(inputStream, FileUtils.getExtension(fileUrl), wrapper)) + .map(PropertyMetadataExcelInfo::toMetadata) + .collectList()) + .filter(CollectionUtils::isNotEmpty) + .map(list -> { + SimpleDeviceMetadata metadata = new SimpleDeviceMetadata(); + list.forEach(metadata::addProperty); + return JetLinksDeviceMetadataCodec.getInstance().doEncode(metadata); + }); + } + } diff --git a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/web/excel/PropertyMetadataExcelInfo.java b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/web/excel/PropertyMetadataExcelInfo.java new file mode 100644 index 00000000..9ea68ce8 --- /dev/null +++ b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/web/excel/PropertyMetadataExcelInfo.java @@ -0,0 +1,321 @@ +package org.jetlinks.community.device.web.excel; + +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; +import com.google.common.collect.Lists; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.hswebframework.reactor.excel.CellDataType; +import org.hswebframework.reactor.excel.ExcelHeader; +import org.hswebframework.web.bean.FastBeanCopier; +import org.hswebframework.web.dict.EnumDict; +import org.hswebframework.web.exception.BusinessException; +import org.hswebframework.web.exception.ValidationException; +import org.hswebframework.web.validator.ValidatorUtils; +import org.jetlinks.core.metadata.*; +import org.jetlinks.core.metadata.types.*; +import org.jetlinks.core.metadata.unit.ValueUnit; +import org.jetlinks.core.metadata.unit.ValueUnits; +import org.jetlinks.supports.official.JetLinksDataTypeCodecs; +import org.springframework.util.CollectionUtils; +import reactor.core.publisher.Flux; + +import javax.validation.constraints.NotBlank; +import java.util.*; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +@Getter +@Setter +@Slf4j +public class PropertyMetadataExcelInfo { + + @NotBlank(message = "属性ID不能为空") + private String property; + + @NotBlank(message = "属性名称不能为空") + private String name; + + private String valueType; + + private Map expands; + //数据类型 + @NotBlank(message = "数据类型不能为空") + private String dataType; + //单位 + private String unit; + //精度 + private String scale; + //来源 + @NotBlank(message = "来源不能为空") + private String source; + + private String description; + + private String storageType; + + private long rowNumber; + //读写类型 + private List type; + + /** + * 单位 + */ + private static final List idList = ValueUnits.getAllUnit(); + + /** + * 所有数据类型 + */ + private static final List DATA_TYPES = Lists.newArrayList(ArrayType.ID, BooleanType.ID, + DateTimeType.ID, DoubleType.ID, EnumType.ID, FloatType.ID, IntType.ID, LongType.ID, + ObjectType.ID, StringType.ID, GeoType.ID, FileType.ID, PasswordType.ID, GeoShapeType.ID); + + private static final List OBJECT_NOT_HAVE = Lists.newArrayList(DateTimeType.ID, FileType.ID, ObjectType.ID, PasswordType.ID); + /** + * 简单模板支持类型 + */ + private static final List SIMPLE = Lists.newArrayList(IntType.ID, FloatType.ID, DoubleType.ID, LongType.ID); + + public void with(String key, Object value) { + FastBeanCopier.copy(Collections.singletonMap(key, value), this); + } + + public PropertyMetadata toMetadata() { + SimplePropertyMetadata metadata = new SimplePropertyMetadata(); + try { + ValidatorUtils.tryValidate(this); + if (CollectionUtils.isEmpty(type) || type.size() == 1 && StringUtils.isEmpty(type.get(0))) { + throw new ValidationException("读写类型不能为空"); + } + metadata.setId(property); + metadata.setName(name); + metadata.setValueType(parseDataType()); + metadata.setExpands(parseExpands()); + metadata.setDescription(description); + return metadata; + } catch (Throwable e) { + throw new BusinessException("第" + this.getRowNumber() + "行错误:" + e.getMessage()); + } + } + + public static List getTemplateHeaderMapping(List configMetadataList) { + List arr = new ArrayList<>(Arrays.asList( + new ExcelHeader("property", "属性ID", CellDataType.STRING), + new ExcelHeader("name", "属性名称", CellDataType.STRING), + new ExcelHeader("dataType", "数据类型", CellDataType.STRING), + new ExcelHeader("unit", "单位", CellDataType.STRING), + new ExcelHeader("scale", "精度", CellDataType.STRING), + new ExcelHeader("valueType", "数据类型配置", CellDataType.STRING), + new ExcelHeader("source", "来源", CellDataType.STRING), + new ExcelHeader("description", "属性说明", CellDataType.STRING), + new ExcelHeader("type", "读写类型", CellDataType.STRING) + )); + + Set expandsKeys = new HashSet<>(); + for (ConfigMetadata configMetadata : configMetadataList) { + for (ConfigPropertyMetadata property : configMetadata.getProperties()) { + String header = property.getName(); + if (expandsKeys.contains(header)) { + header = configMetadata.getName() + "-" + header; + } + arr.add(new ExcelHeader("expands." + property.getProperty(), header, CellDataType.STRING)); + expandsKeys.add(property.getName()); + } + } + + return arr; + } + + protected DataType parseDataType() { + JSONObject dataTypeJson = new JSONObject(); + //默认先使用json格式的数据解析物模型,没有json则使用简单模板,只支持int long double float + if (!StringUtils.isEmpty(this.valueType)) { + dataTypeJson = JSON.parseObject(this.valueType); + this.dataType = dataTypeJson.getString("type"); + } else { + dataTypeJson.put("type", this.dataType); + dataTypeJson.put("unit", this.unit); + dataTypeJson.put("scale", this.scale); + } + DataType dataType = Optional.ofNullable(this.dataType) + .map(DataTypes::lookup) + .map(Supplier::get) + .orElseThrow(() -> new BusinessException("error.unknown_data_type" ,500, this, getDataType())); + JSONObject finalDataTypeJson = dataTypeJson; + JetLinksDataTypeCodecs + .getCodec(dataType.getId()) + .ifPresent(codec -> codec.decode(dataType, finalDataTypeJson)); + return dataType; + } + + protected Map parseExpands() { + Map map = new HashMap<>(4); + map.put("source", PropertySource.getValue(source)); + map.put("storageType", PropertyStorage.getValue(storageType)); + map.put("tags", ""); + map.put("type", type.stream().map(PropertyType::getValue).collect(Collectors.toList())); + return map; + } + + public static Flux getTemplateContentMapping() { + return Flux.fromIterable(DATA_TYPES) + .flatMap(dt -> { + PropertyMetadataExcelInfo excelInfo = new PropertyMetadataExcelInfo(); + DataType dataType = DataTypes.lookup(dt).get(); + excelInfo.setProperty(dataType.getType() + "_id"); + excelInfo.setName(dataType.getType() + "类型属性示例"); + excelInfo.setDataType(dataType.getId()); + excelInfo.setUnit(""); + excelInfo.setScale(""); + Random random = new Random(); + excelInfo.setStorageType(random.nextBoolean() ? "direct" : "ignore"); + excelInfo.setSource(random.nextInt(2) == 1 ? "manual" : random.nextInt(2) < 1 ? "device" : "rule"); + excelInfo.setDescription(excelInfo.getName() + "的说明"); + if (SIMPLE.contains(dt)) { + excelInfo.setUnit(idList.get(0).getId()); + excelInfo.setDataType(dt); + excelInfo.setScale(String.valueOf(random.nextInt(2))); + excelInfo.setDescription(excelInfo.getName() + "的说明,优先使用json数据类型配置,没有则使用简单模板,仅支持int double float long四种"); + } + Map valueType = JetLinksDataTypeCodecs.encode(buildValueType(dataType, random)).orElse(Collections.emptyMap()); + excelInfo.setValueType(JSONObject.toJSONString(valueType)); + excelInfo.setExpands(Collections.singletonMap("storageType", excelInfo.getStorageType())); + excelInfo.setType(Arrays.asList("read", "write", "report")); + return Flux.just(excelInfo); + }).doOnError(e -> { + log.error("填充模板异常:", e); + }); + } + + private static DataType buildValueType(DataType dataType, Random random) { + switch (dataType.getId()) { + case ArrayType.ID: + ((ArrayType) dataType).elementType(new IntType().unit(idList.get(random.nextInt(idList.size() - 1)))); + break; + case DoubleType.ID: + ((DoubleType) dataType).scale(random.nextInt(10)).unit(idList.get(random.nextInt(idList.size() - 1))); + break; + case FloatType.ID: + ((FloatType) dataType).scale(random.nextInt(10)).unit(idList.get(random.nextInt(idList.size() - 1))); + break; + case EnumType.ID: + dataType = new EnumType(); + for (int i = 0; i < random.nextInt(5); i++) { + ((EnumType) dataType).addElement(EnumType.Element.of("枚举值" + i, String.valueOf(i), "枚举说明" + i)); + } + break; + case IntType.ID: + dataType = new IntType(); + ((IntType) dataType).unit(idList.get(random.nextInt(idList.size() - 1))); + break; + case LongType.ID: + ((LongType) dataType).unit(idList.get(random.nextInt(idList.size() - 1))); + break; + case FileType.ID: + ((FileType) dataType).bodyType(FileType.BodyType.url); + break; + case ObjectType.ID: + int i = 1; + List objectParam = Lists.newCopyOnWriteArrayList(DATA_TYPES); + dataType = new ObjectType(); + objectParam.removeAll(OBJECT_NOT_HAVE); + for (String id : objectParam) { + ((ObjectType) dataType).addProperty("param" + i, "参数" + i, buildValueType(DataTypes.lookup(id).get(), random)); + i++; + } + break; + case StringType.ID: + ((StringType) dataType).expand("maxLength", random.nextInt(2000)); + break; + case PasswordType.ID: + ((PasswordType) dataType).expand("maxLength", random.nextInt(30)); + break; + default: + break; + } + return dataType; + } + + + public Map toMap() { + setSource(PropertySource.getText(source)); + setStorageType(PropertyStorage.getText(storageType)); + setExpands(Collections.singletonMap("storageType", storageType)); + Map map = FastBeanCopier.copy(this, new HashMap<>(8)); + map.put("type", type.stream() + .map(PropertyType::getText) + .collect(Collectors.joining(","))); + return map; + } + + @AllArgsConstructor + @Getter + private enum PropertySource implements EnumDict { + device("设备"), + manual("手动"), + rule("规则"); + + private String text; + + @Override + public String getValue() { + return name(); + } + + public static String getText(String value) { + return EnumDict.findByValue(PropertySource.class, value).map(PropertySource::getText).orElse(""); + } + + public static String getValue(String text) { + return EnumDict.findByText(PropertySource.class, text).map(PropertySource::getValue).orElse(""); + } + } + + @AllArgsConstructor + @Getter + private enum PropertyType implements EnumDict { + read("读"), + write("写"), + report("上报"); + + private String text; + + @Override + public String getValue() { + return name(); + } + + public static String getText(String value) { + return EnumDict.findByValue(PropertyType.class, value).map(PropertyType::getText).orElse(""); + } + + public static String getValue(String text) { + return EnumDict.findByText(PropertyType.class, text).map(PropertyType::getValue).orElse(""); + } + } + + @AllArgsConstructor + @Getter + private enum PropertyStorage implements EnumDict { + direct("存储"), + ignore("不存储"); + + private String text; + + @Override + public String getValue() { + return name(); + } + + public static String getText(String value) { + return EnumDict.findByValue(PropertyStorage.class, value).map(PropertyStorage::getText).orElse(""); + } + + public static String getValue(String text) { + return EnumDict.findByText(PropertyStorage.class, text).map(PropertyStorage::getValue).orElse(""); + } + } +} diff --git a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/web/excel/PropertyMetadataWrapper.java b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/web/excel/PropertyMetadataWrapper.java new file mode 100644 index 00000000..c3812006 --- /dev/null +++ b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/web/excel/PropertyMetadataWrapper.java @@ -0,0 +1,56 @@ +package org.jetlinks.community.device.web.excel; + +import org.hswebframework.reactor.excel.Cell; +import org.hswebframework.reactor.excel.converter.RowWrapper; +import org.jetlinks.core.metadata.ConfigMetadata; +import org.jetlinks.core.metadata.ConfigPropertyMetadata; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class PropertyMetadataWrapper extends RowWrapper { + + private final Map propertyMapping = new HashMap<>(); + + public PropertyMetadataWrapper(List expands) { + propertyMapping.put("属性ID", "property"); + propertyMapping.put("属性名称", "name"); + propertyMapping.put("数据类型", "dataType"); + propertyMapping.put("单位", "unit"); + propertyMapping.put("精度", "scale"); + propertyMapping.put("数据类型配置", "valueType"); + propertyMapping.put("来源", "source"); + propertyMapping.put("属性说明", "description"); + propertyMapping.put("读写类型", "type"); + for (ConfigMetadata expand : expands) { + for (ConfigPropertyMetadata property : expand.getProperties()) { + propertyMapping.put(expand.getName() + "-" + property.getName(), property.getProperty()); + propertyMapping.put(property.getName(), property.getProperty()); + } + } + } + + @Override + protected PropertyMetadataExcelInfo newInstance() { + return new PropertyMetadataExcelInfo(); + } + + @Override + protected PropertyMetadataExcelInfo wrap(PropertyMetadataExcelInfo instance, Cell header, Cell dataCell) { + String headerText = header.valueAsText().orElse("null"); + Object value = dataCell.valueAsText().orElse(""); + if (propertyMapping.containsKey(headerText)) { + instance.with(propertyMapping.get(headerText), propertyTypeToLowerCase(headerText, value)); + } + instance.setRowNumber(dataCell.getRowIndex() + 1); + return instance; + } + + private Object propertyTypeToLowerCase(String headerText, Object value) { + if ("类型".equals(headerText)) { + return value.toString().toLowerCase(); + } + return value; + } +} diff --git a/jetlinks-manager/device-manager/src/main/resources/i18n/device-manager/messages_zh.properties b/jetlinks-manager/device-manager/src/main/resources/i18n/device-manager/messages_zh.properties index 0f79848c..109c9401 100644 --- a/jetlinks-manager/device-manager/src/main/resources/i18n/device-manager/messages_zh.properties +++ b/jetlinks-manager/device-manager/src/main/resources/i18n/device-manager/messages_zh.properties @@ -28,8 +28,8 @@ org.jetlinks.community.device.enums.TaskState.sendError=\u53D1\u9001\u5931\u8D25 org.jetlinks.community.device.enums.FirmwareUpgradeState.waiting=\u7B49\u5F85\u5347\u7EA7 org.jetlinks.community.device.enums.FirmwareUpgradeState.processing=\u5347\u7EA7\u4E2D org.jetlinks.community.device.enums.FirmwareUpgradeState.failed=\u5347\u7EA7\u5931\u8D25 -org.jetlinks.community.device.enums.FirmwareUpgradeState.success=\u5347\u7ea7\u5b8c\u6210 -org.jetlinks.community.device.enums.FirmwareUpgradeState.canceled=\u5df2\u505c\u6b62 +org.jetlinks.community.device.enums.FirmwareUpgradeState.success=\u5347\u7EA7\u5B8C\u6210 +org.jetlinks.community.device.enums.FirmwareUpgradeState.canceled=\u5DF2\u505C\u6B62 #message @@ -69,7 +69,7 @@ error.product_does_not_exist=\u4EA7\u54C1{0}\u4E0D\u5B58\u5728 error.reply_is_error=\u8BBE\u5907\u54CD\u5E94\u9519\u8BEF error.cannot_deleted_because_device_is_associated_with_it=\u5B58\u5728\u5173\u8054\u8BBE\u5907,\u65E0\u6CD5\u5220\u9664! error.unable_to_load_protocol=\u65E0\u6CD5\u52A0\u8F7D\u534F\u8BAE:{0} -error.unable_to_load_protocol_by_access_id=\u627e\u4e0d\u5230\u5f53\u524d\u63a5\u5165\u65b9\u5f0f\u4e2d\u7684\u534f\u8bae:{0} +error.unable_to_load_protocol_by_access_id=\u627E\u4E0D\u5230\u5F53\u524D\u63A5\u5165\u65B9\u5F0F\u4E2D\u7684\u534F\u8BAE:{0} error.unknown_data_type=\u672A\u77E5\u7684\u6570\u636E\u7C7B\u578B:{0} error.unrecognized_message=\u65E0\u6CD5\u8BC6\u522B\u7684\u6D88\u606F error.device_ID_already_exists=\u8BBE\u5907ID\u5DF2\u5B58\u5728 @@ -81,7 +81,7 @@ error.product_ID_cannot_be_empty=\u4EA7\u54C1ID\u4E0D\u80FD\u4E3A\u7A7A error.message_format=\u6D88\u606F\u683C\u5F0F\u9519\u8BEF error.gateway_cannot_be_bound_as_a_child_device=\u4E0D\u80FD\u7ED1\u5B9A\u7F51\u5173\u81EA\u8EAB\u4E3A\u5B50\u8BBE\u5907 error.device_not_found_or_not_activated=\u8BBE\u5907\u4E0D\u5B58\u5728\u6216\u672A\u542F\u7528 -error.device_category_has_bean_use_by_product=\u8BBE\u5907\u5206\u7C7B\u5DF2\u7ECF\u88AB\u5176\u4ED6\u4EA7\u54C1\u4F7F\u7528 +error.device_category_has_bean_use_by_product=\u4EA7\u54C1\u5206\u7C7B\u5DF2\u7ECF\u88AB\u5176\u4ED6\u4EA7\u54C1\u4F7F\u7528 error.storage_policy_unsupported_operation=\u5B58\u50A8\u7B56\u7565\u4E0D\u652F\u6301\u6B64\u64CD\u4F5C error.message_protocol_can_not_be_empty=\u6D88\u606F\u534F\u8BAE\u4E0D\u80FD\u4E3A\u7A7A error.please_select_the_access_mode_first=\u8BF7\u5148\u9009\u62E9\u63A5\u5165\u65B9\u5F0F \ No newline at end of file diff --git a/jetlinks-manager/network-manager/src/main/java/org/jetlinks/community/network/manager/service/NetworkEntityEventHandler.java b/jetlinks-manager/network-manager/src/main/java/org/jetlinks/community/network/manager/service/NetworkEntityEventHandler.java new file mode 100644 index 00000000..9b39cd88 --- /dev/null +++ b/jetlinks-manager/network-manager/src/main/java/org/jetlinks/community/network/manager/service/NetworkEntityEventHandler.java @@ -0,0 +1,113 @@ +package org.jetlinks.community.network.manager.service; + +import lombok.AllArgsConstructor; +import org.hswebframework.web.crud.events.EntityBeforeDeleteEvent; +import org.hswebframework.web.crud.events.EntityCreatedEvent; +import org.hswebframework.web.crud.events.EntityModifyEvent; +import org.hswebframework.web.crud.events.EntitySavedEvent; +import org.hswebframework.web.exception.BusinessException; +import org.jetlinks.community.network.NetworkManager; +import org.jetlinks.community.network.NetworkProperties; +import org.jetlinks.community.network.manager.entity.CertificateEntity; +import org.jetlinks.community.network.manager.entity.NetworkConfigEntity; +import org.jetlinks.community.network.manager.enums.NetworkConfigState; +import org.jetlinks.community.reference.DataReferenceManager; +import org.springframework.context.event.EventListener; +import org.springframework.stereotype.Component; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.util.Collection; + +@Component +@AllArgsConstructor +public class NetworkEntityEventHandler { + + private final NetworkConfigService networkService; + + private final DataReferenceManager referenceManager; + + private final NetworkManager networkManager; + + //禁止删除已有网络组件使用的证书 + @EventListener + public void handleCertificateDelete(EntityBeforeDeleteEvent event) { + event.async( + Flux.fromIterable(event.getEntity()) + .flatMap(e -> networkService + .createQuery() + // FIXME: 2021/9/13 由于网络组件没有直接记录证书,还有更好的处理办法? + .$like$(NetworkConfigEntity::getConfiguration, e.getId()) + .or() + .$like$(NetworkConfigEntity::getCluster, e.getId()) + .count() + .doOnNext(i -> { + if (i > 0) { + throw new BusinessException("error.certificate_has_bean_use_by_network"); + } + }) + ) + ); + } + + //禁止删除已有网关使用的网络组件 + @EventListener + public void handleNetworkDelete(EntityBeforeDeleteEvent event) { + event.async( + Flux.fromIterable(event.getEntity()) + .flatMap(e -> referenceManager.assertNotReferenced(DataReferenceManager.TYPE_NETWORK, e.getId())) + ); + + } + + + @EventListener + public void handleNetworkCreated(EntityCreatedEvent event) { + event.async( + Flux.fromIterable(event.getEntity()) + .flatMapIterable(NetworkConfigEntity::toNetworkPropertiesList) + .flatMap(this::networkConfigValidate) + .then(handleEvent(event.getEntity())) + ); + } + + @EventListener + public void handleNetworkSaved(EntitySavedEvent event) { + event.async( + Flux.fromIterable(event.getEntity()) + .filter(conf -> conf.getConfiguration() != null || conf.getCluster() != null) + .flatMapIterable(NetworkConfigEntity::toNetworkPropertiesList) + .flatMap(this::networkConfigValidate) + .then(handleEvent(event.getEntity())) + ); + } + + @EventListener + public void handleNetworkModify(EntityModifyEvent event) { + event.async( + Flux.fromIterable(event.getAfter()) + .filter(conf -> conf.getConfiguration() != null || conf.getCluster() != null) + .flatMapIterable(NetworkConfigEntity::toNetworkPropertiesList) + .flatMap(this::networkConfigValidate) + .then(handleEvent(event.getAfter())) + ); + } + + + //网络组件配置验证 + private Mono networkConfigValidate(NetworkProperties properties) { + return Mono.justOrEmpty(networkManager.getProvider(properties.getType())) + .flatMap(networkProvider -> networkProvider.createConfig(properties)) + .then(); + } + + private Mono handleEvent(Collection entities) { + return Flux + .fromIterable(entities) + .filter(conf -> conf.getState() == NetworkConfigState.enabled) + .flatMap(conf -> networkManager.reload(conf.lookupNetworkType(), conf.getId())) + .then(); + } + + +} diff --git a/jetlinks-manager/network-manager/src/main/java/org/jetlinks/community/network/manager/service/ProtocolDataReferenceProvider.java b/jetlinks-manager/network-manager/src/main/java/org/jetlinks/community/network/manager/service/ProtocolDataReferenceProvider.java new file mode 100644 index 00000000..fce0781d --- /dev/null +++ b/jetlinks-manager/network-manager/src/main/java/org/jetlinks/community/network/manager/service/ProtocolDataReferenceProvider.java @@ -0,0 +1,47 @@ +package org.jetlinks.community.network.manager.service; + +import lombok.AllArgsConstructor; +import org.jetlinks.community.network.manager.entity.DeviceGatewayEntity; +import org.jetlinks.community.reference.DataReferenceInfo; +import org.jetlinks.community.reference.DataReferenceManager; +import org.jetlinks.community.reference.DataReferenceProvider; +import org.springframework.stereotype.Component; +import reactor.core.publisher.Flux; + +/** + * 消息协议的数据引用提供商. + * + * 返回被设备接入网关使用的消息协议 + * + * @author zhangji 2022/4/12 + */ +@Component +@AllArgsConstructor +public class ProtocolDataReferenceProvider implements DataReferenceProvider { + private final DeviceGatewayService deviceGatewayService; + + @Override + public String getId() { + return DataReferenceManager.TYPE_PROTOCOL; + } + + @Override + public Flux getReference(String protocolId) { + return deviceGatewayService + .createQuery() + .where() + .is(DeviceGatewayEntity::getProtocol, protocolId) + .fetch() + .map(e -> DataReferenceInfo.of(e.getId(),DataReferenceManager.TYPE_PROTOCOL, e.getProtocol(), e.getName())); + } + + @Override + public Flux getReferences() { + return deviceGatewayService + .createQuery() + .where() + .notNull(DeviceGatewayEntity::getChannelId) + .fetch() + .map(e -> DataReferenceInfo.of(e.getId(),DataReferenceManager.TYPE_PROTOCOL, e.getChannelId(), e.getName())); + } +} diff --git a/jetlinks-manager/notify-manager/pom.xml b/jetlinks-manager/notify-manager/pom.xml index a33d2e2d..ea5acd85 100644 --- a/jetlinks-manager/notify-manager/pom.xml +++ b/jetlinks-manager/notify-manager/pom.xml @@ -76,6 +76,19 @@ notify-wechat ${project.version} + + + ${project.groupId} + notify-webhook + ${project.version} + + + + ${project.groupId} + notify-voice + ${project.version} + + org.jetlinks.community notify-core diff --git a/jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/subscriber/Notify.java b/jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/subscriber/Notify.java index 82260d1b..dde66e7b 100644 --- a/jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/subscriber/Notify.java +++ b/jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/subscriber/Notify.java @@ -15,4 +15,14 @@ public class Notify { private String dataId; private long notifyTime; + + + private String code; + + private Object detail; + + + public static Notify of(String message, String dataId, long timestamp) { + return new Notify(message, dataId, timestamp, null,null); + } } diff --git a/jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/subscriber/SubscriberProvider.java b/jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/subscriber/SubscriberProvider.java index 27065ad2..f1d2d088 100644 --- a/jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/subscriber/SubscriberProvider.java +++ b/jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/subscriber/SubscriberProvider.java @@ -2,6 +2,8 @@ package org.jetlinks.community.notify.manager.subscriber; import org.hswebframework.web.authorization.Authentication; import org.jetlinks.core.metadata.ConfigMetadata; +import org.jetlinks.core.metadata.PropertyMetadata; +import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import java.util.Map; @@ -13,5 +15,9 @@ public interface SubscriberProvider { Mono createSubscriber(String id, Authentication authentication, Map config); + default Flux getDetailProperties(Map config) { + return Flux.empty(); + } + ConfigMetadata getConfigMetadata(); } diff --git a/jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/subscriber/providers/AlarmProvider.java b/jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/subscriber/providers/AlarmProvider.java new file mode 100755 index 00000000..458bced2 --- /dev/null +++ b/jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/subscriber/providers/AlarmProvider.java @@ -0,0 +1,125 @@ +package org.jetlinks.community.notify.manager.subscriber.providers; + +import com.alibaba.fastjson.JSONObject; +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.hswebframework.web.authorization.Authentication; +import org.hswebframework.web.i18n.LocaleUtils; +import org.jetlinks.community.ValueObject; +import org.jetlinks.community.notify.manager.subscriber.Notify; +import org.jetlinks.community.notify.manager.subscriber.Subscriber; +import org.jetlinks.community.notify.manager.subscriber.SubscriberProvider; +import org.jetlinks.community.topic.Topics; +import org.jetlinks.core.event.EventBus; +import org.jetlinks.core.event.Subscription; +import org.jetlinks.core.metadata.ConfigMetadata; +import org.jetlinks.core.metadata.DefaultConfigMetadata; +import org.jetlinks.core.metadata.PropertyMetadata; +import org.jetlinks.core.metadata.SimplePropertyMetadata; +import org.jetlinks.core.metadata.types.StringType; +import org.springframework.stereotype.Component; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.util.Map; +import java.util.Objects; + +@Component +public class AlarmProvider implements SubscriberProvider { + + private final EventBus eventBus; + + public AlarmProvider(EventBus eventBus) { + this.eventBus = eventBus; + } + + @Override + public String getId() { + return "alarm"; + } + + @Override + public String getName() { + return "告警"; + } + + @Override + public ConfigMetadata getConfigMetadata() { + return new DefaultConfigMetadata() + .add("alarmConfigId", "告警规则", "告警规则,支持通配符:*", StringType.GLOBAL); + } + + @Override + public Mono createSubscriber(String id, Authentication authentication, Map config) { + ValueObject configs = ValueObject.of(config); + + String alarmId = configs.getString("alarmConfigId").orElse("*"); + + String topic = Topics.alarm("*", "*", alarmId); + return Mono.justOrEmpty(()-> createSubscribe(id, new String[]{topic})); + + } + + private Flux createSubscribe(String id, + String[] topics) { + Subscription.Feature[] features = new Subscription.Feature[]{Subscription.Feature.local}; + return Flux + .defer(() -> this + .eventBus + .subscribe(Subscription.of("alarm:" + id, topics, features)) + .map(msg -> { + JSONObject json = msg.bodyToJson(); + return Notify.of( + getNotifyMessage(json), + //告警记录ID + json.getString("id"), + System.currentTimeMillis(), + "alarm", + json + ); + })); + } + + private static String getNotifyMessage(JSONObject json) { + + String message; + TargetType targetType = TargetType.of(json.getString("targetType")); + String targetName = json.getString("targetName"); + String alarmName = json.getString("alarmName"); + if (targetType == TargetType.other) { + message = String.format("[%s]发生告警:[%s]!", targetName, alarmName); + } else { + message = String.format("%s[%s]发生告警:[%s]!", targetType.getText(), targetName, alarmName); + } + return LocaleUtils.resolveMessage("message.alarm.notify." + targetType.name(), message, targetName, alarmName); + } + + @Override + public Flux getDetailProperties(Map config) { + //todo 根据配置来获取输出数据 + return Flux.just( + SimplePropertyMetadata.of("targetType", "告警类型", StringType.GLOBAL), + SimplePropertyMetadata.of("alarmName", "告警名称", StringType.GLOBAL), + SimplePropertyMetadata.of("targetName", "目标名称", StringType.GLOBAL) + ); + } + + @AllArgsConstructor + @Getter + enum TargetType { + device("设备"), + product("产品"), + other("其它"); + + private final String text; + + public static TargetType of(String name) { + for (TargetType value : TargetType.values()) { + if (Objects.equals(value.name(), name)) { + return value; + } + } + return TargetType.other; + } + } +} diff --git a/jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/subscriber/providers/DeviceAlarmProvider.java b/jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/subscriber/providers/DeviceAlarmProvider.java deleted file mode 100644 index 5d93f696..00000000 --- a/jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/subscriber/providers/DeviceAlarmProvider.java +++ /dev/null @@ -1,74 +0,0 @@ -package org.jetlinks.community.notify.manager.subscriber.providers; - -import com.alibaba.fastjson.JSONObject; -import org.hswebframework.web.authorization.Authentication; -import org.jetlinks.community.ValueObject; -import org.jetlinks.community.notify.manager.subscriber.Notify; -import org.jetlinks.community.notify.manager.subscriber.Subscriber; -import org.jetlinks.community.notify.manager.subscriber.SubscriberProvider; -import org.jetlinks.core.event.EventBus; -import org.jetlinks.core.event.Subscription; -import org.jetlinks.core.metadata.ConfigMetadata; -import org.jetlinks.core.metadata.DefaultConfigMetadata; -import org.jetlinks.core.metadata.types.StringType; -import org.springframework.stereotype.Component; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; - -import java.util.Map; - -@Component -public class DeviceAlarmProvider implements SubscriberProvider { - - private final EventBus eventBus; - - public DeviceAlarmProvider(EventBus eventBus) { - this.eventBus = eventBus; - } - - @Override - public String getId() { - return "device_alarm"; - } - - @Override - public String getName() { - return "设备告警"; - } - - @Override - public ConfigMetadata getConfigMetadata() { - return new DefaultConfigMetadata() - .add("productId", "产品ID", "产品ID,支持通配符:*", StringType.GLOBAL) - .add("deviceId", "设备ID", "设备ID,支持通配符:*", StringType.GLOBAL) - .add("productId", "告警ID", "告警ID,支持通配符:*", StringType.GLOBAL) - ; - } - - @Override - public Mono createSubscriber(String id, Authentication authentication, Map config) { - ValueObject configs = ValueObject.of(config); - - String productId = configs.getString("productId").orElse("*"); - String deviceId = configs.getString("deviceId").orElse("*"); - String alarmId = configs.getString("alarmId").orElse("*"); - - Flux flux = eventBus - .subscribe(Subscription.of("device-alarm:" + id, - String.format("/rule-engine/device/alarm/%s/%s/%s", productId, deviceId, alarmId), - Subscription.Feature.local - )) - .map(msg -> { - JSONObject json = msg.bodyToJson(true); - - return Notify.of( - String.format("设备[%s]发生告警:[%s]!", json.getString("deviceName"), json.getString("alarmName")), - json.getString("alarmId"), - System.currentTimeMillis() - ); - - }); - - return Mono.just(() -> flux); - } -} diff --git a/jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/web/NotifierController.java b/jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/web/NotifierController.java old mode 100644 new mode 100755 index e2a8a56f..992cfbcc --- a/jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/web/NotifierController.java +++ b/jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/web/NotifierController.java @@ -4,15 +4,16 @@ import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.AllArgsConstructor; import lombok.Getter; import lombok.Setter; import org.hswebframework.web.authorization.annotation.Resource; import org.hswebframework.web.authorization.annotation.ResourceAction; import org.hswebframework.web.exception.NotFoundException; -import org.jetlinks.community.notify.DefaultNotifyType; import org.jetlinks.community.notify.NotifierManager; import org.jetlinks.community.notify.NotifyType; import org.jetlinks.community.notify.manager.entity.NotifyTemplateEntity; +import org.jetlinks.community.notify.manager.service.NotifyConfigService; import org.jetlinks.community.notify.template.TemplateManager; import org.jetlinks.core.Values; import org.springframework.web.bind.annotation.*; @@ -27,17 +28,15 @@ import java.util.function.Function; @RequestMapping("/notifier") @Resource(id = "notifier", name = "通知管理") @Tag(name = "消息通知管理") +@AllArgsConstructor public class NotifierController { + private final NotifyConfigService configService; private final NotifierManager notifierManager; private final TemplateManager templateManager; - public NotifierController(NotifierManager notifierManager, TemplateManager templateManager) { - this.notifierManager = notifierManager; - this.templateManager = templateManager; - } /** * 指定通知器以及模版.发送通知. @@ -52,16 +51,37 @@ public class NotifierController { public Mono sendNotify(@PathVariable @Parameter(description = "通知配置ID") String notifierId, @RequestBody Mono mono) { return mono.flatMap(tem -> { - NotifyType type = DefaultNotifyType.valueOf(tem.getTemplate().getType()); - return Mono.zip( - notifierManager.getNotifier(type, notifierId) - .switchIfEmpty(Mono.error(() -> new NotFoundException("通知器[" + notifierId + "]不存在"))), - templateManager.createTemplate(type, tem.getTemplate().toTemplateProperties()), - (notifier, template) -> notifier.send(template, Values.of(tem.getContext()))) + NotifyType type = NotifyType.of(tem.getTemplate().getType()); + return Mono + .zip( + notifierManager + .getNotifier(type, notifierId) + .switchIfEmpty(Mono.error(() -> new NotFoundException("error.notifier_does_not_exist", notifierId))), + templateManager.createTemplate(type, tem.getTemplate().toTemplateProperties()), + (notifier, template) -> notifier.send(template, Values.of(tem.getContext()))) .flatMap(Function.identity()); }); } + @PostMapping("/{notifierId}/{templateId}/_send") + @ResourceAction(id = "send", name = "发送通知") + @Operation(summary = "根据配置和模版ID发送消息通知") + public Mono sendNotify(@PathVariable @Parameter(description = "通知配置ID") String notifierId, + @PathVariable @Parameter(description = "通知模版ID") String templateId, + @RequestBody Mono> contextMono) { + return configService + .findById(notifierId) + .flatMap(conf -> Mono + .zip( + notifierManager + .getNotifier(NotifyType.of(conf.getType()), notifierId) + .switchIfEmpty(Mono.error(() -> new NotFoundException("error.notifier_does_not_exist", notifierId))), + contextMono, + (notifier, contextMap) -> notifier.send(templateId, Values.of(contextMap)) + ) + .flatMap(Function.identity())); + } + @Getter @Setter public static class SendNotifyRequest { @@ -74,4 +94,4 @@ public class NotifierController { private Map context = new HashMap<>(); } -} \ No newline at end of file +} diff --git a/jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/web/NotifierTemplateController.java b/jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/web/NotifierTemplateController.java index 7b3e88a5..66cc3fcb 100644 --- a/jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/web/NotifierTemplateController.java +++ b/jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/web/NotifierTemplateController.java @@ -1,23 +1,26 @@ package org.jetlinks.community.notify.manager.web; import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; +import org.hswebframework.web.api.crud.entity.QueryParamEntity; import org.hswebframework.web.authorization.annotation.Authorize; import org.hswebframework.web.authorization.annotation.QueryAction; import org.hswebframework.web.authorization.annotation.Resource; import org.hswebframework.web.crud.service.ReactiveCrudService; import org.hswebframework.web.crud.web.reactive.ReactiveServiceCrudController; +import org.hswebframework.web.exception.ValidationException; import org.jetlinks.community.notify.manager.entity.NotifyTemplateEntity; +import org.jetlinks.community.notify.manager.service.NotifyConfigService; import org.jetlinks.community.notify.manager.service.NotifyTemplateService; +import org.jetlinks.community.notify.manager.web.response.TemplateInfo; import org.jetlinks.community.notify.template.TemplateProvider; import org.jetlinks.core.metadata.ConfigMetadata; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import java.util.ArrayList; import java.util.List; /** @@ -35,11 +38,14 @@ public class NotifierTemplateController implements ReactiveServiceCrudController private final NotifyTemplateService templateService; private final List providers; + private final NotifyConfigService configService; - - public NotifierTemplateController(NotifyTemplateService templateService, List providers) { + public NotifierTemplateController(NotifyTemplateService templateService, + NotifyConfigService configService, + List providers) { this.templateService = templateService; this.providers = providers; + this.configService = configService; } @Override @@ -47,17 +53,69 @@ public class NotifierTemplateController implements ReactiveServiceCrudController return templateService; } + @PostMapping("/{configId}/_query") + @QueryAction + @Operation(summary = "根据配置ID查询通知模版列表") + public Flux queryTemplatesByConfigId(@PathVariable + @Parameter(description = "配置ID") String configId, + @RequestBody Mono query) { + return configService + .findById(configId) + .flatMapMany(conf -> query + .flatMapMany(param -> param + .toNestQuery(nest -> nest + //where type = ? and provider = ? and (config_id = ? or config_id is null or config_id = '') + .is(NotifyTemplateEntity::getType, conf.getType()) + .is(NotifyTemplateEntity::getProvider, conf.getProvider()) + .nest() + /**/.is(NotifyTemplateEntity::getConfigId, configId) + /* */.or() + /**/.isNull(NotifyTemplateEntity::getConfigId) + .isEmpty(NotifyTemplateEntity::getConfigId) + ) + .noPaging() + .execute(templateService::query))); + + } + + @GetMapping("/{templateId}/detail") + @QueryAction + @Operation(summary = "获取模版详情信息") + public Mono getTemplateDetail(@PathVariable + @Parameter(description = "模版ID") String templateId) { + return templateService + .findById(templateId) + .flatMap(e -> { + TemplateInfo info = new TemplateInfo(); + info.setId(e.getId()); + info.setName(e.getName()); + return this + .getProvider(e.getType(), e.getProvider()) + .createTemplate(e.toTemplateProperties()) + .doOnNext(t -> info.setVariableDefinitions(new ArrayList<>(t.getVariables().values()))) + .thenReturn(info); + }); + } @GetMapping("/{type}/{provider}/config/metadata") @QueryAction @Operation(summary = "获取指定类型和服务商所需模版配置定义") - public Mono getAllTypes(@PathVariable String type, - @PathVariable String provider) { - return Flux.fromIterable(providers) - .filter(prov -> prov.getType().getId().equalsIgnoreCase(type) && prov.getProvider().getId().equalsIgnoreCase(provider)) - .flatMap(prov -> Mono.justOrEmpty(prov.getTemplateConfigMetadata())) - .next(); + public Mono getConfigMetadata(@PathVariable @Parameter(description = "通知类型ID") String type, + @PathVariable @Parameter(description = "服务商ID") String provider) { + return Mono.justOrEmpty(getProvider(type, provider).getTemplateConfigMetadata()); + } + + public TemplateProvider getProvider(String type, String provider) { + for (TemplateProvider prov : providers) { + if (prov.getType().getId().equalsIgnoreCase(type) && prov + .getProvider() + .getId() + .equalsIgnoreCase(provider)) { + return prov; + } + } + throw new ValidationException("error.unsupported_notify_provider"); } } diff --git a/jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/web/response/TemplateInfo.java b/jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/web/response/TemplateInfo.java new file mode 100644 index 00000000..360e4ca5 --- /dev/null +++ b/jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/web/response/TemplateInfo.java @@ -0,0 +1,28 @@ +package org.jetlinks.community.notify.manager.web.response; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.jetlinks.community.notify.template.VariableDefinition; + +import java.util.List; + +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor +public class TemplateInfo { + + @Schema(description = "模版ID") + private String id; + + @Schema(description = "模版名称") + private String name; + + @Schema(description = "变量定义信息") + private List variableDefinitions; + + +} diff --git a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/configuration/RuleEngineManagerConfiguration.java b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/configuration/RuleEngineManagerConfiguration.java index 6600da65..1b017550 100644 --- a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/configuration/RuleEngineManagerConfiguration.java +++ b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/configuration/RuleEngineManagerConfiguration.java @@ -1,11 +1,16 @@ package org.jetlinks.community.rule.engine.configuration; +import org.jetlinks.community.elastic.search.index.ElasticSearchIndexManager; +import org.jetlinks.community.elastic.search.service.ElasticSearchService; import org.jetlinks.community.rule.engine.scene.SceneFilter; import org.jetlinks.community.rule.engine.scene.SceneTaskExecutorProvider; +import org.jetlinks.community.rule.engine.service.ElasticSearchAlarmHistoryService; import org.jetlinks.core.event.EventBus; import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; @AutoConfiguration public class RuleEngineManagerConfiguration { @@ -17,4 +22,15 @@ public class RuleEngineManagerConfiguration { return new SceneTaskExecutorProvider(eventBus, SceneFilter.composite(filters)); } + + @Configuration(proxyBeanMethods = false) + @ConditionalOnClass(ElasticSearchService.class) + static class ElasticSearchAlarmHistoryConfiguration { + + @Bean(initMethod = "init") + public ElasticSearchAlarmHistoryService alarmHistoryService(ElasticSearchService elasticSearchService, + ElasticSearchIndexManager indexManager) { + return new ElasticSearchAlarmHistoryService(indexManager, elasticSearchService); + } + } } diff --git a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/measurement/AlarmDashboardDefinition.java b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/measurement/AlarmDashboardDefinition.java new file mode 100644 index 00000000..b45e13f5 --- /dev/null +++ b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/measurement/AlarmDashboardDefinition.java @@ -0,0 +1,21 @@ +package org.jetlinks.community.rule.engine.measurement; + +import lombok.AllArgsConstructor; +import lombok.Generated; +import lombok.Getter; +import org.jetlinks.community.dashboard.DashboardDefinition; + +/** + * @author bestfeng + */ +@Getter +@AllArgsConstructor +@Generated +public enum AlarmDashboardDefinition implements DashboardDefinition { + + alarm("alarm","告警信息"); + + private String id; + + private String name; +} diff --git a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/measurement/AlarmObjectDefinition.java b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/measurement/AlarmObjectDefinition.java new file mode 100755 index 00000000..87294118 --- /dev/null +++ b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/measurement/AlarmObjectDefinition.java @@ -0,0 +1,21 @@ +package org.jetlinks.community.rule.engine.measurement; + +import lombok.AllArgsConstructor; +import lombok.Generated; +import lombok.Getter; +import org.jetlinks.community.dashboard.ObjectDefinition; + +@Getter +@AllArgsConstructor +@Generated +public enum AlarmObjectDefinition implements ObjectDefinition { + + record("告警记录"); + + @Override + public String getId() { + return name(); + } + + private String name; +} diff --git a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/measurement/AlarmRecordMeasurementProvider.java b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/measurement/AlarmRecordMeasurementProvider.java new file mode 100644 index 00000000..0234d1ee --- /dev/null +++ b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/measurement/AlarmRecordMeasurementProvider.java @@ -0,0 +1,51 @@ +package org.jetlinks.community.rule.engine.measurement; + +import com.google.common.collect.Maps; +import io.micrometer.core.instrument.MeterRegistry; +import org.jetlinks.community.PropertyConstants; +import org.jetlinks.community.dashboard.supports.StaticMeasurementProvider; +import org.jetlinks.community.micrometer.MeterRegistryManager; +import org.jetlinks.community.rule.engine.entity.AlarmHistoryInfo; +import org.jetlinks.community.timeseries.TimeSeriesManager; +import org.jetlinks.community.utils.ConverterUtils; +import org.springframework.context.event.EventListener; +import org.springframework.stereotype.Component; + +import java.util.Map; + +/** + * @author bestfeng + */ +@Component +public class AlarmRecordMeasurementProvider extends StaticMeasurementProvider { + + MeterRegistry registry; + + public AlarmRecordMeasurementProvider(MeterRegistryManager registryManager, + TimeSeriesManager timeSeriesManager) { + super(AlarmDashboardDefinition.alarm, AlarmObjectDefinition.record); + + registry = registryManager.getMeterRegister(AlarmTimeSeriesMetric.alarmStreamMetrics().getId()); + addMeasurement(new AlarmRecordTrendMeasurement(timeSeriesManager)); + addMeasurement(new AlarmRecordRankMeasurement(timeSeriesManager)); + + } + + @EventListener + public void aggAlarmRecord(AlarmHistoryInfo info) { + registry + .counter("record-agg", getTags(info)) + .increment(); + } + + + + public String[] getTags(AlarmHistoryInfo info) { + Map tagMap = Maps.newLinkedHashMap(); + tagMap.put("targetId", info.getTargetId()); + tagMap.put("targetType", info.getTargetType()); + tagMap.put("targetName", info.getTargetName()); + tagMap.put("alarmConfigId", info.getAlarmConfigId()); + return ConverterUtils.convertMapToTags(tagMap); + } +} diff --git a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/measurement/AlarmRecordRankMeasurement.java b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/measurement/AlarmRecordRankMeasurement.java new file mode 100644 index 00000000..a68cada1 --- /dev/null +++ b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/measurement/AlarmRecordRankMeasurement.java @@ -0,0 +1,135 @@ +package org.jetlinks.community.rule.engine.measurement; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.jetlinks.community.dashboard.*; +import org.jetlinks.community.dashboard.supports.StaticMeasurement; +import org.jetlinks.community.timeseries.TimeSeriesManager; +import org.jetlinks.community.timeseries.query.Aggregation; +import org.jetlinks.community.timeseries.query.AggregationData; +import org.jetlinks.community.timeseries.query.AggregationQueryParam; +import org.jetlinks.core.metadata.ConfigMetadata; +import org.jetlinks.core.metadata.DataType; +import org.jetlinks.core.metadata.DefaultConfigMetadata; +import org.jetlinks.core.metadata.types.IntType; +import org.jetlinks.core.metadata.types.StringType; +import reactor.core.publisher.Flux; + +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.util.Comparator; +import java.util.Date; +import java.util.Objects; + +/** + * @author bestfeng + */ +public class AlarmRecordRankMeasurement extends StaticMeasurement { + + TimeSeriesManager timeSeriesManager; + + public AlarmRecordRankMeasurement(TimeSeriesManager timeSeriesManager) { + super(MeasurementDefinition.of("rank", "告警记录排名")); + this.timeSeriesManager = timeSeriesManager; + addDimension(new AggRecordRankDimension()); + } + + + static ConfigMetadata aggConfigMetadata = new DefaultConfigMetadata() + .add("time", "周期", "例如: 1h,10m,30s", StringType.GLOBAL) + .add("agg", "聚合类型", "count,sum,avg,max,min", StringType.GLOBAL) + .add("format", "时间格式", "如: MM-dd:HH", StringType.GLOBAL) + .add("limit", "最大数据量", "", StringType.GLOBAL) + .add("from", "时间从", "", StringType.GLOBAL) + .add("to", "时间至", "", StringType.GLOBAL); + + + class AggRecordRankDimension implements MeasurementDimension { + + @Override + public DimensionDefinition getDefinition() { + return CommonDimensionDefinition.agg; + } + + @Override + public DataType getValueType() { + return IntType.GLOBAL; + } + + @Override + public ConfigMetadata getParams() { + return aggConfigMetadata; + } + + @Override + public boolean isRealTime() { + return false; + } + + public AggregationQueryParam createQueryParam(MeasurementParameter parameter) { + return AggregationQueryParam + .of() + .groupBy(parameter.getString("group", "targetId")) + .sum("count", "count") + .agg("targetId", Aggregation.TOP) + .filter(query -> query + .where("name", "record-agg") + .where("targetType", parameter.getString("targetType", null)) + ) + .limit(parameter.getInt("limit").orElse(1)) + .from(parameter + .getDate("from") + .orElseGet(() -> Date + .from(LocalDateTime + .now() + .plusDays(-1) + .atZone(ZoneId.systemDefault()) + .toInstant()))) + .to(parameter.getDate("to").orElse(new Date())); + } + + @Override + public Flux getValue(MeasurementParameter parameter) { + + Comparator comparator; + if (Objects.equals(parameter.getString("order",""), "asc")){ + comparator = Comparator.comparingInt(d-> d.getInt("count", 0)); + }else { + comparator = Comparator.comparingInt(d-> d.getInt("count", 0)).reversed(); + } + + AggregationQueryParam param = createQueryParam(parameter); + + return Flux.defer(() -> param + .execute(timeSeriesManager.getService(AlarmTimeSeriesMetric.alarmStreamMetrics())::aggregation) + .groupBy(a -> a.getString("targetId", null)) + .flatMap(fluxGroup -> fluxGroup.reduce(AggregationData::merge)) + .sort(comparator) + .map(data -> SimpleMeasurementValue.of(new SimpleResult(data), 0)) + ) + .take(param.getLimit()); + } + + @Getter + @Setter + @AllArgsConstructor + @NoArgsConstructor + class SimpleResult { + + private String targetId; + + private String targetName; + + private Integer count; + + public SimpleResult(AggregationData data) { + String targetId = data.getString("targetId", ""); + this.setCount(data.getInt("count", 0)); + this.setTargetName(data.getString("targetName", targetId)); + this.setTargetId(data.getString("targetId", "")); + } + } + } +} diff --git a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/measurement/AlarmRecordTrendMeasurement.java b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/measurement/AlarmRecordTrendMeasurement.java new file mode 100644 index 00000000..58d0f804 --- /dev/null +++ b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/measurement/AlarmRecordTrendMeasurement.java @@ -0,0 +1,101 @@ +package org.jetlinks.community.rule.engine.measurement; + +import org.jetlinks.community.dashboard.*; +import org.jetlinks.community.dashboard.supports.StaticMeasurement; +import org.jetlinks.community.timeseries.TimeSeriesManager; +import org.jetlinks.community.timeseries.query.AggregationQueryParam; +import org.jetlinks.core.metadata.ConfigMetadata; +import org.jetlinks.core.metadata.DataType; +import org.jetlinks.core.metadata.DefaultConfigMetadata; +import org.jetlinks.core.metadata.types.IntType; +import org.jetlinks.core.metadata.types.StringType; +import reactor.core.publisher.Flux; + +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.util.Date; + +/** + * @author bestfeng + */ +public class AlarmRecordTrendMeasurement extends StaticMeasurement { + + TimeSeriesManager timeSeriesManager; + + public AlarmRecordTrendMeasurement(TimeSeriesManager timeSeriesManager) { + super(MeasurementDefinition.of("trend", "告警记录趋势")); + this.timeSeriesManager = timeSeriesManager; + addDimension(new AggRecordTrendDimension()); + } + + + static ConfigMetadata aggConfigMetadata = new DefaultConfigMetadata() + .add("alarmConfigId", "告警配置Id", "", StringType.GLOBAL) + .add("time", "周期", "例如: 1h,10m,30s", StringType.GLOBAL) + .add("agg", "聚合类型", "count,sum,avg,max,min", StringType.GLOBAL) + .add("format", "时间格式", "如: MM-dd:HH", StringType.GLOBAL) + .add("limit", "最大数据量", "", StringType.GLOBAL) + .add("from", "时间从", "", StringType.GLOBAL) + .add("to", "时间至", "", StringType.GLOBAL); + + + + class AggRecordTrendDimension implements MeasurementDimension{ + + @Override + public DimensionDefinition getDefinition() { + return CommonDimensionDefinition.agg; + } + + @Override + public DataType getValueType() { + return IntType.GLOBAL; + } + + @Override + public ConfigMetadata getParams() { + return aggConfigMetadata; + } + + @Override + public boolean isRealTime() { + return false; + } + + public AggregationQueryParam createQueryParam(MeasurementParameter parameter) { + return AggregationQueryParam + .of() + .groupBy(parameter.getInterval("time", null), + parameter.getString("format").orElse("MM月dd日 HH时")) + .sum("count", "count") + .filter(query -> query + .where("name", "record-agg") + .and("targetType",parameter.getString("targetType").orElse(null)) + .and("targetId",parameter.getString("targetId").orElse(null)) + .is("alarmConfigId", parameter.getString("alarmConfigId").orElse(null)) + ) + .limit(parameter.getInt("limit").orElse(1)) + .from(parameter + .getDate("from") + .orElseGet(() -> Date + .from(LocalDateTime + .now() + .plusDays(-1) + .atZone(ZoneId.systemDefault()) + .toInstant()))) + .to(parameter.getDate("to").orElse(new Date())); + } + + @Override + public Flux getValue(MeasurementParameter parameter) { + AggregationQueryParam param = createQueryParam(parameter); + return Flux.defer(()-> param + .execute(timeSeriesManager.getService(AlarmTimeSeriesMetric.alarmStreamMetrics())::aggregation) + .index((index, data) -> SimpleMeasurementValue.of( + data.getLong("count",0), + data.getString("time",""), + index))) + .take(param.getLimit()); + } + } +} diff --git a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/measurement/AlarmTimeSeriesMetric.java b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/measurement/AlarmTimeSeriesMetric.java new file mode 100755 index 00000000..aeced99d --- /dev/null +++ b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/measurement/AlarmTimeSeriesMetric.java @@ -0,0 +1,23 @@ +package org.jetlinks.community.rule.engine.measurement; + +import org.jetlinks.community.timeseries.TimeSeriesMetric; + +/** + * 媒体时序数据度量标识 + * + * @author bestfeng + * + * @see org.jetlinks.pro.timeseries.TimeSeriesService + * @see TimeSeriesMetric + */ +public interface AlarmTimeSeriesMetric { + + /** + * 告警监控指标,用于对告警进行进行监控 + * + * @return 度量标识 + */ + static TimeSeriesMetric alarmStreamMetrics() { + return TimeSeriesMetric.of("alarm_metrics"); + } +} diff --git a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/scene/DeviceTrigger.java b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/scene/DeviceTrigger.java index 868126da..0972f0ba 100644 --- a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/scene/DeviceTrigger.java +++ b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/scene/DeviceTrigger.java @@ -16,6 +16,7 @@ import org.hswebframework.web.i18n.LocaleUtils; import org.hswebframework.web.validator.ValidatorUtils; import org.jetlinks.core.device.DeviceRegistry; import org.jetlinks.core.metadata.DeviceMetadata; +import org.jetlinks.core.utils.Reactors; import org.jetlinks.community.TimerSpec; import org.jetlinks.community.rule.engine.executor.DeviceMessageSendTaskExecutorProvider; import org.jetlinks.community.rule.engine.executor.device.DeviceSelectorSpec; @@ -24,9 +25,12 @@ import org.jetlinks.community.rule.engine.scene.term.TermColumn; import org.jetlinks.community.rule.engine.scene.term.TermTypeSupport; import org.jetlinks.community.rule.engine.scene.term.TermTypes; import org.jetlinks.community.rule.engine.scene.value.TermValue; +import org.jetlinks.reactor.ql.ReactorQL; +import org.jetlinks.reactor.ql.ReactorQLContext; import org.jetlinks.rule.engine.api.model.RuleModel; import org.jetlinks.rule.engine.api.model.RuleNodeModel; import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -53,6 +57,10 @@ public class DeviceTrigger extends DeviceSelectorSpec implements Serializable { private DeviceOperation operation; public SqlRequest createSql(List terms) { + return createSql(terms, true); + } + + public SqlRequest createSql(List terms, boolean hasWhere) { Map termsMap = SceneUtils.expandTerm(terms); // select * from ( @@ -78,6 +86,8 @@ public class DeviceTrigger extends DeviceSelectorSpec implements Serializable { selectColumns.add("this.headers._uid \"_uid\""); //维度绑定信息,如部门等 selectColumns.add("this.headers.bindings \"_bindings\""); + //链路追踪ID + selectColumns.add("this.headers.traceparent \"traceparent\""); switch (this.operation.getOperator()) { case readProperty: @@ -133,14 +143,56 @@ public class DeviceTrigger extends DeviceSelectorSpec implements Serializable { builder.append("\t\nfrom ").append(createFromTable()); builder.append("\n) t \n"); - - SqlFragments fragments = terms == null ? EmptySqlFragments.INSTANCE : termBuilder.createTermFragments(this, terms); - if (!fragments.isEmpty()) { - SqlRequest request = fragments.toRequest(); - builder.append("where ").append(request.getSql()); + if (hasWhere) { + SqlFragments fragments = terms == null ? EmptySqlFragments.INSTANCE : termBuilder.createTermFragments(this, terms); + if (!fragments.isEmpty()) { + SqlRequest request = fragments.toRequest(); + builder.append("where ").append(request.getSql()); + } + return PrepareSqlRequest.of(builder.toString(), fragments.getParameters().toArray()); } - return PrepareSqlRequest.of(builder.toString(), fragments.getParameters().toArray()); + return PrepareSqlRequest.of(builder.toString(), new Object[0]); + + } + + String createFilterDescription(List terms) { + SqlFragments fragments = CollectionUtils.isEmpty(terms) ? EmptySqlFragments.INSTANCE : termBuilder.createTermFragments(this, terms); + return fragments.isEmpty() ? "true" : fragments.toRequest().toNativeSql(); + } + + Function, Mono> createFilter(List terms) { + SqlFragments fragments = CollectionUtils.isEmpty(terms) ? EmptySqlFragments.INSTANCE : termBuilder.createTermFragments(this, terms); + if (!fragments.isEmpty()) { + SqlRequest request = fragments.toRequest(); + String sql = "select 1 from t where " + request.getSql(); + ReactorQL ql = ReactorQL + .builder() + .sql(sql) + .build(); + Object[] args = request.getParameters(); + String sqlString = request.toNativeSql(); + return new Function, Mono>() { + @Override + public Mono apply(Map map) { + ReactorQLContext context = ReactorQLContext.ofDatasource((t) -> Flux.just(map)); + for (Object arg : args) { + context.bind(arg); + } + + return ql + .start(context) + .hasElements(); + } + + @Override + public String toString() { + return sqlString; + } + }; + } + + return ignore -> Reactors.ALWAYS_TRUE; } @@ -363,6 +415,8 @@ public class DeviceTrigger extends DeviceSelectorSpec implements Serializable { timerNode.setId("scene:device:timer"); timerNode.setName("定时下发指令"); timerNode.setExecutor("timer"); + //使用最小负载节点来执行定时 + // timerNode.setSchedulingRule(SchedulerSelectorStrategy.minimumLoad()); timerNode.setConfiguration(FastBeanCopier.copy(timer, new HashMap<>())); model.getNodes().add(timerNode); diff --git a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/scene/SceneActions.java b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/scene/SceneActions.java new file mode 100644 index 00000000..71dfa81c --- /dev/null +++ b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/scene/SceneActions.java @@ -0,0 +1,21 @@ +package org.jetlinks.community.rule.engine.scene; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; + +import java.io.Serializable; +import java.util.List; + +@Getter +@Setter +public class SceneActions implements Serializable { + + @Schema(description = "是否并行执行动作") + private boolean parallel; + + @Schema(description = "执行动作") + private List actions; + + +} diff --git a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/scene/SceneConditionAction.java b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/scene/SceneConditionAction.java new file mode 100644 index 00000000..9a9a1981 --- /dev/null +++ b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/scene/SceneConditionAction.java @@ -0,0 +1,25 @@ +package org.jetlinks.community.rule.engine.scene; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; +import org.hswebframework.ezorm.core.param.Term; +import org.jetlinks.community.rule.engine.commons.ShakeLimit; + +import java.io.Serializable; +import java.util.List; + +@Getter +@Setter +public class SceneConditionAction implements Serializable { + + @Schema(description = "条件") + private List when; + + @Schema(description = "防抖配置") + private ShakeLimit shakeLimit; + + @Schema(description = "满足条件时执行的动作") + private SceneActions then; + +} diff --git a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/scene/SceneRule.java b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/scene/SceneRule.java index edc7fb08..b35980ed 100644 --- a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/scene/SceneRule.java +++ b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/scene/SceneRule.java @@ -13,21 +13,28 @@ import org.hswebframework.web.i18n.LocaleUtils; import org.hswebframework.web.validator.ValidatorUtils; import org.jetlinks.core.device.DeviceRegistry; import org.jetlinks.core.metadata.types.DateTimeType; +import org.jetlinks.core.utils.Reactors; +import org.jetlinks.community.rule.engine.commons.ShakeLimit; import org.jetlinks.community.rule.engine.commons.TermsConditionEvaluator; import org.jetlinks.community.rule.engine.scene.term.TermColumn; +import org.jetlinks.community.rule.engine.scene.term.limit.ShakeLimitGrouping; import org.jetlinks.rule.engine.api.model.RuleLink; import org.jetlinks.rule.engine.api.model.RuleModel; import org.jetlinks.rule.engine.api.model.RuleNodeModel; import org.jetlinks.rule.engine.defaults.AbstractExecutionContext; +import reactor.core.Disposable; +import reactor.core.Disposables; import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.core.publisher.Sinks; +import reactor.util.concurrent.Queues; import javax.validation.constraints.NotBlank; import javax.validation.constraints.NotNull; import java.io.Serializable; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; +import java.util.*; +import java.util.function.BiFunction; +import java.util.function.Function; @Getter @Setter @@ -45,11 +52,6 @@ public class SceneRule implements Serializable { @NotNull(message = "error.scene_rule_trigger_cannot_be_null") private Trigger trigger; - /** - * @see TermColumn - * @see org.jetlinks.community.rule.engine.scene.term.TermType - * @see org.jetlinks.community.rule.engine.scene.value.TermValue - */ @Schema(description = "触发条件") private List terms; @@ -59,17 +61,42 @@ public class SceneRule implements Serializable { @Schema(description = "执行动作") private List actions; + @Schema(description = "动作分支") + private List branches; + @Schema(description = "说明") private String description; - public SqlRequest createSql() { + public SqlRequest createSql(boolean hasWhere) { if (trigger != null && trigger.getType() == TriggerType.device) { - return trigger.getDevice().createSql(terms); + return trigger.getDevice().createSql(terms, hasWhere); } return EmptySqlRequest.INSTANCE; } + public Function, Mono> createFilter(List terms) { + if (trigger != null && trigger.getType() == TriggerType.device) { + return trigger.getDevice().createFilter(terms); + } + + return ignore -> Reactors.ALWAYS_TRUE; + } + + String createFilterDescription(List terms) { + if (trigger != null && trigger.getType() == TriggerType.device) { + return trigger.getDevice().createFilterDescription(terms); + } + + return "true"; + } + + public ShakeLimitGrouping> createGrouping() { + //todo 其他分组方式实现 + return flux -> flux + .groupBy(map -> map.getOrDefault("deviceId", "null"), Integer.MAX_VALUE); + } + private Flux createSceneVariables(List columns) { return LocaleUtils .currentReactive() @@ -87,8 +114,8 @@ public class SceneRule implements Serializable { List termVar = SceneUtils.parseVariable(terms, columns); List variables = new ArrayList<>(defaultVariables.size() + termVar.size()); - //设备触发但是没有指定条件,以下是内置的输出参数 - if (CollectionUtils.isEmpty(termVar) && trigger.getType() == TriggerType.device) { + //设备触发但是没有指定条件,或者其它触发类型,以下是内置的输出参数 + if (trigger.getType() != TriggerType.device || CollectionUtils.isEmpty(termVar)) { variables.add(Variable .of("_now", LocaleUtils.resolveMessage( @@ -113,22 +140,153 @@ public class SceneRule implements Serializable { } public Flux createVariables(List columns, + Integer branchIndex, Integer actionIndex, DeviceRegistry registry) { Flux variables = createSceneVariables(columns); //执行动作会输出的变量,串行执行才会生效 - if (!parallel && actionIndex != null && CollectionUtils.isNotEmpty(actions)) { + if (branchIndex == null && !parallel && actionIndex != null && CollectionUtils.isNotEmpty(actions)) { for (int i = 0; i < Math.min(actions.size(), actionIndex + 1); i++) { variables = variables.concatWith(actions.get(i).createVariables(registry, i)); } - } + //分支条件 + if (branchIndex != null && CollectionUtils.isNotEmpty(branches) && branches.size() > branchIndex) { + SceneConditionAction branch = branches.get(branchIndex); + List actionList; + if (branch.getThen() != null && !branch.getThen().isParallel() && + + CollectionUtils.isNotEmpty(actionList = branch.getThen().getActions())) { + + for (int i = 0; i < Math.min(actionList.size(), actionIndex + 1); i++) { + variables = variables.concatWith(actionList.get(i).createVariables(registry, i)); + } + + } + } + return variables .doOnNext(Variable::refactorPrefix); } + public Disposable createBranchHandler(Flux> sourceData, + BiFunction, Mono> output) { + if (CollectionUtils.isEmpty(branches)) { + return Disposables.disposed(); + } + + Function, Mono> last = null; + + Disposable.Composite disposable = Disposables.composite(); + int branchIndex = 0; + for (SceneConditionAction branch : branches) { + int _branchIndex = ++branchIndex; + //执行条件 + Function, Mono> filter = createFilter(branch.getWhen()); + //满足条件后的输出操作 + Function, Mono> out; + + SceneActions then = branch.getThen(); + //执行动作 + if (then != null && CollectionUtils.isNotEmpty(then.getActions())) { + + int size = then.getActions().size(); + //串行,只传递到第一个动作 + if (!then.isParallel() || size == 1) { + String nodeId = "branch_" + _branchIndex + "_action_1"; + out = data -> output.apply(nodeId, data); + } else { + //多个并行执行动作 + String[] nodeIds = new String[size]; + for (int i = 0; i < nodeIds.length; i++) { + nodeIds[0] = "branch_" + _branchIndex + "_action_" + (i + 1); + } + Flux nodeIdFlux = Flux.fromArray(nodeIds); + //并行 + out = data -> nodeIdFlux + .flatMap(nodeId -> output.apply(nodeId, data)) + .then(); + } + //防抖 + ShakeLimit shakeLimit = branch.getShakeLimit(); + if (shakeLimit != null && shakeLimit.isEnabled()) { + + Sinks.Many> sinks = Sinks + .many() + .unicast() + .onBackpressureBuffer(Queues.>unboundedMultiproducer().get()); + + //分组方式,比如设备触发时,应该按设备分组,每个设备都走独立的防抖策略 + ShakeLimitGrouping> grouping = createGrouping(); + + Function, Mono> handler = out; + + disposable.add( + shakeLimit + .transfer(sinks.asFlux(), + (duration, stream) -> + grouping + .group(stream)//先按自定义分组再按事件窗口进行分组 + .flatMap(group -> group.window(duration), Integer.MAX_VALUE), + (map, total) -> map.put("_total", total)) + .flatMap(handler) + .subscribe() + ); + //输出到sink进行防抖控制 + out = data -> { + sinks.emitNext(data, Reactors.emitFailureHandler()); + return Mono.empty(); + }; + } + } else { + out = ignore -> Mono.empty(); + } + + Function, Mono> fOut = out; + + + Function, Mono> handler = + data -> filter + .apply(data) + .flatMap(match -> { + // 满足条件后执行输出 + if (match) { + return fOut.apply(data).thenReturn(true); + } + return Reactors.ALWAYS_FALSE; + }); + + if (last == null) { + last = handler; + } else { + Function, Mono> _last = last; + + last = data -> _last + .apply(data) + .flatMap(match -> { + //上一个分支满足了则返回,不执行此分支逻辑 + if (match) { + return Reactors.ALWAYS_FALSE; + } + return handler.apply(data); + }); + } + } + //never happen + if (last == null) { + disposable.dispose(); + throw new IllegalArgumentException(); + } + + disposable.add( + sourceData.flatMap(last).subscribe() + ); + + return disposable; + } + public List createDefaultVariable() { return trigger != null ? trigger.createDefaultVariable() @@ -199,6 +357,49 @@ public class SceneRule implements Serializable { } } + //使用分支条件时 + if (CollectionUtils.isNotEmpty(branches)) { + int branchIndex = 0; + for (SceneConditionAction branch : branches) { + branchIndex++; + + SceneActions actions = branch.getThen(); + if (actions != null && CollectionUtils.isNotEmpty(actions.getActions())) { + int actionIndex = 1; + RuleNodeModel preNode = null; + SceneAction preAction = null; + for (SceneAction action : actions.getActions()) { + RuleNodeModel actionNode = new RuleNodeModel(); + actionNode.setId("branch_" + branchIndex + "_action_" + actionIndex); + actionNode.setName("条件_" + branchIndex + "_动作_" + actionIndex); + + action.applyNode(actionNode); + //串行 + if (!actions.isParallel()) { + //串行的时候 标记记录每一个动作的数据到header中,用于进行条件判断或者数据引用 + actionNode.addConfiguration(AbstractExecutionContext.RECORD_DATA_TO_HEADER, true); + actionNode.addConfiguration(AbstractExecutionContext.RECORD_DATA_TO_HEADER_KEY, actionNode.getId()); + + if (preNode != null) { + //上一个节点->当前动作节点 + RuleLink link = model.link(preNode, actionNode); + //设置上一个节点到此节点的输出条件 + if (CollectionUtils.isNotEmpty(preAction.getTerms())) { + link.setCondition(TermsConditionEvaluator.createCondition(preAction.getTerms())); + } + } + + preNode = actionNode; + } + + model.getNodes().add(actionNode); + preAction = action; + actionIndex++; + } + } + } + } + return model; } diff --git a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/scene/SceneTaskExecutorProvider.java b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/scene/SceneTaskExecutorProvider.java index 2c163481..acc5bab3 100644 --- a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/scene/SceneTaskExecutorProvider.java +++ b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/scene/SceneTaskExecutorProvider.java @@ -2,6 +2,7 @@ package org.jetlinks.community.rule.engine.scene; import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections4.CollectionUtils; import org.hswebframework.ezorm.rdb.executor.SqlRequest; import org.hswebframework.web.bean.FastBeanCopier; import org.hswebframework.web.id.IDGenerator; @@ -89,7 +90,9 @@ public class SceneTaskExecutorProvider implements TaskExecutorProvider { if (disposable != null) { disposable.dispose(); } - SqlRequest request = rule.createSql(); + boolean useBranch = CollectionUtils.isNotEmpty(rule.getBranches()); + + SqlRequest request = rule.createSql(!useBranch); //不是通过SQL来处理数据 if (request.isEmpty()) { diff --git a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/web/AlarmConfigController.java b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/web/AlarmConfigController.java index acef15ce..09985640 100755 --- a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/web/AlarmConfigController.java +++ b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/web/AlarmConfigController.java @@ -3,15 +3,13 @@ package org.jetlinks.community.rule.engine.web; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.Setter; import org.hswebframework.ezorm.rdb.mapping.ReactiveRepository; import org.hswebframework.web.authorization.annotation.Authorize; import org.hswebframework.web.authorization.annotation.QueryAction; import org.hswebframework.web.authorization.annotation.Resource; import org.hswebframework.web.authorization.annotation.SaveAction; import org.hswebframework.web.crud.service.ReactiveCrudService; -import org.hswebframework.web.crud.web.reactive.ReactiveServiceQueryController; +import org.hswebframework.web.crud.web.reactive.ReactiveServiceCrudController; import org.jetlinks.community.rule.engine.alarm.AlarmLevelInfo; import org.jetlinks.community.rule.engine.alarm.AlarmTargetSupplier; import org.jetlinks.community.rule.engine.entity.AlarmConfigEntity; @@ -29,8 +27,7 @@ import reactor.core.publisher.Mono; @Authorize @Tag(name = "告警配置") @AllArgsConstructor -public class AlarmConfigController implements ReactiveServiceQueryController { - +public class AlarmConfigController implements ReactiveServiceCrudController { private final AlarmConfigService alarmConfigService; private final ReactiveRepository alarmLevelRepository; @@ -89,5 +86,4 @@ public class AlarmConfigController implements ReactiveServiceQueryController queryAlarmLevel() { return alarmLevelRepository.findById(AlarmLevelService.DEFAULT_ALARM_ID); } - } diff --git a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/web/SceneController.java b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/web/SceneController.java index 84e47c4e..d0d4ca50 100644 --- a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/web/SceneController.java +++ b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/web/SceneController.java @@ -110,6 +110,7 @@ public class SceneController implements ReactiveServiceQueryController parseVariables(@RequestBody Mono ruleMono, + @RequestParam(required = false) Integer branch, @RequestParam(required = false) Integer action) { Mono cache = ruleMono.cache(); return Mono @@ -123,6 +124,7 @@ public class SceneController implements ReactiveServiceQueryController column.hasColumn(terms.keySet())) .map(column -> column.copyColumn(terms::containsKey)) .collect(Collectors.toList()), + branch, action, deviceRegistry); }) diff --git a/jetlinks-standalone/src/main/resources/application-dev.yml b/jetlinks-standalone/src/main/resources/application-dev.yml new file mode 100644 index 00000000..d2711189 --- /dev/null +++ b/jetlinks-standalone/src/main/resources/application-dev.yml @@ -0,0 +1,191 @@ +server: + port: 8850 + +spring: + redis: + host: 127.0.0.1 + port: 6380 + lettuce: + pool: + max-active: 1024 + timeout: 20s + serializer: jdk # 设置fst时,redis key使用string序列化,value使用 fst序列化. +# database: 3 + # max-wait: 10s + r2dbc: + # 需要手动创建数据库,启动会自动创建表,修改了配置easyorm相关配置也要修改 + url: r2dbc:postgresql://localhost:5433/jetlinks +# url: r2dbc:mysql://localhost:3306/jetlinks?ssl=false&serverZoneId=Asia/Shanghai # 修改了配置easyorm相关配置也要修改 + username: postgres + password: jetlinks + pool: + max-size: 32 + max-idle-time: 2m # 值不能大于mysql server的wait_timeout配置 + max-life-time: 10m + acquire-retry: 3 + reactor: + debug-agent: + enabled: false + elasticsearch: + uris: localhost:9201 + socket-timeout: 10s + connection-timeout: 15s + webclient: + max-in-memory-size: 100MB +easyorm: + default-schema: public # 数据库默认的schema + dialect: postgres #数据库方言 +elasticsearch: + embedded: + enabled: false # 为true时使用内嵌的elasticsearch,不建议在生产环境中使用 + data-path: ./data/elasticsearch + port: 9201 + host: 0.0.0.0 + index: + default-strategy: time-by-month #默认es的索引按月进行分表, direct则为直接操作索引. + settings: + number-of-shards: 1 # es 分片数量 + number-of-replicas: 0 # 副本数量 +device: + message: + writer: + time-series: + enabled: true #写出设备消息数据到elasticsearch +captcha: + enabled: false # 开启验证码 + ttl: 2m #验证码过期时间,2分钟 +hsweb: + cors: + enable: true + configs: + - path: /** + allowed-headers: "*" + allowed-methods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"] + allowed-origins: ["*"] +# allow-credentials: true + max-age: 1800 + dict: + enum-packages: org.jetlinks + file: + upload: + static-file-path: ./static/upload + static-location: http://192.168.32.65:8850/upload + webflux: + response-wrapper: + enabled: true #开启响应包装器(将返回值包装为ResponseMessage) + excludes: # 这下包下的接口不包装 + - org.springdoc + # auth: #默认的用户配置 + # users: + # admin: + # username: admin + # password: admin + # name: 超级管理员 + authorize: + auto-parse: true + permission: + filter: + enabled: true # 设置为true开启权限过滤,赋权时,不能赋予比自己多的权限. + exclude-username: admin # admin用户不受上述限制 + un-auth-strategy: ignore # error表示:发生越权时,抛出403错误. ignore表示会忽略越权的赋权. + cache: + type: none + redis: + local-cache-type: guava +file: + manager: + storage-base-path: ./data/files +api: + # 访问api接口的根地址 + base-path: http://127.0.0.1:${server.port} + +jetlinks: + server-id: ${spring.application.name}:${server.port} #设备服务网关服务ID,不同服务请设置不同的ID + logging: + system: + context: + server: ${spring.application.name} + protocol: + spi: + enabled: true # 为true时开启自动加载通过依赖引入的协议包 +logging: + level: + org.jetlinks: debug + rule.engine: debug + org.hswebframework: debug + org.springframework.transaction: debug + org.springframework.data.r2dbc.connectionfactory: warn + io.micrometer: warn + org.hswebframework.expands: error + system: debug + org.jetlinks.rule.engine: warn + org.jetlinks.supports.event: warn + org.springframework: warn + org.jetlinks.community.device.message.writer: warn + org.jetlinks.community.timeseries.micrometer: warn + org.jetlinks.community.elastic.search.service.reactive: trace + org.jetlinks.community.network: warn + io.vertx.mqtt.impl: warn + org.jetlinks.supports.scalecube.rpc: warn + "org.jetlinks.community.buffer": debug + # org.springframework.data.elasticsearch.client: trace + # org.elasticsearch: error + org.elasticsearch: error + org.elasticsearch.deprecation.search.aggregations.bucket.histogram: error + config: classpath:logback-spring.xml +vertx: + max-event-loop-execute-time-unit: seconds + max-event-loop-execute-time: 30 + max-worker-execute-time-unit: seconds + max-worker-execute-time: 30 + prefer-native-transport: true +micrometer: + time-series: + tags: + server: ${spring.application.name} + metrics: + default: + step: 30s +management: + health: + elasticsearch: + enabled: false # 关闭elasticsearch健康检查 +springdoc: + swagger-ui: + path: /swagger-ui.html + # packages-to-scan: org.jetlinks + group-configs: + - group: 设备管理相关接口 + packages-to-scan: + - org.jetlinks.community.device + paths-to-exclude: + - /device-instance/** + - /device-product/** + - /protocol/** + - group: 规则引擎相关接口 + packages-to-scan: org.jetlinks.community.rule.engine.web + paths-to-exclude: /api/** + - group: 通知管理相关接口 + packages-to-scan: org.jetlinks.community.notify.manager.web + - group: 设备接入相关接口 + packages-to-scan: + - org.jetlinks.community.network.manager.web + - org.jetlinks.community.device.web + paths-to-match: + - /gateway/** + - /network/** + - /protocol/** + - group: 系统管理相关接口 + packages-to-scan: + - org.jetlinks.community.auth + - org.hswebframework.web.system.authorization.defaults.webflux + - org.hswebframework.web.file + - org.hswebframework.web.authorization.basic.web + - org.jetlinks.community.logging.controller + cache: + disabled: false +network: + resources: + - 2883-2890 + - 18800-18810 + - 15060-15061 \ No newline at end of file