From 7fdfe87e9da178cfc06b3e57e72ac31e15ed5418 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=80=81=E5=91=A8?= Date: Fri, 25 Apr 2025 19:15:32 +0800 Subject: [PATCH] =?UTF-8?q?build:=202.3=20=E7=89=88=E6=9C=AC=E5=8F=91?= =?UTF-8?q?=E5=B8=83=20(#615)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * build: 2.3 版本发布 * feat: 更新网关及设备管理相关模块代码 (#614) Co-authored-by: fighter-wang <11291691+fighter-wang@user.noreply.gitee.com> * feat(community): 场景联动以及通知模块2.3相关功能更新 (#612) * feat(community): 场景联动以及通知模块2.3相关功能更新 * Update pull_request.yml * feat(community): 场景联动清除资产相关代码 * fix(场景联动): 修复场景初始化问题 * feat(community): 注册个人订阅通道 * fix(通知模块): 订阅通道初始化 * fix(通知模块): 删除无用类 * fix(community): 解决合并冲突 --------- Co-authored-by: fighter-wang <11291691+fighter-wang@user.noreply.gitee.com> Co-authored-by: 老周 * build: 升级依赖版本 * feat(community): 认证模块更新2.3相关功能 (#616) * feat(community): 认证模块更新2.3相关功能 * fix(community): 修复自定义查询报错的问题 * fix(community): 优化邮件附件名称获取 --------- Co-authored-by: fighter-wang <11291691+fighter-wang@user.noreply.gitee.com> * feat(community): 补充国际化相关内容,修复设备上线/离线没有日志的问题 (#619) * feat(community): 补充国际化相关内容,修复设备上线/离线没有日志的问题 * feat(community): 补充设备物模型映射接口 --------- Co-authored-by: fighter-wang <11291691+fighter-wang@user.noreply.gitee.com> * fix(notify): 修复消息通知相关问题 (#620) * fix(notify): 修复消息通知相关问题 * fix(notify): 修复订阅管理数据展示错误问题 --------- Co-authored-by: fighter-wang <11291691+fighter-wang@user.noreply.gitee.com> * fix(rule-engine): 场景联动相关bug修复 (#621) * fix(rule-engine): 场景联动相关bug修复 * fix(rule-engine): 场景联动相关bug修复 * fix(rule-engine): 场景联动相关bug修复 * fix(rule-engine): 修复程序启动时订阅提供商禁用,但用户已订阅仍加载成功并订阅的问题 --------- Co-authored-by: fighter-wang <11291691+fighter-wang@user.noreply.gitee.com> * fix(community): 修复认证中心以及设备相关的bug (#622) Co-authored-by: fighter-wang <11291691+fighter-wang@user.noreply.gitee.com> * feat(auth): 增加密码校验功能 (#625) Co-authored-by: fighter-wang <11291691+fighter-wang@user.noreply.gitee.com> * fix(auth): 修复组织父节点清除失败问题 (#627) * fix(auth): 修复组织父节点清除失败问题 * fix(store): 补充存储模式名称国际化方法 --------- Co-authored-by: fighter-wang <11291691+fighter-wang@user.noreply.gitee.com> * fix(auth): 修复用户绑定组织,查询用户关联组织失败的问题 (#628) Co-authored-by: fighter-wang <11291691+fighter-wang@user.noreply.gitee.com> * build: 升级依赖版本 --------- Co-authored-by: fighter-wang <118291973+fighter-wang@users.noreply.github.com> Co-authored-by: fighter-wang <11291691+fighter-wang@user.noreply.gitee.com> --- .../community/ConfigMetadataConstants.java | 7 + .../community/DynamicOperationType.java | 16 + .../org/jetlinks/community/Operation.java | 53 ++ .../org/jetlinks/community/OperationType.java | 11 + .../jetlinks/community/PropertyConstants.java | 67 ++- .../org/jetlinks/community/TimerSpec.java | 336 ++++++++++- .../FastSerializableAuthentication.java | 433 ++++++++++++++ .../community/authorize/OrgDimensionType.java | 22 + .../buffer/AbstractBufferEviction.java | 81 +++ .../community/buffer/BufferEviction.java | 133 +++++ .../community/buffer/BufferEvictionSpec.java | 55 ++ .../community/buffer/BufferProperties.java | 9 + .../community/buffer/BufferSettings.java | 95 +++- .../community/buffer/ConsumeStrategy.java | 10 + .../community/buffer/DiskFreeEviction.java | 59 ++ .../community/buffer/DiskUsageEviction.java | 59 ++ .../community/buffer/EvictionContext.java | 44 ++ .../community/buffer/PersistenceBuffer.java | 374 +++++++++++- .../community/buffer/SizeLimitEviction.java | 37 ++ .../community/command/CrudCommandSupport.java | 307 ++++++++++ .../StaticCommandSupportManagerProvider.java | 75 +++ .../CommandServiceEndpointRegister.java | 2 +- .../command/rule/RelievedAlarmCommand.java | 35 ++ .../command/rule/RuleCommandServices.java | 34 ++ .../command/rule/TriggerAlarmCommand.java | 38 ++ .../command/rule/data/AlarmInfo.java | 8 +- .../command/rule/data/RelieveInfo.java | 3 + .../configuration/CommonConfiguration.java | 9 +- .../event/OperationAssetProvider.java | 15 + .../event/OperationAssetProviders.java | 35 ++ .../jetlinks/community/event/SystemEvent.java | 77 +++ .../event/SystemEventDispatcher.java | 48 ++ .../community/event/SystemEventHandler.java | 22 + .../event/SystemEventHandlerRegister.java | 13 + .../community/event/SystemEventHolder.java | 54 ++ .../community/lock/DefaultReactiveLock.java | 296 ++++++++++ .../lock/DefaultReactiveLockManager.java | 20 + .../jetlinks/community/lock/ReactiveLock.java | 76 +++ .../community/lock/ReactiveLockHolder.java | 39 ++ .../community/lock/ReactiveLockManager.java | 20 + .../reactorql/term/ExistsTermSupport.java | 73 --- .../reactorql/term/FixedTermTypeSupport.java | 199 ++++++- .../reactorql/term/TermTypeSupport.java | 24 +- .../community/reactorql/term/TermTypes.java | 1 - .../community/reactorql/term/TermUtils.java | 120 ++++ .../community/reactorql/term/TermValue.java | 105 ++++ .../reference/DataReferenceManager.java | 17 +- .../reference/DataReferencedException.java | 13 + .../jetlinks/community/terms/I18nSpec.java | 10 +- .../terms/SubTableTermFragmentBuilder.java | 133 +++++ .../jetlinks/community/terms/TermSpec.java | 78 ++- .../org/jetlinks/community/topic/Topics.java | 168 ++++++ .../community/utils/ConverterUtils.java | 236 ++++++-- .../jetlinks/community/utils/FormatUtils.java | 44 ++ .../jetlinks/community/utils/TopicUtils.java | 344 +++++++++++ ...ot.autoconfigure.AutoConfiguration.imports | 1 + .../common-component/messages_en.properties | 123 +++- .../common-component/messages_zh.properties | 126 ++++- .../messages_en.properties | 1 + .../messages_zh.properties | 1 + .../ElasticSearchColumnModeDDLOperations.java | 1 + .../ElasticSearchRowModeDDLOperations.java | 5 + .../gateway/DeviceGatewayHelper.java | 36 +- .../supports/DeviceGatewayProperties.java | 39 +- .../DeviceGatewayPropertiesManager.java | 4 + .../network/NetworkConfigManager.java | 40 +- .../community/notify/SimpleProvider.java | 99 ++++ .../notify/StaticTemplateManager.java | 3 +- .../NotifierAutoConfiguration.java | 5 +- .../notify/enums/SubscriberTypeEnum.java | 39 ++ .../event/SerializableNotifierEvent.java | 7 +- .../notify/subscription/SubscribeType.java | 20 + .../template/AbstractTemplateManager.java | 42 +- .../email/embedded/DefaultEmailNotifier.java | 5 + .../notify/email/embedded/EmailTemplate.java | 4 + .../community/protocol/ProtocolDetail.java | 20 +- .../community/protocol/ProtocolInfo.java | 13 +- .../protocol/ProtocolSupportEntity.java | 138 +++++ .../ProtocolAutoConfiguration.java | 2 + .../service/LocalProtocolSupportService.java | 127 +++++ .../relation/impl/DefaultRelationManager.java | 43 +- .../relation/impl/SimpleObjectType.java | 14 + .../relation/impl/SimpleRelation.java | 23 +- .../rule/engine/commons/ShakeLimit.java | 42 ++ .../rule/engine/commons/ShakeLimitFlux.java | 318 +++++++++++ .../engine/commons/ShakeLimitProvider.java | 29 +- .../impl/SimpleShakeLimitProvider.java | 47 +- .../configuration/ThingsConfiguration.java | 16 +- .../NoneThingsDataRepositoryStrategy.java | 5 + .../ColumnModeDDLOperationsBase.java | 7 + .../things/data/operations/DDLOperations.java | 1 + .../things/data/operations/MetricBuilder.java | 109 +++- .../operations/RowModeDDLOperationsBase.java | 52 ++ .../things/holder/ThingsRegistryHolder.java | 15 + .../ThingsRegistryHolderInitializer.java | 11 + .../things/utils/ThingMetadataHelper.java | 43 ++ .../things/utils/ThingsDatabaseUtils.java | 254 +++++++++ .../community/things/utils/ThingsUtils.java | 52 ++ .../things-component/messages_en.properties | 8 + .../things-component/messages_zh.properties | 7 + .../auth/captcha/CaptchaController.java | 21 - .../auth/captcha/CaptchaProperties.java | 17 + .../auth/captcha/CaptchaProvider.java | 39 ++ .../auth/captcha/ValidationContext.java | 8 + .../captcha/impl/ImageCaptchaProvider.java | 122 ++++ .../community/auth/cipher/CipherConfig.java | 15 + .../community/auth/cipher/CipherHelper.java | 82 +++ .../auth/cipher/CipherProperties.java | 16 + .../CustomAuthenticationConfiguration.java | 9 + .../auth/configuration/MenuProperties.java | 1 + .../UserEntityTypeJSONDeerializer.java | 37 ++ .../auth/constant/AuthManagerConstants.java | 21 + .../auth/constants/AuthConstants.java | 14 + .../auth/dimension/BaseDimensionProvider.java | 49 +- .../auth/dimension/OrgDimensionType.java | 3 +- .../OrganizationDimensionProvider.java | 41 +- .../auth/dimension/RoleDimensionProvider.java | 8 +- .../UserAuthenticationEventPublisher.java | 93 +++ .../community/auth/entity/MenuBindEntity.java | 18 +- .../community/auth/entity/MenuButtonInfo.java | 14 +- .../community/auth/entity/MenuEntity.java | 14 +- .../auth/entity/OrganizationEntity.java | 32 +- .../auth/entity/OrganizationInfo.java | 43 +- .../community/auth/entity/PermissionInfo.java | 12 +- .../community/auth/entity/RoleDetailInfo.java | 45 ++ .../community/auth/entity/RoleEntity.java | 14 +- .../auth/entity/RoleGroupEntity.java | 24 +- .../community/auth/entity/RoleInfo.java | 21 +- .../auth/entity/ThirdPartyUserBindEntity.java | 6 +- .../community/auth/entity/UserDetail.java | 122 +++- .../auth/entity/UserDetailEntity.java | 106 +++- .../auth/enums/DefaultUserEntityType.java | 8 +- .../community/auth/enums/GenderEnum.java | 25 + .../community/auth/enums/RegisterEnum.java | 24 + .../MenuAuthenticationInitializeService.java | 24 +- .../initialize/PermissionCacheHelper.java | 89 +++ .../auth/login/UserLoginLogicInterceptor.java | 79 +-- .../auth/login/UserLoginProperties.java | 11 +- .../relation/UserRelationObjectProvider.java | 4 +- .../AuthorizationSettingDetailService.java | 78 ++- .../auth/service/DefaultMenuService.java | 125 +++- .../auth/service/MenuGrantService.java | 22 +- .../auth/service/OrganizationService.java | 79 ++- .../service/PasswordStrengthValidator.java | 65 +++ .../auth/service/RoleGroupService.java | 51 +- .../community/auth/service/RoleService.java | 34 +- .../auth/service/UserDetailService.java | 47 +- .../auth/service/info/UserLoginInfo.java | 65 +++ .../service/request/MenuGrantRequest.java | 17 +- .../service/terms/OrgUserTermBuilder.java | 85 +++ .../terms/OrganizationTreeChildTerm.java | 23 + .../service/terms/UserDetailTermBuilder.java | 119 +--- .../auth/utils/DimensionUserBindUtils.java | 66 ++- .../AuthorizationSettingDetailController.java | 9 +- .../community/auth/web/MenuController.java | 50 +- .../auth/web/OrganizationController.java | 41 +- .../auth/web/PermissionController.java | 19 +- .../community/auth/web/RoleController.java | 4 + .../auth/web/RoleGroupController.java | 6 +- .../auth/web/ThirdPartyUserController.java | 66 +-- .../auth/web/UserDetailController.java | 14 +- .../auth/web/WebFluxUserController.java | 15 + .../request/AuthorizationSettingDetail.java | 34 ++ .../web/response/RoleGroupDetailTree.java | 8 + .../messages_en.properties | 66 ++- .../messages_zh.properties | 69 ++- .../configuration/DeviceEventProperties.java | 15 + .../DeviceManagerConfiguration.java | 37 +- .../device/entity/DeviceCategoryEntity.java | 15 +- .../{response => entity}/DeviceDetail.java | 6 +- .../community/device/entity/DeviceEvent.java | 7 + .../device/entity/DeviceInstanceEntity.java | 45 +- .../device/entity/DeviceLatestData.java | 4 +- .../entity/DeviceMetadataMappingDetail.java | 76 +++ .../entity/DeviceOperationLogEntity.java | 4 +- .../device/entity/DeviceProductEntity.java | 55 +- .../device/entity/DevicePropertiesEntity.java | 18 +- .../device/entity/DeviceProperty.java | 10 + .../device/entity/DeviceSaveDetail.java | 80 +++ .../device/entity/DeviceTagEntity.java | 42 ++ .../device/entity/ProductDetail.java | 132 +++++ .../device/entity/ProtocolSupportEntity.java | 78 --- .../community/device/enums/DeviceFeature.java | 12 +- .../community/device/enums/DeviceLogType.java | 74 ++- .../device/enums/DeviceProductState.java | 13 +- .../community/device/enums/DeviceState.java | 6 +- .../community/device/enums/DeviceType.java | 14 +- .../device/enums/ValidateDataType.java | 170 ++++++ .../events/DeviceAutoRegisterEvent.java | 29 + .../events/DeviceProductDeployEvent.java | 15 + .../handler/DeviceProductDeployHandler.java | 16 +- .../events/handler/ValueTypeTranslator.java | 57 +- .../device/function/DeviceConfigFunction.java | 39 ++ .../device/function/DeviceEventFunction.java | 43 ++ .../function/DeviceMetadataEventFunction.java | 26 + .../function/DeviceMetadataFunction.java | 22 + .../DeviceMetadataFunctionFunction.java | 26 + .../DeviceMetadataPropertyFunction.java | 26 + .../device/function/DeviceStateFunction.java | 41 ++ .../device/function/DeviceTagFunction.java | 46 ++ .../device/function/DeviceTagsFunction.java | 73 +++ .../ReactorQLDeviceSelectorBuilder.java | 8 +- .../RelationDeviceSelectorProvider.java | 5 +- .../device/measurements/DeviceDashboard.java | 2 +- .../DeviceDashboardDefinition.java | 13 +- .../measurements/DeviceDashboardObject.java | 8 +- .../measurements/DeviceDynamicDashboard.java | 23 +- .../measurements/DeviceEventMeasurement.java | 10 +- .../measurements/DeviceEventsMeasurement.java | 22 +- .../measurements/DeviceObjectDefinition.java | 2 + .../DevicePropertiesMeasurement.java | 2 +- .../DevicePropertyMeasurement.java | 24 +- .../MetadataMeasurementDefinition.java | 6 +- .../message/DeviceMessageMeasurement.java | 52 +- .../DeviceMessageMeasurementProvider.java | 7 +- .../status/DeviceStatusChangeMeasurement.java | 83 ++- .../DeviceStatusMeasurementProvider.java | 53 +- .../status/DeviceStatusRecordMeasurement.java | 49 +- .../message/DefaultDeviceDataManager.java | 46 +- ...iceBatchOperationSubscriptionProvider.java | 5 +- ...eviceCurrentStateSubscriptionProvider.java | 20 +- .../message/DeviceMessageConnector.java | 84 +-- .../DeviceMessageSendLogInterceptor.java | 89 +-- ...DeviceMessageSendSubscriptionProvider.java | 19 +- .../DeviceMessageSubscriptionProvider.java | 23 +- .../TraceDeviceMessageSenderInterceptor.java | 45 ++ .../TransparentDeviceMessageConnector.java | 2 +- ...Jsr223TransparentMessageCodecProvider.java | 10 +- .../TimeSeriesMessageWriterConnector.java | 19 +- .../device/relation/DeviceObjectProvider.java | 14 +- .../response/DeviceAllInfoResponse.java | 69 --- .../community/device/response/DeviceInfo.java | 89 --- .../device/response/DeviceRunInfo.java | 30 - .../ResetDeviceConfigurationResult.java | 35 -- .../service/AutoDiscoverDeviceRegistry.java | 17 +- .../DefaultDeviceConfigMetadataManager.java | 107 ++-- .../DefaultDeviceConfigMetadataSupplier.java | 73 +-- .../device/service/DeviceCategoryService.java | 10 +- .../service/DeviceConfigMetadataManager.java | 86 ++- .../service/DeviceEntityEventHandler.java | 200 ++++++- .../service/DeviceMessageBusinessHandler.java | 258 +++++---- .../service/DeviceMetadataMappingService.java | 154 +++++ .../device/service/DeviceProductHandler.java | 74 ++- .../DeviceProductNameSynchronizer.java | 63 ++- .../device/service/DeviceTagHandler.java | 66 +-- .../service/LocalDeviceInstanceService.java | 411 +++++++++----- .../service/LocalDeviceProductService.java | 282 ++++++++- .../service/LocalProtocolSupportService.java | 55 -- .../device/service/ProductEventHandler.java | 94 +++ ...ProductReferenceDeviceGatewayProvider.java | 4 +- .../service/ProtocolSupportHandler.java | 2 +- .../data/DatabaseDeviceLatestDataService.java | 199 ++----- .../data/DeviceDataManagerSupport.java | 106 ++++ .../service/data/DeviceDataRepository.java | 36 ++ .../service/data/DeviceDataService.java | 220 ++++++-- .../data/DeviceDataStorageProperties.java | 46 ++ .../service/data/DeviceLatestDataService.java | 10 + .../data/DeviceThingsDataCustomizer.java | 49 +- .../StorageDeviceConfigMetadataSupplier.java | 97 +++- .../data/ThingsBridgingDeviceDataService.java | 75 ++- .../service/term/DeviceInstanceTerm.java | 10 +- .../service/term/DeviceLatestDataTerm.java | 104 ++++ .../device/service/term/DeviceTagTerm.java | 56 +- .../device/service/term/DeviceTypeTerm.java | 4 +- .../spi/DeviceConfigMetadataSupplier.java | 21 +- .../DeviceEventTimeSeriesMetadata.java | 27 +- .../DeviceLogTimeSeriesMetadata.java | 30 +- .../DevicePropertiesTimeSeriesMetadata.java | 49 +- .../timeseries/DeviceTimeSeriesMetadata.java | 4 +- .../timeseries/DeviceTimeSeriesMetric.java | 9 +- .../FixedPropertiesTimeSeriesMetadata.java | 32 +- .../device/utils/DeviceCacheUtils.java | 62 ++ .../device/web/DeviceCategoryController.java | 9 +- .../device/web/DeviceInstanceController.java | 50 +- .../device/web/DeviceMessageController.java | 131 +---- .../web/DeviceMetadataMappingController.java | 59 ++ .../device/web/DeviceProductController.java | 172 +++--- .../device/web/GatewayDeviceController.java | 36 +- .../device/web/ProtocolSupportController.java | 175 +++--- .../web/excel/DeviceExcelConstants.java | 7 +- .../device/web/excel/DeviceExcelImporter.java | 12 +- .../device/web/excel/DeviceExcelInfo.java | 122 ++-- .../device/web/excel/DeviceWrapper.java | 6 +- .../PropertyMetadataExcelImportInfo.java | 33 +- .../web/excel/PropertyMetadataExcelInfo.java | 124 ++-- .../excel/PropertyMetadataImportWrapper.java | 4 +- .../web/protocol/TransportSupportType.java | 3 + .../device/web/request/AggRequest.java | 8 + .../web/request/ProtocolDecodePayload.java | 10 +- .../web/request/ProtocolDecodeRequest.java | 2 +- .../web/request/ProtocolEncodePayload.java | 17 +- .../web/request/ProtocolEncodeRequest.java | 2 +- .../web/response/ChildrenDeviceInfo.java | 9 +- .../response/DeviceDeployResult.java | 12 +- .../web/response/GatewayDeviceInfo.java | 11 +- .../response/ImportDeviceInstanceResult.java | 8 +- .../device-manager/messages_en.properties | 145 ++++- .../device-manager/messages_zh.properties | 142 ++++- .../manager/entity/CertificateEntity.java | 32 +- .../manager/entity/DeviceGatewayEntity.java | 21 +- .../manager/entity/NetworkConfigEntity.java | 40 +- .../CertificateAuthenticationMethod.java | 19 + .../network/manager/enums/ConnectorState.java | 22 + .../manager/enums/NetworkConfigState.java | 16 +- .../manager/service/CertificateService.java | 18 +- .../service/DeviceGatewayConfigService.java | 32 +- .../service/DeviceGatewayEventHandler.java | 69 +-- .../manager/service/DeviceGatewayService.java | 2 - .../service/LocalNetworkConfigManager.java | 2 +- .../service/NetworkChannelProvider.java | 7 +- .../manager/service/NetworkConfigService.java | 17 +- .../service/NetworkEntityEventHandler.java | 3 +- .../ProtocolDataReferenceProvider.java | 6 +- .../DeviceSessionMeasurementProvider.java | 19 +- .../manager/web/CertificateController.java | 18 +- .../manager/web/DeviceGatewayController.java | 24 +- .../web/request/MqttMessageRequest.java | 7 + .../web/response/DeviceGatewayDetail.java | 24 +- .../manager/web/response/NetworkTypeInfo.java | 6 +- .../network-manager/messages_en.properties | 45 +- .../network-manager/messages_zh.properties | 65 ++- .../configuration/NotificationProperties.java | 22 + .../configuration/NotifyConfiguration.java | 2 +- .../NotifySubscriberProperties.java | 2 - .../SubscriptionConfiguration.java | 23 + .../notify/manager/entity/Notification.java | 2 +- .../manager/entity/NotificationEntity.java | 6 +- .../manager/entity/NotifyHistoryEntity.java | 12 +- .../entity/NotifySubscriberChannelEntity.java | 34 +- .../entity/NotifySubscriberEntity.java | 27 +- .../NotifySubscriberProviderEntity.java | 15 +- .../manager/enums/NotificationState.java | 4 +- .../notify/manager/enums/NotifyState.java | 8 +- .../message/NotificationsPublishProvider.java | 2 +- .../service/DefaultNotifyConfigManager.java | 2 +- .../service/DefaultTemplateManager.java | 11 +- .../InDatabaseNotifyHistoryRepository.java | 2 +- .../manager/service/NotificationService.java | 77 ++- .../manager/service/NotifierCacheManager.java | 16 +- .../manager/service/NotifyConfigService.java | 2 + .../notify/manager/service/NotifyHistory.java | 11 +- .../manager/service/NotifyHistoryService.java | 10 - .../NotifySubscriberProviderService.java | 90 +++ .../service/NotifySubscriberService.java | 104 ++-- .../service/NotifyTemplateService.java | 1 - .../notify/manager/subscriber/Subscriber.java | 11 + .../subscriber/SubscriberProvider.java | 53 +- .../channel/InsideMailChannelProvider.java | 6 +- .../channel/NotificationDispatcher.java | 10 +- .../providers/AlarmDeviceProvider.java | 13 +- .../providers/AlarmProductProvider.java | 13 +- .../subscriber/providers/AlarmProvider.java | 42 +- .../providers/AlarmSceneProvider.java | 11 +- .../manager/web/NotificationController.java | 116 +++- .../manager/web/NotifyChannelController.java | 133 ++--- ...ot.autoconfigure.AutoConfiguration.imports | 1 + .../notify-manager/messages_en.properties | 27 + .../notify-manager/messages_zh.properties | 24 + .../engine/alarm/AbstractAlarmTarget.java | 2 +- .../rule/engine/alarm/AlarmData.java | 2 + .../rule/engine/alarm/AlarmHandleInfo.java | 43 +- .../rule/engine/alarm/AlarmHandler.java | 1 + .../rule/engine/alarm/AlarmLevelInfo.java | 13 +- .../rule/engine/alarm/AlarmProperties.java | 25 + .../rule/engine/alarm/AlarmSceneHandler.java | 7 +- .../rule/engine/alarm/AlarmTarget.java | 4 + .../rule/engine/alarm/AlarmTargetInfo.java | 8 +- .../alarm/AlarmTaskExecutorProvider.java | 1 - .../engine/alarm/DefaultAlarmHandler.java | 534 +++++++++++++----- .../engine/alarm/DefaultAlarmRuleHandler.java | 90 +-- .../rule/engine/alarm/DeviceAlarmTarget.java | 35 +- .../rule/engine/alarm/ProductAlarmTarget.java | 42 +- .../rule/engine/alarm/SceneAlarmTarget.java | 23 +- .../rule/engine/cmd/AlarmCommandSupport.java | 34 ++ .../engine/cmd/AlarmConfigCommandSupport.java | 106 ++++ .../cmd/AlarmHistoryCommandSupport.java | 23 + .../engine/cmd/AlarmRecordCommandSupport.java | 46 ++ .../cmd/AlarmRuleBindCommandSupport.java | 15 + .../rule/engine/cmd/RuleCommandSupport.java | 33 ++ .../engine/cmd/RuleCommandSupportManager.java | 27 + .../rule/engine/cmd/SceneCommandSupport.java | 148 +++++ .../configuration/AlarmConfiguration.java | 15 + .../AlarmTargetConfiguration.java | 2 +- .../RuleEngineManagerConfiguration.java | 33 +- .../rule/engine/entity/AlarmConfigDetail.java | 3 - .../rule/engine/entity/AlarmConfigEntity.java | 1 + .../entity/AlarmHandleHistoryEntity.java | 105 +++- .../rule/engine/entity/AlarmHistoryInfo.java | 27 +- .../rule/engine/entity/AlarmLevelEntity.java | 1 - .../rule/engine/entity/AlarmRecordEntity.java | 33 +- .../engine/entity/RuleInstanceEntity.java | 14 +- .../rule/engine/enums/AlarmHandleState.java | 20 + .../rule/engine/enums/AlarmHandleType.java | 4 +- .../AlarmRecordMeasurementProvider.java | 11 +- .../AlarmRecordRankMeasurement.java | 15 +- .../AlarmRecordTrendMeasurement.java | 24 +- .../measurement/AlarmTimeSeriesMetric.java | 2 +- .../rule/engine/scene/DeviceOperation.java | 10 +- .../rule/engine/scene/SceneAction.java | 22 +- .../engine/scene/SceneActionProvider.java | 21 +- .../engine/scene/SceneConditionAction.java | 22 + .../rule/engine/scene/SceneRule.java | 63 ++- .../scene/SceneTaskExecutorProvider.java | 29 +- .../engine/scene/SceneTriggerProvider.java | 89 ++- .../rule/engine/scene/SceneUtils.java | 210 +++---- .../community/rule/engine/scene/Trigger.java | 8 +- .../community/rule/engine/scene/Variable.java | 2 + .../scene/internal/actions/AlarmAction.java | 18 +- .../internal/actions/AlarmActionProvider.java | 4 +- .../scene/internal/actions/DeviceAction.java | 17 +- .../actions/DeviceActionProvider.java | 5 + .../actions/DeviceDataActionProvider.java | 20 +- .../internal/triggers/DeviceTrigger.java | 36 +- .../triggers/DeviceTriggerProvider.java | 35 +- .../triggers/ManualTriggerProvider.java | 40 +- .../triggers/TimerTriggerProvider.java | 40 +- .../rule/engine/scene/term/TermColumn.java | 32 +- .../rule/engine/scene/value/TermValue.java | 4 + .../engine/service/AlarmConfigService.java | 6 +- .../service/AlarmHandleHistoryService.java | 2 - .../service/AlarmHandleTypeDictInit.java | 4 + .../engine/service/AlarmHistoryService.java | 55 +- .../ElasticSearchAlarmHistoryService.java | 35 +- .../service/LocalRuleInstanceRepository.java | 4 +- .../rule/engine/service/SceneService.java | 19 +- .../service/terms/AlarmBindRuleTerm.java | 25 +- .../engine/service/terms/AlarmRecordTerm.java | 48 ++ .../service/terms/RuleBindAlarmTerm.java | 1 - .../rule/engine/utils/TermColumnUtils.java | 13 +- .../engine/web/AlarmConfigController.java | 8 +- .../engine/web/AlarmHistoryController.java | 20 +- .../engine/web/AlarmRecordController.java | 70 ++- .../rule/engine/web/SceneController.java | 145 +---- .../rule/engine/web/SceneUtilsController.java | 91 +++ .../engine/web/response/SceneActionInfo.java | 2 +- .../web/response/SceneAggregationInfo.java | 10 +- .../engine/web/response/SceneRuleInfo.java | 7 +- .../engine/web/response/SceneTriggerInfo.java | 4 +- ...ot.autoconfigure.AutoConfiguration.imports | 3 +- .../messages_en.properties | 192 +++++-- .../messages_zh.properties | 188 ++++-- .../src/main/resources/application.yml | 2 +- pom.xml | 27 +- 443 files changed, 16775 insertions(+), 4112 deletions(-) create mode 100644 jetlinks-components/common-component/src/main/java/org/jetlinks/community/DynamicOperationType.java create mode 100644 jetlinks-components/common-component/src/main/java/org/jetlinks/community/Operation.java create mode 100644 jetlinks-components/common-component/src/main/java/org/jetlinks/community/OperationType.java create mode 100644 jetlinks-components/common-component/src/main/java/org/jetlinks/community/authorize/FastSerializableAuthentication.java create mode 100644 jetlinks-components/common-component/src/main/java/org/jetlinks/community/authorize/OrgDimensionType.java create mode 100644 jetlinks-components/common-component/src/main/java/org/jetlinks/community/buffer/AbstractBufferEviction.java create mode 100644 jetlinks-components/common-component/src/main/java/org/jetlinks/community/buffer/BufferEviction.java create mode 100644 jetlinks-components/common-component/src/main/java/org/jetlinks/community/buffer/BufferEvictionSpec.java create mode 100644 jetlinks-components/common-component/src/main/java/org/jetlinks/community/buffer/ConsumeStrategy.java create mode 100644 jetlinks-components/common-component/src/main/java/org/jetlinks/community/buffer/DiskFreeEviction.java create mode 100644 jetlinks-components/common-component/src/main/java/org/jetlinks/community/buffer/DiskUsageEviction.java create mode 100644 jetlinks-components/common-component/src/main/java/org/jetlinks/community/buffer/EvictionContext.java create mode 100644 jetlinks-components/common-component/src/main/java/org/jetlinks/community/buffer/SizeLimitEviction.java create mode 100644 jetlinks-components/common-component/src/main/java/org/jetlinks/community/command/CrudCommandSupport.java create mode 100644 jetlinks-components/common-component/src/main/java/org/jetlinks/community/command/StaticCommandSupportManagerProvider.java create mode 100644 jetlinks-components/common-component/src/main/java/org/jetlinks/community/command/rule/RelievedAlarmCommand.java create mode 100644 jetlinks-components/common-component/src/main/java/org/jetlinks/community/command/rule/RuleCommandServices.java create mode 100644 jetlinks-components/common-component/src/main/java/org/jetlinks/community/command/rule/TriggerAlarmCommand.java create mode 100644 jetlinks-components/common-component/src/main/java/org/jetlinks/community/event/OperationAssetProvider.java create mode 100644 jetlinks-components/common-component/src/main/java/org/jetlinks/community/event/OperationAssetProviders.java create mode 100644 jetlinks-components/common-component/src/main/java/org/jetlinks/community/event/SystemEvent.java create mode 100644 jetlinks-components/common-component/src/main/java/org/jetlinks/community/event/SystemEventDispatcher.java create mode 100644 jetlinks-components/common-component/src/main/java/org/jetlinks/community/event/SystemEventHandler.java create mode 100644 jetlinks-components/common-component/src/main/java/org/jetlinks/community/event/SystemEventHandlerRegister.java create mode 100644 jetlinks-components/common-component/src/main/java/org/jetlinks/community/event/SystemEventHolder.java create mode 100644 jetlinks-components/common-component/src/main/java/org/jetlinks/community/lock/DefaultReactiveLock.java create mode 100644 jetlinks-components/common-component/src/main/java/org/jetlinks/community/lock/DefaultReactiveLockManager.java create mode 100644 jetlinks-components/common-component/src/main/java/org/jetlinks/community/lock/ReactiveLock.java create mode 100644 jetlinks-components/common-component/src/main/java/org/jetlinks/community/lock/ReactiveLockHolder.java create mode 100644 jetlinks-components/common-component/src/main/java/org/jetlinks/community/lock/ReactiveLockManager.java delete mode 100644 jetlinks-components/common-component/src/main/java/org/jetlinks/community/reactorql/term/ExistsTermSupport.java create mode 100644 jetlinks-components/common-component/src/main/java/org/jetlinks/community/reactorql/term/TermUtils.java create mode 100644 jetlinks-components/common-component/src/main/java/org/jetlinks/community/reactorql/term/TermValue.java create mode 100644 jetlinks-components/common-component/src/main/java/org/jetlinks/community/terms/SubTableTermFragmentBuilder.java create mode 100644 jetlinks-components/common-component/src/main/java/org/jetlinks/community/utils/FormatUtils.java create mode 100644 jetlinks-components/common-component/src/main/java/org/jetlinks/community/utils/TopicUtils.java create mode 100644 jetlinks-components/common-component/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports create mode 100644 jetlinks-components/dashboard-component/src/main/resources/i18n/dashboard-component/messages_en.properties create mode 100644 jetlinks-components/dashboard-component/src/main/resources/i18n/dashboard-component/messages_zh.properties create mode 100644 jetlinks-components/notify-component/notify-core/src/main/java/org/jetlinks/community/notify/SimpleProvider.java create mode 100644 jetlinks-components/notify-component/notify-core/src/main/java/org/jetlinks/community/notify/enums/SubscriberTypeEnum.java create mode 100644 jetlinks-components/notify-component/notify-core/src/main/java/org/jetlinks/community/notify/subscription/SubscribeType.java create mode 100644 jetlinks-components/protocol-component/src/main/java/org/jetlinks/community/protocol/ProtocolSupportEntity.java create mode 100644 jetlinks-components/protocol-component/src/main/java/org/jetlinks/community/protocol/service/LocalProtocolSupportService.java create mode 100644 jetlinks-components/rule-engine-component/src/main/java/org/jetlinks/community/rule/engine/commons/ShakeLimitFlux.java create mode 100644 jetlinks-components/things-component/src/main/java/org/jetlinks/community/things/holder/ThingsRegistryHolder.java create mode 100644 jetlinks-components/things-component/src/main/java/org/jetlinks/community/things/holder/ThingsRegistryHolderInitializer.java create mode 100644 jetlinks-components/things-component/src/main/java/org/jetlinks/community/things/utils/ThingMetadataHelper.java create mode 100644 jetlinks-components/things-component/src/main/java/org/jetlinks/community/things/utils/ThingsDatabaseUtils.java create mode 100644 jetlinks-components/things-component/src/main/java/org/jetlinks/community/things/utils/ThingsUtils.java create mode 100644 jetlinks-components/things-component/src/main/resources/i18n/things-component/messages_en.properties create mode 100644 jetlinks-components/things-component/src/main/resources/i18n/things-component/messages_zh.properties create mode 100644 jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/captcha/CaptchaProvider.java create mode 100644 jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/captcha/ValidationContext.java create mode 100644 jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/captcha/impl/ImageCaptchaProvider.java create mode 100644 jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/cipher/CipherConfig.java create mode 100644 jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/cipher/CipherHelper.java create mode 100644 jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/cipher/CipherProperties.java create mode 100644 jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/configuration/UserEntityTypeJSONDeerializer.java create mode 100644 jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/constant/AuthManagerConstants.java create mode 100644 jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/constants/AuthConstants.java create mode 100644 jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/dimension/UserAuthenticationEventPublisher.java create mode 100644 jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/entity/RoleDetailInfo.java create mode 100644 jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/enums/GenderEnum.java create mode 100644 jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/enums/RegisterEnum.java create mode 100644 jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/initialize/PermissionCacheHelper.java create mode 100644 jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/service/PasswordStrengthValidator.java create mode 100644 jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/service/info/UserLoginInfo.java create mode 100644 jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/service/terms/OrgUserTermBuilder.java create mode 100644 jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/service/terms/OrganizationTreeChildTerm.java create mode 100644 jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/configuration/DeviceEventProperties.java rename jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/{response => entity}/DeviceDetail.java (97%) create mode 100644 jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/entity/DeviceMetadataMappingDetail.java create mode 100644 jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/entity/DeviceSaveDetail.java create mode 100644 jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/entity/ProductDetail.java delete mode 100644 jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/entity/ProtocolSupportEntity.java create mode 100644 jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/enums/ValidateDataType.java create mode 100644 jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/events/DeviceAutoRegisterEvent.java create mode 100644 jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/function/DeviceConfigFunction.java create mode 100644 jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/function/DeviceEventFunction.java create mode 100644 jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/function/DeviceMetadataEventFunction.java create mode 100644 jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/function/DeviceMetadataFunction.java create mode 100644 jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/function/DeviceMetadataFunctionFunction.java create mode 100644 jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/function/DeviceMetadataPropertyFunction.java create mode 100644 jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/function/DeviceStateFunction.java create mode 100644 jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/function/DeviceTagFunction.java create mode 100644 jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/function/DeviceTagsFunction.java create mode 100644 jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/message/TraceDeviceMessageSenderInterceptor.java delete mode 100644 jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/response/DeviceAllInfoResponse.java delete mode 100644 jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/response/DeviceInfo.java delete mode 100644 jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/response/DeviceRunInfo.java delete mode 100644 jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/response/ResetDeviceConfigurationResult.java create mode 100644 jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/service/DeviceMetadataMappingService.java delete mode 100644 jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/service/LocalProtocolSupportService.java create mode 100644 jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/service/ProductEventHandler.java create mode 100644 jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/service/data/DeviceDataManagerSupport.java create mode 100644 jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/service/data/DeviceDataRepository.java create mode 100644 jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/service/term/DeviceLatestDataTerm.java create mode 100644 jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/utils/DeviceCacheUtils.java create mode 100644 jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/web/DeviceMetadataMappingController.java rename jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/{ => web}/response/DeviceDeployResult.java (54%) rename jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/{ => web}/response/ImportDeviceInstanceResult.java (80%) create mode 100644 jetlinks-manager/network-manager/src/main/java/org/jetlinks/community/network/manager/enums/CertificateAuthenticationMethod.java create mode 100644 jetlinks-manager/network-manager/src/main/java/org/jetlinks/community/network/manager/enums/ConnectorState.java create mode 100644 jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/configuration/NotificationProperties.java create mode 100644 jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/configuration/SubscriptionConfiguration.java create mode 100644 jetlinks-manager/notify-manager/src/main/resources/i18n/notify-manager/messages_en.properties create mode 100644 jetlinks-manager/notify-manager/src/main/resources/i18n/notify-manager/messages_zh.properties create mode 100644 jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/alarm/AlarmProperties.java create mode 100644 jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/cmd/AlarmCommandSupport.java create mode 100644 jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/cmd/AlarmConfigCommandSupport.java create mode 100644 jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/cmd/AlarmHistoryCommandSupport.java create mode 100644 jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/cmd/AlarmRecordCommandSupport.java create mode 100644 jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/cmd/AlarmRuleBindCommandSupport.java create mode 100644 jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/cmd/RuleCommandSupport.java create mode 100644 jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/cmd/RuleCommandSupportManager.java create mode 100644 jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/cmd/SceneCommandSupport.java create mode 100644 jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/configuration/AlarmConfiguration.java create mode 100644 jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/enums/AlarmHandleState.java create mode 100644 jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/service/terms/AlarmRecordTerm.java create mode 100644 jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/web/SceneUtilsController.java diff --git a/jetlinks-components/common-component/src/main/java/org/jetlinks/community/ConfigMetadataConstants.java b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/ConfigMetadataConstants.java index 5341b4df..87705353 100644 --- a/jetlinks-components/common-component/src/main/java/org/jetlinks/community/ConfigMetadataConstants.java +++ b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/ConfigMetadataConstants.java @@ -1,6 +1,9 @@ package org.jetlinks.community; import org.jetlinks.core.config.ConfigKey; +import org.jetlinks.core.metadata.MergeOption; + +import java.util.Map; /** * 数据验证配置常量类 @@ -20,4 +23,8 @@ public interface ConfigMetadataConstants { ConfigKey format = ConfigKey.of("format", "格式", String.class); + ConfigKey defaultValue = ConfigKey.of("defaultValue", "默认值", String.class); + + ConfigKey indexEnabled = ConfigKey.of("indexEnabled", "开启索引", Boolean.TYPE); + } diff --git a/jetlinks-components/common-component/src/main/java/org/jetlinks/community/DynamicOperationType.java b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/DynamicOperationType.java new file mode 100644 index 00000000..9e91a562 --- /dev/null +++ b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/DynamicOperationType.java @@ -0,0 +1,16 @@ +package org.jetlinks.community; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor +public class DynamicOperationType implements OperationType { + private String id; + private String name; + +} diff --git a/jetlinks-components/common-component/src/main/java/org/jetlinks/community/Operation.java b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/Operation.java new file mode 100644 index 00000000..3ec070bf --- /dev/null +++ b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/Operation.java @@ -0,0 +1,53 @@ +package org.jetlinks.community; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.jetlinks.core.utils.SerializeUtils; + +import java.io.Externalizable; +import java.io.IOException; +import java.io.ObjectInput; +import java.io.ObjectOutput; + +/** + * 描述,用于对某些操作的通用描述. + * + * @author zhouhao + * @since 2.0 + */ +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor(staticName = "of") +public class Operation implements Externalizable { + /** + * 操作来源 + */ + private OperationSource source; + + /** + * 操作类型,比如: transparent-codec等 + */ + private OperationType type; + + @Override + public String toString() { + return type.getId() + "(" + type.getName() + "):[" + source.getId() + "]"; + } + + @Override + public void writeExternal(ObjectOutput out) throws IOException { + source.writeExternal(out); + SerializeUtils.writeObject(type.getId(), out); + SerializeUtils.writeObject(type.getName(), out); + } + + @Override + public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { + source = new OperationSource(); + source.readExternal(in); + type = new DynamicOperationType((String) SerializeUtils.readObject(in), (String) SerializeUtils.readObject(in)); + } +} diff --git a/jetlinks-components/common-component/src/main/java/org/jetlinks/community/OperationType.java b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/OperationType.java new file mode 100644 index 00000000..92b3b4ce --- /dev/null +++ b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/OperationType.java @@ -0,0 +1,11 @@ +package org.jetlinks.community; + +public interface OperationType { + String getId(); + + String getName(); + + static OperationType of(String id,String name){ + return new DynamicOperationType(id,name); + } +} diff --git a/jetlinks-components/common-component/src/main/java/org/jetlinks/community/PropertyConstants.java b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/PropertyConstants.java index 84cfd09f..804166c4 100644 --- a/jetlinks-components/common-component/src/main/java/org/jetlinks/community/PropertyConstants.java +++ b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/PropertyConstants.java @@ -1,10 +1,13 @@ package org.jetlinks.community; import lombok.Generated; +import org.hswebframework.web.id.IDGenerator; import org.jetlinks.core.config.ConfigKey; import org.jetlinks.core.message.HeaderKey; +import org.springframework.core.ResolvableType; import java.lang.reflect.Type; +import java.util.List; import java.util.Map; import java.util.Optional; import java.util.function.Supplier; @@ -13,32 +16,82 @@ import java.util.function.Supplier; * @author wangzheng * @since 1.0 */ +@Generated public interface PropertyConstants { + //机构ID Key orgId = Key.of("orgId"); + //设备名称 Key deviceName = Key.of("deviceName"); + //产品名称 Key productName = Key.of("productName"); + //产品ID Key productId = Key.of("productId"); - Key uid = Key.of("_uid"); - //设备创建者 - Key creatorId = Key.of("creatorId"); + + /** + * 关系信息.值格式: + *
{@code
+     * [{"type":"user","id":"userId","rel":"manager"}]
+     * }
+ */ + Key>> relations = Key.of("relations"); + + /** + * 租户ID + * + * @see org.jetlinks.pro.tenant.TenantMember + */ + Key> tenantId = Key.of("tenantId"); + + //分组ID + Key> groupId = Key.of("groupId"); + + //是否记录task记录 + Key useTask = Key.of("useTask", false); + + //taskId + Key taskId = Key.of("taskId"); + + //最大重试次数 + Key maxRetryTimes = Key.of("maxRetryTimes", () -> Long.getLong("device.message.task.retryTimes", 1), Long.class); + //当前重试次数 + Key retryTimes = Key.of("retryTimes", () -> 0L, Long.class); + + //服务ID + Key serverId = Key.of("serverId"); + + //全局唯一ID + Key uid = Key.of("_uid", IDGenerator.RANDOM::generate); //设备接入网关ID Key accessId = Key.of("accessId"); /** * 设备接入方式 - * @see org.jetlinks.community.gateway.supports.DeviceGatewayProvider#getId + * + * @see org.jetlinks.pro.gateway.supports.DeviceGatewayProvider#getId */ Key accessProvider = Key.of("accessProvider"); + //设备创建者 + Key creatorId = Key.of("creatorId"); + @SuppressWarnings("all") static Optional getFromMap(ConfigKey key, Map map) { return Optional.ofNullable((T) map.get(key.getKey())); } + @SuppressWarnings("all") + static T getFromMapOrElse(ConfigKey key, Map map, Supplier defaultIfEmpty) { + Object value = map.get(key.getKey()); + if (value == null) { + return defaultIfEmpty.get(); + } + return (T) value; + } + @Generated interface Key extends ConfigKey, HeaderKey { @@ -89,13 +142,14 @@ public interface PropertyConstants { @Override public T getDefaultValue() { - return defaultValue.get(); + return defaultValue == null ? null : defaultValue.get(); } }; } static Key of(String key, Supplier defaultValue, Type type) { return new Key() { + @Override public Type getValueType() { return type; @@ -108,10 +162,11 @@ public interface PropertyConstants { @Override public T getDefaultValue() { - return defaultValue.get(); + return defaultValue == null ? null : defaultValue.get(); } }; } + } } diff --git a/jetlinks-components/common-component/src/main/java/org/jetlinks/community/TimerSpec.java b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/TimerSpec.java index e91d52c8..285ef9d3 100644 --- a/jetlinks-components/common-component/src/main/java/org/jetlinks/community/TimerSpec.java +++ b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/TimerSpec.java @@ -15,7 +15,10 @@ import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; import org.apache.commons.collections4.CollectionUtils; +import org.hswebframework.ezorm.core.param.Term; import org.hswebframework.web.exception.ValidationException; +import org.hswebframework.web.i18n.LocaleUtils; +import org.jetlinks.community.terms.I18nSpec; import org.reactivestreams.Subscription; import org.springframework.util.Assert; import reactor.core.CoreSubscriber; @@ -35,6 +38,7 @@ import java.util.*; import java.util.concurrent.TimeUnit; import java.util.function.Function; import java.util.function.Predicate; +import java.util.stream.Collectors; @Getter @Setter @@ -45,6 +49,9 @@ public class TimerSpec implements Serializable { @NotNull private Trigger trigger; + @Schema(description = "使用日程标签进行触发") + private Set scheduleTags; + //Cron表达式 @Schema(description = "触发方式为[cron]时不能为空") private String cron; @@ -58,9 +65,15 @@ public class TimerSpec implements Serializable { @Schema(description = "执行模式为[period]时不能为空") private Period period; + @Schema(description = "执行模式为[period]时不能与period同时为空") + private List periods; + @Schema(description = "执行模式为[once]时不能为空") private Once once; + @Schema(description = "组合触发配置列表") + private Multi multi; + public static TimerSpec cron(String cron) { TimerSpec spec = new TimerSpec(); spec.cron = cron; @@ -68,6 +81,17 @@ public class TimerSpec implements Serializable { return spec; } + public List periods() { + List list = new ArrayList<>(1); + if (periods != null) { + list.addAll(periods); + } + if (period != null) { + list.add(period); + } + return list; + } + public Predicate createRangeFilter() { if (CollectionUtils.isEmpty(when)) { return ignore -> true; @@ -82,14 +106,25 @@ public class TimerSpec implements Serializable { public Predicate createTimeFilter() { Predicate range = createRangeFilter(); - //周期执行指定了to,表示只在时间范围段内执行 if (mod == ExecuteMod.period) { - LocalTime to = period.toLocalTime(); - LocalTime from = period.fromLocalTime(); - Predicate predicate - = time -> time.toLocalTime().compareTo(from) >= 0; - if (to != null) { - predicate = predicate.and(time -> time.toLocalTime().compareTo(to) <= 0); + Predicate predicate = null; + //可能多个周期 + for (Period period : periods()) { + //周期执行指定了to,表示只在时间范围段内执行 + LocalTime to = period.toLocalTime(); + LocalTime from = period.fromLocalTime(); + Predicate _predicate = time -> !time.toLocalTime().isBefore(from); + if (to != null) { + _predicate = _predicate.and(time -> !time.toLocalTime().isAfter(to)); + } + if (predicate == null) { + predicate = _predicate; + } else { + predicate = _predicate.or(predicate); + } + } + if (predicate == null) { + return range; } return predicate.and(range); } @@ -232,6 +267,13 @@ public class TimerSpec implements Serializable { exception.addSuppressed(e); throw exception; } + } else if (trigger == Trigger.multi) { + List multiSpec = multi.getSpec(); + if (CollectionUtils.isNotEmpty(multiSpec)) { + for (TimerSpec spec : multiSpec) { + spec.validate(); + } + } } else { nextDurationBuilder().apply(ZonedDateTime.now()); } @@ -280,6 +322,21 @@ public class TimerSpec implements Serializable { } + @Getter + @Setter + public static class Multi implements Serializable { + private static final long serialVersionUID = 1L; + + @Schema(description = "组合触发配置列表") + private List spec; + + @Schema(description = "多个触发的关系。and、or") + private Term.Type type = Term.Type.or; + + @Schema(description = "组合触发时,只触发一次的最小时间间隔") + private int timeSpanSecond = 2; + } + private static LocalTime parsTime(String time) { return LocalTime.parse(time); } @@ -370,11 +427,40 @@ public class TimerSpec implements Serializable { } private TimerIterable periodIterable() { - Assert.notNull(period, "period can not be null"); + List periods = periods(); + Assert.notEmpty(periods, "period or periods can not be null"); Predicate filter = createTimeFilter(); - Duration duration = Duration.of(period.every, period.unit.temporal); - LocalTime time = period.fromLocalTime(); + Duration _Duration = null; + LocalTime earliestFrom = LocalTime.MAX; + LocalTime latestTo = LocalTime.MIN; + + //使用最小的执行周期进行判断? + for (Period period : periods) { + Duration duration = Duration.of(period.every, period.unit.temporal); + LocalTime from = period.fromLocalTime(); + LocalTime to = period.toLocalTime(); + + // 更新最小的duration + if (_Duration == null || duration.compareTo(_Duration) < 0) { + _Duration = duration; + } + + // 更新最早的起始时间 + if (from != null && from.isBefore(earliestFrom)) { + earliestFrom = from; + } + + // 更新最晚的结束时间 + if (to != null && to.isAfter(latestTo)) { + latestTo = to; + } + } + + Duration duration = _Duration; + LocalTime firstFrom = earliestFrom.equals(LocalTime.MAX) ? LocalTime.MIDNIGHT : earliestFrom; + LocalTime endTo = latestTo.equals(LocalTime.MIN) ? null : latestTo; + return baseTime -> new Iterator() { ZonedDateTime current = baseTime; @@ -389,6 +475,21 @@ public class TimerSpec implements Serializable { int max = MAX_IT_TIMES; do { dateTime = dateTime.plus(duration); + // 检查时间是否在 firstFrom 和 endTo 之间 + LocalTime time = dateTime.toLocalTime(); + if (time.isBefore(firstFrom) || time.isAfter(endTo)) { + // 获取第二天的 firstFrom + ZonedDateTime nextDayFromTime = dateTime.toLocalDate().plusDays(1).atTime(firstFrom).atZone(dateTime.getZone()); + + // 计算当前时间到 nextDayFromTime 的差值 + Duration timeDifference = Duration.between(dateTime, nextDayFromTime); + + // 计算可以整除的 duration 数量 + long n = timeDifference.toMillis() / duration.toMillis(); + + // 跳转到下一个 n * duration 的时间点 + dateTime = dateTime.plus(n * duration.toMillis(), ChronoUnit.MILLIS); + } if (filter.test(dateTime.toLocalDateTime())) { break; } @@ -432,7 +533,110 @@ public class TimerSpec implements Serializable { }; } + private TimerIterable multiSpecIterable() { + List multiSpec = multi.getSpec(); + Assert.notEmpty(multiSpec, "multiSpec can not be empty"); + return baseTime -> new Iterator() { + final List timeList = new ArrayList<>(multiSpec.size()); + + final List> iterators = multiSpec + .stream() + .map(spec -> spec.iterable().iterator(baseTime)) + .collect(Collectors.toList()); + + @Override + public boolean hasNext() { + switch (multi.getType()) { + case and: + return iterators.stream().allMatch(Iterator::hasNext); + case or: + return iterators.stream().anyMatch(Iterator::hasNext); + default: + return false; + } + } + + @Override + public ZonedDateTime next() { + switch (multi.getType()) { + case and: + return handleNextAnd(); + case or: + return handleNextOr(); + default: + return baseTime; + } + } + + private ZonedDateTime handleNextAnd() { + ZonedDateTime dateTime = null; + int max = MAX_IT_TIMES; + int match = 0; + do { + for (Iterator iterator : iterators) { + ZonedDateTime next = iterator.next(); + if (dateTime == null) { + dateTime = next; + } + // 若生成的时间比当前选中的时间早,则继续生成 + while (next.isBefore(dateTime)) { + next = iterator.next(); + } + if (next.isEqual(dateTime)) { + // 所有定时器的next时间一致时,返回时间 + if (++match == iterators.size()) { + return dateTime; + } + } else { + dateTime = next; + } + } + max--; + } while ( + max > 0 + ); + return dateTime; + } + + private ZonedDateTime handleNextOr() { + ZonedDateTime earliest = null; + // 每个定时器生成next + fillTimeList(); + + // 获取最早的一个时间 + for (ZonedDateTime zonedDateTime : timeList) { + if (earliest == null || earliest.isAfter(zonedDateTime) || earliest.isEqual(zonedDateTime)) { + earliest = zonedDateTime; + } + } + // 清空被选中的最早时间 + for (int i = 0; i < timeList.size(); i++) { + if (timeList.get(i).isEqual(earliest)) { + timeList.set(i, null); + } + } + return earliest; + } + + /** + * 遍历所有定时器,若有next为空的则生成新的 + */ + private void fillTimeList() { + for (int i = 0; i < iterators.size(); i++) { + if (timeList.size() <= i) { + timeList.add(iterators.get(i).next()); + } else if (timeList.get(i) == null) { + timeList.set(i, iterators.get(i).next()); + } + } + } + }; + } + public TimerIterable iterable() { + if (trigger == Trigger.multi) { + return multiSpecIterable(); + } if ((trigger == Trigger.cron || trigger == null) && cron != null) { return cronIterable(); } @@ -448,7 +652,6 @@ public class TimerSpec implements Serializable { return timeList; } - public Flux flux() { return flux(Schedulers.parallel()); } @@ -457,9 +660,111 @@ public class TimerSpec implements Serializable { return new TimerFlux(nextDurationBuilder(), scheduler); } + @Override + public String toString() { + if (getTrigger() == null) { + return null; + } + switch (getTrigger()) { + case week: { + return weekDesc(); + } + case month: { + return monthDesc(); + } + case cron: { + return getCron(); + } + } + return null; + } + + private String weekDesc() { + I18nSpec spec = new I18nSpec(); + spec.setCode("message.timer_spec_desc"); + List args = new ArrayList<>(); + if (when == null || when.isEmpty()) { + args.add(I18nSpec.of("message.timer_spec_desc_everyday", "每天")); + } else { + String week = when + .stream() + .map(weekNum -> LocaleUtils.resolveMessage("message.timer_spec_desc_week_" + weekNum)) + .collect(Collectors.joining(LocaleUtils.resolveMessage("message.timer_spec_desc_seperator"))); + args.add(I18nSpec.of( + "message.timer_spec_desc_everyweek", + "每周" + week, + week)); + } + args.add(timerModDesc()); + spec.setArgs(args); + return spec.resolveI18nMessage(); + } + + private String monthDesc() { + I18nSpec spec = new I18nSpec(); + spec.setCode("message.timer_spec_desc"); + List args = new ArrayList<>(); + if (when == null || when.isEmpty()) { + args.add(I18nSpec.of("message.timer_spec_desc_everyday", "每天")); + } else { + String month = when + .stream() + .map(monthNum -> { + switch (monthNum) { + case 1: + return LocaleUtils.resolveMessage("message.timer_spec_desc_month_1", monthNum); + case 2: + return LocaleUtils.resolveMessage("message.timer_spec_desc_month_2", monthNum); + case 3: + return LocaleUtils.resolveMessage("message.timer_spec_desc_month_3", monthNum); + default: + return LocaleUtils.resolveMessage("message.timer_spec_desc_month", monthNum); + } + }) + .collect(Collectors.joining(LocaleUtils.resolveMessage("message.timer_spec_desc_seperator"))); + args.add(I18nSpec.of( + "message.timer_spec_desc_everymonth", + "每月" + month, + month)); + } + args.add(timerModDesc()); + spec.setArgs(args); + return spec.resolveI18nMessage(); + } + + private I18nSpec timerModDesc() { + switch (getMod()) { + case period: { + if (getPeriod() == null) { + break; + } + return I18nSpec.of( + "message.timer_spec_desc_period", + getPeriod().getFrom() + "-" + getPeriod().getTo() + + " 每" + getPeriod().getEvery() + getPeriod().getUnit().name(), + I18nSpec.of("message.timer_spec_desc_period_duration", + getPeriod().getFrom() + "-" + getPeriod().getTo(), + getPeriod().getFrom(), + getPeriod().getTo()), + getPeriod().getEvery(), + I18nSpec.of("message.timer_spec_desc_period_" + getPeriod().getUnit().name(), + getPeriod().getUnit().name()) + ); + } + case once: { + // [time],执行1次 + if (getOnce() == null) { + break; + } + return I18nSpec.of("message.timer_spec_desc_period_once", getOnce().getTime(), getOnce().getTime()); + } + } + return I18nSpec.of("", ""); + } + @AllArgsConstructor static class TimerFlux extends Flux { - final Function spec; + final Function spec; final Scheduler scheduler; @Override @@ -530,9 +835,14 @@ public class TimerSpec implements Serializable { } public enum Trigger { + //按周 week, + //按月 month, - cron + //cron表达式 + cron, + // 多个触发组合 + multi } public enum ExecuteMod { diff --git a/jetlinks-components/common-component/src/main/java/org/jetlinks/community/authorize/FastSerializableAuthentication.java b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/authorize/FastSerializableAuthentication.java new file mode 100644 index 00000000..250efe77 --- /dev/null +++ b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/authorize/FastSerializableAuthentication.java @@ -0,0 +1,433 @@ +package org.jetlinks.community.authorize; + +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONArray; +import com.alibaba.fastjson.JSONObject; +import com.google.common.collect.Collections2; +import com.google.common.collect.Maps; +import com.google.common.collect.Sets; +import lombok.AllArgsConstructor; +import lombok.Setter; +import lombok.SneakyThrows; +import org.apache.commons.lang3.StringUtils; +import org.hswebframework.web.authorization.*; +import org.hswebframework.web.authorization.simple.*; +import org.hswebframework.web.bean.FastBeanCopier; +import org.jetlinks.core.metadata.Jsonable; +import org.jetlinks.core.utils.RecyclerUtils; +import org.jetlinks.core.utils.SerializeUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.*; +import java.util.*; +import java.util.function.Function; +import java.util.stream.Collectors; + +@Setter +public class FastSerializableAuthentication extends SimpleAuthentication + implements Externalizable, Jsonable { + + private static final Logger log = LoggerFactory.getLogger(FastSerializableAuthentication.class); + + static { + SerializeUtils.registerSerializer( + 0x90, + FastSerializableAuthentication.class, + (ignore) -> new FastSerializableAuthentication()); + } + + public static void load() { + + } + + @SuppressWarnings("all") + public static Authentication of(Object jsonOrObject, boolean share) { + if (jsonOrObject == null) { + return null; + } + //json + if (jsonOrObject instanceof String) { + return of((String) jsonOrObject, share); + } + // map + if (jsonOrObject instanceof Map) { + FastSerializableAuthentication fast = new FastSerializableAuthentication(); + fast.shared = share; + fast.fromJson(new JSONObject((Map) jsonOrObject)); + return fast; + } + //auth + if (jsonOrObject instanceof Authentication) { + return of(((Authentication) jsonOrObject)); + } + throw new UnsupportedOperationException(); + } + + public static Authentication of(String json, boolean share) { + if (StringUtils.isEmpty(json)) { + return null; + } + FastSerializableAuthentication fast = new FastSerializableAuthentication(); + fast.shared = share; + fast.fromJson(JSON.parseObject(json)); + return fast; + } + + + public static Authentication of(Authentication auth) { + return of(auth, false); + } + + public static Authentication of(Authentication auth, boolean simplify) { + if (auth instanceof FastSerializableAuthentication) { + ((FastSerializableAuthentication) auth).simplify = simplify; + return auth; + } + FastSerializableAuthentication fast = new FastSerializableAuthentication(); + fast.setUser(auth.getUser()); + fast.setSimplify(simplify); + fast.getPermissions().addAll(auth.getPermissions()); + fast.getDimensions().addAll(auth.getDimensions()); + fast.getAttributes().putAll(auth.getAttributes()); + return fast; + } + + @Override + protected FastSerializableAuthentication newInstance() { + return new FastSerializableAuthentication(); + } + + /** + * 是否简化,为true时,不序列化权限名称 + * + * @see Permission#getName() + */ + private boolean simplify = false; + + private transient boolean shared; + + public void makeShared() { + shared = true; + List dimensions = getDimensions() + .stream() + .map(RecyclerUtils::intern) + .collect(Collectors.toList()); + setDimensions(dimensions); + } + + @Override + public void writeExternal(ObjectOutput out) throws IOException { + String userId = null; + try { + out.writeByte(0x01); + //是否简化模式 + out.writeBoolean(simplify); + + //user + User user = getUser(); + out.writeUTF(user.getId() == null ? "" : (userId = user.getId())); + SerializeUtils.writeNullableUTF(user.getName(), out); + out.writeUTF(user.getUsername() == null ? "" : user.getUsername()); + SerializeUtils.writeNullableUTF(user.getUserType(), out); + SerializeUtils.writeKeyValue(user.getOptions(), out); + + //permission + { + List permissions = getPermissions(); + if (permissions == null) { + permissions = Collections.emptyList(); + } + out.writeInt(permissions.size()); + for (Permission permission : permissions) { + write(permission, out); + } + } + //dimension + { + List dimensions = getDimensions(); + if (dimensions == null) { + dimensions = Collections.emptyList(); + } + out.writeInt(dimensions.size()); + for (Dimension permission : dimensions) { + write(permission, out); + } + } + + SerializeUtils.writeKeyValue(getAttributes(), out); + + } catch (Throwable e) { + log.warn("write FastSerializableAuthentication [{}] error", userId, e); + throw e; + } + } + + @Override + public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { + byte version = in.readByte(); + simplify = in.readBoolean(); + + //user + SimpleUser user = new SimpleUser(); + user.setId(in.readUTF()); + user.setName(SerializeUtils.readNullableUTF(in)); + user.setUsername(in.readUTF()); + user.setUserType(SerializeUtils.readNullableUTF(in)); + user.setOptions(SerializeUtils.readMap(in, Maps::newHashMapWithExpectedSize)); + + setUser0(user); + + //permission + { + int size = in.readInt(); + List permissions = new ArrayList<>(size); + for (int i = 0; i < size; i++) { + Permission permission = readPermission(in); + permissions.add(permission); + } + setPermissions(permissions); + } + //dimension + { + int size = in.readInt(); + Set dimensions = new HashSet<>(size); + for (int i = 0; i < size; i++) { + Dimension dimension = readDimension(in); + dimensions.add(dimension); + } + setDimensions(dimensions); + } + + setAttributes(SerializeUtils.readMap(in, Maps::newHashMapWithExpectedSize)); + } + + @SneakyThrows + private Dimension readDimension(ObjectInput in) { + SimpleDimension dimension = new SimpleDimension(); + dimension.setId(in.readUTF()); + dimension.setName(in.readUTF()); + + dimension.setOptions(SerializeUtils.readMap( + in, + k -> RecyclerUtils.intern(String.valueOf(k)), + Function.identity(), + Maps::newHashMapWithExpectedSize)); + + boolean known = in.readBoolean(); + if (known) { + KnownDimension knownDimension = KnownDimension.ALL[in.readByte()]; + dimension.setType(knownDimension.type); + } else { + SimpleDimensionType type = new SimpleDimensionType(); + type.setId(in.readUTF()); + type.setName(in.readUTF()); + dimension.setType(RecyclerUtils.intern(type)); + } + return dimension; + } + + + @SneakyThrows + private void write(Dimension dimension, ObjectOutput out) { + out.writeUTF(dimension.getId()); + out.writeUTF(dimension.getName() == null ? "" : dimension.getName()); + + SerializeUtils.writeKeyValue(dimension.getOptions(), out); + + KnownDimension knownDimension = KnownDimension.MAPPING.get(dimension.getType().getId()); + + out.writeBoolean(knownDimension != null); + if (knownDimension != null) { + out.writeByte(knownDimension.ordinal()); + } else { + out.writeUTF(dimension.getType().getId()); + out.writeUTF(dimension.getType().getName()); + } + + } + + + @SneakyThrows + private Permission readPermission(ObjectInput in) { + SimplePermission permission = new SimplePermission(); + permission.setId(in.readUTF()); + if (!simplify) { + permission.setName(in.readUTF()); + } else { + permission.setName(permission.getId()); + } + permission.setOptions(SerializeUtils.readMap(in, Maps::newHashMapWithExpectedSize)); + + int actionSize = in.readUnsignedShort(); + Set actions = Sets.newHashSetWithExpectedSize(actionSize); + for (int i = 0; i < actionSize; i++) { + if (in.readBoolean()) { + actions.add(KnownAction.ALL[in.readByte()].action); + } else { + actions.add(in.readUTF()); + } + } + permission.setActions(actions); + return permission; + } + + @SneakyThrows + private void write(Permission permission, ObjectOutput out) { + out.writeUTF(permission.getId()); + if (!simplify) { + out.writeUTF(permission.getName() == null ? "" : permission.getName()); + } + + SerializeUtils.writeKeyValue(permission.getOptions(), out); + Set actions = permission.getActions(); + out.writeShort(actions.size()); + + for (String action : actions) { + KnownAction knownAction = KnownAction.ACTION_MAP.get(action); + out.writeBoolean(knownAction != null); + if (null != knownAction) { + out.writeByte(knownAction.ordinal()); + } else { + out.writeUTF(action); + } + } + } + + @AllArgsConstructor + enum KnownDimension { + user(DefaultDimensionType.user), + role(DefaultDimensionType.role), + org(OrgDimensionType.org), + parentOrg(OrgDimensionType.parentOrg); + private final DimensionType type; + static final KnownDimension[] ALL = values(); + static final Map MAPPING = new HashMap<>(); + + static { + for (KnownDimension value : ALL) { + MAPPING.put(value, value); + MAPPING.put(value.ordinal(), value); + MAPPING.put(value.name(), value); + } + } + } + + enum KnownAction { + query, + get, + update, + save, + delete, + export, + _import(Permission.ACTION_IMPORT), + enable, + disable; + static final KnownAction[] ALL = values(); + static final Map ACTION_MAP = new HashMap<>(); + + static { + for (KnownAction value : ALL) { + ACTION_MAP.put(value, value); + ACTION_MAP.put(value.ordinal(), value); + ACTION_MAP.put(value.action, value); + } + } + + private final String action; + + KnownAction() { + this.action = name(); + } + + KnownAction(String action) { + this.action = action; + } + } + + @Override + public JSONObject toJson() { + JSONObject obj = new JSONObject(); + obj.put("user", SerializeUtils.convertToSafelySerializable(getUser())); + obj.put("permissions", SerializeUtils.convertToSafelySerializable(getPermissions())); + //忽略user + obj.put("dimensions", SerializeUtils.convertToSafelySerializable( + Collections2.filter(getDimensions(), i -> !(i instanceof User)) + )); + obj.put("attributes", new HashMap<>(getAttributes())); + + return obj; + } + + @Override + public void fromJson(JSONObject json) { + JSONObject user = json.getJSONObject("user"); + if (user != null) { + setUser(user.toJavaObject(SimpleUser.class)); + } + JSONArray permissions = json.getJSONArray("permissions"); + if (permissions != null) { + for (int i = 0, size = permissions.size(); i < size; i++) { + JSONObject permission = permissions.getJSONObject(i); + //不再支持 + permission.remove("dataAccesses"); + + Object actions = permission.remove("actions"); + SimplePermission perm = permission.toJavaObject(SimplePermission.class); + + if (actions instanceof Collection) { + @SuppressWarnings("all") + Collection _actions = (Collection) actions; + Set acts = Sets.newHashSetWithExpectedSize(_actions.size()); + for (Object action : _actions) { + KnownAction act = KnownAction.ACTION_MAP.get(action); + if (act == null) { + acts.add(String.valueOf(action)); + } else { + acts.add(act.action); + } + } + perm.setActions(acts); + } + + getPermissions().add(shared ? RecyclerUtils.intern(perm) : perm); + } + } + JSONArray dimensions = json.getJSONArray("dimensions"); + + if (dimensions != null) { + for (int i = 0, size = dimensions.size(); i < size; i++) { + JSONObject dimension = dimensions.getJSONObject(i); + Object type = dimension.remove("type"); + if (type == null) { + continue; + } + SimpleDimension simpleDimension = dimension.toJavaObject(SimpleDimension.class); + if (type instanceof DimensionType) { + simpleDimension.setType((DimensionType) type); + } else { + KnownDimension knownDimension = KnownDimension.MAPPING.get(type); + if (knownDimension != null) { + simpleDimension.setType(knownDimension.type); + } else { + SimpleDimensionType dimensionType; + if (type instanceof String) { + dimensionType = SimpleDimensionType.of(String.valueOf(type)); + } else { + dimensionType = FastBeanCopier.copy(type, new SimpleDimensionType()); + } + if (StringUtils.isNoneEmpty(dimensionType.getId())) { + simpleDimension.setType(shared ? RecyclerUtils.intern(dimensionType) : dimensionType); + } + } + } + getDimensions().add(shared ? RecyclerUtils.intern(simpleDimension) : simpleDimension); + } + } + JSONObject attr = json.getJSONObject("attributes"); + if (attr != null) { + getAttributes().putAll(Maps.transformValues(attr, Serializable.class::cast)); + } + + } +} diff --git a/jetlinks-components/common-component/src/main/java/org/jetlinks/community/authorize/OrgDimensionType.java b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/authorize/OrgDimensionType.java new file mode 100644 index 00000000..8484b6f3 --- /dev/null +++ b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/authorize/OrgDimensionType.java @@ -0,0 +1,22 @@ +package org.jetlinks.community.authorize; + +import lombok.AllArgsConstructor; +import lombok.Generated; +import lombok.Getter; +import org.hswebframework.web.authorization.DimensionType; + +/** + * @author wangzheng + * @since 1.0 + */ +@AllArgsConstructor +@Getter +@Generated +public enum OrgDimensionType implements DimensionType { + org("org","组织"), + parentOrg("parentOrg","上级组织"); + + private final String id; + private final String name; + +} diff --git a/jetlinks-components/common-component/src/main/java/org/jetlinks/community/buffer/AbstractBufferEviction.java b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/buffer/AbstractBufferEviction.java new file mode 100644 index 00000000..f7aca626 --- /dev/null +++ b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/buffer/AbstractBufferEviction.java @@ -0,0 +1,81 @@ +package org.jetlinks.community.buffer; + +import org.jetlinks.community.Operation; +import org.jetlinks.community.OperationSource; +import org.jetlinks.community.OperationType; +import org.jetlinks.community.event.SystemEventHolder; +import org.jetlinks.community.utils.TimeUtils; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; +import java.util.concurrent.atomic.AtomicLongFieldUpdater; + +public abstract class AbstractBufferEviction implements BufferEviction { + + public static final OperationType OPERATION_TYPE = OperationType.of("buffer-eviction", "缓冲区数据丢弃"); + + private static final AtomicLongFieldUpdater + LAST_EVENT_TIME = AtomicLongFieldUpdater.newUpdater(AbstractBufferEviction.class, "lastEventTime"); + private static final AtomicIntegerFieldUpdater + LAST_TIMES = AtomicIntegerFieldUpdater.newUpdater(AbstractBufferEviction.class, "lastTimes"); + + //最大事件推送频率 + //可通过java -Djetlinks.buffer.eviction.event.max-interval=10m修改配置 + private static final long MAX_EVENT_INTERVAL = + TimeUtils.parse(System.getProperty("jetlinks.buffer.eviction.event.max-interval", "10m")).toMillis(); + + private volatile long lastEventTime; + private volatile int lastTimes; + + abstract boolean doEviction(EvictionContext context); + + @Override + public boolean tryEviction(EvictionContext context) { + if (doEviction(context)) { + sendEvent(context); + return true; + } + return false; + } + + private String operationCode() { + return getClass().getSimpleName(); + } + + private void sendEvent(EvictionContext context) { + long now = System.currentTimeMillis(); + long time = LAST_EVENT_TIME.get(this); + //记录事件推送周期内总共触发了多少次 + LAST_TIMES.incrementAndGet(this); + + //超过间隔事件则推送事件,防止推送太多错误事件 + if (now - time > MAX_EVENT_INTERVAL) { + LAST_EVENT_TIME.set(this, now); + Map info = new HashMap<>(); + + //缓冲区数量 + info.put("bufferSize", context.size(EvictionContext.BufferType.buffer)); + //死数据数量 + info.put("deadSize", context.size(EvictionContext.BufferType.dead)); + //总计触发次数 + info.put("times", LAST_TIMES.getAndSet(this, 0)); + + //应用自定义的数据,比如磁盘剩余空间等信息 + applyEventData(info); + + //推送系统事件 + SystemEventHolder.warn( + Operation.of( + OperationSource.of(context.getName(), "eviction"), + OPERATION_TYPE), + operationCode(), + info + ); + } + } + + protected void applyEventData(Map data) { + + } +} diff --git a/jetlinks-components/common-component/src/main/java/org/jetlinks/community/buffer/BufferEviction.java b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/buffer/BufferEviction.java new file mode 100644 index 00000000..2536e1f0 --- /dev/null +++ b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/buffer/BufferEviction.java @@ -0,0 +1,133 @@ +package org.jetlinks.community.buffer; + +import org.springframework.util.unit.DataSize; + +import java.io.File; + +/** + * 缓存淘汰策略 + * + * @author zhouhao + * @since 2.0 + */ +public interface BufferEviction { + + BufferEviction NONE = new BufferEviction() { + @Override + public boolean tryEviction(EvictionContext context) { + return false; + } + + @Override + public String toString() { + return "None"; + } + }; + + /** + * 根据磁盘使用率来进行淘汰,磁盘使用率超过阈值时则淘汰旧数据 + * + * @param path 文件路径 + * @param threshold 使用率阈值 范围为0-1 .如: 0.8 表示磁盘使用率超过80%则丢弃数据 + * @return 淘汰策略 + */ + static BufferEviction disk(String path, float threshold) { + return new DiskUsageEviction(new File(path), threshold); + } + + /** + * 根据磁盘可用空间来进行淘汰,磁盘剩余空间低于阈值时则淘汰旧数据 + * + * @param path 文件路径 + * @param minUsableDataSize 磁盘最小可用空间阈值,当磁盘可用空间低于此值时则则淘汰旧数据 + * @return 淘汰策略 + */ + static BufferEviction disk(String path, DataSize minUsableDataSize) { + return new DiskFreeEviction(new File(path), minUsableDataSize.toBytes()); + } + + /** + * 根据缓冲区数量来淘汰数据,当数量超过指定阈值后则淘汰旧数据 + * + * @param bufferLimit 数量阈值 + * @return 淘汰策略 + */ + static BufferEviction limit(long bufferLimit) { + return limit(bufferLimit, bufferLimit); + } + + /** + * 根据缓冲区数量来淘汰数据,当数量超过指定阈值后则淘汰旧数据 + * + * @param bufferLimit 缓冲数量阈值 + * @param deadLimit 死数据数量阈值 + * @return 淘汰策略 + */ + static BufferEviction limit(long bufferLimit, long deadLimit) { + return new SizeLimitEviction(bufferLimit, deadLimit); + } + + /** + * 根据缓冲区数量来淘汰死数据 + * + * @param deadLimit 死数据数量阈值 + * @return 淘汰策略 + */ + static BufferEviction deadLimit(long deadLimit) { + return new SizeLimitEviction(-1, deadLimit); + } + + /** + * 尝试执行淘汰 + * + * @param context 上下文 + * @return 是否有数据被淘汰 + */ + boolean tryEviction(EvictionContext context); + + /** + * 组合另外一个淘汰策略,2个策略同时执行. + * + * @param after 后续策略 + * @return 淘汰策略 + */ + default BufferEviction and(BufferEviction after) { + BufferEviction self = this; + return new BufferEviction() { + @Override + public boolean tryEviction(EvictionContext context) { + return self.tryEviction(context) & after.tryEviction(context); + } + + @Override + public String toString() { + return self + " and " + after; + } + }; + } + + /** + * 组合另外一个淘汰策略,当前策略淘汰了数据才执行另外一个策略 + * + * @param after 后续策略 + * @return 淘汰策略 + */ + default BufferEviction then(BufferEviction after) { + BufferEviction self = this; + return new BufferEviction() { + @Override + public boolean tryEviction(EvictionContext context) { + if (self.tryEviction(context)) { + after.tryEviction(context); + return true; + } + return false; + } + + @Override + public String toString() { + return self + " then " + after; + } + }; + } +} diff --git a/jetlinks-components/common-component/src/main/java/org/jetlinks/community/buffer/BufferEvictionSpec.java b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/buffer/BufferEvictionSpec.java new file mode 100644 index 00000000..75073246 --- /dev/null +++ b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/buffer/BufferEvictionSpec.java @@ -0,0 +1,55 @@ +package org.jetlinks.community.buffer; + +import lombok.Getter; +import lombok.Setter; +import org.springframework.util.unit.DataSize; + +@Getter +@Setter +public class BufferEvictionSpec { + + public static final BufferEviction DEFAULT = new BufferEvictionSpec().build(); + + //最大队列数量,超过则淘汰最旧的数据 + private int maxSize = -1; + + //最大死信数量,超过则淘汰dead数据 + private int maxDeadSize = Integer.getInteger("jetlinks.buffer.dead.limit", 100_0000); + + //根据磁盘空间淘汰数据 + private DataSize diskFree = DataSize.parse(System.getProperty("jetlinks.buffer.disk.free.threshold", "4GB")); + + //磁盘最大使用率 + private float diskThreshold; + + //判断磁盘空间大小的目录 + private String diskPath = System.getProperty("jetlinks.buffer.disk.free.path", "./"); + + public BufferEviction build() { + + BufferEviction + eviction = null, + size = BufferEviction.limit(maxSize, maxDeadSize), + disk = null; + + if (diskThreshold > 0) { + disk = BufferEviction.disk(diskPath, diskThreshold); + } else if (diskFree != null) { + disk = BufferEviction.disk(diskPath, diskFree); + } + + if (disk != null) { + eviction = disk; + } + + if (eviction == null) { + eviction = size; + } else { + eviction = eviction.then(size); + } + + return eviction; + } + + +} diff --git a/jetlinks-components/common-component/src/main/java/org/jetlinks/community/buffer/BufferProperties.java b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/buffer/BufferProperties.java index 46f04ba5..08c12181 100644 --- a/jetlinks-components/common-component/src/main/java/org/jetlinks/community/buffer/BufferProperties.java +++ b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/buffer/BufferProperties.java @@ -23,6 +23,15 @@ public class BufferProperties { //最大重试次数,超过此次数的数据将会放入死队列. private long maxRetryTimes = 64; + //文件操作的最大并行度,默认为1,不建议设置超过4. + private int fileConcurrency = 1; + + //消费策略 默认先进先出 + private ConsumeStrategy strategy = ConsumeStrategy.FIFO; + + //淘汰策略 + private BufferEvictionSpec eviction = new BufferEvictionSpec(); + public boolean isExceededRetryCount(int count) { return maxRetryTimes > 0 && count >= maxRetryTimes; } diff --git a/jetlinks-components/common-component/src/main/java/org/jetlinks/community/buffer/BufferSettings.java b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/buffer/BufferSettings.java index 82afff31..f44d957e 100644 --- a/jetlinks-components/common-component/src/main/java/org/jetlinks/community/buffer/BufferSettings.java +++ b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/buffer/BufferSettings.java @@ -10,19 +10,18 @@ import org.springframework.transaction.CannotCreateTransactionException; import java.io.IOException; import java.time.Duration; import java.util.Objects; +import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.TimeoutException; import java.util.function.Predicate; -/** - * @author zhouhao - * @since 2.0 - */ @Getter @AllArgsConstructor public class BufferSettings { private static final Predicate DEFAULT_RETRY_WHEN_ERROR = e -> ErrorUtils.hasException(e, IOException.class, + IllegalStateException.class, + RejectedExecutionException.class, TimeoutException.class, DataAccessResourceFailureException.class, CannotCreateTransactionException.class, @@ -32,10 +31,17 @@ public class BufferSettings { return DEFAULT_RETRY_WHEN_ERROR; } + public static BufferEviction defaultEviction(){ + return BufferEvictionSpec.DEFAULT; + } + private final String filePath; private final String fileName; + //缓存淘汰策略 + private final BufferEviction eviction; + private final Predicate retryWhenError; //缓冲区大小,超过此大小将执行 handler 处理逻辑 @@ -50,16 +56,23 @@ public class BufferSettings { //最大重试次数,超过此次数的数据将会放入死队列. private final long maxRetryTimes; + private final int fileConcurrency; + + private final ConsumeStrategy strategy; public static BufferSettings create(String filePath, String fileName) { return new BufferSettings( filePath, fileName, + defaultEviction(), + //默认重试逻辑 defaultRetryWhenError(), 1000, Duration.ofSeconds(1), Math.max(1, Runtime.getRuntime().availableProcessors() / 2), - 5); + 5, + 1, + ConsumeStrategy.FIFO); } public static BufferSettings create(BufferProperties properties) { @@ -70,64 +83,122 @@ public class BufferSettings { return create(properties.getFilePath(), fileName).properties(properties); } - public BufferSettings bufferSize(int bufferSize) { + public BufferSettings eviction(BufferEviction eviction) { return new BufferSettings(filePath, fileName, + eviction, retryWhenError, bufferSize, bufferTimeout, parallelism, - maxRetryTimes); + maxRetryTimes, + fileConcurrency, + strategy); + } + + public BufferSettings bufferSize(int bufferSize) { + return new BufferSettings(filePath, + fileName, + eviction, + retryWhenError, + bufferSize, + bufferTimeout, + parallelism, + maxRetryTimes, + fileConcurrency, + strategy); } public BufferSettings bufferTimeout(Duration bufferTimeout) { return new BufferSettings(filePath, fileName, + eviction, retryWhenError, bufferSize, bufferTimeout, parallelism, - maxRetryTimes); + maxRetryTimes, + fileConcurrency, + strategy); } public BufferSettings parallelism(int parallelism) { return new BufferSettings(filePath, fileName, + eviction, retryWhenError, bufferSize, bufferTimeout, parallelism, - maxRetryTimes); + maxRetryTimes, + fileConcurrency, + strategy); } public BufferSettings maxRetry(int maxRetryTimes) { return new BufferSettings(filePath, fileName, + eviction, retryWhenError, bufferSize, bufferTimeout, parallelism, - maxRetryTimes); + maxRetryTimes, + fileConcurrency, + strategy); } public BufferSettings retryWhenError(Predicate retryWhenError) { return new BufferSettings(filePath, fileName, + eviction, Objects.requireNonNull(retryWhenError), bufferSize, bufferTimeout, parallelism, - maxRetryTimes); + maxRetryTimes, + fileConcurrency, + strategy); + } + + public BufferSettings fileConcurrency(int fileConcurrency) { + return new BufferSettings(filePath, + fileName, + eviction, + retryWhenError, + bufferSize, + bufferTimeout, + parallelism, + maxRetryTimes, + fileConcurrency, + strategy); + } + + public BufferSettings strategy(ConsumeStrategy strategy) { + return new BufferSettings(filePath, + fileName, + eviction, + retryWhenError, + bufferSize, + bufferTimeout, + parallelism, + maxRetryTimes, + fileConcurrency, + strategy); } public BufferSettings properties(BufferProperties properties) { return new BufferSettings(filePath, fileName, + properties.getEviction().build(), Objects.requireNonNull(retryWhenError), properties.getSize(), properties.getTimeout(), properties.getParallelism(), - properties.getMaxRetryTimes()); + properties.getMaxRetryTimes(), + properties.getFileConcurrency(), + properties.getStrategy() + ); } diff --git a/jetlinks-components/common-component/src/main/java/org/jetlinks/community/buffer/ConsumeStrategy.java b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/buffer/ConsumeStrategy.java new file mode 100644 index 00000000..abafd2ae --- /dev/null +++ b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/buffer/ConsumeStrategy.java @@ -0,0 +1,10 @@ +package org.jetlinks.community.buffer; + +public enum ConsumeStrategy { + + // 先进先出 + FIFO, + + // 后进先出 + LIFO +} diff --git a/jetlinks-components/common-component/src/main/java/org/jetlinks/community/buffer/DiskFreeEviction.java b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/buffer/DiskFreeEviction.java new file mode 100644 index 00000000..cd03f643 --- /dev/null +++ b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/buffer/DiskFreeEviction.java @@ -0,0 +1,59 @@ +package org.jetlinks.community.buffer; + + +import org.jetlinks.community.utils.FormatUtils; +import org.springframework.util.unit.DataSize; + +import java.io.File; +import java.util.Map; + +class DiskFreeEviction extends AbstractBufferEviction { + + private final File path; + private final long minUsableBytes; + + public DiskFreeEviction(File path, long minUsableBytes) { + this.path = path; + this.minUsableBytes = minUsableBytes; + } + + private volatile long usableSpace = -1; + private volatile long lastUpdateTime; + + @Override + public boolean doEviction(EvictionContext context) { + tryUpdate(); + + if (freeOutOfThreshold()) { + context.removeOldest(EvictionContext.BufferType.buffer); + return true; + } + return false; + } + + protected boolean freeOutOfThreshold() { + return usableSpace != -1 && usableSpace <= minUsableBytes; + } + + private void tryUpdate() { + long now = System.currentTimeMillis(); + //1秒更新一次 + if (now - lastUpdateTime <= 1000) { + return; + } + usableSpace = path.getUsableSpace(); + lastUpdateTime = now; + } + + @Override + protected void applyEventData(Map data) { + data.put("usableSpace", DataSize.ofBytes(usableSpace).toMegabytes()); + data.put("minUsableBytes", DataSize.ofBytes(minUsableBytes).toMegabytes()); + } + + @Override + public String toString() { + return "DiskFree(path=" + path + + ",space=" + FormatUtils.formatDataSize(usableSpace) + "/" + FormatUtils.formatDataSize(minUsableBytes) + ")"; + } +} diff --git a/jetlinks-components/common-component/src/main/java/org/jetlinks/community/buffer/DiskUsageEviction.java b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/buffer/DiskUsageEviction.java new file mode 100644 index 00000000..487802dd --- /dev/null +++ b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/buffer/DiskUsageEviction.java @@ -0,0 +1,59 @@ +package org.jetlinks.community.buffer; + + +import java.io.File; +import java.util.Map; + +class DiskUsageEviction extends AbstractBufferEviction { + + private final File path; + private final float threshold; + + public DiskUsageEviction(File path, float threshold) { + this.path = path; + this.threshold = threshold; + } + + private volatile float usage; + private volatile long lastUpdateTime; + + @Override + public boolean doEviction(EvictionContext context) { + tryUpdate(); + + if (freeOutOfThreshold()) { + context.removeOldest(EvictionContext.BufferType.buffer); + return true; + } + return false; + } + + protected boolean freeOutOfThreshold() { + return usage >= threshold; + } + + private void tryUpdate() { + long now = System.currentTimeMillis(); + //1秒更新一次 + if (now - lastUpdateTime <= 1000) { + return; + } + long total = path.getTotalSpace(); + long usable = path.getUsableSpace(); + + usage = (float) ((total - usable) / (double) total); + lastUpdateTime = now; + } + + @Override + protected void applyEventData(Map data) { + data.put("usage", String.format("%.2f%%", usage * 100)); + } + + @Override + public String toString() { + return "DiskUsage(path=" + path + + ", threshold=" + String.format("%.2f%%", threshold * 100) + + ", usage=" + String.format("%.2f%%", usage * 100) + ")"; + } +} diff --git a/jetlinks-components/common-component/src/main/java/org/jetlinks/community/buffer/EvictionContext.java b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/buffer/EvictionContext.java new file mode 100644 index 00000000..afa19659 --- /dev/null +++ b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/buffer/EvictionContext.java @@ -0,0 +1,44 @@ +package org.jetlinks.community.buffer; + +/** + * 缓冲淘汰上下文 + * + * @author zhouhao + * @since 2.0 + */ +public interface EvictionContext { + + /** + * 获取指定类型的数据量 + * + * @param type 类型 + * @return 数据量 + */ + long size(BufferType type); + + /** + * 删除最新的数据 + * + * @param type 类型 + */ + void removeLatest(BufferType type); + + /** + * 删除最旧的数据 + * + * @param type 类型 + */ + void removeOldest(BufferType type); + + /** + * @return 缓冲区名称, 用于区分多个不同的缓冲区 + */ + String getName(); + + enum BufferType { + //缓冲区 + buffer, + //死数据 + dead + } +} diff --git a/jetlinks-components/common-component/src/main/java/org/jetlinks/community/buffer/PersistenceBuffer.java b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/buffer/PersistenceBuffer.java index a12c0e55..8fb119d7 100644 --- a/jetlinks-components/common-component/src/main/java/org/jetlinks/community/buffer/PersistenceBuffer.java +++ b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/buffer/PersistenceBuffer.java @@ -5,14 +5,16 @@ import io.netty.buffer.*; import io.netty.util.ReferenceCountUtil; import lombok.AllArgsConstructor; import lombok.Getter; +import lombok.RequiredArgsConstructor; import lombok.SneakyThrows; import org.apache.commons.lang3.exception.ExceptionUtils; import org.h2.mvstore.WriteBuffer; import org.h2.mvstore.type.BasicDataType; -import org.jetlinks.community.codec.Serializers; import org.jetlinks.core.cache.FileQueue; import org.jetlinks.core.cache.FileQueueProxy; import org.jetlinks.core.utils.SerializeUtils; +import org.jetlinks.community.codec.Serializers; +import org.jetlinks.community.utils.FormatUtils; import org.reactivestreams.Subscription; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -21,9 +23,15 @@ import reactor.core.publisher.BaseSubscriber; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.core.publisher.SignalType; +import reactor.core.scheduler.Scheduler; +import reactor.core.scheduler.Schedulers; import javax.annotation.Nonnull; +import javax.management.MBeanServer; +import javax.management.ObjectName; +import javax.management.StandardMBean; import java.io.*; +import java.lang.management.ManagementFactory; import java.lang.reflect.Array; import java.nio.ByteBuffer; import java.nio.file.Path; @@ -38,6 +46,7 @@ import java.util.function.BiFunction; import java.util.function.Function; import java.util.function.Predicate; import java.util.function.Supplier; +import java.util.stream.Collectors; /** * 支持持久化的缓存批量操作工具,用于支持数据的批量操作,如批量写入数据到数据库等. @@ -46,11 +55,13 @@ import java.util.function.Supplier; * *
{@code
  *
- *    BufferWriter writer = BufferWriter
+ *    PersistenceBuffer writer = PersistenceBuffer
  *    .create(
  *       "./data/buffer", //文件目录
  *      "my-data.queue", //文件名
+ *      Data::new,
  *      buffer->{
+ *           // 返回false表示不重试
  *           return saveData(buffer);
  *      })
  *    .bufferSize(1000)//缓冲大小,当缓冲区超过此数量时将会立即执行写出操作.
@@ -66,9 +77,9 @@ import java.util.function.Supplier;
  *
  * @param  数据类型,需要实现Serializable接口
  * @author zhouhao
- * @since pro 2.0
+ * @since 2.0
  */
-public class PersistenceBuffer implements Disposable {
+public class PersistenceBuffer implements EvictionContext, Disposable {
     @SuppressWarnings("all")
     private final static AtomicIntegerFieldUpdater WIP =
         AtomicIntegerFieldUpdater.newUpdater(PersistenceBuffer.class, "wip");
@@ -125,11 +136,16 @@ public class PersistenceBuffer implements Disposable {
     //刷新缓冲区定时任务
     private Disposable intervalFlush;
 
+    //独立的读写调度器
+    private Scheduler writer, reader;
+
     private Throwable lastError;
 
     private volatile Boolean disposed = false;
     private boolean started = false;
 
+    private final PersistenceBufferMBeanImpl monitor = new PersistenceBufferMBeanImpl<>(this);
+
     public PersistenceBuffer(String filePath,
                              String fileName,
                              Supplier newInstance,
@@ -228,6 +244,7 @@ public class PersistenceBuffer implements Disposable {
                               .name(fileName)
                               .path(path)
                               .option("valueType", dataType)
+                              .option("concurrency", settings.getFileConcurrency())
                               .build());
         this.remainder = queue.size();
         //死队列,用于存放失败的数据
@@ -238,8 +255,27 @@ public class PersistenceBuffer implements Disposable {
                                   .option("valueType", dataType)
                                   .build());
         this.deadSize = this.deadQueue.size();
-
         this.buffer = newBuffer();
+        initScheduler();
+        registerMbean();
+    }
+
+    private void initScheduler() {
+        shutdownScheduler();
+        this.writer = settings.getFileConcurrency() > 1
+            ? Schedulers.newParallel(name + "-writer", settings.getFileConcurrency())
+            : Schedulers.newSingle(name + "-writer");
+
+        this.reader = Schedulers.newSingle(name + "-reader");
+    }
+
+    private void shutdownScheduler() {
+        if (this.writer != null) {
+            this.writer.dispose();
+        }
+        if (this.reader != null) {
+            this.reader.dispose();
+        }
     }
 
     public synchronized void start() {
@@ -286,6 +322,7 @@ public class PersistenceBuffer implements Disposable {
                 //直接写入queue,而不是使用write,等待后续有新的数据进入再重试
                 if (queue.offer(buf)) {
                     // REMAINDER.incrementAndGet(this);
+                    settings.getEviction().tryEviction(this);
                 } else {
                     dead(buf);
                 }
@@ -313,12 +350,42 @@ public class PersistenceBuffer implements Disposable {
             }
             return;
         }
+        // remainder ++
+        monitor.in();
+        // REMAINDER.incrementAndGet(this);
 
         queue.offer(data);
 
         drain();
+
+        //尝试执行淘汰策略
+        settings.getEviction().tryEviction(this);
     }
 
+    //异步写入数据到buffer
+    public Mono writeAsync(T data) {
+        if (isDisposed()) {
+            return Mono.fromRunnable(() -> write(data));
+        }
+        return Mono
+            .fromRunnable(() -> write(data))
+            .subscribeOn(writer)
+            .then();
+    }
+
+    //异步写入数据到buffer
+    public Mono writeAsync(Collection data) {
+        if (isDisposed()) {
+            return Mono.fromRunnable(() -> data.forEach(this::write));
+        }
+        return Mono
+            .fromRunnable(() -> data.forEach(this::write))
+            .subscribeOn(writer)
+            .then();
+    }
+
+    //写入数据到buffer,此操作可能阻塞
+    @Deprecated
     public void write(T data) {
         write(new Buf<>(data, instanceBuilder));
     }
@@ -328,6 +395,9 @@ public class PersistenceBuffer implements Disposable {
         if (this.intervalFlush != null) {
             this.intervalFlush.dispose();
         }
+        for (FlushSubscriber subscriber : new ArrayList<>(flushing)) {
+            subscriber.doCancel();
+        }
     }
 
     @SneakyThrows
@@ -368,7 +438,9 @@ public class PersistenceBuffer implements Disposable {
             deadQueue.close();
             queue = null;
             deadQueue = null;
+            shutdownScheduler();
         }
+        unregisterMbean();
     }
 
     @Override
@@ -377,7 +449,37 @@ public class PersistenceBuffer implements Disposable {
     }
 
     public long size() {
-        return queue == null ? 0 : queue.size();
+        return queue == null || disposed ? 0 : queue.size() + buffer().size();
+    }
+
+    public long size(BufferType type) {
+        return type == BufferType.buffer ? size() : deadQueue == null || disposed ? 0 : deadQueue.size();
+    }
+
+    @Override
+    public void removeLatest(BufferType type) {
+        if (type == BufferType.buffer) {
+            if (queue.removeLast() != null) {
+                monitor.dropped();
+            }
+        } else {
+            if (deadQueue.removeLast() != null) {
+                // DEAD_SZIE.decrementAndGet(this);
+            }
+        }
+    }
+
+    @Override
+    public void removeOldest(BufferType type) {
+        if (type == BufferType.buffer) {
+            if (queue.removeFirst() != null) {
+                monitor.dropped();
+            }
+        } else {
+            if (deadQueue.removeFirst() != null) {
+                // DEAD_SZIE.decrementAndGet(this);
+            }
+        }
     }
 
     private void intervalFlush() {
@@ -446,7 +548,7 @@ public class PersistenceBuffer implements Disposable {
                     logger.debug("write {} data,size:{},remainder:{},requeue: {}.take up time: {} ms",
                                  name,
                                  buffer.size(),
-                                 queue.size(),
+                                 size(),
                                  doRequeue,
                                  System.currentTimeMillis() - startWith);
                 }
@@ -535,6 +637,7 @@ public class PersistenceBuffer implements Disposable {
             }
             buffer.forEach(Buf::reset);
             flushing.remove(this);
+            monitor.out(size, System.currentTimeMillis() - startWith);
             // wip--
             WIP.decrementAndGet(PersistenceBuffer.this);
             drain();
@@ -556,22 +659,27 @@ public class PersistenceBuffer implements Disposable {
         if (!started) {
             return;
         }
-        //当前未执行完成的操作小于并行度才请求
+        // 当前未执行完成的操作小于并行度才请求
         if (WIP.incrementAndGet(this) <= settings.getParallelism()) {
-            int size = settings.getBufferSize();
-            for (int i = 0; i < size; i++) {
-                if (isDisposed()) {
-                    break;
-                }
-                Buf poll = queue.poll();
-                if (poll != null) {
-                    onNext(poll);
-                } else {
-                    break;
-                }
-            }
+            // 使用boundedElastic线程执行poll,避免阻塞线程
+            reader
+                .schedule(() -> {
+                    int size = settings.getBufferSize();
+                    for (int i = 0; i < size && started; i++) {
+                        Buf poll = settings.getStrategy() == ConsumeStrategy.LIFO
+                            ? queue.removeLast()
+                            : queue.poll();
+                        if (poll != null) {
+                            onNext(poll);
+                        } else {
+                            break;
+                        }
+                    }
+                    WIP.decrementAndGet(this);
+                });
+        } else {
+            WIP.decrementAndGet(this);
         }
-        WIP.decrementAndGet(this);
     }
 
     private void onNext(@Nonnull Buf value) {
@@ -739,7 +847,7 @@ public class PersistenceBuffer implements Disposable {
             if (obj.data instanceof String) {
                 return ((String) obj.data).length() * 2;
             }
-            return 10_000;
+            return 4096;
         }
 
         @Override
@@ -802,6 +910,228 @@ public class PersistenceBuffer implements Disposable {
         }
     }
 
+    private ObjectName objectName;
+
+    void registerMbean() {
+        try {
+            String safeName = name.replaceAll("[\\s\\\\/:*?\"<>|]", "_");
+            MBeanServer mBeanServer = ManagementFactory.getPlatformMBeanServer();
+            objectName = new ObjectName("org.jetlinks:type=PersistenceBuffer,name=" + safeName);
+            mBeanServer.registerMBean(new StandardMBean(monitor, PersistenceBufferMBean.class), objectName);
+        } catch (Throwable error) {
+            logger.warn("registerMBean {} error ", name, error);
+        }
+    }
+
+    void unregisterMbean() {
+        try {
+            if (objectName != null) {
+                MBeanServer mBeanServer = ManagementFactory.getPlatformMBeanServer();
+                mBeanServer.unregisterMBean(objectName);
+            }
+        } catch (Throwable ignore) {
+        }
+    }
+
+
+    @RequiredArgsConstructor
+    private static class PersistenceBufferMBeanImpl implements PersistenceBufferMBean {
+        @SuppressWarnings("all")
+        private static final AtomicLongFieldUpdater
+            IN = AtomicLongFieldUpdater.newUpdater(PersistenceBufferMBeanImpl.class, "in"),
+            OUT = AtomicLongFieldUpdater.newUpdater(PersistenceBufferMBeanImpl.class, "out"),
+            COST = AtomicLongFieldUpdater.newUpdater(PersistenceBufferMBeanImpl.class, "cost"),
+            OPT = AtomicLongFieldUpdater.newUpdater(PersistenceBufferMBeanImpl.class, "opt"),
+            DROPPED = AtomicLongFieldUpdater.newUpdater(PersistenceBufferMBeanImpl.class, "dropped");
+
+        private final PersistenceBuffer buffer;
+
+        private volatile long
+            //写入数量
+            in,
+        //写出数量
+        out,
+        //写出次数
+        opt,
+        //写出总耗时
+        cost,
+        //淘汰数量
+        dropped;
+
+        private final long[] costDist = new long[5];
+
+        private void dropped() {
+            DROPPED.incrementAndGet(this);
+        }
+
+        private void in() {
+            IN.incrementAndGet(this);
+        }
+
+        private void out(long outSize, long cost) {
+            COST.addAndGet(this, cost);
+            OUT.addAndGet(this, outSize);
+            OPT.incrementAndGet(this);
+            if (cost < 50) {
+                costDist[0]++;
+            } else if (cost < 200) {
+                costDist[1]++;
+            } else if (cost < 1000) {
+                costDist[2]++;
+            } else if (cost < 5000) {
+                costDist[3]++;
+            } else {
+                costDist[4]++;
+            }
+        }
+
+        @Override
+        public String getMonitor() {
+            return String.format(
+                "\nqueue(in %s,out %s,dropped %s);\nconsume(opt %s,cost %s ms);\ndist[0-50ms(%s),50-200ms(%s),0.2-1s(%s),1-5s(%s),>5s(%s)]\n",
+                in, out, dropped, opt, cost, costDist[0], costDist[1], costDist[2], costDist[3], costDist[4]);
+        }
+
+        @Override
+        public void resetMonitor() {
+            IN.set(this, 0);
+            OUT.set(this, 0);
+            OPT.set(this, 0);
+            COST.set(this, 0);
+            Arrays.fill(costDist, 0);
+        }
+
+        @Override
+        public long getRemainder() {
+            return buffer.queue.size();
+        }
+
+        @Override
+        public long getDeadSize() {
+            return buffer.deadQueue.size();
+        }
+
+        @Override
+        public long getWip() {
+            return buffer.wip;
+        }
+
+        @Override
+        public String getLastError() {
+            Throwable error = buffer.lastError;
+            return error == null ? "nil"
+                : ExceptionUtils.getRootCauseMessage(error) + ":" + ExceptionUtils.getStackTrace(error);
+        }
+
+        @Override
+        public String getStoragePath() {
+            return buffer.settings.getFilePath();
+        }
+
+        @Override
+        public List getDataBytes() {
+            File[] files = new File(buffer.settings.getFilePath())
+                .listFiles(filter -> filter.getName().startsWith(getSafeFileName(buffer.settings.getFileName())));
+            if (files == null) {
+                return Collections.emptyList();
+            }
+
+            return Arrays
+                .stream(files)
+                .map(file -> file.getName() + " " + FormatUtils.formatDataSize(file.length()))
+                .collect(Collectors.toList());
+        }
+
+        @Override
+        public void flush() {
+            buffer.queue.flush();
+            buffer.deadQueue.flush();
+        }
+
+        @Override
+        public void retryDead(int maxSize) {
+            //单次请求最大重试次数
+            maxSize = Math.min(50_0000, maxSize);
+            while (maxSize-- > 0) {
+                Buf buf = buffer.deadQueue.poll();
+                if (buf == null) {
+                    break;
+                }
+                buf.retry = 0;
+                if (!buffer.queue.offer(buf)) {
+                    buffer.deadQueue.offer(buf);
+                    break;
+                }
+            }
+            buffer.drain();
+        }
+
+        @Override
+        public String getSettings() {
+            return String.format("\nbufferSize: %s" +
+                                     ",bufferTimeout: %s" +
+                                     ",parallelism: %s" +
+                                     ",maxRetryTimes: %s" +
+                                     ",fileConcurrency: %s" + "\nEviction:%s ",
+                                 buffer.settings.getBufferSize(),
+                                 buffer.settings.getBufferTimeout(),
+                                 buffer.settings.getParallelism(),
+                                 buffer.settings.getMaxRetryTimes(),
+                                 buffer.settings.getFileConcurrency(),
+                                 buffer.settings.getEviction());
+        }
+
+        @Override
+        public long recovery(String fileName, boolean dead) {
+            return buffer.recovery(fileName, dead);
+        }
+
+        @Override
+        public List peekDead(int size) {
+            size = size <= 0 ? 1 : Math.min(1024, size);
+
+            List result = new ArrayList<>(size);
+
+            for (Buf tBuf : buffer.deadQueue) {
+                if (size-- <= 0) {
+                    break;
+                }
+                result.add(tBuf.data);
+            }
+
+            return result;
+        }
+    }
+
+    public interface PersistenceBufferMBean {
+
+        String getSettings();
+
+        String getMonitor();
+
+        void resetMonitor();
+
+        String getStoragePath();
+
+        long getRemainder();
+
+        long getDeadSize();
+
+        long getWip();
+
+        String getLastError();
+
+        List getDataBytes();
+
+        void flush();
+
+        void retryDead(int maxSize);
+
+        long recovery(String fileName, boolean dead);
+
+        List peekDead(int size);
+    }
+
     public interface FlushContext {
 
         //标记错误信息
diff --git a/jetlinks-components/common-component/src/main/java/org/jetlinks/community/buffer/SizeLimitEviction.java b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/buffer/SizeLimitEviction.java
new file mode 100644
index 00000000..40467c47
--- /dev/null
+++ b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/buffer/SizeLimitEviction.java
@@ -0,0 +1,37 @@
+package org.jetlinks.community.buffer;
+
+import lombok.AllArgsConstructor;
+
+import java.util.Map;
+
+@AllArgsConstructor
+class SizeLimitEviction extends AbstractBufferEviction {
+
+    private final long bufferLimit;
+    private final long deadLimit;
+
+    @Override
+    public boolean doEviction(EvictionContext context) {
+        boolean anyEviction = false;
+        if (bufferLimit > 0 && context.size(EvictionContext.BufferType.buffer) >= bufferLimit) {
+            context.removeOldest(EvictionContext.BufferType.buffer);
+            anyEviction = true;
+        }
+        if (deadLimit > 0 && context.size(EvictionContext.BufferType.dead) >= deadLimit) {
+            context.removeOldest(EvictionContext.BufferType.dead);
+            anyEviction = true;
+        }
+        return anyEviction;
+    }
+
+    @Override
+    protected void applyEventData(Map data) {
+        data.put("bufferLimit", bufferLimit);
+        data.put("deadLimit", deadLimit);
+    }
+
+    @Override
+    public String toString() {
+        return "SizeLimit(buffer=" + bufferLimit + ", dead=" + deadLimit + ")";
+    }
+}
diff --git a/jetlinks-components/common-component/src/main/java/org/jetlinks/community/command/CrudCommandSupport.java b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/command/CrudCommandSupport.java
new file mode 100644
index 00000000..c8c366a9
--- /dev/null
+++ b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/command/CrudCommandSupport.java
@@ -0,0 +1,307 @@
+package org.jetlinks.community.command;
+
+import lombok.SneakyThrows;
+import org.hswebframework.web.api.crud.entity.EntityFactoryHolder;
+import org.hswebframework.web.authorization.Authentication;
+import org.hswebframework.web.authorization.Permission;
+import org.hswebframework.web.authorization.exception.AccessDenyException;
+import org.hswebframework.web.bean.FastBeanCopier;
+import org.hswebframework.web.crud.service.ReactiveCrudService;
+import org.jetlinks.core.command.AbstractCommandSupport;
+import org.jetlinks.core.command.Command;
+import org.jetlinks.core.metadata.FunctionMetadata;
+import org.jetlinks.core.metadata.SimplePropertyMetadata;
+import org.jetlinks.core.metadata.types.ArrayType;
+import org.jetlinks.core.metadata.types.IntType;
+import org.jetlinks.core.metadata.types.ObjectType;
+import org.jetlinks.core.metadata.types.StringType;
+import org.jetlinks.core.utils.Reactors;
+import org.jetlinks.sdk.server.commons.cmd.*;
+import org.jetlinks.supports.official.DeviceMetadataParser;
+import org.springframework.core.ResolvableType;
+import reactor.bool.BooleanUtils;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 通用增删改查命令支持,基于{@link ReactiveCrudService}来实现增删改查相关命令
+ *
+ * @param  实体类型
+ * @author zhouhao
+ * @see QueryByIdCommand
+ * @see QueryPagerCommand
+ * @see QueryListCommand
+ * @see CountCommand
+ * @see SaveCommand
+ * @see AddCommand
+ * @see UpdateCommand
+ * @see DeleteCommand
+ * @see DeleteByIdCommand
+ * @since 2.2
+ */
+public class CrudCommandSupport extends AbstractCommandSupport {
+
+    final ReactiveCrudService service;
+    final ResolvableType _entityType;
+
+    public CrudCommandSupport(ReactiveCrudService service) {
+        this(service, ResolvableType
+            .forClass(ReactiveCrudService.class, service.getClass())
+            .getGeneric(0));
+    }
+
+    public CrudCommandSupport(ReactiveCrudService service, ResolvableType _entityType) {
+        this.service = service;
+        this._entityType = _entityType;
+
+        registerQueries();
+        registerSaves();
+        registerDelete();
+    }
+
+    @Override
+    public Flux getCommandMetadata() {
+        return super
+            .getCommandMetadata()
+            .filterWhen(func -> commandIsSupported(func.getId()));
+    }
+
+    @Override
+    public Mono commandIsSupported(String commandId) {
+
+        return BooleanUtils.and(
+            super.commandIsSupported(commandId),
+            hasPermission(getPermissionId(), getAction(commandId))
+        );
+    }
+
+    @Override
+    public Mono getCommandMetadata(String commandId) {
+        return super
+            .getCommandMetadata(commandId)
+            .filterWhen(func -> commandIsSupported(func.getId()));
+    }
+
+    @Override
+    public Mono getCommandMetadata(Command command) {
+        return super
+            .getCommandMetadata(command)
+            .filterWhen(func -> commandIsSupported(func.getId()));
+    }
+
+    @Override
+    public Mono getCommandMetadata(@Nonnull String commandId,
+                                                     @Nullable Map parameters) {
+        return super
+            .getCommandMetadata(commandId, parameters)
+            .filterWhen(func -> commandIsSupported(func.getId()));
+    }
+
+    @SneakyThrows
+    @SuppressWarnings("all")
+    private T newInstance0() {
+        return (T) _entityType.toClass().getConstructor().newInstance();
+    }
+
+    protected T newInstance() {
+        @SuppressWarnings("all")
+        Class clazz = (Class) _entityType.toClass();
+        return EntityFactoryHolder
+            .newInstance(clazz,
+                         this::newInstance0);
+    }
+
+    protected ResolvableType getResolvableType() {
+        return _entityType;
+    }
+
+    protected ObjectType createEntityType() {
+        return (ObjectType) DeviceMetadataParser.withType(_entityType);
+    }
+
+    protected String getPermissionId() {
+        return null;
+    }
+
+    protected Mono assetPermission(String action) {
+        return assetPermission(getPermissionId(), action);
+    }
+
+    protected Mono hasPermission(String permissionId, String action) {
+        if (permissionId == null) {
+            return Reactors.ALWAYS_TRUE;
+        }
+        return Authentication
+            .currentReactive()
+            .map(auth -> auth.hasPermission(permissionId, action))
+            .defaultIfEmpty(true);
+    }
+
+    protected Mono assetPermission(String permissionId, String action) {
+        if (permissionId == null) {
+            return Mono.empty();
+        }
+        return Authentication
+            .currentReactive()
+            .flatMap(
+                auth -> auth.hasPermission(permissionId, action)
+                    ? Mono.empty()
+                    : Mono.error(new AccessDenyException.NoStackTrace(permissionId, Collections.singleton(action))));
+    }
+
+    protected String getAction(String commandId) {
+        if (commandId.startsWith("Delete")) {
+            return Permission.ACTION_DELETE;
+        }
+        if (commandId.startsWith("Update") ||
+            commandId.startsWith("Save") ||
+            commandId.startsWith("Add") ||
+            commandId.startsWith("Disable") ||
+            commandId.startsWith("Enable")) {
+            return Permission.ACTION_SAVE;
+        }
+        return Permission.ACTION_QUERY;
+    }
+
+    protected void registerQueries() {
+        //根据id查询
+        registerHandler(
+            QueryByIdCommand
+                .createHandler(
+                    metadata -> {
+                        metadata
+                            .setInputs(Collections.singletonList(
+                                SimplePropertyMetadata
+                                    .of("id", "id", new ArrayType()
+                                        .elementType(StringType.GLOBAL))
+                            ));
+                        metadata.setOutput(createEntityType());
+                    },
+                    cmd -> assetPermission(Permission.ACTION_QUERY)
+                        .then(service.findById(cmd.getId())),
+                    _entityType)
+        );
+
+        //分页查询
+        registerHandler(
+            QueryPagerCommand
+                .createHandler(
+                    metadata -> metadata.setOutput(
+                        QueryPagerCommand
+                            .createOutputType(createEntityType().getProperties())),
+                    cmd -> assetPermission(Permission.ACTION_QUERY)
+                        .then(service.queryPager(cmd.asQueryParam())),
+                    _entityType)
+        );
+        //查询列表
+        registerHandler(
+            QueryListCommand
+                .createHandler(
+                    metadata -> metadata.setOutput(createEntityType()),
+                    cmd -> assetPermission(Permission.ACTION_QUERY)
+                        .thenMany(service.query(cmd.asQueryParam())),
+                    _entityType)
+        );
+        //查询数量
+        registerHandler(
+            CountCommand
+                .createHandler(
+                    metadata -> metadata.setOutput(new ObjectType()
+                                                       .addProperty("total", "总数", IntType.GLOBAL)),
+                    cmd -> assetPermission(Permission.ACTION_QUERY)
+                        .then(service.count(cmd.asQueryParam())))
+        );
+        //todo 聚合查询?
+
+    }
+
+    protected void registerSaves() {
+        //批量保存
+        registerHandler(
+            SaveCommand.createHandler(
+                metadata -> {
+                    metadata
+                        .setInputs(Collections.singletonList(
+                            SimplePropertyMetadata.of("data", "数据列表", new ArrayType().elementType(createEntityType()))
+                        ));
+                    metadata.setOutput(createEntityType());
+                },
+                cmd -> {
+                    List list = cmd.dataList((data) -> FastBeanCopier.copy(data, newInstance()));
+                    return assetPermission(Permission.ACTION_SAVE)
+                        .then(service.save(list))
+                        .thenMany(Flux.fromIterable(list));
+                },
+                _entityType)
+        );
+        //新增
+        registerHandler(
+            AddCommand
+                .createHandler(
+                    metadata -> {
+                        metadata
+                            .setInputs(Collections.singletonList(
+                                SimplePropertyMetadata.of("data", "数据列表", new ArrayType().elementType(createEntityType()))
+                            ));
+                        metadata.setOutput(createEntityType());
+                    },
+                    cmd -> Flux
+                        .fromIterable(cmd.dataList((data) -> FastBeanCopier.copy(data, newInstance())))
+                        .as(flux -> assetPermission(Permission.ACTION_SAVE)
+                            .then(service.insert(flux))
+                            .thenMany(flux)))
+        );
+        //修改
+        registerHandler(
+            UpdateCommand
+                .createHandler(
+                    metadata -> {
+                        metadata.setInputs(
+                            Arrays.asList(
+                                SimplePropertyMetadata.of("data", "数据", createEntityType()),
+                                QueryCommand.getTermsMetadata()
+                            ));
+                        metadata.setOutput(IntType.GLOBAL);
+                    },
+                    cmd -> this
+                        .assetPermission(Permission.ACTION_SAVE)
+                        .then(cmd
+                                  .applyUpdate(service.createUpdate(), map -> FastBeanCopier.copy(map, newInstance()))
+                                  .execute()))
+        );
+    }
+
+    protected void registerDelete() {
+
+        //删除
+        registerHandler(
+            DeleteCommand.createHandler(
+                metadata -> {
+                    metadata.setInputs(Collections.singletonList(
+                        SimplePropertyMetadata.of("terms", "删除条件", QueryCommand.getTermsDataType())));
+                    metadata.setOutput(IntType.GLOBAL);
+                },
+                cmd -> this
+                    .assetPermission(Permission.ACTION_DELETE)
+                    .then(cmd.applyDelete(service.createDelete()).execute()))
+        );
+        //根据id移除
+        registerHandler(
+            DeleteByIdCommand
+                .>createHandler(
+                    metadata -> {
+                    },
+                    cmd -> this
+                        .assetPermission(Permission.ACTION_DELETE)
+                        .then(service.deleteById(cmd.getId()).then()))
+        );
+    }
+
+}
diff --git a/jetlinks-components/common-component/src/main/java/org/jetlinks/community/command/StaticCommandSupportManagerProvider.java b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/command/StaticCommandSupportManagerProvider.java
new file mode 100644
index 00000000..a2d427ac
--- /dev/null
+++ b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/command/StaticCommandSupportManagerProvider.java
@@ -0,0 +1,75 @@
+package org.jetlinks.community.command;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import org.hswebframework.web.i18n.LocaleUtils;
+import org.jetlinks.core.command.AbstractCommandSupport;
+import org.jetlinks.core.command.CommandSupport;
+import org.jetlinks.community.annotation.command.CommandService;
+import org.springframework.core.annotation.AnnotationUtils;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * 通用静态命令管理.
+ *
+ * @author zhangji 2024/2/2
+ * @since 2.2.0
+ */
+@AllArgsConstructor
+public class StaticCommandSupportManagerProvider extends AbstractCommandSupport implements CommandSupportManagerProvider {
+
+    @Getter
+    public String provider;
+
+    private final Map commandSupports = new HashMap<>();
+
+    public void register(String id, CommandSupport commandSupport) {
+        commandSupports.put(id, commandSupport);
+    }
+
+    @Override
+    public final Mono getCommandSupport(String id, Map options) {
+        CommandSupport cmd = commandSupports.get(id);
+        if (cmd == null) {
+            return getUndefined(id, options);
+        }
+        return Mono.just(cmd);
+    }
+
+    protected Mono getUndefined(String id, Map options) {
+        return Mono.just(this);
+    }
+
+    @Override
+    public Flux getSupportInfo() {
+        Flux another = Flux
+            .fromIterable(commandSupports.entrySet())
+            .map(entry -> createCommandSupport(entry.getKey(), entry.getValue().getClass()));
+
+        if (!this.handlers.isEmpty()) {
+            return Flux.concat(another, Flux.just(createCommandSupport(null, this.getClass())));
+        }
+        return another;
+    }
+
+    protected final CommandSupportInfo createCommandSupport(String id, Class clazz) {
+        String name = id;
+        String description = null;
+        Schema schema = AnnotationUtils.findAnnotation(clazz, Schema.class);
+        if (null != schema) {
+            name = schema.title();
+            description = schema.description();
+        }
+        CommandService service = AnnotationUtils.findAnnotation(clazz, CommandService.class);
+        if (null != service) {
+            name = LocaleUtils.resolveMessage(service.name(), service.name());
+            description = String.join("", service.description());
+        }
+        return CommandSupportInfo.of(id, name, description);
+    }
+}
diff --git a/jetlinks-components/common-component/src/main/java/org/jetlinks/community/command/register/CommandServiceEndpointRegister.java b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/command/register/CommandServiceEndpointRegister.java
index aeacf60b..f7a17eba 100644
--- a/jetlinks-components/common-component/src/main/java/org/jetlinks/community/command/register/CommandServiceEndpointRegister.java
+++ b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/command/register/CommandServiceEndpointRegister.java
@@ -1,7 +1,6 @@
 package org.jetlinks.community.command.register;
 
 import lombok.extern.slf4j.Slf4j;
-import org.jetlinks.community.annotation.command.CommandService;
 import org.jetlinks.community.command.CommandSupportManagerProvider;
 import org.jetlinks.community.command.CommandSupportManagerProviders;
 import org.jetlinks.community.command.CompositeCommandSupportManagerProvider;
@@ -11,6 +10,7 @@ import org.springframework.context.ApplicationContext;
 import org.springframework.context.ApplicationContextAware;
 import org.springframework.core.annotation.AnnotatedElementUtils;
 import org.springframework.util.ClassUtils;
+import org.jetlinks.community.annotation.command.CommandService;
 
 import javax.annotation.Nonnull;
 import java.util.HashMap;
diff --git a/jetlinks-components/common-component/src/main/java/org/jetlinks/community/command/rule/RelievedAlarmCommand.java b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/command/rule/RelievedAlarmCommand.java
new file mode 100644
index 00000000..b8859dac
--- /dev/null
+++ b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/command/rule/RelievedAlarmCommand.java
@@ -0,0 +1,35 @@
+package org.jetlinks.community.command.rule;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import org.hswebframework.web.bean.FastBeanCopier;
+import org.jetlinks.core.command.AbstractCommand;
+import org.jetlinks.core.command.CommandMetadataResolver;
+import org.jetlinks.core.command.CommandUtils;
+import org.jetlinks.core.metadata.FunctionMetadata;
+import org.jetlinks.core.metadata.SimpleFunctionMetadata;
+import org.jetlinks.community.command.rule.data.RelieveInfo;
+import org.jetlinks.community.command.rule.data.RelieveResult;
+import org.springframework.core.ResolvableType;
+import reactor.core.publisher.Mono;
+
+@Schema(title = "解除告警命令")
+public class RelievedAlarmCommand extends AbstractCommand,RelievedAlarmCommand> {
+
+    @Schema(description = "解除告警传参信息")
+    public RelieveInfo getRelieveInfo() {
+        return FastBeanCopier.copy(readable(),  new RelieveInfo());
+    }
+
+    public RelievedAlarmCommand setRelieveInfo(RelieveInfo relieveInfo) {
+        return with(FastBeanCopier.copy(relieveInfo, writable()));
+    }
+
+    public static FunctionMetadata metadata() {
+        SimpleFunctionMetadata metadata = new SimpleFunctionMetadata();
+        metadata.setId(CommandUtils.getCommandIdByType(RelievedAlarmCommand.class));
+        metadata.setName("解除告警命令");
+        metadata.setInputs(CommandMetadataResolver.resolveInputs(ResolvableType.forClass(RelieveInfo.class)));
+        metadata.setOutput(CommandMetadataResolver.resolveOutput(ResolvableType.forClass(RelievedAlarmCommand.class)));
+        return metadata;
+    }
+}
\ No newline at end of file
diff --git a/jetlinks-components/common-component/src/main/java/org/jetlinks/community/command/rule/RuleCommandServices.java b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/command/rule/RuleCommandServices.java
new file mode 100644
index 00000000..25b8013e
--- /dev/null
+++ b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/command/rule/RuleCommandServices.java
@@ -0,0 +1,34 @@
+package org.jetlinks.community.command.rule;
+
+public interface RuleCommandServices {
+    /**
+     * 场景
+     */
+    String sceneService = "sceneService";
+
+    /**
+     * 告警配置
+     */
+    String alarmConfigService = "alarmConfigService";
+
+    /**
+     * 告警记录
+     */
+    String alarmRecordService = "alarmRecordService";
+
+    /**
+     * 告警历史
+     */
+    String alarmHistoryService = "alarmHistoryService";
+
+    /**
+     * 告警规则绑定
+     */
+    String alarmRuleBindService = "alarmRuleBindService";
+
+    /**
+     * 告警相关
+     */
+    String alarm = "alarm";
+
+}
\ No newline at end of file
diff --git a/jetlinks-components/common-component/src/main/java/org/jetlinks/community/command/rule/TriggerAlarmCommand.java b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/command/rule/TriggerAlarmCommand.java
new file mode 100644
index 00000000..abce55ee
--- /dev/null
+++ b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/command/rule/TriggerAlarmCommand.java
@@ -0,0 +1,38 @@
+package org.jetlinks.community.command.rule;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import org.hswebframework.web.bean.FastBeanCopier;
+import org.jetlinks.core.command.AbstractCommand;
+import org.jetlinks.core.command.CommandMetadataResolver;
+import org.jetlinks.core.command.CommandUtils;
+import org.jetlinks.core.metadata.FunctionMetadata;
+import org.jetlinks.core.metadata.SimpleFunctionMetadata;
+import org.jetlinks.community.command.rule.data.AlarmInfo;
+import org.jetlinks.community.command.rule.data.AlarmResult;
+import org.springframework.core.ResolvableType;
+import reactor.core.publisher.Mono;
+
+
+@Schema(title = "触发告警命令")
+public class TriggerAlarmCommand extends AbstractCommand,TriggerAlarmCommand> {
+    private static final long serialVersionUID = 7056867872399432831L;
+
+
+    @Schema(description = "告警传参信息")
+    public AlarmInfo getAlarmInfo() {
+        return FastBeanCopier.copy(readable(), new AlarmInfo());
+    }
+
+    public TriggerAlarmCommand setAlarmInfo(AlarmInfo alarmInfo) {
+        return with(FastBeanCopier.copy(alarmInfo, writable()));
+    }
+
+    public static FunctionMetadata metadata() {
+        SimpleFunctionMetadata metadata = new SimpleFunctionMetadata();
+        metadata.setId(CommandUtils.getCommandIdByType(TriggerAlarmCommand.class));
+        metadata.setName("触发告警命令");
+        metadata.setInputs(CommandMetadataResolver.resolveInputs(ResolvableType.forClass(AlarmInfo.class)));
+        metadata.setOutput(CommandMetadataResolver.resolveOutput(ResolvableType.forClass(TriggerAlarmCommand.class)));
+        return metadata;
+    }
+}
\ No newline at end of file
diff --git a/jetlinks-components/common-component/src/main/java/org/jetlinks/community/command/rule/data/AlarmInfo.java b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/command/rule/data/AlarmInfo.java
index 6ea0ee2b..d708f62a 100644
--- a/jetlinks-components/common-component/src/main/java/org/jetlinks/community/command/rule/data/AlarmInfo.java
+++ b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/command/rule/data/AlarmInfo.java
@@ -9,7 +9,6 @@ import lombok.Setter;
 import org.jetlinks.community.terms.TermSpec;
 
 import java.io.Serializable;
-import java.util.List;
 import java.util.Map;
 
 /**
@@ -49,6 +48,9 @@ public class AlarmInfo implements Serializable {
     @Schema(description = "告警来源ID")
     private String sourceId;
 
+    @Schema(description = "告警来源的创建人ID")
+    private String sourceCreatorId;
+
     @Schema(description = "告警来源名称")
     private String sourceName;
 
@@ -65,4 +67,8 @@ public class AlarmInfo implements Serializable {
      * 告警触发条件
      */
     private TermSpec termSpec;
+
+    @Schema(description = "告警时间")
+    private Long alarmTime;
+
 }
\ No newline at end of file
diff --git a/jetlinks-components/common-component/src/main/java/org/jetlinks/community/command/rule/data/RelieveInfo.java b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/command/rule/data/RelieveInfo.java
index 41580d99..ef36e7ed 100644
--- a/jetlinks-components/common-component/src/main/java/org/jetlinks/community/command/rule/data/RelieveInfo.java
+++ b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/command/rule/data/RelieveInfo.java
@@ -18,6 +18,9 @@ public class RelieveInfo extends AlarmInfo{
     @Schema(description = "解除原因")
     private String relieveReason;
 
+    @Schema(description = "解除时间")
+    private Long relieveTime;
+
     @Schema(description = "解除说明")
     private String describe;
 
diff --git a/jetlinks-components/common-component/src/main/java/org/jetlinks/community/configuration/CommonConfiguration.java b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/configuration/CommonConfiguration.java
index f71836a8..b648f099 100644
--- a/jetlinks-components/common-component/src/main/java/org/jetlinks/community/configuration/CommonConfiguration.java
+++ b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/configuration/CommonConfiguration.java
@@ -7,7 +7,6 @@ import lombok.Generated;
 import org.apache.commons.beanutils.BeanUtilsBean;
 import org.apache.commons.beanutils.Converter;
 import org.hswebframework.ezorm.rdb.mapping.ReactiveRepository;
-import org.hswebframework.web.api.crud.entity.EntityFactory;
 import org.hswebframework.web.bean.FastBeanCopier;
 import org.hswebframework.web.cache.ReactiveCacheManager;
 import org.hswebframework.web.dict.EnumDict;
@@ -33,10 +32,8 @@ import org.jetlinks.community.resource.TypeScriptDeclareResourceProvider;
 import org.jetlinks.community.resource.initialize.PermissionResourceProvider;
 import org.jetlinks.community.service.DefaultUserBindService;
 import org.jetlinks.community.utils.TimeUtils;
-import org.jetlinks.core.event.EventBus;
 import org.jetlinks.core.metadata.DataType;
 import org.jetlinks.core.metadata.types.DataTypes;
-import org.jetlinks.core.rpc.RpcManager;
 import org.jetlinks.reactor.ql.feature.Feature;
 import org.jetlinks.reactor.ql.supports.DefaultReactorQLMetadata;
 import org.jetlinks.reactor.ql.utils.CastUtils;
@@ -59,15 +56,11 @@ import reactor.core.publisher.Hooks;
 
 import javax.annotation.Nonnull;
 import java.time.Duration;
-import java.time.LocalDate;
-import java.time.LocalDateTime;
-import java.time.LocalTime;
-import java.time.format.DateTimeFormatter;
 import java.time.temporal.ChronoUnit;
 import java.util.Date;
 import java.util.Map;
 
-@Configuration
+@AutoConfiguration
 @SuppressWarnings("all")
 @EnableConfigurationProperties({ConfigScopeProperties.class})
 public class CommonConfiguration {
diff --git a/jetlinks-components/common-component/src/main/java/org/jetlinks/community/event/OperationAssetProvider.java b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/event/OperationAssetProvider.java
new file mode 100644
index 00000000..740c24e5
--- /dev/null
+++ b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/event/OperationAssetProvider.java
@@ -0,0 +1,15 @@
+package org.jetlinks.community.event;
+
+import org.jetlinks.community.Operation;
+import org.jetlinks.community.OperationType;
+import reactor.core.publisher.Flux;
+
+public interface OperationAssetProvider {
+
+    OperationType[] getSupportTypes();
+
+    Flux createTopics(Operation operation, String original);
+
+    Flux getAssetTypes(String operationType);
+
+}
diff --git a/jetlinks-components/common-component/src/main/java/org/jetlinks/community/event/OperationAssetProviders.java b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/event/OperationAssetProviders.java
new file mode 100644
index 00000000..13b9491e
--- /dev/null
+++ b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/event/OperationAssetProviders.java
@@ -0,0 +1,35 @@
+package org.jetlinks.community.event;
+
+import lombok.extern.slf4j.Slf4j;
+import org.jetlinks.community.Operation;
+import org.jetlinks.community.OperationType;
+
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.ConcurrentHashMap;
+
+@Slf4j
+public class OperationAssetProviders {
+
+    private static final Map providers = new ConcurrentHashMap<>();
+
+    public static void register(OperationAssetProvider provider) {
+        for (OperationType supportType : provider.getSupportTypes()) {
+            OperationAssetProvider old = providers.put(supportType.getId(), provider);
+
+            if (old != null && old != provider) {
+                log.warn("operation asset provider [{}] already exists,will be replaced by [{}]", old, provider);
+            }
+
+        }
+    }
+
+    public static Optional lookup(Operation operation) {
+        return lookup(operation.getType().getId());
+    }
+
+    public static Optional lookup(String operationType) {
+        return Optional.ofNullable(providers.get(operationType));
+    }
+
+}
diff --git a/jetlinks-components/common-component/src/main/java/org/jetlinks/community/event/SystemEvent.java b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/event/SystemEvent.java
new file mode 100644
index 00000000..1d13af7f
--- /dev/null
+++ b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/event/SystemEvent.java
@@ -0,0 +1,77 @@
+package org.jetlinks.community.event;
+
+import lombok.Getter;
+import lombok.Setter;
+import org.jetlinks.core.utils.SerializeUtils;
+import org.jetlinks.community.Operation;
+
+import java.io.Externalizable;
+import java.io.IOException;
+import java.io.ObjectInput;
+import java.io.ObjectOutput;
+
+@Getter
+@Setter
+public class SystemEvent implements Externalizable {
+    private static final long serialVersionUID = 1L;
+
+    private Level level;
+
+    private String code;
+
+    private Operation operation;
+
+    /**
+     * 描述详情,不同的类型详情内容不同
+     *
+     * @see org.jetlinks.community.monitor.ExecutionMonitorInfo
+     */
+    private Object detail;
+
+    private long timestamp;
+
+    public SystemEvent(Level level, String code, Operation operation, Object detail) {
+        this.level = level;
+        this.code = code;
+        this.operation = operation;
+        this.detail = detail;
+        this.timestamp = System.currentTimeMillis();
+    }
+
+    public SystemEvent() {
+    }
+
+
+    @Override
+    public void writeExternal(ObjectOutput out) throws IOException {
+        out.writeByte(level.ordinal());
+
+        out.writeUTF(code);
+
+        operation.writeExternal(out);
+
+        SerializeUtils.writeObject(detail, out);
+
+        out.writeLong(timestamp);
+    }
+
+    @Override
+    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
+        level = Level.values()[in.readByte()];
+
+        code = in.readUTF();
+
+        operation = new Operation();
+        operation.readExternal(in);
+
+        detail = SerializeUtils.readObject(in);
+        timestamp = in.readLong();
+    }
+
+    public enum Level {
+        info,
+        warn,
+        error
+    }
+
+}
diff --git a/jetlinks-components/common-component/src/main/java/org/jetlinks/community/event/SystemEventDispatcher.java b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/event/SystemEventDispatcher.java
new file mode 100644
index 00000000..8c1ce233
--- /dev/null
+++ b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/event/SystemEventDispatcher.java
@@ -0,0 +1,48 @@
+package org.jetlinks.community.event;
+
+import lombok.AllArgsConstructor;
+import org.jetlinks.core.event.EventBus;
+import org.springframework.context.ApplicationEventPublisher;
+
+/**
+ * 推送系统事件到事件总线,topic: /sys-event/{operationType}/{operationId}/{level}
+ *
+ * @author zhouhao
+ * @since 2.0
+ */
+@AllArgsConstructor
+public class SystemEventDispatcher implements SystemEventHandler {
+
+    private final EventBus eventBus;
+
+    private final ApplicationEventPublisher eventPublisher;
+
+
+    @Override
+    public final void handle(SystemEvent event) {
+
+        String topic = SystemEventHandler
+            .topic(event.getOperation().getType().getId(),
+                   event.getOperation().getSource().getId(),
+                   event.getLevel().name());
+
+        eventPublisher.publishEvent(event);
+
+        OperationAssetProvider provider = OperationAssetProviders
+            .lookup(event.getOperation())
+            .orElse(null);
+
+        //对数据权限控制的支持
+        if (provider != null) {
+            provider
+                .createTopics(event.getOperation(), topic)
+                .flatMap(_topic -> eventBus.publish(_topic, event))
+                .subscribe();
+        } else {
+            eventBus.publish(topic, event)
+                    .subscribe();
+        }
+
+
+    }
+}
diff --git a/jetlinks-components/common-component/src/main/java/org/jetlinks/community/event/SystemEventHandler.java b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/event/SystemEventHandler.java
new file mode 100644
index 00000000..3fbc99f4
--- /dev/null
+++ b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/event/SystemEventHandler.java
@@ -0,0 +1,22 @@
+package org.jetlinks.community.event;
+
+import org.jetlinks.core.utils.StringBuilderUtils;
+
+public interface SystemEventHandler {
+
+    static String topic(String operationType, String operationId, String level) {
+        return StringBuilderUtils
+            .buildString(operationType, operationId, level, (a, b, c, builder) -> {
+                //     /sys-event/{operationType}/{operationId}/{level}
+                builder.append("/sys-event/")
+                       .append(a)
+                       .append('/')
+                       .append(b)
+                       .append('/')
+                       .append(c);
+            });
+    }
+
+    void handle(SystemEvent event);
+
+}
diff --git a/jetlinks-components/common-component/src/main/java/org/jetlinks/community/event/SystemEventHandlerRegister.java b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/event/SystemEventHandlerRegister.java
new file mode 100644
index 00000000..0ad88ea5
--- /dev/null
+++ b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/event/SystemEventHandlerRegister.java
@@ -0,0 +1,13 @@
+package org.jetlinks.community.event;
+
+import org.springframework.beans.factory.ObjectProvider;
+import org.springframework.stereotype.Component;
+
+@Component
+public class SystemEventHandlerRegister {
+
+    public SystemEventHandlerRegister(ObjectProvider handlers){
+         handlers.forEach(SystemEventHolder::register);
+    }
+
+}
diff --git a/jetlinks-components/common-component/src/main/java/org/jetlinks/community/event/SystemEventHolder.java b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/event/SystemEventHolder.java
new file mode 100644
index 00000000..ff0c4cae
--- /dev/null
+++ b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/event/SystemEventHolder.java
@@ -0,0 +1,54 @@
+package org.jetlinks.community.event;
+
+import lombok.extern.slf4j.Slf4j;
+import org.jetlinks.community.Operation;
+import reactor.core.Disposable;
+
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+@Slf4j
+public class SystemEventHolder {
+
+    private static final List eventHandlers = new CopyOnWriteArrayList<>();
+
+
+    public static Disposable register(SystemEventHandler handler) {
+        eventHandlers.add(handler);
+        return () -> eventHandlers.remove(handler);
+    }
+
+    public static void error(Operation operation, String code, Object detail) {
+        log.error("{} {} :{}", operation, code, detail);
+        if (eventHandlers.isEmpty()) {
+            return;
+        }
+        fireEvent(new SystemEvent(SystemEvent.Level.error, code, operation, detail));
+    }
+
+    public static void warn(Operation operation, String code, Object detail) {
+        log.warn("{} {} :{}", operation, code, detail);
+        if (eventHandlers.isEmpty()) {
+            return;
+        }
+        fireEvent(new SystemEvent(SystemEvent.Level.warn, code, operation, detail));
+    }
+
+    public static void info(Operation operation, String code, Object detail) {
+        log.info("{} {} :{}", operation, code, detail);
+        if (eventHandlers.isEmpty()) {
+            return;
+        }
+        fireEvent(new SystemEvent(SystemEvent.Level.info, code, operation, detail));
+    }
+
+    private static void fireEvent(SystemEvent event) {
+        for (SystemEventHandler eventHandler : eventHandlers) {
+            try {
+                eventHandler.handle(event);
+            } catch (Throwable e) {
+                log.warn("handle system log error", e);
+            }
+        }
+    }
+}
diff --git a/jetlinks-components/common-component/src/main/java/org/jetlinks/community/lock/DefaultReactiveLock.java b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/lock/DefaultReactiveLock.java
new file mode 100644
index 00000000..dd6a8353
--- /dev/null
+++ b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/lock/DefaultReactiveLock.java
@@ -0,0 +1,296 @@
+package org.jetlinks.community.lock;
+
+import org.reactivestreams.Publisher;
+import org.reactivestreams.Subscription;
+import reactor.core.CoreSubscriber;
+import reactor.core.Disposable;
+import reactor.core.publisher.*;
+import reactor.core.scheduler.Schedulers;
+import reactor.util.context.Context;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import java.time.Duration;
+import java.util.Deque;
+import java.util.concurrent.ConcurrentLinkedDeque;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;
+import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;
+import java.util.function.Consumer;
+
+class DefaultReactiveLock implements ReactiveLock {
+    @SuppressWarnings("all")
+    static final AtomicReferenceFieldUpdater
+        PENDING = AtomicReferenceFieldUpdater
+        .newUpdater(DefaultReactiveLock.class, LockingSubscriber.class, "pending");
+
+    final Deque> queue = new ConcurrentLinkedDeque<>();
+
+    volatile LockingSubscriber pending;
+
+
+    @Override
+    public  Flux lock(Flux job) {
+        return new LockingFlux<>(this, job);
+    }
+
+    @Override
+    public  Flux lock(Flux flux, Duration timeout) {
+        return new LockingFlux<>(this, flux, timeout);
+    }
+
+    @Override
+    public  Flux lock(Flux flux, Duration timeout, Flux fallback) {
+        return new LockingFlux<>(this, flux, timeout, fallback);
+    }
+
+    @Override
+    public  Mono lock(Mono job) {
+        return new LockingMono<>(this, job);
+    }
+
+    @Override
+    public  Mono lock(Mono mono, Duration timeout) {
+        return new LockingMono<>(this, mono, timeout);
+    }
+
+    @Override
+    public  Mono lock(Mono mono, Duration timeout, Mono fallback) {
+        return new LockingMono<>(this, mono, timeout, fallback);
+    }
+
+    protected void drain() {
+        if (PENDING.get(this) != null) {
+            return;
+        }
+
+        LockingSubscriber locking;
+
+        for (; ; ) {
+            locking = queue.pollFirst();
+            if (locking == null) {
+                return;
+            }
+            if (locking.isDisposed()) {
+                continue;
+            }
+            if (PENDING.compareAndSet(this, null, locking)) {
+                //使用单独的线程池来调度,防止参与锁太多导致栈溢出.
+                Schedulers.parallel().schedule(locking::subscribe);
+            } else {
+                queue.addLast(locking);
+            }
+            break;
+        }
+
+    }
+
+
+     void registerSubscriber(CoreSubscriber actual,
+                                Consumer> subscribeCallback,
+                                @Nullable Duration timeout,
+                                @Nullable Publisher timeoutFallback) {
+        registerSubscriber(new LockingSubscriber<>(
+            this,
+            actual,
+            subscribeCallback,
+            timeout,
+            timeoutFallback));
+    }
+
+    void registerSubscriber(LockingSubscriber subscriber) {
+        if (PENDING.compareAndSet(this, null, subscriber)) {
+            subscriber.subscribe();
+            return;
+        }
+
+        queue.addLast(subscriber);
+        drain();
+    }
+
+    static class LockingFlux extends FluxOperator {
+        private final DefaultReactiveLock main;
+
+        private Duration timeout;
+
+        private Publisher timeoutFallback;
+
+        protected LockingFlux(DefaultReactiveLock main, Flux source) {
+            super(source);
+            this.main = main;
+        }
+
+        protected LockingFlux(DefaultReactiveLock main, Flux source, Duration timeout) {
+            super(source);
+            this.main = main;
+            this.timeout = timeout;
+        }
+
+        protected LockingFlux(DefaultReactiveLock main, Flux source, Duration timeout, Flux timeoutFallback) {
+            super(source);
+            this.main = main;
+            this.timeout = timeout;
+            this.timeoutFallback = timeoutFallback;
+        }
+
+        @Override
+        public void subscribe(@Nonnull CoreSubscriber actual) {
+            Consumer> subscribeCallback = source::subscribe;
+            main.registerSubscriber(actual, subscribeCallback, timeout, timeoutFallback);
+        }
+
+    }
+
+    static class LockingMono extends MonoOperator {
+        private final DefaultReactiveLock main;
+        private Duration timeout;
+
+        private Publisher fallback;
+
+        protected LockingMono(DefaultReactiveLock main, Mono source) {
+            super(source);
+            this.main = main;
+        }
+
+        protected LockingMono(DefaultReactiveLock main, Mono source, Duration timeout) {
+            super(source);
+            this.main = main;
+            this.timeout = timeout;
+        }
+
+        protected LockingMono(DefaultReactiveLock main, Mono source, Duration timeout, Mono fallback) {
+            super(source);
+            this.main = main;
+            this.timeout = timeout;
+            this.fallback = fallback;
+        }
+
+        @Override
+        public void subscribe(@Nonnull CoreSubscriber actual) {
+            Consumer> subscribeCallback = source::subscribe;
+            main.registerSubscriber(actual, subscribeCallback, timeout, fallback);
+        }
+
+
+    }
+
+    static class LockingSubscriber extends BaseSubscriber {
+        protected final DefaultReactiveLock main;
+        protected final CoreSubscriber actual;
+        private final Consumer> subscriber;
+        private Disposable timeoutTask;
+        private final Publisher timeoutFallback;
+        @SuppressWarnings("all")
+        protected static final AtomicIntegerFieldUpdater statusUpdater =
+            AtomicIntegerFieldUpdater.newUpdater(LockingSubscriber.class, "status");
+
+        private volatile int status;
+
+        //初始
+        private static final int INIT = 0;
+
+        //订阅备用流
+        private static final int SUB_TIMEOUT_FALLBACK = -1;
+
+        //订阅原本上游流
+        private static final int SUB_SOURCE = 1;
+
+        //流结束
+        private static final int UN_SUB = -2;
+
+        public LockingSubscriber(DefaultReactiveLock main,
+                                 CoreSubscriber actual,
+                                 Consumer> subscriber,
+                                 @Nullable Duration timeout,
+                                 @Nullable Publisher timeoutFallback) {
+            this.actual = actual;
+            this.main = main;
+            this.subscriber = subscriber;
+            this.timeoutFallback = timeoutFallback;
+            if (timeout != null) {
+                this.timeoutTask = Schedulers
+                    .parallel()
+                    .schedule(this::onTimeout, timeout.toMillis(), TimeUnit.MILLISECONDS);
+            }
+
+        }
+
+        private void onTimeout() {
+            if (statusUpdater.compareAndSet(this, INIT, SUB_TIMEOUT_FALLBACK)) {
+                //不代理订阅,直接取消流及释放当前锁,以免并发时等待备用流释放锁
+                doComplete();
+                if (timeoutFallback != null) {
+                    timeoutFallback.subscribe(actual);
+                } else {
+                    this.onError(new TimeoutException("Lock timed out"));
+                }
+            }
+        }
+
+        protected void subscribe() {
+            if (statusUpdater.compareAndSet(this, INIT, SUB_SOURCE)) {
+                if (timeoutTask != null && !timeoutTask.isDisposed()) {
+                    timeoutTask.dispose();
+                }
+                subscriber.accept(this);
+            }
+        }
+
+        protected void complete() {
+            if (statusUpdater.compareAndSet(this, INIT, UN_SUB) || statusUpdater.compareAndSet(this, SUB_SOURCE, UN_SUB)) {
+                if (timeoutTask != null && !timeoutTask.isDisposed()) {
+                    timeoutTask.dispose();
+                }
+                doComplete();
+            }
+        }
+
+        protected void doComplete() {
+            //防止非hookFinally触发的结束
+            if (!this.isDisposed()) {
+                this.cancel();
+            }
+            if (PENDING.compareAndSet(main, this, null)) {
+                main.drain();
+            }
+        }
+
+        @Override
+        protected final void hookOnError(@Nonnull Throwable throwable) {
+            actual.onError(throwable);
+        }
+
+        @Override
+        protected final void hookOnNext(@Nonnull T value) {
+            actual.onNext(value);
+        }
+
+        @Override
+        protected final void hookOnSubscribe(@Nonnull Subscription subscription) {
+            actual.onSubscribe(this);
+        }
+
+        @Override
+        protected final void hookOnComplete() {
+            actual.onComplete();
+        }
+
+        @Override
+        protected final void hookOnCancel() {
+            super.hookOnCancel();
+        }
+
+        @Override
+        protected final void hookFinally(@Nonnull SignalType type) {
+            complete();
+        }
+
+        @Override
+        @Nonnull
+        public Context currentContext() {
+            return actual.currentContext();
+        }
+    }
+
+}
diff --git a/jetlinks-components/common-component/src/main/java/org/jetlinks/community/lock/DefaultReactiveLockManager.java b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/lock/DefaultReactiveLockManager.java
new file mode 100644
index 00000000..5dfb1e59
--- /dev/null
+++ b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/lock/DefaultReactiveLockManager.java
@@ -0,0 +1,20 @@
+package org.jetlinks.community.lock;
+
+import com.github.benmanes.caffeine.cache.Caffeine;
+
+import java.time.Duration;
+import java.util.Map;
+
+class DefaultReactiveLockManager implements ReactiveLockManager {
+
+    private final Map cache = Caffeine
+        .newBuilder()
+        .expireAfterAccess(Duration.ofMinutes(30))
+        .build()
+        .asMap();
+
+    @Override
+    public ReactiveLock getLock(String name) {
+        return cache.computeIfAbsent(name, ignore -> new DefaultReactiveLock());
+    }
+}
diff --git a/jetlinks-components/common-component/src/main/java/org/jetlinks/community/lock/ReactiveLock.java b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/lock/ReactiveLock.java
new file mode 100644
index 00000000..358333b1
--- /dev/null
+++ b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/lock/ReactiveLock.java
@@ -0,0 +1,76 @@
+package org.jetlinks.community.lock;
+
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+import java.time.Duration;
+
+/**
+ * 响应式锁
+ *
+ * @author zhouhao
+ * @since 2.2
+ */
+public interface ReactiveLock {
+
+    /**
+     * 对Mono进行加锁,将等待之前的lock完成后再执行.
+     *
+     * @param mono 要加锁的Mono
+     * @param   T
+     * @return 加锁后的Mono
+     */
+     Mono lock(Mono mono);
+
+    /**
+     * 对Mono进行加锁,将等待之前的lock完成后再执行,若等待锁时间超过{@link Duration},则报错{@link java.util.concurrent.TimeoutException}
+     *
+     * @param mono    要加锁的Mono
+     * @param timeout 等待锁的时间
+     * @param      T
+     * @return 加锁后的Mono
+     */
+     Mono lock(Mono mono, Duration timeout);
+
+    /**
+     * 对Mono进行加锁,将等待之前的lock完成后再执行,若等待锁时间超过{@link Duration},则切换到回退流
+     *
+     * @param mono     要加锁的Mono
+     * @param timeout  等待锁的时间
+     * @param fallback 发生超时时要订阅的回退流
+     * @param       T
+     * @return 加锁后的Mono
+     */
+     Mono lock(Mono mono, Duration timeout, Mono fallback);
+
+    /**
+     * 对Flux进行加锁,将等待之前的lock完成后再执行.
+     *
+     * @param flux 要加锁的Flux
+     * @param   T
+     * @return 加锁后的Flux
+     */
+     Flux lock(Flux flux);
+
+    /**
+     * 对Flux进行加锁,将等待之前的lock完成后再执行,若等待锁时间超过{@link Duration},则报错{@link java.util.concurrent.TimeoutException}
+     *
+     * @param flux    要加锁的Flux
+     * @param timeout 等待锁的时间
+     * @param      T
+     * @return 加锁后的Mono
+     */
+     Flux lock(Flux flux, Duration timeout);
+
+    /**
+     * 对Flux进行加锁,将等待之前的lock完成后再执行,若等待锁时间超过{@link Duration},则切换到回退流
+     *
+     * @param flux     要加锁的Flux
+     * @param timeout  等待锁的时间
+     * @param fallback 发生超时时要订阅的回退流
+     * @param       T
+     * @return 加锁后的Mono
+     */
+     Flux lock(Flux flux, Duration timeout, Flux fallback);
+
+}
diff --git a/jetlinks-components/common-component/src/main/java/org/jetlinks/community/lock/ReactiveLockHolder.java b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/lock/ReactiveLockHolder.java
new file mode 100644
index 00000000..e765dd30
--- /dev/null
+++ b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/lock/ReactiveLockHolder.java
@@ -0,0 +1,39 @@
+package org.jetlinks.community.lock;
+
+/**
+ * 响应式锁持有器,用于通过静态方法获取锁.
+ * 
{@code
+ *
+ *  Mono execute(MyEntity entity){
+ *
+ *  return ReactiveLockHolder
+ *          .getLock("lock-test:"+entity.getId())
+ *          .lock(updateAndGet(entity));
+ *
+ * }
+ * }
+ * + * @author zhouhao + * @see ReactiveLock + * @see ReactiveLockManager + * @since 2.2 + */ +public class ReactiveLockHolder { + + private static ReactiveLockManager lockManager = new DefaultReactiveLockManager(); + + static void setup(ReactiveLockManager manager) { + lockManager = manager; + } + + /** + * 根据锁名称获取锁 + * + * @param name 锁名称 + * @return 锁 + */ + public static ReactiveLock getLock(String name) { + return lockManager.getLock(name); + } + +} diff --git a/jetlinks-components/common-component/src/main/java/org/jetlinks/community/lock/ReactiveLockManager.java b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/lock/ReactiveLockManager.java new file mode 100644 index 00000000..2d10aaa1 --- /dev/null +++ b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/lock/ReactiveLockManager.java @@ -0,0 +1,20 @@ +package org.jetlinks.community.lock; + +/** + * 响应式锁管理器 + * + * @author zhouhao + * @see ReactiveLock + * @since 2.2 + */ +public interface ReactiveLockManager { + + /** + * 根据名称获取一个锁. + * + * @param name 锁名称 + * @return 锁 + */ + ReactiveLock getLock(String name); + +} diff --git a/jetlinks-components/common-component/src/main/java/org/jetlinks/community/reactorql/term/ExistsTermSupport.java b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/reactorql/term/ExistsTermSupport.java deleted file mode 100644 index cdb52da6..00000000 --- a/jetlinks-components/common-component/src/main/java/org/jetlinks/community/reactorql/term/ExistsTermSupport.java +++ /dev/null @@ -1,73 +0,0 @@ -package org.jetlinks.community.reactorql.term; - -import org.hswebframework.ezorm.core.param.Term; -import org.hswebframework.ezorm.rdb.operator.builder.fragments.BatchSqlFragments; -import org.hswebframework.ezorm.rdb.operator.builder.fragments.SqlFragments; -import org.jetlinks.community.reactorql.impl.ComplexExistsFunction; -import org.jetlinks.core.metadata.DataType; -import org.jetlinks.core.metadata.types.ArrayType; - -import java.util.function.BiFunction; - -/** - * @author zhangji 2025/1/23 - * @since 2.3 - */ -public class ExistsTermSupport implements TermTypeSupport { - static { - ComplexExistsFunction.register(); - } - - @Override - public String getType() { - return "complex_exists"; - } - - @Override - public String getName() { - return "满足"; - } - - @Override - public boolean isSupported(DataType type) { - return type instanceof ArrayType; - } - - @Override - public Term refactorTerm(String tableName, - Term term, - BiFunction refactor) { - Term t = refactor.apply(tableName, term); - ComplexExistsFunction.ExistsSpec existsSpec = ComplexExistsFunction.createExistsSpec(t.getValue()); - - existsSpec.walkTerms(__term -> { - - String col = __term.getColumn(); - //使用 _row 获取原始行数据 - refactor.apply(ComplexExistsFunction.COL_ROW, __term); - //由原始条件指定的列名为准,如: _element.this 、_element.num - __term.setColumn(col); - - }); - - t.setValue(existsSpec); - - return t; - } - - @Override - public SqlFragments createSql(String column, Object value, Term term) { - - ComplexExistsFunction.ExistsSpec existsSpec = ComplexExistsFunction.createExistsSpec(value); - - BatchSqlFragments fragments = new BatchSqlFragments(); - - fragments - .addSql("complex_exists(?,", column, ")") - .addParameter(existsSpec.compile()); - - - return fragments; - } - -} diff --git a/jetlinks-components/common-component/src/main/java/org/jetlinks/community/reactorql/term/FixedTermTypeSupport.java b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/reactorql/term/FixedTermTypeSupport.java index e4f2ac67..7455029c 100644 --- a/jetlinks-components/common-component/src/main/java/org/jetlinks/community/reactorql/term/FixedTermTypeSupport.java +++ b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/reactorql/term/FixedTermTypeSupport.java @@ -9,65 +9,158 @@ import org.hswebframework.ezorm.rdb.operator.builder.fragments.SqlFragments; import org.hswebframework.web.i18n.LocaleUtils; import org.jetlinks.core.metadata.DataType; import org.jetlinks.core.metadata.types.*; +import org.jetlinks.community.utils.ConverterUtils; -import java.util.Arrays; import java.util.Collection; -import java.util.HashSet; +import java.util.List; import java.util.Set; - -import static org.jetlinks.community.reactorql.term.TermType.OPTIONS_NATIVE_SQL; +import java.util.stream.Collectors; +import java.util.stream.Stream; @Getter public enum FixedTermTypeSupport implements TermTypeSupport { - eq("等于", "eq"), - neq("不等于", "neq"), + eq("等于", "eq") { + @Override + public boolean isSupported(DataType type) { + return !type.getType().equals(ArrayType.ID) && super.isSupported(type); + } + }, + neq("不等于", "neq") { + @Override + public boolean isSupported(DataType type) { + return !type.getType().equals(ArrayType.ID) && super.isSupported(type); + } - gt("大于", "gt", DateTimeType.ID, IntType.ID, LongType.ID, FloatType.ID, DoubleType.ID), - gte("大于等于", "gte", DateTimeType.ID, IntType.ID, LongType.ID, FloatType.ID, DoubleType.ID), - lt("小于", "lt", DateTimeType.ID, IntType.ID, LongType.ID, FloatType.ID, DoubleType.ID), - lte("小于等于", "lte", DateTimeType.ID, IntType.ID, LongType.ID, FloatType.ID, DoubleType.ID), + }, + notnull("不为空", "notnull", false) { + @Override + protected String createDefaultDesc(String property, Object expect, Object actual) { + return String.format("%s%s", property, getName()); + } + }, + isnull("为空", "isnull", false) { + @Override + public String createDefaultDesc(String property, Object expect, Object actual) { + return String.format("%s%s", property, getName()); + } + }, - btw("在...之间", "btw", DateTimeType.ID, IntType.ID, LongType.ID, FloatType.ID, DoubleType.ID) { + gt("大于", "gt", DateTimeType.ID, ShortType.ID, IntType.ID, LongType.ID, FloatType.ID, DoubleType.ID), + gte("大于等于", "gte", DateTimeType.ID, ShortType.ID, IntType.ID, LongType.ID, FloatType.ID, DoubleType.ID), + lt("小于", "lt", DateTimeType.ID, ShortType.ID, IntType.ID, LongType.ID, FloatType.ID, DoubleType.ID), + lte("小于等于", "lte", DateTimeType.ID, ShortType.ID, IntType.ID, LongType.ID, FloatType.ID, DoubleType.ID), + + btw("在...之间", "btw", DateTimeType.ID, ShortType.ID, IntType.ID, LongType.ID, FloatType.ID, DoubleType.ID) { @Override protected Object convertValue(Object val, Term term) { return val; } + + @Override + protected String createValueDesc(Object expect) { + return arrayToSpec(expect); + } + + @Override + public String createDefaultDesc(String property, Object expect, Object actual) { + return String.format("%s在%s之间", property, arrayToSpec(expect)); + } }, - nbtw("不在...之间", "nbtw", DateTimeType.ID, IntType.ID, LongType.ID, FloatType.ID, DoubleType.ID) { + nbtw("不在...之间", "nbtw", DateTimeType.ID, ShortType.ID, IntType.ID, LongType.ID, FloatType.ID, DoubleType.ID) { @Override protected Object convertValue(Object val, Term term) { return val; } + + @Override + protected String createValueDesc(Object expect) { + return arrayToSpec(expect); + } + + @Override + public String createDefaultDesc(String property, Object expect, Object actual) { + return String.format("%s不在%s之间", property, createValueDesc(expect)); + } }, - in("在...之中", "in", StringType.ID, IntType.ID, LongType.ID, FloatType.ID, DoubleType.ID, EnumType.ID) { + in("在...之中", "in", StringType.ID, ShortType.ID, IntType.ID, LongType.ID, FloatType.ID, DoubleType.ID, EnumType.ID) { @Override protected Object convertValue(Object val, Term term) { return val; } + + @Override + protected String createValueDesc(Object expect) { + return arrayToSpec(expect); + } + + @Override + public String createDefaultDesc(String property, Object expect, Object actual) { + return String.format("%s在%s之中", property, createValueDesc(expect)); + } }, - nin("不在...之中", "nin", StringType.ID, IntType.ID, LongType.ID, FloatType.ID, DoubleType.ID, EnumType.ID) { + nin("不在...之中", "nin", StringType.ID, ShortType.ID, IntType.ID, LongType.ID, FloatType.ID, DoubleType.ID, EnumType.ID) { @Override protected Object convertValue(Object val, Term term) { return val; } + + @Override + protected String createValueDesc(Object expect) { + return arrayToSpec(expect); + } + + @Override + public String createDefaultDesc(String property, Object expect, Object actual) { + return String.format("%s不在%s之中", property, createValueDesc(expect)); + } }, contains_all("全部包含在...之中", "contains_all", ArrayType.ID) { @Override protected Object convertValue(Object val, Term term) { - return val; + return ConverterUtils.convertToList(val); + } + + @Override + protected String createValueDesc(Object expect) { + return arrayToSpec(expect); + } + + @Override + public String createDefaultDesc(String property, Object expect, Object actual) { + return String.format("%s全部包含在%s之中", property, createValueDesc(expect)); } }, contains_any("任意包含在...之中", "contains_any", ArrayType.ID) { @Override protected Object convertValue(Object val, Term term) { - return val; + return ConverterUtils.convertToList(val); + } + + @Override + protected String createValueDesc(Object expect) { + return arrayToSpec(expect); + } + + @Override + public String createDefaultDesc(String property, Object expect, Object actual) { + return String.format("%s任意包含在%s之中", property, createValueDesc(expect)); } }, not_contains("不包含在...之中", "not_contains", ArrayType.ID) { @Override protected Object convertValue(Object val, Term term) { - return val; + return ConverterUtils.convertToList(val); + } + + @Override + protected String createValueDesc(Object expect) { + return arrayToSpec(expect); + } + + @Override + public String createDefaultDesc(String property, Object expect, Object actual) { + return String.format("%s不包含在%s之中", property, createValueDesc(expect)); } }, @@ -88,7 +181,7 @@ public enum FixedTermTypeSupport implements TermTypeSupport { nlike("不包含字符", "str_nlike", StringType.ID) { @Override protected Object convertValue(Object val, Term term) { - return like.convertValue(val,term); + return like.convertValue(val, term); } }, @@ -98,16 +191,37 @@ public enum FixedTermTypeSupport implements TermTypeSupport { protected void appendFunction(String column, PrepareSqlFragments fragments) { fragments.addSql("gt(math.divi(math.sub(now(),", column, "),1000),"); } + + @Override + protected String createValueDesc(Object expect) { + return arrayToSpec(expect); + } + + @Override + public String createDefaultDesc(String property, Object expect, Object actual) { + return String.format("%s距离当前时间大于%s秒", property, expect); + } }, time_lt_now("距离当前时间小于...秒", "time_lt_now", DateTimeType.ID) { @Override protected void appendFunction(String column, PrepareSqlFragments fragments) { fragments.addSql("lt(math.divi(math.sub(now(),", column, "),1000),"); } + + @Override + protected String createValueDesc(Object expect) { + return arrayToSpec(expect); + } + + @Override + public String createDefaultDesc(String property, Object expect, Object actual) { + return String.format("%s距离当前时间小于%s秒", property, expect); + } }; private final String text; private final boolean needValue; + private final Set supportTypes; private final String function; @@ -119,6 +233,13 @@ public enum FixedTermTypeSupport implements TermTypeSupport { this.supportTypes = Sets.newHashSet(supportTypes); } + FixedTermTypeSupport(String text, String function, boolean needValue, String... supportTypes) { + this.text = text; + this.function = function; + this.needValue = needValue; + this.supportTypes = Sets.newHashSet(supportTypes); + } + @Override public boolean isSupported(DataType type) { return supportTypes.isEmpty() || supportTypes.contains(type.getType()); @@ -135,7 +256,11 @@ public enum FixedTermTypeSupport implements TermTypeSupport { } protected void appendFunction(String column, PrepareSqlFragments fragments) { - fragments.addSql(function + "(", column, ","); + if (needValue) { + fragments.addSql(function + "(", column, ","); + } else { + fragments.addSql(function + "(", column, ")"); + } } @Override @@ -158,6 +283,20 @@ public enum FixedTermTypeSupport implements TermTypeSupport { return fragments; } + static String arrayToSpec(Object value) { + if (value == null) { + return "[]"; + } + List list = ConverterUtils + .convertToList(value, String::valueOf); + if (list.size() > 8) { + return Stream + .concat(list.stream().limit(8), Stream.of("...")) + .collect(Collectors.joining(",", "[", "]")); + } + return list.toString(); + } + @Override public String getType() { return name(); @@ -167,4 +306,26 @@ public enum FixedTermTypeSupport implements TermTypeSupport { public String getName() { return LocaleUtils.resolveMessage("message.term_type_" + name(), text); } + + protected String createValueDesc(Object expect) { + return String.valueOf(expect); + } + + protected String createDefaultDesc(String property, Object expect, Object actual) { + return String.format("%s%s(%s)", property, getName(), expect); + } + + @Override + public String createDesc(String property, Object expect, Object actual) { + //在国际化资源文件中查找对应的描述 + // {0}=属性名称,{1}=期望值 + //如: message.term_type_neq_desc={0}不等于{1} + return LocaleUtils.resolveMessage( + "message.term_type_" + name() + "_desc", + createDefaultDesc(property, expect, expect), + property, + createValueDesc(expect), + actual + ); + } } diff --git a/jetlinks-components/common-component/src/main/java/org/jetlinks/community/reactorql/term/TermTypeSupport.java b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/reactorql/term/TermTypeSupport.java index 5acc1c72..ecfaa24f 100644 --- a/jetlinks-components/common-component/src/main/java/org/jetlinks/community/reactorql/term/TermTypeSupport.java +++ b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/reactorql/term/TermTypeSupport.java @@ -3,14 +3,23 @@ package org.jetlinks.community.reactorql.term; import lombok.SneakyThrows; import org.hswebframework.ezorm.core.param.Term; import org.hswebframework.ezorm.rdb.operator.builder.fragments.SqlFragments; +import org.hswebframework.web.i18n.LocaleUtils; import org.jetlinks.community.utils.ReactorUtils; import org.jetlinks.core.metadata.DataType; import reactor.core.publisher.Mono; import java.util.Collections; +import java.util.List; import java.util.concurrent.TimeUnit; import java.util.function.BiFunction; +/** + * 查询条件类型支持 + * + * @author zhouhao + * @see org.jetlinks.community.utils.ReactorUtils#createFilter(List) + * @since 2.0 + */ public interface TermTypeSupport { /** @@ -71,8 +80,21 @@ public interface TermTypeSupport { return TermType.of(getType(), getName()); } + default String createDesc(String property, Object expect, Object actual) { - return String.format("%s%s(%s)", property, getName(), expect); + + return LocaleUtils.resolveMessage( + "message.term_" + getType() + "_desc", + String.format("%s%s(%s)", property, getName(), expect), + property, + getName(), + expect, + actual + ); + } + + default String createActualDesc(String property, Object actual) { + return property + " = " + actual; } /** diff --git a/jetlinks-components/common-component/src/main/java/org/jetlinks/community/reactorql/term/TermTypes.java b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/reactorql/term/TermTypes.java index be7fbece..b737a1aa 100644 --- a/jetlinks-components/common-component/src/main/java/org/jetlinks/community/reactorql/term/TermTypes.java +++ b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/reactorql/term/TermTypes.java @@ -18,7 +18,6 @@ public class TermTypes { for (FixedTermTypeSupport value : FixedTermTypeSupport.values()) { register(value); } - register(new ExistsTermSupport()); } public static void register(TermTypeSupport support){ diff --git a/jetlinks-components/common-component/src/main/java/org/jetlinks/community/reactorql/term/TermUtils.java b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/reactorql/term/TermUtils.java new file mode 100644 index 00000000..95484096 --- /dev/null +++ b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/reactorql/term/TermUtils.java @@ -0,0 +1,120 @@ +package org.jetlinks.community.reactorql.term; + +import org.hswebframework.ezorm.core.param.Term; +import org.hswebframework.ezorm.rdb.executor.SqlRequest; +import org.hswebframework.ezorm.rdb.operator.builder.fragments.NativeSql; +import org.jetlinks.community.reactorql.function.FunctionSupport; +import org.springframework.util.StringUtils; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * @author zhangji 2025/2/8 + * @since 2.3 + */ +public class TermUtils { + + public static List expandTermToList(List terms) { + Map> termsMap = expandTerm(terms); + List termList = new ArrayList<>(); + for (List values : termsMap.values()) { + termList.addAll(values); + } + return termList; + } + + public static Map> expandTerm(List terms) { + Map> termCache = new LinkedHashMap<>(); + expandTerm(terms, termCache); + return termCache; + } + + private static void expandTerm(List terms, Map> container) { + if (terms == null) { + return; + } + for (Term term : terms) { + if (StringUtils.hasText(term.getColumn())) { + List termList = container.get(term.getColumn()); + if (termList == null) { + List list = new ArrayList<>(); + list.add(term); + container.put(term.getColumn(), list); + } else { + termList.add(term); + container.put(term.getColumn(), termList); + } + } + if (term.getTerms() != null) { + expandTerm(term.getTerms(), container); + } + } + } + + public static Term refactorTerm(String tableName, + Term term, + BiFunction columnRefactor) { + if (term.getColumn() == null) { + return term; + } + String[] arr = term.getColumn().split("[.]"); + + List values = TermValue.of(term); + if (values.isEmpty()) { + return term; + } + + Function parser = value -> { + //上游变量 + if (value.getSource() == TermValue.Source.variable + || value.getSource() == TermValue.Source.upper) { + term.getOptions().add(TermType.OPTIONS_NATIVE_SQL); + return columnRefactor.apply(tableName, value.getValue().toString()); + } + //指标 + else if (value.getSource() == TermValue.Source.metric) { + term.getOptions().add(TermType.OPTIONS_NATIVE_SQL); + return tableName + "['" + arr[1] + "_metric_" + value.getMetric() + "']"; + } + //函数, 如: array_len() , device_prop() + else if (value.getSource() == TermValue.Source.function) { + SqlRequest request = FunctionSupport + .supports + .getNow(value.getFunction()) + .createSql(columnRefactor.apply(tableName, value.getColumn()), value.getArgs()) + .toRequest(); + return NativeSql.of(request.getSql(), request.getParameters()); + } + //手动设置值 + else { + return value.getValue(); + } + }; + Object val; + if (values.size() == 1) { + val = parser.apply(values.get(0)); + } else { + val = values + .stream() + .map(parser) + .collect(Collectors.toList()); + } + + if (term.getOptions().contains(TermType.OPTIONS_NATIVE_SQL) && !(val instanceof NativeSql)) { + val = NativeSql.of(String.valueOf(val)); + } + + term.setColumn(columnRefactor.apply(tableName, term.getColumn())); + + term.setValue(val); + + return term; + } + +} diff --git a/jetlinks-components/common-component/src/main/java/org/jetlinks/community/reactorql/term/TermValue.java b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/reactorql/term/TermValue.java new file mode 100644 index 00000000..106cc84f --- /dev/null +++ b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/reactorql/term/TermValue.java @@ -0,0 +1,105 @@ +package org.jetlinks.community.reactorql.term; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; +import org.hswebframework.ezorm.core.param.Term; +import org.hswebframework.web.bean.FastBeanCopier; + +import java.io.Serializable; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * + * @author zhangji 2025/2/27 + * @since 2.3 + */ +@Getter +@Setter +public class TermValue implements Serializable { + + private static final long serialVersionUID = 1; + + @Schema(description = "来源") + private Source source; + + @Schema(description = "[source]为[manual]时不能为空") + private Object value; + + @Schema(description = "[source]为[metric]时不能为空") + private String metric; + + @Schema(description = "[source]为[function]时不能为空") + private String function; + + @Schema(description = "[source]为[function]时有效") + private String column; + + @Schema(description = "[source]为[function]时有效") + private Map args; + + public static TermValue manual(Object value) { + TermValue termValue = new TermValue(); + termValue.setValue(value); + termValue.setSource(Source.manual); + return termValue; + } + + public static TermValue metric(String metric) { + TermValue termValue = new TermValue(); + termValue.setMetric(metric); + termValue.setSource(Source.metric); + return termValue; + } + + public static List of(Term term) { + return of(term.getValue()); + } + + public static List of(Object value) { + if (value == null) { + return Collections.emptyList(); + } + if (value instanceof Map) { + return Collections.singletonList(FastBeanCopier.copy(value, new TermValue())); + } + if (value instanceof TermValue) { + return Collections.singletonList(((TermValue) value)); + } + if (value instanceof Collection) { + return ((Collection) value) + .stream() + .flatMap(val -> of(val).stream()) + .collect(Collectors.toList()); + } + return Collections.singletonList(TermValue.manual(value)); + } + + public enum Source { + + /** + * 和manual一样, + * 兼容{@link org.jetlinks.pro.relation.utils.VariableSource.Source#fixed} + */ + fixed, + manual, + + metric, + variable, + /** + * 和variable一样,兼容{@link org.jetlinks.pro.relation.utils.VariableSource.Source#upper} + */ + upper, + + /** + * 函数 + * + * @see org.jetlinks.pro.reactorql.function.FunctionSupport + */ + function + } +} diff --git a/jetlinks-components/common-component/src/main/java/org/jetlinks/community/reference/DataReferenceManager.java b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/reference/DataReferenceManager.java index faf173da..d631d254 100644 --- a/jetlinks-components/common-component/src/main/java/org/jetlinks/community/reference/DataReferenceManager.java +++ b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/reference/DataReferenceManager.java @@ -20,6 +20,8 @@ public interface DataReferenceManager { //数据类型: 设备接入网关 String TYPE_DEVICE_GATEWAY = "device-gateway"; + //数据类型: 产品 + String TYPE_PRODUCT = "product"; //数据类型: 网络组件 String TYPE_NETWORK = "network"; //数据类型:关系配置 @@ -53,7 +55,6 @@ public interface DataReferenceManager { */ Flux getReferences(String dataType); - /** * 断言数据没有被引用,如果存在引用,则抛出异常: {@link DataReferencedException} * @@ -69,4 +70,18 @@ public interface DataReferenceManager { .flatMap(list -> Mono.error(new DataReferencedException(dataType, dataId, list))); } + /** + * 断言数据没有被引用,如果存在引用,则抛出异常: {@link DataReferencedException} + * + * @param dataType 数据类型 + * @param dataId 数据ID + * @return void + */ + default Mono assertNotReferenced(String dataType, String dataId, String code, Object... args) { + return this + .getReferences(dataType, dataId) + .collectList() + .filter(CollectionUtils::isNotEmpty) + .flatMap(list -> Mono.error(new DataReferencedException(dataType, dataId, list, code, args))); + } } diff --git a/jetlinks-components/common-component/src/main/java/org/jetlinks/community/reference/DataReferencedException.java b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/reference/DataReferencedException.java index 33a666c3..cfc3442a 100644 --- a/jetlinks-components/common-component/src/main/java/org/jetlinks/community/reference/DataReferencedException.java +++ b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/reference/DataReferencedException.java @@ -27,4 +27,17 @@ public class DataReferencedException extends I18nSupportException { super.setI18nCode("error.data.referenced"); } + public DataReferencedException(String dataType, + String dataId, + List referenceList, + String code, + Object... args) { + this.dataType = dataType; + this.dataId = dataId; + this.referenceList = referenceList; + + super.setI18nCode(code); + super.setArgs(args); + } + } diff --git a/jetlinks-components/common-component/src/main/java/org/jetlinks/community/terms/I18nSpec.java b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/terms/I18nSpec.java index 7db3b51e..eb349c3d 100644 --- a/jetlinks-components/common-component/src/main/java/org/jetlinks/community/terms/I18nSpec.java +++ b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/terms/I18nSpec.java @@ -101,9 +101,13 @@ public class I18nSpec implements Serializable { private static List of(Object... args) { List codes = new ArrayList<>(); for (Object arg : args) { - I18nSpec i18NSpec = new I18nSpec(); - i18NSpec.setDefaultMessage(arg); - codes.add(i18NSpec); + if (arg instanceof I18nSpec) { + codes.add((I18nSpec)arg); + } else { + I18nSpec i18NSpec = new I18nSpec(); + i18NSpec.setDefaultMessage(arg); + codes.add(i18NSpec); + } } return codes; } diff --git a/jetlinks-components/common-component/src/main/java/org/jetlinks/community/terms/SubTableTermFragmentBuilder.java b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/terms/SubTableTermFragmentBuilder.java new file mode 100644 index 00000000..a65f5ce0 --- /dev/null +++ b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/terms/SubTableTermFragmentBuilder.java @@ -0,0 +1,133 @@ +package org.jetlinks.community.terms; + +import lombok.AllArgsConstructor; +import org.hswebframework.ezorm.core.param.Term; +import org.hswebframework.ezorm.rdb.metadata.RDBColumnMetadata; +import org.hswebframework.ezorm.rdb.metadata.RDBTableMetadata; +import org.hswebframework.ezorm.rdb.metadata.TableOrViewMetadata; +import org.hswebframework.ezorm.rdb.operator.builder.fragments.*; +import org.hswebframework.ezorm.rdb.operator.builder.fragments.term.AbstractTermFragmentBuilder; +import org.jetlinks.community.utils.ConverterUtils; + +import java.util.List; + +/** + * 关联子查询动态条件抽象类,统一封装和另外的表进行关联查询的动态条件. + * + *
{@code
+ *
+ * // 动态参数
+ * {
+ *     "column":"subId",
+ *     "value":"name is 123" //支持的格式见: ConverterUtils.convertTerms
+ * }
+ *
+ * exists(
+ *       select 1 from {SubTableName} _st where _st.{subTableColumn} = t.sub_id
+ *       and _st.name = ?
+ *        )
+ *
+ * }
+ * + * @author zhouhao + * @see ConverterUtils#convertTerms(Object) + * @since 2.2 + */ +public abstract class SubTableTermFragmentBuilder extends AbstractTermFragmentBuilder { + public SubTableTermFragmentBuilder(String termType, String name) { + super(termType, name); + } + + static final SqlFragments AND_L = SqlFragments.single("and ("); + + @Override + public SqlFragments createFragments(String columnFullName, RDBColumnMetadata column, Term term) { + List terms = ConverterUtils.convertTerms(term.getValue()); + String subTableName = getSubTableName(); + + RDBTableMetadata subTable = column + .getOwner() + .getSchema() + .getTable(subTableName, false) + .orElseThrow(() -> new UnsupportedOperationException("unsupported " + getSubTableName())); + + RDBColumnMetadata subTableColumn = subTable.getColumnNow(getSubTableColumn()); + + BatchSqlFragments sqlFragments = new BatchSqlFragments(5, 0); + + if (term.getOptions().contains("not")) { + sqlFragments.add(SqlFragments.NOT); + } + + sqlFragments + .addSql("exists(select 1 from", subTable.getFullName(), getTableAlias(), + "where", subTableColumn.getFullName(getTableAlias()), "=", columnFullName); + + SqlFragments where = builder.createTermFragments(subTable, terms); + if (where.isNotEmpty()) { + sqlFragments + .add(AND_L) + .addFragments(where) + .add(SqlFragments.RIGHT_BRACKET); + } + sqlFragments.add(SqlFragments.RIGHT_BRACKET); + return sqlFragments; + } + + /** + * @return 子表名 + */ + protected abstract String getSubTableName(); + + /** + * @return 子表列名 + */ + protected String getSubTableColumn() { + return "id"; + } + + /** + * @return 子查询别名 + */ + protected String getTableAlias() { + return "_st"; + } + + // 动态条件构造器 + TermsBuilder builder = new TermsBuilder(getTableAlias()); + + @AllArgsConstructor + static class TermsBuilder extends AbstractTermsFragmentBuilder { + + private final String tableAlias; + + @Override + protected SqlFragments createTermFragments(TableOrViewMetadata parameter, + List terms) { + return super.createTermFragments(parameter, terms); + } + + @Override + protected SqlFragments createTermFragments(TableOrViewMetadata table, + Term term) { + if (term.getValue() instanceof NativeSql) { + NativeSql sql = ((NativeSql) term.getValue()); + return SimpleSqlFragments.of(sql.getSql(), sql.getParameters()); + } + RDBColumnMetadata column = table.getColumn(term.getColumn()).orElse(null); + if (column == null) { + return EmptySqlFragments.INSTANCE; + } + + TermFragmentBuilder builder = column + .findFeature(TermFragmentBuilder.createFeatureId(term.getTermType())) + .orElse(null); + + if (builder != null) { + return builder + .createFragments(column.getFullName(tableAlias), column, term); + } + return EmptySqlFragments.INSTANCE; + } + } +} diff --git a/jetlinks-components/common-component/src/main/java/org/jetlinks/community/terms/TermSpec.java b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/terms/TermSpec.java index a4f9d961..37aaaad3 100644 --- a/jetlinks-components/common-component/src/main/java/org/jetlinks/community/terms/TermSpec.java +++ b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/terms/TermSpec.java @@ -11,10 +11,10 @@ import org.hswebframework.ezorm.core.param.TermType; import org.hswebframework.ezorm.rdb.operator.builder.fragments.NativeSql; import org.hswebframework.web.bean.FastBeanCopier; import org.hswebframework.web.i18n.LocaleUtils; -import org.jetlinks.community.reactorql.term.TermTypeSupport; -import org.jetlinks.community.reactorql.term.TermTypes; import org.jetlinks.core.metadata.Jsonable; import org.jetlinks.core.utils.SerializeUtils; +import org.jetlinks.community.reactorql.term.TermTypeSupport; +import org.jetlinks.community.reactorql.term.TermTypes; import org.jetlinks.reactor.ql.supports.DefaultPropertyFeature; import org.springframework.util.StringUtils; @@ -67,6 +67,12 @@ public class TermSpec implements Jsonable, Serializable { @Schema(description = "是否为物模型变量") private boolean metadata; + @Schema(description = "触发条件描述") + private I18nSpec triggerSpec; + + @Schema(description = "实际触发描述") + private I18nSpec actualSpec; + @Override public JSONObject toJson() { @SuppressWarnings("all") @@ -100,11 +106,36 @@ public class TermSpec implements Jsonable, Serializable { .ifPresent(support -> termSpec.matched = support.matchBlocking(termSpec.getExpected(),termSpec.getActual())); } if (termSpec.matched != null && termSpec.matched) { - actualDesc.add(termSpec.getDisplayName() + " = " + termSpec.getActual()); + + actualDesc.add( + termSpec.getActualSpec() != null + ? termSpec.getActualSpec().resolveI18nMessage() + : TermTypes + .lookupSupport(termSpec.getTermType()) + .map(support -> support.createActualDesc(termSpec.getDisplayName(), termSpec.getActual())) + .orElse(termSpec.getDisplayName() + " = " + termSpec.getActual()) + ); } if (CollectionUtils.isNotEmpty(termSpec.children)) { + boolean conditionMatch = true; + Set andConditionDesc = new HashSet<>(); for (TermSpec child : termSpec.children) { - actualDesc.addAll(parseTermSpecActualDesc(child)); + Set desc = parseTermSpecActualDesc(child); + // 任一条件不满足,则不添加and条件的描述 + if (child.getColumn() != null && CollectionUtils.isEmpty(desc)) { + conditionMatch = false; + } + if (child.getType() == Term.Type.or) { + actualDesc.addAll(desc); + } + if (child.getType() == Term.Type.and) { + andConditionDesc.addAll(desc); + } + } + + // 所有条件都满足时,再添加and条件匹配的描述 + if (conditionMatch) { + actualDesc.addAll(andConditionDesc); } } return actualDesc; @@ -115,6 +146,15 @@ public class TermSpec implements Jsonable, Serializable { }); } + public static TermSpec ofTermSpecs(List termSpecs) { + TermSpec spec = new TermSpec(); + if (CollectionUtils.isEmpty(termSpecs)) { + return spec; + } + spec.setChildren(new ArrayList<>(termSpecs)); + return spec; + } + public static List of(List terms, BiConsumer customizer) { if (terms == null) { return null; @@ -156,7 +196,7 @@ public class TermSpec implements Jsonable, Serializable { this.actual = DefaultPropertyFeature .GLOBAL .getProperty(this.column, context) - .orElse(null); + .orElse(this.actual); if (expectIsExpr) { this.expected = DefaultPropertyFeature .GLOBAL @@ -164,10 +204,10 @@ public class TermSpec implements Jsonable, Serializable { .orElse(null); expectIsExpr = false; } + TermTypes .lookupSupport(getTermType()) .ifPresent(support -> this.matched = support.matchBlocking(expected, actual)); - } if (this.children != null) { for (TermSpec child : this.children) { @@ -222,7 +262,11 @@ public class TermSpec implements Jsonable, Serializable { .orElse(null); if (support != null) { hasSpec = true; - builder.append(support.createDesc(getDisplayName(), expected, actual)); + builder.append( + triggerSpec != null + ? triggerSpec.resolveI18nMessage() + : support.createDesc(getDisplayName(), expected, actual) + ); } } List children = compressChildren(); @@ -240,13 +284,27 @@ public class TermSpec implements Jsonable, Serializable { builder.append('!'); } } - builder.append("( "); - toString(builder, children); - builder.append(" )"); + if (StringUtils.hasText(builder)) { + builder.append("( "); + toString(builder, children); + builder.append(" )"); + } else { + toString(builder, children); + } } } + public void compress() { + List children = compressChildren(); + if (children != this.children) { + setChildren(children); + } + for (TermSpec child : children) { + child.compress(); + } + } + public void setActual(Object actual) { this.actual = actual; diff --git a/jetlinks-components/common-component/src/main/java/org/jetlinks/community/topic/Topics.java b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/topic/Topics.java index 10f4d3b8..f042f199 100755 --- a/jetlinks-components/common-component/src/main/java/org/jetlinks/community/topic/Topics.java +++ b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/topic/Topics.java @@ -1,7 +1,17 @@ package org.jetlinks.community.topic; import lombok.Generated; +import org.jetlinks.core.lang.SeparatedCharSequence; +import org.jetlinks.core.lang.SharedPathString; import org.jetlinks.core.utils.StringBuilderUtils; +import org.jetlinks.community.utils.TopicUtils; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Consumer; +import java.util.stream.Collectors; public interface Topics { @@ -17,6 +27,13 @@ public interface Topics { return StringBuilderUtils.buildString(creatorId, topic, Topics::creator); } + static SeparatedCharSequence creator(String creatorId, SeparatedCharSequence topic) { + // /user/{creatorId}/{topic} + return SharedPathString + .of(new String[]{"", "user", creatorId}) + .append(topic); + } + static void creator(String creatorId, String topic, StringBuilder builder) { builder.append("/user/").append(creatorId); if (topic.charAt(0) != '/') { @@ -25,6 +42,101 @@ public interface Topics { builder.append(topic); } + @Deprecated + static String tenant(String tenantId, String topic) { + if (!topic.startsWith("/")) { + topic = "/" + topic; + } + return String.join("", "/tenant/", tenantId, topic); + } + + @Deprecated + static List tenants(List tenants, String topic) { + List topics = new ArrayList<>(tenants.size()); + for (String tenant : tenants) { + topics.add(tenant(tenant, topic)); + } + return topics; + } + + @Deprecated + static String deviceGroup(String groupId, String topic) { + if (!topic.startsWith("/")) { + topic = "/" + topic; + } + return String.join("", "/device-group/", groupId, topic); + + } + + @Deprecated + static List deviceGroups(List groupIds, String topic) { + List topics = new ArrayList<>(groupIds.size()); + for (String groupId : groupIds) { + topics.add(deviceGroup(groupId, topic)); + } + return topics; + } + + /** + * 根据绑定信息构造topic + * + * @param bindings 绑定信息 + * @param topic topic + * @return topic + */ + static List bindings(List> bindings, String topic) { + List topics = new ArrayList<>(bindings.size()); + bindings(bindings, topic, topics::add); + return topics; + } + + static void bindings(List> bindings, + String topic, + Consumer consumer) { + for (Map binding : bindings) { + consumer.accept(binding(String.valueOf(binding.get("type")), String.valueOf(binding.get("id")), topic)); + } + } + + + static void bindings(List> bindings, + SeparatedCharSequence topic, + Consumer consumer) { + for (Map binding : bindings) { + consumer.accept(binding(String.valueOf(binding.get("type")), String.valueOf(binding.get("id")), topic)); + } + } + + static void relations(List> relations, + String topic, + Consumer consumer) { + + for (Map relation : relations) { + consumer.accept( + relation(String.valueOf(relation.get("type")), + String.valueOf(relation.get("id")), + String.valueOf(relation.get("rel")), + topic) + ); + } + + } + + static void relations(List> relations, + SeparatedCharSequence topic, + Consumer consumer) { + + for (Map relation : relations) { + consumer.accept( + relation(String.valueOf(relation.get("type")), + String.valueOf(relation.get("id")), + String.valueOf(relation.get("rel")), + topic) + ); + } + + } + static void binding(String type, String id, String topic, StringBuilder builder) { builder.append('/') .append(type) @@ -63,6 +175,39 @@ public interface Topics { builder.append(topic); } + static String relation(String objectType, String objectId, String relation, String topic) { + return StringBuilderUtils.buildString(objectType, objectId, relation, topic, Topics::relation); + } + + static SeparatedCharSequence relation(String objectType, String objectId, String relation, SeparatedCharSequence topic) { + return SharedPathString.of(new String[]{"", objectType, objectId, relation}).append(topic); + } + + static String binding(String type, String id, String topic) { + return StringBuilderUtils.buildString(type, id, topic, Topics::binding); + } + + static SeparatedCharSequence binding(String type, String id, SeparatedCharSequence topic) { + return SharedPathString.of(new String[]{"", type, id}).append(topic); + } + + + @Deprecated + static String tenantMember(String memberId, String topic) { + if (!topic.startsWith("/")) { + topic = "/" + topic; + } + return String.join("", "/member/", memberId, topic); + } + + @Deprecated + static List tenantMembers(List members, String topic) { + return members + .stream() + .map(id -> tenantMember(id, topic)) + .collect(Collectors.toList()); + } + String allDeviceRegisterEvent = "/_sys/registry-device/*/register"; String allDeviceUnRegisterEvent = "/_sys/registry-device/*/unregister"; String allDeviceMetadataChangedEvent = "/_sys/registry-device/*/metadata"; @@ -110,11 +255,34 @@ public interface Topics { return "/_sys/registry-product/" + deviceId + "/" + event; } + + /** + * 重构topic,将topic拼接上租户等信息的前缀 + * + * @param topic topic + * @param configs 包含的信息 + * @return + */ + @SuppressWarnings("all") + static Set refactorTopic(String topic, Map configs) { + return TopicUtils.refactorTopic(configs, topic); + } + static String alarm(String targetType, String targetId, String alarmId) { // /alarm/{targetType}/{targetId}/{alarmId}/record return String.join("", "/alarm/", targetType, "/", targetId, "/", alarmId, "/record"); } + static String alarmHandleHistory(String targetType, String targetId, String alarmId) { + // /alarm/{targetType}/{targetId}/{alarmId}/handle-history + return String.join("", "/alarm/", targetType, "/", targetId, "/", alarmId, "/handle-history"); + } + + static String alarmLog(String targetType, String targetId, String alarmId, String recordId) { + // /alarm/{targetType}/{targetId}/{alarmId}/{recordId}/log + return String.join("", "/alarm/", targetType, "/", targetId, "/", alarmId, "/", recordId, "/log"); + } + static String alarmRelieve(String targetType, String targetId, String alarmId) { // /alarm/{targetType}/{targetId}/{alarmId}/relieve return String.join("", "/alarm/", targetType, "/", targetId, "/", alarmId, "/relieve"); diff --git a/jetlinks-components/common-component/src/main/java/org/jetlinks/community/utils/ConverterUtils.java b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/utils/ConverterUtils.java index ba4ac3b3..7c860ca5 100644 --- a/jetlinks-components/common-component/src/main/java/org/jetlinks/community/utils/ConverterUtils.java +++ b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/utils/ConverterUtils.java @@ -1,23 +1,33 @@ package org.jetlinks.community.utils; -import com.alibaba.fastjson.JSON; -import com.alibaba.fastjson.JSONArray; +import io.micrometer.core.instrument.Tags; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufUtil; +import io.netty.buffer.Unpooled; +import io.netty.util.CharsetUtil; +import lombok.SneakyThrows; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.collections4.ComparatorUtils; import org.apache.commons.collections4.MapUtils; import org.hswebframework.ezorm.core.param.Sort; import org.hswebframework.ezorm.core.param.Term; -import org.hswebframework.web.api.crud.entity.TermExpressionParser; import org.hswebframework.web.bean.FastBeanCopier; +import org.jetlinks.core.utils.StringBuilderUtils; import org.jetlinks.reactor.ql.utils.CompareUtils; +import org.springframework.util.ConcurrentReferenceHashMap; +import org.springframework.util.StringUtils; import reactor.core.publisher.Flux; +import java.nio.charset.Charset; +import java.nio.charset.CharsetEncoder; import java.util.*; +import java.util.function.BiFunction; import java.util.function.Function; +import java.util.regex.Pattern; import java.util.stream.Collectors; public class ConverterUtils { - + private static final Pattern HEX_PATTERN = Pattern.compile("^[0-9a-fA-F]+$"); /** * 尝试转换值为集合,如果不是集合格式则直接返回该值 * @@ -28,7 +38,7 @@ public class ConverterUtils { public static Object tryConvertToList(Object value, Function converter) { List list = convertToList(value, converter); if (list.size() == 1) { - return list.get(0); + return converter.apply(list.get(0)); } return list; } @@ -92,15 +102,45 @@ public class ConverterUtils { * @return 排序后的流 */ public static Flux convertSortedStream(Flux flux, Collection sorts) { + return convertSortedStream(flux, FastBeanCopier::getProperty, sorts); + } + + /** + * 根据排序参数对指定对Flux流进行排序 + * + * @param flux Flux + * @param sorts 排序参数 + * @param propertyGetter 对比字段获取器,用于获取元素中的字段数据. + * @param 流中元素类型 + * @return 排序后的流 + */ + public static Flux convertSortedStream(Flux flux, + BiFunction propertyGetter, + Collection sorts) { if (CollectionUtils.isEmpty(sorts)) { return flux; } - List> comparators = new ArrayList<>(sorts.size()); + return flux.sort(convertComparator(sorts, propertyGetter)); + } + + /** + * 将排序参数转为Comparator + * + * @param sorts 排序参数 + * @param 元素类型 + * @return Comparator + */ + public static Comparator convertComparator(Collection sorts, + BiFunction propertyGetter) { + if (CollectionUtils.isEmpty(sorts)) { + return Comparator.comparing(k -> 0); + } + List> comparators = new ArrayList<>(sorts.size()); for (Sort sort : sorts) { String column = sort.getName(); - Comparator comparator = (left, right) -> { - Object leftVal = FastBeanCopier.copy(left, new HashMap<>()).get(column); - Object rightVal = FastBeanCopier.copy(right, new HashMap<>()).get(column); + Comparator comparator = (left, right) -> { + Object leftVal = propertyGetter.apply(left, column); + Object rightVal = propertyGetter.apply(right, column); return CompareUtils.compare(leftVal, rightVal); }; if (sort.getOrder().equalsIgnoreCase("desc")) { @@ -109,7 +149,18 @@ public class ConverterUtils { comparators.add(comparator); } - return flux.sort(ComparatorUtils.chainedComparator(comparators)); + return ComparatorUtils.chainedComparator(comparators); + } + + private static final String[] EMPTY_TAG = new String[0]; + + private static final Map, Tags> tagCache = new ConcurrentReferenceHashMap<>(); + + public static Tags convertMapToTagsInfo(Map map) { + if (MapUtils.isEmpty(map)) { + return Tags.empty(); + } + return tagCache.computeIfAbsent(map, m -> Tags.of(convertMapToTags(m))); } /** @@ -121,29 +172,9 @@ public class ConverterUtils { * @param map map * @return tags */ + @SneakyThrows public static String[] convertMapToTags(Map map) { - if (MapUtils.isEmpty(map)) { - return new String[0]; - } - String[] tags = new String[map.size() * 2]; - int index = 0; - for (Map.Entry entry : map.entrySet()) { - String key = entry.getKey(); - Object value = entry.getValue(); - if (value == null) { - continue; - } - String strValue = value instanceof String - ? String.valueOf(value) - : JSON.toJSONString(value); - - tags[index++] = key; - tags[index++] = strValue; - } - if (tags.length > index) { - return Arrays.copyOf(tags, index); - } - return tags; + return org.jetlinks.sdk.server.utils.ConverterUtils.convertMapToTags(map); } /** @@ -152,27 +183,146 @@ public class ConverterUtils { * //name = xxx and age > 10 * convertTerms("name is xxx and age gt 10") * + * convertTerms({"name":"xxx","age$gt":10}) * * * @param value * @return 条件集合 */ @SuppressWarnings("all") + @SneakyThrows public static List convertTerms(Object value) { - if (value instanceof String) { - String strVal = String.valueOf(value); - //json字符串 - if (strVal.startsWith("[")) { - value = JSON.parseArray(strVal); + return org.jetlinks.sdk.server.utils.ConverterUtils.convertTerms(value); + } + + public static String byteBufToString(ByteBuf buf, + Charset charset) { + return StringBuilderUtils + .buildString( + buf, charset, + (_buf, _charset, builder) -> byteBufToString(builder, _buf, _charset)); + } + + + /** + * 将字符串转为ByteBuf,字符串中包含\x开头的内容将按16进制解析为byte. + * + * @param str 字符串 + * @param charset 字符集 + */ + public static ByteBuf stringToByteBuf(CharSequence str, Charset charset) { + ByteBuf buf = Unpooled.buffer(str.length()); + stringToByteBuf(str, buf, charset); + return buf; + } + + /** + * 将字符串转为ByteBuf,字符串中包含\x开头的内容将按16进制解析为byte. + * + * @param str 字符串 + * @param buf ByteBuf + * @param charset 字符集 + */ + public static void stringToByteBuf(CharSequence str, + ByteBuf buf, + Charset charset) { + + int idx = 0; + int len = str.length(); + while (idx < len) { + char c = str.charAt(idx); + if (c == '\\' && str.charAt(idx + 1) == 'x') { + buf.writeByte(ByteBufUtil.decodeHexByte(str, idx + 2)); + idx += 4; } else { - //表达式 - return TermExpressionParser.parse(strVal); + buf.writeCharSequence(String.valueOf(c), charset); + idx++; } } - if (value instanceof List) { - return new JSONArray(((List) value)).toJavaList(Term.class); - } else { - throw new UnsupportedOperationException("unsupported term value:" + value); + + } + + private static final Set + visibleChar = new HashSet<>( + Arrays.asList( + ' ', '!', '"', '#', '$', '%', '&', '\'', '(', ')', '*', '+', ',', '-', '.', '/', + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', ':', ';', '<', '=', '>', '?', + '@', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', + '[', '\\', ']', '^', '_', '`', + 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', + '{', '|', '}', '~', '`', '[', ']', '{', '}', ';', '\'', ',', '.', '/', '?', '<', '>', '~', '!', '@', '#', '$', '%', '^', '&', '*', '(', ')', '-', '_', '+', '=', '|', + '·', '!', '@', '#', '¥', '%', '…', '&', '*', '(', ')', '—', '+', '|', ':', '“', '”', '《', '》', '?', '、', + '。', ',', ';', '‘', '’', '【', '】', '、', '·', '~', '·', '!', '@', '#', '¥', '%', '…', '&', '*', '(', ')', + '—', '+', '|', ':', '“', '”' + ) + ); + + + /** + * 将netty的ByteBuf转为字符串,buf中包含非指定字符集的字符,则转换为16进制如: \x00 + * + * @param builder StringBuilder 用于接收字符串 + * @param buf ByteBuf + * @param charset 字符集 + */ + public static void byteBufToString(StringBuilder builder, + ByteBuf buf, + Charset charset) { + + + int len = buf.readableBytes(); + int idx = buf.readerIndex(); + + CharsetEncoder encoder = CharsetUtil.encoder(charset); + int avgPerChar = (int) encoder.averageBytesPerChar(); + + int maxPerChar = (int) encoder.maxBytesPerChar(); + + while (len > 0) { + + if (len >= avgPerChar && ByteBufUtil.isText(buf, idx, avgPerChar, charset)) { + CharSequence cs = buf.getCharSequence(idx, avgPerChar, charset); + int clen = cs.length(); + for (int i = 0; i < clen; i++) { + char c = cs.charAt(i); + if (visibleChar.contains(c)) { + builder.append(c); + } else { + builder + .append("\\x") + .append(ByteBufUtil.hexDump(buf, idx + i, 1)); + } + } + len -= avgPerChar; + idx += avgPerChar; + continue; + } + + + if (len >= maxPerChar && ByteBufUtil.isText(buf, idx, maxPerChar, charset)) { + CharSequence cs = buf.getCharSequence(idx, maxPerChar, charset); + builder.append(cs); + len -= maxPerChar; + idx += maxPerChar; + continue; + } + + //不可识别的转为hex + builder + .append("\\x") + .append(ByteBufUtil.hexDump(buf, idx, 1)); + len--; + idx++; } } + + public static ByteBuf convertBuffer(Object obj) { + return org.jetlinks.sdk.server.utils.ConverterUtils.convertNettyBuffer(obj); + } + + public static boolean isHexEncoded(String str) { + return StringUtils.hasText(str) && + str.length() % 2 == 0 && + HEX_PATTERN.matcher(str).matches(); + } } diff --git a/jetlinks-components/common-component/src/main/java/org/jetlinks/community/utils/FormatUtils.java b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/utils/FormatUtils.java new file mode 100644 index 00000000..d1ccfa17 --- /dev/null +++ b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/utils/FormatUtils.java @@ -0,0 +1,44 @@ +package org.jetlinks.community.utils; + +/** + * @author gyl + * @since 2.0 + */ +public class FormatUtils { + + private static final String[] formats = {"B", "KB", "MB", "GB"}; + + /** + * 单位为字节的大小转换为最大单位 + * + * @param bytes + * @return + */ + public static String formatDataSize(long bytes) { + int i = 0; + float total = bytes; + while (total >= 1024 && i < formats.length - 1) { + total /= 1024; + i++; + } + + return String.format("%.2f%s", total, formats[i]); + } + + + /** + * 将毫秒格式化为x小时x分钟x秒表示 + * + * @param diffTime + * @return + */ + public static String calculateLifeTime(long diffTime) { + long hours = diffTime / 3600000; + long minutes = (diffTime % 3600000) / 60000; + long seconds = (diffTime % 60000) / 1000; + return hours + "小时" + minutes + "分钟" + seconds + "秒"; + } + + + +} diff --git a/jetlinks-components/common-component/src/main/java/org/jetlinks/community/utils/TopicUtils.java b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/utils/TopicUtils.java new file mode 100644 index 00000000..d49e9287 --- /dev/null +++ b/jetlinks-components/common-component/src/main/java/org/jetlinks/community/utils/TopicUtils.java @@ -0,0 +1,344 @@ +package org.jetlinks.community.utils; + +import com.google.common.collect.Sets; +import lombok.SneakyThrows; +import org.apache.commons.collections4.MapUtils; +import org.jetlinks.community.PropertyConstants; +import org.jetlinks.community.topic.Topics; +import org.jetlinks.core.lang.SeparatedCharSequence; +import org.jetlinks.core.lang.SharedPathString; +import org.jetlinks.core.message.*; +import org.jetlinks.core.message.collector.ReportCollectorDataMessage; +import org.jetlinks.core.message.event.ThingEventMessage; +import org.jetlinks.core.message.module.ThingModuleMessage; +import org.jetlinks.core.utils.StringBuilderUtils; +import org.springframework.util.StringUtils; +import reactor.function.Consumer3; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.function.BiConsumer; +import java.util.function.BiFunction; +import java.util.function.Consumer; + +public class TopicUtils { + + public static final List, String, Consumer>> TOPIC_REFACTOR_HOOK + = new CopyOnWriteArrayList<>(); + + public static Set refactorTopic(Map configs, String original) { + if (MapUtils.isEmpty(configs)) { + return Collections.singleton(original); + } + + Set topics = Sets.newHashSetWithExpectedSize(2); + //原始的topic + topics.add(original); + + //创建人 + String creatorId = PropertyConstants.getFromMapOrElse(PropertyConstants.creatorId, configs, () -> null); + if (StringUtils.hasText(creatorId)) { + topics.add(org.jetlinks.community.topic.Topics.creator(creatorId, original)); + } + + // 执行hooks + if (!TOPIC_REFACTOR_HOOK.isEmpty()) { + for (Consumer3, String, Consumer> hook : TOPIC_REFACTOR_HOOK) { + hook.accept(configs, original, topics::add); + } + } + return topics; + } + + public static Set refactorTopic(DeviceMessage message, SeparatedCharSequence original) { + return refactorTopic(message.getHeaders(), original); + } + + public static Set refactorTopic(Map configs, SeparatedCharSequence original) { + if (MapUtils.isEmpty(configs)) { + return Collections.singleton(original); + } + + Set container = Sets.newHashSetWithExpectedSize(2); + container.add(original); + + //创建人 + String creatorId = PropertyConstants.getFromMapOrElse(PropertyConstants.creatorId, configs, () -> null); + if (StringUtils.hasText(creatorId)) { + container.add( + Topics.creator(creatorId, original) + ); + } + + // 执行hooks + if (!TOPIC_REFACTOR_HOOK.isEmpty()) { + for (Consumer3, String, Consumer> hook : TOPIC_REFACTOR_HOOK) { + hook.accept(configs, original.toString(), (str) -> container.add(SharedPathString.of(str))); + } + } + return container; + } + + + public static Set refactorTopic(ThingMessage message, String original) { + return refactorTopic(message.getHeaders(), original); + } + + private static final TopicBuilder[] TOPIC_BUILDERS; + + static { + TOPIC_BUILDERS = new TopicBuilder[MessageType.values().length]; + + { + SharedPathString shared = SharedPathString.of("/message/event"); + //事件 + createFastBuilder(MessageType.EVENT, + (message, builder) -> { + ThingEventMessage event = ((ThingEventMessage) message); + builder.append("/message/event/").append(event.getEvent()); + }, + (message, charSequences) -> { + ThingEventMessage event = ((ThingEventMessage) message); + return charSequences.isEmpty() + ? shared.append(event.getEvent()) + : charSequences.append(shared).append(event.getEvent()); + }); + + } + //上报属性 + createFastBuilder(MessageType.REPORT_PROPERTY, "/message/property/report"); + //读取属性 + createFastBuilder(MessageType.READ_PROPERTY, "/message/send/property/read"); + //读取属性回复 + createFastBuilder(MessageType.READ_PROPERTY_REPLY, "/message/property/read/reply"); + //修改属性 + createFastBuilder(MessageType.WRITE_PROPERTY, "/message/send/property/write"); + //修改属性回复 + createFastBuilder(MessageType.WRITE_PROPERTY_REPLY, "/message/property/write/reply"); + //调用功能 + createFastBuilder(MessageType.INVOKE_FUNCTION, "/message/send/function"); + //调用功能回复 + createFastBuilder(MessageType.INVOKE_FUNCTION_REPLY, "/message/function/reply"); + //注册 + createFastBuilder(MessageType.REGISTER, "/register"); + //注销 + createFastBuilder(MessageType.UN_REGISTER, "/unregister"); + //拉取固件 + createFastBuilder(MessageType.REQUEST_FIRMWARE, "/firmware/pull"); + //拉取固件回复 + createFastBuilder(MessageType.REQUEST_FIRMWARE_REPLY, "/firmware/pull/reply"); + //上报固件信息 + createFastBuilder(MessageType.REPORT_FIRMWARE, "/firmware/report"); + //上报固件安装进度 + createFastBuilder(MessageType.UPGRADE_FIRMWARE_PROGRESS, "/firmware/progress"); + //推送固件 + createFastBuilder(MessageType.UPGRADE_FIRMWARE, "/firmware/push"); + //推送固件回复 + createFastBuilder(MessageType.UPGRADE_FIRMWARE_REPLY, "/firmware/push/reply"); + //未知 + createFastBuilder(MessageType.UNKNOWN, "/message/unknown"); + //应答消息 since 1.8 + createFastBuilder(MessageType.ACKNOWLEDGE, "/message/acknowledge"); + //日志 + createFastBuilder(MessageType.LOG, "/message/log"); + //透传 + createFastBuilder(MessageType.DIRECT, "/message/direct"); + //更新标签 + createFastBuilder(MessageType.UPDATE_TAG, "/message/tags/update"); + //上线 + createFastBuilder(MessageType.ONLINE, "/online"); + //离线 + createFastBuilder(MessageType.OFFLINE, "/offline"); + //断开连接 + createFastBuilder(MessageType.DISCONNECT, "/disconnect"); + //断开连接回复 + createFastBuilder(MessageType.DISCONNECT_REPLY, "/disconnect/reply"); + { + SharedPathString shared = SharedPathString.of("/message/children"); + //子设备消息 + createFastBuilder(MessageType.CHILD, (message, builder) -> { + Message msg = ((ChildDeviceMessage) message).getChildDeviceMessage(); + if (msg instanceof ThingMessage) { + builder.append("/message/children/") + .append(((ThingMessage) msg).getThingId()); + } else { + builder.append("/message/children"); + } + appendMessageTopic(msg, builder); + }, (message, builder) -> { + Message msg = ((ChildDeviceMessage) message).getChildDeviceMessage(); + if (msg instanceof ThingMessage) { + String thingId = ((ThingMessage) msg).getThingId(); + builder = builder.isEmpty() ? shared.append(thingId) : builder.append(shared).append(thingId); + } else { + builder = builder.isEmpty() ? shared : builder.append(shared); + } + return createMessageTopic(msg, builder); + }); + } + { + SharedPathString shared = SharedPathString.of("/message/children/reply"); + //子设备消息回复 + createFastBuilder(MessageType.CHILD_REPLY, + (message, builder) -> { + Message msg = ((ChildDeviceMessageReply) message).getChildDeviceMessage(); + if (msg instanceof ThingMessage) { + builder.append("/message/children/reply/") + .append(((ThingMessage) msg).getThingId()); + } else { + builder.append("/message/children/reply"); + } + appendMessageTopic(msg, builder); + }, + (message, builder) -> { + Message msg = ((ChildDeviceMessageReply) message).getChildDeviceMessage(); + if (msg instanceof ThingMessage) { + String thingId = ((ThingMessage) msg).getThingId(); + builder = builder.isEmpty() ? shared.append(thingId) : builder.append(shared).append(thingId); + } else { + builder = builder.isEmpty() ? shared : builder.append(shared); + } + return createMessageTopic(msg, builder); + }); + } + //上报了新的物模型 + createFastBuilder(MessageType.DERIVED_METADATA, "/metadata/derived"); + //状态检查 + createFastBuilder(MessageType.STATE_CHECK, "/message/state_check"); + createFastBuilder(MessageType.STATE_CHECK_REPLY, "/message/state_check_reply"); + + { + SharedPathString shared = SharedPathString.of("/message/collector/report"); + //数采相关消息 since 2.1 + createFastBuilder( + MessageType.REPORT_COLLECTOR, + ((message, stringBuilder) -> { + String addr = message.getHeaderOrElse(ReportCollectorDataMessage.ADDRESS, null); + stringBuilder.append("/message/collector/report"); + if (StringUtils.hasText(addr)) { + if (!addr.startsWith("/")) { + stringBuilder.append('/'); + } + stringBuilder.append(addr); + } + }), + (message, charSequences) -> { + String addr = message.getHeaderOrElse(ReportCollectorDataMessage.ADDRESS, null); + charSequences = charSequences.append(shared); + if (StringUtils.hasText(addr)) { + charSequences = charSequences.append(SharedPathString.of(addr)); + } + return charSequences; + }); + } + createFastBuilder(MessageType.READ_COLLECTOR_DATA, "/message/collector/read"); + createFastBuilder(MessageType.READ_COLLECTOR_DATA_REPLY, "/message/collector/read/reply"); + createFastBuilder(MessageType.WRITE_COLLECTOR_DATA, "/message/collector/write"); + createFastBuilder(MessageType.WRITE_COLLECTOR_DATA_REPLY, "/message/collector/write/reply"); + + //模块消息 since 2.3 + createFastBuilder(MessageType.MODULE, + (message, builder) -> { + ThingModuleMessage msg = ((ThingModuleMessage) message); + + builder.append("/module/").append(msg.getModule()); + + appendMessageTopic(msg.getMessage(), builder); + }, + (message, builder) -> { + ThingModuleMessage msg = ((ThingModuleMessage) message); + if (builder.isEmpty()) { + builder = SharedPathString.of(new String[]{ + "", "module", msg.getModule() + }); + } else { + builder = builder + .append("module") + .append(SharedPathString.of(msg.getModule())); + } + return createMessageTopic(msg.getMessage(), builder); + }); + } + + private static void createFastBuilder(MessageType messageType, + String topic) { + SharedPathString shared = SharedPathString.of(topic); + TOPIC_BUILDERS[messageType.ordinal()] = new TopicBuilder() { + @Override + public void build(Message message, StringBuilder builder) { + builder.append(topic); + } + + @Override + public SeparatedCharSequence build(Message message, SeparatedCharSequence prefix) { + return prefix.isEmpty() ? shared : prefix.append(shared); + } + }; + } + + private static void createFastBuilder(MessageType messageType, + BiConsumer builderBiConsumer, + BiFunction builderBiConsumer2) { + TOPIC_BUILDERS[messageType.ordinal()] = new TopicBuilder() { + @Override + public void build(Message message, StringBuilder builder) { + builderBiConsumer.accept(message, builder); + } + + @Override + public SeparatedCharSequence build(Message message, SeparatedCharSequence prefix) { + return builderBiConsumer2.apply(message, prefix); + } + }; + } + + public static String createMessageTopic(Message message, String prefix) { + return StringBuilderUtils + .buildString(message, prefix, (msg, _prefix, builder) -> { + builder.append(_prefix); + TopicUtils.appendMessageTopic(msg, builder); + }); + } + + @SneakyThrows + public static SeparatedCharSequence createMessageTopic(Message message, + SeparatedCharSequence prefix) { + //根据消息类型枚举快速获取拼接器器 + TopicBuilder fastBuilder = TOPIC_BUILDERS[message.getMessageType().ordinal()]; + if (null != fastBuilder) { + //执行拼接 + return fastBuilder.build(message, prefix); + } else { + //不支持的类型,则直接拼接类型 + return prefix.append("message", message.getMessageType().name().toLowerCase()); + } + } + + @SneakyThrows + public static void appendMessageTopic(Message message, StringBuilder builder) { + //根据消息类型枚举快速获取拼接器器 + TopicBuilder fastBuilder = TOPIC_BUILDERS[message.getMessageType().ordinal()]; + if (null != fastBuilder) { + //执行拼接 + fastBuilder.build(message, builder); + } else { + //不支持的类型,则直接拼接类型 + builder.append("/message/").append(message.getMessageType().name().toLowerCase()); + } + } + + private interface TopicBuilder { + void build(Message message, StringBuilder builder); + + + default SeparatedCharSequence build(Message message, SeparatedCharSequence prefix) { + return prefix.append( + SharedPathString.of(StringBuilderUtils.buildString(message, this::build)) + ); + } + } + +} diff --git a/jetlinks-components/common-component/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/jetlinks-components/common-component/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 00000000..c761205a --- /dev/null +++ b/jetlinks-components/common-component/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +org.jetlinks.community.configuration.CommonConfiguration \ No newline at end of file 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 b5e10f58..b07bf062 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,14 +1,129 @@ message.device_message_handing=Message sent to device, processing... +message.value_cannot_be_empty=Value cannot be empty +message.value_max_valida_not_passed=The maximum value check fail +message.value_min_valida_not_passed=The minimum value check fail +message.value_range_valida_not_passed=The range value check fail +message.value_must_conform_regular_check=The value must conform to the check regular +message.validator_not_passed=Validator check fail +message.validator_is_passed=Validator check passed +message.term-type-and=and +message.term-type-or=or error.duplicate_key_detail=Duplicate Data:{0} error.data.referenced=The data has been used elsewhere +error.scene_rule_timer_cron_cannot_be_empty=The cron cannot be null +error.entity_template_exist=The entity template already exists error.base_path_error=base-path error.\ - format:{http/https}://{IP address of the server where the front-end is located}:{Front end exposed service port}/api + format:{http/https}://{IP address of the server where the front-end is located}:{Front end exposed service \ + port}/{Front end agent api} error.base_path_DNS_resolution_failed=base-path DNS resolution failed.\ - format:{http/https}://{IP address of the server where the front-end is located}:{Front end exposed service port}/api + format:{http/https}://{IP address of the server where the front-end is located}:{Front end exposed service \ + port}/{Front end agent api} error.base_path_validate_request_timeout=base-path validate request timeout,\ Please check if the internal access of the server can be done normally using the base path address - error.base_path_host_error=The HOST for base path cannot be:{0},please use an IPV4 address or domain name.\ Example:192.168.x.x -error.jvm_error=System Internal error \ No newline at end of file +error.user_binding_code_incorrect=The user binding code has expired or is incorrect +error.illegal_bind=illegal bind +error.jvm_error=System Internal error +error.system_dictionary_can_not_delete=Operation failed, system dictionary cannot be deleted +error.system_dictionary_can_not_update=Operation failed, system dictionary cannot be modify + +message.timer_spec_desc_seperator=\u0020and\u0020 +message.timer_spec_desc_everyday=everyday +message.timer_spec_desc_everyweek=on every {0} +message.timer_spec_desc_everymonth=on the {0} of each month +message.timer_spec_desc=Execute once {1} {0} +message.timer_spec_desc_period= every {1}{2} {0} +message.timer_spec_desc_period_duration=from {0} to {1} +message.timer_spec_desc_period_once=at {0} +message.timer_spec_desc_calendar=calendar trigger with {0} rule(s) +message.timer_spec_desc_week_1=monday +message.timer_spec_desc_week_2=tuesday +message.timer_spec_desc_week_3=wednesday +message.timer_spec_desc_week_4=thursday +message.timer_spec_desc_week_5=friday +message.timer_spec_desc_week_6=saturday +message.timer_spec_desc_week_7=Sunday +message.timer_spec_desc_month={0}th +message.timer_spec_desc_month_1=1st +message.timer_spec_desc_month_2=2nd +message.timer_spec_desc_month_3=3rd +message.timer_spec_desc_period_hours=\u0020hour +message.timer_spec_desc_period_minutes=\u0020minute +message.timer_spec_desc_period_seconds=\u0020second +error.the_template_is_enabled_and_cannot_be_deleted=The template[{0}] is enabled and cannot be deleted +error.exception.illegal_argument=Illegal argument +error.exception.unsupported_operation=Unsupported operation +error.exception.timeout=Timeout +error.internal_error=Internal error + +message.system.constant.dict.name=classification of system constants +message.calendar.tags.dict.item.weekend=weekend +message.calendar.tags.dict.item.workday=workday +message.calendar.tags.dict.item.holiday=holiday +message.calendar.tags.dict.name=schedule tags +message.system.constant.dict.item.default.name=default + +hswebframework.web.system.dictionary.schedule-tags=Calendar Tags +hswebframework.web.system.dictionary.item.workday=Workday +hswebframework.web.system.dictionary.item.weekend=Weekend +hswebframework.web.system.dictionary.item.holiday=Holiday + +hswebframework.web.system.action.query=Query +hswebframework.web.system.action.save=Save +hswebframework.web.system.action.delete=Delete +hswebframework.web.system.action.upload-static=Static File +hswebframework.web.system.permission.system_environment=System Environment Parameters +hswebframework.web.system.permission.system-monitor=System Internal Monitoring +hswebframework.web.system.permission.system-resources=System Resources +hswebframework.web.system.permission.system_config=System Configuration Management +hswebframework.web.system.permission.entity-template=Resource Library(Old) +hswebframework.web.system.permission.system_constant=System Constant Configuration Management +hswebframework.web.system.permission.calendar-manager=Calendar Management + +#FixedTermType +message.term_type_eq=Equals +message.term_type_neq=Not equal +message.term_type_notnull=Is not empty +message.term_type_isnull=Is empty +message.term_type_gt=Greater than +message.term_type_gte=Greater than or equal to +message.term_type_lt=Less than +message.term_type_lte=Less than or equal to +message.term_type_btw=Between +message.term_type_nbtw=Not between +message.term_type_in=In +message.term_type_nin=Not in +message.term_type_contains_all=Include all +message.term_type_contains_any=Include any +message.term_type_not_contains=Exclude +message.term_type_like=Like +message.term_type_nlike=Not like +message.term_type_time_gt_now=Time greater than now +message.term_type_time_lt_now=Time less than now +message.term_type_complex_exists=Complex exists + +message.term_type_eq_desc={0} equals {1} +message.term_type_neq_desc={0} not equal to {1} +message.term_type_notnull_desc={0} is not null {1} +message.term_type_isnull_desc={0} is null {1} +message.term_type_gt_desc={0} is greater than {1} +message.term_type_gte_desc={0} is greater than or equal to {1} +message.term_type_lt_desc={0} is less than {1} +message.term_type_lte_desc={0} is less than or equal to {1} +message.term_type_btw_desc={0} is between {1} +message.term_type_nbtw_desc={0} is not between {1} +message.term_type_in_desc={0} is in {1} +message.term_type_nin_desc={0} is not in {1} +message.term_type_contains_all_desc={0} contains all in {1} +message.term_type_contains_any_desc={0} contains any in {1} +message.term_type_not_contains_desc={0} does not contain in {1} +message.term_type_like_desc={0} contains character {1} +message.term_type_nlike_desc={0} does not contain character {1} +message.term_type_time_gt_now_desc={0} is greater than {1} seconds from current time +message.term_type_time_lt_now_desc={0} is less than {1} seconds from current time +message.term_complex_exists_desc={0} is {1} + +org.jetlinks.community.template.EntityTemplateState.enabled=Enabled +org.jetlinks.community.template.EntityTemplateState.disabled=Disabled \ 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 bad8ee29..2017e356 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,8 +1,124 @@ -message.device_message_handing=\u6D88\u606F\u5DF2\u53D1\u5F80\u8BBE\u5907\uFF0C\u5904\u7406\u4E2D... -error.data.referenced=\u6570\u636E\u5DF2\u7ECF\u88AB\u5176\u4ED6\u5730\u65B9\u4F7F\u7528 + +message.device_message_handing=\u6D88\u606F\u5DF2\u53D1\u5F80\u8BBE\u5907,\u5904\u7406\u4E2D... +message.value_cannot_be_empty=\u503C\u4E0D\u80FD\u4E3A\u7A7A +message.value_max_valida_not_passed=\u6700\u5927\u503C\u6821\u9A8C\u672A\u901A\u8FC7 +message.value_min_valida_not_passed=\u6700\u5C0F\u503C\u6821\u9A8C\u672A\u901A\u8FC7 +message.value_range_valida_not_passed=\u8303\u56F4\u503C\u6821\u9A8C\u672A\u901A\u8FC7 +message.value_must_conform_regular_check=\u503C\u9700\u7B26\u5408\u6821\u9A8C\u6B63\u5219 +message.validator_not_passed=\u68C0\u9A8C\u5668\u68C0\u67E5\u4E0D\u901A\u8FC7 +message.validator_is_passed=\u68C0\u9A8C\u5668\u68C0\u67E5\u901A\u8FC7 +message.term-type-and=\u5E76\u4E14 +message.term-type-or=\u6216\u8005 + error.duplicate_key_detail=\u91CD\u590D\u7684\u6570\u636E:{0} -error.base_path_error=base-path\u9519\u8BEF\uFF0C\u6B63\u786E\u683C\u5F0F:{http/https}://{\u524D\u7AEF\u6240\u5728\u670D\u52A1\u5668IP\u5730\u5740}:{\u524D\u7AEF\u66B4\u9732\u7684\u670D\u52A1\u7AEF\u53E3}/api -error.base_path_DNS_resolution_failed=base-path DNS\u89E3\u6790\u5931\u8D25\uFF0C\u6B63\u786E\u683C\u5F0F:{http/https}://{\u524D\u7AEF\u6240\u5728\u670D\u52A1\u5668IP\u5730\u5740}:{\u524D\u7AEF\u66B4\u9732\u7684\u670D\u52A1\u7AEF\u53E3}/api +error.data.referenced=\u6570\u636E\u5DF2\u7ECF\u88AB\u5176\u4ED6\u5730\u65B9\u4F7F\u7528 +error.scene_rule_timer_cron_cannot_be_empty=cron\u8868\u8FBE\u5F0F\u4E0D\u80FD\u4E3A\u7A7A +error.entity_template_exist=\u5B9E\u4F53\u6A21\u677F\u5DF2\u5B58\u5728 +error.base_path_error=base-path\u9519\u8BEF\uFF0C\u6B63\u786E\u683C\u5F0F:{http/https}://{\u524D\u7AEF\u6240\u5728\u670D\u52A1\u5668IP\u5730\u5740}:{\u524D\u7AEF\u66B4\u9732\u7684\u670D\u52A1\u7AEF\u53E3}/{\u524D\u7AEF\u4EE3\u7406api} +error.base_path_DNS_resolution_failed=base-path DNS\u89E3\u6790\u5931\u8D25\uFF0C\u6B63\u786E\u683C\u5F0F:{http/https}://{\u524D\u7AEF\u6240\u5728\u670D\u52A1\u5668IP\u5730\u5740}:{\u524D\u7AEF\u66B4\u9732\u7684\u670D\u52A1\u7AEF\u53E3}/{\u524D\u7AEF\u4EE3\u7406api} error.base_path_validate_request_timeout=base-path\u8BF7\u6C42\u9A8C\u8BC1\u8D85\u65F6\uFF0C\u8BF7\u68C0\u67E5\u670D\u52A1\u5668\u5185\u90E8\u8BBF\u95EE\u662F\u5426\u80FD\u6B63\u5E38\u65B9\u5F0Fbase-path\u5730\u5740 error.base_path_host_error=base-path\u7684HOST\u4E0D\u80FD\u4E3A:{0}\uFF0C\u8BF7\u4F7F\u7528IPv4\u5730\u5740\u6216\u57DF\u540D.\u5982:192.168.x.x -error.jvm_error=\u7CFB\u7EDF\u5185\u90E8\u9519\u8BEF \ No newline at end of file +error.user_binding_code_incorrect=\u7528\u6237\u7ED1\u5B9A\u7801\u5DF2\u8FC7\u671F\u6216\u9519\u8BEF +error.illegal_bind=\u975E\u6CD5\u7ED1\u5B9A +error.jvm_error=\u7CFB\u7EDF\u5185\u90E8\u9519\u8BEF +error.system_dictionary_can_not_delete=\u64CD\u4F5C\u5931\u8D25\uFF0C\u7CFB\u7EDF\u5B57\u5178\u65E0\u6CD5\u5220\u9664 +error.system_dictionary_can_not_update=\u64CD\u4F5C\u5931\u8D25\uFF0C\u7CFB\u7EDF\u5B57\u5178\u7981\u6B62\u4FEE\u6539 + +message.timer_spec_desc_seperator=\uFF0C +message.timer_spec_desc_everyday=\u6BCF\u5929 +message.timer_spec_desc_everyweek=\u6BCF\u5468{0} +message.timer_spec_desc_everymonth=\u6BCF\u6708{0} +message.timer_spec_desc={0} {1}\u6267\u884C1\u6B21 +message.timer_spec_desc_period={0} \u6BCF{1}{2} +message.timer_spec_desc_period_duration={0}-{1} +message.timer_spec_desc_period_once={0} +message.timer_spec_desc_calendar=\u81EA\u5B9A\u4E49\u65E5\u5386 \u5171{0}\u4E2A\u89C4\u5219 +message.timer_spec_desc_week_1=\u661F\u671F\u4E00 +message.timer_spec_desc_week_2=\u661F\u671F\u4E8C +message.timer_spec_desc_week_3=\u661F\u671F\u4E09 +message.timer_spec_desc_week_4=\u661F\u671F\u56DB +message.timer_spec_desc_week_5=\u661F\u671F\u4E94 +message.timer_spec_desc_week_6=\u661F\u671F\u516D +message.timer_spec_desc_week_7=\u661F\u671F\u65E5 +message.timer_spec_desc_month={0}\u53F7 +message.timer_spec_desc_month_1={0}\u53F7 +message.timer_spec_desc_month_2={0}\u53F7 +message.timer_spec_desc_month_3={0}\u53F7 +message.timer_spec_desc_period_hours=\u5C0F\u65F6 +message.timer_spec_desc_period_minutes=\u5206\u949F +message.timer_spec_desc_period_seconds=\u79D2 +error.the_template_is_enabled_and_cannot_be_deleted=\u6A21\u677F[{0}]\u5DF2\u542F\u7528\u65E0\u6CD5\u5220\u9664 +error.exception.illegal_argument=\u53C2\u6570\u9519\u8BEF +error.exception.unsupported_operation=\u4E0D\u652F\u6301\u7684\u64CD\u4F5C +error.exception.timeout=\u8D85\u65F6 +error.internal_error=\u7CFB\u7EDF\u5185\u90E8\u9519\u8BEF + +message.system.constant.dict.name=\u7CFB\u7EDF\u5E38\u91CF\u5206\u7C7B +message.calendar.tags.dict.item.weekend=\u5468\u672B +message.calendar.tags.dict.item.workday=\u5DE5\u4F5C\u65E5 +message.calendar.tags.dict.item.holiday=\u8282\u5047\u65E5 +message.calendar.tags.dict.name=\u65E5\u5386\u6807\u7B7E +message.system.constant.dict.item.default.name=\u9ED8\u8BA4 + +hswebframework.web.system.dictionary.schedule-tags=\u65E5\u5386\u6807\u7B7E +hswebframework.web.system.dictionary.item.workday=\u5DE5\u4F5C\u65E5 +hswebframework.web.system.dictionary.item.weekend=\u5468\u672B +hswebframework.web.system.dictionary.item.holiday=\u8282\u5047\u65E5 + +hswebframework.web.system.action.query=\u67E5\u8BE2 +hswebframework.web.system.action.save=\u4FDD\u5B58 +hswebframework.web.system.action.delete=\u5220\u9664 +hswebframework.web.system.action.upload-static=\u9759\u6001\u6587\u4EF6 +hswebframework.web.system.permission.system_environment=\u7CFB\u7EDF\u73AF\u5883\u53C2\u6570 +hswebframework.web.system.permission.system-monitor=\u7CFB\u7EDF\u5185\u90E8\u76D1\u63A7 +hswebframework.web.system.permission.system-resources=\u7CFB\u7EDF\u8D44\u6E90 +hswebframework.web.system.permission.system_config=\u7CFB\u7EDF\u914D\u7F6E\u7BA1\u7406 +hswebframework.web.system.permission.entity-template=\u8D44\u6E90\u5E93(\u65E7) +hswebframework.web.system.permission.system_constant=\u7CFB\u7EDF\u5E38\u91CF\u914D\u7F6E\u7BA1\u7406 +hswebframework.web.system.permission.calendar-manager=\u65E5\u5386\u7BA1\u7406 + +#FixedTermType +message.term_type_eq=\u7B49\u4E8E +message.term_type_neq=\u4E0D\u7B49\u4E8E +message.term_type_notnull=\u4E0D\u4E3A\u7A7A +message.term_type_isnull=\u4E3A\u7A7A +message.term_type_gt=\u5927\u4E8E +message.term_type_gte=\u5927\u4E8E\u7B49\u4E8E +message.term_type_lt=\u5C0F\u4E8E +message.term_type_lte=\u5C0F\u4E8E\u7B49\u4E8E +message.term_type_btw=\u5728...\u4E4B\u95F4 +message.term_type_nbtw=\u4E0D\u5728...\u4E4B\u95F4 +message.term_type_in=\u5728...\u4E4B\u4E2D +message.term_type_nin=\u4E0D\u5728...\u4E4B\u4E2D +message.term_type_contains_all=\u5168\u90E8\u5305\u542B\u5728...\u4E4B\u4E2D +message.term_type_contains_any=\u4EFB\u610F\u5305\u542B\u5728...\u4E4B\u4E2D +message.term_type_not_contains=\u4E0D\u5305\u542B\u5728...\u4E4B\u4E2D +message.term_type_like=\u5305\u542B\u5B57\u7B26 +message.term_type_nlike=\u4E0D\u5305\u542B\u5B57\u7B26 +message.term_type_time_gt_now=\u8DDD\u79BB\u5F53\u524D\u65F6\u95F4\u5927\u4E8E...\u79D2 +message.term_type_time_lt_now=\u8DDD\u79BB\u5F53\u524D\u65F6\u95F4\u5C0F\u4E8E...\u79D2 +message.term_type_complex_exists=\u6EE1\u8DB3 + +message.term_type_eq_desc={0}\u7B49\u4E8E{1} +message.term_type_neq_desc={0}\u4E0D\u7B49\u4E8E{1} +message.term_type_notnull_desc={0}\u4E0D\u4E3A\u7A7A +message.term_type_isnull_desc={0}\u4E3A\u7A7A +message.term_type_gt_desc={0}\u5927\u4E8E{1} +message.term_type_gte_desc={0}\u5927\u4E8E\u7B49\u4E8E{1} +message.term_type_lt_desc={0}\u5C0F\u4E8E{1} +message.term_type_lte_desc={0}\u5C0F\u4E8E\u7B49\u4E8E{1} +message.term_type_btw_desc={0}\u5728{1}\u4E4B\u95F4 +message.term_type_nbtw_desc={0}\u4E0D\u5728{1}\u4E4B\u95F4 +message.term_type_in_desc={0}\u5728{1}\u4E4B\u4E2D +message.term_type_nin_desc={0}\u4E0D\u5728{1}\u4E4B\u4E2D +message.term_type_contains_all_desc={0}\u5168\u90E8\u5305\u542B\u5728{1}\u4E4B\u4E2D +message.term_type_contains_any_desc={0}\u4EFB\u610F\u5305\u542B\u5728{1}\u4E4B\u4E2D +message.term_type_not_contains_desc={0}\u4E0D\u5305\u542B\u5728{1}\u4E4B\u4E2D +message.term_type_like_desc={0}\u5305\u542B\u5B57\u7B26{1} +message.term_type_nlike_desc={0}\u4E0D\u5305\u542B\u5B57\u7B26{1} +message.term_type_time_gt_now_desc={0}\u8DDD\u79BB\u5F53\u524D\u65F6\u95F4\u5927\u4E8E{1}\u79D2 +message.term_type_time_lt_now_desc={0}\u8DDD\u79BB\u5F53\u524D\u65F6\u95F4\u5C0F\u4E8E{1}\u79D2 +message.term_complex_exists_desc={0}{1}\u6761\u4EF6 + +org.jetlinks.community.template.EntityTemplateState.enabled=\u6B63\u5E38 +org.jetlinks.community.template.EntityTemplateState.disabled=\u7981\u7528 \ No newline at end of file diff --git a/jetlinks-components/dashboard-component/src/main/resources/i18n/dashboard-component/messages_en.properties b/jetlinks-components/dashboard-component/src/main/resources/i18n/dashboard-component/messages_en.properties new file mode 100644 index 00000000..eab295e0 --- /dev/null +++ b/jetlinks-components/dashboard-component/src/main/resources/i18n/dashboard-component/messages_en.properties @@ -0,0 +1 @@ +hswebframework.web.system.permission.dashboard=Dashboard \ No newline at end of file diff --git a/jetlinks-components/dashboard-component/src/main/resources/i18n/dashboard-component/messages_zh.properties b/jetlinks-components/dashboard-component/src/main/resources/i18n/dashboard-component/messages_zh.properties new file mode 100644 index 00000000..bdc45ec5 --- /dev/null +++ b/jetlinks-components/dashboard-component/src/main/resources/i18n/dashboard-component/messages_zh.properties @@ -0,0 +1 @@ +hswebframework.web.system.permission.dashboard=\u4EEA\u8868\u76D8 \ No newline at end of file diff --git a/jetlinks-components/elasticsearch-component/src/main/java/org/jetlinks/community/elastic/search/things/ElasticSearchColumnModeDDLOperations.java b/jetlinks-components/elasticsearch-component/src/main/java/org/jetlinks/community/elastic/search/things/ElasticSearchColumnModeDDLOperations.java index ba82742d..f55d369b 100644 --- a/jetlinks-components/elasticsearch-component/src/main/java/org/jetlinks/community/elastic/search/things/ElasticSearchColumnModeDDLOperations.java +++ b/jetlinks-components/elasticsearch-component/src/main/java/org/jetlinks/community/elastic/search/things/ElasticSearchColumnModeDDLOperations.java @@ -6,6 +6,7 @@ import org.jetlinks.community.elastic.search.index.ElasticSearchIndexManager; import org.jetlinks.community.things.data.operations.ColumnModeDDLOperationsBase; import org.jetlinks.community.things.data.operations.DataSettings; import org.jetlinks.community.things.data.operations.MetricBuilder; +import org.jetlinks.core.things.ThingMetadata; import reactor.core.publisher.Mono; import java.util.List; diff --git a/jetlinks-components/elasticsearch-component/src/main/java/org/jetlinks/community/elastic/search/things/ElasticSearchRowModeDDLOperations.java b/jetlinks-components/elasticsearch-component/src/main/java/org/jetlinks/community/elastic/search/things/ElasticSearchRowModeDDLOperations.java index b5816669..e06d3ecd 100644 --- a/jetlinks-components/elasticsearch-component/src/main/java/org/jetlinks/community/elastic/search/things/ElasticSearchRowModeDDLOperations.java +++ b/jetlinks-components/elasticsearch-component/src/main/java/org/jetlinks/community/elastic/search/things/ElasticSearchRowModeDDLOperations.java @@ -35,4 +35,9 @@ class ElasticSearchRowModeDDLOperations extends RowModeDDLOperationsBase { return indexManager .putIndex(new DefaultElasticSearchIndexMetadata(metric, properties)); } + + @Override + protected boolean isOnlySupportsOneObjectOrArrayProperty() { + return true; + } } diff --git a/jetlinks-components/gateway-component/src/main/java/org/jetlinks/community/gateway/DeviceGatewayHelper.java b/jetlinks-components/gateway-component/src/main/java/org/jetlinks/community/gateway/DeviceGatewayHelper.java index 80bc9c26..293331bb 100755 --- a/jetlinks-components/gateway-component/src/main/java/org/jetlinks/community/gateway/DeviceGatewayHelper.java +++ b/jetlinks-components/gateway-component/src/main/java/org/jetlinks/community/gateway/DeviceGatewayHelper.java @@ -13,8 +13,8 @@ import org.jetlinks.core.server.session.ChildrenDeviceSession; import org.jetlinks.core.server.session.DeviceSession; import org.jetlinks.core.server.session.KeepOnlineSession; import org.jetlinks.core.server.session.LostDeviceSession; -import org.jetlinks.community.PropertyConstants; import org.jetlinks.core.utils.Reactors; +import org.jetlinks.community.PropertyConstants; import org.jetlinks.supports.server.DecodedClientMessageHandler; import org.springframework.util.StringUtils; import reactor.core.publisher.Mono; @@ -45,6 +45,19 @@ public class DeviceGatewayHelper { private final DeviceSessionManager sessionManager; private final DecodedClientMessageHandler messageHandler; + public static Consumer applySessionKeepaliveTimeout(DeviceMessage msg, Supplier timeoutSupplier) { + return session -> { + Integer timeout = msg.getHeaderOrElse(Headers.keepOnlineTimeoutSeconds, () -> null); + if (null != timeout) { + session.setKeepAliveTimeout(Duration.ofSeconds(timeout)); + } else { + Duration defaultTimeout = timeoutSupplier.get(); + if (null != defaultTimeout) { + session.setKeepAliveTimeout(defaultTimeout); + } + } + }; + } public Mono handleDeviceMessage(DeviceMessage message, Function sessionBuilder) { @@ -248,7 +261,8 @@ public class DeviceGatewayHelper { private Mono handleMessage(DeviceOperator device, Message message) { return messageHandler .handleMessage(device, message) - .then(); + //转换为empty,减少触发discard + .flatMap(ignore -> Mono.empty()); } private Mono createOrUpdateSession(String deviceId, @@ -442,6 +456,24 @@ public class DeviceGatewayHelper { } + /** + * 校验设备消息的网关ID + * + * @param accessId 当前网关ID + * @param message 设备消息 + * @return 是否一致 + */ + public Mono checkAccessId(@NotNull String accessId, DeviceMessage message) { + if (message.getHeaderOrDefault(Headers.multiGateway)) { + return Reactors.ALWAYS_TRUE; + } + return registry + .getDevice(message.getDeviceId()) + .flatMap(operator -> operator.getConfig(PropertyConstants.accessId)) + .map(accessId::equals) + .defaultIfEmpty(true); + } + private static class HandlerContext { Mono before; diff --git a/jetlinks-components/gateway-component/src/main/java/org/jetlinks/community/gateway/supports/DeviceGatewayProperties.java b/jetlinks-components/gateway-component/src/main/java/org/jetlinks/community/gateway/supports/DeviceGatewayProperties.java index 4601c200..ace559bd 100644 --- a/jetlinks-components/gateway-component/src/main/java/org/jetlinks/community/gateway/supports/DeviceGatewayProperties.java +++ b/jetlinks-components/gateway-component/src/main/java/org/jetlinks/community/gateway/supports/DeviceGatewayProperties.java @@ -1,22 +1,19 @@ package org.jetlinks.community.gateway.supports; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Generated; import lombok.Getter; import lombok.Setter; +import org.jetlinks.core.ProtocolSupport; import org.jetlinks.community.ValueObject; +import javax.validation.constraints.NotBlank; import java.util.HashMap; import java.util.Map; -/** - * 设备网关属性外观类 - *

- * 转换设备网关属性数据 - *

- * - * @author zhouhao - */ @Getter @Setter +@Generated public class DeviceGatewayProperties implements ValueObject { private String id; @@ -25,16 +22,40 @@ public class DeviceGatewayProperties implements ValueObject { private String description; + /** + * 设备接入网关提供商标识 + * + * @see DeviceGatewayProvider#getId() + */ + @NotBlank private String provider; + @Schema(description = "接入通道ID,如网络组件ID") + @NotBlank private String channelId; + /** + * @see ProtocolSupport#getId() + */ + @Schema(description = "接入使用的消息协议") private String protocol; + /** + * 通信协议 + * + * @see org.jetlinks.core.message.codec.DefaultTransport + */ private String transport; + /** + * 网关配置信息,由{@link this#provider}决定 + */ + private Map configuration = new HashMap<>(); - private Map configuration=new HashMap<>(); + /** + * 是否启用 + */ + private boolean enabled = true; @Override public Map values() { diff --git a/jetlinks-components/gateway-component/src/main/java/org/jetlinks/community/gateway/supports/DeviceGatewayPropertiesManager.java b/jetlinks-components/gateway-component/src/main/java/org/jetlinks/community/gateway/supports/DeviceGatewayPropertiesManager.java index 51131c4d..71c34f5a 100644 --- a/jetlinks-components/gateway-component/src/main/java/org/jetlinks/community/gateway/supports/DeviceGatewayPropertiesManager.java +++ b/jetlinks-components/gateway-component/src/main/java/org/jetlinks/community/gateway/supports/DeviceGatewayPropertiesManager.java @@ -20,4 +20,8 @@ public interface DeviceGatewayPropertiesManager { Flux getPropertiesByChannel(String channel); + default Flux getAllProperties() { + return Flux.empty(); + } + } diff --git a/jetlinks-components/network-component/network-core/src/main/java/org/jetlinks/community/network/NetworkConfigManager.java b/jetlinks-components/network-component/network-core/src/main/java/org/jetlinks/community/network/NetworkConfigManager.java index 1a221e15..c3f444ee 100644 --- a/jetlinks-components/network-component/network-core/src/main/java/org/jetlinks/community/network/NetworkConfigManager.java +++ b/jetlinks-components/network-component/network-core/src/main/java/org/jetlinks/community/network/NetworkConfigManager.java @@ -3,12 +3,27 @@ package org.jetlinks.community.network; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + /** * 网络组件配置管理器 * * @author zhouhao + * @since 1.0 */ public interface NetworkConfigManager { + + /** + * 获取全部的网络配置 + * + * @return 配置信息 + * @since 2.0 + */ + default Flux getAllConfigs(boolean selfServer) { + return getAllConfigs(); + } + /** * 获取全部的网络配置 * @@ -19,6 +34,29 @@ public interface NetworkConfigManager { return Flux.empty(); } - Mono getConfig(NetworkType networkType, String id); + /** + * 根据网络类型和配置ID获取配置信息 + * + * @param networkType 网络类型 + * @param id 配置ID + * @param selfServer 是否只获取当前集群节点的配置 + * @return 配置信息 + */ + default Flux getConfig( + @Nullable NetworkType networkType, + @Nonnull String id, + boolean selfServer) { + return getConfig(networkType, id).flux(); + } + + /** + * 根据网络类型和配置ID获取配置信息 + * + * @param networkType 网络类型 + * @param id 配置ID + * @return 配置信息 + */ + Mono getConfig(@Nullable NetworkType networkType, + @Nonnull String id); } diff --git a/jetlinks-components/notify-component/notify-core/src/main/java/org/jetlinks/community/notify/SimpleProvider.java b/jetlinks-components/notify-component/notify-core/src/main/java/org/jetlinks/community/notify/SimpleProvider.java new file mode 100644 index 00000000..3235a504 --- /dev/null +++ b/jetlinks-components/notify-component/notify-core/src/main/java/org/jetlinks/community/notify/SimpleProvider.java @@ -0,0 +1,99 @@ +package org.jetlinks.community.notify; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import org.hswebframework.ezorm.core.CastUtil; +import org.hswebframework.web.exception.BusinessException; +import org.jetlinks.community.spi.Provider; +import reactor.core.Disposable; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.function.BiConsumer; +import java.util.function.Function; + +@AllArgsConstructor(access = AccessLevel.PACKAGE) +public class SimpleProvider implements Provider { + + private final Map providers = new ConcurrentHashMap<>(); + + private final List> hooks = new CopyOnWriteArrayList<>(); + + private final String name; + + + public Disposable register(String id, T provider) { + providers.put(id, provider); + executeHook(provider, Hook::onRegister); + return () -> unregister(id, provider); + } + + public List getAll() { + return new ArrayList<>(providers.values()); + } + + public T registerIfAbsent(String id, T provider) { + T old = providers.putIfAbsent(id, provider); + if (old == null || old != provider) { + executeHook(provider, Hook::onRegister); + } + return old; + } + + @Override + public T registerIfAbsent(String id, Function providerBuilder) { + Object[] absent = new Object[1]; + T old = providers + .computeIfAbsent(id, _id -> { + T provider = providerBuilder.apply(_id); + absent[0] = provider; + return provider; + }); + + if (absent[0] != null) { + executeHook(CastUtil.cast(absent[0]), Hook::onRegister); + } + return old; + } + + public void unregister(String id, T provider) { + if (providers.remove(id, provider)) { + executeHook(provider, Hook::onUnRegister); + } + } + + public void unregister(String id) { + T provider = providers.remove(id); + if (provider != null) { + executeHook(provider, Hook::onUnRegister); + } + } + + public Disposable addHook(Hook hook) { + hooks.add(hook); + return () -> hooks.remove(hook); + } + + public Optional get(String id) { + return Optional.ofNullable(providers.get(id)); + } + + public T getNow(String id) { + return get(id) + .orElseThrow(() -> new BusinessException.NoStackTrace("Unsupported " + name + ":" + id)); + } + + protected void executeHook(T provider, BiConsumer, T> executor) { + if (hooks.isEmpty()) { + return; + } + for (Hook hook : hooks) { + executor.accept(hook, provider); + } + } + +} diff --git a/jetlinks-components/notify-component/notify-core/src/main/java/org/jetlinks/community/notify/StaticTemplateManager.java b/jetlinks-components/notify-component/notify-core/src/main/java/org/jetlinks/community/notify/StaticTemplateManager.java index 07edeece..9c1a5f69 100644 --- a/jetlinks-components/notify-component/notify-core/src/main/java/org/jetlinks/community/notify/StaticTemplateManager.java +++ b/jetlinks-components/notify-component/notify-core/src/main/java/org/jetlinks/community/notify/StaticTemplateManager.java @@ -17,7 +17,8 @@ public class StaticTemplateManager extends AbstractTemplateManager implements Be private StaticNotifyProperties properties; - public StaticTemplateManager(StaticNotifyProperties properties) { + public StaticTemplateManager(StaticNotifyProperties properties, EventBus eventBus) { + super(eventBus); this.properties = properties; } diff --git a/jetlinks-components/notify-component/notify-core/src/main/java/org/jetlinks/community/notify/configuration/NotifierAutoConfiguration.java b/jetlinks-components/notify-component/notify-core/src/main/java/org/jetlinks/community/notify/configuration/NotifierAutoConfiguration.java index 3b283ec1..8a4f22c5 100644 --- a/jetlinks-components/notify-component/notify-core/src/main/java/org/jetlinks/community/notify/configuration/NotifierAutoConfiguration.java +++ b/jetlinks-components/notify-component/notify-core/src/main/java/org/jetlinks/community/notify/configuration/NotifierAutoConfiguration.java @@ -30,8 +30,9 @@ public class NotifierAutoConfiguration { } @Bean - public StaticTemplateManager staticTemplateManager(StaticNotifyProperties properties) { - return new StaticTemplateManager(properties); + public StaticTemplateManager staticTemplateManager(StaticNotifyProperties properties, + EventBus eventBus) { + return new StaticTemplateManager(properties, eventBus); } @Bean diff --git a/jetlinks-components/notify-component/notify-core/src/main/java/org/jetlinks/community/notify/enums/SubscriberTypeEnum.java b/jetlinks-components/notify-component/notify-core/src/main/java/org/jetlinks/community/notify/enums/SubscriberTypeEnum.java new file mode 100644 index 00000000..964eccff --- /dev/null +++ b/jetlinks-components/notify-component/notify-core/src/main/java/org/jetlinks/community/notify/enums/SubscriberTypeEnum.java @@ -0,0 +1,39 @@ +package org.jetlinks.community.notify.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.hswebframework.web.dict.I18nEnumDict; +import org.hswebframework.web.i18n.LocaleUtils; +import org.jetlinks.community.notify.subscription.SubscribeType; + +/** + * @author bestfeng + */ +@AllArgsConstructor +@Getter +public enum SubscriberTypeEnum implements SubscribeType, I18nEnumDict { + + + alarm("告警"), + systemEvent("系统事件"), + businessEvent("业务事件"), + other("其它"); + + private final String text; + + + @Override + public String getValue() { + return name(); + } + + @Override + public String getId() { + return getValue(); + } + + @Override + public String getName() { + return getI18nMessage(LocaleUtils.current()); + } +} diff --git a/jetlinks-components/notify-component/notify-core/src/main/java/org/jetlinks/community/notify/event/SerializableNotifierEvent.java b/jetlinks-components/notify-component/notify-core/src/main/java/org/jetlinks/community/notify/event/SerializableNotifierEvent.java index 028bce63..f8351290 100755 --- a/jetlinks-components/notify-component/notify-core/src/main/java/org/jetlinks/community/notify/event/SerializableNotifierEvent.java +++ b/jetlinks-components/notify-component/notify-core/src/main/java/org/jetlinks/community/notify/event/SerializableNotifierEvent.java @@ -5,6 +5,7 @@ import org.jetlinks.community.notify.template.Template; import javax.annotation.Nonnull; import javax.annotation.Nullable; +import java.io.Serializable; import java.util.Map; @Getter @@ -12,7 +13,9 @@ import java.util.Map; @Builder @NoArgsConstructor @AllArgsConstructor -public class SerializableNotifierEvent { +public class SerializableNotifierEvent implements Serializable { + + private String id; private boolean success; @@ -39,4 +42,6 @@ public class SerializableNotifierEvent { @Nonnull private Map context; + + private long sendTime; } diff --git a/jetlinks-components/notify-component/notify-core/src/main/java/org/jetlinks/community/notify/subscription/SubscribeType.java b/jetlinks-components/notify-component/notify-core/src/main/java/org/jetlinks/community/notify/subscription/SubscribeType.java new file mode 100644 index 00000000..a0fb924d --- /dev/null +++ b/jetlinks-components/notify-component/notify-core/src/main/java/org/jetlinks/community/notify/subscription/SubscribeType.java @@ -0,0 +1,20 @@ +package org.jetlinks.community.notify.subscription; + +/** + * 订阅类型 + * + * @author bestfeng + */ +public interface SubscribeType { + + /** + * @return 唯一标识 + */ + String getId(); + + /** + * @return 名称 + */ + String getName(); + +} diff --git a/jetlinks-components/notify-component/notify-core/src/main/java/org/jetlinks/community/notify/template/AbstractTemplateManager.java b/jetlinks-components/notify-component/notify-core/src/main/java/org/jetlinks/community/notify/template/AbstractTemplateManager.java index e3f2f9b4..db389ec8 100755 --- a/jetlinks-components/notify-component/notify-core/src/main/java/org/jetlinks/community/notify/template/AbstractTemplateManager.java +++ b/jetlinks-components/notify-component/notify-core/src/main/java/org/jetlinks/community/notify/template/AbstractTemplateManager.java @@ -2,9 +2,10 @@ package org.jetlinks.community.notify.template; import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.jetlinks.community.notify.NotifyType; +import org.jetlinks.core.cache.ReactiveCacheContainer; import org.jetlinks.core.event.EventBus; import org.jetlinks.core.event.Subscription; -import org.jetlinks.community.notify.NotifyType; import org.springframework.boot.CommandLineRunner; import reactor.core.publisher.Mono; @@ -13,14 +14,17 @@ import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @Slf4j -public abstract class AbstractTemplateManager implements TemplateManager { +@AllArgsConstructor +public abstract class AbstractTemplateManager implements TemplateManager, CommandLineRunner { protected final Map> providers = new ConcurrentHashMap<>(); - protected final Map templates = new ConcurrentHashMap<>(); + protected final ReactiveCacheContainer templates = ReactiveCacheContainer.create(); protected abstract Mono getProperties(NotifyType type, String id); + private EventBus eventBus; + protected void register(TemplateProvider provider) { providers.computeIfAbsent(provider.getType().getId(), ignore -> new ConcurrentHashMap<>()) .put(provider.getProvider().getId(), provider); @@ -40,27 +44,43 @@ public abstract class AbstractTemplateManager implements TemplateManager { @Nonnull @Override public Mono getTemplate(@Nonnull NotifyType type, @Nonnull String id) { - return Mono.justOrEmpty(templates.get(id)) - .switchIfEmpty(Mono.defer(() -> this - .getProperties(type, id) - .flatMap(prop -> this.createTemplate(type, prop)) - .doOnNext(template -> templates.put(id, template)) - .switchIfEmpty(Mono.error(() -> new UnsupportedOperationException("通知类型不支持:" + type.getId()))) - )); + return templates.computeIfAbsent(id, _id -> this + .getProperties(type, _id) + .flatMap(prop -> this.createTemplate(type, prop))); } @Override @Nonnull public Mono reload(String templateId) { return doReload(templateId) + .then(eventBus.publish("/_sys/notifier-temp/reload", templateId)) .then(); } private Mono doReload(String templateId) { - log.debug("reload notify template {}",templateId); + log.debug("reload notify template {}", templateId); return Mono.justOrEmpty(templates.remove(templateId)) .thenReturn(templateId); } + @Override + public void run(String... args) { + eventBus + .subscribe( + Subscription.builder() + .subscriberId("notifier-template-loader") + .topics("/_sys/notifier-temp/reload") + .justBroker() + .build(), + String.class + ) + .flatMap(id -> this + .doReload(id) + .onErrorResume(err -> { + log.error("reload notify template config error", err); + return Mono.empty(); + })) + .subscribe(); + } } diff --git a/jetlinks-components/notify-component/notify-email/src/main/java/org/jetlinks/community/notify/email/embedded/DefaultEmailNotifier.java b/jetlinks-components/notify-component/notify-email/src/main/java/org/jetlinks/community/notify/email/embedded/DefaultEmailNotifier.java index bf256d41..c3102f88 100755 --- a/jetlinks-components/notify-component/notify-email/src/main/java/org/jetlinks/community/notify/email/embedded/DefaultEmailNotifier.java +++ b/jetlinks-components/notify-component/notify-email/src/main/java/org/jetlinks/community/notify/email/embedded/DefaultEmailNotifier.java @@ -252,6 +252,11 @@ public class DefaultEmailNotifier extends AbstractNotifier { String name = template.render(tempAttachment.getName(), context); + String attachName = template.get(null, EmailTemplate.Attachment.locationName(index), context); + if (StringUtils.isNotBlank(attachName)) { + name = attachName; + } + String location = template.get(tempAttachment.getLocation(), EmailTemplate.Attachment.locationKey(index), context); attachments.put(name, location); diff --git a/jetlinks-components/notify-component/notify-email/src/main/java/org/jetlinks/community/notify/email/embedded/EmailTemplate.java b/jetlinks-components/notify-component/notify-email/src/main/java/org/jetlinks/community/notify/email/embedded/EmailTemplate.java index 70596124..1cc45018 100755 --- a/jetlinks-components/notify-component/notify-email/src/main/java/org/jetlinks/community/notify/email/embedded/EmailTemplate.java +++ b/jetlinks-components/notify-component/notify-email/src/main/java/org/jetlinks/community/notify/email/embedded/EmailTemplate.java @@ -63,6 +63,10 @@ public class EmailTemplate extends AbstractTemplate { public static String locationKey(int index) { return "_attach_location_" + index; } + + public static String locationName(int index) { + return "_attach_name_" + index; + } } diff --git a/jetlinks-components/protocol-component/src/main/java/org/jetlinks/community/protocol/ProtocolDetail.java b/jetlinks-components/protocol-component/src/main/java/org/jetlinks/community/protocol/ProtocolDetail.java index 825f7167..6839d2ca 100755 --- a/jetlinks-components/protocol-component/src/main/java/org/jetlinks/community/protocol/ProtocolDetail.java +++ b/jetlinks-components/protocol-component/src/main/java/org/jetlinks/community/protocol/ProtocolDetail.java @@ -3,8 +3,11 @@ package org.jetlinks.community.protocol; import io.swagger.v3.oas.annotations.media.Schema; import lombok.*; import org.jetlinks.core.ProtocolSupport; +import org.jetlinks.core.message.codec.Transport; +import org.springframework.util.StringUtils; import reactor.core.publisher.Mono; +import java.util.Collections; import java.util.List; @Getter @@ -24,13 +27,24 @@ public class ProtocolDetail { private List transports; - public static Mono of(ProtocolSupport support) { + public static Mono of(ProtocolSupport support, String transport) { + if (!StringUtils.hasText(transport)){ + return of(support); + } + return getTransDetail(support, Transport.of(transport)) + .map(detail -> new ProtocolDetail(support.getId(), support.getName(), support.getDescription(), Collections.singletonList(detail))); + } + public static Mono of(ProtocolSupport support) { return support .getSupportedTransport() - .flatMap(trans -> TransportDetail.of(support, trans)) + .flatMap(trans -> getTransDetail(support, trans)) .collectList() - .map(details -> new ProtocolDetail(support.getId(), support.getName(),support.getDescription(), details)); + .map(details -> new ProtocolDetail(support.getId(), support.getName(), support.getDescription(), details)); + } + + private static Mono getTransDetail(ProtocolSupport support, Transport transport) { + return TransportDetail.of(support, transport); } } diff --git a/jetlinks-components/protocol-component/src/main/java/org/jetlinks/community/protocol/ProtocolInfo.java b/jetlinks-components/protocol-component/src/main/java/org/jetlinks/community/protocol/ProtocolInfo.java index 7e05e305..79082dfb 100755 --- a/jetlinks-components/protocol-component/src/main/java/org/jetlinks/community/protocol/ProtocolInfo.java +++ b/jetlinks-components/protocol-component/src/main/java/org/jetlinks/community/protocol/ProtocolInfo.java @@ -4,6 +4,9 @@ import io.swagger.v3.oas.annotations.media.Schema; import lombok.*; import org.jetlinks.core.ProtocolSupport; +import java.util.HashMap; +import java.util.Map; + @Getter @Setter @AllArgsConstructor(staticName = "of") @@ -17,10 +20,18 @@ public class ProtocolInfo { @Schema(description = "协议名称") private String name; + @Schema(description = "拓展配置信息") + private Map configuration; + + @Schema(description = "说明") private String description; public static ProtocolInfo of(ProtocolSupport support) { - return of(support.getId(), support.getName(), support.getDescription()); + return of(support.getId(), support.getName(), new HashMap<>(), support.getDescription()); + } + + public static ProtocolInfo of(ProtocolSupportEntity support) { + return of(support.getId(), support.getName(), support.getConfiguration(), support.getDescription()); } } \ No newline at end of file diff --git a/jetlinks-components/protocol-component/src/main/java/org/jetlinks/community/protocol/ProtocolSupportEntity.java b/jetlinks-components/protocol-component/src/main/java/org/jetlinks/community/protocol/ProtocolSupportEntity.java new file mode 100644 index 00000000..66351f6c --- /dev/null +++ b/jetlinks-components/protocol-component/src/main/java/org/jetlinks/community/protocol/ProtocolSupportEntity.java @@ -0,0 +1,138 @@ +package org.jetlinks.community.protocol; + +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.Comment; +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.api.crud.entity.RecordModifierEntity; +import org.hswebframework.web.crud.annotation.EnableEntityEvent; +import org.hswebframework.web.crud.generator.Generators; +import org.hswebframework.web.validator.CreateGroup; +import org.jetlinks.core.metadata.PropertyMetadata; +import org.jetlinks.core.metadata.SimplePropertyMetadata; +import org.jetlinks.core.metadata.types.EnumType; +import org.jetlinks.core.metadata.types.ObjectType; +import org.jetlinks.core.metadata.types.StringType; +import org.jetlinks.supports.protocol.management.ProtocolSupportDefinition; + +import javax.persistence.Column; +import javax.persistence.GeneratedValue; +import javax.persistence.Table; +import javax.validation.constraints.Pattern; +import java.sql.JDBCType; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +@Getter +@Setter +@Table(name = "dev_protocol") +@Comment("协议信息表") +@EnableEntityEvent +public class ProtocolSupportEntity extends GenericEntity implements RecordCreationEntity, RecordModifierEntity { + + @Column + @Schema(description = "协议名称") + private String name; + + @Column + @Schema(description = "说明") + private String description; + + @Column + @Schema(description = "类型") + private String type; + + @Column + @Schema(description = "状态,1启用,0禁用") + @DefaultValue("1") + private Byte state; + + @Column + @ColumnType(jdbcType = JDBCType.CLOB) + @JsonCodec + @Schema(description = "配置") + private Map configuration; + + @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(name = "creator_name", updatable = false) + @Schema( + description = "创建者名称(只读)" + , accessMode = Schema.AccessMode.READ_ONLY + ) + private String creatorName; + + @Column(length = 64) + @Schema(description = "修改人") + private String modifierId; + + @Column + @Schema(description = "修改时间") + private Long modifyTime; + + @Column(length = 64) + @Schema(description = "修改人名称") + private String modifierName; + + public ProtocolSupportDefinition toUnDeployDefinition() { + ProtocolSupportDefinition definition = toDeployDefinition(); + definition.setState((byte) 0); + return definition; + } + + public ProtocolSupportDefinition toDeployDefinition() { + ProtocolSupportDefinition definition = new ProtocolSupportDefinition(); + definition.setId(getId()); + definition.setConfiguration(configuration); + definition.setName(name); + definition.setDescription(description); + definition.setProvider(type); + definition.setState((byte) 1); + + return definition; + } + + public ProtocolSupportDefinition toDefinition() { + ProtocolSupportDefinition definition = new ProtocolSupportDefinition(); + definition.setId(getId()); + definition.setConfiguration(configuration); + definition.setName(name); + definition.setDescription(description); + definition.setProvider(type); + definition.setState(getState()); + return definition; + } + + + public static List createMetadata(){ + return Arrays.asList( + SimplePropertyMetadata.of("id", "协议id", StringType.GLOBAL), + SimplePropertyMetadata.of("name", "协议名称", StringType.GLOBAL), + SimplePropertyMetadata.of("type", "协议类型", StringType.GLOBAL), + SimplePropertyMetadata.of("configuration", "配置", new ObjectType()), + SimplePropertyMetadata.of("describe", "说明", StringType.GLOBAL), + SimplePropertyMetadata.of("state", "状态", new EnumType() + .addElement(EnumType.Element.of("0", "禁用")) + .addElement(EnumType.Element.of("1", "离线"))) + ); + } +} diff --git a/jetlinks-components/protocol-component/src/main/java/org/jetlinks/community/protocol/configuration/ProtocolAutoConfiguration.java b/jetlinks-components/protocol-component/src/main/java/org/jetlinks/community/protocol/configuration/ProtocolAutoConfiguration.java index f385cd97..ae3cdcf6 100644 --- a/jetlinks-components/protocol-component/src/main/java/org/jetlinks/community/protocol/configuration/ProtocolAutoConfiguration.java +++ b/jetlinks-components/protocol-component/src/main/java/org/jetlinks/community/protocol/configuration/ProtocolAutoConfiguration.java @@ -1,5 +1,6 @@ package org.jetlinks.community.protocol.configuration; +import org.hswebframework.web.crud.annotation.EnableEasyormRepository; import org.jetlinks.community.protocol.*; import org.jetlinks.community.protocol.local.LocalProtocolSupportLoader; import org.jetlinks.core.ProtocolSupport; @@ -26,6 +27,7 @@ import org.springframework.web.reactive.function.client.WebClient; @Configuration(proxyBeanMethods = false) @AutoConfigureBefore(DeviceClusterConfiguration.class) +@EnableEasyormRepository("org.jetlinks.community.protocol.ProtocolSupportEntity") public class ProtocolAutoConfiguration { // @Bean diff --git a/jetlinks-components/protocol-component/src/main/java/org/jetlinks/community/protocol/service/LocalProtocolSupportService.java b/jetlinks-components/protocol-component/src/main/java/org/jetlinks/community/protocol/service/LocalProtocolSupportService.java new file mode 100644 index 00000000..1f0f8e15 --- /dev/null +++ b/jetlinks-components/protocol-component/src/main/java/org/jetlinks/community/protocol/service/LocalProtocolSupportService.java @@ -0,0 +1,127 @@ +package org.jetlinks.community.protocol.service; + +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.hswebframework.web.api.crud.entity.QueryParamEntity; +import org.hswebframework.web.crud.service.GenericReactiveCrudService; +import org.hswebframework.web.exception.BusinessException; +import org.hswebframework.web.exception.NotFoundException; +import org.jetlinks.community.protocol.ProtocolInfo; +import org.jetlinks.community.protocol.ProtocolSupportEntity; +import org.jetlinks.community.protocol.TransportDetail; +import org.jetlinks.community.reference.DataReferenceManager; +import org.jetlinks.core.ProtocolSupport; +import org.jetlinks.core.ProtocolSupports; +import org.jetlinks.core.message.codec.Transport; +import org.jetlinks.core.metadata.ConfigMetadata; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.Assert; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.util.function.Tuple2; +import reactor.util.function.Tuples; + +import java.util.Comparator; +import java.util.List; + +@Service +@Slf4j +@AllArgsConstructor +public class LocalProtocolSupportService extends GenericReactiveCrudService { + + private final DataReferenceManager referenceManager; + + private final ProtocolSupports protocolSupports; + + @Transactional + public Mono deploy(String id) { + return findById(id) + .switchIfEmpty(Mono.error(NotFoundException::new)) + .flatMap(r -> createUpdate() + .set(ProtocolSupportEntity::getState, 1) + .where(ProtocolSupportEntity::getId, id) + .execute()) + .map(i -> i > 0); + } + + @Transactional + public Mono unDeploy(String id) { + // 消息协议被使用时,不能禁用 + return referenceManager + .assertNotReferenced(DataReferenceManager.TYPE_PROTOCOL, id, "error.protocol_referenced") + .then(findById(id)) + .switchIfEmpty(Mono.error(NotFoundException::new)) + .flatMap(r -> createUpdate() + .set(ProtocolSupportEntity::getState, 0) + .where(ProtocolSupportEntity::getId, id) + .execute()) + .map(i -> i > 0); + } + + @Transactional + public Mono addProtocolsThenDeploy(List entities) { + return Flux.fromIterable(entities) + .doOnNext(entity -> Assert.hasText(entity.getId(), "message.Id_cannot_be_empty")) + .as(this::save) + .thenMany(Flux.fromIterable(entities)) + .flatMap(entity -> deploy(entity.getId()) + .onErrorResume((err) -> { + log.warn("同步协议失败:", err); + return Mono.empty(); + })) + .then(); + + } + + public Flux getSupportTransportProtocols(String transport, + QueryParamEntity query) { + return protocolSupports + .getProtocols() + .collectMap(ProtocolSupport::getId) + .flatMapMany(protocols -> this.createQuery() + .setParam(query) + .fetch() + .index() + .flatMap(tp2 -> Mono + .justOrEmpty(protocols.get(tp2.getT2().getId())) + .filterWhen(support -> support + .getSupportedTransport() + .filter(t -> t.isSame(transport)) + .hasElements()) + .map(ignore -> ProtocolInfo.of(tp2.getT2())) + .map(protocolInfo -> Tuples.of(tp2.getT1(), protocolInfo)))) + .sort(Comparator.comparingLong(Tuple2::getT1)) + .map(Tuple2::getT2); + } + + public Mono getTransportDetail(String id, String transport) { + return protocolSupports + .getProtocol(id) + .onErrorMap(e -> new BusinessException("error.unable_to_load_protocol_by_access_id", 404, id)) + .flatMapMany(protocol -> protocol + .getSupportedTransport() + .filter(trans -> trans.isSame(transport)) + .distinct() + .flatMap(_transport -> TransportDetail.of(protocol, _transport))) + .singleOrEmpty(); + } + + public Mono getTransportConfiguration(String id, String transport) { + return protocolSupports + .getProtocol(id) + .onErrorMap(e -> new BusinessException("error.unable_to_load_protocol_by_access_id", 404, id)) + .flatMap(support -> support.getConfigMetadata(Transport.of(transport))); + } + + public Mono getDefaultMetadata(String id, String transport) { + return protocolSupports + .getProtocol(id) + .onErrorMap(e -> new BusinessException("error.unable_to_load_protocol_by_access_id", 404, id)) + .flatMap(support -> support + .getDefaultMetadata(Transport.of(transport)) + .flatMap(support.getMetadataCodec()::encode) + ).defaultIfEmpty("{}"); + } + +} diff --git a/jetlinks-components/relation-component/src/main/java/org/jetlinks/community/relation/impl/DefaultRelationManager.java b/jetlinks-components/relation-component/src/main/java/org/jetlinks/community/relation/impl/DefaultRelationManager.java index 37810ca4..7a0e387f 100644 --- a/jetlinks-components/relation-component/src/main/java/org/jetlinks/community/relation/impl/DefaultRelationManager.java +++ b/jetlinks-components/relation-component/src/main/java/org/jetlinks/community/relation/impl/DefaultRelationManager.java @@ -1,12 +1,16 @@ package org.jetlinks.community.relation.impl; import lombok.AllArgsConstructor; +import org.apache.commons.collections4.CollectionUtils; import org.hswebframework.ezorm.rdb.mapping.ReactiveRepository; import org.hswebframework.web.exception.I18nSupportException; import org.jetlinks.community.relation.RelationObjectProvider; -import org.jetlinks.core.things.relation.*; import org.jetlinks.community.relation.entity.RelatedEntity; import org.jetlinks.community.relation.entity.RelationEntity; +import org.jetlinks.core.things.relation.ObjectType; +import org.jetlinks.core.things.relation.Relation; +import org.jetlinks.core.things.relation.RelationManager; +import org.jetlinks.core.things.relation.RelationObject; import org.springframework.util.StringUtils; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -38,19 +42,38 @@ public class DefaultRelationManager implements RelationManager { .getType() .flatMap(type -> relationRepository .createQuery() + //动态关系双向查询 .where(RelationEntity::getObjectType, typeId) + .or(RelationEntity::getTargetType, typeId) .fetch() .collect(Collectors.groupingBy( - RelationEntity::getTargetType, - Collectors.mapping(SimpleRelation::of, Collectors.toList()))) - .map(group -> { - SimpleObjectType custom = new SimpleObjectType(typeId, type.getName(), type.getDescription()); - for (Map.Entry> entry : group.entrySet()) { - custom.withRelation(entry.getKey(), entry.getValue()); + //根据关系对象分组 + entity -> typeId.equals(entity.getObjectType()) ? entity.getTargetType() : entity.getObjectType(), + Collectors.mapping(e -> SimpleRelation.of(typeId, e), Collectors.toList()))) + .flatMap(group -> fillRelations(type, group))); + } + + private Mono fillRelations(ObjectType type, Map> relations) { + String typeId = type.getId(); + SimpleObjectType custom = new SimpleObjectType(typeId, type.getName(), type.getDescription()); + for (Map.Entry> entry : relations.entrySet()) { + //添加动态关系 + custom.withRelation(entry.getKey(), entry.getValue()); + } + return getObjectTypes() + .doOnNext(other -> { + if (!typeId.equals(other.getId())) { + List fixRelations = other.getRelations(typeId); + if (CollectionUtils.isNotEmpty(other.getRelations(typeId))) { + //添加其它类型提供的固定关系 + custom.withRelation(other.getId(), fixRelations + .stream() + .map(r -> SimpleRelation.of(r.getId(), r.getName(), r.getReverseName(), !r.isReverse(), r.getExpands())) + .collect(Collectors.toList())); } - return new CompositeObjectType(type, custom); - }) - .defaultIfEmpty(type)); + } + }) + .then(Mono.just(new CompositeObjectType(type, custom))); } @Override diff --git a/jetlinks-components/relation-component/src/main/java/org/jetlinks/community/relation/impl/SimpleObjectType.java b/jetlinks-components/relation-component/src/main/java/org/jetlinks/community/relation/impl/SimpleObjectType.java index bcf6da78..468ccde7 100644 --- a/jetlinks-components/relation-component/src/main/java/org/jetlinks/community/relation/impl/SimpleObjectType.java +++ b/jetlinks-components/relation-component/src/main/java/org/jetlinks/community/relation/impl/SimpleObjectType.java @@ -88,6 +88,20 @@ public class SimpleObjectType implements ObjectType, Externalizable { return this; } + public SimpleObjectType withRelation(ObjectType type, List relation) { + withRelation(type.getId(), relation); + withRelatedType(type); + return this; + } + + public SimpleObjectType withRelatedType(ObjectType type) { + if (relatedTypes == null) { + relatedTypes = new ArrayList<>(); + } + relatedTypes.add(type); + return this; + } + public SimpleObjectType withProperty(String id, String name, DataType type) { return withProperty(SimplePropertyMetadata.of(id, name, type)); } diff --git a/jetlinks-components/relation-component/src/main/java/org/jetlinks/community/relation/impl/SimpleRelation.java b/jetlinks-components/relation-component/src/main/java/org/jetlinks/community/relation/impl/SimpleRelation.java index 9363a78f..1c3a3ea7 100644 --- a/jetlinks-components/relation-component/src/main/java/org/jetlinks/community/relation/impl/SimpleRelation.java +++ b/jetlinks-components/relation-component/src/main/java/org/jetlinks/community/relation/impl/SimpleRelation.java @@ -4,6 +4,7 @@ import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; +import org.apache.commons.lang3.StringUtils; import org.jetlinks.core.things.relation.Relation; import org.jetlinks.core.utils.SerializeUtils; import org.jetlinks.community.relation.entity.RelationEntity; @@ -18,14 +19,24 @@ import java.util.Map; @Setter @AllArgsConstructor(staticName = "of") @NoArgsConstructor -public class SimpleRelation implements Relation , Externalizable { +public class SimpleRelation implements Relation, Externalizable { private String id; private String name; - private Map expands; + private String reverseName; + private boolean reverse; + private Map expands; + @Deprecated + public SimpleRelation(String id, String name, Map expands) { + this.id = id; + this.name = name; + this.reverseName = StringUtils.EMPTY; + this.expands = expands; + this.reverse = false; + } - public static SimpleRelation of(RelationEntity entity){ - return of(entity.getId(),entity.getName(),entity.getExpands()); + public static SimpleRelation of(String objectType, RelationEntity entity) { + return of(entity.getId(), entity.getName(), entity.getReverseName(), objectType.equals(entity.getTargetType()), entity.getExpands()); } @Override @@ -33,6 +44,8 @@ public class SimpleRelation implements Relation , Externalizable { out.writeUTF(id); out.writeUTF(name); SerializeUtils.writeObject(expands,out); + out.writeUTF(reverseName); + out.writeBoolean(reverse); } @Override @@ -41,5 +54,7 @@ public class SimpleRelation implements Relation , Externalizable { id = in.readUTF(); name = in.readUTF(); expands = (Map)SerializeUtils.readObject(in); + reverseName = in.readUTF(); + reverse = in.readBoolean(); } } diff --git a/jetlinks-components/rule-engine-component/src/main/java/org/jetlinks/community/rule/engine/commons/ShakeLimit.java b/jetlinks-components/rule-engine-component/src/main/java/org/jetlinks/community/rule/engine/commons/ShakeLimit.java index b8dc5a3d..fc05ad9e 100644 --- a/jetlinks-components/rule-engine-component/src/main/java/org/jetlinks/community/rule/engine/commons/ShakeLimit.java +++ b/jetlinks-components/rule-engine-component/src/main/java/org/jetlinks/community/rule/engine/commons/ShakeLimit.java @@ -19,6 +19,29 @@ import java.util.function.BiFunction; * 抖动限制 * https://github.com/jetlinks/jetlinks-community/issues/8 * + *
{@code
+ *  // [每][10]秒内[连续]出现[3]次及以上[立即]输出[第一次]
+ * {
+ *
+ *    "rolling":false, //每,无论是否满足条件都要等到时间到了才重新计时
+ *    "time":10, //10秒
+ *    "continuous":true, //连续, false则表示总共出现3次
+ *    "threshold":3, //3次
+ *    "alarmFirst":true, //立即输出
+ *    "outputFirst":true // 输出第一次
+ * }
+ *
+ *  // [滚动][10]秒内[总共]出现[3]次及以上[延迟]输出[最后一次]
+ * {
+ *    "rolling":true, //滚动,满足条件或者超时后重新计时
+ *    "time":10, //10秒
+ *    "continuous":false, //总共
+ *    "threshold":3, //3次
+ *    "alarmFirst":true, //延迟输出
+ *    "outputFirst":false // 输出最后一次
+ * }
+ * }
+ * * @since 1.3 */ @Getter @@ -37,10 +60,19 @@ public class ShakeLimit implements Serializable { @Schema(description = "触发阈值(次)") private int threshold; + @Schema(description = "是否连续出现才触发") + private boolean continuous; + //当发生第一次告警时就触发,为false时表示最后一次才触发(告警有延迟,但是可以统计出次数) @Schema(description = "是否第一次满足条件就触发") private boolean alarmFirst; + @Schema(description = "是否输出第一次为结果") + private boolean outputFirst; + + @Schema(description = "是否滚动计算,触发后重新计时.") + private boolean rolling; + /** * 利用窗口函数,将ReactorQL语句包装为支持抖动限制的SQL. *

@@ -112,4 +144,14 @@ public class ShakeLimit implements Serializable { return next; }), Integer.MAX_VALUE); } + + @Override + public String toString() { + return (rolling ? "每" : "滚动" )+ + time + "秒内" + + (continuous ? "连续" : "总共") + + "出现" + threshold + "次及以上" + + (alarmFirst ? "立即" : "延迟") + + "输出" + (outputFirst ? "第一次" : "最后一次"); + } } \ No newline at end of file diff --git a/jetlinks-components/rule-engine-component/src/main/java/org/jetlinks/community/rule/engine/commons/ShakeLimitFlux.java b/jetlinks-components/rule-engine-component/src/main/java/org/jetlinks/community/rule/engine/commons/ShakeLimitFlux.java new file mode 100644 index 00000000..46fb5128 --- /dev/null +++ b/jetlinks-components/rule-engine-component/src/main/java/org/jetlinks/community/rule/engine/commons/ShakeLimitFlux.java @@ -0,0 +1,318 @@ +package org.jetlinks.community.rule.engine.commons; + +import lombok.RequiredArgsConstructor; +import org.reactivestreams.Publisher; +import org.reactivestreams.Subscription; +import reactor.core.CoreSubscriber; +import reactor.core.Disposables; +import reactor.core.Scannable; +import reactor.core.publisher.BaseSubscriber; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.core.publisher.SignalType; +import reactor.core.scheduler.Schedulers; +import reactor.util.context.Context; + +import javax.annotation.Nonnull; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; +import java.util.concurrent.atomic.AtomicLongFieldUpdater; +import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; + +public class ShakeLimitFlux extends Flux> implements Scannable { + private final String key; + private final Flux source; + private final ShakeLimit limit; + private final Publisher resetSignal; + + private ShakeLimitFlux(String key, Flux source, ShakeLimit limit, Publisher resetSignal) { + this.key = key; + this.source = source; + this.limit = limit; + this.resetSignal = resetSignal; + } + + public static ShakeLimitFlux create(String key, + Flux source, + ShakeLimit limit) { + return new ShakeLimitFlux<>(key, source, limit, Mono.never()); + } + + public static ShakeLimitFlux create(String key, + Flux source, + ShakeLimit limit, + Publisher resetSignal) { + return new ShakeLimitFlux<>(key, source, limit, resetSignal); + } + + + @Override + public void subscribe(@Nonnull CoreSubscriber> actual) { + ShakeLimitSubscriber subscriber = new ShakeLimitSubscriber<>(key, actual, limit, resetSignal); + source.subscribe(subscriber); + } + + @Override + public Object scanUnsafe(@Nonnull Attr key) { + if (key == Attr.RUN_STYLE) return Attr.RunStyle.ASYNC; + if (key == Attr.PARENT) return source; + if (key == Attr.NAME) return stepName(); + return null; + } + + @Override + @Nonnull + public String stepName() { + return "ShakeLimit('" + key + "','" + limit + "')"; + } + + @RequiredArgsConstructor + static class ShakeLimitSubscriber extends BaseSubscriber implements Scannable, Runnable { + final String key; + final CoreSubscriber> actual; + final ShakeLimit limit; + final Publisher resetSignal; + final Swap timer = Disposables.swap(); + + static final int STATE_INIT = 0, + STATE_PAUSED = 1; + + @SuppressWarnings("all") + static final AtomicIntegerFieldUpdater + STATE = AtomicIntegerFieldUpdater.newUpdater(ShakeLimitSubscriber.class, "state"); + volatile int state; + + ResetSubscriber resetSubscriber; + @SuppressWarnings("all") + static final AtomicLongFieldUpdater + COUNT = AtomicLongFieldUpdater.newUpdater(ShakeLimitSubscriber.class, "count"); + volatile long count; + + @SuppressWarnings("all") + static final AtomicReferenceFieldUpdater FIRST = + AtomicReferenceFieldUpdater.newUpdater(ShakeLimitSubscriber.class, Object.class, "first"); + volatile T first; + + @SuppressWarnings("all") + static final AtomicLongFieldUpdater + FIRST_TIME = AtomicLongFieldUpdater.newUpdater(ShakeLimitSubscriber.class, "firstTime"); + volatile long firstTime; + + @SuppressWarnings("all") + static final AtomicReferenceFieldUpdater LAST = + AtomicReferenceFieldUpdater.newUpdater(ShakeLimitSubscriber.class, Object.class, "last"); + volatile T last; + + @SuppressWarnings("all") + static final AtomicLongFieldUpdater + LAST_TIME = AtomicLongFieldUpdater.newUpdater(ShakeLimitSubscriber.class, "lastTime"); + volatile long lastTime; + + @Override + public void run() { + long count = COUNT.getAndSet(this, 0); + //尝试触发 + if (STATE.getAndSet(this, STATE_INIT) != STATE_PAUSED) { + handle(count, true); + } + } + + class ResetSubscriber extends BaseSubscriber { + + @Override + protected void hookOnNext(@Nonnull Object value) { + //重置 + reset(true); + + } + + @Override + @Nonnull + public Context currentContext() { + return actual.currentContext(); + } + } + + private void reset(boolean force) { + if (force) { + STATE.set(this, STATE_INIT); + FIRST_TIME.set(this, 0); + } + COUNT.set(this, 0); + FIRST.set(this, null); + LAST.set(this, null); + LAST_TIME.set(this, 0); + //重置定时 + completeTimer(); + } + + @Override + @Nonnull + public Context currentContext() { + return actual.currentContext(); + } + + @Override + protected void hookOnSubscribe(@Nonnull Subscription subscription) { + //连续触发的场景,需要根据重置信号进行重置 + if (resetSignal != null && limit.isContinuous()) { + resetSubscriber = new ResetSubscriber(); + resetSignal.subscribe(resetSubscriber); + } + + actual.onSubscribe(this); + request(1); + + } + + @Override + protected void hookOnNext(@Nonnull T value) { + long now = System.currentTimeMillis(); + + startTimer(); + +// long firstTime = FIRST_TIME.get(this); +//// +// //定时重置未及时生效? +// if (limit.isRolling() +// && firstTime > 0 && limit.getTime() > 0 +// //跨越了新的时间窗口 +// && now - firstTime > limit.getTime() * 1000L) { +// //重置,重新开始计数 +// handle(COUNT.getAndSet(this,0),true); +// } + + FIRST.compareAndSet(this, null, value); + FIRST_TIME.compareAndSet(this, 0, now); + + LAST.set(this, value); + LAST_TIME.set(this, now); + + long count = COUNT.incrementAndGet(this); + //尝试立即触发 + if (limit.isAlarmFirst()) { + //滚动窗口,或者不按时间窗口,直接处理 + if (limit.isRolling() || limit.getTime() <= 0) { + handle(count, false); + } else { + STATE.accumulateAndGet( + this, STATE_PAUSED, + (old, update) -> { + if (old != STATE_PAUSED) { + if (handle(count, false)) { + return STATE_PAUSED; + } + } + return old; + }); + } + } + request(1); + } + + + @Override + protected void hookOnError(@Nonnull Throwable throwable) { + actual.onError(throwable); + } + + @Override + protected void hookOnComplete() { + try { + if (STATE.get(this) != STATE_PAUSED) { + long now = System.currentTimeMillis(); + + long count = COUNT.getAndSet(this, 0); + if (count < limit.getThreshold()) { + return; + } + //没在窗口内 + if (limit.getTime() > 0 && now - firstTime <= limit.getTime() * 1000L) { + return; + } + handle(count, true); + } + } finally { + actual.onComplete(); + } + + } + + @Override + protected void hookFinally(@Nonnull SignalType type) { + timer.dispose(); + if (resetSubscriber != null) { + resetSubscriber.cancel(); + } + } + + protected void completeTimer() { + //滚动窗口才重置 + if (limit.isRolling()) { + timer.update(Disposables.disposed()); + startTimer(); + } + } + + protected void startTimer() { + if (limit.getTime() <= 0 || isDisposed()) { + return; + } + + if (timer.get() == null || timer.get().isDisposed()) { + synchronized (this) { + if (timer.get() == null || timer.get().isDisposed()) { + if (limit.isRolling()) { + timer.update( + Schedulers + .parallel() + .schedule(this, + limit.getTime(), + TimeUnit.SECONDS) + ); + } else { + timer.update( + Schedulers + .parallel() + .schedulePeriodically( + this, + limit.getTime(), + limit.getTime(), + TimeUnit.SECONDS) + ); + } + + } + } + } + + } + + protected boolean handle(long count, boolean reset) { + //未满足条件 + if (count < limit.getThreshold()) { + if (reset) { + reset(true); + } + return false; + } + //take first or last + T val = limit.isOutputFirst() ? first : last; + reset(reset); + if (val != null) { + actual.onNext(new ShakeLimitResult<>(key, count, val)); + return true; + } + return false; + } + + @Override + public Object scanUnsafe(@Nonnull Attr key) { + if (key == Attr.PREFETCH) return 1; + if (key == Attr.ACTUAL) return actual; + if (key == Attr.RUN_STYLE) return Attr.RunStyle.SYNC; + + return null; + } + } +} diff --git a/jetlinks-components/rule-engine-component/src/main/java/org/jetlinks/community/rule/engine/commons/ShakeLimitProvider.java b/jetlinks-components/rule-engine-component/src/main/java/org/jetlinks/community/rule/engine/commons/ShakeLimitProvider.java index 86215b9e..a1d3ab3c 100644 --- a/jetlinks-components/rule-engine-component/src/main/java/org/jetlinks/community/rule/engine/commons/ShakeLimitProvider.java +++ b/jetlinks-components/rule-engine-component/src/main/java/org/jetlinks/community/rule/engine/commons/ShakeLimitProvider.java @@ -1,8 +1,12 @@ package org.jetlinks.community.rule.engine.commons; import org.jetlinks.community.spi.Provider; +import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; import reactor.core.publisher.GroupedFlux; +import reactor.core.publisher.Mono; + +import java.util.function.Function; /** * 防抖提供商 @@ -28,10 +32,31 @@ public interface ShakeLimitProvider { * @param 数据类型 * @return 防抖结果 */ - Flux> shakeLimit( + default Flux> shakeLimit( String sourceKey, Flux> grouped, - ShakeLimit limit); + ShakeLimit limit) { + return shakeLimit(sourceKey, + grouped, + limit, + ignore -> Mono.never()); + } + /** + * 对指定分组数据源进行防抖,并输出满足条件的数据. + * + * @param sourceKey 数据源唯一标识 + * @param grouped 分组数据源 + * @param limit 防抖条件 + * @param resetSignal 重置信号 + * @param 数据类型 + * @return 防抖结果 + * @see org.jetlinks.community.rule.engine.commons.ShakeLimitFlux + */ + Flux> shakeLimit(String sourceKey, + Flux> grouped, + ShakeLimit limit, + Function> resetSignal); + } diff --git a/jetlinks-components/rule-engine-component/src/main/java/org/jetlinks/community/rule/engine/commons/impl/SimpleShakeLimitProvider.java b/jetlinks-components/rule-engine-component/src/main/java/org/jetlinks/community/rule/engine/commons/impl/SimpleShakeLimitProvider.java index f270bad4..35576dd9 100644 --- a/jetlinks-components/rule-engine-component/src/main/java/org/jetlinks/community/rule/engine/commons/impl/SimpleShakeLimitProvider.java +++ b/jetlinks-components/rule-engine-component/src/main/java/org/jetlinks/community/rule/engine/commons/impl/SimpleShakeLimitProvider.java @@ -2,14 +2,16 @@ package org.jetlinks.community.rule.engine.commons.impl; import lombok.extern.slf4j.Slf4j; import org.jetlinks.community.rule.engine.commons.ShakeLimit; +import org.jetlinks.community.rule.engine.commons.ShakeLimitFlux; import org.jetlinks.community.rule.engine.commons.ShakeLimitProvider; import org.jetlinks.community.rule.engine.commons.ShakeLimitResult; +import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; import reactor.core.publisher.GroupedFlux; import reactor.core.publisher.Mono; -import reactor.util.function.Tuples; import java.time.Duration; +import java.util.function.Function; @Slf4j public class SimpleShakeLimitProvider implements ShakeLimitProvider { @@ -30,10 +32,10 @@ public class SimpleShakeLimitProvider implements ShakeLimitProvider { @Override public Flux> shakeLimit(String sourceKey, Flux> grouped, - ShakeLimit limit) { - int thresholdNumber = limit.getThreshold(); - boolean isAlarmFirst = limit.isAlarmFirst(); + ShakeLimit limit, + Function> resetSignal) { Duration windowSpan = Duration.ofSeconds(limit.getTime()); + return grouped .flatMap(group -> { String groupKey = group.key(); @@ -42,15 +44,12 @@ public class SimpleShakeLimitProvider implements ShakeLimitProvider { .defer(() -> this //使用timeout,当2倍窗口时间没有收到数据时,则结束分组.释放内存. .wrapSource(key, group.timeout(windowSpan.plus(windowSpan), Mono.empty()))) - //按时间窗口分组 - .window(windowSpan) - .flatMap(source -> this + .as(source -> this .handleWindow(key, groupKey, - windowSpan, + limit, source, - thresholdNumber, - isAlarmFirst)) + resetSignal.apply(groupKey))) .onErrorResume(err -> { log.warn("shake limit [{}] error", key, err); return Mono.empty(); @@ -58,30 +57,12 @@ public class SimpleShakeLimitProvider implements ShakeLimitProvider { }, Integer.MAX_VALUE); } - protected Mono> handleWindow(String key, + + protected Flux> handleWindow(String key, String groupKey, - Duration duration, + ShakeLimit limit, Flux source, - long thresholdNumber, - boolean isAlarmFirst) { - //给数据打上索引,索引号就是告警次数 - return source - .index((index, data) -> Tuples.of(index + 1, data)) - .switchOnFirst((e, flux) -> { - if (e.hasValue()) { - @SuppressWarnings("all") - T ele = e.get().getT2(); - return flux.map(tp2 -> Tuples.of(tp2.getT1(), tp2.getT2(), ele)); - } - return flux.then(Mono.empty()); - }) - //超过阈值告警时 - .filter(tp -> tp.getT1() >= thresholdNumber) - .as(flux -> isAlarmFirst ? flux.take(1) : flux.takeLast(1))//取第一个或者最后一个 - .map(tp3 -> { - T next = isAlarmFirst ? tp3.getT3() : tp3.getT2(); - return new ShakeLimitResult<>(groupKey, tp3.getT1(), next); - }) - .singleOrEmpty(); + Publisher resetSignal) { + return ShakeLimitFlux.create(groupKey, source, limit, resetSignal); } } 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 0224ab8a..54921342 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 @@ -4,6 +4,7 @@ import lombok.Generated; import org.hswebframework.ezorm.rdb.mapping.ReactiveRepository; import org.jetlinks.community.things.ThingsDataProperties; import org.jetlinks.community.things.data.*; +import org.jetlinks.community.things.holder.ThingsRegistryHolderInitializer; import org.jetlinks.community.things.impl.entity.PropertyMetricEntity; import org.jetlinks.community.things.impl.metric.DefaultPropertyMetricManager; import org.jetlinks.core.defaults.DeviceThingsRegistrySupport; @@ -33,13 +34,6 @@ public class ThingsConfiguration { return new AutoUpdateThingsDataManager(fileName, eventBus); } - @Bean - @Primary - public AutoRegisterThingsRegistry thingsRegistry() { - return new AutoRegisterThingsRegistry(); - } - - @Bean public DefaultPropertyMetricManager propertyMetricManager(ThingsRegistry registry, EventBus eventBus, @@ -66,4 +60,12 @@ public class ThingsConfiguration { } return service; } + + @Bean + @Primary + public AutoRegisterThingsRegistry thingsRegistry() { + AutoRegisterThingsRegistry registry = new AutoRegisterThingsRegistry(); + ThingsRegistryHolderInitializer.init(registry); + return registry; + } } diff --git a/jetlinks-components/things-component/src/main/java/org/jetlinks/community/things/data/NoneThingsDataRepositoryStrategy.java b/jetlinks-components/things-component/src/main/java/org/jetlinks/community/things/data/NoneThingsDataRepositoryStrategy.java index fdcefbae..1528b05e 100644 --- a/jetlinks-components/things-component/src/main/java/org/jetlinks/community/things/data/NoneThingsDataRepositoryStrategy.java +++ b/jetlinks-components/things-component/src/main/java/org/jetlinks/community/things/data/NoneThingsDataRepositoryStrategy.java @@ -138,4 +138,9 @@ public class NoneThingsDataRepositoryStrategy implements public Mono reloadMetadata(ThingMetadata metadata) { return Mono.empty(); } + + @Override + public Mono validateMetadata(ThingMetadata metadata) { + return Mono.empty(); + } } diff --git a/jetlinks-components/things-component/src/main/java/org/jetlinks/community/things/data/operations/ColumnModeDDLOperationsBase.java b/jetlinks-components/things-component/src/main/java/org/jetlinks/community/things/data/operations/ColumnModeDDLOperationsBase.java index 5869bd10..2aac4b71 100644 --- a/jetlinks-components/things-component/src/main/java/org/jetlinks/community/things/data/operations/ColumnModeDDLOperationsBase.java +++ b/jetlinks-components/things-component/src/main/java/org/jetlinks/community/things/data/operations/ColumnModeDDLOperationsBase.java @@ -1,6 +1,8 @@ package org.jetlinks.community.things.data.operations; import org.jetlinks.core.metadata.PropertyMetadata; +import org.jetlinks.core.things.ThingMetadata; +import reactor.core.publisher.Mono; import java.util.ArrayList; import java.util.List; @@ -21,4 +23,9 @@ public abstract class ColumnModeDDLOperationsBase extends AbstractDDLOperations{ props.addAll(propertyMetadata); return props; } + + @Override + public Mono validateMetadata(ThingMetadata metadata) { + return Mono.empty(); + } } diff --git a/jetlinks-components/things-component/src/main/java/org/jetlinks/community/things/data/operations/DDLOperations.java b/jetlinks-components/things-component/src/main/java/org/jetlinks/community/things/data/operations/DDLOperations.java index 98da8601..744f844b 100644 --- a/jetlinks-components/things-component/src/main/java/org/jetlinks/community/things/data/operations/DDLOperations.java +++ b/jetlinks-components/things-component/src/main/java/org/jetlinks/community/things/data/operations/DDLOperations.java @@ -9,4 +9,5 @@ public interface DDLOperations { Mono reloadMetadata(ThingMetadata metadata); + Mono validateMetadata(ThingMetadata metadata); } diff --git a/jetlinks-components/things-component/src/main/java/org/jetlinks/community/things/data/operations/MetricBuilder.java b/jetlinks-components/things-component/src/main/java/org/jetlinks/community/things/data/operations/MetricBuilder.java index 287d73cf..d164b032 100644 --- a/jetlinks-components/things-component/src/main/java/org/jetlinks/community/things/data/operations/MetricBuilder.java +++ b/jetlinks-components/things-component/src/main/java/org/jetlinks/community/things/data/operations/MetricBuilder.java @@ -1,43 +1,124 @@ package org.jetlinks.community.things.data.operations; import org.jetlinks.community.things.data.ThingsDataConstants; +import org.jetlinks.core.config.ConfigKey; +import org.jetlinks.core.metadata.PropertyMetadata; + +import javax.annotation.Nonnull; +import java.util.Optional; public interface MetricBuilder { - MetricBuilder DEFAULT = new MetricBuilder() { }; + /** + * 获取自定义配置,通常由不同的存储策略来决定配置项 + * + * @param key 配置KEY + * @return 配置值 + */ + default Optional option(ConfigKey key) { + return Optional.empty(); + } + /** + * @return 物ID的字段标识, 默认为{@link ThingsDataConstants#COLUMN_THING_ID} + */ default String getThingIdProperty() { return ThingsDataConstants.COLUMN_THING_ID; } - default String createLogMetric(String thingType, - String thingTemplateId, + /** + * @return 物模版ID的字段标识, 默认为{@link ThingsDataConstants#COLUMN_THING_ID} + */ + default String getTemplateIdProperty() { + return ThingsDataConstants.COLUMN_TEMPLATE_ID; + } + + /** + * 创建日志存储的度量名称 + * + * @param thingType 物类型 + * @param thingTemplateId 物模版ID(设备产品ID) + * @param thingId 物ID + * @return 度量名 + */ + default String createLogMetric(@Nonnull String thingType, + @Nonnull String thingTemplateId, String thingId) { return thingType + "_log_" + thingTemplateId; } - default String createPropertyMetric(String thingType, - String thingTemplateId, + /** + * 创建日志存储的度量名称,当{@link DataSettings#getLogFilter()}, + * {@link DataSettings.Log#isAllInOne()}为true时调用. + * + * @param thingType 物类型 + * @return 度量名 + */ + default String createLogMetric(@Nonnull String thingType) { + return thingType + "_all_log"; + } + + + /** + * 创建属性存储的度量名称 + * + * @param thingType 物类型 + * @param thingTemplateId 物模版ID(设备产品ID) + * @param thingId 物ID + * @return 度量名 + */ + default String createPropertyMetric(@Nonnull String thingType, + @Nonnull String thingTemplateId, String thingId) { return thingType + "_properties_" + thingTemplateId; } - default String createEventAllInOneMetric(String thingType, - String thingTemplateId, + /** + * 创建属性存储的度量名称 + * + * @param thingType 物类型 + * @param thingTemplateId 物模版ID(设备产品ID) + * @param thingId 物ID + * @param group 物模型分组 + * @see ThingsDataConstants#propertyGroup(PropertyMetadata) + * @return 度量名 + */ + default String createPropertyMetric(@Nonnull String thingType, + @Nonnull String thingTemplateId, + String thingId, + String group) { + return thingType + "_properties_" + group + "_" + thingTemplateId; + } + + /** + * 创建事件存储的度量名称,用于存储所有事件数据 + * + * @param thingType 物类型 + * @param thingTemplateId 物模版ID(设备产品ID) + * @param thingId 物ID + * @return 度量名 + */ + default String createEventAllInOneMetric(@Nonnull String thingType, + @Nonnull String thingTemplateId, String thingId) { return thingType + "_event_" + thingTemplateId + "_events"; } - default String createEventMetric(String thingType, - String thingTemplateId, + /** + * 创建事件存储的度量名称,用于存储单个事件数据 + * + * @param thingType 物类型 + * @param thingTemplateId 物模版ID(设备产品ID) + * @param thingId 物ID + * @param eventId 事件ID + * @return 度量名 + */ + default String createEventMetric(@Nonnull String thingType, + @Nonnull String thingTemplateId, String thingId, - String eventId) { + @Nonnull String eventId) { return thingType + "_event_" + thingTemplateId + "_" + eventId; } - - default String getTemplateIdProperty() { - return ThingsDataConstants.COLUMN_TEMPLATE_ID; - } } diff --git a/jetlinks-components/things-component/src/main/java/org/jetlinks/community/things/data/operations/RowModeDDLOperationsBase.java b/jetlinks-components/things-component/src/main/java/org/jetlinks/community/things/data/operations/RowModeDDLOperationsBase.java index e5276230..42a1274f 100644 --- a/jetlinks-components/things-component/src/main/java/org/jetlinks/community/things/data/operations/RowModeDDLOperationsBase.java +++ b/jetlinks-components/things-component/src/main/java/org/jetlinks/community/things/data/operations/RowModeDDLOperationsBase.java @@ -1,9 +1,12 @@ package org.jetlinks.community.things.data.operations; +import org.hswebframework.web.exception.I18nSupportException; import org.jetlinks.core.metadata.PropertyMetadata; import org.jetlinks.core.metadata.SimplePropertyMetadata; import org.jetlinks.core.metadata.types.*; import org.jetlinks.community.things.data.ThingsDataConstants; +import org.jetlinks.core.things.ThingMetadata; +import reactor.core.publisher.Mono; import java.util.ArrayList; import java.util.List; @@ -21,6 +24,9 @@ public abstract class RowModeDDLOperationsBase extends AbstractDDLOperations{ @Override protected List createPropertyProperties(List propertyMetadata) { List props = new ArrayList<>(createBasicColumns()); + + this.validateMetadata(propertyMetadata,props); + props.add(SimplePropertyMetadata.of(ThingsDataConstants.COLUMN_PROPERTY_ID, "属性ID", StringType.GLOBAL)); props.add(SimplePropertyMetadata.of(ThingsDataConstants.COLUMN_PROPERTY_NUMBER_VALUE, "数字值", DoubleType.GLOBAL)); @@ -35,4 +41,50 @@ public abstract class RowModeDDLOperationsBase extends AbstractDDLOperations{ return props; } + + //是否只支持一个对象或数组类型的属性 + protected boolean isOnlySupportsOneObjectOrArrayProperty() { + return false; + } + + @Override + public Mono validateMetadata(ThingMetadata metadata) { + this.validateMetadata(metadata.getProperties(), null); + return Mono.empty(); + } + + private void validateMetadata(List propertyMetadata, List props){ + ArrayType arrayType = null; + ObjectType objectType = null; + if (isOnlySupportsOneObjectOrArrayProperty()) { + for (PropertyMetadata metadata : propertyMetadata) { + if (ThingsDataConstants.propertyIsJsonStringStorage(metadata)) { + continue; + } + if (metadata.getValueType() instanceof ArrayType) { + if (arrayType != null) { + throw new I18nSupportException("error.thing_storage_only_supports_one_array_type", metadata.getId()); + } + arrayType = (ArrayType) metadata.getValueType(); + } + if (metadata.getValueType() instanceof ObjectType) { + if (objectType != null) { + throw new I18nSupportException("error.thing_storage_only_supports_one_object_type", metadata.getId()); + } + objectType = (ObjectType) metadata.getValueType(); + } + } + } + if (props == null) { + return; + } + if (arrayType == null) { + arrayType = new ArrayType(); + } + if (objectType == null) { + objectType = new ObjectType(); + } + props.add(SimplePropertyMetadata.of(ThingsDataConstants.COLUMN_PROPERTY_ARRAY_VALUE, "数组值", arrayType)); + props.add(SimplePropertyMetadata.of(ThingsDataConstants.COLUMN_PROPERTY_OBJECT_VALUE, "对象值", objectType)); + } } diff --git a/jetlinks-components/things-component/src/main/java/org/jetlinks/community/things/holder/ThingsRegistryHolder.java b/jetlinks-components/things-component/src/main/java/org/jetlinks/community/things/holder/ThingsRegistryHolder.java new file mode 100644 index 00000000..b726a6e1 --- /dev/null +++ b/jetlinks-components/things-component/src/main/java/org/jetlinks/community/things/holder/ThingsRegistryHolder.java @@ -0,0 +1,15 @@ +package org.jetlinks.community.things.holder; + +import org.jetlinks.core.things.ThingsRegistry; + +public class ThingsRegistryHolder { + + static ThingsRegistry REGISTRY; + + public static ThingsRegistry registry() { + if (REGISTRY == null) { + throw new IllegalStateException("registry not initialized"); + } + return REGISTRY; + } +} diff --git a/jetlinks-components/things-component/src/main/java/org/jetlinks/community/things/holder/ThingsRegistryHolderInitializer.java b/jetlinks-components/things-component/src/main/java/org/jetlinks/community/things/holder/ThingsRegistryHolderInitializer.java new file mode 100644 index 00000000..931c4886 --- /dev/null +++ b/jetlinks-components/things-component/src/main/java/org/jetlinks/community/things/holder/ThingsRegistryHolderInitializer.java @@ -0,0 +1,11 @@ +package org.jetlinks.community.things.holder; + +import org.jetlinks.core.things.ThingsRegistry; + +public class ThingsRegistryHolderInitializer { + + public static void init(ThingsRegistry registry) { + ThingsRegistryHolder.REGISTRY = registry; + } + +} diff --git a/jetlinks-components/things-component/src/main/java/org/jetlinks/community/things/utils/ThingMetadataHelper.java b/jetlinks-components/things-component/src/main/java/org/jetlinks/community/things/utils/ThingMetadataHelper.java new file mode 100644 index 00000000..a14a5d9e --- /dev/null +++ b/jetlinks-components/things-component/src/main/java/org/jetlinks/community/things/utils/ThingMetadataHelper.java @@ -0,0 +1,43 @@ +package org.jetlinks.community.things.utils; + +import org.jetlinks.core.things.MetadataId; +import org.jetlinks.core.things.ThingId; +import org.jetlinks.core.things.ThingMetadataType; +import reactor.function.Function3; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.BiFunction; +import java.util.function.Function; + +public class ThingMetadataHelper { + + private final Map> mappers = new ConcurrentHashMap<>(); + + public static ThingMetadataHelper create() { + return new ThingMetadataHelper<>(); + } + + public ThingMetadataHelper when(ThingMetadataType type, Function3 mapper) { + mappers.put(type, mapper); + return this; + } + + public ThingMetadataHelper when(ThingMetadataType type, BiFunction mapper) { + mappers.put(type, (ignore, mid, t) -> mapper.apply(mid, t)); + return this; + } + + public Function toFunction(MetadataId metadataId) { + return toFunction(null, metadataId); + } + + public Function toFunction(ThingId thingId, MetadataId metadataId) { + Function3 function = mappers.get(metadataId.getType()); + if (function == null) { + throw new UnsupportedOperationException("unsupported metadata type " + metadataId.getType()); + } + return (source) -> function.apply(thingId, metadataId, source); + } + +} diff --git a/jetlinks-components/things-component/src/main/java/org/jetlinks/community/things/utils/ThingsDatabaseUtils.java b/jetlinks-components/things-component/src/main/java/org/jetlinks/community/things/utils/ThingsDatabaseUtils.java new file mode 100644 index 00000000..4f5195c0 --- /dev/null +++ b/jetlinks-components/things-component/src/main/java/org/jetlinks/community/things/utils/ThingsDatabaseUtils.java @@ -0,0 +1,254 @@ +package org.jetlinks.community.things.utils; + +import org.hswebframework.ezorm.core.ValueCodec; +import org.hswebframework.ezorm.core.param.Term; +import org.hswebframework.ezorm.core.param.TermType; +import org.hswebframework.ezorm.rdb.codec.BooleanValueCodec; +import org.hswebframework.ezorm.rdb.codec.ClobValueCodec; +import org.hswebframework.ezorm.rdb.codec.JsonValueCodec; +import org.hswebframework.ezorm.rdb.codec.NumberValueCodec; +import org.hswebframework.ezorm.rdb.metadata.RDBColumnMetadata; +import org.jetlinks.community.ConfigMetadataConstants; +import org.jetlinks.community.utils.ConverterUtils; +import org.jetlinks.core.metadata.DataType; +import org.jetlinks.core.metadata.PropertyMetadata; +import org.jetlinks.core.metadata.types.*; +import org.jetlinks.core.utils.StringBuilderUtils; +import org.jetlinks.reactor.ql.utils.CastUtils; +import org.jetlinks.supports.official.DeviceMetadataParser; +import org.springframework.core.ResolvableType; +import org.springframework.util.ObjectUtils; + +import java.sql.JDBCType; +import java.sql.SQLType; +import java.util.*; +import java.util.function.BiFunction; +import java.util.function.BiPredicate; + +public class ThingsDatabaseUtils { + + static GeoCodec geoCodec = new GeoCodec(); + + static StringCodec stringCodec = new StringCodec(); + + public static DataType sqlTypeToDataType(SQLType sqlType) { + if (sqlType == JDBCType.BIGINT) { + return LongType.GLOBAL; + } + if (sqlType == JDBCType.INTEGER || sqlType == JDBCType.SMALLINT || sqlType == JDBCType.TINYINT) { + return IntType.GLOBAL; + } + if (sqlType == JDBCType.TIMESTAMP || sqlType == JDBCType.DATE || sqlType == JDBCType.TIME) { + return DateTimeType.GLOBAL; + } + if (sqlType == JDBCType.DOUBLE || sqlType == JDBCType.FLOAT || sqlType == JDBCType.NUMERIC || sqlType == JDBCType.DECIMAL) { + return DoubleType.GLOBAL; + } + return StringType.GLOBAL; + } + + 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); + } + } + + public static Class convertJavaType(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; + } + } + + public static RDBColumnMetadata convertColumn(PropertyMetadata metadata, RDBColumnMetadata column) { + 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(4)); + column.setValueCodec(new NumberValueCodec(Double.class)); + column.setJdbcType(JDBCType.DOUBLE, 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.FLOAT, Float.class); + } else if (type instanceof LongType) { + column.setValueCodec(new NumberValueCodec(Long.class)); + column.setJdbcType(JDBCType.BIGINT, 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, convertJavaType(arrayType.getElementType()))); + } else if (type instanceof DateTimeType) { + column.setJdbcType(JDBCType.BIGINT, Long.class); + column.setValueCodec(new NumberValueCodec(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 if (type instanceof BooleanType) { + column.setJdbcType(JDBCType.BOOLEAN, Boolean.class); + column.setValueCodec(new BooleanValueCodec(JDBCType.BOOLEAN)); + column.setLength(64); + } else { + int len = type + .getExpand(ConfigMetadataConstants.maxLength.getKey()) + .filter(o -> !ObjectUtils.isEmpty(o)) + .map(CastUtils::castNumber) + .map(Number::intValue) + .orElse(255); + if (len < 0 || len > 2048) { + column.setJdbcType(JDBCType.LONGVARCHAR, String.class); + column.setValueCodec(ClobValueCodec.INSTANCE); + } else { + column.setJdbcType(JDBCType.VARCHAR, String.class); + column.setLength(len == 0 ? 255 : len); + column.setValueCodec(stringCodec); + } + } + + return column; + } + + public static RDBColumnMetadata convertColumn(PropertyMetadata metadata) { + return convertColumn(metadata, new RDBColumnMetadata()); + } + + public static DataType convertDataType(RDBColumnMetadata column) { + DataType type; + if (column.getJavaType() != null) { + type = DeviceMetadataParser.withType(ResolvableType.forType(column.getJavaType())); + } else if (column.getSqlType() != null) { + type = ThingsDatabaseUtils.sqlTypeToDataType(column.getSqlType()); + } else { + type = StringType.GLOBAL; + } + return type; + } + + private final static Base64.Encoder tableEncoder = Base64.getUrlEncoder().withoutPadding(); + + private static String createTableName0(String prefix, String... suffixes) { + return StringBuilderUtils + .buildString(prefix, suffixes, (_prefix, _suffixes, builder) -> { + + //前缀 + appendTable(_prefix, builder); + + //后缀 + for (String suffix : _suffixes) { + builder.append('_'); + appendTable(suffix, builder); + } + }); + } + + private static void appendTable(String table, StringBuilder builder) { + for (int i = 0; i < table.length(); i++) { + char ch = Character.toLowerCase(table.charAt(i)); + if ((ch < 'a' || ch > 'z') && (ch < 'A' || ch > 'Z') && (ch < '0' || ch > '9')) { + builder.append('_'); + } else { + builder.append(ch); + } + } + } + + public static void tryConvertTermValue(DataType type, + Term term, + BiPredicate isDoNotConvertValue, + BiPredicate maybeIsList, + BiFunction tryConvertTermValue) { + if (ObjectUtils.isEmpty(term.getColumn()) || isDoNotConvertValue.test(type, term)) { + return; + } + + Object value; + if (maybeIsList.test(type, term)) { + value = ConverterUtils.tryConvertToList(term.getValue(), v -> tryConvertTermValue.apply(type, v)); + } else { + value = tryConvertTermValue.apply(type, term.getValue()); + } + if (null != value) { + term.setValue(value); + } + } + + public static boolean maybeList(DataType type, Term term) { + switch (term.getTermType().toLowerCase()) { + case TermType.in: + case TermType.nin: + case TermType.btw: + case TermType.nbtw: + return true; + } + return false; + } + + public static boolean isDoNotConvertValue(DataType type, Term term) { + switch (term.getTermType().toLowerCase()) { + case TermType.isnull: + case TermType.notnull: + case TermType.empty: + case TermType.nempty: + return true; + } + return false; + } + + +} diff --git a/jetlinks-components/things-component/src/main/java/org/jetlinks/community/things/utils/ThingsUtils.java b/jetlinks-components/things-component/src/main/java/org/jetlinks/community/things/utils/ThingsUtils.java new file mode 100644 index 00000000..0941e58d --- /dev/null +++ b/jetlinks-components/things-component/src/main/java/org/jetlinks/community/things/utils/ThingsUtils.java @@ -0,0 +1,52 @@ +package org.jetlinks.community.things.utils; + +import com.google.common.collect.Maps; +import lombok.extern.slf4j.Slf4j; +import org.jetlinks.core.message.ThingMessage; +import org.jetlinks.core.message.ThingMessageReply; +import org.jetlinks.core.message.event.ThingEventMessage; +import org.jetlinks.core.message.function.ThingFunctionInvokeMessageReply; +import org.jetlinks.core.message.property.Property; +import org.jetlinks.core.message.property.PropertyMessage; + +import java.util.Map; + +@Slf4j +public class ThingsUtils { + public static final String FUNCTION_OUTPUT_CONTEXT_KEY = "__output"; + public static final String EVENT_DATA_CONTEXT_KEY = "__data"; + + @SuppressWarnings("all") + public static Map messageToContextMap(ThingMessage message) { + Map map = Maps.newHashMapWithExpectedSize(32); + + if (message instanceof ThingMessageReply) { + map.put("success", ((ThingMessageReply) message).isSuccess()); + } + if (message instanceof ThingFunctionInvokeMessageReply) { + Object output = ((ThingFunctionInvokeMessageReply) message).getOutput(); + if (output instanceof Map) { + map.putAll(((Map) output)); + } else if (null != output) { + map.put(FUNCTION_OUTPUT_CONTEXT_KEY, output); + } + } else if (message instanceof PropertyMessage) { + PropertyMessage msg = ((PropertyMessage) message); + for (Property property : msg.getCompleteProperties()) { + map.put(property.getProperty(), property.getValue()); + map.put(property.getProperty() + ".timestamp", property.getTimestamp()); + map.put(property.getProperty() + ".state", property.getState()); + } + } else if (message instanceof ThingEventMessage) { + Object data = ((ThingEventMessage) message).getData(); + if (data instanceof Map) { + map.putAll(((Map) data)); + } else if (null != data) { + map.put(EVENT_DATA_CONTEXT_KEY, data); + } + } + + return map; + } + +} diff --git a/jetlinks-components/things-component/src/main/resources/i18n/things-component/messages_en.properties b/jetlinks-components/things-component/src/main/resources/i18n/things-component/messages_en.properties new file mode 100644 index 00000000..d4e1cb26 --- /dev/null +++ b/jetlinks-components/things-component/src/main/resources/i18n/things-component/messages_en.properties @@ -0,0 +1,8 @@ + +error.thing_storage_only_supports_one_array_type=The current storage policy does not support the simultaneous \ + existence of properties of multiple array types :{0} +error.thing_storage_only_supports_one_object_type=The current storage policy does not support the simultaneous \ + existence of properties of multiple object types :{0} +error.thing_data_policy_unsupported=Unsupported storage policy {0} + +message.property_threshold_alarm_name={0} threshold alarm \ No newline at end of file diff --git a/jetlinks-components/things-component/src/main/resources/i18n/things-component/messages_zh.properties b/jetlinks-components/things-component/src/main/resources/i18n/things-component/messages_zh.properties new file mode 100644 index 00000000..e3c53046 --- /dev/null +++ b/jetlinks-components/things-component/src/main/resources/i18n/things-component/messages_zh.properties @@ -0,0 +1,7 @@ + +error.thing_storage_only_supports_one_array_type=\u5F53\u524D\u5B58\u50A8\u7B56\u7565\u4E0D\u652F\u6301\u540C\u65F6\u5B58\u5728\u591A\u4E2A\u6570\u7EC4\u7C7B\u578B\u7684\u5C5E\u6027:{0} +error.thing_storage_only_supports_one_object_type=\u5F53\u524D\u5B58\u50A8\u7B56\u7565\u4E0D\u652F\u6301\u540C\u65F6\u5B58\u5728\u591A\u4E2A\u5BF9\u8C61\u7C7B\u578B\u7684\u5C5E\u6027:{0} +error.thing_data_policy_unsupported=\u4E0D\u652F\u6301\u7684\u5B58\u50A8\u7B56\u7565{0} + + +message.property_threshold_alarm_name={0}\u9608\u503C\u544A\u8B66 \ No newline at end of file diff --git a/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/captcha/CaptchaController.java b/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/captcha/CaptchaController.java index d539072e..d7bb1589 100644 --- a/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/captcha/CaptchaController.java +++ b/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/captcha/CaptchaController.java @@ -43,27 +43,6 @@ public class CaptchaController { return Mono.just(captchaConfig); } - @GetMapping("/image") - @Operation(summary = "获取验证码图片") - public Mono createCaptcha(@RequestParam(defaultValue = "130") - @Parameter(description = "宽度,默认130px") int width, - @RequestParam(defaultValue = "40") - @Parameter(description = "高度,默认40px") int height) { - if (!properties.isEnabled()) { - return Mono.empty(); - } - SpecCaptcha captcha = new SpecCaptcha(width, height, 4); - captcha.setCharType(Captcha.TYPE_DEFAULT); - - String base64 = captcha.toBase64(); - String key = UUID.randomUUID().toString(); - - return redis - .opsForValue() - .set("captcha:" + key, captcha.text(), properties.getTtl()) - .thenReturn(new CaptchaInfo(key, base64)); - } - @EventListener public void handleAuthEvent(AuthorizationDecodeEvent event) { if (!properties.isEnabled()) { diff --git a/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/captcha/CaptchaProperties.java b/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/captcha/CaptchaProperties.java index 589a012d..f5d036eb 100644 --- a/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/captcha/CaptchaProperties.java +++ b/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/captcha/CaptchaProperties.java @@ -1,17 +1,34 @@ package org.jetlinks.community.auth.captcha; +import lombok.Generated; import lombok.Getter; import lombok.Setter; +import org.jetlinks.community.auth.captcha.impl.ImageCaptchaProvider; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; import java.time.Duration; +/** + * 验证码配置 + *

+ * 如: + *

+ *      captcha:
+ *          enabled: true # 开启验证码
+ *          ttl: 2m #验证码过期时间,2分钟
+ * 
+ * 

+ * @author zhouhao + * @since 1.4 + */ @Component @ConfigurationProperties(prefix = "captcha") @Getter @Setter +@Generated public class CaptchaProperties { + //是否开启验证码 private boolean enabled = false; private Duration ttl = Duration.ofMinutes(2); diff --git a/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/captcha/CaptchaProvider.java b/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/captcha/CaptchaProvider.java new file mode 100644 index 00000000..95d13344 --- /dev/null +++ b/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/captcha/CaptchaProvider.java @@ -0,0 +1,39 @@ +package org.jetlinks.community.auth.captcha; + +import org.jetlinks.community.spi.Provider; +import reactor.core.publisher.Mono; + +/** + * 验证码提供商,实现此接口并注册为spring bean即可提供验证码服务. + * + * @author zhouhao + * @see CaptchaProperties + * @since 2.1 + */ +public interface CaptchaProvider { + + Provider supports = Provider.create(CaptchaProvider.class); + + /** + * @return 验证码类型 + */ + String getType(); + + /** + * 执行验证,如果验证失败,应当返回异常,如: + *
{@code
+     *
+     *  return Mono.error(new ValidationException("error.verification_code"));
+     *
+     * }
+ * + * @param context 校验上下文,可通过上下文来获取参数 + * @return 验证结果 + */ + Mono validate(ValidationContext context); + + /** + * @return 提供给前端所需的配置信息 + */ + Object getConfigForFront(); +} diff --git a/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/captcha/ValidationContext.java b/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/captcha/ValidationContext.java new file mode 100644 index 00000000..70268866 --- /dev/null +++ b/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/captcha/ValidationContext.java @@ -0,0 +1,8 @@ +package org.jetlinks.community.auth.captcha; + +import java.util.Optional; + +public interface ValidationContext { + Optional getParameter(String name); + +} diff --git a/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/captcha/impl/ImageCaptchaProvider.java b/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/captcha/impl/ImageCaptchaProvider.java new file mode 100644 index 00000000..5b2b89e5 --- /dev/null +++ b/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/captcha/impl/ImageCaptchaProvider.java @@ -0,0 +1,122 @@ +package org.jetlinks.community.auth.captcha.impl; + +import com.wf.captcha.SpecCaptcha; +import com.wf.captcha.base.Captcha; +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.*; +import org.hswebframework.web.exception.ValidationException; +import org.hswebframework.web.id.IDGenerator; +import org.hswebframework.web.id.RandomIdGenerator; +import org.jetlinks.community.auth.captcha.CaptchaProvider; +import org.jetlinks.community.auth.captcha.ValidationContext; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.data.redis.core.ReactiveRedisOperations; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import reactor.core.publisher.Mono; + +import java.time.Duration; +import java.util.Collections; + +@Getter +@Setter +@RestController +@RequiredArgsConstructor +@ConfigurationProperties(prefix = "captcha.image") +@ConditionalOnProperty(prefix = "captcha.image", name = "enabled", havingValue = "true", matchIfMissing = true) +@Tag(name = "图片验证码接口") +public class ImageCaptchaProvider implements CaptchaProvider { + + public static final String provider = "image"; + + private final ReactiveRedisOperations redis; + + private Duration ttl = Duration.ofMinutes(2); + + private boolean enabled = true; + + private int charType = Captcha.TYPE_DEFAULT; + + private int length = 4; + + @Override + public String getType() { + return "image"; + } + + @Override + public Object getConfigForFront() { + return Collections.singletonMap( + "imageUrl","/authorize/captcha/image" + ); + } + + protected boolean isLegalKey(String key) { + return RandomIdGenerator.timestampRangeOf(key, ttl); + } + + @Override + public Mono validate(ValidationContext event) { + if (!enabled) { + return Mono.empty(); + } + + String key = event + .getParameter("verifyKey") + .map(String::valueOf) + .filter(this::isLegalKey) + .orElseThrow(() -> new ValidationException.NoStackTrace("error.verification_code_expired")); + + String code = event + .getParameter("verifyCode") + .map(String::valueOf) + .orElseThrow(() -> new ValidationException.NoStackTrace("error.verification_code")); + + String redisKey = "captcha:" + key; + return redis + .opsForValue() + .get(redisKey) + .map(code::equalsIgnoreCase) + .defaultIfEmpty(false) + .flatMap(checked -> redis + .delete(redisKey) + .then(checked ? Mono.empty() : Mono.error(new ValidationException.NoStackTrace("error.verification_code")))); + } + + @GetMapping("/authorize/captcha/image") + @Operation(summary = "获取验证码图片") + public Mono createCaptcha(@RequestParam(defaultValue = "130") + @Parameter(description = "宽度,默认130px") int width, + @RequestParam(defaultValue = "40") + @Parameter(description = "高度,默认40px") int height) { + if (!enabled) { + return Mono.empty(); + } + SpecCaptcha captcha = new SpecCaptcha(width, height, length); + captcha.setCharType(charType); + + String base64 = captcha.toBase64(); + String key = IDGenerator.RANDOM.generate(); + return redis + .opsForValue() + .set("captcha:" + key, captcha.text(), getTtl()) + .thenReturn(new CaptchaInfo(key, base64)); + } + + @Getter + @Setter + @AllArgsConstructor + @NoArgsConstructor + public static class CaptchaInfo { + @Schema(description = "验证码标识,登录时需要在参数[verifyKey]传入此值.") + private String key; + + @Schema(description = "图片Base64,以data:image/png;base64,开头") + private String base64; + } +} diff --git a/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/cipher/CipherConfig.java b/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/cipher/CipherConfig.java new file mode 100644 index 00000000..46d75a66 --- /dev/null +++ b/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/cipher/CipherConfig.java @@ -0,0 +1,15 @@ +package org.jetlinks.community.auth.cipher; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class CipherConfig { + + private boolean enabled; + + private String publicKey; + + private String id; +} diff --git a/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/cipher/CipherHelper.java b/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/cipher/CipherHelper.java new file mode 100644 index 00000000..bac51bc1 --- /dev/null +++ b/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/cipher/CipherHelper.java @@ -0,0 +1,82 @@ +package org.jetlinks.community.auth.cipher; + +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.hswebframework.web.authorization.exception.AuthenticationException; +import org.hswebframework.web.id.IDGenerator; +import org.hswebframework.web.id.RandomIdGenerator; +import org.jetlinks.community.utils.CryptoUtils; +import org.springframework.data.redis.core.ReactiveRedisOperations; +import reactor.core.publisher.Mono; + +import java.security.KeyPair; +import java.util.Base64; + +@Slf4j +@AllArgsConstructor +public class CipherHelper { + + private final ReactiveRedisOperations redis; + + private final CipherProperties properties; + + private String createEncRedisKey(String encryptId) { + return "cipher:encrypt-key:" + encryptId; + } + + + public boolean isEnabled(){ + return properties.isEnabled(); + } + + public Mono getConfig() { + if (!isEnabled()) { + CipherConfig config = new CipherConfig(); + config.setEnabled(false); + return Mono.just(config); + } + String id = IDGenerator.RANDOM.generate(); + KeyPair rasKey = CryptoUtils.generateRSAKey(); + String pubKeyBase64 = Base64.getEncoder().encodeToString(rasKey.getPublic().getEncoded()); + String priKeyBase64 = Base64.getEncoder().encodeToString(rasKey.getPrivate().getEncoded()); + + CipherConfig value = new CipherConfig(); + value.setEnabled(true); + value.setPublicKey(pubKeyBase64); + value.setId(id); + return redis + .opsForValue() + .set(createEncRedisKey(id), priKeyBase64, properties.getKeyTtl()) + .thenReturn(value); + } + + + public Mono decrypt(String encId, String text) { + if (!isLegalEncryptId(encId)) { + return Mono.error(() -> new AuthenticationException.NoStackTrace(AuthenticationException.ILLEGAL_PASSWORD)); + } + String redisKey = createEncRedisKey(encId); + return redis + .opsForValue() + .get(redisKey) + .map(privateKey -> new String( + CryptoUtils.decryptRSA(Base64.getDecoder().decode(text), + CryptoUtils.decodeRSAPrivateKey(privateKey)) + )) + .onErrorResume(err -> { + log.warn("decrypt password error", err); + return redis + .opsForValue() + .delete(redisKey) + .then(Mono.error(() -> new AuthenticationException.NoStackTrace(AuthenticationException.ILLEGAL_PASSWORD))); + }) + .flatMap(s -> redis + .opsForValue() + .delete(redisKey) + .thenReturn(s)); + } + + private boolean isLegalEncryptId(String id) { + return RandomIdGenerator.timestampRangeOf(id, properties.getKeyTtl()); + } +} diff --git a/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/cipher/CipherProperties.java b/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/cipher/CipherProperties.java new file mode 100644 index 00000000..483962cb --- /dev/null +++ b/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/cipher/CipherProperties.java @@ -0,0 +1,16 @@ +package org.jetlinks.community.auth.cipher; + +import lombok.Getter; +import lombok.Setter; + +import java.time.Duration; + +@Getter +@Setter +public class CipherProperties { + + private boolean enabled; + + private Duration keyTtl = Duration.ofMinutes(5); + +} diff --git a/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/configuration/CustomAuthenticationConfiguration.java b/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/configuration/CustomAuthenticationConfiguration.java index 2f953d55..45e5fb82 100644 --- a/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/configuration/CustomAuthenticationConfiguration.java +++ b/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/configuration/CustomAuthenticationConfiguration.java @@ -4,8 +4,10 @@ import com.github.benmanes.caffeine.cache.Caffeine; import org.hswebframework.web.authorization.token.UserTokenManager; import org.hswebframework.web.authorization.token.redis.RedisUserTokenManager; import org.hswebframework.web.authorization.token.redis.SimpleUserToken; +import org.jetlinks.community.auth.dimension.UserAuthenticationEventPublisher; import org.jetlinks.community.auth.enums.UserEntityType; import org.jetlinks.community.auth.web.WebFluxUserController; +import org.jetlinks.core.event.EventBus; import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties; @@ -21,6 +23,8 @@ import java.time.Duration; @EnableConfigurationProperties({MenuProperties.class}) public class CustomAuthenticationConfiguration { + static final String CONDITION_CLASS_NAME = "org.jetlinks.community.microservice.configuration.CloudServicesConfiguration"; + @Bean @Primary public WebFluxUserController webFluxUserController() { @@ -42,6 +46,11 @@ public class CustomAuthenticationConfiguration { return userTokenManager; } + @Bean(destroyMethod = "shutdown") + public UserAuthenticationEventPublisher userDimensionEventPublisher(EventBus eventBus) { + return new UserAuthenticationEventPublisher(eventBus); + } + @Bean public Jackson2ObjectMapperBuilderCustomizer jackson2ObjectMapperBuilderAuthCustomizer() { return builder -> { diff --git a/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/configuration/MenuProperties.java b/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/configuration/MenuProperties.java index 248d0c92..d83f9f13 100644 --- a/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/configuration/MenuProperties.java +++ b/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/configuration/MenuProperties.java @@ -15,6 +15,7 @@ import java.util.Set; public class MenuProperties { private Set allowAllMenusUsers = new HashSet<>(Collections.singletonList("admin")); + private String allowPermission = "menu"; public boolean isAllowAllMenu(Authentication auth) { return allowAllMenusUsers.contains(auth.getUser().getUsername()); diff --git a/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/configuration/UserEntityTypeJSONDeerializer.java b/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/configuration/UserEntityTypeJSONDeerializer.java new file mode 100644 index 00000000..75788000 --- /dev/null +++ b/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/configuration/UserEntityTypeJSONDeerializer.java @@ -0,0 +1,37 @@ +package org.jetlinks.community.auth.configuration; + +import com.fasterxml.jackson.core.JacksonException; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import org.jetlinks.community.auth.enums.UserEntityType; + +import java.io.IOException; +import java.util.Map; + +/** + * 用户类型-反序列化. + * + * @author zhangji 2022/12/8 + */ +public class UserEntityTypeJSONDeerializer extends JsonDeserializer { + @Override + public UserEntityType deserialize(JsonParser jsonParser, + DeserializationContext ctxt) throws IOException, JacksonException { + if (jsonParser.hasToken(JsonToken.VALUE_STRING)) { + String str = jsonParser.getText().trim(); + if (str.length() != 0) { + return UserEntityType.of(str, null); + } + } + + if (jsonParser.hasToken(JsonToken.START_OBJECT)) { + Map map = ctxt.readValue(jsonParser, Map.class); + if (map != null) { + return UserEntityType.of(map.get("id"), map.get("name")); + } + } + return null; + } +} diff --git a/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/constant/AuthManagerConstants.java b/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/constant/AuthManagerConstants.java new file mode 100644 index 00000000..007fc2ec --- /dev/null +++ b/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/constant/AuthManagerConstants.java @@ -0,0 +1,21 @@ +package org.jetlinks.community.auth.constant; + +/** + * + * @author zhangji 2025/3/6 + * @since 2.3 + */ +public interface AuthManagerConstants { + + interface Resource { + // 组织管理 + String organization = "organization"; + + // 用户管理 + String user = "user"; + + // 角色管理 + String role = "role"; + } + +} diff --git a/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/constants/AuthConstants.java b/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/constants/AuthConstants.java new file mode 100644 index 00000000..f0e1a82a --- /dev/null +++ b/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/constants/AuthConstants.java @@ -0,0 +1,14 @@ +package org.jetlinks.community.auth.constants; + +/** + * @author wangsheng + */ +public interface AuthConstants { + + /** + * 标识是否为直接关联的维度,如直属部门 + * + * @see org.hswebframework.web.authorization.Dimension#getOption(String) + */ + public static String IS_DIRECT = "direct"; +} diff --git a/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/dimension/BaseDimensionProvider.java b/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/dimension/BaseDimensionProvider.java index a1111ecd..863301bb 100644 --- a/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/dimension/BaseDimensionProvider.java +++ b/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/dimension/BaseDimensionProvider.java @@ -1,6 +1,7 @@ package org.jetlinks.community.auth.dimension; -import lombok.AllArgsConstructor; +import com.google.common.collect.Collections2; +import com.google.common.collect.Lists; import lombok.RequiredArgsConstructor; import org.apache.commons.collections4.CollectionUtils; import org.hswebframework.ezorm.rdb.mapping.ReactiveQuery; @@ -17,20 +18,17 @@ import org.hswebframework.web.system.authorization.api.entity.DimensionUserEntit import org.hswebframework.web.system.authorization.api.event.ClearUserAuthorizationCacheEvent; import org.hswebframework.web.system.authorization.defaults.service.DefaultDimensionUserService; import org.hswebframework.web.system.authorization.defaults.service.terms.DimensionTerm; +import org.jetlinks.community.auth.utils.DimensionUserBindUtils; import org.reactivestreams.Publisher; import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.event.EventListener; import org.springframework.core.GenericTypeResolver; import org.springframework.transaction.reactive.TransactionSynchronization; -import org.springframework.util.StringUtils; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import javax.annotation.Nonnull; -import java.util.Collection; -import java.util.Collections; -import java.util.List; -import java.util.Map; +import java.util.*; import java.util.function.Function; import java.util.stream.Collectors; @@ -113,14 +111,19 @@ public abstract class BaseDimensionProvider> imp return genType == null || !genType.isAssignableFrom(type); } - @EventListener public void handleEvent(EntityDeletedEvent event) { if (isNotSameType(event.getEntityType())) { return; } + // 删除绑定信息 event.async( - clearUserAuthenticationCache(event.getEntity()) + DimensionUserBindUtils.unbindUser( + dimensionUserService, + null, + getDimensionType().getId(), + Lists.transform(event.getEntity(), GenericEntity::getId)) +// clearUserAuthenticationCache(event.getEntity()) ); } @@ -162,11 +165,10 @@ public abstract class BaseDimensionProvider> imp return true; } - - private Mono clearUserAuthenticationCache0(Collection entities) { + @SuppressWarnings("all") + private Mono clearUserAuthenticationCache0(Collection idList) { return Flux - .fromIterable(entities) - .mapNotNull(GenericEntity::getId) + .fromIterable(Collections2.filter(idList, Objects::nonNull)) .buffer(200) .flatMap(list -> dimensionUserService .createQuery() @@ -182,15 +184,22 @@ public abstract class BaseDimensionProvider> imp .then(); } + protected Mono clearUserAuthenticationCacheById(Collection entities) { + return ClearUserAuthorizationCacheEvent + .doOnEnabled( + TransactionUtils + .registerSynchronization(new TransactionSynchronization() { + @Override + @Nonnull + public Mono afterCommit() { + return clearUserAuthenticationCache0(entities); + } + }, TransactionSynchronization::afterCommit) + ); + } + protected Mono clearUserAuthenticationCache(Collection entities) { - return TransactionUtils - .registerSynchronization(new TransactionSynchronization() { - @Override - @Nonnull - public Mono afterCommit() { - return clearUserAuthenticationCache0(entities); - } - }, TransactionSynchronization::afterCommit); + return clearUserAuthenticationCacheById(Collections2.transform(entities, GenericEntity::getId)); } } diff --git a/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/dimension/OrgDimensionType.java b/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/dimension/OrgDimensionType.java index b6628c3a..d23a5fad 100755 --- a/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/dimension/OrgDimensionType.java +++ b/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/dimension/OrgDimensionType.java @@ -13,7 +13,8 @@ import org.hswebframework.web.authorization.DimensionType; @Getter @Generated public enum OrgDimensionType implements DimensionType { - org("org","机构"); + org("org","组织"), + parentOrg("parentOrg","上级组织"); private final String id; private final String name; diff --git a/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/dimension/OrganizationDimensionProvider.java b/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/dimension/OrganizationDimensionProvider.java index e293be07..3ee5e43d 100644 --- a/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/dimension/OrganizationDimensionProvider.java +++ b/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/dimension/OrganizationDimensionProvider.java @@ -1,29 +1,35 @@ package org.jetlinks.community.auth.dimension; -import org.hswebframework.ezorm.rdb.mapping.ReactiveRepository; +import org.hswebframework.web.api.crud.entity.TreeSupportEntity; import org.hswebframework.web.authorization.Dimension; import org.hswebframework.web.authorization.DimensionType; +import org.hswebframework.web.crud.events.EntityCreatedEvent; +import org.hswebframework.web.id.IDGenerator; +import org.hswebframework.web.system.authorization.api.event.ClearUserAuthorizationCacheEvent; import org.hswebframework.web.system.authorization.defaults.service.DefaultDimensionUserService; import org.hswebframework.web.system.authorization.defaults.service.terms.DimensionTerm; import org.jetlinks.community.auth.entity.OrganizationEntity; -import org.jetlinks.community.auth.entity.OrganizationEntity; +import org.jetlinks.community.auth.service.OrganizationService; import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.event.EventListener; import org.springframework.stereotype.Component; import org.springframework.util.ObjectUtils; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; -import java.util.Collections; -import java.util.LinkedHashMap; -import java.util.Map; +import java.util.*; +import java.util.stream.Collectors; @Component public class OrganizationDimensionProvider extends BaseDimensionProvider { - public OrganizationDimensionProvider(ReactiveRepository repository, + private final OrganizationService organizationService; + + public OrganizationDimensionProvider(OrganizationService organizationService, DefaultDimensionUserService dimensionUserService, ApplicationEventPublisher eventPublisher) { - super(repository, eventPublisher, dimensionUserService); + super(organizationService.getRepository(), eventPublisher, dimensionUserService); + this.organizationService = organizationService; } @Override @@ -58,4 +64,25 @@ public class OrganizationDimensionProvider extends BaseDimensionProvider Flux.fromIterable(dimensions.values()))); } + @Override + protected Mono clearUserAuthenticationCache(Collection entities) { + //清空上下级,因为维度中记录了上下级信息. + return Flux.concat( + organizationService.queryIncludeParent(Flux.fromIterable(entities)), + organizationService.queryIncludeChildren(Flux.fromIterable(entities)) + ) + .distinct(OrganizationEntity::getId) + .collectList() + .flatMap(super::clearUserAuthenticationCache) + .as(ClearUserAuthorizationCacheEvent::doOnEnabled); + } + + + @EventListener + public void handleCreatEvent(EntityCreatedEvent event) { + event.async( + clearUserAuthenticationCache(event.getEntity()) + ); + } + } diff --git a/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/dimension/RoleDimensionProvider.java b/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/dimension/RoleDimensionProvider.java index 80cfbc5b..f9437ed2 100644 --- a/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/dimension/RoleDimensionProvider.java +++ b/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/dimension/RoleDimensionProvider.java @@ -15,6 +15,7 @@ import org.hswebframework.web.system.authorization.defaults.service.DefaultDimen import org.jetlinks.community.auth.entity.MenuEntity; import org.jetlinks.community.auth.entity.RoleEntity; import org.jetlinks.community.auth.enums.RoleState; +import org.jetlinks.community.auth.service.DefaultMenuService; import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.event.EventListener; import org.springframework.stereotype.Component; @@ -23,18 +24,24 @@ import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import java.util.Collection; +import java.util.Collections; +import java.util.Map; import java.util.Objects; @Component public class RoleDimensionProvider extends BaseDimensionProvider { + private final DefaultMenuService menuService; + private final ReactiveCache cache; public RoleDimensionProvider(ReactiveRepository repository, DefaultDimensionUserService dimensionUserService, ApplicationEventPublisher eventPublisher, + DefaultMenuService menuService, ReactiveCacheManager cacheManager) { super(repository, eventPublisher, dimensionUserService); + this.menuService = menuService; this.cache = cacheManager.getCache("role-dimension"); } @@ -45,7 +52,6 @@ public class RoleDimensionProvider extends BaseDimensionProvider { @Override protected Mono convertToDimension(RoleEntity entity) { - return Mono.just(entity.toDimension()); } diff --git a/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/dimension/UserAuthenticationEventPublisher.java b/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/dimension/UserAuthenticationEventPublisher.java new file mode 100644 index 00000000..7dcb866a --- /dev/null +++ b/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/dimension/UserAuthenticationEventPublisher.java @@ -0,0 +1,93 @@ +package org.jetlinks.community.auth.dimension; + +import lombok.extern.slf4j.Slf4j; +import org.hswebframework.web.authorization.ReactiveAuthenticationHolder; +import org.hswebframework.web.system.authorization.api.event.ClearUserAuthorizationCacheEvent; +import org.jetlinks.community.authorize.FastSerializableAuthentication; +import org.jetlinks.core.event.EventBus; +import org.jetlinks.core.trace.MonoTracer; +import org.jetlinks.core.utils.Reactors; +import org.jetlinks.community.topic.Topics; +import org.springframework.context.event.EventListener; +import reactor.core.Disposable; +import reactor.core.publisher.BufferOverflowStrategy; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.core.publisher.Sinks; + +import java.time.Duration; +import java.util.Collection; +import java.util.HashSet; + +@Slf4j +public class UserAuthenticationEventPublisher { + private final EventBus eventBus; + + private final Sinks.Many asyncPublish = Sinks + .many() + .multicast() + .directBestEffort(); + + private final Disposable disposable; + + public UserAuthenticationEventPublisher(EventBus eventBus) { + this.eventBus = eventBus; + + disposable = asyncPublish + .asFlux() + .bufferTimeout(32, Duration.ofSeconds(1), HashSet::new) + .onBackpressureBuffer( + 10240, + dropped -> log.warn("user authentication changed event dropped:{}", dropped.size()), + BufferOverflowStrategy.DROP_LATEST) + .concatMap(list -> publish0(list) + .as(MonoTracer.create("/user/authentication/changed-async")) + .onErrorResume(err -> { + log.warn("publish user authentication changed error", err); + return Mono.empty(); + })) + .subscribe(); + + } + + public void shutdown() { + disposable.dispose(); + } + + @EventListener + public void handleEvent(ClearUserAuthorizationCacheEvent event) { + if (event.isAll()) { + return; + } + event.async(publish(event.getUserId())); + } + + private Mono publish0(Collection userIdList) { + return Flux + .fromIterable(userIdList) + .flatMapDelayError( + userId -> ReactiveAuthenticationHolder + .get(userId) + .flatMap(auth -> eventBus + .publish(Topics + .Authentications + .userAuthenticationChanged(auth.getUser().getId()), + FastSerializableAuthentication.of(auth, true) + )) + .as(MonoTracer.create("/user/" + userId + "/authentication/changed")), + 4, 4) + .then(); + } + + private Mono publish(Collection userIdList) { + if (userIdList.size() == 1) { + return publish0(userIdList); + } + for (String userId : userIdList) { + asyncPublish.emitNext(userId, Reactors.emitFailureHandler()); + } + return Mono.empty(); + } + + +} diff --git a/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/entity/MenuBindEntity.java b/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/entity/MenuBindEntity.java index 4ac1cf40..5cd5964a 100644 --- a/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/entity/MenuBindEntity.java +++ b/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/entity/MenuBindEntity.java @@ -4,11 +4,14 @@ import io.swagger.v3.oas.annotations.media.Schema; import lombok.Getter; import lombok.Setter; import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; import org.hswebframework.ezorm.rdb.mapping.annotation.ColumnType; import org.hswebframework.ezorm.rdb.mapping.annotation.Comment; import org.hswebframework.ezorm.rdb.mapping.annotation.DefaultValue; import org.hswebframework.ezorm.rdb.mapping.annotation.JsonCodec; +import org.hswebframework.web.api.crud.entity.EntityFactoryHolder; import org.hswebframework.web.api.crud.entity.GenericEntity; +import org.hswebframework.web.bean.FastBeanCopier; import org.hswebframework.web.utils.DigestUtils; import org.springframework.util.ObjectUtils; @@ -89,11 +92,15 @@ public class MenuBindEntity extends GenericEntity { public void generateId() { generateTargetKey(); - setId(DigestUtils.md5Hex(String.join("|", targetKey, menuId))); + if (StringUtils.isNotEmpty(targetKey)){ + setId(DigestUtils.md5Hex(String.join("|", targetKey, menuId))); + } } public void generateTargetKey() { - setTargetKey(generateTargetKey(targetType, targetId)); + if (StringUtils.isNotEmpty(targetId) && StringUtils.isNotEmpty(targetType)) { + setTargetKey(generateTargetKey(targetType, targetId)); + } } public static String generateTargetKey(String dimensionType, String dimensionId) { @@ -113,11 +120,16 @@ public class MenuBindEntity extends GenericEntity { return this; } + public static MenuBindEntity create() { + return EntityFactoryHolder.newInstance(MenuBindEntity.class, MenuBindEntity::new); + } + public static MenuBindEntity of(MenuView view) { - MenuBindEntity entity = new MenuBindEntity(); + MenuBindEntity entity = FastBeanCopier.copy(view, create(), "id"); entity.setMenuId(view.getId()); entity.setOptions(view.getOptions()); + entity.setOwner(view.getOwner()); if (CollectionUtils.isNotEmpty(view.getButtons())) { //只保存已经授权的按钮 diff --git a/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/entity/MenuButtonInfo.java b/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/entity/MenuButtonInfo.java index dbba0a9a..c3f94a32 100644 --- a/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/entity/MenuButtonInfo.java +++ b/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/entity/MenuButtonInfo.java @@ -4,6 +4,8 @@ import io.swagger.v3.oas.annotations.media.Schema; import lombok.Getter; import lombok.Setter; import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.collections4.MapUtils; +import org.hswebframework.web.i18n.SingleI18nSupportEntity; import java.io.Serializable; import java.util.*; @@ -11,7 +13,7 @@ import java.util.function.BiPredicate; @Getter @Setter -public class MenuButtonInfo implements Serializable { +public class MenuButtonInfo implements SingleI18nSupportEntity, Serializable { private static final long serialVersionUID = 1L; @Schema(description = "按钮ID") @@ -29,6 +31,16 @@ public class MenuButtonInfo implements Serializable { @Schema(description = "其他配置") private Map options; + @Schema(description = "i18n配置") + private Map i18nMessages; + + public String getI18nName() { + if (MapUtils.isEmpty(i18nMessages)) { + return name; + } + return getI18nMessage("name", name); + } + public boolean hasPermission(BiPredicate> predicate) { if (CollectionUtils.isEmpty(permissions)) { return true; diff --git a/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/entity/MenuEntity.java b/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/entity/MenuEntity.java index f1da76f2..4c0e2725 100755 --- a/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/entity/MenuEntity.java +++ b/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/entity/MenuEntity.java @@ -4,6 +4,7 @@ import io.swagger.v3.oas.annotations.media.Schema; import lombok.Getter; import lombok.Setter; import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; import org.hibernate.validator.constraints.Length; import org.hswebframework.ezorm.rdb.mapping.annotation.ColumnType; import org.hswebframework.ezorm.rdb.mapping.annotation.Comment; @@ -13,6 +14,7 @@ import org.hswebframework.web.api.crud.entity.GenericTreeSortSupportEntity; 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 javax.persistence.Column; @@ -49,13 +51,13 @@ public class MenuEntity private String owner; @Schema(description = "名称") - @Column(length = 32, nullable = false) - @Length(max = 32, min = 1, groups = CreateGroup.class) + @Column(length = 64, nullable = false) + @Length(max = 64, min = 1, groups = CreateGroup.class) private String name; @Schema(description = "编码") - @Column(length = 32) - @Length(max = 32, groups = CreateGroup.class) + @Column(length = 64) + @Length(max = 64, groups = CreateGroup.class) private String code; @Schema(description = "所属应用") @@ -185,7 +187,9 @@ public class MenuEntity */ public MenuEntity ofApp(String appId, String owner) { - setId(null); + if (StringUtils.isBlank(getId())) { + setId(DigestUtils.md5Hex(appId + owner + code)); + } setParentId(null); setOwner(owner); return this; diff --git a/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/entity/OrganizationEntity.java b/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/entity/OrganizationEntity.java index a94f571b..50515016 100644 --- a/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/entity/OrganizationEntity.java +++ b/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/entity/OrganizationEntity.java @@ -14,12 +14,16 @@ import org.hswebframework.web.authorization.simple.SimpleDimension; import org.hswebframework.web.crud.annotation.EnableEntityEvent; import org.hswebframework.web.crud.generator.Generators; import org.hswebframework.web.validator.CreateGroup; +import org.jetlinks.community.PropertyConstants; +import org.jetlinks.community.auth.constants.AuthConstants; import org.jetlinks.community.auth.dimension.OrgDimensionType; +import org.jetlinks.core.things.ThingInfo; import javax.persistence.Column; import javax.persistence.Table; import javax.validation.constraints.Pattern; import java.sql.JDBCType; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -76,9 +80,31 @@ public class OrganizationEntity extends GenericTreeSortSupportEntity imp private List children; - public Dimension toDimension(boolean direct) { - Map options = new HashMap<>(); - options.put("direct", direct); + public Dimension toDimension(boolean direct,Map _options) { + Map options = new HashMap<>(_options); + options.put(AuthConstants.IS_DIRECT, direct); return SimpleDimension.of(getId(), getName(), OrgDimensionType.org, options); } + public Dimension toDimension(boolean direct) { + return toDimension(direct, Collections.emptyMap()); + } + + public Dimension toParentDimension(){ + return toParentDimension(Collections.emptyMap()); + } + + public Dimension toParentDimension(Map _options) { + Map options = new HashMap<>(_options); + return SimpleDimension.of(getId(), getName(), OrgDimensionType.parentOrg, options); + } + + public ThingInfo toThingInfo() { + return ThingInfo + .builder() + .name(name) + .id(getId()) + .configuration(properties) + .build() + .addConfig(PropertyConstants.creatorId, getCreatorId()); + } } diff --git a/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/entity/OrganizationInfo.java b/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/entity/OrganizationInfo.java index 74fed5ab..bfcb4410 100644 --- a/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/entity/OrganizationInfo.java +++ b/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/entity/OrganizationInfo.java @@ -3,6 +3,8 @@ package org.jetlinks.community.auth.entity; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Getter; import lombok.Setter; +import org.apache.commons.lang3.StringUtils; +import org.hswebframework.web.api.crud.entity.EntityFactoryHolder; import org.hswebframework.web.authorization.Dimension; import org.jetlinks.reactor.ql.utils.CastUtils; @@ -25,8 +27,22 @@ public class OrganizationInfo { @Schema(description = "序号") private long sortIndex; - public static OrganizationInfo of(Dimension dimension) { - OrganizationInfo info = new OrganizationInfo(); + @Schema(description = "组织机构完整名称") + private String fullName; + + public static final String INTERVAL_CHARACTER = "/"; + + public static OrganizationInfo of() { + return EntityFactoryHolder.newInstance(OrganizationInfo.class, OrganizationInfo::new); + } + + public static OrganizationInfo from(OrganizationEntity entity) { + return entity.copyTo(of()); + } + + + public OrganizationInfo with(Dimension dimension) { + OrganizationInfo info = this; info.setId(dimension.getId()); info.setName(dimension.getName()); @@ -40,7 +56,28 @@ public class OrganizationInfo { dimension.getOption("sortIndex") .map(sortIndex -> CastUtils.castNumber(sortIndex).longValue()) .ifPresent(info::setSortIndex); - return info; } + + /** + * 添加上一级和本级名称,以/分隔 + * + * @param parentName 父级名称 + */ + public void addParentFullName(String parentName) { + String fullName = this.fullName == null ? this.getName() : this.fullName; + if (StringUtils.isNotBlank(parentName)) { + this.setFullName(parentName + INTERVAL_CHARACTER + fullName); + } else { + this.setFullName(fullName); + } + } + + public String getFullName() { + return fullName == null ? name : fullName; + } + + public static OrganizationInfo of(Dimension dimension) { + return of().with(dimension); + } } diff --git a/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/entity/PermissionInfo.java b/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/entity/PermissionInfo.java index bd90faf6..0e4fbdd8 100644 --- a/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/entity/PermissionInfo.java +++ b/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/entity/PermissionInfo.java @@ -1,7 +1,6 @@ package org.jetlinks.community.auth.entity; import io.swagger.v3.oas.annotations.media.Schema; -import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; @@ -11,7 +10,6 @@ import java.util.Set; @Getter @Setter -@AllArgsConstructor(staticName = "of") @NoArgsConstructor public class PermissionInfo implements Serializable { private static final long serialVersionUID = 1L; @@ -19,7 +17,17 @@ public class PermissionInfo implements Serializable { @Schema(description = "权限ID") private String permission; + @Schema(description = "权限名称") + private String name; + @Schema(description = "权限操作") private Set actions; + public static PermissionInfo of(String permission, Set actions) { + PermissionInfo permissionInfo = new PermissionInfo(); + permissionInfo.setPermission(permission); + permissionInfo.setActions(actions); + return permissionInfo; + } + } diff --git a/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/entity/RoleDetailInfo.java b/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/entity/RoleDetailInfo.java new file mode 100644 index 00000000..886a40d0 --- /dev/null +++ b/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/entity/RoleDetailInfo.java @@ -0,0 +1,45 @@ +package org.jetlinks.community.auth.entity; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; + +/** + * @author: tangchao + * @since: 2.2 + */ +@Getter +@Setter +public class RoleDetailInfo { + + @Schema(description = "角色id") + private String id; + + @Schema(description = "名称") + private String name; + + @Schema(description = "说明") + private String description; + + @Schema(description = "所属分组") + private String groupId; + + @Schema(description = "创建者ID(只读)") + private String creatorId; + + @Schema(description = "创建时间") + private Long createTime; + + @Schema(description = "修改人ID", accessMode = Schema.AccessMode.READ_ONLY) + private String modifierId; + + @Schema(description = "修改时间", accessMode = Schema.AccessMode.READ_ONLY) + private Long modifyTime; + + @Schema(description = "成员数") + private Integer memberSelfCount = 0; + + public static RoleDetailInfo from(RoleEntity role) { + return role.copyTo(new RoleDetailInfo()); + } +} diff --git a/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/entity/RoleEntity.java b/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/entity/RoleEntity.java index 3c193115..f1eb8c9b 100644 --- a/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/entity/RoleEntity.java +++ b/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/entity/RoleEntity.java @@ -10,9 +10,11 @@ import org.hswebframework.ezorm.rdb.mapping.annotation.DefaultValue; import org.hswebframework.ezorm.rdb.mapping.annotation.EnumCodec; import org.hswebframework.web.api.crud.entity.GenericEntity; import org.hswebframework.web.api.crud.entity.RecordCreationEntity; +import org.hswebframework.web.api.crud.entity.RecordModifierEntity; import org.hswebframework.web.authorization.DefaultDimensionType; import org.hswebframework.web.authorization.Dimension; import org.hswebframework.web.authorization.simple.SimpleDimension; +import org.hswebframework.web.crud.annotation.EnableEntityEvent; import org.hswebframework.web.crud.generator.Generators; import org.jetlinks.community.auth.enums.RoleState; import org.jetlinks.community.auth.service.RoleGroupService; @@ -24,7 +26,8 @@ import javax.persistence.Table; @Setter @Table(name = "s_role") @Comment("角色信息表") -public class RoleEntity extends GenericEntity implements RecordCreationEntity { +@EnableEntityEvent +public class RoleEntity extends GenericEntity implements RecordCreationEntity, RecordModifierEntity { @Column(length = 64) @Length(min = 1, max = 64) @@ -62,6 +65,15 @@ public class RoleEntity extends GenericEntity implements RecordCreationE ) private Long createTime; + @Column(length = 64) + @Schema(description = "修改人ID", accessMode = Schema.AccessMode.READ_ONLY) + private String modifierId; + + @Column(length = 64) + @DefaultValue(generator = Generators.CURRENT_TIME) + @Schema(description = "修改时间", accessMode = Schema.AccessMode.READ_ONLY) + private Long modifyTime; + public Dimension toDimension() { SimpleDimension dimension = new SimpleDimension(); dimension.setId(getId()); diff --git a/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/entity/RoleGroupEntity.java b/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/entity/RoleGroupEntity.java index 76b59e33..768b32bc 100644 --- a/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/entity/RoleGroupEntity.java +++ b/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/entity/RoleGroupEntity.java @@ -4,23 +4,29 @@ import io.swagger.v3.oas.annotations.media.Schema; import lombok.Getter; import lombok.Setter; import org.hibernate.validator.constraints.Length; +import org.hswebframework.ezorm.rdb.mapping.annotation.ColumnType; import org.hswebframework.ezorm.rdb.mapping.annotation.Comment; import org.hswebframework.ezorm.rdb.mapping.annotation.DefaultValue; +import org.hswebframework.ezorm.rdb.mapping.annotation.JsonCodec; import org.hswebframework.web.api.crud.entity.GenericTreeSortSupportEntity; 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.i18n.MultipleI18nSupportEntity; import javax.persistence.Column; import javax.persistence.Table; +import java.sql.JDBCType; import java.util.List; +import java.util.Locale; +import java.util.Map; @Getter @Setter @Table(name = "s_role_group") @Comment("角色分组表") @EnableEntityEvent -public class RoleGroupEntity extends GenericTreeSortSupportEntity implements RecordCreationEntity { +public class RoleGroupEntity extends GenericTreeSortSupportEntity implements RecordCreationEntity, MultipleI18nSupportEntity { @Column(length = 64) @Length(min = 1, max = 64) @@ -46,5 +52,21 @@ public class RoleGroupEntity extends GenericTreeSortSupportEntity implem ) private Long createTime; + @Schema(title = "国际化信息定义") + @Column + @JsonCodec + @ColumnType(jdbcType = JDBCType.LONGVARCHAR, javaType = String.class) + private Map> i18nMessages; + + private List children; + + public String getI18nName() { + return getI18nMessage("name", name); + } + + public String getI18nName(Locale locale) { + return getI18nMessage("name", locale, name); + } + } diff --git a/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/entity/RoleInfo.java b/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/entity/RoleInfo.java index d3a7cd28..b85ec17f 100644 --- a/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/entity/RoleInfo.java +++ b/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/entity/RoleInfo.java @@ -2,6 +2,7 @@ package org.jetlinks.community.auth.entity; import lombok.Getter; import lombok.Setter; +import org.hswebframework.web.api.crud.entity.EntityFactoryHolder; import org.hswebframework.web.authorization.Dimension; @Getter @@ -11,10 +12,22 @@ public class RoleInfo { private String id; private String name; + public static RoleInfo of() { + return EntityFactoryHolder.newInstance(RoleInfo.class, RoleInfo::new); + } + + public RoleInfo with(Dimension dimension) { + RoleInfo info = this; + info.setId(dimension.getId()); + info.setName(dimension.getName()); + return info; + } + public static RoleInfo of(Dimension dimension) { - RoleInfo detail = new RoleInfo(); - detail.setId(dimension.getId()); - detail.setName(dimension.getName()); - return detail; + return of().with(dimension); + } + + public static RoleInfo of(RoleEntity role) { + return role.copyTo(of()); } } diff --git a/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/entity/ThirdPartyUserBindEntity.java b/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/entity/ThirdPartyUserBindEntity.java index a006b0cb..b6acb977 100755 --- a/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/entity/ThirdPartyUserBindEntity.java +++ b/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/entity/ThirdPartyUserBindEntity.java @@ -51,7 +51,7 @@ public class ThirdPartyUserBindEntity extends GenericEntity { private String providerName; @Schema(description = "第三方用户ID") - @Column(nullable = false, length = 64, updatable = false) + @Column(nullable = false, length = 64) @NotBlank(groups = CreateGroup.class) private String thirdPartyUserId; @@ -61,7 +61,7 @@ public class ThirdPartyUserBindEntity extends GenericEntity { private String userId; @Schema(description = "绑定时间") - @Column(nullable = false, updatable = false) + @Column(nullable = false) @DefaultValue(generator = Generators.CURRENT_TIME) private Long bindTime; @@ -76,7 +76,7 @@ public class ThirdPartyUserBindEntity extends GenericEntity { private Map others; public void generateId() { - setId(generateId(type, provider, thirdPartyUserId)); + setId(generateId(type, provider, userId)); } public static String generateId(String... arr) { diff --git a/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/entity/UserDetail.java b/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/entity/UserDetail.java index 6932a651..1680d48e 100644 --- a/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/entity/UserDetail.java +++ b/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/entity/UserDetail.java @@ -1,24 +1,32 @@ package org.jetlinks.community.auth.entity; import com.fasterxml.jackson.annotation.JsonIgnore; +import com.google.common.collect.Lists; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; +import org.apache.commons.lang3.StringUtils; +import org.hswebframework.web.api.crud.entity.EntityFactoryHolder; import org.hswebframework.web.authorization.Authentication; import org.hswebframework.web.authorization.DefaultDimensionType; import org.hswebframework.web.authorization.Dimension; import org.hswebframework.web.bean.FastBeanCopier; import org.hswebframework.web.system.authorization.api.entity.UserEntity; import org.jetlinks.community.auth.dimension.OrgDimensionType; -import org.jetlinks.community.auth.enums.DefaultUserEntityType; -import org.jetlinks.community.auth.enums.UserEntityType; -import org.jetlinks.community.auth.enums.UserEntityTypes; +import org.jetlinks.community.auth.enums.*; +import org.jetlinks.community.auth.service.info.UserLoginInfo; import org.jetlinks.reactor.ql.utils.CastUtils; import java.util.List; import java.util.stream.Collectors; +/** + * 用户信息详情 + * + * @author zhouhao + * @since 1.0 + */ @Getter @Setter @NoArgsConstructor @@ -34,14 +42,20 @@ public class UserDetail { private String password; @Schema(hidden = true) - private UserEntityType type; - - @Schema(description = "用户类型ID") private String typeId; + @Schema(hidden = true) + private UserEntityType type; + @Schema(description = "用户状态。1启用,0禁用") private Byte status; + @Schema(description = "是否授权") + private Boolean loggedIn; + + @Schema(description = "最后一次请求时间") + private Long lastRequestTime; + @Schema(description = "姓名") private String name; @@ -58,7 +72,7 @@ public class UserDetail { private String description; @Schema(description = "创建时间") - private long createTime; + private Long createTime; @Schema(description = "角色信息") private List roleList; @@ -69,21 +83,57 @@ public class UserDetail { @Schema(description = "创建者ID") private String creatorId; - private boolean tenantDisabled; + @Schema(description = "创建人名称") + private String creatorName; + + @Schema(description = "修改人ID") + private String modifierId; + + @Schema(description = "修改人名称") + private String modifierName; + + @Schema(description = "修改时间") + private Long modifyTime; + + @Schema(description = "性别") + private GenderEnum gender; + + @Schema(description = "生日") + private Long birthday; + + @Schema(description = "真实姓名") + private String realName; + + @Schema(description = "居民身份证号") + private String idNumber; + + @Schema(description = "公司") + private String company; + + @Schema(description = "注册方式") + private RegisterEnum register; + + @Schema(description = "登录信息") + private UserLoginInfo loginInfo; + + @Schema(description = "关联时间") + private Long relationTime; + + + public static UserDetail of() { + return EntityFactoryHolder.newInstance(UserDetail.class, UserDetail::new); + } public static UserDetail of(UserEntity entity) { - return new UserDetail().with(entity); + return of().with(entity); } public UserDetail with(UserDetailEntity entity) { - this.setAvatar(entity.getAvatar()); - this.setDescription(entity.getDescription()); - this.setTelephone(entity.getTelephone()); - this.setEmail(entity.getEmail()); - + FastBeanCopier.copy(entity, this); return this; } + public UserDetail with(UserEntity entity) { this.setId(entity.getId()); this.setName(entity.getName()); @@ -105,12 +155,14 @@ public class UserDetail { } public UserDetail withDimension(List details) { + //角色 roleList = details .stream() .filter(dim -> DefaultDimensionType.role.isSameType(dim.getType())) .map(RoleInfo::of) .collect(Collectors.toList()); + //组织 orgList = details .stream() .filter(dim -> OrgDimensionType.org.isSameType(dim.getType())) @@ -124,20 +176,37 @@ public class UserDetail { return this; } - public UserDetail withType() { - this.setType(UserEntityTypes.getType(this.getTypeId())); + public UserDetail withLastRequestTime(Long lastRequestTime) { + if (lastRequestTime == null || lastRequestTime == 0) { + this.setLoggedIn(false); + } else { + this.setLoggedIn(true); + this.setLastRequestTime(lastRequestTime); + } + return this; + } + + public UserDetail withUserLoginInfo(UserLoginInfo info) { + this.setLoginInfo(info); return this; } public UserEntity toUserEntity() { - UserEntity userEntity = new UserEntity(); - userEntity.setId(id); + UserEntity userEntity = EntityFactoryHolder.newInstance(UserEntity.class, UserEntity::new); + if (StringUtils.isNotBlank(id)) { + userEntity.setId(id); + } userEntity.setName(name); userEntity.setUsername(username); userEntity.setPassword(password); - // 默认设置类型为普通用户 - if (type == null && !username.equals("admin")) { - userEntity.setType(DefaultUserEntityType.USER.getId()); + userEntity.setStatus(status); + if (type == null) { + if (!username.equals("admin")) { + // 默认设置类型为普通用户 + userEntity.setType(DefaultUserEntityType.USER.getId()); + } + } else { + userEntity.setType(type.getId()); } return userEntity; } @@ -148,18 +217,11 @@ public class UserDetail { @JsonIgnore public List getOrgIdList() { - return orgList == null ? null : orgList - .stream() - .map(OrganizationInfo::getId) - .collect(Collectors.toList()); + return orgList == null ? null : Lists.transform(orgList, OrganizationInfo::getId); } @JsonIgnore public List getRoleIdList() { - return roleList == null ? null : roleList - .stream() - .map(RoleInfo::getId) - .collect(Collectors.toList()); + return roleList == null ? null : Lists.transform(roleList, RoleInfo::getId); } - } diff --git a/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/entity/UserDetailEntity.java b/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/entity/UserDetailEntity.java index faefe085..92b9cebc 100644 --- a/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/entity/UserDetailEntity.java +++ b/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/entity/UserDetailEntity.java @@ -1,35 +1,137 @@ package org.jetlinks.community.auth.entity; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Getter; import lombok.Setter; import org.hibernate.validator.constraints.URL; +import org.hswebframework.ezorm.rdb.mapping.annotation.*; +import org.hswebframework.web.api.crud.entity.EntityFactoryHolder; import org.hswebframework.web.api.crud.entity.GenericEntity; +import org.hswebframework.web.api.crud.entity.RecordCreationEntity; +import org.hswebframework.web.api.crud.entity.RecordModifierEntity; +import org.hswebframework.web.crud.annotation.EnableEntityEvent; +import org.hswebframework.web.crud.generator.Generators; +import org.hswebframework.web.validator.CreateGroup; +import org.jetlinks.core.things.ThingInfo; +import org.jetlinks.community.auth.enums.GenderEnum; +import org.jetlinks.community.auth.enums.RegisterEnum; +import org.jetlinks.community.relation.RelationConstants; import javax.persistence.Column; import javax.persistence.Table; import javax.validation.constraints.Email; import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Pattern; +/** + * 用户信息 + * + * @author zhouhao + * @since 1.0 + */ @Table(name = "s_user_detail") +@Comment("用户详情信息表") @Getter @Setter -public class UserDetailEntity extends GenericEntity { +@EnableEntityEvent +public class UserDetailEntity extends GenericEntity implements RecordCreationEntity, RecordModifierEntity { + @Schema(description = "姓名") @Column(nullable = false) - @NotBlank(message = "姓名不能为空") + @NotBlank(message = "姓名不能为空", groups = {CreateGroup.class}) private String name; + @Schema(description = "邮箱") @Column @Email(message = "邮件格式错误") private String email; + @Schema(description = "电话") @Column(length = 32) private String telephone; + @Schema(description = "头像图片地址") @Column(length = 2000) @URL(message = "头像格式错误") private String avatar; + @Schema(description = "说明") @Column(length = 2000) private String description; + + @Schema(description = "创建人ID") + @Column(length = 64, updatable = false) + private String creatorId; + + @Schema(description = "创建人名称") + @Column + @Upsert(insertOnly = true) + private String creatorName; + + @Schema(description = "创建时间") + @DefaultValue(generator = Generators.CURRENT_TIME) + @Column(updatable = false) + private Long createTime; + + @Column(length = 64) + @Schema(description = "修改人ID") + private String modifierId; + + @Column + @Schema(description = "修改人名称") + private String modifierName; + + @Column + @DefaultValue(generator = Generators.CURRENT_TIME) + @Schema(description = "修改时间") + private Long modifyTime; + + @Column(length = 32) + @ColumnType(javaType = String.class) + @EnumCodec + @DefaultValue("unknown") + @Schema(description = "性别") + private GenderEnum gender; + + @Column + @Schema(description = "生日") + private Long birthday; + + @Column + @Schema(description = "真实姓名") + private String realName; + + @Column + @Schema(description = "居民身份证号") + @Pattern(regexp = "(^[1-9]\\d{7}(0[1-9]|1[0-2])([0-2][1-9]|[1-2]0|31)\\d{3}$)|(^[1-9]\\d{5}[1-9]\\d{3}(0[1-9]|1[0-2])([0-2][1-9]|[1-2]0|31)(\\d{4}|\\d{3}[Xx])$)", + message = "请输入正确的身份证号", groups = CreateGroup.class) + private String idNumber; + + @Column(length = 32) + @ColumnType(javaType = String.class) + @EnumCodec + @DefaultValue("backstage") + @Schema(description = "注册方式") + private RegisterEnum register; + + @Column + @Schema(description = "公司") + private String company; + + + public static UserDetailEntity of() { + return EntityFactoryHolder.newInstance(UserDetailEntity.class, UserDetailEntity::new); + } + + public ThingInfo toThingInfo() { + return ThingInfo + .builder() + .id(getId()) + .name(getName()) + .build() + .addConfig(RelationConstants.UserProperty.email, getEmail()) + .addConfig(RelationConstants.UserProperty.telephone, getTelephone()) + .addConfig("avatar", getAvatar()) + ; + } } diff --git a/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/enums/DefaultUserEntityType.java b/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/enums/DefaultUserEntityType.java index a90e1439..9c746ed6 100644 --- a/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/enums/DefaultUserEntityType.java +++ b/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/enums/DefaultUserEntityType.java @@ -2,7 +2,8 @@ package org.jetlinks.community.auth.enums; import lombok.AllArgsConstructor; import lombok.Getter; -import org.hswebframework.web.dict.EnumDict; +import org.hswebframework.web.dict.I18nEnumDict; +import org.hswebframework.web.i18n.LocaleUtils; import java.util.Arrays; import java.util.HashMap; @@ -16,10 +17,11 @@ import java.util.Optional; */ @Getter @AllArgsConstructor -public enum DefaultUserEntityType implements UserEntityType, EnumDict { +public enum DefaultUserEntityType implements UserEntityType, I18nEnumDict { ADMIN("admin", "超级管理员"), USER("user", "普通用户"), + APPLICATION("application", "第三方用户"), OTHER("other", "其他"); private final String id; @@ -48,7 +50,7 @@ public enum DefaultUserEntityType implements UserEntityType, EnumDict { if (isWriteJSONObjectEnabled()) { Map jsonObject = new HashMap<>(); jsonObject.put("id", getId()); - jsonObject.put("name", getName()); + jsonObject.put("name", getI18nMessage(LocaleUtils.current())); return jsonObject; } return name(); diff --git a/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/enums/GenderEnum.java b/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/enums/GenderEnum.java new file mode 100644 index 00000000..27b5ae85 --- /dev/null +++ b/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/enums/GenderEnum.java @@ -0,0 +1,25 @@ +package org.jetlinks.community.auth.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.hswebframework.web.dict.EnumDict; + +/** + * @author: tangchao + * @since: 2.2 + */ +@Getter +@AllArgsConstructor +public enum GenderEnum implements EnumDict { + man("男"), + unknown("未知"), + female("女"); + + private final String text; + + + @Override + public String getValue() { + return name(); + } +} diff --git a/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/enums/RegisterEnum.java b/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/enums/RegisterEnum.java new file mode 100644 index 00000000..99a57106 --- /dev/null +++ b/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/enums/RegisterEnum.java @@ -0,0 +1,24 @@ +package org.jetlinks.community.auth.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.hswebframework.web.dict.EnumDict; + +/** + * @author hsy 2024/12/11 + * @since 2.3 + */ +@Getter +@AllArgsConstructor +public enum RegisterEnum implements EnumDict { + myself("自主注册"), + backstage("后台注册"); + + private final String text; + + + @Override + public String getValue() { + return name(); + } +} diff --git a/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/initialize/MenuAuthenticationInitializeService.java b/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/initialize/MenuAuthenticationInitializeService.java index 1f3c718d..4ac443fa 100644 --- a/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/initialize/MenuAuthenticationInitializeService.java +++ b/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/initialize/MenuAuthenticationInitializeService.java @@ -1,6 +1,6 @@ package org.jetlinks.community.auth.initialize; -import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.apache.commons.collections4.CollectionUtils; import org.hswebframework.web.api.crud.entity.QueryParamEntity; import org.hswebframework.web.authorization.DefaultDimensionType; @@ -10,7 +10,6 @@ import org.hswebframework.web.authorization.simple.SimpleAuthentication; import org.hswebframework.web.authorization.simple.SimplePermission; import org.hswebframework.web.system.authorization.api.entity.ActionEntity; import org.hswebframework.web.system.authorization.api.entity.PermissionEntity; -import org.hswebframework.web.system.authorization.defaults.service.DefaultPermissionService; import org.jetlinks.community.auth.entity.MenuEntity; import org.jetlinks.community.auth.entity.MenuView; import org.jetlinks.community.auth.service.DefaultMenuService; @@ -21,16 +20,24 @@ import org.springframework.stereotype.Component; import reactor.core.publisher.Mono; import java.util.*; -import java.util.function.Function; import java.util.stream.Collectors; -@AllArgsConstructor @Component +@Slf4j public class MenuAuthenticationInitializeService { private final DefaultMenuService menuService; - private final DefaultPermissionService permissionService; + private final PermissionCacheHelper permissionCacheHelper; + + private final Mono> menuCaching; + + public MenuAuthenticationInitializeService(DefaultMenuService menuService, PermissionCacheHelper permissionCacheHelper) { + this.menuService = menuService; + this.permissionCacheHelper = permissionCacheHelper; + this.menuCaching = Mono + .defer(this.menuService::getEnabledMenus); + } /** * 根据角色配置的菜单权限来重构权限信息 @@ -46,11 +53,8 @@ public class MenuAuthenticationInitializeService { Mono .zip( // T1: 权限定义列表 - permissionService - .createQuery() - .where(PermissionEntity::getStatus, 1) - .fetch() - .collectMap(PermissionEntity::getId, Function.identity()), + permissionCacheHelper + .getPermissionCaching(), // T2: 菜单定义列表 menuService .createQuery() diff --git a/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/initialize/PermissionCacheHelper.java b/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/initialize/PermissionCacheHelper.java new file mode 100644 index 00000000..588ef716 --- /dev/null +++ b/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/initialize/PermissionCacheHelper.java @@ -0,0 +1,89 @@ +package org.jetlinks.community.auth.initialize; + +import lombok.Getter; +import org.apache.commons.collections4.CollectionUtils; +import org.hswebframework.web.authorization.Permission; +import org.hswebframework.web.authorization.simple.SimplePermission; +import org.hswebframework.web.system.authorization.api.entity.ActionEntity; +import org.hswebframework.web.system.authorization.api.entity.PermissionEntity; +import org.hswebframework.web.system.authorization.defaults.service.DefaultPermissionService; +import org.springframework.stereotype.Component; +import reactor.core.publisher.Mono; + +import java.time.Duration; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * @author gyl + * @since 2.2 + */ +@Component +public class PermissionCacheHelper { + + @Getter + private final Mono> permissionCaching; + + public PermissionCacheHelper(DefaultPermissionService permissionService) { + this.permissionCaching = + Mono.defer(() -> permissionService + .createQuery() + .where(PermissionEntity::getStatus, 1) + .fetch() + .collectMap(PermissionEntity::getId, Function.identity(), LinkedHashMap::new)) + // FIXME: 2023/11/14 PermissionEntity不会频繁变更 + .cache(Duration.ofSeconds(1)); + + } + + + /** + * 根据权限实体id过滤对应操作 + * + * @param permissionId 权限id + * @param perActions 目标操作 + * @return Permission(操作为实体操作与目标操作的交集) + */ + public Mono filterByDefaultPermission(String permissionId, Set perActions) { + return permissionCaching + .mapNotNull(map -> filterByDefaultPermission(map.get(permissionId), perActions)); + } + + /** + * 根据权限实体过滤对应操作 + * + * @param entity 权限实体 + * @param perActions 目标操作 + * @return Permission(操作为实体操作与目标操作的交集) + */ + public static Permission filterByDefaultPermission(PermissionEntity entity, Set perActions) { + if (entity == null || CollectionUtils.isEmpty(perActions)) { + return null; + } + + Set actions; + if (CollectionUtils.isEmpty(entity.getActions())) { + actions = new HashSet<>(); + } else { + Set defActions = entity + .getActions() + .stream() + .map(ActionEntity::getAction) + .collect(Collectors.toSet()); + actions = new HashSet<>(perActions); + actions.retainAll(defActions); + } + + return SimplePermission + .builder() + .id(entity.getId()) + .name(entity.getName()) + .options(entity.getProperties()) + .actions(actions) + .build(); + } +} diff --git a/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/login/UserLoginLogicInterceptor.java b/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/login/UserLoginLogicInterceptor.java index 1618a97e..ffb6478d 100644 --- a/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/login/UserLoginLogicInterceptor.java +++ b/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/login/UserLoginLogicInterceptor.java @@ -3,7 +3,6 @@ package org.jetlinks.community.auth.login; import io.swagger.v3.oas.annotations.Hidden; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; -import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.hswebframework.web.authorization.annotation.Authorize; import org.hswebframework.web.authorization.events.AbstractAuthorizationEvent; @@ -11,13 +10,10 @@ import org.hswebframework.web.authorization.events.AuthorizationDecodeEvent; import org.hswebframework.web.authorization.events.AuthorizationFailedEvent; import org.hswebframework.web.authorization.exception.AccessDenyException; import org.hswebframework.web.authorization.exception.AuthenticationException; -import org.hswebframework.web.exception.ValidationException; -import org.hswebframework.web.id.IDGenerator; -import org.hswebframework.web.id.RandomIdGenerator; import org.hswebframework.web.logging.RequestInfo; import org.hswebframework.web.utils.DigestUtils; -import org.jetlinks.core.utils.Reactors; -import org.jetlinks.community.utils.CryptoUtils; +import org.jetlinks.community.auth.cipher.CipherConfig; +import org.jetlinks.community.auth.cipher.CipherHelper; import org.jetlinks.reactor.ql.utils.CastUtils; import org.springframework.context.event.EventListener; import org.springframework.data.redis.core.ReactiveRedisOperations; @@ -27,8 +23,10 @@ import org.springframework.web.bind.annotation.RestController; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; -import java.security.KeyPair; -import java.util.*; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @RestController @@ -36,14 +34,19 @@ import java.util.concurrent.ConcurrentHashMap; @RequestMapping @Tag(name = "登录配置接口") @Hidden -@AllArgsConstructor @Slf4j public class UserLoginLogicInterceptor { private final UserLoginProperties properties; - + private final CipherHelper cipherHelper; private final ReactiveRedisOperations redis; + public UserLoginLogicInterceptor(UserLoginProperties properties, ReactiveRedisOperations redis) { + this.properties = properties; + this.redis = redis; + this.cipherHelper = new CipherHelper(redis, properties.getEncrypt()); + } + @GetMapping("/authorize/login/configs") @Operation(summary = "获取登录所需配置信息") public Mono> getConfigs() { @@ -66,23 +69,11 @@ public class UserLoginLogicInterceptor { @GetMapping("/authorize/encrypt-key") @Operation(summary = "获取登录加密key") - public Mono> getEncryptKey() { + public Mono getEncryptKey() { if (!properties.getEncrypt().isEnabled()) { return Mono.empty(); } - String id = IDGenerator.RANDOM.generate(); - KeyPair rasKey = CryptoUtils.generateRSAKey(); - String pubKeyBase64 = Base64.getEncoder().encodeToString(rasKey.getPublic().getEncoded()); - String priKeyBase64 = Base64.getEncoder().encodeToString(rasKey.getPrivate().getEncoded()); - - Map value = new LinkedHashMap<>(); - value.put("enabled", true); - value.put("publicKey", pubKeyBase64); - value.put("id", id); - return redis - .opsForValue() - .set(createEncRedisKey(id), priKeyBase64, properties.getEncrypt().getKeyTtl()) - .thenReturn(value); + return cipherHelper.getConfig(); } @EventListener @@ -96,43 +87,18 @@ public class UserLoginLogicInterceptor { } } - protected boolean isLegalEncryptId(String id) { - return RandomIdGenerator.timestampRangeOf(id, properties.getEncrypt().getKeyTtl()); - } Mono doDecrypt(AuthorizationDecodeEvent event) { String encId = event .getParameter("encryptId") .map(String::valueOf) - .filter(this::isLegalEncryptId) //统一返回密码错误 - .orElseThrow(() -> new AuthenticationException(AuthenticationException.ILLEGAL_PASSWORD)); - String redisKey = createEncRedisKey(encId); - return redis - .opsForValue() - .get(redisKey) - .map(privateKey -> { - event.setPassword( - new String( - CryptoUtils.decryptRSA(Base64.getDecoder().decode(event.getPassword()), - CryptoUtils.decodeRSAPrivateKey(privateKey)) - ) - ); - return true; - }) - .onErrorResume(err -> { - log.warn("decrypt password error", err); - return Reactors.ALWAYS_FALSE; - }) - .defaultIfEmpty(false) - .flatMap(ignore -> redis.opsForValue().delete(redisKey).thenReturn(ignore)) - .doOnSuccess(success -> { - if (!success) { - throw new AuthenticationException(AuthenticationException.ILLEGAL_PASSWORD); - } - }) - .then(); + .orElseThrow(() -> new AuthenticationException.NoStackTrace(AuthenticationException.ILLEGAL_PASSWORD)); + return cipherHelper + .decrypt(encId, event.getPassword()) + .doOnNext(event::setPassword) + .then(); } @EventListener @@ -189,9 +155,4 @@ public class UserLoginLogicInterceptor { .then(); } - - private static String createEncRedisKey(String encryptId) { - return "login:encrypt-key:" + encryptId; - } - } diff --git a/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/login/UserLoginProperties.java b/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/login/UserLoginProperties.java index d200ac94..3d304645 100644 --- a/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/login/UserLoginProperties.java +++ b/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/login/UserLoginProperties.java @@ -2,6 +2,7 @@ package org.jetlinks.community.auth.login; import lombok.Getter; import lombok.Setter; +import org.jetlinks.community.auth.cipher.CipherProperties; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; @@ -14,20 +15,12 @@ import java.time.Duration; public class UserLoginProperties { //加密相关配置 - private EncryptionLogic encrypt = new EncryptionLogic(); + private CipherProperties encrypt = new CipherProperties(); //登录失败限制相关配置 private BlockLogic block = new BlockLogic(); - @Getter - @Setter - public static class EncryptionLogic { - private boolean enabled; - - private Duration keyTtl = Duration.ofMinutes(5); - } - @Getter @Setter public static class BlockLogic { diff --git a/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/relation/UserRelationObjectProvider.java b/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/relation/UserRelationObjectProvider.java index 6d3e9fa1..7aec873b 100644 --- a/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/relation/UserRelationObjectProvider.java +++ b/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/relation/UserRelationObjectProvider.java @@ -2,8 +2,6 @@ package org.jetlinks.community.auth.relation; import lombok.AllArgsConstructor; import org.hswebframework.ezorm.rdb.mapping.ReactiveRepository; -import org.jetlinks.core.things.relation.ObjectType; -import org.jetlinks.core.things.relation.PropertyOperation; import org.jetlinks.community.auth.entity.ThirdPartyUserBindEntity; import org.jetlinks.community.auth.entity.UserDetail; import org.jetlinks.community.auth.service.UserDetailService; @@ -11,6 +9,8 @@ import org.jetlinks.community.relation.RelationConstants; import org.jetlinks.community.relation.RelationObjectProvider; import org.jetlinks.community.relation.impl.SimpleObjectType; import org.jetlinks.community.relation.impl.property.PropertyOperationStrategy; +import org.jetlinks.core.things.relation.ObjectType; +import org.jetlinks.core.things.relation.PropertyOperation; import org.springframework.stereotype.Component; import reactor.core.publisher.Mono; diff --git a/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/service/AuthorizationSettingDetailService.java b/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/service/AuthorizationSettingDetailService.java index ef3f6d40..2431c140 100755 --- a/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/service/AuthorizationSettingDetailService.java +++ b/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/service/AuthorizationSettingDetailService.java @@ -1,6 +1,7 @@ package org.jetlinks.community.auth.service; import lombok.AllArgsConstructor; +import org.apache.commons.codec.digest.DigestUtils; import org.apache.commons.collections4.CollectionUtils; import org.hswebframework.web.authorization.Authentication; import org.hswebframework.web.authorization.DimensionProvider; @@ -13,13 +14,16 @@ import org.jetlinks.community.auth.web.request.AuthorizationSettingDetail; import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.util.StringUtils; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.util.function.Tuple2; import reactor.util.function.Tuples; import javax.annotation.Nullable; -import java.util.List; +import java.util.*; import java.util.stream.Collectors; /** @@ -52,7 +56,7 @@ public class AuthorizationSettingDetailService { .flatMapMany(typeProviderMapping -> Mono .justOrEmpty(typeProviderMapping.get(type)) .flatMapMany(provider -> provider.getUserIdByDimensionId(id))) - .collectList() + .>collect(HashSet::new, Set::add) .flatMap(lst-> ClearUserAuthorizationCacheEvent.of(lst).publish(eventPublisher)) .then(); } @@ -67,17 +71,24 @@ public class AuthorizationSettingDetailService { @Transactional public Mono saveDetail(@Nullable Authentication currentAuthentication, Flux detailFlux) { + MultiValueMap delDimension=new LinkedMultiValueMap<>(); return detailFlux - //先删除旧的权限设置 - .flatMap(detail -> settingService - .getRepository() - .createDelete() - .where(AuthorizationSettingEntity::getDimensionType, detail.getTargetType()) - .and(AuthorizationSettingEntity::getDimensionTarget, detail.getTargetId()) - .execute() - .thenReturn(detail)) - .flatMap(detail -> addDetail(currentAuthentication, detailFlux)) - .then(); + .doOnNext(detail->delDimension.add(detail.getTargetType(),detail.getTargetId())) + .then(Mono.defer(()->{ + //先删除旧的权限设置 + return Flux + .fromIterable(delDimension.entrySet()) + .filter(entry->CollectionUtils.isNotEmpty(entry.getValue())) + .flatMap(entry-> settingService + .getRepository() + .createDelete() + .where(AuthorizationSettingEntity::getDimensionType, entry.getKey()) + .in(AuthorizationSettingEntity::getDimensionTarget,entry.getValue()) + .execute()) + .then(); + + })) + .then(addDetail(currentAuthentication, detailFlux)); } @@ -101,8 +112,8 @@ public class AuthorizationSettingDetailService { .flatMap(type -> provider.getDimensionById(type, detail.getTargetId())) .flatMapIterable(detail::toEntity)) .switchIfEmpty(Flux.defer(() -> Flux.fromIterable(detail.toEntity()))) - .distinct(AuthorizationSettingEntity::getPermission) ) + .as(this::mergePermissionActionAndDataAccesses) .map(entity -> null == currentAuthentication ? entity : permissionProperties.getFilter().handleSetting(currentAuthentication, entity)) @@ -111,6 +122,47 @@ public class AuthorizationSettingDetailService { .then(); } + + public Flux mergePermissionActionAndDataAccesses(Flux entityFlux) { + return entityFlux + .doOnNext(AuthorizationSettingDetailService::generateId) + .groupBy(AuthorizationSettingEntity::getId) + .flatMap(entityGroup -> { + AuthorizationSettingEntity newOne = new AuthorizationSettingEntity(); + return entityGroup + .switchOnFirst((signal, entities) -> { + if (signal.hasValue() && signal.get() != null) { + signal.get().copyTo(newOne); + newOne.setDataAccesses(new ArrayList<>()); + } + return entities; + }) + //根据权重排序 + .sort(Comparator.comparingInt(AuthorizationSettingEntity::getPriority)) + .doOnNext(entity -> { + boolean merge = Boolean.TRUE.equals(entity.getMerge()); + newOne.setPriority(Math.max(newOne.getPriority(),entity.getPriority())); + if (!merge) { + newOne.getActions().clear(); + } + + if (CollectionUtils.isNotEmpty(entity.getDataAccesses())) { + newOne.getDataAccesses().addAll(entity.getDataAccesses()); + } + if (CollectionUtils.isNotEmpty(entity.getActions())) { + newOne.getActions().addAll(entity.getActions()); + } + }) + .then(Mono.just(newOne)); + }); + } + + public static void generateId(AuthorizationSettingEntity entity) { + if (StringUtils.isEmpty(entity.getId())) { + entity.setId(DigestUtils.md5Hex(entity.getPermission() + entity.getDimensionType() + entity.getDimensionTarget())); + } + } + /** * 删除授权设置详情 * diff --git a/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/service/DefaultMenuService.java b/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/service/DefaultMenuService.java index 1ded2b0f..fd94a9af 100755 --- a/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/service/DefaultMenuService.java +++ b/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/service/DefaultMenuService.java @@ -1,27 +1,39 @@ package org.jetlinks.community.auth.service; import lombok.Generated; +import lombok.extern.slf4j.Slf4j; import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang.ObjectUtils; import org.hswebframework.ezorm.rdb.mapping.ReactiveRepository; -import org.hswebframework.ezorm.rdb.operator.dml.query.SortOrder; +import org.hswebframework.web.api.crud.entity.GenericEntity; import org.hswebframework.web.api.crud.entity.QueryParamEntity; +import org.hswebframework.web.api.crud.entity.TreeSupportEntity; +import org.hswebframework.web.authorization.Authentication; import org.hswebframework.web.authorization.Dimension; +import org.hswebframework.web.cache.ReactiveCacheManager; +import org.hswebframework.web.crud.events.EntityCreatedEvent; import org.hswebframework.web.crud.events.EntityDeletedEvent; import org.hswebframework.web.crud.events.EntityModifyEvent; import org.hswebframework.web.crud.events.EntitySavedEvent; import org.hswebframework.web.crud.service.GenericReactiveCrudService; import org.hswebframework.web.crud.service.ReactiveTreeSortEntityService; +import org.hswebframework.web.exception.BusinessException; +import org.hswebframework.web.i18n.LocaleUtils; import org.hswebframework.web.id.IDGenerator; import org.hswebframework.web.system.authorization.api.event.ClearUserAuthorizationCacheEvent; +import org.jetlinks.community.auth.configuration.MenuProperties; import org.jetlinks.community.auth.entity.MenuBindEntity; import org.jetlinks.community.auth.entity.MenuEntity; import org.jetlinks.community.auth.entity.MenuView; +import org.jetlinks.reactor.ql.utils.CastUtils; +import org.reactivestreams.Publisher; import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.event.EventListener; import org.springframework.stereotype.Service; import org.springframework.util.StringUtils; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import reactor.math.MathFlux; import java.util.*; import java.util.function.Function; @@ -38,18 +50,31 @@ import java.util.stream.Collectors; * @since 1.0 */ @Service +@Slf4j public class DefaultMenuService extends GenericReactiveCrudService implements ReactiveTreeSortEntityService { private final ReactiveRepository bindRepository; + private final ApplicationEventPublisher eventPublisher; + private final ReactiveCacheManager cacheManager; + + private final MenuProperties properties; + + + public static final String MENU_CACHE_KEY = "menus-cache"; + public DefaultMenuService(ReactiveRepository bindRepository, - ApplicationEventPublisher eventPublisher) { + ApplicationEventPublisher eventPublisher, + ReactiveCacheManager cacheManager, + MenuProperties properties1) { this.bindRepository = bindRepository; this.eventPublisher = eventPublisher; + this.cacheManager = cacheManager; + this.properties = properties1; } @Override @@ -70,13 +95,30 @@ public class DefaultMenuService return menuEntity.getChildren(); } + public Flux getMenuViews(String targetType, String targetId, Flux menus) { + Flux allMenu = menus.cache(); + return Mono + .zip( + this + .getGrantedMenus(targetType, targetId) + .collectMap(GenericEntity::getId), + allMenu.collectMap(MenuView::getId, Function.identity(), LinkedHashMap::new), + (granted, all) -> LocaleUtils + .currentReactive() + .flatMapMany(locale -> allMenu + .doOnNext(MenuView::resetGrant) + .map(view -> view + .withGranted(granted.get(view.getId())) + ))) + .flatMapMany(Function.identity()); + } + public Flux getMenuViews(QueryParamEntity queryParam, Predicate menuPredicate) { return this .createQuery() .setParam(queryParam.noPaging()) - .orderBy(SortOrder.asc(MenuEntity::getSortIndex)) .fetch() - .collectMap(MenuEntity::getId, Function.identity()) + .collectMap(MenuEntity::getId, Function.identity(), LinkedHashMap::new) .flatMapIterable(menus -> convertMenuView(menus, menuPredicate, MenuView::of)); } @@ -121,16 +163,12 @@ public class DefaultMenuService } - private Flux convertToView(Flux entityFlux) { + + private Flux convertToView(Flux entityFlux, Mono> menuEntityMap) { return Mono .zip( //全部菜单数据 - this - .createQuery() - .where() - .and(MenuEntity::getStatus, 1) - .fetch() - .collectMap(MenuEntity::getId, Function.identity(), LinkedHashMap::new), + menuEntityMap, //菜单绑定信息 entityFlux.collect(Collectors.groupingBy(MenuBindEntity::getMenuId)), (menus, binds) -> convertMenuView(menus, @@ -140,11 +178,46 @@ public class DefaultMenuService .flatMapIterable(Function.identity()); } + public Mono> getEnabledMenus() { + return getCacheMenus() + .filter(menu -> ObjectUtils.equals((byte) 1, menu.getStatus())) + .collectMap(MenuEntity::getId, Function.identity(), LinkedHashMap::new); + } + + protected Flux getCacheMenus() { + return cacheManager + .getCache(MENU_CACHE_KEY) + .getFlux(MENU_CACHE_KEY, () -> this + .createQuery() + .fetch() + ); + } + + public Mono> getEnabledMenus(QueryParamEntity queryParam) { + return this + .createQuery() + .setParam(queryParam.noPaging()) + .where() + .and(MenuEntity::getStatus, 1) + .fetch() + .collectMap(MenuEntity::getId, Function.identity(), LinkedHashMap::new); + } + + private Flux convertToView(Flux entityFlux) { + return convertToView(entityFlux, getEnabledMenus()); + } + public Collection convertMenuView(Map menuMap, Predicate menuPredicate, Function converter) { + + List menus = new ArrayList<>(menuMap.values()); + Map menuSort = new HashMap<>(); Map group = new HashMap<>(); - for (MenuEntity menu : menuMap.values()) { + + for (int i = 0; i < menus.size(); i++) { + MenuEntity menu = menus.get(i); + menuSort.put(menu.getId(), i); if (group.containsKey(menu.getId())) { continue; } @@ -174,22 +247,44 @@ public class DefaultMenuService @EventListener public void handleMenuEntity(EntityModifyEvent e) { e.async( - ClearUserAuthorizationCacheEvent.all().publish(eventPublisher) + evictCacheMenus() + .then(ClearUserAuthorizationCacheEvent.all().publish(eventPublisher)) ); } @EventListener public void handleMenuEntity(EntityDeletedEvent e) { e.async( - ClearUserAuthorizationCacheEvent.all().publish(eventPublisher) + evictCacheMenus() + .then(ClearUserAuthorizationCacheEvent.all().publish(eventPublisher)) ); } @EventListener public void handleMenuEntity(EntitySavedEvent e) { e.async( - ClearUserAuthorizationCacheEvent.all().publish(eventPublisher) + evictCacheMenus() + .then(ClearUserAuthorizationCacheEvent.all().publish(eventPublisher)) + ); } + private Mono evictCacheMenus() { + return cacheManager + .getCache(MENU_CACHE_KEY) + .evict(MENU_CACHE_KEY); + } + + public Flux getUserMenuAsList(Authentication auth, QueryParamEntity queryParam) { + if (queryParam.getSorts().isEmpty()) { + queryParam.toQuery().orderByAsc(MenuEntity::getSortIndex); + } + return properties.isAllowAllMenu(auth) + ? this + .getMenuViews(queryParam, menu -> true) + .doOnNext(MenuView::grantAll) + : this + .getGrantedMenus(queryParam, auth.getDimensions()); + } + } diff --git a/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/service/MenuGrantService.java b/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/service/MenuGrantService.java index 53e0e060..2f936706 100644 --- a/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/service/MenuGrantService.java +++ b/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/service/MenuGrantService.java @@ -6,6 +6,7 @@ import org.hswebframework.ezorm.rdb.mapping.ReactiveRepository; import org.jetlinks.community.auth.entity.MenuBindEntity; import org.jetlinks.community.auth.entity.MenuEntity; import org.jetlinks.community.auth.service.request.MenuGrantRequest; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import reactor.core.publisher.Flux; @@ -20,7 +21,7 @@ public class MenuGrantService { private final AuthorizationSettingDetailService settingService; private final ReactiveRepository bindRepository; private final ReactiveRepository menuRepository; - + private final ApplicationEventPublisher eventPublisher; /** * 对菜单进行授权 @@ -30,30 +31,15 @@ public class MenuGrantService { */ @Transactional public Mono grant(MenuGrantRequest request) { + Set owner = request.containOwner(); return Flux .concat( //先删除原已保存的菜单信息 - bindRepository - .createDelete() - .where(MenuBindEntity::getTargetType, request.getTargetType()) - .and(MenuBindEntity::getTargetId, request.getTargetId()) - .execute(), + deleteMenuBind(request.getTargetType(), request.getTargetId(), owner), //保存菜单信息 bindRepository.save(request.toBindEntities()), settingService.clearPermissionUserAuth(request.getTargetType(), request.getTargetId()) ) -// //保存权限信息 -// Mono -// .zip(Mono.just(request), -// menuRepository -// .createQuery() -// .where(MenuEntity::getStatus, 1) -// .fetch() -// .collectList(), -// MenuGrantRequest::toAuthorizationSettingDetail -// ) -// //保存后端权限信息 -// .flatMap(setting -> settingService.saveDetail(null, Flux.just(setting)))) .then() ; } diff --git a/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/service/OrganizationService.java b/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/service/OrganizationService.java index 1b5ed19f..3871567d 100644 --- a/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/service/OrganizationService.java +++ b/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/service/OrganizationService.java @@ -2,14 +2,19 @@ package org.jetlinks.community.auth.service; import lombok.AllArgsConstructor; import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; import org.hswebframework.ezorm.rdb.mapping.defaults.SaveResult; +import org.hswebframework.web.crud.events.EntityEventHelper; +import org.hswebframework.web.crud.events.EntityModifyEvent; +import org.hswebframework.web.crud.events.EntitySavedEvent; import org.hswebframework.web.crud.service.GenericReactiveTreeSupportCrudService; import org.hswebframework.web.id.IDGenerator; import org.hswebframework.web.system.authorization.api.entity.DimensionUserEntity; import org.hswebframework.web.system.authorization.defaults.service.DefaultDimensionUserService; -import org.jetlinks.community.auth.dimension.OrgDimensionType; import org.jetlinks.community.auth.entity.OrganizationEntity; import org.jetlinks.community.auth.utils.DimensionUserBindUtils; +import org.jetlinks.community.authorize.OrgDimensionType; +import org.springframework.context.event.EventListener; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import reactor.core.publisher.Flux; @@ -39,19 +44,19 @@ public class OrganizationService extends GenericReactiveTreeSupportCrudService userIdStream = Flux.fromIterable(userIdList); return this - .findById(orgId) - .flatMap(org -> userIdStream - .map(userId -> { - DimensionUserEntity userEntity = new DimensionUserEntity(); - userEntity.setUserId(userId); - userEntity.setUserName(userId); - userEntity.setDimensionId(orgId); - userEntity.setDimensionTypeId(OrgDimensionType.org.getId()); - userEntity.setDimensionName(org.getName()); - return userEntity; - }) - .as(dimensionUserService::save)) - .map(SaveResult::getTotal); + .findById(orgId) + .flatMap(org -> userIdStream + .map(userId -> { + DimensionUserEntity userEntity = new DimensionUserEntity(); + userEntity.setUserId(userId); + userEntity.setUserName(userId); + userEntity.setDimensionId(orgId); + userEntity.setDimensionTypeId(OrgDimensionType.org.getId()); + userEntity.setDimensionName(org.getName()); + return userEntity; + }) + .as(dimensionUserService::save)) + .map(SaveResult::getTotal); } @@ -60,15 +65,15 @@ public class OrganizationService extends GenericReactiveTreeSupportCrudService userIdStream = Flux.fromIterable(userIdList); return userIdStream - .collectList() - .filter(CollectionUtils::isNotEmpty) - .flatMap(newUserIdList -> dimensionUserService - .createDelete() - .where(DimensionUserEntity::getDimensionTypeId, OrgDimensionType.org.getId()) - .in(DimensionUserEntity::getUserId, newUserIdList) - .and(DimensionUserEntity::getDimensionId, orgId) - .execute()) - ; + .collectList() + .filter(CollectionUtils::isNotEmpty) + .flatMap(newUserIdList -> dimensionUserService + .createDelete() + .where(DimensionUserEntity::getDimensionTypeId, OrgDimensionType.org.getId()) + .in(DimensionUserEntity::getUserId, newUserIdList) + .and(DimensionUserEntity::getDimensionId, orgId) + .execute()) + ; } @@ -79,6 +84,7 @@ public class OrganizationService extends GenericReactiveTreeSupportCrudService bindUser(Collection userIdList, @@ -90,6 +96,33 @@ public class OrganizationService extends GenericReactiveTreeSupportCrudService event) { + event.async( + Flux.fromIterable(event.getEntity()) + .flatMap(this::handleParentId) + ); + } + @EventListener + public void doHandleModified(EntityModifyEvent event) { + event.async( + Flux.fromIterable(event.getAfter()) + .flatMap(this::handleParentId) + ); + } + + private Mono handleParentId(OrganizationEntity organization) { + return Mono + .just(organization) + .filter(org -> StringUtils.isBlank(organization.getParentId())) + .flatMap(org -> createUpdate() + .setNull(OrganizationEntity::getParentId) + .where(OrganizationEntity::getId, organization.getId()) + .execute() + .as(EntityEventHelper::setDoNotFireEvent) + .then() + ); + } } diff --git a/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/service/PasswordStrengthValidator.java b/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/service/PasswordStrengthValidator.java new file mode 100644 index 00000000..0912aeb6 --- /dev/null +++ b/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/service/PasswordStrengthValidator.java @@ -0,0 +1,65 @@ +package org.jetlinks.community.auth.service; + +import lombok.Generated; +import lombok.Getter; +import lombok.Setter; +import org.hswebframework.web.exception.ValidationException; +import org.hswebframework.web.system.authorization.api.PasswordValidator; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; + +import java.util.HashSet; +import java.util.Set; + +/** + * 密码验证器,用于过滤简单密码. + * + * @author zhouhao + * @see PasswordValidator + * @since 1.0 + */ +@Component +@ConfigurationProperties(prefix = "hsweb.user.password.validator") +@Getter +@Setter +@Generated +public class PasswordStrengthValidator implements PasswordValidator { + + private String[] regex = { + ".*\\d+.*", //数字 + ".*[A-Z]+.*", //大写字母 + ".*[a-z]+.*", //小写字母 + ".*[^x00-xff]+.*", //2位字符(中文?) + ".*[~!@#$%^&*()_+|<>,.?/:;'\\[\\]{}\"]+.*", //特殊符号 + }; + + private Set blackList = new HashSet<>(); + + private int minLength = 8; + + private int maxLength = 64; + + private int level = 2; + + private String message = "密码必须由数字和字母组成"; + + @Override + public void validate(String password) { + if (StringUtils.isEmpty(password) || password.length() < minLength || password.length() > maxLength) { + throw new ValidationException("","error.password_length", minLength, maxLength); + } + if (blackList.contains(password)) { + throw new ValidationException("error.insufficient_password_strength"); + } + int _level = 0; + for (String s : regex) { + if (password.matches(s)) { + _level++; + } + } + if (_level <= level) { + throw new ValidationException("error.insufficient_password_strength"); + } + } +} diff --git a/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/service/RoleGroupService.java b/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/service/RoleGroupService.java index e69e567f..7268dfb0 100644 --- a/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/service/RoleGroupService.java +++ b/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/service/RoleGroupService.java @@ -8,10 +8,10 @@ import org.hswebframework.web.crud.events.EntityDeletedEvent; import org.hswebframework.web.crud.query.QueryHelper; import org.hswebframework.web.crud.service.GenericReactiveTreeSupportCrudService; import org.hswebframework.web.exception.I18nSupportException; +import org.hswebframework.web.i18n.LocaleUtils; import org.hswebframework.web.id.IDGenerator; - -import org.jetlinks.community.auth.entity.RoleGroupEntity; import org.jetlinks.community.auth.entity.RoleEntity; +import org.jetlinks.community.auth.entity.RoleGroupEntity; import org.jetlinks.community.auth.web.response.RoleGroupDetailTree; import org.springframework.boot.CommandLineRunner; import org.springframework.context.event.EventListener; @@ -30,6 +30,8 @@ public class RoleGroupService extends GenericReactiveTreeSupportCrudService !DEFAULT_GROUP_ID.equals(id)) .collectList() .filter(CollectionUtils::isNotEmpty) .flatMapMany(ids -> roleService @@ -56,17 +56,19 @@ public class RoleGroupService extends GenericReactiveTreeSupportCrudService queryDetailTree(QueryParamEntity groupParam, QueryParamEntity roleParam) { groupParam.setPaging(false); roleParam.setPaging(false); - Flux groupDetails = this + return LocaleUtils + .currentReactive() + .flatMapMany(locale -> this .query(groupParam) - .map(RoleGroupDetailTree::of); - return QueryHelper + .map(tree -> RoleGroupDetailTree.of(tree, locale))) + .as(fluxTree -> QueryHelper .combineOneToMany( - groupDetails, - RoleGroupDetailTree::getGroupId, - roleService.createQuery().setParam(roleParam), - RoleEntity::getGroupId, - RoleGroupDetailTree::setRoles - ); + fluxTree, + RoleGroupDetailTree::getGroupId, + roleService.createQuery().setParam(roleParam), + RoleEntity::getGroupId, + RoleGroupDetailTree::setRoles + )); } @@ -86,15 +88,20 @@ public class RoleGroupService extends GenericReactiveTreeSupportCrudService { - }, - err -> log.error("init role groupId error", err)); + RoleGroupEntity groupEntity = new RoleGroupEntity(); + groupEntity.setName("默认"); + groupEntity.setId(DEFAULT_GROUP_ID); + //变更为由auto-init下的json文件初始化数据 +// this +// .save(groupEntity) +// .then(roleService +// .createUpdate() +// .set(RoleEntity::getGroupId, DEFAULT_GROUP_ID) +// .isNull(RoleEntity::getGroupId) +// .execute()) +// .subscribe(ignore -> { +// }, +// err -> log.error("init role groupId error", err)); } diff --git a/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/service/RoleService.java b/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/service/RoleService.java index 56a72481..38649fb7 100644 --- a/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/service/RoleService.java +++ b/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/service/RoleService.java @@ -3,7 +3,8 @@ package org.jetlinks.community.auth.service; import lombok.AllArgsConstructor; import org.apache.commons.collections4.CollectionUtils; import org.hswebframework.web.authorization.DefaultDimensionType; -import org.hswebframework.web.crud.service.GenericReactiveCrudService; +import org.hswebframework.web.crud.query.QueryHelper; +import org.hswebframework.web.crud.service.GenericReactiveCacheSupportCrudService; import org.hswebframework.web.system.authorization.defaults.service.DefaultDimensionUserService; import org.jetlinks.community.auth.entity.RoleEntity; import org.jetlinks.community.auth.utils.DimensionUserBindUtils; @@ -17,11 +18,13 @@ import java.util.Collection; @Service @AllArgsConstructor -public class RoleService extends GenericReactiveCrudService { +public class RoleService extends GenericReactiveCacheSupportCrudService { private final DefaultDimensionUserService dimensionUserService; + private QueryHelper queryHelper; + /** * 绑定用户到角色 @@ -51,12 +54,12 @@ public class RoleService extends GenericReactiveCrudService } /** - * 绑定用户到角色 + * 解绑角色的用户 * * @param userIdList 用户ID * @param roleIdList 角色Id * @return void - * @see DimensionUserBindUtils#bindUser(DefaultDimensionUserService, Collection, String, Collection, boolean) + * @see DimensionUserBindUtils#unbindUser(DefaultDimensionUserService, Collection, String, Collection) */ @Transactional public Mono unbindUser(@NotNull Collection userIdList, @@ -66,12 +69,27 @@ public class RoleService extends GenericReactiveCrudService return Mono.empty(); } return DimensionUserBindUtils - .unbindUser(dimensionUserService, - userIdList, - DefaultDimensionType.role.getId(), - roleIdList); + .unbindUser(dimensionUserService, userIdList, DefaultDimensionType.role.getId(), roleIdList) + .then(); } + /** + * 解绑角色的所有用户 + * + * @param roleIdList 角色Id + * @return void + * @see DimensionUserBindUtils#unbindUser(DefaultDimensionUserService, Collection, String, Collection) + */ + @Transactional + public Mono unbindAllUser(@NotNull Collection roleIdList) { + if (CollectionUtils.isEmpty(roleIdList)) { + return Mono.empty(); + } + return DimensionUserBindUtils + .unbindUser(dimensionUserService, null, DefaultDimensionType.role.getId(), roleIdList) + .then(); + + } } diff --git a/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/service/UserDetailService.java b/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/service/UserDetailService.java index 19ba151d..a391e4a7 100755 --- a/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/service/UserDetailService.java +++ b/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/service/UserDetailService.java @@ -5,11 +5,13 @@ import org.hswebframework.web.api.crud.entity.QueryParamEntity; import org.hswebframework.web.authorization.Authentication; import org.hswebframework.web.authorization.ReactiveAuthenticationHolder; import org.hswebframework.web.authorization.ReactiveAuthenticationManager; +import org.hswebframework.web.authorization.token.UserTokenManager; import org.hswebframework.web.bean.FastBeanCopier; import org.hswebframework.web.crud.query.QueryHelper; import org.hswebframework.web.crud.service.GenericReactiveCrudService; import org.hswebframework.web.i18n.LocaleUtils; import org.hswebframework.web.system.authorization.api.entity.UserEntity; +import org.hswebframework.web.system.authorization.api.event.ClearUserAuthorizationCacheEvent; import org.hswebframework.web.system.authorization.api.event.UserDeletedEvent; import org.hswebframework.web.system.authorization.api.service.reactive.ReactiveUserService; import org.hswebframework.web.validator.ValidatorUtils; @@ -19,6 +21,8 @@ import org.jetlinks.community.auth.enums.DefaultUserEntityType; import org.jetlinks.community.auth.enums.UserEntityTypes; import org.jetlinks.community.auth.service.request.SaveUserDetailRequest; import org.jetlinks.community.auth.service.request.SaveUserRequest; +import org.jetlinks.core.things.ThingsRegistry; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.event.EventListener; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -30,6 +34,7 @@ import reactor.core.publisher.Mono; import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.function.Function; /** * 用户详情管理 @@ -44,31 +49,42 @@ import java.util.List; @Service public class UserDetailService extends GenericReactiveCrudService { + public static final String recursiveTagContextKey = UserDetailService.class.getName() + "_RECURSIVE_TAG"; + private final ReactiveUserService userService; private final RoleService roleService; - private final OrganizationService organizationService; + private final static UserDetailEntity emptyDetail = new UserDetailEntity(); + private final ApplicationEventPublisher eventPublisher; private final ReactiveAuthenticationManager authenticationManager; - + private final UserTokenManager userTokenManager; + private final UserSettingService userSettingService; private final QueryHelper queryHelper; - private final static UserDetailEntity emptyDetail = new UserDetailEntity(); + private final ThingsRegistry registry; public UserDetailService(ReactiveUserService userService, RoleService roleService, OrganizationService organizationService, + ApplicationEventPublisher eventPublisher, ReactiveAuthenticationManager authenticationManager, - QueryHelper queryHelper) { + UserTokenManager userTokenManager, + UserSettingService userSettingService, + QueryHelper queryHelper, + ThingsRegistry registry) { this.userService = userService; this.roleService = roleService; this.organizationService = organizationService; - this.authenticationManager = authenticationManager; + this.eventPublisher = eventPublisher; + this.userTokenManager = userTokenManager; + this.userSettingService = userSettingService; this.queryHelper = queryHelper; + this.registry = registry; + this.authenticationManager = authenticationManager; // 注册默认用户类型 UserEntityTypes.register(Arrays.asList(DefaultUserEntityType.values())); - } /** @@ -77,6 +93,7 @@ public class UserDetailService extends GenericReactiveCrudService findUserDetail(String userId) { return Mono .zip( @@ -103,6 +120,7 @@ public class UserDetailService extends GenericReactiveCrudService saveUserDetail(String userId, SaveUserDetailRequest request) { ValidatorUtils.tryValidate(request); UserDetailEntity entity = FastBeanCopier.copy(request, new UserDetailEntity()); @@ -128,7 +146,7 @@ public class UserDetailService extends GenericReactiveCrudService j.is(UserDetailEntity::getId, UserEntity::getId)) .where(query) .fetchPaged(), - list -> this.fillUserDetail(list). - collectList()); + list -> this + .fillUserDetail(list) + .collectList() + ); } private Flux fillUserDetail(List users) { @@ -153,8 +173,9 @@ public class UserDetailService extends GenericReactiveCrudService detail.withDimension(dimensions).withType()) - ); + .map(detail::withDimension) + ) + .thenMany(Flux.fromIterable(users)); } /** @@ -183,6 +204,10 @@ public class UserDetailService extends GenericReactiveCrudService ClearUserAuthorizationCacheEvent.of(userId).publish(eventPublisher).thenReturn(userId)) .as(LocaleUtils::transform); } diff --git a/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/service/info/UserLoginInfo.java b/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/service/info/UserLoginInfo.java new file mode 100644 index 00000000..76852256 --- /dev/null +++ b/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/service/info/UserLoginInfo.java @@ -0,0 +1,65 @@ +package org.jetlinks.community.auth.service.info; + +import com.google.common.collect.Sets; +import lombok.Getter; +import lombok.Setter; +import org.hswebframework.web.bean.FastBeanCopier; +import org.hswebframework.web.logging.RequestInfo; +import org.jetlinks.community.PropertyConstants; +import org.jetlinks.core.things.Thing; +import reactor.core.publisher.Mono; + +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +/** + * @author gyl + * @since 2.2 + */ +@Getter +@Setter +public class UserLoginInfo { + + public static final PropertyConstants.Key CONFIG_KEY_LOGIN_TIME = + PropertyConstants.Key.of("loginTime", null, Long.class); + public static final PropertyConstants.Key CONFIG_KEY_LOGIN_IP = + PropertyConstants.Key.of("loginIp", null, String.class); + + private static final Set allConfigKey = Sets.newHashSet( + CONFIG_KEY_LOGIN_TIME.getKey(), + CONFIG_KEY_LOGIN_IP.getKey() + ); + + private Long loginTime; + private String loginIp; + + public static final UserLoginInfo EMPTY = new UserLoginInfo(); + + + public UserLoginInfo with(RequestInfo info) { + if (info == null) { + return this; + } + this.loginIp = info.getIpAddr();//.split(",")[0]; + return this; + } + + public Mono writeTo(Thing thing) { + Map configs = FastBeanCopier.copy(this, new HashMap<>()); + return thing + .setConfigs(configs) + .then(); + } + + public static Mono readFrom(Thing thing) { + return thing + .getSelfConfigs(allConfigKey) + .mapNotNull(values -> { + if (values.isEmpty()) { + return null; + } + return FastBeanCopier.copy(values.getAllValues(), new UserLoginInfo()); + }); + } +} \ No newline at end of file diff --git a/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/service/request/MenuGrantRequest.java b/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/service/request/MenuGrantRequest.java index dfd0b542..89530ab6 100644 --- a/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/service/request/MenuGrantRequest.java +++ b/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/service/request/MenuGrantRequest.java @@ -8,7 +8,6 @@ import lombok.Setter; import org.apache.commons.collections4.CollectionUtils; import org.hswebframework.web.api.crud.entity.TreeSupportEntity; import org.hswebframework.web.id.IDGenerator; -import org.jetlinks.community.auth.entity.MenuView; import org.jetlinks.community.auth.entity.MenuBindEntity; import org.jetlinks.community.auth.entity.MenuEntity; import org.jetlinks.community.auth.entity.MenuView; @@ -130,7 +129,21 @@ public class MenuGrantRequest { .of(menu) .withTarget(targetType, targetId) .withMerge(merge, priority)) - .collect(Collectors.toList()); + .collect(Collectors.collectingAndThen( + Collectors.toCollection(() -> new TreeSet<>(Comparator.comparing(MenuBindEntity::getId))), + ArrayList::new + )); } + + public Set containOwner() { + if (CollectionUtils.isEmpty(menus)) { + return Collections.emptySet(); + } + return menus + .stream() + .map(MenuView::getOwner) + .filter(StringUtils::hasText) + .collect(Collectors.toSet()); + } } diff --git a/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/service/terms/OrgUserTermBuilder.java b/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/service/terms/OrgUserTermBuilder.java new file mode 100644 index 00000000..61b0cdb9 --- /dev/null +++ b/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/service/terms/OrgUserTermBuilder.java @@ -0,0 +1,85 @@ +package org.jetlinks.community.auth.service.terms; + +import org.hswebframework.ezorm.core.param.Term; +import org.hswebframework.ezorm.rdb.metadata.RDBColumnMetadata; +import org.hswebframework.ezorm.rdb.operator.builder.fragments.BatchSqlFragments; +import org.hswebframework.ezorm.rdb.operator.builder.fragments.SqlFragments; +import org.hswebframework.ezorm.rdb.operator.builder.fragments.term.AbstractTermFragmentBuilder; +import org.hswebframework.ezorm.rdb.utils.SqlUtils; +import org.jetlinks.community.auth.dimension.OrgDimensionType; +import org.springframework.stereotype.Component; + +import java.util.List; + +/** + * 查询组织关联的用户. + * + * 只查询组织绑定的用户: + *
{@code
+ *     "terms":[
+ *         {
+ *             "column":"id$in-org-user$org",
+ *             "value":["orgId"]
+ *         }
+ *     ]
+ * }
+ * + * + * 查询组织或职位绑定的用户 + *
{@code
+ *     "terms":[
+ *         {
+ *             "column":"id$in-org-user",
+ *             "value":["orgId"]
+ *         }
+ *     ]
+ * }
+ * + * @author zhangji 2025/2/12 + * @since 2.3 + */ +@Component +public class OrgUserTermBuilder extends AbstractTermFragmentBuilder { + public static final String termType = "in-org-user"; + + public OrgUserTermBuilder() { + super(termType, "组织关联的所有用户"); + } + + @Override + public SqlFragments createFragments(String columnFullName, RDBColumnMetadata column, Term term) { + + List values = convertList(column, term); + + BatchSqlFragments fragments = new BatchSqlFragments(15, 4); + List options = term.getOptions(); + + if (options.contains("not")) { + fragments.add(SqlFragments.NOT); + } + + // 组织绑定条件,options未指定时也查询 + if (options.contains("org")) { + fragments.addSql("exists(select 1 from", + getTableName("s_dimension_user", column), + "d where d.user_id =", columnFullName, + "and ("); + fragments.addSql("d.dimension_type_id = ?") + .addParameter(OrgDimensionType.org.getId()); + if (!options.contains("any")) { + fragments + .addSql("and d.dimension_id in(") + .add(SqlUtils.createQuestionMarks(values.size())) + .add(SqlFragments.RIGHT_BRACKET) + .addParameter(values); + } + } else { + return fragments; + } + + fragments.add(SqlFragments.RIGHT_BRACKET) + .add(SqlFragments.RIGHT_BRACKET); + + return fragments; + } +} diff --git a/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/service/terms/OrganizationTreeChildTerm.java b/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/service/terms/OrganizationTreeChildTerm.java new file mode 100644 index 00000000..ae92d1ef --- /dev/null +++ b/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/service/terms/OrganizationTreeChildTerm.java @@ -0,0 +1,23 @@ +package org.jetlinks.community.auth.service.terms; + +import org.hswebframework.web.crud.sql.terms.TreeChildTermBuilder; +import org.springframework.stereotype.Component; + +/** + * 查询组织及下级的数据. + */ +@Component +public class OrganizationTreeChildTerm extends TreeChildTermBuilder { + + public static final String TERM_TYPE = "org-child"; + + public OrganizationTreeChildTerm() { + super(TERM_TYPE, "查询组织及下级的数据"); + } + + @Override + protected String tableName() { + return "s_organization"; + } +} + diff --git a/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/service/terms/UserDetailTermBuilder.java b/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/service/terms/UserDetailTermBuilder.java index 3ebc1f5f..45871465 100644 --- a/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/service/terms/UserDetailTermBuilder.java +++ b/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/service/terms/UserDetailTermBuilder.java @@ -1,116 +1,47 @@ package org.jetlinks.community.auth.service.terms; -import com.alibaba.fastjson.JSON; -import com.alibaba.fastjson.JSONArray; -import org.hswebframework.ezorm.core.param.Term; -import org.hswebframework.ezorm.rdb.metadata.RDBColumnMetadata; -import org.hswebframework.ezorm.rdb.metadata.RDBTableMetadata; -import org.hswebframework.ezorm.rdb.metadata.TableOrViewMetadata; -import org.hswebframework.ezorm.rdb.operator.builder.fragments.*; -import org.hswebframework.ezorm.rdb.operator.builder.fragments.term.AbstractTermFragmentBuilder; -import org.hswebframework.web.api.crud.entity.TermExpressionParser; +import org.jetlinks.community.terms.SubTableTermFragmentBuilder; import org.springframework.stereotype.Component; -import java.util.List; - /** * 根据用户详情条件 查询用户. *

* 将用户详情的条件(手机号、邮箱)嵌套到此条件中 - *

+ *

{@code
  * "terms": [
- * {
- * "column": "id$user-detail",
- * "value": [
- * {
- * "column": "telephone",
- * "termType": "like",
- * "value": "%888%"
- * },
- * {
- * "column": "email",
- * "termType": "like",
- * "value": "%123%"
- * }
- * ]
- * }
- * ],
- *
+ *      {
+ *          "column": "id$user-detail",
+ *          "value": [
+ *              {
+ *              "column": "telephone",
+ *              "termType": "like",
+ *              "value": "%888%"
+ *              },
+ *              {
+ *              "column": "email",
+ *              "termType": "like",
+ *              "value": "%123%"
+ *              }
+ *          ]
+ *      }
+ *  ]
+ * }
* @author zhangji 2022/6/29 */ @Component -public class UserDetailTermBuilder extends AbstractTermFragmentBuilder { +public class UserDetailTermBuilder extends SubTableTermFragmentBuilder { public UserDetailTermBuilder() { super("user-detail", "按用户详情查询"); } - @SuppressWarnings("all") - public static List convertTerms(Object value) { - if (value instanceof String) { - String strVal = String.valueOf(value); - //json字符串 - if (strVal.startsWith("[")) { - return JSON.parseArray(strVal, Term.class); - } else { - //表达式 - return TermExpressionParser.parse(strVal); - } - } - if (value instanceof List) { - return new JSONArray(((List) value)).toJavaList(Term.class); - } else { - throw new UnsupportedOperationException("unsupported term value:" + value); - } + @Override + protected String getTableAlias() { + return "_detail"; } - @Override - public SqlFragments createFragments(String columnFullName, RDBColumnMetadata column, Term term) { - List terms = convertTerms(term.getValue()); - PrepareSqlFragments sqlFragments = PrepareSqlFragments.of(); - if (term.getOptions().contains("not")) { - sqlFragments.addSql("not"); - } - sqlFragments - .addSql("exists(select 1 from ", getTableName("s_user_detail", column), " _detail where _detail.id = ", columnFullName); - - RDBTableMetadata metadata = column - .getOwner() - .getSchema() - .getTable("s_user_detail") - .orElseThrow(() -> new UnsupportedOperationException("unsupported s_user_detail")); - - SqlFragments where = builder.createTermFragments(metadata, terms); - if (!where.isEmpty()) { - sqlFragments.addSql("and") - .addFragments(where); - } - sqlFragments.addSql(")"); - return sqlFragments; + protected String getSubTableName() { + return "s_user_detail"; } - - static UserDetailTermsBuilder builder = new UserDetailTermsBuilder(); - - static class UserDetailTermsBuilder extends AbstractTermsFragmentBuilder { - - @Override - protected SqlFragments createTermFragments(TableOrViewMetadata parameter, List terms) { - return super.createTermFragments(parameter, terms); - } - - @Override - protected SqlFragments createTermFragments(TableOrViewMetadata table, Term term) { - if (term.getValue() instanceof NativeSql) { - NativeSql sql = ((NativeSql) term.getValue()); - return PrepareSqlFragments.of(sql.getSql(), sql.getParameters()); - } - return table - .getColumn(term.getColumn()) - .flatMap(column -> table - .findFeature(TermFragmentBuilder.createFeatureId(term.getTermType())) - .map(termFragment -> termFragment.createFragments(column.getFullName("_detail"), column, term))) - .orElse(EmptySqlFragments.INSTANCE); - } - } } diff --git a/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/utils/DimensionUserBindUtils.java b/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/utils/DimensionUserBindUtils.java index a6004840..f9343a6e 100644 --- a/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/utils/DimensionUserBindUtils.java +++ b/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/utils/DimensionUserBindUtils.java @@ -1,14 +1,15 @@ package org.jetlinks.community.auth.utils; import org.apache.commons.collections4.CollectionUtils; +import org.hswebframework.ezorm.rdb.mapping.ReactiveDelete; import org.hswebframework.web.authorization.DefaultDimensionType; import org.hswebframework.web.system.authorization.api.entity.DimensionUserEntity; import org.hswebframework.web.system.authorization.defaults.service.DefaultDimensionUserService; -import org.jetlinks.community.auth.dimension.OrgDimensionType; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import java.util.Collection; +import java.util.function.Function; public class DimensionUserBindUtils { @@ -27,15 +28,20 @@ public class DimensionUserBindUtils { Collection userIdList, String dimensionType, Collection dimensionIdList, - boolean removeOldBind) { + boolean removeOldBind, + Function deleteCustomizer, + Function customizer) { Mono before = Mono.empty(); if (removeOldBind) { - before = dimensionUserService - .createDelete() - .where() - .in(DimensionUserEntity::getUserId, userIdList) - .and(DimensionUserEntity::getDimensionTypeId, dimensionType) + before = deleteCustomizer + .apply( + dimensionUserService + .createDelete() + .where() + .in(DimensionUserEntity::getUserId, userIdList) + .and(DimensionUserEntity::getDimensionTypeId, dimensionType) + ) .execute() .then(); } @@ -48,26 +54,50 @@ public class DimensionUserBindUtils { .fromIterable(userIdList) .flatMap(userId -> Flux .fromIterable(dimensionIdList) - .map(dimensionId -> createEntity(dimensionType, dimensionId, userId))) + .mapNotNull(dimensionId -> customizer.apply(createEntity(dimensionType, dimensionId, userId)))) .as(dimensionUserService::save) .then() ); - } - public static Mono unbindUser(DefaultDimensionUserService dimensionUserService, - Collection userIdList, - String dimensionType, - Collection dimensionIdList) { + /** + * 绑定用户到指定的维度中,removeOldBind设置为true时,在绑定前会删除旧的绑定信息(全量绑定). + * 否则不会删除旧的绑定信息(增量绑定) + * + * @param userIdList 用户ID列表 + * @param dimensionType 维度类型, + * 如:角色{@link DefaultDimensionType#role},部门{@link OrgDimensionType#org}. + * @param dimensionIdList 角色ID列表 + * @param removeOldBind 是否删除旧的绑定信息 + * @return void + */ + public static Mono bindUser(DefaultDimensionUserService dimensionUserService, + Collection userIdList, + String dimensionType, + Collection dimensionIdList, + boolean removeOldBind) { + return bindUser(dimensionUserService, userIdList, dimensionType, dimensionIdList, removeOldBind,Function.identity(), Function.identity()); + } + + public static Mono unbindUser(DefaultDimensionUserService dimensionUserService, + Collection userIdList, + String dimensionType, + Collection dimensionIdList, + Function customizer) { return dimensionUserService .createDelete() - .where() - .in(DimensionUserEntity::getUserId, userIdList) - .and(DimensionUserEntity::getDimensionTypeId, dimensionType) + .where(DimensionUserEntity::getDimensionTypeId, dimensionType) + .when(CollectionUtils.isNotEmpty(userIdList), + delete -> delete.in(DimensionUserEntity::getUserId, userIdList)) .when(CollectionUtils.isNotEmpty(dimensionIdList), delete -> delete.in(DimensionUserEntity::getDimensionId, dimensionIdList)) - .execute() - .then(); + .execute(); + } + public static Mono unbindUser(DefaultDimensionUserService dimensionUserService, + Collection userIdList, + String dimensionType, + Collection dimensionIdList) { + return unbindUser(dimensionUserService, userIdList, dimensionType, dimensionIdList,Function.identity()); } diff --git a/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/web/AuthorizationSettingDetailController.java b/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/web/AuthorizationSettingDetailController.java index 8aa45248..ef3ad301 100644 --- a/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/web/AuthorizationSettingDetailController.java +++ b/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/web/AuthorizationSettingDetailController.java @@ -14,6 +14,8 @@ import org.springframework.web.bind.annotation.*; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import java.util.List; + @RestController @RequestMapping("/autz-setting/detail") @Authorize @@ -31,12 +33,13 @@ public class AuthorizationSettingDetailController { @PostMapping("/_save") @SaveAction @Operation(summary = "赋权") - public Mono saveSettings(@RequestBody Flux detailFlux) { + public Mono saveSettings(@RequestBody Mono> detailMono) { return Authentication .currentReactive() - .flatMap(authentication -> settingService - .saveDetail(authentication, detailFlux) + .flatMap(authentication -> detailMono + .flatMapMany(Flux::fromIterable) + .as(flux -> settingService.saveDetail(authentication, flux)) .thenReturn(true) ); } 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 fdde380a..d881537e 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 @@ -9,7 +9,10 @@ import lombok.NoArgsConstructor; import lombok.Setter; import org.hswebframework.ezorm.core.param.TermType; import org.hswebframework.ezorm.rdb.mapping.defaults.SaveResult; -import org.hswebframework.web.api.crud.entity.*; +import org.hswebframework.web.api.crud.entity.QueryNoPagingOperation; +import org.hswebframework.web.api.crud.entity.QueryParamEntity; +import org.hswebframework.web.api.crud.entity.TreeSupportEntity; +import org.hswebframework.web.api.crud.entity.TreeUtils; import org.hswebframework.web.authorization.Authentication; import org.hswebframework.web.authorization.annotation.*; import org.hswebframework.web.authorization.exception.UnAuthorizedException; @@ -18,7 +21,6 @@ import org.hswebframework.web.crud.web.reactive.ReactiveServiceCrudController; import org.hswebframework.web.exception.ValidationException; import org.hswebframework.web.i18n.LocaleUtils; import org.hswebframework.web.system.authorization.defaults.service.DefaultPermissionService; -import org.jetlinks.community.auth.configuration.MenuProperties; import org.jetlinks.community.auth.entity.MenuEntity; import org.jetlinks.community.auth.entity.MenuView; import org.jetlinks.community.auth.service.DefaultMenuService; @@ -33,7 +35,6 @@ import reactor.core.publisher.Mono; import java.util.Collections; import java.util.List; -import java.util.function.Function; /** * 菜单管理 @@ -53,8 +54,6 @@ public class MenuController implements ReactiveServiceCrudController getAllMenuAsTree(@RequestBody Mono queryMono) { return queryMono - .doOnNext(query -> query - .toQuery() - .orderByAsc(MenuEntity::getSortIndex) - .noPaging()) + .doOnNext(query -> { + query.toQuery() + .orderByAsc(MenuEntity::getSortIndex) + .noPaging(); + }) .flatMapMany(defaultMenuService::query) .collectList() .flatMapIterable(list -> TreeSupportEntity.list2tree(list, MenuEntity::setChildren)); @@ -118,14 +118,7 @@ public class MenuController implements ReactiveServiceCrudController properties.isAllowAllMenu(autz) - ? - defaultMenuService - .getMenuViews(queryParam, menu -> true) - .doOnNext(MenuView::grantAll) - : - defaultMenuService.getGrantedMenus(queryParam, autz.getDimensions()) - ); + .flatMapMany(autz -> defaultMenuService.getUserMenuAsList(autz, queryParam)); } /** @@ -143,7 +136,7 @@ public class MenuController implements ReactiveServiceCrudController getGrantInfo(@PathVariable String targetType, @PathVariable String targetId, QueryParamEntity query) { - - Flux allMenu = this.getUserMenuAsList(query).cache(); - return Mono - .zip( - defaultMenuService - .getGrantedMenus(targetType, targetId) - .collectMap(GenericEntity::getId), - allMenu.collectMap(MenuView::getId, Function.identity()), - (granted, all) -> LocaleUtils - .currentReactive() - .flatMapMany(locale -> allMenu - .doOnNext(MenuView::resetGrant) - .map(view -> view - .withGranted(granted.get(view.getId())) - ))) - .flatMapMany(Function.identity()); - + return defaultMenuService.getMenuViews(targetType, targetId, this.getUserMenuAsList(query)); } @@ -275,7 +252,6 @@ public class MenuController implements ReactiveServiceCrudController listToTree(Flux flux) { + protected static Flux listToTree(Flux flux) { return flux .collectList() .flatMapIterable(list -> TreeUtils.list2tree(list, MenuView::getId, MenuView::getParentId, MenuView::setChildren)); diff --git a/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/web/OrganizationController.java b/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/web/OrganizationController.java index 036d891a..6fe1753a 100755 --- a/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/web/OrganizationController.java +++ b/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/web/OrganizationController.java @@ -3,6 +3,7 @@ package org.jetlinks.community.auth.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.QueryNoPagingOperation; import org.hswebframework.web.api.crud.entity.QueryOperation; import org.hswebframework.web.api.crud.entity.QueryParamEntity; import org.hswebframework.web.api.crud.entity.TreeSupportEntity; @@ -19,11 +20,12 @@ import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import java.util.List; +import java.util.function.Function; @RequestMapping("/organization") @RestController -@Resource(id = "organization", name = "部门管理") -@Tag(name = "部门管理") +@Resource(id = "organization", name = "组织管理") +@Tag(name = "组织管理") public class OrganizationController implements ReactiveServiceCrudController { private final OrganizationService organizationService; @@ -42,11 +44,11 @@ public class OrganizationController implements ReactiveServiceCrudController getAllOrgTree() { - return queryAll() - .collectList() - .flatMapIterable(list -> TreeSupportEntity.list2tree(list, OrganizationEntity::setChildren)); + @QueryNoPagingOperation(summary = "获取全部数据(树结构)") + public Flux getAllOrgTree(QueryParamEntity query) { + return organizationService + .queryResultToTree(query) + .flatMapIterable(Function.identity()); } @PostMapping("/_all/tree") @@ -60,36 +62,38 @@ public class OrganizationController implements ReactiveServiceCrudController getAllOrg() { - return queryAll(); + @Operation(summary = "获取全部数据") + public Flux getAllOrg(QueryParamEntity query) { + return organizationService + .query(query.noPaging()); } @PostMapping("/_all") @Authorize(merge = false) - @Operation(summary = "获取全部机构信息") + @Operation(summary = "获取全部数据") public Flux getAllOrg(@RequestBody Mono query) { - return queryAll(query); + return organizationService + .query(query.doOnNext(QueryParamEntity::noPaging)); } @GetMapping("/_query/_children/tree") @QueryAction - @QueryOperation(summary = "查询机构列表(包含子机构)树结构") + @QueryOperation(summary = "查询列表(包含子级)树结构") public Mono> queryChildrenTree(@Parameter(hidden = true) QueryParamEntity entity) { return organizationService.queryIncludeChildrenTree(entity); } @GetMapping("/_query/_children") @QueryAction - @QueryOperation(summary = "查询机构列表(包含子机构)") + @QueryOperation(summary = "查询列表(包含子级)") public Flux queryChildren(@Parameter(hidden = true) QueryParamEntity entity) { return organizationService.queryIncludeChildren(entity); } @PostMapping("/{id}/users/_bind") @ResourceAction(id = "bind-user", name = "绑定用户") - @Operation(summary = "绑定用户到机构") - public Mono bindUser(@Parameter(description = "机构ID") @PathVariable String id, + @Operation(summary = "绑定用户") + public Mono bindUser(@Parameter(description = "组织ID") @PathVariable String id, @Parameter(description = "用户ID") @RequestBody Mono> userId) { @@ -97,10 +101,11 @@ public class OrganizationController implements ReactiveServiceCrudController unbindUser(@Parameter(description = "机构ID") @PathVariable String id, + @Operation(summary = "解绑用户") + public Mono unbindUser(@Parameter(description = "组织ID") @PathVariable String id, @Parameter(description = "用户ID") @RequestBody Mono> userId) { return userId.flatMap(list -> organizationService.unbindUser(id, list)); diff --git a/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/web/PermissionController.java b/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/web/PermissionController.java index 2a75c76e..b8076cd4 100644 --- a/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/web/PermissionController.java +++ b/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/web/PermissionController.java @@ -38,19 +38,16 @@ public class PermissionController { @QueryAction @Operation(summary = "验证权限ID是否合法") public Mono permissionIdValidate2(@RequestParam @Parameter(description = "权限ID") String id) { - return LocaleUtils.currentReactive() - .flatMap(locale -> { - PermissionEntity entity = new PermissionEntity(); - entity.setId(id); - entity.tryValidate("id", CreateGroup.class); + PermissionEntity entity = new PermissionEntity(); + entity.setId(id); + entity.tryValidate("id", CreateGroup.class); - return permissionService - .findById(id) - .map(permission -> ValidationResult - .error(LocaleUtils.resolveMessage("error.id_already_exists", locale))); - }) + return permissionService + .findById(id) + .flatMap(permission -> LocaleUtils.resolveMessageReactive("error.id_already_exists")) + .map(ValidationResult::error) .defaultIfEmpty(ValidationResult.success()) .onErrorResume(ValidationException.class, e -> Mono.just(e.getI18nCode()) - .map(ValidationResult::error)); + .map(ValidationResult::error)); } } diff --git a/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/web/RoleController.java b/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/web/RoleController.java index d00c843e..b34028b0 100644 --- a/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/web/RoleController.java +++ b/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/web/RoleController.java @@ -4,9 +4,13 @@ import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.AllArgsConstructor; import lombok.Getter; +import org.hswebframework.web.api.crud.entity.PagerResult; +import org.hswebframework.web.api.crud.entity.QueryParamEntity; +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.jetlinks.community.auth.entity.RoleDetailInfo; import org.jetlinks.community.auth.entity.RoleEntity; import org.jetlinks.community.auth.service.RoleService; import org.springframework.web.bind.annotation.*; diff --git a/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/web/RoleGroupController.java b/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/web/RoleGroupController.java index 5c3e8200..6f0110b2 100644 --- a/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/web/RoleGroupController.java +++ b/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/web/RoleGroupController.java @@ -6,8 +6,8 @@ import io.swagger.v3.oas.annotations.tags.Tag; import lombok.AllArgsConstructor; import lombok.Getter; import org.hswebframework.web.api.crud.entity.QueryParamEntity; +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.ReactiveServiceCrudController; import org.jetlinks.community.auth.entity.RoleGroupEntity; @@ -29,7 +29,7 @@ public class RoleGroupController implements ReactiveServiceCrudController queryDetailTree(@RequestParam(defaultValue = "false") @Parameter(description = "true:query为角色条件,false:query为分组条件") boolean queryByRole, @RequestBody Mono query) { @@ -37,8 +37,6 @@ public class RoleGroupController implements ReactiveServiceCrudController roleGroupService.queryDetailTree(tp2.getT2(), tp2.getT1())); - - } @Override diff --git a/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/web/ThirdPartyUserController.java b/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/web/ThirdPartyUserController.java index 915fcf77..d68392fd 100644 --- a/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/web/ThirdPartyUserController.java +++ b/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/web/ThirdPartyUserController.java @@ -3,7 +3,6 @@ package org.jetlinks.community.auth.web; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.AllArgsConstructor; -import org.hswebframework.ezorm.rdb.mapping.ReactiveRepository; import org.hswebframework.web.authorization.Authentication; import org.hswebframework.web.authorization.annotation.Authorize; import org.hswebframework.web.authorization.annotation.QueryAction; @@ -24,11 +23,8 @@ import reactor.core.publisher.Mono; @Tag(name = "第三方用户") public class ThirdPartyUserController { - private final ThirdPartyUserBindService thirdPartyUserBindService; - private final ReactiveRepository repository; - private final UserBindService userBindService; @PatchMapping("/{type}/{provider}") @@ -48,32 +44,26 @@ public class ThirdPartyUserController { @PathVariable String provider, @RequestBody(required = false) Flux requestFlux) { - return requestFlux - .map(request -> { - ThirdPartyUserBindEntity entity = new ThirdPartyUserBindEntity(); - entity.setType(type); - entity.setProvider(provider); - entity.setThirdPartyUserId(request.getThirdPartyUserId()); - entity.setUserId(request.getUserId()); - entity.setProviderName(request.getProviderName()); - entity.generateId(); - return entity; - }) - .as(repository::save) + Flux cache = requestFlux.cache(); + return cache + .mapNotNull(ThirdPartyBindUserInfo::getId) + .as(thirdPartyUserBindService::deleteById) + .flatMap(ignore -> cache + .map(request -> { + ThirdPartyUserBindEntity entity = new ThirdPartyUserBindEntity(); + entity.setType(type); + entity.setProvider(provider); + entity.setThirdPartyUserId(request.getThirdPartyUserId()); + entity.setUserId(request.getUserId()); + entity.setProviderName(request.getProviderName()); + entity.generateId(); + return entity; + }) + .as(thirdPartyUserBindService::save)) .then(); } - @PostMapping("/{id}/_unbind") - @Operation(summary = "解绑用户") - @SaveAction - public Mono unbind(@PathVariable String id) { - - return repository - .deleteById(id) - .then(); - } - @PostMapping("/me/{type}/{provider}/{bindCode}/_bind") @Operation(summary = "根据绑定码绑定当前用户") @Authorize(merge = false) @@ -96,24 +86,33 @@ public class ThirdPartyUserController { entity.generateId(); return entity; })) - .as(thirdPartyUserBindService::save) - .then(); + .as(thirdPartyUserBindService::save) + .then(); } + @PostMapping("/{id}/_unbind") + @Operation(summary = "解绑用户") + @SaveAction + public Mono unbind(@PathVariable String id) { + + return thirdPartyUserBindService + .deleteById(id) + .then(); + } + @GetMapping("/{type}/{provider}") @Operation(summary = "获取绑定信息") @QueryAction public Flux queryBindings(@PathVariable String type, @PathVariable String provider) { - return repository + return thirdPartyUserBindService .createQuery() .where(ThirdPartyUserBindEntity::getType, type) .and(ThirdPartyUserBindEntity::getProvider, provider) .fetch() - .map(bind -> ThirdPartyBindUserInfo.of( - bind.getId(), bind.getUserId(), bind.getProviderName(), bind.getThirdPartyUserId())); + .map(bind -> ThirdPartyBindUserInfo.of(bind.getId(), bind.getUserId(), bind.getProviderName(), bind.getThirdPartyUserId())); } @GetMapping("/me") @@ -122,7 +121,7 @@ public class ThirdPartyUserController { public Flux getCurrentUserBindings() { return Authentication .currentReactive() - .flatMapMany(auth -> repository + .flatMapMany(auth -> thirdPartyUserBindService .createQuery() .where(ThirdPartyUserBindEntity::getUserId, auth.getUser().getId()) .fetch()); @@ -134,7 +133,7 @@ public class ThirdPartyUserController { public Mono deleteBinding(@PathVariable String bindingId) { return Authentication .currentReactive() - .flatMap(auth -> repository + .flatMap(auth -> thirdPartyUserBindService .createDelete() .where(ThirdPartyUserBindEntity::getUserId, auth.getUser().getId()) .and(ThirdPartyUserBindEntity::getId, bindingId) @@ -142,4 +141,5 @@ public class ThirdPartyUserController { ) .then(); } + } diff --git a/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/web/UserDetailController.java b/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/web/UserDetailController.java index c82305e6..99b09822 100755 --- a/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/web/UserDetailController.java +++ b/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/web/UserDetailController.java @@ -1,6 +1,7 @@ package org.jetlinks.community.auth.web; import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.AllArgsConstructor; import org.hswebframework.web.api.crud.entity.PagerResult; @@ -11,6 +12,7 @@ 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.authorization.exception.UnAuthorizedException; +import org.hswebframework.web.system.authorization.defaults.service.DefaultDimensionUserService; import org.jetlinks.community.auth.entity.UserDetail; import org.jetlinks.community.auth.enums.UserEntityType; import org.jetlinks.community.auth.enums.UserEntityTypes; @@ -30,6 +32,7 @@ import reactor.core.publisher.Mono; public class UserDetailController { private final UserDetailService userDetailService; + private final DefaultDimensionUserService dimensionUserService; @PostMapping("/_create") @SaveAction @@ -55,7 +58,7 @@ public class UserDetailController { } @GetMapping("/{userId}") - @SaveAction + @QueryAction @Operation(summary = "获取用户详情信息") public Mono getUserDetail(@PathVariable String userId) { return userDetailService.findUserDetail(userId); @@ -79,7 +82,7 @@ public class UserDetailController { public Mono getCurrentLoginUserDetail() { return Authentication .currentReactive() - .switchIfEmpty(Mono.error(UnAuthorizedException::new)) + .switchIfEmpty(Mono.error(UnAuthorizedException.NoStackTrace::new)) .flatMap(autz -> userDetailService .findUserDetail(autz.getUser().getId()) .switchIfEmpty(Mono.fromSupplier(() -> new UserDetail().with(autz))) @@ -95,16 +98,17 @@ public class UserDetailController { @Operation(summary = "保存当前用户详情") @Authorize(merge = false) public Mono saveUserDetail(@RequestBody Mono request) { + // todo 个人资料的权限校验 return Authentication .currentReactive() .zipWith(request) - .switchIfEmpty(Mono.error(UnAuthorizedException::new)) - .flatMap(tp2 -> userDetailService.saveUserDetail(tp2.getT1().getUser().getId(), tp2.getT2())); + .switchIfEmpty(Mono.error(UnAuthorizedException.NoStackTrace::new)) + .flatMap(tp2 -> userDetailService + .saveUserDetail(tp2.getT1().getUser().getId(), tp2.getT2())); } @GetMapping("/types") @Operation(summary = "获取所有用户类型") - @Authorize(merge = false) public Flux getUserEntityTypes() { return Flux.fromIterable(UserEntityTypes.getAllType()); } diff --git a/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/web/WebFluxUserController.java b/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/web/WebFluxUserController.java index 9a53b95e..b995e9b9 100644 --- a/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/web/WebFluxUserController.java +++ b/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/web/WebFluxUserController.java @@ -6,9 +6,11 @@ import org.apache.commons.lang3.StringUtils; import org.hswebframework.web.authorization.Authentication; import org.hswebframework.web.authorization.User; import org.hswebframework.web.authorization.annotation.Authorize; +import org.hswebframework.web.authorization.annotation.ResourceAction; import org.hswebframework.web.authorization.annotation.SaveAction; import org.hswebframework.web.exception.ValidationException; import org.hswebframework.web.i18n.LocaleUtils; +import org.hswebframework.web.logging.AccessLogger; import org.hswebframework.web.system.authorization.api.PasswordValidator; import org.hswebframework.web.system.authorization.api.UsernameValidator; import org.hswebframework.web.system.authorization.api.entity.UserEntity; @@ -18,6 +20,7 @@ import org.jetlinks.community.web.response.ValidationResult; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; import reactor.core.publisher.Mono; @@ -40,6 +43,7 @@ public class WebFluxUserController extends org.hswebframework.web.system.authori @PostMapping("/{id}/password/_reset") @SaveAction + @AccessLogger(ignoreParameter = {"password"}) @Operation(summary = "重置密码") public Mono resetPassword(@PathVariable String id, @RequestBody String password) { @@ -159,6 +163,17 @@ public class WebFluxUserController extends org.hswebframework.web.system.authori .then(super.getById(id)); } + @PutMapping("/passwd") + @ResourceAction(id = "update-self-pwd",name = "修改当前用户密码") + @Operation(summary = "修改当前用户密码") + @Override + @Authorize(merge = false) + @AccessLogger(ignoreParameter = {"request"}) + public Mono changePassword(@RequestBody ChangePasswordRequest request) { + return super.changePassword(request); + } + + private Mono assertUserPermission(String userId) { return Mono.empty(); } diff --git a/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/web/request/AuthorizationSettingDetail.java b/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/web/request/AuthorizationSettingDetail.java index 666bd953..42b1df20 100755 --- a/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/web/request/AuthorizationSettingDetail.java +++ b/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/web/request/AuthorizationSettingDetail.java @@ -6,8 +6,11 @@ import lombok.*; import org.apache.commons.collections.CollectionUtils; import org.hswebframework.web.authorization.Dimension; import org.hswebframework.web.authorization.access.DataAccessConfig; +import org.hswebframework.web.system.authorization.api.entity.ActionEntity; import org.hswebframework.web.system.authorization.api.entity.AuthorizationSettingEntity; import org.hswebframework.web.system.authorization.api.entity.DataAccessEntity; +import org.hswebframework.web.system.authorization.api.entity.PermissionEntity; +import org.springframework.util.StringUtils; import javax.validation.constraints.NotBlank; import java.util.*; @@ -90,6 +93,9 @@ public class AuthorizationSettingDetail { @Schema(description = "权限ID") private String id; + @Schema(description = "权限名称") + private String name; + /** * 授权操作 */ @@ -108,6 +114,15 @@ public class AuthorizationSettingDetail { @Hidden private List dataAccess; + /** + * 是否来自外部菜单的权限 + */ + @Hidden + private boolean external; + + @Schema(description = "额外配置") + private Map options; + private PermissionInfo unwrap(AuthorizationSettingEntity entity) { this.id = entity.getPermission(); this.actions = entity.getActions(); @@ -181,6 +196,23 @@ public class AuthorizationSettingDetail { entity.setDataAccesses(entities); } + public PermissionEntity toPermissionEntity() { + PermissionEntity permissionEntity = new PermissionEntity(); + permissionEntity.setId(id); + permissionEntity.setName(StringUtils.hasText(name) ? name : id); + List actionEntityList = getActions() + .stream() + .map(action -> { + ActionEntity actionEntity = new ActionEntity(); + actionEntity.setAction(action); + return actionEntity; + }) + .collect(Collectors.toList()); + + permissionEntity.setActions(actionEntityList); + return permissionEntity; + } + public static PermissionInfo of(String id, Collection actions) { PermissionInfo info = new PermissionInfo(); info.setId(id); @@ -195,6 +227,7 @@ public class AuthorizationSettingDetail { */ @Getter @Setter + @Generated public static class DataAccess { /** @@ -249,6 +282,7 @@ public class AuthorizationSettingDetail { @Setter @AllArgsConstructor @NoArgsConstructor + @Generated public static class FieldAccess { /** diff --git a/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/web/response/RoleGroupDetailTree.java b/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/web/response/RoleGroupDetailTree.java index ac9f9f9d..238e889a 100644 --- a/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/web/response/RoleGroupDetailTree.java +++ b/jetlinks-manager/authentication-manager/src/main/java/org/jetlinks/community/auth/web/response/RoleGroupDetailTree.java @@ -8,6 +8,7 @@ import org.jetlinks.community.auth.entity.RoleEntity; import org.jetlinks.community.auth.entity.RoleGroupEntity; import java.util.List; +import java.util.Locale; /** * @author gyl @@ -31,5 +32,12 @@ public class RoleGroupDetailTree { return roleGroupDetailTree; } + public static RoleGroupDetailTree of(RoleGroupEntity group, Locale locale) { + RoleGroupDetailTree roleGroupDetailTree = new RoleGroupDetailTree(); + roleGroupDetailTree.setGroupId(group.getId()); + roleGroupDetailTree.setGroupName(group.getI18nName(locale)); + return roleGroupDetailTree; + } + } diff --git a/jetlinks-manager/authentication-manager/src/main/resources/i18n/authentication-manager/messages_en.properties b/jetlinks-manager/authentication-manager/src/main/resources/i18n/authentication-manager/messages_en.properties index b10a09f8..3a424ac0 100644 --- a/jetlinks-manager/authentication-manager/src/main/resources/i18n/authentication-manager/messages_en.properties +++ b/jetlinks-manager/authentication-manager/src/main/resources/i18n/authentication-manager/messages_en.properties @@ -5,12 +5,24 @@ org.jetlinks.community.auth.enums.TenantMemberState.disabled=Disabled org.jetlinks.community.auth.enums.TenantState.enabled=Normal org.jetlinks.community.auth.enums.TenantState.disabled=Disabled +org.jetlinks.community.auth.enums.DefaultUserEntityType.ADMIN=Administrator +org.jetlinks.community.auth.enums.DefaultUserEntityType.USER=Ordinary User +org.jetlinks.community.auth.enums.DefaultUserEntityType.APPLICATION=Third party User +org.jetlinks.community.auth.enums.DefaultUserEntityType.OTHER=Other + +org.jetlinks.community.auth.enums.AccessSupportState.support=Support +org.jetlinks.community.auth.enums.AccessSupportState.unsupported=Unsupported +org.jetlinks.community.auth.enums.AccessSupportState.indirect=Indirect + #error -error.password_length=The password length must not be less than{0} +error.password_length=Please enter {0}-{1} bit password +error.unsupported_register=unsupported register error.insufficient_password_strength=The password strength not enough error.password_not_correct=The password is not correct error.verification_code=Verification code error +error.verification_code_expired=Verification code expired +error.unsupported_verification=unsupported verification error.user_already_exist=User {0} already exists error.user_cannot_be_empty=Username cannot be empty error.userId_cannot_be_null=userId can not be null @@ -19,5 +31,55 @@ error.tenant_member_information_cannot_delete=Member information exists under th error.unsupported_notify=unsupported notify error.unsupported_provider=unsupported:{0} +error.user_create=User create error + error.dingtalk_inside_app_permission_denied=The dingtalk app permission denied,Please grant app [{0}] [\u901A\u8BAF\u5F55\u4E2A\u4EBA\u4FE1\u606F\u8BFB\u6743\u9650] \ - permission \ No newline at end of file + permission +error.access_can_not_be_granted=Please select menu [{0}] DataAccess. +error.id_already_exists=The ID already exists +error.menu_code_already_exists=The code of menu already exists +error.application_notfound=Application [{0}] not found. +error.user.login.blocked=Please try again later! +error.group_role_exists=The group has roles +error.unsupported_types_for_request_menu=Please check the configuration information +error.menu_data_exists=Menu data already exists +error.request_menus_empty=Menu data returned as empty +error.api_grant_permission_denied=Denied operation, insufficient user permissions +error.app_id_can_not_null=The application ID cannot be empty +error.integration_mode_not_support=Integration method not supported +error.exists_subordinate_position=Exists subordinate position and cannot be deleted + +message.authentication.application.group.dict.name=application group +message.authentication.dict.item.internal_group=internal group + +# Permission and Action i18n +hswebframework.web.system.permission.autz-setting=Permission Assignment +hswebframework.web.system.permission.api_spec=API Management +hswebframework.web.system.permission.permission=Permission Management +hswebframework.web.system.permission.menu=Menu Management +hswebframework.web.system.permission.relation=Relationship Management +hswebframework.web.system.permission.role=Role Management +hswebframework.web.system.permission.dictionary=Data Dictionary +hswebframework.web.system.permission.role-group=Role Group Management +hswebframework.web.system.permission.user=System User +hswebframework.web.system.permission.api_group=API Group Management +hswebframework.web.system.permission.user-third-party-manager=Third-party User +hswebframework.web.system.permission.open-api=API Client Management +hswebframework.web.system.permission.dimension=Permission Dimension Management +hswebframework.web.system.permission.application=Application Management +hswebframework.web.system.permission.user-token=User Token Information Management +hswebframework.web.system.permission.organization=Organization Management + + +hswebframework.web.system.action.query=Query +hswebframework.web.system.action.save=Save +hswebframework.web.system.action.delete=Delete +hswebframework.web.system.action.grant=Grant +hswebframework.web.system.action.grant_query=Query Grant +hswebframework.web.system.action.bind-user=Bind User +hswebframework.web.system.action.unbind-user=Unbind User +hswebframework.web.system.action.update-self-info=Update Current User Info + +message.menu.access.unsupported.description=This menu does not support data permission control +message.menu.access.indirect.description=This menu uses [{0}] to support data permission control +message.menu.access.support.description=This menu support data permission control diff --git a/jetlinks-manager/authentication-manager/src/main/resources/i18n/authentication-manager/messages_zh.properties b/jetlinks-manager/authentication-manager/src/main/resources/i18n/authentication-manager/messages_zh.properties index 70dc4ade..f9f43afc 100644 --- a/jetlinks-manager/authentication-manager/src/main/resources/i18n/authentication-manager/messages_zh.properties +++ b/jetlinks-manager/authentication-manager/src/main/resources/i18n/authentication-manager/messages_zh.properties @@ -5,12 +5,19 @@ org.jetlinks.community.auth.enums.TenantMemberState.TenantMemberState.disabled=\ org.jetlinks.community.auth.enums.TenantState.enabled=\u6B63\u5E38 org.jetlinks.community.auth.enums.TenantState.disabled=\u5DF2\u7981\u7528 +org.jetlinks.community.auth.enums.DefaultUserEntityType.ADMIN=\u8D85\u7EA7\u7BA1\u7406\u5458 +org.jetlinks.community.auth.enums.DefaultUserEntityType.USER=\u666E\u901A\u7528\u6237 +org.jetlinks.community.auth.enums.DefaultUserEntityType.APPLICATION=\u7B2C\u4E09\u65B9\u7528\u6237 +org.jetlinks.community.auth.enums.DefaultUserEntityType.OTHER=\u5176\u4ED6 #error -error.password_length=\u5BC6\u7801\u957F\u5EA6\u4E0D\u80FD\u4F4E\u4E8E{0} -error.insufficient_password_strength=\u5BC6\u7801\u5F3A\u5EA6\u4E0D\u591F -error.password_not_correct=\u5bc6\u7801\u9519\u8bef -error.verification_code=\u9A8C\u8BC1\u7801\u9519\u8BEF +error.password_length=\u8BF7\u8F93\u5165{0}~{1}\u4F4D\u7684\u5BC6\u7801 +error.unsupported_register=\u6CE8\u518C\u529F\u80FD\u672A\u5F00\u542F +error.insufficient_password_strength=\u5BC6\u7801\u5FC5\u987B\u5305\u542B\u5927\u5C0F\u82F1\u6587\u3001\u6570\u5B57 +error.password_not_correct=\u5BC6\u7801\u9519\u8BEF +error.verification_code=\u9A8C\u8BC1\u7801\u9519\u8BEF +error.verification_code_expired=\u9A8C\u8BC1\u7801\u5DF2\u8FC7\u671F +error.unsupported_verification=\u4E0D\u652F\u6301\u7684\u9A8C\u8BC1\u65B9\u5F0F error.user_already_exist=\u7528\u6237{0}\u5DF2\u5B58\u5728 error.user_cannot_be_empty=\u7528\u6237\u540D\u4E0D\u80FD\u4E3A\u7A7A error.userId_cannot_be_null=userId\u4E0D\u80FD\u4E3A\u7A7A @@ -19,4 +26,56 @@ error.tenant_member_information_cannot_delete=\u79DF\u6237\u4E0B\u5B58\u5728\u62 error.unsupported_notify=\u4E0D\u652F\u6301\u7684\u901A\u77E5 error.unsupported_provider=\u4E0D\u652F\u6301:{0} -error.dingtalk_inside_app_permission_denied=\u9489\u9489\u4F01\u4E1A\u5185\u90E8\u5E94\u7528\u6743\u9650\u4E0D\u8DB3,\u8BF7\u5F00\u901A\u5E94\u7528[{0}]\u7684[\u901A\u8BAF\u5F55\u4E2A\u4EBA\u4FE1\u606F\u8BFB\u6743\u9650]\u6743\u9650 \ No newline at end of file +error.dingtalk_inside_app_permission_denied=\u9489\u9489\u4F01\u4E1A\u5185\u90E8\u5E94\u7528\u6743\u9650\u4E0D\u8DB3,\u8BF7\u5F00\u901A\u5E94\u7528[{0}]\u7684[\u901A\u8BAF\u5F55\u4E2A\u4EBA\u4FE1\u606F\u8BFB\u6743\u9650]\u6743\u9650 +error.access_can_not_be_granted=\u8BF7\u9009\u62E9\u83DC\u5355[{0}]\u7684\u6570\u636E\u6743\u9650 +error.id_already_exists=\u8BE5\u6807\u8BC6\u5DF2\u5B58\u5728 +error.menu_code_already_exists=\u7F16\u7801\u5DF2\u5B58\u5728 +error.application_notfound=\u5E94\u7528[{0}]\u4E0D\u5B58\u5728 +error.user.login.blocked=\u5BC6\u7801\u9519\u8BEF\u6B21\u6570\u8FC7\u591A,\u8BF7\u7A0D\u540E\u91CD\u8BD5! +error.group_role_exists=\u5206\u7EC4\u4E0B\u4ECD\u5B58\u5728\u89D2\u8272 +error.unsupported_types_for_request_menu=\u4E0D\u652F\u6301\u67E5\u8BE2\u83DC\u5355\u7684\u5E94\u7528\u7C7B\u578B\uFF0C\u8BF7\u68C0\u67E5\u914D\u7F6E\u4FE1\u606F +error.menu_data_exists=\u83DC\u5355\u6570\u636E\u5DF2\u5B58\u5728 +error.request_menus_empty=\u83DC\u5355\u6570\u636E\u8FD4\u56DE\u4E3A\u7A7A +error.api_grant_permission_denied=\u62D2\u7EDD\u64CD\u4F5C\uFF0C\u7528\u6237\u6743\u9650\u4E0D\u8DB3 +error.app_id_can_not_null=\u5E94\u7528id\u4E0D\u80FD\u4E3A\u7A7A +error.integration_mode_not_support=\u96C6\u6210\u65B9\u5F0F\u4E0D\u652F\u6301 + +error.exists_subordinate_position=\u5B58\u5728\u5173\u8054\u4E0B\u7EA7\u804C\u4F4D\uFF0C\u65E0\u6CD5\u5220\u9664 + +message.authentication.application.group.dict.name=\u5E94\u7528\u5206\u7EC4 +message.authentication.dict.item.internal_group=\u5185\u5EFA\u5E94\u7528 + +# \u6743\u9650\u3001\u6309\u94AE\u64CD\u4F5C\u56FD\u9645\u5316\u7FFB\u8BD1 +# \u6743\u9650\u4EE5hswebframework.web.system.permission.{\u6743\u9650id} +# \u6309\u94AE\u64CD\u4F5C\u4EE5hswebframework.web.system.action.{\u64CD\u4F5Cid} + +hswebframework.web.system.permission.autz-setting=\u6743\u9650\u5206\u914D +hswebframework.web.system.permission.api_spec=API\u7BA1\u7406 +hswebframework.web.system.permission.permission=\u6743\u9650\u7BA1\u7406 +hswebframework.web.system.permission.menu=\u83DC\u5355\u7BA1\u7406 +hswebframework.web.system.permission.relation=\u5173\u7CFB\u7BA1\u7406 +hswebframework.web.system.permission.role=\u89D2\u8272\u7BA1\u7406 +hswebframework.web.system.permission.dictionary=\u6570\u636E\u5B57\u5178 +hswebframework.web.system.permission.role-group=\u89D2\u8272\u7EC4\u7BA1\u7406 +hswebframework.web.system.permission.user=\u7CFB\u7EDF\u7528\u6237 +hswebframework.web.system.permission.api_group=API\u5206\u7EC4\u7BA1\u7406 +hswebframework.web.system.permission.user-third-party-manager=\u7B2C\u4E09\u65B9\u7528\u6237 +hswebframework.web.system.permission.open-api=API\u5BA2\u6237\u7AEF\u7BA1\u7406 +hswebframework.web.system.permission.dimension=\u6743\u9650\u7EF4\u5EA6\u7BA1\u7406 +hswebframework.web.system.permission.application=\u5E94\u7528\u7BA1\u7406 +hswebframework.web.system.permission.user-token=\u7528\u6237\u4EE4\u724C\u4FE1\u606F\u7BA1\u7406 +hswebframework.web.system.permission.organization=\u7EC4\u7EC7\u7BA1\u7406 + + +hswebframework.web.system.action.query=\u67E5\u8BE2 +hswebframework.web.system.action.save=\u4FDD\u5B58 +hswebframework.web.system.action.delete=\u5220\u9664 +hswebframework.web.system.action.grant=\u6388\u6743 +hswebframework.web.system.action.grant_query=\u67E5\u8BE2\u6388\u6743 +hswebframework.web.system.action.bind-user=\u7ED1\u5B9A\u7528\u6237 +hswebframework.web.system.action.unbind-user=\u89E3\u7ED1\u7528\u6237 +hswebframework.web.system.action.update-self-info=\u4FEE\u6539\u5F53\u524D\u7528\u6237\u4FE1\u606F + +message.menu.access.unsupported.description=\u6B64\u83DC\u5355\u4E0D\u652F\u6301\u6570\u636E\u6743\u9650\u63A7\u5236 +message.menu.access.indirect.description=\u6B64\u83DC\u5355\u4F7F\u7528[{0}]\u652F\u6301\u6570\u636E\u6743\u9650\u63A7\u5236 +message.menu.access.support.description=\u6B64\u83DC\u5355\u652F\u6301\u6570\u636E\u6743\u9650\u63A7\u5236 \ No newline at end of file diff --git a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/configuration/DeviceEventProperties.java b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/configuration/DeviceEventProperties.java new file mode 100644 index 00000000..671af13d --- /dev/null +++ b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/configuration/DeviceEventProperties.java @@ -0,0 +1,15 @@ +package org.jetlinks.community.device.configuration; + +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * @author wangsheng + */ +@Getter +@Setter +@ConfigurationProperties(prefix = "jetlinks.device.event-handler") +public class DeviceEventProperties{ + boolean offlineWhenProductDisabled; +} \ No newline at end of file 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 dcd4d287..cbcb993e 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 @@ -15,31 +15,24 @@ 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.jetlinks.core.things.ThingsRegistry; +import org.springframework.boot.autoconfigure.AutoConfiguration; 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 org.springframework.context.annotation.DependsOn; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; import java.time.Duration; @Configuration -@EnableConfigurationProperties(DeviceDataStorageProperties.class) +@EnableConfigurationProperties({DeviceDataStorageProperties.class, DeviceEventProperties.class}) public class DeviceManagerConfiguration { - - @Bean - public DeviceSelectorProvider relationSelectorProvider() { - return new RelationDeviceSelectorProvider(); - } - - @Bean - public DeviceSelectorBuilder deviceSelectorBuilder(ReactiveRepository deviceRepository, - DeviceRegistry deviceRegistry) { - return new ReactorQLDeviceSelectorBuilder(deviceRegistry, deviceRepository); - } - @Bean public DeviceMessageConnector deviceMessageConnector(EventBus eventBus, MessageHandler messageHandler, @@ -48,14 +41,25 @@ public class DeviceManagerConfiguration { return new DeviceMessageConnector(eventBus, registry, messageHandler, sessionManager); } + @Bean + public DeviceSelectorProvider relationSelectorProvider() { + return new RelationDeviceSelectorProvider(); + } + + + @Bean + public DeviceSelectorBuilder deviceSelectorBuilder(ReactiveRepository deviceRepository, + DeviceRegistry deviceRegistry) { + return new ReactorQLDeviceSelectorBuilder(deviceRegistry, deviceRepository); + } + @Bean @ConditionalOnProperty(prefix = "device.message.writer.time-series", name = "enabled", havingValue = "true", matchIfMissing = true) public TimeSeriesMessageWriterConnector timeSeriesMessageWriterConnector(DeviceDataService dataService) { return new TimeSeriesMessageWriterConnector(dataService); } - - @Configuration + @AutoConfiguration @ConditionalOnProperty(prefix = "jetlinks.device.storage", name = "enable-last-data-in-db", havingValue = "true") static class DeviceLatestDataServiceConfiguration { @@ -88,7 +92,4 @@ public class DeviceManagerConfiguration { public DeviceLatestDataService deviceLatestDataService() { return new NonDeviceLatestDataService(); } - - - } diff --git a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/entity/DeviceCategoryEntity.java b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/entity/DeviceCategoryEntity.java index 8ae16e08..a22a073b 100755 --- a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/entity/DeviceCategoryEntity.java +++ b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/entity/DeviceCategoryEntity.java @@ -6,10 +6,12 @@ import lombok.Setter; import org.hswebframework.ezorm.rdb.mapping.annotation.ColumnType; import org.hswebframework.ezorm.rdb.mapping.annotation.Comment; import org.hswebframework.ezorm.rdb.mapping.annotation.DefaultValue; +import org.hswebframework.ezorm.rdb.mapping.annotation.JsonCodec; import org.hswebframework.web.api.crud.entity.GenericTreeSortSupportEntity; 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.i18n.MultipleI18nSupportEntity; import org.hswebframework.web.validator.CreateGroup; import javax.persistence.Column; @@ -20,13 +22,14 @@ import javax.validation.constraints.NotBlank; import javax.validation.constraints.Pattern; import java.sql.JDBCType; import java.util.List; +import java.util.Map; @Getter @Setter @Table(name = "dev_product_category") @Comment("产品分类信息表") @EnableEntityEvent -public class DeviceCategoryEntity extends GenericTreeSortSupportEntity implements RecordCreationEntity { +public class DeviceCategoryEntity extends GenericTreeSortSupportEntity implements RecordCreationEntity, MultipleI18nSupportEntity { @Override @Id @@ -76,4 +79,14 @@ public class DeviceCategoryEntity extends GenericTreeSortSupportEntity i , accessMode = Schema.AccessMode.READ_ONLY ) private Long createTime; + + @Schema(title = "国际化信息定义") + @Column + @JsonCodec + @ColumnType(jdbcType = JDBCType.LONGVARCHAR, javaType = String.class) + private Map> i18nMessages; + + public String getI18nName() { + return getI18nMessage("name", name); + } } 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/entity/DeviceDetail.java similarity index 97% rename from jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/response/DeviceDetail.java rename to jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/entity/DeviceDetail.java index ba5ad082..d385d3b8 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/entity/DeviceDetail.java @@ -1,13 +1,9 @@ -package org.jetlinks.community.device.response; +package org.jetlinks.community.device.entity; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Getter; import lombok.Setter; 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; diff --git a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/entity/DeviceEvent.java b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/entity/DeviceEvent.java index 7259535e..011aafb8 100644 --- a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/entity/DeviceEvent.java +++ b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/entity/DeviceEvent.java @@ -7,6 +7,7 @@ import lombok.Setter; import org.jetlinks.core.metadata.DataType; import org.jetlinks.core.metadata.EventMetadata; import org.jetlinks.core.metadata.types.ObjectType; +import org.jetlinks.community.things.data.ThingEvent; import java.util.HashMap; import java.util.Map; @@ -35,6 +36,12 @@ public class DeviceEvent extends HashMap { super(data); } + public DeviceEvent(ThingEvent data) { + super(data); + putIfAbsent("deviceId", data.getThingId()); + } + + @SuppressWarnings("all") public void putFormat(EventMetadata metadata) { if (metadata != null) { diff --git a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/entity/DeviceInstanceEntity.java b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/entity/DeviceInstanceEntity.java index 7aa74335..cc2fd3a0 100755 --- a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/entity/DeviceInstanceEntity.java +++ b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/entity/DeviceInstanceEntity.java @@ -5,6 +5,7 @@ import lombok.Getter; import lombok.Setter; import org.apache.commons.collections.MapUtils; import org.hswebframework.ezorm.rdb.mapping.annotation.*; +import org.hswebframework.web.api.crud.entity.EntityFactoryHolder; import org.hswebframework.web.api.crud.entity.GenericEntity; import org.hswebframework.web.api.crud.entity.RecordCreationEntity; import org.hswebframework.web.api.crud.entity.RecordModifierEntity; @@ -17,13 +18,18 @@ import org.jetlinks.core.device.DeviceConfigKey; import org.jetlinks.core.device.DeviceInfo; import org.jetlinks.core.metadata.DeviceMetadata; import org.jetlinks.core.metadata.MergeOption; +import org.jetlinks.core.metadata.PropertyMetadata; +import org.jetlinks.core.metadata.SimplePropertyMetadata; +import org.jetlinks.core.metadata.types.EnumType; +import org.jetlinks.core.metadata.types.ObjectType; +import org.jetlinks.core.metadata.types.StringType; import org.jetlinks.community.PropertyConstants; import org.jetlinks.community.device.enums.DeviceFeature; import org.jetlinks.community.device.enums.DeviceState; import org.jetlinks.community.device.enums.DeviceType; import org.jetlinks.supports.official.JetLinksDeviceMetadataCodec; import org.springframework.util.CollectionUtils; -import org.springframework.util.StringUtils; +import org.springframework.util.ObjectUtils; import reactor.core.publisher.Mono; import javax.persistence.Column; @@ -75,7 +81,7 @@ public class DeviceInstanceEntity extends GenericEntity implements Recor @Schema(description = "说明") private String describe; - @Column(name = "product_id", length = 64) + @Column(name = "product_id", length = 64, updatable = false) @NotBlank(message = "产品ID不能为空", groups = CreateGroup.class) @Schema(description = "产品ID") private String productId; @@ -203,13 +209,10 @@ public class DeviceInstanceEntity extends GenericEntity implements Recor if (!CollectionUtils.isEmpty(configuration)) { info.addConfigs(configuration); } - if (StringUtils.hasText(deriveMetadata)) { - info.addConfig(DeviceConfigKey.metadata, deriveMetadata); - } info.addConfig(DeviceConfigKey.parentGatewayId, this.getParentId()); info.addConfig(PropertyConstants.deviceName, name); info.addConfig(PropertyConstants.productName, productName); - info.addConfig(PropertyConstants.creatorId,creatorId); + info.addConfig(PropertyConstants.creatorId, creatorId); if (hasFeature(DeviceFeature.selfManageState)) { info.addConfig(DeviceConfigKey.selfManageState, true); } @@ -264,7 +267,7 @@ public class DeviceInstanceEntity extends GenericEntity implements Recor public Mono mergeMetadata(DeviceMetadata metadata) { JetLinksDeviceMetadataCodec codec = JetLinksDeviceMetadataCodec.getInstance(); - if (StringUtils.isEmpty(this.getDeriveMetadata())) { + if (ObjectUtils.isEmpty(this.getDeriveMetadata())) { return codec.encode(metadata) .doOnNext(this::setDeriveMetadata); } @@ -283,7 +286,7 @@ public class DeviceInstanceEntity extends GenericEntity implements Recor if (this.features == null) { this.features = features; } - if (features.length > 0) { + else if (features.length > 0) { this.features = Stream .concat(Stream.of(this.features), Stream.of(features)) .toArray(DeviceFeature[]::new); @@ -311,4 +314,30 @@ public class DeviceInstanceEntity extends GenericEntity implements Recor public void validateId() { tryValidate(DeviceInstanceEntity::getId, CreateGroup.class); } + + public static DeviceInstanceEntity of(){ + return EntityFactoryHolder.newInstance(DeviceInstanceEntity.class,DeviceInstanceEntity::new); + } + + public static List createMetadata(){ + return Arrays.asList( + SimplePropertyMetadata.of("id", "设备id", StringType.GLOBAL), + SimplePropertyMetadata.of("name", "设备名称", StringType.GLOBAL), + SimplePropertyMetadata.of("deviceType", "设备类型", new EnumType() + .addElement(EnumType.Element.of("device", "直连设备")) + .addElement(EnumType.Element.of("childrenDevice", "网关子设备")) + .addElement(EnumType.Element.of("gateway", "网关设备"))), + SimplePropertyMetadata.of("describe", "说明", StringType.GLOBAL), + SimplePropertyMetadata.of("productId", "产品id", StringType.GLOBAL), + SimplePropertyMetadata.of("productName", "产品名称", StringType.GLOBAL), + SimplePropertyMetadata.of("configuration", "配置", new ObjectType()), + SimplePropertyMetadata.of("state", "设备状态", new EnumType() + .addElement(EnumType.Element.of("notActive", "禁用")) + .addElement(EnumType.Element.of("offline", "离线")) + .addElement(EnumType.Element.of("online", "在线"))), + SimplePropertyMetadata.of("orgId", "机构id", StringType.GLOBAL), + SimplePropertyMetadata.of("parentId", "父设备id", StringType.GLOBAL), + SimplePropertyMetadata.of("deriveMetadata", "独立物模型", StringType.GLOBAL) + ); + } } 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 index 015d0e8a..3ac0fa92 100755 --- 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 @@ -4,6 +4,7 @@ import io.swagger.v3.oas.annotations.media.Schema; import java.util.HashMap; import java.util.Map; +import java.util.Optional; public class DeviceLatestData extends HashMap { @@ -24,9 +25,10 @@ public class DeviceLatestData extends HashMap { @Schema(description = "设备ID") public String getDeviceId(){ - return (String)get("deviceId"); + return (String)Optional.ofNullable(get("deviceId")).orElseGet(()->get("id")); } + @Schema(description = "设备名称") public String getDeviceName(){ return (String)get("deviceName"); diff --git a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/entity/DeviceMetadataMappingDetail.java b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/entity/DeviceMetadataMappingDetail.java new file mode 100644 index 00000000..d2a93c26 --- /dev/null +++ b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/entity/DeviceMetadataMappingDetail.java @@ -0,0 +1,76 @@ +package org.jetlinks.community.device.entity; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; +import org.jetlinks.core.metadata.PropertyMetadata; +import org.jetlinks.core.things.ThingMetadataType; + +import java.util.Map; + +@Getter +@Setter +public class DeviceMetadataMappingDetail { + + @Schema(description = "是否自定义了映射") + private boolean customMapping; + + @Schema(description = "产品ID") + private String productId; + + @Schema(description = "设备ID,为空时表示映射对产品下所有设备生效") + private String deviceId; + + @Schema(description = "物模型名称") + private String metadataName; + + @Schema(description = "物模型类型,如:property") + private ThingMetadataType metadataType; + + @Schema(description = "物模型ID,如:属性ID") + private String metadataId; + + @Schema(description = "原始物模型ID") + private String originalId; + + @Schema(description = "其他配置") + private Map others; + + @Schema(description = "说明") + private String description; + + public static DeviceMetadataMappingDetail ofProduct(String productId) { + DeviceMetadataMappingDetail detail = new DeviceMetadataMappingDetail(); + detail.setProductId(productId); + return detail; + } + + public static DeviceMetadataMappingDetail ofDevice(String productId, String deviceId) { + DeviceMetadataMappingDetail detail = ofProduct(productId); + detail.setDeviceId(deviceId); + return detail; + } + + public DeviceMetadataMappingDetail with(PropertyMetadata metadata) { + DeviceMetadataMappingDetail detail = new DeviceMetadataMappingDetail(); + detail.setMetadataId(metadata.getId()); + detail.setOriginalId(metadata.getId()); + detail.setMetadataName(metadata.getName()); + detail.setMetadataType(ThingMetadataType.property); + return detail; + } + + public DeviceMetadataMappingDetail with(DeviceMetadataMappingEntity mapping) { + if (null == mapping) { + return this; + } + this.customMapping = true; + this.description = mapping.getDescription(); + this.others = mapping.getOthers(); + this.originalId = mapping.getOriginalId(); + this.metadataId = mapping.getMetadataId(); + this.metadataType = mapping.getMetadataType(); + return this; + } + +} diff --git a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/entity/DeviceOperationLogEntity.java b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/entity/DeviceOperationLogEntity.java index 9a747255..1d39172f 100644 --- a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/entity/DeviceOperationLogEntity.java +++ b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/entity/DeviceOperationLogEntity.java @@ -1,7 +1,6 @@ package org.jetlinks.community.device.entity; import com.alibaba.fastjson.JSON; -import com.alibaba.fastjson.annotation.JSONField; import io.swagger.v3.oas.annotations.Hidden; import io.swagger.v3.oas.annotations.media.Schema; import lombok.*; @@ -11,7 +10,8 @@ import org.jetlinks.community.device.enums.DeviceLogType; import org.jetlinks.community.things.data.ThingMessageLog; import org.springframework.util.StringUtils; -import java.util.*; +import java.util.HashMap; +import java.util.Map; /** * @author bsetfeng diff --git a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/entity/DeviceProductEntity.java b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/entity/DeviceProductEntity.java index 37094761..fade55f7 100755 --- a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/entity/DeviceProductEntity.java +++ b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/entity/DeviceProductEntity.java @@ -17,10 +17,16 @@ import org.jetlinks.core.device.DeviceConfigKey; import org.jetlinks.core.device.ProductInfo; import org.jetlinks.core.message.codec.Transport; import org.jetlinks.core.metadata.DeviceMetadata; +import org.jetlinks.core.metadata.PropertyMetadata; import org.jetlinks.core.metadata.SimpleDeviceMetadata; +import org.jetlinks.core.metadata.SimplePropertyMetadata; +import org.jetlinks.core.metadata.types.EnumType; +import org.jetlinks.core.metadata.types.ObjectType; +import org.jetlinks.core.metadata.types.StringType; import org.jetlinks.community.PropertyConstants; import org.jetlinks.community.device.enums.DeviceType; import org.jetlinks.community.gateway.supports.DeviceGatewayProvider; +import org.jetlinks.community.things.data.ThingsDataConstants; import org.jetlinks.supports.official.JetLinksDeviceMetadataCodec; import org.springframework.util.StringUtils; @@ -31,9 +37,7 @@ import javax.persistence.Table; import javax.validation.constraints.NotBlank; import javax.validation.constraints.Pattern; import java.sql.JDBCType; -import java.util.Collection; -import java.util.Map; -import java.util.Optional; +import java.util.*; import static org.jetlinks.community.device.enums.DeviceType.gateway; @@ -139,6 +143,13 @@ public class DeviceProductEntity extends GenericEntity implements Record @DefaultValue(generator = Generators.CURRENT_TIME) private Long createTime; + @Column(name = "creator_name", updatable = false) + @Schema( + description = "创建者名称(只读)" + , accessMode = Schema.AccessMode.READ_ONLY + ) + private String creatorName; + @Column(name = "org_id", length = 64) @Schema(description = "机构ID") @Deprecated @@ -178,6 +189,10 @@ public class DeviceProductEntity extends GenericEntity implements Record @Schema(description = "修改时间") private Long modifyTime; + @Column(length = 64) + @Schema(description = "修改人名称") + private String modifierName; + public Optional getTransportEnum(Collection candidates) { for (Transport transport : candidates) { if (transport.isSame(transportProtocol)) { @@ -195,11 +210,13 @@ public class DeviceProductEntity extends GenericEntity implements Record .metadata(getMetadata()) .build() .addConfig(DeviceConfigKey.isGatewayDevice, getDeviceType() == gateway) - .addConfig("storePolicy", storePolicy) + .addConfig(ThingsDataConstants.storePolicyConfigKey, storePolicy) .addConfig("storePolicyConfiguration", storePolicyConfiguration) .addConfig("deviceType", deviceType == null ? "device" : deviceType.getValue()) .addConfig(PropertyConstants.accessId, accessId) + .addConfig(PropertyConstants.productName, name) .addConfig(PropertyConstants.accessProvider, accessProvider) + .addConfig(PropertyConstants.creatorId,creatorId) .addConfigs(configuration); } @@ -213,4 +230,34 @@ public class DeviceProductEntity extends GenericEntity implements Record public void validateId() { tryValidate(DeviceProductEntity::getId, CreateGroup.class); } + + public static List createMetadata(){ + return Arrays.asList( + SimplePropertyMetadata.of("id", "产品id", StringType.GLOBAL), + SimplePropertyMetadata.of("name", "产品名称", StringType.GLOBAL), + SimplePropertyMetadata.of("deviceType", "设备类型", new EnumType() + .addElement(EnumType.Element.of("device", "直连设备")) + .addElement(EnumType.Element.of("childrenDevice", "网关子设备")) + .addElement(EnumType.Element.of("gateway", "网关设备"))), + SimplePropertyMetadata.of("describe", "说明", StringType.GLOBAL), + SimplePropertyMetadata.of("projectId", "所属项目", StringType.GLOBAL), + SimplePropertyMetadata.of("projectName", "项目名称", StringType.GLOBAL), + SimplePropertyMetadata.of("classifiedId", "所属品类id", StringType.GLOBAL), + SimplePropertyMetadata.of("classifiedName", "所属品类名称", StringType.GLOBAL), + SimplePropertyMetadata.of("messageProtocol", "消息协议ID", StringType.GLOBAL), + SimplePropertyMetadata.of("protocolName", "消息协议名称", StringType.GLOBAL), + SimplePropertyMetadata.of("metadata", "物模型定义", StringType.GLOBAL), + SimplePropertyMetadata.of("transportProtocol", "传输协议", StringType.GLOBAL), + SimplePropertyMetadata.of("networkWay", "入网方式", StringType.GLOBAL), + SimplePropertyMetadata.of("accessId", "设备接入方式ID", StringType.GLOBAL), + SimplePropertyMetadata.of("accessName", "设备接入方式名称", StringType.GLOBAL), + SimplePropertyMetadata.of("accessProvider", "设备接入方式", StringType.GLOBAL), + SimplePropertyMetadata.of("storePolicy", "数据存储策略", StringType.GLOBAL), + SimplePropertyMetadata.of("configuration", "配置", new ObjectType()), + SimplePropertyMetadata.of("state", "产品状态", new EnumType() + .addElement(EnumType.Element.of("0", "禁用")) + .addElement(EnumType.Element.of("1", "离线"))), + SimplePropertyMetadata.of("orgId", "机构id", StringType.GLOBAL) + ); + } } diff --git a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/entity/DevicePropertiesEntity.java b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/entity/DevicePropertiesEntity.java index f68fdbf4..9f93a3fc 100644 --- a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/entity/DevicePropertiesEntity.java +++ b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/entity/DevicePropertiesEntity.java @@ -3,6 +3,7 @@ package org.jetlinks.community.device.entity; import com.alibaba.fastjson.JSON; import lombok.*; import org.hswebframework.web.bean.FastBeanCopier; +import org.hswebframework.web.exception.BusinessException; import org.jetlinks.core.metadata.DataType; import org.jetlinks.core.metadata.PropertyMetadata; import org.jetlinks.core.metadata.types.*; @@ -12,13 +13,12 @@ import java.util.Date; import java.util.HashMap; import java.util.Map; -import static java.util.Optional.ofNullable; - @Getter @Setter @Builder -@NoArgsConstructor @AllArgsConstructor +@NoArgsConstructor +@Generated public class DevicePropertiesEntity { private String id; @@ -43,8 +43,10 @@ public class DevicePropertiesEntity { private String value; + @Deprecated private String orgId; + @Deprecated private String productId; private Date timeValue; @@ -68,7 +70,7 @@ public class DevicePropertiesEntity { return this; } setProperty(metadata.getId()); - setPropertyName(metadata.getName()); +// setPropertyName(metadata.getName()); return withValue(metadata.getValueType(), value); } @@ -84,7 +86,7 @@ public class DevicePropertiesEntity { NumberType numberType = (NumberType) type; Number number = numberType.convertNumber(value); if (number == null) { - throw new UnsupportedOperationException("无法将" + value + "转为" + type.getId()); + throw new BusinessException("error.cannot_convert" , 500, value , type.getId()); } convertedValue = number.toString(); BigDecimal numberVal; @@ -122,9 +124,9 @@ public class DevicePropertiesEntity { } setValue(convertedValue); - ofNullable(type.format(value)) - .map(String::valueOf) - .ifPresent(this::setFormatValue); +// ofNullable(type.format(value)) +// .map(String::valueOf) +// .ifPresent(this::setFormatValue); return this; } diff --git a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/entity/DeviceProperty.java b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/entity/DeviceProperty.java index 44cff975..435c1cc3 100644 --- a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/entity/DeviceProperty.java +++ b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/entity/DeviceProperty.java @@ -66,6 +66,9 @@ public class DeviceProperty implements Serializable { @Schema(description = "属性值") private Object value; + @Schema(description = "原始值") + private Object originValue; + @Schema(description = "格式化值") private Object formatValue; @@ -86,6 +89,13 @@ public class DeviceProperty implements Serializable { @Schema(description = "状态值") private String state; + public Object getOriginValue(){ + if (originValue == null){ + return value; + } + return originValue; + } + public DeviceProperty deviceId(String deviceId) { this.deviceId = deviceId; return this; diff --git a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/entity/DeviceSaveDetail.java b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/entity/DeviceSaveDetail.java new file mode 100644 index 00000000..93c516d7 --- /dev/null +++ b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/entity/DeviceSaveDetail.java @@ -0,0 +1,80 @@ +package org.jetlinks.community.device.entity; + +import io.swagger.v3.oas.annotations.Hidden; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; +import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; + +import java.util.Collections; +import java.util.List; +import java.util.Map; + +@Getter +@Setter +public class DeviceSaveDetail { + + //设备ID + @Schema(defaultValue = "设备ID") + private String id; + + //设备名称 + @Schema(defaultValue = "设备名称") + private String name; + + //型号ID + @Schema(defaultValue = "产品ID") + private String productId; + + @Hidden + private String creatorId; + + @Hidden + private String creatorName; + + //设备配置信息 + @Schema(defaultValue = "配置信息") + private Map configuration; + + //标签 + @Schema(defaultValue = "标签信息") + private List tags; + + //父设备ID + @Schema(defaultValue = "父设备ID") + private String parentId; + + public List getTags() { + if (CollectionUtils.isEmpty(tags)) { + return Collections.emptyList(); + } + return tags; + } + + public DeviceInstanceEntity toInstance() { + DeviceInstanceEntity entity = DeviceInstanceEntity.of(); + + entity.setId(id); + entity.setName(name); + entity.setProductId(productId); + entity.setConfiguration(configuration); + entity.setCreatorId(creatorId); + entity.setCreatorName(creatorName); + entity.setCreateTimeNow(); + entity.setParentId(parentId); + return entity; + } + + public void prepare() { + for (DeviceTagEntity tag : getTags()) { + Assert.hasText(tag.getKey(), "tag.key不能为空"); + Assert.hasText(tag.getValue(), "tag.value不能为空"); + Assert.hasText(tag.getType(), "tag.type不能为空"); + + tag.setId(DeviceTagEntity.createTagId(id, tag.getKey())); + tag.setDeviceId(id); + } + } + +} diff --git a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/entity/DeviceTagEntity.java b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/entity/DeviceTagEntity.java index 34d605f3..1be4c687 100644 --- a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/entity/DeviceTagEntity.java +++ b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/entity/DeviceTagEntity.java @@ -5,8 +5,10 @@ import com.alibaba.fastjson.JSON; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Getter; import lombok.Setter; +import lombok.SneakyThrows; import org.apache.commons.codec.digest.DigestUtils; import org.hibernate.validator.constraints.Length; +import org.hswebframework.ezorm.rdb.mapping.annotation.Comment; import org.hswebframework.ezorm.rdb.mapping.annotation.DefaultValue; import org.hswebframework.web.api.crud.entity.GenericEntity; import org.hswebframework.web.crud.annotation.EnableEntityEvent; @@ -20,6 +22,7 @@ import org.jetlinks.core.metadata.types.ArrayType; import org.jetlinks.core.metadata.types.DataTypes; import org.jetlinks.core.metadata.types.ObjectType; import org.jetlinks.core.metadata.types.UnknownType; +import org.jetlinks.core.utils.SerializeUtils; import org.jetlinks.supports.official.JetLinksDeviceMetadataCodec; import javax.persistence.Column; @@ -27,6 +30,8 @@ import javax.persistence.Index; import javax.persistence.Table; import javax.validation.constraints.NotBlank; import javax.validation.constraints.NotNull; +import java.io.ObjectInput; +import java.io.ObjectOutput; import java.util.Date; import java.util.Optional; import java.util.Set; @@ -39,6 +44,7 @@ import java.util.stream.Collectors; @Index(name = "dev_dev_id_idx", columnList = "device_id"), @Index(name = "dev_tag_idx", columnList = "device_id,key,value") }) +@Comment("设备标签表") @EnableEntityEvent public class DeviceTagEntity extends GenericEntity { @@ -72,12 +78,18 @@ public class DeviceTagEntity extends GenericEntity { @Schema(description = "创建时间(只读)") private Date createTime; + @Column + @DefaultValue(generator = Generators.CURRENT_TIME) + @Schema(description = "时间戳") + private Long timestamp; + @Column @Schema(description = "说明") private String description; private DataType dataType; + public static DeviceTagEntity of(PropertyMetadata property) { DeviceTagEntity entity = new DeviceTagEntity(); entity.setKey(property.getId()); @@ -170,4 +182,34 @@ public class DeviceTagEntity extends GenericEntity { .map(PropertyMetadata::getId) .collect(Collectors.toSet()); } + + @SneakyThrows + public void writeExternal(ObjectOutput out) { + SerializeUtils.writeNullableUTF(getId(), out); + SerializeUtils.writeNullableUTF(getDeviceId(), out); + SerializeUtils.writeNullableUTF(getKey(), out); + SerializeUtils.writeNullableUTF(getName(), out); + SerializeUtils.writeNullableUTF(getValue(), out); + SerializeUtils.writeNullableUTF(getType(), out); + out.writeLong(getCreateTime() == null ? -1 : getCreateTime().getTime()); + out.writeLong(getTimestamp() == null ? -1 : getTimestamp()); + SerializeUtils.writeNullableUTF(getDescription(), out); + + + } + + @SneakyThrows + public void readExternal(ObjectInput in) { + setId(SerializeUtils.readNullableUTF(in)); + setDeviceId(SerializeUtils.readNullableUTF(in)); + setKey(SerializeUtils.readNullableUTF(in)); + setName(SerializeUtils.readNullableUTF(in)); + setValue(SerializeUtils.readNullableUTF(in)); + setType(SerializeUtils.readNullableUTF(in)); + long time = in.readLong(); + setCreateTime(time == -1 ? null : new Date(time)); + long ts = in.readLong(); + setTimestamp(ts == -1 ? null : ts); + setDescription(SerializeUtils.readNullableUTF(in)); + } } diff --git a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/entity/ProductDetail.java b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/entity/ProductDetail.java new file mode 100644 index 00000000..a5cb92f2 --- /dev/null +++ b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/entity/ProductDetail.java @@ -0,0 +1,132 @@ +package org.jetlinks.community.device.entity; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Getter; +import lombok.Setter; +import org.hswebframework.web.api.crud.entity.EntityFactoryHolder; +import org.hswebframework.web.bean.FastBeanCopier; +import org.jetlinks.core.metadata.ConfigMetadata; +import org.jetlinks.core.metadata.Feature; +import org.jetlinks.core.metadata.SimpleFeature; +import org.jetlinks.community.device.enums.DeviceType; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; + +/** + * 产品详情信息. + * + * @author zhangji 2022/7/28 + */ +@Getter +@Setter +public class ProductDetail { + @Schema(description = "产品ID") + private String id; + + @Schema(description = "产品名称") + private String name; + + @Schema(description = "所属项目") + private String projectId; + + @Schema(description = "图片地址") + private String photoUrl; + + @Schema(description = "项目名称") + private String projectName; + + @Schema(description = "说明") + private String describe; + + @Schema(description = "所属品类ID") + private String classifiedId; + + @Schema(description = "所属品类名称") + private String classifiedName; + + @Schema(description = "消息协议ID") + private String messageProtocol; + + @Schema(description = "消息协议名称") + private String protocolName; + + @Schema(description = "物模型定义") + private String metadata; + + @Schema(description = "传输协议") + private String transportProtocol; + + @Schema(description = "入网方式") + private String networkWay; + + @Schema(description = "设备类型") + private DeviceType deviceType; + + @Schema(description = "设备数量") + private int deviceCount; + + @Schema(description = "协议相关配置") + private Map configuration; + + @Schema(description = "产品状态 1正常,0禁用") + private Byte state; + + @Schema(description = "创建者ID(只读)") + private String creatorId; + + @Schema(description = "创建者时间(只读)") + private Long createTime; + + @Schema(description = "设备接入方式ID") + private String accessId; + + @Schema(description = "设备接入方式") + private String accessProvider; + + @Schema(description = "设备接入方式名称") + private String accessName; + + @Schema(description = "数据存储策略") + private String storePolicy; + + @Schema(description = "数据存储策略相关配置") + private Map storePolicyConfiguration; + + @Schema(description = "产品配置信息") + private List configMetadatas = new ArrayList<>(); + + @Schema(description = "产品特性") + private List features = new ArrayList<>(); + + public static ProductDetail of(){ + return EntityFactoryHolder.newInstance(ProductDetail.class,ProductDetail::new); + } + + public ProductDetail with(DeviceProductEntity entity) { + FastBeanCopier.copy(entity, this); + return this; + } + + public ProductDetail withConfigMetadatas(Collection configMetadatas) { + this.configMetadatas.addAll(configMetadatas); + return this; + } + + public ProductDetail withFeatures(Collection features) { + for (Feature feature : features) { + this.features.add(new SimpleFeature(feature.getId(), feature.getName())); + } + return this; + } + + public ProductDetail withDeviceCount(Integer deviceCount) { + this.setDeviceCount(deviceCount); + return this; + } +} + + + 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 deleted file mode 100644 index ac182792..00000000 --- a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/entity/ProtocolSupportEntity.java +++ /dev/null @@ -1,78 +0,0 @@ -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; -import javax.persistence.Table; -import java.sql.JDBCType; -import java.util.Map; - -@Getter -@Setter -@Table(name = "dev_protocol") -@EnableEntityEvent -public class ProtocolSupportEntity extends GenericEntity implements RecordCreationEntity { - - @Column - private String name; - - @Column - private String description; - - @Column - 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 - private Map configuration; - - public ProtocolSupportDefinition toUnDeployDefinition() { - ProtocolSupportDefinition definition = toDeployDefinition(); - definition.setState((byte) 0); - return definition; - } - - public ProtocolSupportDefinition toDeployDefinition() { - ProtocolSupportDefinition definition = new ProtocolSupportDefinition(); - definition.setId(getId()); - definition.setConfiguration(configuration); - definition.setName(name); - definition.setProvider(type); - definition.setState((byte) 1); - - return definition; - } -} 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 4848580e..7752c935 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 @@ -7,10 +7,13 @@ import org.hswebframework.web.dict.Dict; import org.hswebframework.web.dict.EnumDict; import org.jetlinks.core.metadata.Feature; +import java.util.Arrays; + @Dict("device-feature") @Getter @AllArgsConstructor -public enum DeviceFeature implements EnumDict , Feature { +@Generated +public enum DeviceFeature implements EnumDict, Feature { selfManageState("子设备自己管理状态") @@ -32,4 +35,11 @@ public enum DeviceFeature implements EnumDict , Feature { public String getName() { return text; } + + public static DeviceFeature get(String id) { + return Arrays.stream(values()) + .filter(deviceFeature -> deviceFeature.getId().equals(id)) + .findAny() + .orElse(null); + } } diff --git a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/enums/DeviceLogType.java b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/enums/DeviceLogType.java index 773fb261..4f9b51be 100644 --- a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/enums/DeviceLogType.java +++ b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/enums/DeviceLogType.java @@ -2,18 +2,19 @@ package org.jetlinks.community.device.enums; import com.alibaba.fastjson.annotation.JSONField; import lombok.AllArgsConstructor; +import lombok.Generated; import lombok.Getter; -import org.hswebframework.web.dict.EnumDict; +import org.hswebframework.web.dict.I18nEnumDict; import org.jetlinks.core.message.DeviceMessage; import org.jetlinks.core.message.MessageType; -import java.util.EnumMap; -import java.util.Map; -import java.util.Optional; +import java.util.*; +import java.util.stream.Collectors; @AllArgsConstructor @Getter -public enum DeviceLogType implements EnumDict { +@Generated +public enum DeviceLogType implements I18nEnumDict { event("事件上报"), readProperty("读取属性"), writeProperty("修改属性"), @@ -26,11 +27,34 @@ public enum DeviceLogType implements EnumDict { functionReply("调用功能回复"), register("设备注册"), unregister("设备注销"), + readFirmware("读取固件信息"), + readFirmwareReply("读取固件信息回复"), + reportFirmware("上报固件信息"), + pullFirmware("拉取固件信息"), + pullFirmwareReply("拉取固件信息回复"), + upgradeFirmware("推送固件信息"), + upgradeFirmwareReply("推送固件信息回复"), + upgradeFirmwareProgress("固件更新进度"), log("日志"), tag("标签更新"), offline("离线"), online("上线"), - other("其它"); + other("其它"), + direct("透传"), + acknowledge("应答"), + metadata("上报物模型"), + stateCheck("状态检查"), + stateCheckReply("状态检查回复"), + //状态检查 + disconnect("断开连接"), + disconnectReply("断开连接回复"), + reportCollectorData("上报数采数据"), + readCollectorData("读取数采数据"), + readCollectorDataReply("读取数采数据回复"), + writeCollectorData("修改数采数据"), + writeCollectorDataReply("修改数采数据回复") + + ; @JSONField(serialize = false) @@ -43,8 +67,14 @@ public enum DeviceLogType implements EnumDict { private final static Map typeMapping = new EnumMap<>(MessageType.class); - static { + public final static List nameList; + static { + nameList = Collections.unmodifiableList( + Arrays.stream(values()) + .map(DeviceLogType::name) + .collect(Collectors.toList()) + ); typeMapping.put(MessageType.EVENT, event); typeMapping.put(MessageType.ONLINE, online); typeMapping.put(MessageType.OFFLINE, offline); @@ -66,12 +96,42 @@ public enum DeviceLogType implements EnumDict { typeMapping.put(MessageType.REGISTER, register); typeMapping.put(MessageType.UN_REGISTER, unregister); + typeMapping.put(MessageType.READ_FIRMWARE, readFirmware); + typeMapping.put(MessageType.READ_FIRMWARE_REPLY, readFirmwareReply); + + typeMapping.put(MessageType.REPORT_FIRMWARE, reportFirmware); + + typeMapping.put(MessageType.REQUEST_FIRMWARE, pullFirmware); + typeMapping.put(MessageType.REQUEST_FIRMWARE_REPLY, pullFirmwareReply); + + typeMapping.put(MessageType.UPGRADE_FIRMWARE, upgradeFirmware); + typeMapping.put(MessageType.UPGRADE_FIRMWARE_REPLY, upgradeFirmwareReply); + typeMapping.put(MessageType.UPGRADE_FIRMWARE_PROGRESS, upgradeFirmwareProgress); + typeMapping.put(MessageType.ACKNOWLEDGE, acknowledge); + typeMapping.put(MessageType.DERIVED_METADATA, metadata); + typeMapping.put(MessageType.STATE_CHECK, stateCheck); + typeMapping.put(MessageType.STATE_CHECK_REPLY, stateCheckReply); + + typeMapping.put(MessageType.DISCONNECT, disconnect); + typeMapping.put(MessageType.DISCONNECT_REPLY, disconnectReply); + + typeMapping.put(MessageType.DIRECT, direct); + + typeMapping.put(MessageType.REPORT_COLLECTOR, reportCollectorData); + typeMapping.put(MessageType.READ_COLLECTOR_DATA, readCollectorData); + typeMapping.put(MessageType.READ_COLLECTOR_DATA_REPLY, readCollectorDataReply); + typeMapping.put(MessageType.WRITE_COLLECTOR_DATA, writeCollectorData); + typeMapping.put(MessageType.WRITE_COLLECTOR_DATA_REPLY, writeCollectorDataReply); + } + @Generated public static DeviceLogType of(DeviceMessage message) { return Optional.ofNullable(typeMapping.get(message.getMessageType())).orElse(DeviceLogType.other); } + + // @Override // public Object getWriteJSONObject() { // return getValue(); diff --git a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/enums/DeviceProductState.java b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/enums/DeviceProductState.java index 5f2a6e27..9ed4938e 100644 --- a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/enums/DeviceProductState.java +++ b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/enums/DeviceProductState.java @@ -1,22 +1,25 @@ 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.hswebframework.web.dict.I18nEnumDict; @AllArgsConstructor @Getter @Dict("device-product-state") -public enum DeviceProductState implements EnumDict { - unregistered("未发布", (byte) 0), - registered("已发布", (byte) 1), +@Generated +public enum DeviceProductState implements I18nEnumDict { + unregistered("正常", (byte) 0), + registered("禁用", (byte) 1), other("其它", (byte) -100), forbidden("禁用", (byte) -1); - private String text; + private final String text; - private Byte value; + private final Byte value; public String getName() { return name(); diff --git a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/enums/DeviceState.java b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/enums/DeviceState.java index a84d09f7..afb45270 100644 --- a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/enums/DeviceState.java +++ b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/enums/DeviceState.java @@ -1,16 +1,17 @@ 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.hswebframework.web.dict.I18nEnumDict; @AllArgsConstructor @Getter @Dict("device-state") +@Generated public enum DeviceState implements I18nEnumDict { - notActive("未激活"), + notActive("禁用"), offline("离线"), online("在线"); @@ -21,6 +22,7 @@ public enum DeviceState implements I18nEnumDict { return name(); } + @Generated public static DeviceState of(byte state) { switch (state) { case org.jetlinks.core.device.DeviceState.offline: diff --git a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/enums/DeviceType.java b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/enums/DeviceType.java index 253c5ad2..f8f5a350 100644 --- a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/enums/DeviceType.java +++ b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/enums/DeviceType.java @@ -1,16 +1,17 @@ package org.jetlinks.community.device.enums; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import lombok.AllArgsConstructor; +import lombok.Generated; import lombok.Getter; import org.hswebframework.web.dict.Dict; -import org.hswebframework.web.dict.EnumDict; +import org.hswebframework.web.dict.I18nEnumDict; @AllArgsConstructor @Getter @Dict("device-type") -@JsonDeserialize(contentUsing = EnumDict.EnumDictJSONDeserializer.class) -public enum DeviceType implements EnumDict { +//@JsonDeserialize(contentUsing = EnumDict.EnumDictJSONDeserializer.class) +@Generated +public enum DeviceType implements I18nEnumDict { device("直连设备"), childrenDevice("网关子设备"), gateway("网关设备") @@ -23,8 +24,5 @@ public enum DeviceType implements EnumDict { return name(); } -// @Override -// public boolean isWriteJSONObjectEnabled() { -// return false; -// } + } diff --git a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/enums/ValidateDataType.java b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/enums/ValidateDataType.java new file mode 100644 index 00000000..542efa71 --- /dev/null +++ b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/enums/ValidateDataType.java @@ -0,0 +1,170 @@ +package org.jetlinks.community.device.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.apache.commons.collections4.CollectionUtils; +import org.hswebframework.web.dict.EnumDict; +import org.hswebframework.web.dict.I18nEnumDict; +import org.hswebframework.web.exception.ValidationException; +import org.jetlinks.core.metadata.DataType; +import org.jetlinks.core.metadata.Metadata; +import org.jetlinks.core.metadata.types.*; +import org.springframework.util.StringUtils; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.util.List; + +/** + * @author: wangsheng + */ +@Getter +@AllArgsConstructor +public enum ValidateDataType implements I18nEnumDict { + type_int("int", "整数类型", IntType.class) { + @Override + public Mono validate(DataType valueType, String property) { + return Mono.empty(); + } + }, + + type_long("long", "长整类型", LongType.class) { + @Override + public Mono validate(DataType valueType, String property) { + return Mono.empty(); + } + }, + + type_float("float", "浮点类型", FloatType.class) { + @Override + public Mono validate(DataType valueType, String property) { + return Mono.empty(); + } + }, + + type_double("double", "双精度浮点类型", DoubleType.class) { + @Override + public Mono validate(DataType valueType, String property) { + return Mono.empty(); + } + }, + + type_string("string", "字符串类型", StringType.class) { + @Override + public Mono validate(DataType valueType, String property) { + return Mono.empty(); + } + }, + + type_boolean("boolean", "布尔类型", BooleanType.class) { + @Override + public Mono validate(DataType valueType, String property) { + return Mono.empty(); + } + }, + + type_date("date", "日期类型", DateTimeType.class) { + @Override + public Mono validate(DataType valueType, String property) { + DateTimeType type = (DateTimeType) valueType; + if (!StringUtils.hasText(type.getFormat())) { + return Mono.error(new ValidationException.NoStackTrace("error.property_metadata_time_type_format_can_not_be_null", "", property)); + } + return Mono.empty(); + } + }, + + type_enum("enum", "枚举类型", EnumType.class) { + @Override + public Mono validate(DataType valueType, String property) { + EnumType type = (EnumType) valueType; + List elements = type.getElements(); + if (CollectionUtils.isEmpty(elements)) { + return Mono.error(new ValidationException.NoStackTrace("error.property_metadata_enum_type_can_not_be_null", "", property)); + } + for (EnumType.Element element : elements) { + if (!StringUtils.hasText(element.getValue()) && !StringUtils.hasText(element.getValue())) { + return Mono.error(new ValidationException.NoStackTrace("error.property_metadata_enum_type_value_can_not_be_null", "", property)); + } + } + return Mono.empty(); + } + }, + + type_array("array", "数组类型", ArrayType.class) { + @Override + Mono validate(DataType valueType, String property) { + ArrayType type = (ArrayType) valueType; + return handleValidateDataType(type.getElementType(), property); + } + }, + + type_object("object", "对象类型", ObjectType.class) { + @Override + Mono validate(DataType valueType, String property) { + ObjectType type = (ObjectType) valueType; + return Flux + .fromIterable(type.getProperties()) + .flatMap(prop -> validateIdAndName(prop) + .then(handleValidateDataType(prop.getValueType(), property))) + .then(); + } + }, + + type_file("file", "文件类型", FileType.class) { + @Override + public Mono validate(DataType valueType, String property) { + return Mono.empty(); + } + }, + + type_password("password", "密码类型", PasswordType.class) { + @Override + public Mono validate(DataType valueType, String property) { + return Mono.empty(); + } + }, + + type_geoPoint("geoPoint", "地理位置", GeoType.class) { + @Override + public Mono validate(DataType valueType, String property) { + return Mono.empty(); + } + }, + ; + + private final String code; + private final String text; + private final Class type; + + @Override + public String getValue() { + return getCode(); + } + + + abstract Mono validate(DataType valueType, String property); + + public static Mono validateIdAndName(Metadata metadata) { + if (!StringUtils.hasText(metadata.getId())) { + return Mono.error(new ValidationException.NoStackTrace("error.property_metadata_id_can_not_be_null","", metadata.getName())); + } + + if (!StringUtils.hasText(metadata.getName())) { + return Mono.error(new ValidationException.NoStackTrace("error.property_metadata_name_can_not_be_null", "", metadata.getId())); + } + return Mono.empty(); + } + + public static Mono handleValidateDataType(DataType valueType, String property) { + if (valueType == null) { + return Mono.error(new ValidationException.NoStackTrace("error.property_metadata_type_can_not_be_null", "", property)); + } + + ValidateDataType type = EnumDict.findByValue(ValidateDataType.class, valueType.getType()).orElse(null); + if (type == null) { + return Mono.error(new ValidationException.NoStackTrace("error.property_metadata_type_error", "", property)); + } + return type.validate(valueType, property); + } +} diff --git a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/events/DeviceAutoRegisterEvent.java b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/events/DeviceAutoRegisterEvent.java new file mode 100644 index 00000000..ddca01eb --- /dev/null +++ b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/events/DeviceAutoRegisterEvent.java @@ -0,0 +1,29 @@ +package org.jetlinks.community.device.events; + +import lombok.Generated; +import lombok.Getter; +import org.hswebframework.web.event.DefaultAsyncEvent; +import org.jetlinks.community.device.entity.DeviceInstanceEntity; + +@Getter +@Generated +public class DeviceAutoRegisterEvent extends DefaultAsyncEvent { + + private boolean allowRegister = true; + + private final DeviceInstanceEntity entity; + + public DeviceAutoRegisterEvent(DeviceInstanceEntity entity) { + this.entity = entity; + } + + /** + * 设置是否允许自定注册此设备 + * + * @param allowRegister 是否允许自动注册 + */ + @Generated + public void setAllowRegister(boolean allowRegister) { + this.allowRegister = allowRegister; + } +} diff --git a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/events/DeviceProductDeployEvent.java b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/events/DeviceProductDeployEvent.java index 42193c5e..d44d6f9b 100644 --- a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/events/DeviceProductDeployEvent.java +++ b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/events/DeviceProductDeployEvent.java @@ -1,8 +1,11 @@ package org.jetlinks.community.device.events; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Generated; import lombok.Getter; import lombok.Setter; import org.hswebframework.web.event.DefaultAsyncEvent; +import org.jetlinks.community.gateway.supports.DeviceGatewayProvider; import java.util.Map; @@ -12,6 +15,7 @@ import java.util.Map; **/ @Getter @Setter +@Generated public class DeviceProductDeployEvent extends DefaultAsyncEvent { private String id; @@ -42,5 +46,16 @@ public class DeviceProductDeployEvent extends DefaultAsyncEvent { private Long createTime; + @Deprecated private String orgId; + + @Schema(description = "设备接入方式ID") + private String accessId; + + /** + * @see DeviceGatewayProvider#getId() + */ + @Schema(description = "设备接入方式") + private String accessProvider; + } 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 66ad306d..071752c4 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 @@ -1,13 +1,13 @@ package org.jetlinks.community.device.events.handler; import lombok.extern.slf4j.Slf4j; +import org.jetlinks.core.event.EventBus; +import org.jetlinks.core.event.Subscription; +import org.jetlinks.core.metadata.DeviceMetadataCodec; 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; import org.springframework.boot.CommandLineRunner; @@ -21,7 +21,7 @@ import reactor.core.publisher.Mono; import javax.annotation.PreDestroy; /** - * 处理设备产品发布事件 + * 处理设备型号发布事件 * * @author bsetfeng * @author zhouhao @@ -96,8 +96,7 @@ public class DeviceProductDeployHandler implements CommandLineRunner { return codec .decode(metadataString) .flatMap(metadata -> Flux - .mergeDelayError(2, - dataService.reloadMetadata(productId, metadata), + .concatDelayError(dataService.reloadMetadata(productId, metadata), latestDataService.reloadMetadata(productId, metadata)) .then()); } @@ -106,9 +105,8 @@ public class DeviceProductDeployHandler implements CommandLineRunner { return codec .decode(metadataString) .flatMap(metadata -> Flux - .mergeDelayError(2, - dataService.registerMetadata(productId, metadata), - latestDataService.upgradeMetadata(productId, metadata)) + .concatDelayError(dataService.registerMetadata(productId, metadata), + latestDataService.upgradeMetadata(productId, metadata)) .then()); } diff --git a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/events/handler/ValueTypeTranslator.java b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/events/handler/ValueTypeTranslator.java index 8c6f21fe..620dcb0d 100644 --- a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/events/handler/ValueTypeTranslator.java +++ b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/events/handler/ValueTypeTranslator.java @@ -2,14 +2,8 @@ package org.jetlinks.community.device.events.handler; import com.alibaba.fastjson.JSON; import lombok.extern.slf4j.Slf4j; +import org.jetlinks.core.metadata.Converter; import org.jetlinks.core.metadata.DataType; -import org.jetlinks.core.metadata.PropertyMetadata; -import org.jetlinks.core.metadata.types.*; - -import java.util.List; -import java.util.Map; -import java.util.function.Function; -import java.util.stream.Collectors; /** * @author bsetfeng @@ -18,60 +12,17 @@ import java.util.stream.Collectors; @Slf4j public class ValueTypeTranslator { - private static Object propertyMetadataTranslator(Object value, List metadataList) { - if (value instanceof Map) { - return propertyMetadataToMap((Map) value, metadataList); - } else { - return JSON.toJSON(value); - } - } - - private static Map propertyMetadataToMap(Map map, List metadataList) { - Map metadataMap = toMap(metadataList); - for (Map.Entry entry : map.entrySet()) { - PropertyMetadata property = metadataMap.get(entry.getKey()); - if (null != property) { - entry.setValue(translator(entry.getValue(), property.getValueType())); - } - } - return map; - } - - private static Map toMap(List metadata) { - return metadata.stream() - .collect(Collectors.toMap(PropertyMetadata::getId, Function.identity())); - } - public static Object translator(Object value, DataType dataType) { try { - if (dataType instanceof DateTimeType) { - return ((DateTimeType) dataType).convert(value); - } else if (dataType instanceof DoubleType) { - return ((DoubleType) dataType).convert(value); - } else if (dataType instanceof FloatType) { - return ((FloatType) dataType).convert(value); - } else if (dataType instanceof LongType) { - return ((LongType) dataType).convert(value); - } else if (dataType instanceof BooleanType) { - return ((BooleanType) dataType).convert(value); - } else if (dataType instanceof IntType) { - return ((IntType) dataType).convert(value); - } else if (dataType instanceof ObjectType) { - return propertyMetadataTranslator(value, ((ObjectType) dataType).getProperties()); + if (dataType instanceof Converter) { + return ((Converter) dataType).convert(value); } else { return value; } } catch (Exception e) { - log.error("设备上报值与元数据值不匹配.value:{},DataTypeClass:{}", value, dataType.getClass(), e); + log.error("设备上报值与物模型不匹配.value:{},type:{}", value, JSON.toJSONString(dataType), e); return value; } } -// public static String dateFormatTranslator(Date date) { -// DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); -// Instant instant = date.toInstant(); -// ZoneId zone = ZoneId.systemDefault(); -// LocalDateTime localDateTime = LocalDateTime.ofInstant(instant, zone); -// return localDateTime.format(dtf); -// } } diff --git a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/function/DeviceConfigFunction.java b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/function/DeviceConfigFunction.java new file mode 100644 index 00000000..e83edee3 --- /dev/null +++ b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/function/DeviceConfigFunction.java @@ -0,0 +1,39 @@ +package org.jetlinks.community.device.function; + +import org.jetlinks.core.Value; +import org.jetlinks.core.device.DeviceRegistry; +import org.jetlinks.reactor.ql.supports.map.FunctionMapFeature; +import org.springframework.stereotype.Component; +import reactor.core.publisher.Mono; + +/** + * 在reactorQL中获取设备配置 + *
{@code
+ * select device.config(deviceId,'password') pwd from ...
+ *
+ * select * from ... where device.config(deviceId,'password') = 'xxx'
+ * }
+ * + * @author zhouhao + * @since 1.13 + */ +@Component +public class DeviceConfigFunction extends FunctionMapFeature { + public DeviceConfigFunction(DeviceRegistry registry) { + super("device.config", 2, 2, flux -> flux + .collectList() + .flatMap(args -> { + if (args.size() != 2) { + return Mono.empty(); + } + String deviceId = String.valueOf(args.get(0)); + String configKey = String.valueOf(args.get(1)); + + return registry + .getDevice(deviceId) + .flatMap(device -> device + .getConfig(configKey) + .map(Value::get)); + })); + } +} diff --git a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/function/DeviceEventFunction.java b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/function/DeviceEventFunction.java new file mode 100644 index 00000000..58dd5e9b --- /dev/null +++ b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/function/DeviceEventFunction.java @@ -0,0 +1,43 @@ +package org.jetlinks.community.device.function; + +import org.jetlinks.core.device.DeviceThingType; +import org.jetlinks.core.things.ThingEvent; +import org.jetlinks.core.things.ThingsDataManager; +import org.jetlinks.reactor.ql.supports.map.FunctionMapFeature; +import org.jetlinks.reactor.ql.utils.CastUtils; +import org.springframework.stereotype.Component; +import reactor.core.publisher.Mono; + +/** + * 在reactorQL中获取设备最新事件 + *
{@code
+ * select device.event.recent(deviceId,'eventId',timestamp) recent from ...
+ *
+ * select * from ... where device.event.recent(deviceId,'eventId',timestamp)  = 'xxx'
+ * }
+ * + * @author zhouhao + * @since 2.2 + */ +@Component +public class DeviceEventFunction extends FunctionMapFeature { + public DeviceEventFunction(ThingsDataManager dataManager) { + super("device.event.recent", 3, 2, flux -> flux + .collectList() + .flatMap(args -> { + if (args.size() < 2) { + return Mono.empty(); + } + String deviceId = String.valueOf(args.get(0)); + String property = String.valueOf(args.get(1)); + long timestamp = args.size() > 2 + ? CastUtils.castNumber(args.get(2)) + .longValue() + : System.currentTimeMillis(); + + return dataManager + .getLastEvent(DeviceThingType.device.getId(), deviceId, property, timestamp) + .map(ThingEvent::getData); + })); + } +} diff --git a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/function/DeviceMetadataEventFunction.java b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/function/DeviceMetadataEventFunction.java new file mode 100644 index 00000000..6ba0b903 --- /dev/null +++ b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/function/DeviceMetadataEventFunction.java @@ -0,0 +1,26 @@ +package org.jetlinks.community.device.function; + +import org.jetlinks.core.device.DeviceOperator; +import org.jetlinks.core.device.DeviceRegistry; +import org.jetlinks.core.metadata.Jsonable; +import org.jetlinks.reactor.ql.supports.map.FunctionMapFeature; +import org.springframework.stereotype.Component; +import reactor.core.publisher.Mono; + +@Component +public class DeviceMetadataEventFunction extends FunctionMapFeature { + public DeviceMetadataEventFunction(DeviceRegistry registry) { + super("device.metadata.event", 2, 2, args -> args + .collectList() + .flatMap(arg -> { + String deviceId = String.valueOf(arg.get(0)); + String event = String.valueOf(arg.get(1)); + return registry.getDevice(deviceId) + .flatMap(DeviceOperator::getMetadata) + .flatMap(metadata -> Mono + .justOrEmpty(metadata.getEventOrNull(event)) + .map(Jsonable::toJson) + ); + })); + } +} diff --git a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/function/DeviceMetadataFunction.java b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/function/DeviceMetadataFunction.java new file mode 100644 index 00000000..f0d1ddc7 --- /dev/null +++ b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/function/DeviceMetadataFunction.java @@ -0,0 +1,22 @@ +package org.jetlinks.community.device.function; + +import org.jetlinks.core.device.DeviceOperator; +import org.jetlinks.core.device.DeviceRegistry; +import org.jetlinks.core.things.ThingMetadata; +import org.jetlinks.reactor.ql.supports.map.FunctionMapFeature; +import org.springframework.stereotype.Component; + +@Component +public class DeviceMetadataFunction extends FunctionMapFeature { + public DeviceMetadataFunction(DeviceRegistry registry) { + super("device.metadata", 1, 1, args -> args + .collectList() + .flatMap(arg -> { + String deviceId = String.valueOf(arg.get(0)); + return registry + .getDevice(deviceId) + .flatMap(DeviceOperator::getMetadata) + .map(ThingMetadata::toJson); + })); + } +} diff --git a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/function/DeviceMetadataFunctionFunction.java b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/function/DeviceMetadataFunctionFunction.java new file mode 100644 index 00000000..7d28dfb2 --- /dev/null +++ b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/function/DeviceMetadataFunctionFunction.java @@ -0,0 +1,26 @@ +package org.jetlinks.community.device.function; + +import org.jetlinks.core.device.DeviceOperator; +import org.jetlinks.core.device.DeviceRegistry; +import org.jetlinks.core.metadata.Jsonable; +import org.jetlinks.reactor.ql.supports.map.FunctionMapFeature; +import org.springframework.stereotype.Component; +import reactor.core.publisher.Mono; + +@Component +public class DeviceMetadataFunctionFunction extends FunctionMapFeature { + public DeviceMetadataFunctionFunction(DeviceRegistry registry) { + super("device.metadata.func", 2, 2, args -> args + .collectList() + .flatMap(arg -> { + String deviceId = String.valueOf(arg.get(0)); + String function = String.valueOf(arg.get(1)); + return registry.getDevice(deviceId) + .flatMap(DeviceOperator::getMetadata) + .flatMap(metadata -> Mono + .justOrEmpty(metadata.getFunctionOrNull(function)) + .map(Jsonable::toJson) + ); + })); + } +} diff --git a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/function/DeviceMetadataPropertyFunction.java b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/function/DeviceMetadataPropertyFunction.java new file mode 100644 index 00000000..82c61f87 --- /dev/null +++ b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/function/DeviceMetadataPropertyFunction.java @@ -0,0 +1,26 @@ +package org.jetlinks.community.device.function; + +import org.jetlinks.core.device.DeviceOperator; +import org.jetlinks.core.device.DeviceRegistry; +import org.jetlinks.core.metadata.Jsonable; +import org.jetlinks.reactor.ql.supports.map.FunctionMapFeature; +import org.springframework.stereotype.Component; +import reactor.core.publisher.Mono; + +@Component +public class DeviceMetadataPropertyFunction extends FunctionMapFeature { + public DeviceMetadataPropertyFunction(DeviceRegistry registry) { + super("device.metadata.property", 2, 2, args -> args + .collectList() + .flatMap(arg -> { + String deviceId = String.valueOf(arg.get(0)); + String property = String.valueOf(arg.get(1)); + return registry.getDevice(deviceId) + .flatMap(DeviceOperator::getMetadata) + .flatMap(metadata -> Mono + .justOrEmpty(metadata.getPropertyOrNull(property)) + .map(Jsonable::toJson) + ); + })); + } +} diff --git a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/function/DeviceStateFunction.java b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/function/DeviceStateFunction.java new file mode 100644 index 00000000..de3d6ae3 --- /dev/null +++ b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/function/DeviceStateFunction.java @@ -0,0 +1,41 @@ +package org.jetlinks.community.device.function; + +import org.jetlinks.core.device.DeviceRegistry; +import org.jetlinks.core.device.DeviceState; +import org.jetlinks.reactor.ql.supports.map.FunctionMapFeature; +import org.jetlinks.reactor.ql.utils.CastUtils; +import org.springframework.stereotype.Component; + +/** + * 在reactorQL中获取设备当前状态 + * + *
    + *
  • device.state('test1'): 获取设备id为test1的当前状态
  • + *
  • device.state('test1',true): 获取设备id为test1的真实状态
  • + *
+ *
{@code
+ * select device.state(deviceId) state from ...
+ *
+ * select * from ... where device.state(deviceId) = 1
+ * }
+ * + * @author zhouhao + * @see DeviceState + * @since 2.2 + */ +@Component +public class DeviceStateFunction extends FunctionMapFeature { + public DeviceStateFunction(DeviceRegistry registry) { + super("device.state", 2, 1, flux -> flux + .collectList() + .flatMap(args -> { + String deviceId = String.valueOf(args.get(0)); + boolean check = args.size() == 2 && CastUtils.castBoolean(args.get(1)); + + return registry + .getDevice(deviceId) + .flatMap(device -> check ? device.checkState() : device.getState()) + .defaultIfEmpty(DeviceState.noActive); + })); + } +} diff --git a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/function/DeviceTagFunction.java b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/function/DeviceTagFunction.java new file mode 100644 index 00000000..747d0a56 --- /dev/null +++ b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/function/DeviceTagFunction.java @@ -0,0 +1,46 @@ +package org.jetlinks.community.device.function; + +import org.hswebframework.ezorm.rdb.mapping.ReactiveRepository; +import org.jetlinks.core.device.DeviceThingType; +import org.jetlinks.core.things.ThingTag; +import org.jetlinks.core.things.ThingsDataManager; +import org.jetlinks.community.device.entity.DeviceTagEntity; +import org.jetlinks.reactor.ql.supports.map.FunctionMapFeature; +import org.springframework.stereotype.Component; +import reactor.core.publisher.Mono; + +/** + * 获取设备标签函数 + *

+ * select device.tag(deviceId,'tag1') + * + * @since 1.9 + */ +@Component +public class DeviceTagFunction extends FunctionMapFeature { + + public DeviceTagFunction(ReactiveRepository tagReposiotry, + ThingsDataManager dataManager) { + super("device.tag", 2, 2, args -> + args.collectList() + .flatMap(list -> { + Object deviceId = list.get(0); + Object tagKey = list.get(1); + + return dataManager + //优先从缓存中获取 + .getLastTag(DeviceThingType.device.getId(), + String.valueOf(deviceId), String.valueOf(tagKey), + System.currentTimeMillis()) + .map(ThingTag::getValue) + .switchIfEmpty(Mono.defer(() -> tagReposiotry + .createQuery() + .where(DeviceTagEntity::getDeviceId, deviceId) + .and(DeviceTagEntity::getKey, tagKey) + .fetch() + .take(1) + .singleOrEmpty() + .map(DeviceTagEntity::getValue))); + })); + } +} diff --git a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/function/DeviceTagsFunction.java b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/function/DeviceTagsFunction.java new file mode 100644 index 00000000..3f71a716 --- /dev/null +++ b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/function/DeviceTagsFunction.java @@ -0,0 +1,73 @@ +package org.jetlinks.community.device.function; + +import org.hswebframework.ezorm.rdb.mapping.ReactiveRepository; +import org.jetlinks.core.device.DeviceThingType; +import org.jetlinks.core.things.ThingsDataManager; +import org.jetlinks.community.device.entity.DeviceTagEntity; +import org.jetlinks.reactor.ql.supports.map.FunctionMapFeature; +import org.springframework.stereotype.Component; +import org.springframework.util.CollectionUtils; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.util.function.Tuple2; +import reactor.util.function.Tuples; + +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; + +/** + * 获取设备标签函数 + *

+ * select device.tags(deviceId,'tag1','tag2') + * + * @since 1.3 + */ +@Component +public class DeviceTagsFunction extends FunctionMapFeature { + + public DeviceTagsFunction(ReactiveRepository tagReposiotry, + ThingsDataManager dataManager) { + + super("device.tags", 100, 1, args -> + args.collectList() + .flatMap(list -> { + Object deviceId = list.get(0); + Set tags = list.stream().skip(1).collect(Collectors.toCollection(ConcurrentHashMap::newKeySet)); + + //数据库加载 + Flux> loaderFromDb + = Flux.defer(() -> tagReposiotry + .createQuery() + .where(DeviceTagEntity::getDeviceId, deviceId) + .when(!CollectionUtils.isEmpty(tags), query -> query.in(DeviceTagEntity::getKey, tags)) + .fetch() + .map(e -> Tuples.of(e.getKey(), e.getValue()))); + + //没有指定标签,从数据库加载全部. + if (tags.isEmpty()) { + return loaderFromDb.collectMap(Tuple2::getT1, Tuple2::getT2); + } + + return Flux + .fromIterable(tags) + .flatMap(tag -> dataManager + //优先从缓存中获取 + .getLastTag(DeviceThingType.device.getId(), + String.valueOf(deviceId), String.valueOf(tag), + System.currentTimeMillis()) + .map(_tag -> { + tags.remove(_tag.getTag()); + return Tuples.of(_tag.getTag(), _tag.getValue()); + })) + .concatWith(Flux.defer(() -> { + if (!tags.isEmpty()) { + return loaderFromDb; + } + return Mono.empty(); + })) + .collectMap(Tuple2::getT1, Tuple2::getT2); + + })); + } +} diff --git a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/function/ReactorQLDeviceSelectorBuilder.java b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/function/ReactorQLDeviceSelectorBuilder.java index eb242042..5a48a674 100755 --- a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/function/ReactorQLDeviceSelectorBuilder.java +++ b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/function/ReactorQLDeviceSelectorBuilder.java @@ -4,25 +4,23 @@ import lombok.AllArgsConstructor; import org.hswebframework.ezorm.core.NestConditional; import org.hswebframework.ezorm.rdb.mapping.ReactiveQuery; import org.hswebframework.ezorm.rdb.mapping.ReactiveRepository; -import org.jetlinks.core.device.DeviceOperator; -import org.jetlinks.core.device.DeviceRegistry; import org.jetlinks.community.device.entity.DeviceInstanceEntity; import org.jetlinks.community.rule.engine.executor.DeviceSelector; import org.jetlinks.community.rule.engine.executor.DeviceSelectorBuilder; import org.jetlinks.community.rule.engine.executor.device.DeviceSelectorProvider; import org.jetlinks.community.rule.engine.executor.device.DeviceSelectorProviders; import org.jetlinks.community.rule.engine.executor.device.DeviceSelectorSpec; +import org.jetlinks.core.device.DeviceOperator; +import org.jetlinks.core.device.DeviceRegistry; import org.jetlinks.reactor.ql.ReactorQL; import org.jetlinks.reactor.ql.ReactorQLContext; import org.jetlinks.reactor.ql.ReactorQLRecord; import org.jetlinks.reactor.ql.feature.FromFeature; -import org.springframework.data.util.Lazy; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import java.util.Map; import java.util.function.BiFunction; -import java.util.function.Function; /** * 基于ReactorQL的设备选择器,通过自定义{@link FromFeature}来实现设备数据源. @@ -37,7 +35,7 @@ import java.util.function.Function; * * * @author zhouhao - * @since 2.0 + * @since 1.5 */ @AllArgsConstructor public class ReactorQLDeviceSelectorBuilder implements DeviceSelectorBuilder { diff --git a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/function/RelationDeviceSelectorProvider.java b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/function/RelationDeviceSelectorProvider.java index f10d28e7..ca6222a3 100644 --- a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/function/RelationDeviceSelectorProvider.java +++ b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/function/RelationDeviceSelectorProvider.java @@ -13,6 +13,7 @@ import org.jetlinks.community.relation.RelationManagerHolder; import org.jetlinks.community.relation.RelationObjectProvider; import org.jetlinks.community.rule.engine.executor.device.DeviceSelectorProvider; import org.jetlinks.community.rule.engine.executor.device.DeviceSelectorSpec; +import org.jetlinks.community.rule.engine.executor.device.SelectorValue; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -39,7 +40,7 @@ public class RelationDeviceSelectorProvider implements DeviceSelectorProvider { return this .applyCondition( source.resolve(ctx).map(String::valueOf), - Flux.fromIterable(source.getSelectorValues()).map(RelationSpec::of), + Flux.fromIterable(source.getSelectorValues()).mapNotNull(SelectorValue::getValue).map(RelationSpec::of), conditional ); } @@ -58,6 +59,8 @@ public class RelationDeviceSelectorProvider implements DeviceSelectorProvider { public > Mono> applyCondition(Flux source, Flux relations, NestConditional conditional) { + // 和上游设备相同关系的设备 + return source .flatMap(deviceId -> relations .flatMap(spec -> { diff --git a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/measurements/DeviceDashboard.java b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/measurements/DeviceDashboard.java index c4af56bb..195d5f23 100644 --- a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/measurements/DeviceDashboard.java +++ b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/measurements/DeviceDashboard.java @@ -8,6 +8,6 @@ public interface DeviceDashboard extends Dashboard { @Override default DashboardDefinition getDefinition() { - return DeviceDashboardDefinition.instance; + return DeviceDashboardDefinition.device; } } diff --git a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/measurements/DeviceDashboardDefinition.java b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/measurements/DeviceDashboardDefinition.java index b7c66f3d..88f40c07 100644 --- a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/measurements/DeviceDashboardDefinition.java +++ b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/measurements/DeviceDashboardDefinition.java @@ -1,16 +1,23 @@ package org.jetlinks.community.device.measurements; import lombok.AllArgsConstructor; +import lombok.Generated; import lombok.Getter; import org.jetlinks.community.dashboard.DashboardDefinition; @Getter @AllArgsConstructor +@Generated public enum DeviceDashboardDefinition implements DashboardDefinition { - instance("device","设备信息"); + device("设备信息"), + product("产品信息"); - private String id; - private String name; + private final String name; + + @Override + public String getId() { + return name(); + } } diff --git a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/measurements/DeviceDashboardObject.java b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/measurements/DeviceDashboardObject.java index 7b37da5c..a860588c 100644 --- a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/measurements/DeviceDashboardObject.java +++ b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/measurements/DeviceDashboardObject.java @@ -1,14 +1,14 @@ package org.jetlinks.community.device.measurements; import lombok.Generated; -import org.jetlinks.community.dashboard.DashboardObject; -import org.jetlinks.community.dashboard.Measurement; -import org.jetlinks.community.dashboard.ObjectDefinition; -import org.jetlinks.community.device.service.data.DeviceDataService; import org.jetlinks.core.device.DeviceProductOperator; import org.jetlinks.core.device.DeviceRegistry; import org.jetlinks.core.event.EventBus; import org.jetlinks.core.metadata.DeviceMetadata; +import org.jetlinks.community.dashboard.DashboardObject; +import org.jetlinks.community.dashboard.Measurement; +import org.jetlinks.community.dashboard.ObjectDefinition; +import org.jetlinks.community.device.service.data.DeviceDataService; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; diff --git a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/measurements/DeviceDynamicDashboard.java b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/measurements/DeviceDynamicDashboard.java index 58edcd51..796ed3a1 100644 --- a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/measurements/DeviceDynamicDashboard.java +++ b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/measurements/DeviceDynamicDashboard.java @@ -1,11 +1,11 @@ package org.jetlinks.community.device.measurements; -import org.jetlinks.community.dashboard.DashboardObject; -import org.jetlinks.community.device.entity.DeviceProductEntity; -import org.jetlinks.community.device.service.LocalDeviceProductService; -import org.jetlinks.community.device.service.data.DeviceDataService; +import org.hswebframework.ezorm.rdb.mapping.ReactiveRepository; import org.jetlinks.core.device.DeviceRegistry; import org.jetlinks.core.event.EventBus; +import org.jetlinks.community.dashboard.DashboardObject; +import org.jetlinks.community.device.entity.DeviceProductEntity; +import org.jetlinks.community.device.service.data.DeviceDataService; import org.springframework.stereotype.Component; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -15,7 +15,7 @@ import javax.annotation.PostConstruct; @Component public class DeviceDynamicDashboard implements DeviceDashboard { - private final LocalDeviceProductService productService; + private final ReactiveRepository productRepository; private final DeviceRegistry registry; @@ -23,11 +23,11 @@ public class DeviceDynamicDashboard implements DeviceDashboard { private final DeviceDataService dataService; - public DeviceDynamicDashboard(LocalDeviceProductService productService, + public DeviceDynamicDashboard(ReactiveRepository productRepository, DeviceRegistry registry, DeviceDataService deviceDataService, EventBus eventBus) { - this.productService = productService; + this.productRepository = productRepository; this.registry = registry; this.eventBus = eventBus; this.dataService = deviceDataService; @@ -40,19 +40,22 @@ public class DeviceDynamicDashboard implements DeviceDashboard { @Override public Flux getObjects() { - return productService.createQuery() + return productRepository + .createQuery() .fetch() .flatMap(this::convertObject); } @Override public Mono getObject(String id) { - return productService.findById(id) + return productRepository + .findById(id) .flatMap(this::convertObject); } protected Mono convertObject(DeviceProductEntity product) { - return registry.getProduct(product.getId()) + return registry + .getProduct(product.getId()) .map(operator -> DeviceDashboardObject.of(product.getId(), product.getName(), operator, eventBus, dataService,registry)); } } diff --git a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/measurements/DeviceEventMeasurement.java b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/measurements/DeviceEventMeasurement.java index 7bd15502..a6e152d1 100644 --- a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/measurements/DeviceEventMeasurement.java +++ b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/measurements/DeviceEventMeasurement.java @@ -1,9 +1,7 @@ package org.jetlinks.community.device.measurements; +import lombok.Generated; import org.hswebframework.web.api.crud.entity.QueryParamEntity; -import org.jetlinks.community.dashboard.*; -import org.jetlinks.community.dashboard.supports.StaticMeasurement; -import org.jetlinks.community.device.service.data.DeviceDataService; import org.jetlinks.core.event.EventBus; import org.jetlinks.core.event.Subscription; import org.jetlinks.core.message.DeviceMessage; @@ -14,6 +12,9 @@ import org.jetlinks.core.metadata.DefaultConfigMetadata; import org.jetlinks.core.metadata.EventMetadata; import org.jetlinks.core.metadata.types.IntType; import org.jetlinks.core.metadata.types.StringType; +import org.jetlinks.community.dashboard.*; +import org.jetlinks.community.dashboard.supports.StaticMeasurement; +import org.jetlinks.community.device.service.data.DeviceDataService; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -75,16 +76,19 @@ class DeviceEventMeasurement extends StaticMeasurement { } @Override + @Generated public DataType getValueType() { return eventMetadata.getType(); } @Override + @Generated public ConfigMetadata getParams() { return configMetadata; } @Override + @Generated public boolean isRealTime() { return true; } diff --git a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/measurements/DeviceEventsMeasurement.java b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/measurements/DeviceEventsMeasurement.java index 89dcd22e..10412098 100644 --- a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/measurements/DeviceEventsMeasurement.java +++ b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/measurements/DeviceEventsMeasurement.java @@ -1,5 +1,6 @@ package org.jetlinks.community.device.measurements; +import lombok.Generated; import org.hswebframework.web.api.crud.entity.QueryParamEntity; import org.jetlinks.community.dashboard.*; import org.jetlinks.community.dashboard.supports.StaticMeasurement; @@ -16,7 +17,6 @@ import reactor.core.publisher.Mono; import java.util.HashMap; import java.util.Map; -import java.util.concurrent.atomic.AtomicLong; class DeviceEventsMeasurement extends StaticMeasurement { @@ -40,15 +40,16 @@ class DeviceEventsMeasurement extends StaticMeasurement { addDimension(new RealTimeDevicePropertyDimension()); } - static AtomicLong num = new AtomicLong(); - Flux fromHistory(String deviceId, int history) { - return history <= 0 ? Flux.empty() : Flux.fromIterable(metadata.getEvents()) - .flatMap(event -> QueryParamEntity.newQuery() - .doPaging(0, history) - .execute(q -> deviceDataService.queryEvent(deviceId, event.getId(), q, false)) - .map(data -> SimpleMeasurementValue.of(createValue(event.getId(), data), data.getTimestamp())) - .sort(MeasurementValue.sort())); + return history <= 0 + ? Flux.empty() + : Flux.fromIterable(metadata.getEvents()) + .flatMap(event -> QueryParamEntity + .newQuery() + .doPaging(0, history) + .execute(q -> deviceDataService.queryEvent(deviceId, event.getId(), q, false)) + .map(data -> SimpleMeasurementValue.of(createValue(event.getId(), data), data.getTimestamp())) + .sort(MeasurementValue.sort())); } Map createValue(String event, Object value) { @@ -87,6 +88,7 @@ class DeviceEventsMeasurement extends StaticMeasurement { } @Override + @Generated public DataType getValueType() { SimplePropertyMetadata property = new SimplePropertyMetadata(); property.setId("event"); @@ -104,11 +106,13 @@ class DeviceEventsMeasurement extends StaticMeasurement { } @Override + @Generated public ConfigMetadata getParams() { return configMetadata; } @Override + @Generated public boolean isRealTime() { return true; } diff --git a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/measurements/DeviceObjectDefinition.java b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/measurements/DeviceObjectDefinition.java index da58d7dd..1621596b 100644 --- a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/measurements/DeviceObjectDefinition.java +++ b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/measurements/DeviceObjectDefinition.java @@ -1,11 +1,13 @@ package org.jetlinks.community.device.measurements; import lombok.AllArgsConstructor; +import lombok.Generated; import lombok.Getter; import org.jetlinks.community.dashboard.ObjectDefinition; @Getter @AllArgsConstructor +@Generated public enum DeviceObjectDefinition implements ObjectDefinition { status("设备状态"), message("设备消息"); diff --git a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/measurements/DevicePropertiesMeasurement.java b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/measurements/DevicePropertiesMeasurement.java index c17e6723..2450ce7a 100644 --- a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/measurements/DevicePropertiesMeasurement.java +++ b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/measurements/DevicePropertiesMeasurement.java @@ -27,7 +27,7 @@ import java.util.function.Function; import java.util.stream.Collectors; @Slf4j -class DevicePropertiesMeasurement extends StaticMeasurement { +public class DevicePropertiesMeasurement extends StaticMeasurement { private final EventBus eventBus; diff --git a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/measurements/DevicePropertyMeasurement.java b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/measurements/DevicePropertyMeasurement.java index df36c459..91d81d0c 100644 --- a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/measurements/DevicePropertyMeasurement.java +++ b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/measurements/DevicePropertyMeasurement.java @@ -1,5 +1,6 @@ package org.jetlinks.community.device.measurements; +import lombok.Generated; import org.hswebframework.utils.time.DateFormatter; import org.hswebframework.web.api.crud.entity.QueryParamEntity; import org.jetlinks.community.Interval; @@ -16,6 +17,7 @@ import org.jetlinks.core.metadata.types.IntType; import org.jetlinks.core.metadata.types.NumberType; import org.jetlinks.core.metadata.types.ObjectType; import org.jetlinks.core.metadata.types.StringType; +import org.jetlinks.core.metadata.unit.ValueUnit; import org.joda.time.DateTime; import org.joda.time.format.DateTimeFormat; import org.joda.time.format.DateTimeFormatter; @@ -26,6 +28,7 @@ import reactor.core.publisher.Mono; import java.util.Date; import java.util.HashMap; import java.util.Map; +import java.util.Optional; class DevicePropertyMeasurement extends StaticMeasurement { @@ -60,6 +63,12 @@ class DevicePropertyMeasurement extends StaticMeasurement { value = type instanceof Converter ? ((Converter) type).convert(value) : value; values.put("value", value); values.put("formatValue", type.format(value)); + if (type instanceof UnitSupported) { + UnitSupported unitSupported = (UnitSupported) type; + values.put("unit", Optional.ofNullable(unitSupported.getUnit()) + .map(ValueUnit::getSymbol) + .orElse(null)); + } return values; } @@ -113,11 +122,13 @@ class DevicePropertyMeasurement extends StaticMeasurement { private class AggDevicePropertyDimension implements MeasurementDimension { @Override + @Generated public DimensionDefinition getDefinition() { return CommonDimensionDefinition.agg; } @Override + @Generated public DataType getValueType() { return new ObjectType() .addProperty("value", "数据", new ObjectType() @@ -128,11 +139,13 @@ class DevicePropertyMeasurement extends StaticMeasurement { } @Override + @Generated public ConfigMetadata getParams() { return aggConfigMetadata; } @Override + @Generated public boolean isRealTime() { return false; } @@ -181,11 +194,13 @@ class DevicePropertyMeasurement extends StaticMeasurement { private class HistoryDevicePropertyDimension implements MeasurementDimension { @Override + @Generated public DimensionDefinition getDefinition() { return CommonDimensionDefinition.history; } @Override + @Generated public DataType getValueType() { return new ObjectType() .addProperty("property", "属性", StringType.GLOBAL) @@ -194,18 +209,21 @@ class DevicePropertyMeasurement extends StaticMeasurement { } @Override + @Generated public ConfigMetadata getParams() { return configMetadata; } @Override + @Generated public boolean isRealTime() { return false; } @Override public Flux getValue(MeasurementParameter parameter) { - return Mono.justOrEmpty(parameter.getString("deviceId")) + return Mono + .justOrEmpty(parameter.getString("deviceId")) .flatMapMany(deviceId -> { int history = parameter.getInt("history").orElse(1); return QueryParamEntity.newQuery() @@ -229,11 +247,13 @@ class DevicePropertyMeasurement extends StaticMeasurement { private class RealTimeDevicePropertyDimension implements MeasurementDimension { @Override + @Generated public DimensionDefinition getDefinition() { return CommonDimensionDefinition.realTime; } @Override + @Generated public DataType getValueType() { return new ObjectType() .addProperty("property", "属性", StringType.GLOBAL) @@ -242,11 +262,13 @@ class DevicePropertyMeasurement extends StaticMeasurement { } @Override + @Generated public ConfigMetadata getParams() { return configMetadata; } @Override + @Generated public boolean isRealTime() { return true; } diff --git a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/measurements/MetadataMeasurementDefinition.java b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/measurements/MetadataMeasurementDefinition.java index 10bda6c7..517bbc2f 100644 --- a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/measurements/MetadataMeasurementDefinition.java +++ b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/measurements/MetadataMeasurementDefinition.java @@ -1,20 +1,24 @@ package org.jetlinks.community.device.measurements; import lombok.AllArgsConstructor; -import org.jetlinks.community.dashboard.MeasurementDefinition; +import lombok.Generated; import org.jetlinks.core.metadata.Metadata; +import org.jetlinks.community.dashboard.MeasurementDefinition; @AllArgsConstructor(staticName = "of") +@Generated public class MetadataMeasurementDefinition implements MeasurementDefinition { Metadata metadata; @Override + @Generated public String getId() { return metadata.getId(); } @Override + @Generated public String getName() { return metadata.getName(); } 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 7857c7f3..2b9d2591 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,15 +1,11 @@ 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; import org.jetlinks.community.device.timeseries.DeviceTimeSeriesMetric; import org.jetlinks.community.timeseries.TimeSeriesManager; -import org.jetlinks.community.timeseries.TimeSeriesMetric; import org.jetlinks.community.timeseries.query.AggregationQueryParam; -import org.jetlinks.core.device.DeviceProductOperator; -import org.jetlinks.core.device.DeviceRegistry; import org.jetlinks.core.event.EventBus; import org.jetlinks.core.event.Subscription; import org.jetlinks.core.event.TopicPayload; @@ -20,14 +16,57 @@ import org.jetlinks.core.metadata.types.DateTimeType; import org.jetlinks.core.metadata.types.IntType; import org.jetlinks.core.metadata.types.StringType; import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; import java.time.Duration; import java.time.LocalDateTime; import java.time.ZoneId; import java.util.Date; -import java.util.List; +/** + * 设备消息监控支持 + * + *
{@code
+ * POST /dashboard/_multi
+ *
+ * [
+ *     {
+ *         "dashboard": "device",
+ *         "object": "message",
+ *         "measurement": "quantity",
+ *         "dimension": "agg",
+ *         "group": "device_msg",
+ *         "params": {
+ *              "format":"yyyy-MM-dd HH:mm",
+ *              "time":"10m",
+ *              "from":"now-2h",
+ *              "to":"now",
+ *              "limit":2
+ *         }
+ *     }
+ * ]
+ *
+ * [{
+ * 	"group": "device_msg",
+ * 	"data": {
+ * 		"value": 297,
+ * 		"timeString": "2022-05-30 14:20",
+ * 		"timestamp": 0
+ *      }
+ * }, {
+ * 	"group": "device_msg",
+ * 	"data": {
+ * 		"value": 657,
+ * 		"timeString": "2022-05-30 14:10",
+ * 		"timestamp": 1
+ *    }
+ * }]
+ *
+ *
+ * }
+ * + * @author zhouhao + * @since 1.0 + */ class DeviceMessageMeasurement extends StaticMeasurement { private final EventBus eventBus; @@ -157,6 +196,7 @@ class DeviceMessageMeasurement extends StaticMeasurement { data.getString("time").orElse(""), 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 917f60c5..ff08e206 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,11 +21,11 @@ public class DeviceMessageMeasurementProvider extends StaticMeasurementProvider public DeviceMessageMeasurementProvider(EventBus eventBus, MeterRegistryManager registryManager, + DeviceRegistry deviceRegistry, TimeSeriesManager timeSeriesManager) { - super(DeviceDashboardDefinition.instance, DeviceObjectDefinition.message); + super(DeviceDashboardDefinition.device, DeviceObjectDefinition.message); - registry = registryManager.getMeterRegister(DeviceTimeSeriesMetric.deviceMetrics().getId(), - "target", "msgType", "productId"); + registry = registryManager.getMeterRegister(DeviceTimeSeriesMetric.deviceMetrics().getId()); addMeasurement(new DeviceMessageMeasurement(eventBus, timeSeriesManager)); @@ -50,4 +50,5 @@ public class DeviceMessageMeasurementProvider extends StaticMeasurementProvider "productId", message.getHeader("productId").map(String::valueOf).orElse("unknown") }; } + } diff --git a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/measurements/status/DeviceStatusChangeMeasurement.java b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/measurements/status/DeviceStatusChangeMeasurement.java index 9dbb1391..c40216ae 100644 --- a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/measurements/status/DeviceStatusChangeMeasurement.java +++ b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/measurements/status/DeviceStatusChangeMeasurement.java @@ -1,11 +1,13 @@ package org.jetlinks.community.device.measurements.status; -import org.jetlinks.community.Interval; +import lombok.Generated; import org.jetlinks.community.dashboard.*; import org.jetlinks.community.dashboard.supports.StaticMeasurement; +import org.jetlinks.community.device.enums.DeviceState; import org.jetlinks.community.device.timeseries.DeviceTimeSeriesMetric; import org.jetlinks.community.timeseries.TimeSeriesManager; import org.jetlinks.community.timeseries.query.AggregationQueryParam; +import org.jetlinks.core.device.DeviceRegistry; import org.jetlinks.core.event.EventBus; import org.jetlinks.core.event.Subscription; import org.jetlinks.core.message.DeviceMessage; @@ -35,6 +37,8 @@ class DeviceStatusChangeMeasurement extends StaticMeasurement { private final TimeSeriesManager timeSeriesManager; + private final DeviceRegistry deviceRegistry; + static MeasurementDefinition definition = MeasurementDefinition.of("change", "设备状态变更"); static ConfigMetadata configMetadata = new DefaultConfigMetadata() @@ -44,8 +48,11 @@ class DeviceStatusChangeMeasurement extends StaticMeasurement { .addElement(EnumType.Element.of(MessageType.OFFLINE.name().toLowerCase(), "离线")) .addElement(EnumType.Element.of(MessageType.ONLINE.name().toLowerCase(), "在线")); - public DeviceStatusChangeMeasurement(TimeSeriesManager timeSeriesManager, EventBus eventBus) { + public DeviceStatusChangeMeasurement(TimeSeriesManager timeSeriesManager, + EventBus eventBus, + DeviceRegistry deviceRegistry) { super(definition); + this.deviceRegistry = deviceRegistry; this.eventBus = eventBus; this.timeSeriesManager = timeSeriesManager; addDimension(new RealTimeDeviceStateDimension()); @@ -75,46 +82,65 @@ class DeviceStatusChangeMeasurement extends StaticMeasurement { } @Override + @Generated public DataType getValueType() { return historyValueType; } @Override + @Generated public ConfigMetadata getParams() { return historyConfigMetadata; } @Override + @Generated public boolean isRealTime() { return false; } + private AggregationQueryParam createQueryParam(MeasurementParameter parameter) { + String format = parameter.getString("format").orElse("yyyy年MM月dd日"); + return AggregationQueryParam + .of() + .sum("count") + .groupBy(parameter.getInterval("time", null), format) + .filter(query -> + query.where("name", parameter.getString("type").orElse("online")) + .is("productId", parameter.getString("productId").orElse(null)) + ) + .limit(parameter.getInt("limit").orElse(1)) + .from(parameter + .getDate("from") + .orElse(Date.from(LocalDateTime + .now() + .plusDays(-1) + .atZone(ZoneId.systemDefault()) + .toInstant()))) + .to(parameter.getDate("to").orElse(new Date())); + } + @Override public Flux getValue(MeasurementParameter parameter) { String format = parameter.getString("format").orElse("yyyy年MM月dd日"); DateTimeFormatter formatter = DateTimeFormat.forPattern(format); + AggregationQueryParam param = createQueryParam(parameter); - return AggregationQueryParam.of() - .sum("count") - .groupBy(parameter.getInterval("time", Interval.ofDays(1)), format) - .filter(query -> - query.where("name", parameter.getString("type").orElse("online")) - .is("productId", parameter.getString("productId").orElse(null)) - ) - .limit(parameter.getInt("limit").orElse(1)) - .from(parameter.getDate("from").orElse(Date.from(LocalDateTime.now().plusDays(-1).atZone(ZoneId.systemDefault()).toInstant()))) - .to(parameter.getDate("to").orElse(new Date())) - .execute(timeSeriesManager.getService(DeviceTimeSeriesMetric.deviceMetrics())::aggregation) - .map(data -> { - long ts = data.getString("time") - .map(time -> DateTime.parse(time, formatter).getMillis()) - .orElse(System.currentTimeMillis()); - return SimpleMeasurementValue.of( - data.get("count").orElse(0), - data.getString("time", ""), - ts); - }) - .sort(); + return Flux + .defer(() -> param + .execute(timeSeriesManager.getService(DeviceTimeSeriesMetric.deviceMetrics())::aggregation) + .map(data -> { + long ts = data + .getString("time") + .map(time -> DateTime.parse(time, formatter).getMillis()) + .orElse(System.currentTimeMillis()); + return SimpleMeasurementValue.of( + data.get("count").orElse(0), + data.getString("time", ""), + ts); + }) + .sort()) + .take(param.getLimit()); } } @@ -143,10 +169,9 @@ class DeviceStatusChangeMeasurement extends StaticMeasurement { return true; } + @Override public Flux getValue(MeasurementParameter parameter) { - - return Mono.justOrEmpty(parameter.getString("deviceId")) .flatMapMany(deviceId ->//从消息网关订阅消息 eventBus.subscribe(Subscription.of( @@ -167,5 +192,13 @@ class DeviceStatusChangeMeasurement extends StaticMeasurement { val.put("deviceId", message.getDeviceId()); return val; } + + MeasurementValue createMeasurementValue(String deviceId, DeviceState state, long timestamp) { + Map val = new HashMap<>(); + + val.put("type", state.getValue()); + val.put("deviceId", deviceId); + return SimpleMeasurementValue.of(val, timestamp); + } } } diff --git a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/measurements/status/DeviceStatusMeasurementProvider.java b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/measurements/status/DeviceStatusMeasurementProvider.java index 9d0016a8..5db87008 100644 --- a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/measurements/status/DeviceStatusMeasurementProvider.java +++ b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/measurements/status/DeviceStatusMeasurementProvider.java @@ -1,63 +1,38 @@ package org.jetlinks.community.device.measurements.status; import io.micrometer.core.instrument.MeterRegistry; +import org.hswebframework.ezorm.rdb.mapping.ReactiveRepository; +import org.jetlinks.core.device.DeviceRegistry; +import org.jetlinks.core.event.EventBus; import org.jetlinks.community.dashboard.supports.StaticMeasurementProvider; +import org.jetlinks.community.device.entity.DeviceInstanceEntity; import org.jetlinks.community.device.measurements.DeviceDashboardDefinition; import org.jetlinks.community.device.measurements.DeviceObjectDefinition; -import org.jetlinks.community.device.service.LocalDeviceInstanceService; import org.jetlinks.community.device.timeseries.DeviceTimeSeriesMetric; -import org.jetlinks.community.gateway.annotation.Subscribe; import org.jetlinks.community.micrometer.MeterRegistryManager; import org.jetlinks.community.timeseries.TimeSeriesManager; -import org.jetlinks.core.event.EventBus; -import org.jetlinks.core.message.DeviceMessage; import org.springframework.stereotype.Component; -import reactor.core.publisher.Mono; @Component public class DeviceStatusMeasurementProvider extends StaticMeasurementProvider { - private final MeterRegistry registry; + private final DeviceRegistry deviceRegistry; + public DeviceStatusMeasurementProvider(MeterRegistryManager registryManager, - LocalDeviceInstanceService instanceService, + ReactiveRepository deviceRepository, TimeSeriesManager timeSeriesManager, - EventBus eventBus) { - super(DeviceDashboardDefinition.instance, DeviceObjectDefinition.status); + EventBus eventBus, + DeviceRegistry deviceRegistry) { + super(DeviceDashboardDefinition.device, DeviceObjectDefinition.status); - addMeasurement(new DeviceStatusChangeMeasurement(timeSeriesManager, eventBus)); + addMeasurement(new DeviceStatusChangeMeasurement(timeSeriesManager, eventBus, deviceRegistry)); - addMeasurement(new DeviceStatusRecordMeasurement(instanceService, timeSeriesManager)); + addMeasurement(new DeviceStatusRecordMeasurement(deviceRepository, timeSeriesManager)); - registry = registryManager.getMeterRegister(DeviceTimeSeriesMetric.deviceMetrics().getId(), - "target", "msgType", "productId"); + registry = registryManager.getMeterRegister(DeviceTimeSeriesMetric.deviceMetrics().getId()); + this.deviceRegistry = deviceRegistry; } - @Subscribe("/device/*/*/online") - public Mono incrementOnline(DeviceMessage msg) { - return Mono.fromRunnable(() -> { - String productId = parseProductId(msg); - registry - .counter("online", "productId", productId) - .increment(); - }); - } - - @Subscribe("/device/*/*/offline") - public Mono incrementOffline(DeviceMessage msg) { - return Mono.fromRunnable(() -> { - String productId = parseProductId(msg); - registry - .counter("offline", "productId", productId) - .increment(); - }); - } - - private String parseProductId(DeviceMessage msg) { - return msg - .getHeader("productId") - .map(String::valueOf) - .orElse("unknown"); - } } diff --git a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/measurements/status/DeviceStatusRecordMeasurement.java b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/measurements/status/DeviceStatusRecordMeasurement.java index d5c86bfc..9443f627 100644 --- a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/measurements/status/DeviceStatusRecordMeasurement.java +++ b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/measurements/status/DeviceStatusRecordMeasurement.java @@ -1,13 +1,15 @@ package org.jetlinks.community.device.measurements.status; +import lombok.Generated; +import org.hswebframework.ezorm.rdb.mapping.ReactiveRepository; import org.jetlinks.community.Interval; import org.jetlinks.community.dashboard.*; import org.jetlinks.community.dashboard.supports.StaticMeasurement; import org.jetlinks.community.device.entity.DeviceInstanceEntity; import org.jetlinks.community.device.enums.DeviceState; -import org.jetlinks.community.device.service.LocalDeviceInstanceService; import org.jetlinks.community.device.timeseries.DeviceTimeSeriesMetric; import org.jetlinks.community.timeseries.TimeSeriesManager; +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; @@ -16,6 +18,7 @@ import org.jetlinks.core.metadata.types.DateTimeType; import org.jetlinks.core.metadata.types.EnumType; import org.jetlinks.core.metadata.types.IntType; import org.jetlinks.core.metadata.types.StringType; +import org.jetlinks.reactor.ql.ReactorQL; import org.joda.time.DateTime; import org.joda.time.format.DateTimeFormat; import org.joda.time.format.DateTimeFormatter; @@ -29,17 +32,17 @@ import java.util.Date; class DeviceStatusRecordMeasurement extends StaticMeasurement { - public LocalDeviceInstanceService instanceService; + public final ReactiveRepository deviceRepository; - private TimeSeriesManager timeSeriesManager; + private final TimeSeriesManager timeSeriesManager; static MeasurementDefinition definition = MeasurementDefinition.of("record", "设备状态记录"); - public DeviceStatusRecordMeasurement(LocalDeviceInstanceService deviceInstanceService, + public DeviceStatusRecordMeasurement(ReactiveRepository deviceRepository, TimeSeriesManager timeSeriesManager) { super(definition); this.timeSeriesManager = timeSeriesManager; - this.instanceService = deviceInstanceService; + this.deviceRepository = deviceRepository; addDimension(new CurrentNumberOfDeviceDimension()); addDimension(new AggNumberOfOnlineDeviceDimension()); } @@ -62,20 +65,29 @@ class DeviceStatusRecordMeasurement } @Override + @Generated public DataType getValueType() { return new IntType(); } @Override + @Generated public ConfigMetadata getParams() { return aggConfigMetadata; } @Override + @Generated public boolean isRealTime() { return false; } + ReactorQL ql = ReactorQL + .builder() + .sql("select time,sum(value) value from dual group by time") + .build(); + + //select time,sum(value) value from dual group by time @Override public Flux getValue(MeasurementParameter parameter) { String format = parameter.getString("format").orElse("yyyy年MM月dd日"); @@ -84,9 +96,7 @@ class DeviceStatusRecordMeasurement return AggregationQueryParam .of() .max("value") - .filter(query -> - query.where("name", "gateway-server-session") - ) + .filter(query -> query.where("name", "gateway-server-session")) .from(parameter .getDate("from") .orElse(Date.from(LocalDateTime @@ -95,20 +105,22 @@ class DeviceStatusRecordMeasurement .atZone(ZoneId.systemDefault()) .toInstant()))) .to(parameter.getDate("to").orElse(new Date())) - .groupBy(parameter.getInterval("time").orElse(Interval.ofDays(1)), - parameter.getString("format").orElse("yyyy年MM月dd日")) + .groupBy(parameter.getInterval("time").orElse(Interval.ofDays(1)), format) + .groupBy("server") .limit(parameter.getInt("limit").orElse(10)) .execute(timeSeriesManager.getService(DeviceTimeSeriesMetric.deviceMetrics())::aggregation) + .map(AggregationData::asMap) + .as(ql::start) .map(data -> { - long ts = data.getString("time") - .map(time -> DateTime.parse(time, formatter).getMillis()) - .orElse(System.currentTimeMillis()); + String timeStr = String.valueOf(data.get("time")); + long ts = DateTime.parse(timeStr, formatter).getMillis(); return SimpleMeasurementValue.of( - data.get("value").orElse(0), - data.getString("time", ""), + data.getOrDefault("value", 0), + timeStr, ts); }) - .sort(); + .sort() + .take(parameter.getLong("limit").orElse(10L)); } } @@ -129,23 +141,26 @@ class DeviceStatusRecordMeasurement } @Override + @Generated public DataType getValueType() { return new IntType(); } @Override + @Generated public ConfigMetadata getParams() { return currentMetadata; } @Override + @Generated public boolean isRealTime() { return false; } @Override public Mono getValue(MeasurementParameter parameter) { - return instanceService + return deviceRepository .createQuery() .and(DeviceInstanceEntity::getProductId, parameter.getString("productId").orElse(null)) .and(DeviceInstanceEntity::getState, parameter.get("state", DeviceState.class).orElse(null)) diff --git a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/message/DefaultDeviceDataManager.java b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/message/DefaultDeviceDataManager.java index b0a324b2..f26315ce 100644 --- a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/message/DefaultDeviceDataManager.java +++ b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/message/DefaultDeviceDataManager.java @@ -2,14 +2,10 @@ package org.jetlinks.community.device.message; import com.github.benmanes.caffeine.cache.Caffeine; import lombok.AllArgsConstructor; +import lombok.Generated; import lombok.Getter; import org.hswebframework.ezorm.rdb.mapping.ReactiveRepository; import org.hswebframework.web.api.crud.entity.QueryParamEntity; -import org.jetlinks.community.device.entity.DeviceProperty; -import org.jetlinks.community.device.entity.DeviceTagEntity; -import org.jetlinks.community.device.service.data.DeviceDataService; -import org.jetlinks.community.gateway.DeviceMessageUtils; -import org.jetlinks.community.gateway.annotation.Subscribe; import org.jetlinks.core.device.DeviceConfigKey; import org.jetlinks.core.device.DeviceOperator; import org.jetlinks.core.device.DeviceRegistry; @@ -19,7 +15,10 @@ import org.jetlinks.core.message.DeviceDataManager; import org.jetlinks.core.message.DeviceMessage; import org.jetlinks.core.metadata.Converter; import org.jetlinks.core.metadata.PropertyMetadata; -import org.springframework.stereotype.Component; +import org.jetlinks.community.device.entity.DeviceProperty; +import org.jetlinks.community.device.entity.DeviceTagEntity; +import org.jetlinks.community.device.service.data.DeviceDataService; +import org.jetlinks.community.gateway.DeviceMessageUtils; import org.springframework.util.Assert; import reactor.core.Disposable; import reactor.core.publisher.Flux; @@ -38,8 +37,8 @@ import java.util.function.Function; * @since 1.9 */ //@Component - @Deprecated @AllArgsConstructor +@Deprecated public class DefaultDeviceDataManager implements DeviceDataManager { private final DeviceRegistry registry; @@ -68,9 +67,7 @@ public class DefaultDeviceDataManager implements DeviceDataManager { @Override public Mono getLastProperty(@Nonnull String deviceId, @Nonnull String propertyId) { - return localCache - .computeIfAbsent(deviceId, id -> new DevicePropertyRef(id, eventBus, dataService)) - .getLastProperty(propertyId, System.currentTimeMillis()); + return getLastProperty(deviceId, propertyId, System.currentTimeMillis()); } @Override @@ -119,7 +116,9 @@ public class DefaultDeviceDataManager implements DeviceDataManager { @Getter public static class DefaultTagValue implements TagValue { + @Generated private String tagId; + @Generated private Object value; public static DefaultTagValue of(String id, String value, PropertyMetadata metadata) { @@ -135,31 +134,15 @@ public class DefaultDeviceDataManager implements DeviceDataManager { } } - //更新首次上报属性的时间 - @Subscribe(topics = { - "/device/*/*/message/property/report", - "/device/*/*/message/property/read,write/reply" - }, features = Subscription.Feature.local) - public Mono upgradeDeviceFirstPropertyTime(DeviceMessage message) { - return registry - .getDevice(message.getDeviceId()) - .flatMap(device -> device - .getSelfConfig(DeviceConfigKey.firstPropertyTime) - //没有首次上报时间就更新,设备注销后,首次上报属性时间也将失效. - .switchIfEmpty(device - .setConfig(DeviceConfigKey.firstPropertyTime, message.getTimestamp()) - .thenReturn(message.getTimestamp()) - )) - .then(); - } - - private static class PropertyRef implements PropertyValue { @Getter + @Generated private volatile Object value; @Getter + @Generated private volatile long timestamp; @Getter + @Generated private volatile String state; //上一个 @@ -291,8 +274,9 @@ public class DefaultDeviceDataManager implements DeviceDataManager { QueryParamEntity .newQuery() .orderByAsc("timestamp") - .getParam() - ) + .doPaging(0, 1) + .getParam(), + property) .take(1) .singleOrEmpty() .map(prop -> ref.setFirst(prop.getValue(), prop.getTimestamp())) diff --git a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/message/DeviceBatchOperationSubscriptionProvider.java b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/message/DeviceBatchOperationSubscriptionProvider.java index 70e0adc1..d206447f 100644 --- a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/message/DeviceBatchOperationSubscriptionProvider.java +++ b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/message/DeviceBatchOperationSubscriptionProvider.java @@ -2,12 +2,13 @@ package org.jetlinks.community.device.message; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONObject; +import lombok.Generated; import org.hswebframework.web.api.crud.entity.QueryParamEntity; +import org.jetlinks.core.utils.TopicUtils; import org.jetlinks.community.device.entity.DeviceInstanceEntity; import org.jetlinks.community.device.service.LocalDeviceInstanceService; import org.jetlinks.community.gateway.external.SubscribeRequest; import org.jetlinks.community.gateway.external.SubscriptionProvider; -import org.jetlinks.core.utils.TopicUtils; import org.springframework.stereotype.Component; import reactor.core.publisher.Flux; import reactor.core.scheduler.Schedulers; @@ -15,6 +16,7 @@ import reactor.core.scheduler.Schedulers; import java.util.Map; @Component +@Generated public class DeviceBatchOperationSubscriptionProvider implements SubscriptionProvider { private final LocalDeviceInstanceService instanceService; @@ -55,7 +57,6 @@ public class DeviceBatchOperationSubscriptionProvider implements SubscriptionPro }).map(json -> json.toJavaObject(QueryParamEntity.class)) .orElseGet(QueryParamEntity::new); - Map var = TopicUtils.getPathVariables("/device-batch/{type}", topic); String type = var.get("type"); diff --git a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/message/DeviceCurrentStateSubscriptionProvider.java b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/message/DeviceCurrentStateSubscriptionProvider.java index 36ada812..08df3ae1 100644 --- a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/message/DeviceCurrentStateSubscriptionProvider.java +++ b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/message/DeviceCurrentStateSubscriptionProvider.java @@ -1,10 +1,12 @@ package org.jetlinks.community.device.message; import lombok.AllArgsConstructor; +import lombok.Generated; +import org.hswebframework.ezorm.rdb.mapping.ReactiveRepository; import org.jetlinks.community.device.entity.DeviceInstanceEntity; -import org.jetlinks.community.device.service.LocalDeviceInstanceService; import org.jetlinks.community.gateway.external.SubscribeRequest; import org.jetlinks.community.gateway.external.SubscriptionProvider; +import org.jetlinks.reactor.ql.utils.CastUtils; import org.springframework.stereotype.Component; import reactor.core.publisher.Flux; @@ -16,19 +18,22 @@ import java.util.Map; @AllArgsConstructor public class DeviceCurrentStateSubscriptionProvider implements SubscriptionProvider { - private final LocalDeviceInstanceService instanceService; + private final ReactiveRepository deviceRepository; @Override + @Generated public String id() { return "device-state-subscriber"; } @Override + @Generated public String name() { return "设备当前状态消息"; } @Override + @Generated public String[] getTopicPattern() { return new String[]{ "/device-current-state" @@ -37,16 +42,19 @@ public class DeviceCurrentStateSubscriptionProvider implements SubscriptionProvi @Override @SuppressWarnings("all") + @Generated public Flux> subscribe(SubscribeRequest request) { - List deviceId = request.get("deviceId") - .map(List.class::cast) - .orElseThrow(() -> new IllegalArgumentException("deviceId不能为空")); + List deviceId = request + .get("deviceId") + .map(CastUtils::castArray) + .orElseThrow(() -> new IllegalArgumentException("error.deviceId_cannot_be_empty")); return Flux .fromIterable(deviceId) .buffer(200) .concatMap(buf -> { - return instanceService.createQuery() + return deviceRepository + .createQuery() .select(DeviceInstanceEntity::getId, DeviceInstanceEntity::getState) .in(DeviceInstanceEntity::getId, buf) .fetch(); diff --git a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/message/DeviceMessageConnector.java b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/message/DeviceMessageConnector.java index 0fb715e1..6c42cc2c 100644 --- a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/message/DeviceMessageConnector.java +++ b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/message/DeviceMessageConnector.java @@ -39,10 +39,10 @@ public class DeviceMessageConnector implements DecodedClientMessageHandler { //将设备注册中心的配置追加到消息header中,下游订阅者可直接使用. private final static String[] allConfigHeader = { - PropertyConstants.productId.getKey(), - PropertyConstants.productName.getKey(), - PropertyConstants.deviceName.getKey(), - PropertyConstants.orgId.getKey() + PropertyConstants.productId.getKey(), + PropertyConstants.productName.getKey(), + PropertyConstants.deviceName.getKey(), + PropertyConstants.orgId.getKey() }; private final static Function> doOnError = (error) -> { DeviceMessageConnector.log.error(error.getMessage(), error); @@ -204,8 +204,8 @@ public class DeviceMessageConnector implements DecodedClientMessageHandler { message.addHeader("parentId", child.getParentDevice().getDeviceId()); } return this - .onMessage(message) - .onErrorResume(doOnError); + .onMessage(message) + .onErrorResume(doOnError); }); @@ -221,20 +221,20 @@ public class DeviceMessageConnector implements DecodedClientMessageHandler { return Mono.empty(); } return deviceRegistry - .getDevice(deviceId) - .flatMap(configGetter) - .defaultIfEmpty(emptyValues) - .flatMapIterable(configs -> { - configs.getAllValues().forEach(deviceMessage::addHeader); - String productId = deviceMessage.getHeader(PropertyConstants.productId).orElse("null"); - String topic = createDeviceMessageTopic(productId, deviceId, deviceMessage); - List topics = new ArrayList<>(2); - topics.add(topic); - configs.getValue(PropertyConstants.orgId) - .ifPresent(orgId -> topics.add("/org/" + orgId + topic)); + .getDevice(deviceId) + .flatMap(configGetter) + .defaultIfEmpty(emptyValues) + .flatMapIterable(configs -> { + configs.getAllValues().forEach(deviceMessage::addHeader); + String productId = deviceMessage.getHeader(PropertyConstants.productId).orElse("null"); + String topic = createDeviceMessageTopic(productId, deviceId, deviceMessage); + List topics = new ArrayList<>(2); + topics.add(topic); + configs.getValue(PropertyConstants.orgId) + .ifPresent(orgId -> topics.add("/org/" + orgId + topic)); - return topics; - }); + return topics; + }); } return Mono.just("/device/unknown/message/unknown"); }); @@ -242,10 +242,10 @@ public class DeviceMessageConnector implements DecodedClientMessageHandler { public static String createDeviceMessageTopic(String productId, String deviceId, DeviceMessage message) { StringBuilder builder = new StringBuilder(64) - .append("/device/") - .append(productId) - .append("/") - .append(deviceId); + .append("/device/") + .append(productId) + .append("/") + .append(deviceId); appendDeviceMessageTopic(message, builder); return builder.toString(); @@ -277,22 +277,22 @@ public class DeviceMessageConnector implements DecodedClientMessageHandler { } message.addHeaderIfAbsent(PropertyConstants.uid, IDGenerator.RANDOM.generate()); return this - .getTopic(message) - .flatMap(topic -> eventBus.publish(topic, message).then()) - .onErrorResume(doOnError) - .then(); + .getTopic(message) + .flatMap(topic -> eventBus.publish(topic, message).then()) + .onErrorResume(doOnError) + .then(); } private Flux getTopic(Message message) { Flux topicsStream = createDeviceMessageTopic(registry, message); if (message instanceof ChildDeviceMessage) { //子设备消息 return this - .onMessage(((ChildDeviceMessage) message).getChildDeviceMessage()) - .thenMany(topicsStream); + .onMessage(((ChildDeviceMessage) message).getChildDeviceMessage()) + .thenMany(topicsStream); } else if (message instanceof ChildDeviceMessageReply) { //子设备消息 return this - .onMessage(((ChildDeviceMessageReply) message).getChildDeviceMessage()) - .thenMany(topicsStream); + .onMessage(((ChildDeviceMessageReply) message).getChildDeviceMessage()) + .thenMany(topicsStream); } return topicsStream; } @@ -337,10 +337,10 @@ public class DeviceMessageConnector implements DecodedClientMessageHandler { Mono then; if (message instanceof ChildDeviceMessageReply) { then = this - .doReply(((ChildDeviceMessageReply) message)) - .then( - handleChildrenDeviceMessageReply(((ChildDeviceMessageReply) message)) - ); + .doReply(((ChildDeviceMessageReply) message)) + .then( + handleChildrenDeviceMessageReply(((ChildDeviceMessageReply) message)) + ); } else if (message instanceof ChildDeviceMessage) { then = handleChildrenDeviceMessageReply(((ChildDeviceMessage) message)); } else if (message instanceof DeviceMessageReply) { @@ -349,9 +349,9 @@ public class DeviceMessageConnector implements DecodedClientMessageHandler { then = Mono.just(true); } return this - .onMessage(message) - .then(then) - .defaultIfEmpty(false); + .onMessage(message) + .then(then) + .defaultIfEmpty(false); } @@ -366,9 +366,9 @@ public class DeviceMessageConnector implements DecodedClientMessageHandler { log.debug("reply message {}", reply.getMessageId()); } return messageHandler - .reply(reply) - .thenReturn(true) - .doOnError((error) -> log.error("reply message error", error)) - ; + .reply(reply) + .thenReturn(true) + .doOnError((error) -> log.error("reply message error", error)) + ; } } diff --git a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/message/DeviceMessageSendLogInterceptor.java b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/message/DeviceMessageSendLogInterceptor.java index 6c7dd276..f1c8f29d 100644 --- a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/message/DeviceMessageSendLogInterceptor.java +++ b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/message/DeviceMessageSendLogInterceptor.java @@ -2,14 +2,16 @@ package org.jetlinks.community.device.message; import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.hswebframework.web.logger.ReactiveLogger; -import org.jetlinks.community.PropertyMetadataConstants; +import org.hswebframework.web.id.IDGenerator; import org.jetlinks.core.device.DeviceOperator; import org.jetlinks.core.device.DeviceRegistry; import org.jetlinks.core.enums.ErrorCode; import org.jetlinks.core.event.EventBus; import org.jetlinks.core.exception.DeviceOperationException; -import org.jetlinks.core.message.*; +import org.jetlinks.core.message.DeviceMessage; +import org.jetlinks.core.message.Headers; +import org.jetlinks.core.message.Message; +import org.jetlinks.core.message.RepayableDeviceMessage; import org.jetlinks.core.message.function.FunctionInvokeMessage; import org.jetlinks.core.message.function.FunctionParameter; import org.jetlinks.core.message.interceptor.DeviceMessageSenderInterceptor; @@ -18,10 +20,13 @@ import org.jetlinks.core.message.property.WritePropertyMessageReply; import org.jetlinks.core.metadata.FunctionMetadata; import org.jetlinks.core.metadata.PropertyMetadata; import org.jetlinks.core.metadata.ValidateResult; +import org.jetlinks.community.PropertyConstants; +import org.jetlinks.community.PropertyMetadataConstants; import org.springframework.stereotype.Component; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import java.util.LinkedHashMap; import java.util.Map; import java.util.function.Function; import java.util.stream.Collectors; @@ -33,7 +38,7 @@ import java.util.stream.Collectors; * @since 1.1 */ @Component -@Slf4j(topic = "system.device.message.sender") +@Slf4j @AllArgsConstructor public class DeviceMessageSendLogInterceptor implements DeviceMessageSenderInterceptor { @@ -42,17 +47,12 @@ public class DeviceMessageSendLogInterceptor implements DeviceMessageSenderInter private final DeviceRegistry registry; public Mono doPublish(Message message) { - Mono then = Mono.empty(); - if(message.getHeader(Headers.dispatchToParent).orElse(false)){ - return then; - } - if (message instanceof ChildDeviceMessage) { - then = doPublish(((ChildDeviceMessage) message).getChildDeviceMessage()); - } + message.addHeader(PropertyConstants.uid, IDGenerator.RANDOM.generate()); return DeviceMessageConnector .createDeviceMessageTopic(registry, message) .flatMap(topic -> eventBus.publish(topic, message)) - .then(then); + .then() + ; } private Mono convertParameterType(DeviceOperator device, FunctionInvokeMessage message) { @@ -73,7 +73,7 @@ public class DeviceMessageSendLogInterceptor implements DeviceMessageSenderInter message.addHeaderIfAbsent(Headers.async, function.isAsync()); for (PropertyMetadata input : function.getInputs()) { FunctionParameter parameter = parameters.get(input.getId()); - if (parameter == null) { + if (parameter == null || parameter.getValue() == null) { continue; } ValidateResult result = input.getValueType().validate(parameter.getValue()); @@ -83,25 +83,44 @@ public class DeviceMessageSendLogInterceptor implements DeviceMessageSenderInter .thenReturn(message); } - private Mono prepareMessage(DeviceOperator device, DeviceMessage message) { + private Mono convertProperty(DeviceOperator device, WritePropertyMessage message) { + if (message.getHeader(Headers.force).orElse(false)) { + return Mono.just(message); + } + Map properties = new LinkedHashMap<>(message.getProperties()); + //手动写值的属性则直接返回 + return device + .getMetadata() + .doOnNext(metadata -> { + for (Map.Entry entry : properties.entrySet()) { + PropertyMetadata propertyMetadata = metadata.getPropertyOrNull(entry.getKey()); + if (propertyMetadata == null || entry.getValue() == null) { + continue; + } + entry.setValue(propertyMetadata + .getValueType() + .validate(entry.getValue()) + .assertSuccess()); + if (properties.size() == 1) { + if (PropertyMetadataConstants.Source.isManual(propertyMetadata)) { + //标记手动回复 + message.addHeader( + PropertyMetadataConstants.Source.headerKey, PropertyMetadataConstants.Source.manual + ); + } + } + } + message.setProperties(properties); + }) + .thenReturn(message); + } + + protected Mono prepareMessage(DeviceOperator device, DeviceMessage message) { if (message instanceof FunctionInvokeMessage) { return convertParameterType(device, ((FunctionInvokeMessage) message)); } if (message instanceof WritePropertyMessage) { - Map properties = ((WritePropertyMessage) message).getProperties(); - if (properties.size() == 1) { - String property = properties.keySet().iterator().next(); -// Object value = properties.values().iterator().next(); - //手动写值的属性则直接返回 - return device - .getMetadata() - .doOnNext(metadata -> metadata - .getProperty(property) - .filter(PropertyMetadataConstants.Source::isManual) - //标记手动回复 - .ifPresent(ignore -> message.addHeader(PropertyMetadataConstants.Source.headerKey, PropertyMetadataConstants.Source.manual))) - .thenReturn(message); - } + return convertProperty(device, (WritePropertyMessage) message); } return Mono.just(message); } @@ -128,16 +147,14 @@ public class DeviceMessageSendLogInterceptor implements DeviceMessageSenderInter if (message instanceof RepayableDeviceMessage) { return this .prepareMessage(device, message) - .flatMap(msg -> this - .doPublish(msg) - .thenReturn(msg) - .doOnEach(ReactiveLogger.onComplete(() -> { - if (log.isDebugEnabled()) { - log.debug("向设备[{}]发送指令:{}", msg.getDeviceId(), msg.toString()); - } - }))); + .flatMap(msg -> this.doPublish(msg).thenReturn(msg)); } else { return Mono.just(message); } } + + @Override + public int getOrder() { + return Integer.MIN_VALUE+1000; + } } diff --git a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/message/DeviceMessageSendSubscriptionProvider.java b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/message/DeviceMessageSendSubscriptionProvider.java index 61cbecaa..ca6c4db7 100644 --- a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/message/DeviceMessageSendSubscriptionProvider.java +++ b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/message/DeviceMessageSendSubscriptionProvider.java @@ -1,9 +1,10 @@ package org.jetlinks.community.device.message; import lombok.AllArgsConstructor; +import lombok.Generated; +import org.hswebframework.ezorm.rdb.mapping.ReactiveRepository; import org.hswebframework.web.id.IDGenerator; import org.jetlinks.community.device.entity.DeviceInstanceEntity; -import org.jetlinks.community.device.service.LocalDeviceInstanceService; import org.jetlinks.community.gateway.external.Message; import org.jetlinks.community.gateway.external.SubscribeRequest; import org.jetlinks.community.gateway.external.SubscriptionProvider; @@ -27,19 +28,22 @@ public class DeviceMessageSendSubscriptionProvider implements SubscriptionProvid private final DeviceRegistry registry; - private final LocalDeviceInstanceService instanceService; + private final ReactiveRepository deviceRepository; @Override + @Generated public String id() { return "device-message-sender"; } @Override + @Generated public String name() { return "设备消息发送"; } @Override + @Generated public String[] getTopicPattern() { return new String[]{ "/device-message-sender/*/*" @@ -47,6 +51,7 @@ public class DeviceMessageSendSubscriptionProvider implements SubscriptionProvid } @Override + @SuppressWarnings("all") public Flux subscribe(SubscribeRequest request) { String topic = request.getTopic(); @@ -57,7 +62,8 @@ public class DeviceMessageSendSubscriptionProvider implements SubscriptionProvid //发给所有设备 if ("*".equals(deviceId)) { - return instanceService.createQuery() + return deviceRepository + .createQuery() .select(DeviceInstanceEntity::getId) .where(DeviceInstanceEntity::getProductId, productId) //.and(DeviceInstanceEntity::getState, DeviceState.online) @@ -71,13 +77,14 @@ public class DeviceMessageSendSubscriptionProvider implements SubscriptionProvid } public Flux doSend(String requestId, String topic, String deviceId, Map message) { - message.put("messageId", IDGenerator.SNOW_FLAKE_STRING.generate()); + message.putIfAbsent("messageId", IDGenerator.SNOW_FLAKE_STRING.generate()); message.put("deviceId", deviceId); - RepayableDeviceMessage msg = MessageType.convertMessage(message) + RepayableDeviceMessage msg = MessageType + .convertMessage(message) .filter(RepayableDeviceMessage.class::isInstance) .map(RepayableDeviceMessage.class::cast) - .orElseThrow(() -> new UnsupportedOperationException("不支持的消息格式")); + .orElseThrow(() -> new UnsupportedOperationException("error.unsupported_message_format")); return registry .getDevice(deviceId) .switchIfEmpty(Mono.error(() -> new DeviceOperationException(ErrorCode.CLIENT_OFFLINE))) diff --git a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/message/DeviceMessageSubscriptionProvider.java b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/message/DeviceMessageSubscriptionProvider.java index 546f2c9a..a98b3ec2 100644 --- a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/message/DeviceMessageSubscriptionProvider.java +++ b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/message/DeviceMessageSubscriptionProvider.java @@ -1,34 +1,51 @@ package org.jetlinks.community.device.message; -import lombok.AllArgsConstructor; +import lombok.Generated; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.Setter; import org.jetlinks.community.gateway.external.Message; import org.jetlinks.community.gateway.external.SubscribeRequest; import org.jetlinks.community.gateway.external.SubscriptionProvider; import org.jetlinks.core.event.EventBus; import org.jetlinks.core.event.Subscription; +import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; import reactor.core.publisher.Flux; @Component -@AllArgsConstructor +@RequiredArgsConstructor +@ConfigurationProperties(prefix = "jetlinks.messaging.device-message-subscriber") public class DeviceMessageSubscriptionProvider implements SubscriptionProvider { private final EventBus eventBus; + @Getter + @Setter + //是否使用真实产生的topic作为响应 + //当订阅者进行了数据权限控制等场景时实际发生的数据topic可能与订阅的topic不一致 + private boolean responseActualTopic = true; + @Override + @Generated public String id() { return "device-message-subscriber"; } @Override + @Generated public String name() { return "订阅设备消息"; } @Override + @Generated public String[] getTopicPattern() { return new String[]{ - "/device/*/*/**" + //直接订阅设备 + "/device/*/*/**", + //按维度订阅 + "/*/*/device/*/*/**" }; } diff --git a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/message/TraceDeviceMessageSenderInterceptor.java b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/message/TraceDeviceMessageSenderInterceptor.java new file mode 100644 index 00000000..d1d428ee --- /dev/null +++ b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/message/TraceDeviceMessageSenderInterceptor.java @@ -0,0 +1,45 @@ +package org.jetlinks.community.device.message; + +import org.jetlinks.core.device.DeviceOperator; +import org.jetlinks.core.message.DeviceMessage; +import org.jetlinks.core.message.interceptor.DeviceMessageSenderInterceptor; +import org.jetlinks.core.trace.DeviceTracer; +import org.jetlinks.core.trace.FluxTracer; +import org.jetlinks.core.trace.TraceHolder; +import org.jetlinks.community.utils.ObjectMappers; +import org.springframework.core.Ordered; +import org.springframework.stereotype.Component; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +@Component +public class TraceDeviceMessageSenderInterceptor implements DeviceMessageSenderInterceptor, Ordered { + + @Override + @SuppressWarnings("all") + public Mono preSend(DeviceOperator device, DeviceMessage message) { + //跟踪信息放入header中 + return TraceHolder + .writeContextTo(message, DeviceMessage::addHeader); + } + + @Override + public Flux doSend(DeviceOperator device, DeviceMessage source, Flux sender) { + return sender + .as(FluxTracer + .create( + DeviceTracer.SpanName.request0(device.getDeviceId()), + (span, response) -> span + .setAttributeLazy(DeviceTracer.SpanKey.response, ()-> ObjectMappers.toJsonString(response.toJson())), + builder -> builder + .setAttribute(DeviceTracer.SpanKey.deviceId, device.getDeviceId()) + .setAttributeLazy(DeviceTracer.SpanKey.message, ()->ObjectMappers.toJsonString(source.toJson())) + ) + ); + } + + @Override + public int getOrder() { + return Ordered.LOWEST_PRECEDENCE; + } +} diff --git a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/message/transparent/TransparentDeviceMessageConnector.java b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/message/transparent/TransparentDeviceMessageConnector.java index c902a668..061037b9 100644 --- a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/message/transparent/TransparentDeviceMessageConnector.java +++ b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/message/transparent/TransparentDeviceMessageConnector.java @@ -12,7 +12,6 @@ import org.hswebframework.web.crud.events.EntityModifyEvent; import org.hswebframework.web.crud.events.EntitySavedEvent; import org.hswebframework.web.exception.ValidationException; import org.jctools.maps.NonBlockingHashMap; -import org.jetlinks.community.gateway.DeviceGatewayHelper; import org.jetlinks.core.device.DeviceConfigKey; import org.jetlinks.core.device.DeviceOperator; import org.jetlinks.core.device.DeviceRegistry; @@ -23,6 +22,7 @@ import org.jetlinks.core.message.*; import org.jetlinks.core.message.interceptor.DeviceMessageSenderInterceptor; import org.jetlinks.community.OperationSource; import org.jetlinks.community.device.entity.TransparentMessageCodecEntity; +import org.jetlinks.community.gateway.DeviceGatewayHelper; import org.jetlinks.community.gateway.annotation.Subscribe; import org.jetlinks.supports.server.DecodedClientMessageHandler; import org.springframework.beans.factory.ObjectProvider; diff --git a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/message/transparent/script/Jsr223TransparentMessageCodecProvider.java b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/message/transparent/script/Jsr223TransparentMessageCodecProvider.java index 4ca50e86..e253b10a 100644 --- a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/message/transparent/script/Jsr223TransparentMessageCodecProvider.java +++ b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/message/transparent/script/Jsr223TransparentMessageCodecProvider.java @@ -2,6 +2,7 @@ package org.jetlinks.community.device.message.transparent.script; import lombok.RequiredArgsConstructor; import org.hswebframework.web.exception.ValidationException; +import org.jetlinks.community.OperationSource; import org.jetlinks.community.device.message.transparent.SimpleTransparentMessageCodec; import org.jetlinks.community.device.message.transparent.TransparentMessageCodec; import org.jetlinks.community.device.message.transparent.TransparentMessageCodecProvider; @@ -31,16 +32,13 @@ public class Jsr223TransparentMessageCodecProvider implements TransparentMessage String script = (String) configuration.get("script"); Assert.hasText(lang, "lang can not be null"); Assert.hasText(script, "script can not be null"); - ScriptFactory factory = Scripts.getFactory(lang); CodecContext context = new CodecContext(factory); - SimpleTransparentMessageCodec.Codec codec = factory.bind( - Script.of("jsr223-transparent", script), - SimpleTransparentMessageCodec.Codec.class, - ExecutionContext.create(Collections.singletonMap("codec", context))); - + SimpleTransparentMessageCodec.Codec codec = factory.bind(Script.of("jsr223-transparent", script), + SimpleTransparentMessageCodec.Codec.class, + ExecutionContext.create(Collections.singletonMap("codec", context))); if (context.encoder == null && codec != null) { context.onDownstream(codec::encode); diff --git a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/message/writer/TimeSeriesMessageWriterConnector.java b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/message/writer/TimeSeriesMessageWriterConnector.java index 67eb1c59..0e2005b3 100644 --- a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/message/writer/TimeSeriesMessageWriterConnector.java +++ b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/message/writer/TimeSeriesMessageWriterConnector.java @@ -1,10 +1,11 @@ package org.jetlinks.community.device.message.writer; import lombok.AllArgsConstructor; +import lombok.Generated; import lombok.extern.slf4j.Slf4j; +import org.jetlinks.core.message.DeviceMessage; import org.jetlinks.community.device.service.data.DeviceDataService; import org.jetlinks.community.gateway.annotation.Subscribe; -import org.jetlinks.core.message.DeviceMessage; import reactor.core.publisher.Mono; /** @@ -16,18 +17,20 @@ import reactor.core.publisher.Mono; @Slf4j @AllArgsConstructor public class TimeSeriesMessageWriterConnector { + + private final DeviceDataService dataService; - /** - * 订阅设备消息 入库 - * - * @param message 设备消息 - * @return void - */ @Subscribe(topics = "/device/**", id = "device-message-ts-writer") + @Generated public Mono writeDeviceMessageToTs(DeviceMessage message) { - return dataService.saveDeviceMessage(message); + return dataService + .saveDeviceMessage(message) + .onErrorResume(err -> { + log.warn("write device message error {}", message, err); + return Mono.empty(); + }); } 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 index 36bc9bc7..15aa6d6f 100644 --- 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 @@ -3,17 +3,18 @@ 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.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.ObjectProperty; +import org.jetlinks.core.things.relation.ObjectType; +import org.jetlinks.core.things.relation.PropertyOperation; import org.springframework.stereotype.Component; import reactor.core.publisher.Mono; @@ -47,6 +48,7 @@ public class DeviceObjectProvider implements RelationObjectProvider { .simple(registry.getDevice(id), strategy -> strategy .addMapper("id", DeviceOperator::getDeviceId) + .addAsyncMapper(ObjectProperty.name, (opt,ignore)->opt.getSelfConfig(PropertyConstants.deviceName)) .addAsyncMapper(PropertyConstants.deviceName, DeviceOperator::getSelfConfig) .addAsyncMapper(PropertyConstants.productName, DeviceOperator::getSelfConfig) .addAsyncMapper(PropertyConstants.productId, DeviceOperator::getSelfConfig) diff --git a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/response/DeviceAllInfoResponse.java b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/response/DeviceAllInfoResponse.java deleted file mode 100644 index 86f5b6bb..00000000 --- a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/response/DeviceAllInfoResponse.java +++ /dev/null @@ -1,69 +0,0 @@ -package org.jetlinks.community.device.response; - -import lombok.Getter; -import lombok.Setter; -import org.jetlinks.community.device.entity.DevicePropertiesEntity; -import org.jetlinks.community.device.enums.DeviceState; - -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; - -/** - * @author bsetfeng - * @since 1.0 - **/ -@Getter -@Setter -public class DeviceAllInfoResponse { - - /** - * 设备基本信息 - */ - private DeviceInfo deviceInfo; - - /** - * 设备真实状态 - */ - private DeviceState realState; - - /** - * 设备上线时间 - */ - private long onlineTime; - - /** - * 设备离线时间 - */ - private long offlineTime; - - /** - * 元数据 属性id:属性值 映射 - */ - private Map properties; - - /** - * 元数据 事件id:事件数量 映射 - */ - private Map eventCounts; - - public static DeviceAllInfoResponse of(DeviceInfo deviceInfo, DeviceRunInfo deviceRunInfo) { - DeviceAllInfoResponse info = new DeviceAllInfoResponse(); - info.setDeviceInfo(deviceInfo); - info.setOfflineTime(deviceRunInfo.getOfflineTime()); - info.setOnlineTime(deviceRunInfo.getOnlineTime()); - info.setRealState(deviceRunInfo.getState()); - return info; - } - - - public DeviceAllInfoResponse ofProperties(List properties) { - this.setProperties(properties.stream().collect(Collectors.toMap(DevicePropertiesEntity::getProperty, DevicePropertiesEntity::getFormatValue))); - return this; - } - - public DeviceAllInfoResponse ofEventCounts(Map eventCounts) { - this.setEventCounts(eventCounts); - return this; - } -} diff --git a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/response/DeviceInfo.java b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/response/DeviceInfo.java deleted file mode 100644 index d2e708d8..00000000 --- a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/response/DeviceInfo.java +++ /dev/null @@ -1,89 +0,0 @@ -package org.jetlinks.community.device.response; - -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; -import org.hswebframework.web.bean.FastBeanCopier; -import org.jetlinks.community.device.entity.DeviceInstanceEntity; -import org.jetlinks.community.device.entity.DeviceProductEntity; -import org.jetlinks.community.device.enums.DeviceState; -import org.springframework.util.CollectionUtils; -import org.springframework.util.StringUtils; - -import java.util.Map; - -/** - * @author bsetfeng - * @since 1.0 - **/ -@Data -@AllArgsConstructor -@NoArgsConstructor -@Builder -public class DeviceInfo { - - private String id; - - //设备实例名称 - private String name; - - private String projectId; - - private String projectName; - - private String classifiedId; - - private String networkWay; - - private String productId; - - private DeviceState state; - - private String creatorId; - - private String creatorName; - - private Long createTime; - - private Long registryTime; - - private String orgId; - //说明 - private String describe; - - //产品名称 - private String productName; - - private String deviceType; - - //传输协议 - private String transportProtocol; - - //消息协议 - private String messageProtocol; - - private String deriveMetadata; - - private Map configuration; - - public static DeviceInfo of(DeviceInstanceEntity instance, - DeviceProductEntity product) { - DeviceInfo deviceInfo = FastBeanCopier.copy(instance, new DeviceInfo()); - deviceInfo.setMessageProtocol(product.getMessageProtocol()); - deviceInfo.setTransportProtocol(product.getTransportProtocol()); - deviceInfo.setDeviceType(product.getDeviceType().getText()); - deviceInfo.setClassifiedId(product.getClassifiedId()); - deviceInfo.setProjectId(product.getProjectId()); - deviceInfo.setProjectName(product.getProjectName()); - deviceInfo.setProductName(product.getName()); - deviceInfo.setNetworkWay(product.getNetworkWay()); - if (CollectionUtils.isEmpty(instance.getConfiguration())) { - deviceInfo.setConfiguration(product.getConfiguration()); - } - if (StringUtils.isEmpty(instance.getDeriveMetadata())) { - deviceInfo.setDeriveMetadata(product.getMetadata()); - } - return deviceInfo; - } -} diff --git a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/response/DeviceRunInfo.java b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/response/DeviceRunInfo.java deleted file mode 100644 index 60a97fdd..00000000 --- a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/response/DeviceRunInfo.java +++ /dev/null @@ -1,30 +0,0 @@ -package org.jetlinks.community.device.response; - -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; -import org.jetlinks.community.device.enums.DeviceState; - -/** - * @author bsetfeng - * @since 1.0 - **/ -@Data -@AllArgsConstructor(staticName = "of") -@NoArgsConstructor -@Builder -public class DeviceRunInfo { - - private long onlineTime; - - private long offlineTime; - - //设备状态 - private DeviceState state; - - private String metadata; - - //设备型号ID - private String productId; -} diff --git a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/response/ResetDeviceConfigurationResult.java b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/response/ResetDeviceConfigurationResult.java deleted file mode 100644 index e5dad757..00000000 --- a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/response/ResetDeviceConfigurationResult.java +++ /dev/null @@ -1,35 +0,0 @@ -package org.jetlinks.community.device.response; - -import lombok.*; -import org.hswebframework.ezorm.rdb.mapping.defaults.SaveResult; - -/** - * @see ImportDeviceInstanceResult 结构一致方便前端处理 - */ -@Getter -@Setter -@AllArgsConstructor -@NoArgsConstructor -public class ResetDeviceConfigurationResult { - - private SaveResult result; - - private boolean success; - - private String message; - - @Generated - public static ResetDeviceConfigurationResult success(SaveResult result) { - return new ResetDeviceConfigurationResult(result, true, null); - } - - @Generated - public static ResetDeviceConfigurationResult error(String message) { - return new ResetDeviceConfigurationResult(null, false, message); - } - - @Generated - public static ResetDeviceConfigurationResult error(Throwable message) { - return error(message.getMessage()); - } -} diff --git a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/service/AutoDiscoverDeviceRegistry.java b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/service/AutoDiscoverDeviceRegistry.java index b4bd0570..de551441 100644 --- a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/service/AutoDiscoverDeviceRegistry.java +++ b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/service/AutoDiscoverDeviceRegistry.java @@ -1,13 +1,18 @@ package org.jetlinks.community.device.service; import org.hswebframework.ezorm.rdb.mapping.ReactiveRepository; +import org.jetlinks.core.device.*; import org.jetlinks.community.device.entity.DeviceInstanceEntity; import org.jetlinks.community.device.entity.DeviceProductEntity; import org.jetlinks.community.device.enums.DeviceState; -import org.jetlinks.core.device.*; import org.springframework.util.StringUtils; import reactor.core.publisher.Mono; +/** + * 自动发现设备的注册中心,当默认注册中心没有获取到设备信息时,尝试查询数据库来获取设备信息。 + * + * @author zhouhao + */ public class AutoDiscoverDeviceRegistry implements DeviceRegistry { private final DeviceRegistry parent; @@ -71,4 +76,14 @@ public class AutoDiscoverDeviceRegistry implements DeviceRegistry { public Mono unregisterProduct(String productId) { return parent.unregisterProduct(productId); } + + @Override + public Mono unregisterProduct(String productId, String version) { + return parent.unregisterProduct(productId, version); + } + + @Override + public Mono getProduct(String productId, String version) { + return parent.getProduct(productId, version); + } } 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 55476e80..d2b54255 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,24 +1,21 @@ package org.jetlinks.community.device.service; import lombok.extern.slf4j.Slf4j; -import org.jetlinks.community.device.spi.DeviceConfigMetadataSupplier; import org.jetlinks.core.metadata.*; +import org.jetlinks.core.trace.FluxTracer; +import org.jetlinks.community.device.spi.DeviceConfigMetadataSupplier; import org.springframework.beans.factory.config.BeanPostProcessor; import org.springframework.stereotype.Component; import org.springframework.util.CollectionUtils; import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; import javax.annotation.Nonnull; import java.util.Comparator; -import java.util.HashSet; import java.util.List; -import java.util.Set; import java.util.concurrent.CopyOnWriteArrayList; -import java.util.stream.Collectors; -@Component @Slf4j +@Component public class DefaultDeviceConfigMetadataManager implements DeviceConfigMetadataManager, BeanPostProcessor { private final List suppliers = new CopyOnWriteArrayList<>(); @@ -29,29 +26,47 @@ public class DefaultDeviceConfigMetadataManager implements DeviceConfigMetadataM @Override public Flux getDeviceConfigMetadataByProductId(String productId) { - return Flux.fromIterable(suppliers) - .flatMap(supplier -> supplier.getDeviceConfigMetadataByProductId(productId)) - .map(config -> config.copy(DeviceConfigScope.device)) - .filter(config-> !CollectionUtils.isEmpty(config.getProperties())) - .sort(Comparator.comparing(ConfigMetadata::getName)); + return Flux + .fromIterable(suppliers) + .flatMap(supplier -> supplier.getDeviceConfigMetadataByProductId(productId)) + .map(config -> config.copy(DeviceConfigScope.device)) + .filter(config -> !CollectionUtils.isEmpty(config.getProperties())) + .sort(Comparator.comparing(ConfigMetadata::getName)) + .as(FluxTracer.create("/DefaultDeviceConfigMetadataManager/getDeviceConfigMetadataByProductId/" + productId)); } @Override public Flux getDeviceConfigMetadata(String deviceId) { - return Flux.fromIterable(suppliers) - .flatMap(supplier -> supplier.getDeviceConfigMetadata(deviceId)) - .map(config -> config.copy(DeviceConfigScope.device)) - .filter(config-> !CollectionUtils.isEmpty(config.getProperties())) - .sort(Comparator.comparing(ConfigMetadata::getName)); + return Flux + .fromIterable(suppliers) + .flatMap(supplier -> supplier.getDeviceConfigMetadata(deviceId)) + .map(config -> config.copy(DeviceConfigScope.device)) + .filter(config -> !CollectionUtils.isEmpty(config.getProperties())) + .sort(Comparator.comparing(ConfigMetadata::getName)) + .as(FluxTracer.create("/DefaultDeviceConfigMetadataManager/getDeviceConfigMetadata/" + deviceId)); } @Override public Flux getProductConfigMetadata(String productId) { - return Flux.fromIterable(suppliers) - .flatMap(supplier -> supplier.getProductConfigMetadata(productId)) - .map(config -> config.copy(DeviceConfigScope.product)) - .filter(config-> !CollectionUtils.isEmpty(config.getProperties())) - .sort(Comparator.comparing(ConfigMetadata::getName)); + return Flux + .fromIterable(suppliers) + .flatMap(supplier -> supplier.getProductConfigMetadata(productId)) + .map(config -> config.copy(DeviceConfigScope.product)) + .filter(config -> !CollectionUtils.isEmpty(config.getProperties())) + .sort(Comparator.comparing(ConfigMetadata::getName)) + .as(FluxTracer.create("/DefaultDeviceConfigMetadataManager/getProductConfigMetadata/" + productId)); + } + + @Override + public Flux getProductConfigMetadataByAccessId(String productId, + String accessId) { + return Flux + .fromIterable(suppliers) + .flatMap(supplier -> supplier.getProductConfigMetadataByAccessId(productId, accessId)) + .map(config -> config.copy(DeviceConfigScope.product)) + .filter(config -> !CollectionUtils.isEmpty(config.getProperties())) + .sort(Comparator.comparing(ConfigMetadata::getName)) + .as(FluxTracer.create("/DefaultDeviceConfigMetadataManager/getProductConfigMetadataByAccessId/" + productId)); } @Override @@ -60,37 +75,23 @@ public class DefaultDeviceConfigMetadataManager implements DeviceConfigMetadataM String metadataId, String typeId, ConfigScope... scopes) { - return Flux.fromIterable(suppliers) - .flatMap(supplier -> supplier.getMetadataExpandsConfig(productId, metadataType, metadataId, typeId)) - .sort(Comparator.comparing(ConfigMetadata::getName)) - .filter(metadata -> metadata.hasAnyScope(scopes)) - .map(metadata -> metadata.copy(scopes)) - .filter(meta -> org.apache.commons.collections4.CollectionUtils.isNotEmpty(meta.getProperties())); - } - - - @Override - public Flux getProductConfigMetadataByAccessId(String productId, - String accessId) { - return Flux.fromIterable(suppliers) - .flatMap(supplier -> supplier - .getProductConfigMetadataByAccessId(productId, accessId) - .onErrorResume(e -> { - log.error("get product config metatada by gateway error", e); - return Flux.empty(); - })) - .map(config -> config.copy(DeviceConfigScope.product)) - .filter(config -> !CollectionUtils.isEmpty(config.getProperties())) - .sort(Comparator.comparing(ConfigMetadata::getName)); + return Flux + .fromIterable(suppliers) + .flatMap(supplier -> supplier.getMetadataExpandsConfig(productId, metadataType, metadataId, typeId)) + .sort(Comparator.comparing(ConfigMetadata::getName)) + .filter(metadata -> metadata.hasAnyScope(scopes)) + .map(metadata -> metadata.copy(scopes)) + .filter(meta -> org.apache.commons.collections4.CollectionUtils.isNotEmpty(meta.getProperties())) + .as(FluxTracer.create("/DefaultDeviceConfigMetadataManager/getMetadataExpandsConfig/" + productId + "/" + metadataType + ":" + metadataId)); } @Override - public Mono> getProductConfigMetadataProperties(String productId) { - return this - .getProductConfigMetadata(productId) - .flatMapIterable(ConfigMetadata::getProperties) - .map(ConfigPropertyMetadata::getProperty) - .collect(Collectors.toSet()); + public Flux getProductFeatures(String productId) { + return Flux + .fromIterable(suppliers) + .flatMap(supplier -> supplier.getProductFeatures(productId)) + .distinct(Feature::getId) + .as(FluxTracer.create("/DefaultDeviceConfigMetadataManager/getProductFeatures/" + productId)); } @Override @@ -100,12 +101,4 @@ 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 d74a0d33..3f63b522 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 @@ -2,20 +2,21 @@ package org.jetlinks.community.device.service; import lombok.AllArgsConstructor; 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.community.gateway.supports.DeviceGatewayPropertiesManager; 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.jetlinks.community.device.entity.DeviceInstanceEntity; +import org.jetlinks.community.device.entity.DeviceProductEntity; +import org.jetlinks.community.device.spi.DeviceConfigMetadataSupplier; +import org.jetlinks.community.device.utils.DeviceCacheUtils; +import org.jetlinks.community.gateway.supports.DeviceGatewayPropertiesManager; import org.springframework.stereotype.Component; import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -38,14 +39,11 @@ public class DefaultDeviceConfigMetadataSupplier implements DeviceConfigMetadata @Override @SuppressWarnings("all") public Flux getDeviceConfigMetadata(String deviceId) { - if(StringUtils.isEmpty(deviceId)){ + if (StringUtils.isEmpty(deviceId)) { return Flux.empty(); } - return instanceService - .createQuery() - .select(DeviceInstanceEntity::getProductId) - .where(DeviceInstanceEntity::getId,deviceId) - .fetchOne() + return DeviceCacheUtils + .getDeviceOrLoad(deviceId, instanceService::findById) .map(DeviceInstanceEntity::getProductId) .flatMapMany(this::getProductConfigMetadata0) .filter(metadata -> metadata.hasScope(DeviceConfigScope.device)); @@ -53,7 +51,7 @@ public class DefaultDeviceConfigMetadataSupplier implements DeviceConfigMetadata @Override public Flux getDeviceConfigMetadataByProductId(String productId) { - if(StringUtils.isEmpty(productId)){ + if (ObjectUtils.isEmpty(productId)) { return Flux.empty(); } return getProductConfigMetadata0(productId) @@ -62,13 +60,27 @@ public class DefaultDeviceConfigMetadataSupplier implements DeviceConfigMetadata @Override public Flux getProductConfigMetadata(String productId) { - if(StringUtils.isEmpty(productId)){ + if (ObjectUtils.isEmpty(productId)) { return Flux.empty(); } return getProductConfigMetadata0(productId) .filter(metadata -> metadata.hasScope(DeviceConfigScope.product)); } + @Override + public Flux getProductConfigMetadataByAccessId(String productId, + String accessId) { + return gatewayPropertiesManager + .getProperties(accessId) + .flatMapMany(properties -> protocolSupports + .getProtocol(properties.getProtocol()) + .onErrorMap(e -> new BusinessException("error.unable_to_load_protocol_by_access_id", 404, properties.getProtocol())) + .flatMap(support -> support.getConfigMetadata(Transport.of(properties.getTransport()))) + .filter(metadata -> metadata.hasScope(DeviceConfigScope.product)) + ); + } + + @Override public Flux getMetadataExpandsConfig(String productId, DeviceMetadataType metadataType, String metadataId, @@ -82,27 +94,6 @@ public class DefaultDeviceConfigMetadataSupplier implements DeviceConfigMetadata .flatMapMany(Function.identity()); } - @Override - public Flux getProductConfigMetadataByAccessId(String productId, - String accessId) { - return gatewayPropertiesManager - .getProperties(accessId) - .flatMapMany(properties -> protocolSupports - .getProtocol(properties.getProtocol()) - .onErrorMap(e -> new BusinessException("error.unable_to_load_protocol_by_access_id", 404, properties.getProtocol())) - .flatMap(support -> support.getConfigMetadata(Transport.of(properties.getTransport())))); - } - - private Flux getProductConfigMetadata0(String productId) { - return productService - .findById(productId) - .filter(product -> StringUtils.hasText(product.getMessageProtocol())) - .flatMapMany(product -> protocolSupports - .getProtocol(product.getMessageProtocol()) - .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"); @@ -111,13 +102,12 @@ public class DefaultDeviceConfigMetadataSupplier implements DeviceConfigMetadata .flatMapMany(Function.identity()); } - @SuppressWarnings("all") protected Mono computeDeviceProtocol(String productId, BiFunction computer) { return productService .createQuery() .select(DeviceProductEntity::getMessageProtocol, DeviceProductEntity::getTransportProtocol) - .where(DeviceInstanceEntity::getId, productId) + .where(DeviceProductEntity::getId, productId) .fetchOne() .flatMap(product -> { return Mono @@ -133,4 +123,15 @@ public class DefaultDeviceConfigMetadataSupplier implements DeviceConfigMetadata ); }); } + + + private Flux getProductConfigMetadata0(String productId) { + return DeviceCacheUtils + .getProductOrLoad(productId, productService::findById) + .filter(product -> StringUtils.hasText(product.getMessageProtocol())) + .flatMapMany(product -> protocolSupports + .getProtocol(product.getMessageProtocol()) + .onErrorMap(e -> new BusinessException("error.unable_to_load_protocol_by_access_id", 404, product.getMessageProtocol())) + .flatMap(support -> support.getConfigMetadata(Transport.of((product.getTransportProtocol()))))); + } } diff --git a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/service/DeviceCategoryService.java b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/service/DeviceCategoryService.java index c63066e4..adef958c 100755 --- a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/service/DeviceCategoryService.java +++ b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/service/DeviceCategoryService.java @@ -29,6 +29,7 @@ public class DeviceCategoryService extends GenericReactiveTreeSupportCrudService } private static final String category_splitter = "-"; + @Override public void setChildren(DeviceCategoryEntity entity, List children) { entity.setChildren(children); @@ -50,10 +51,12 @@ public class DeviceCategoryService extends GenericReactiveTreeSupportCrudService if (children == null) { return; } + long sortIndex = 1; for (DeviceCategoryEntity child : children) { String id = child.getId(); - child.setId(parentId + category_splitter + id +category_splitter); - child.setParentId(parentId +category_splitter); + child.setId(parentId + category_splitter + id + category_splitter); + child.setParentId(parentId + category_splitter); + child.setSortIndex(sortIndex++); rebuild(parentId + category_splitter + id, child.getChildren()); } } @@ -69,7 +72,7 @@ public class DeviceCategoryService extends GenericReactiveTreeSupportCrudService List all = JSON.parseArray(json, DeviceCategoryEntity.class); List root = TreeSupportEntity.list2tree(all, DeviceCategoryEntity::setChildren); - + long i = 1; for (DeviceCategoryEntity category : root) { String id = category.getId(); category.setId(category_splitter + id + category_splitter); @@ -78,6 +81,7 @@ public class DeviceCategoryService extends GenericReactiveTreeSupportCrudService .ifPresent(parentId -> { category.setParentId(category_splitter + parentId + category_splitter); }); + category.setSortIndex(i++); rebuild(category_splitter + id, category.getChildren()); } return root; 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 264f85cf..ea715e34 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 @@ -1,15 +1,19 @@ package org.jetlinks.community.device.service; +import org.hswebframework.utils.MapUtils; +import org.hswebframework.web.i18n.LocaleUtils; +import org.jetlinks.core.message.codec.Transport; +import org.jetlinks.core.metadata.*; +import org.jetlinks.community.ConfigMetadataConstants; 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.*; +import org.jetlinks.reactor.ql.utils.CastUtils; +import org.springframework.util.CollectionUtils; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; -import java.util.Set; - +import java.util.Map; /** * 设备配置信息管理器,用于获取产品或者设备在运行过程中所需要的配置信息。 @@ -44,7 +48,7 @@ public interface DeviceConfigMetadataManager { * * @param deviceId 产品ID * @return 配置信息 - * @see org.jetlinks.core.metadata.DeviceConfigScope#device + * @see DeviceConfigScope#device */ Flux getDeviceConfigMetadata(String deviceId); @@ -64,28 +68,6 @@ public interface DeviceConfigMetadataManager { */ Flux getProductConfigMetadata(String productId); - /** - * 获取物模型拓展配置定义 - * @param productId 产品ID - * @param metadataType 物模型类型 - * @param metadataId 物模型ID - * @param typeId 类型 - * @return 配置定义信息 - */ - Flux getMetadataExpandsConfig(String productId, - DeviceMetadataType metadataType, - String metadataId, - String typeId, - ConfigScope... scopes); - - /** - * 根据产品ID获取产品所需配置信息 - * - * @param productId 产品ID - * @return 配置property集合 - */ - Mono> getProductConfigMetadataProperties(String productId); - /** * 根据产品ID和网关ID获取配置信息 *

@@ -104,6 +86,56 @@ public interface DeviceConfigMetadataManager { Flux getProductConfigMetadataByAccessId(String productId, String accessId); + /** + * 获取物模型拓展配置定义 + * @param productId 产品ID + * @param metadataType 物模型类型 + * @param metadataId 物模型ID + * @param typeId 类型 + * @return 配置定义信息 + */ + Flux getMetadataExpandsConfig(String productId, + DeviceMetadataType metadataType, + String metadataId, + String typeId, + ConfigScope... scopes); + Flux getProductFeatures(String productId); + /** + * 检验配置中的属性必填项 + * @param configMetadata 产品/设备所需配置信息 + * @param configuration 产品/设备配置信息 + * @return 验证结果 + */ + static Mono validate(ConfigMetadata configMetadata, + Map configuration) { + // 必填项配置为空,不进行校验 + if (configMetadata == null || configMetadata.getProperties() == null) { + return Mono.just(ValidateResult.success()); + } + return Flux + .fromIterable(configMetadata.getProperties()) + .filter(propertyMetadata -> { + // 过滤出必填项 + DataType dataType = propertyMetadata.getType(); + if (dataType == null) { + return false; + } + return dataType + .getExpand(ConfigMetadataConstants.required.getKey()) + .map(CastUtils::castBoolean) + .orElse(false); + }) + .map(ConfigPropertyMetadata::getProperty) + // 配置为空或不包含必填项,都不通过校验 + .filter(property -> MapUtils.isNullOrEmpty(configuration) || configuration.get(property) == null) + .collectList() + .filter(property -> !CollectionUtils.isEmpty(property)) + .map(property -> ValidateResult + .builder() + .errorMsg(LocaleUtils.resolveMessage("error.config_metadata_required_must_not_be_null", property)) + .value(property) + .build()); + } } 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 index 2efdb77a..e9039929 100644 --- 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 @@ -3,21 +3,26 @@ package org.jetlinks.community.device.service; import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.commons.collections4.CollectionUtils; -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.crud.events.*; import org.hswebframework.web.exception.BusinessException; -import org.jetlinks.community.device.enums.DeviceType; -import org.jetlinks.core.ProtocolSupports; -import org.jetlinks.core.device.DeviceConfigKey; -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.community.device.enums.DeviceFeature; +import org.jetlinks.community.device.enums.DeviceType; +import org.jetlinks.community.device.enums.ValidateDataType; +import org.jetlinks.community.gateway.supports.DeviceGatewayProviders; +import org.jetlinks.community.things.ThingsDataRepository; +import org.jetlinks.core.ProtocolSupports; +import org.jetlinks.core.device.DeviceConfigKey; +import org.jetlinks.core.device.DeviceProductOperator; +import org.jetlinks.core.device.DeviceRegistry; +import org.jetlinks.core.device.DeviceThingType; +import org.jetlinks.core.message.codec.Transport; +import org.jetlinks.core.metadata.DeviceMetadata; import org.jetlinks.supports.official.JetLinksDeviceMetadataCodec; +import org.jetlinks.supports.utils.DeviceMetadataUtils; import org.springframework.context.event.EventListener; import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; @@ -44,12 +49,44 @@ public class DeviceEntityEventHandler { private final LocalDeviceInstanceService deviceService; + private final ThingsDataRepository repository; + + static final String thingType = DeviceThingType.device.getId(); + + @EventListener + public void handleDeviceEvent(EntityPrepareCreateEvent event) { + event.async( + Flux.fromIterable(event.getEntity()) + .flatMap(this::handleMetadata) + .flatMap(this::checkParentId) + //新建设备时,若设备没有配置特性,则自动添加协议包中的配置 + .flatMap(this::addDeviceFeature) + ); + } + + @EventListener + public void handleDeviceEvent(EntityPrepareSaveEvent event) { + event.async( + Flux.fromIterable(event.getEntity()) + .flatMap(this::handleMetadata) + ); + } + + + @EventListener + public void handleDeviceEvent(EntityPrepareModifyEvent event) { + event.async( + Flux.fromIterable(event.getAfter()) + .flatMap(this::handleMetadata) + ); + } + @EventListener public void handleDeviceEvent(EntitySavedEvent event) { - //保存设备时,自动更新注册中心里的名称 + //保存设备时,自动更新注册中心里的名称和配置信息 event.first( Flux.fromIterable(event.getEntity()) - .filter(device -> StringUtils.hasText(device.getName())) + .flatMap(this::checkParentId, 16) .flatMap(device -> registry .getDevice(device.getId()) .flatMap(deviceOperator -> { @@ -67,6 +104,7 @@ public class DeviceEntityEventHandler { ); } + @EventListener public void handleDeviceEvent(EntityModifyEvent event) { Map olds = event @@ -75,9 +113,10 @@ public class DeviceEntityEventHandler { .filter(device -> StringUtils.hasText(device.getId())) .collect(Collectors.toMap(DeviceInstanceEntity::getId, Function.identity())); - //更新设备时,自动更新注册中心里的名称 + //更新设备时,自动更新注册中心里的名称和配置信息 event.first( Flux.fromIterable(event.getAfter()) + .flatMap(this::checkParentId, 16) .flatMap(device -> registry .getDevice(device.getId()) .flatMap(deviceOperator -> { @@ -98,6 +137,80 @@ public class DeviceEntityEventHandler { } + + //处理派生物模型,移除掉和产品重复的物模型信息. + private Mono handleMetadata(DeviceInstanceEntity device) { + if (StringUtils.hasText(device.getDeriveMetadata())) { + return validateDeriveMetadata(JetLinksDeviceMetadataCodec + .getInstance() + .doDecode(device.getDeriveMetadata())) + .flatMap(deriveMetadata -> registry + .getProduct(device.getProductId()) + .flatMap(DeviceProductOperator::getMetadata) + .doOnNext(productMetadata -> device + .setDeriveMetadata( + JetLinksDeviceMetadataCodec + .getInstance() + .doEncode( + DeviceMetadataUtils.difference( + productMetadata, + deriveMetadata + ) + ) + )) + .flatMap(productMetadata -> { + DeviceMetadata metadata = productMetadata.merge(deriveMetadata); + return repository + .opsForThing(thingType, device.getId()) + .flatMap(opt -> opt.forDDL().validateMetadata(metadata)); + }) + ) + .thenReturn(device); + } + return Mono.just(device); + } + + + public Mono validateDeriveMetadata(DeviceMetadata deriveMetadata) { + return this + .validateEvents(deriveMetadata) + .then(validateFunction(deriveMetadata)) + .then(validateProperties(deriveMetadata)) + .then(validateTags(deriveMetadata)) + .thenReturn(deriveMetadata); + } + + private Mono validateEvents(DeviceMetadata deviceMetadata) { + return Flux + .fromIterable(deviceMetadata.getEvents()) + .flatMap(ValidateDataType::validateIdAndName) + .then(); + } + + private Mono validateFunction(DeviceMetadata deviceMetadata) { + return Flux + .fromIterable(deviceMetadata.getFunctions()) + .flatMap(ValidateDataType::validateIdAndName) + .then(); + } + + private Mono validateProperties(DeviceMetadata deviceMetadata) { + return Flux + .fromIterable(deviceMetadata.getProperties()) + .flatMap(property -> ValidateDataType.validateIdAndName(property) + .then(ValidateDataType.handleValidateDataType(property.getValueType(), property.getId())) + ) + .then(); + } + + private Mono validateTags(DeviceMetadata deviceMetadata) { + return Flux + .fromIterable(deviceMetadata.getTags()) + .flatMap(ValidateDataType::validateIdAndName) + .then(); + } + + @EventListener public void handleDeviceDeleteEvent(EntityDeletedEvent event) { event.async( @@ -118,6 +231,51 @@ public class DeviceEntityEventHandler { ); } + + private Mono checkParentId(DeviceInstanceEntity device) { + if (StringUtils.hasText(device.getId()) && Objects.equals(device.getId(), device.getParentId())) { + return Mono.error(new BusinessException("error.device_id_and_parent_id_can_not_be_the_same", 500, device.getId())); + } + return deviceService + .checkCyclicDependency(device) + .thenReturn(device); + } + + private Mono addDeviceFeature(DeviceInstanceEntity device) { + // 已配置feature,则不覆盖 + if (StringUtils.hasText(device.getProductId()) && + (device.getFeatures() == null || device.getFeatures().length == 0)) { + return registry + .getProduct(device.getProductId()) + .flatMap(product -> Mono + .zip( + // 协议 + product.getProtocol(), + // 设备接入方式 + product + .getConfig(PropertyConstants.accessProvider) + .flatMap(provider -> Mono + .justOrEmpty(DeviceGatewayProviders.getProvider(provider))) + ) + .flatMapMany(tp2 -> tp2 + .getT1() + .getFeatures(tp2.getT2().getTransport()) + .doOnNext(feature -> { + DeviceFeature deviceFeature = DeviceFeature.get(feature.getId()); + if (deviceFeature != null) { + device.addFeature(deviceFeature); + } + })) + .then() + .onErrorResume(err -> { + log.warn("auto set device[{}] default feature error", device.getName(), err); + return Mono.empty(); + })); + } + + return Mono.empty(); + } + @EventListener public void handleProductDefaultMetadata(EntityPrepareCreateEvent event) { event.async( @@ -145,21 +303,18 @@ public class DeviceEntityEventHandler { @EventListener public void handleCategoryDelete(EntityDeletedEvent event) { - //禁止删除有产品使用的分类 + //修改分类关联的产品 event.async( productService - .createQuery() + .createUpdate() + .setNull(DeviceProductEntity::getClassifiedId) .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"); - } - }) + .execute() + .as(EntityEventHelper::setDoNotFireEvent) ); } @@ -169,12 +324,13 @@ public class DeviceEntityEventHandler { public void handleCategorySave(EntitySavedEvent event) { event.async( Flux.fromIterable(event.getEntity()) - .flatMap(category -> productService + .concatMap(category -> productService .createUpdate() .set(DeviceProductEntity::getClassifiedName, category.getName()) .where(DeviceProductEntity::getClassifiedId, category.getId()) .execute() - .then()) + .then() + .as(EntityEventHelper::setDoNotFireEvent)) ); } } diff --git a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/service/DeviceMessageBusinessHandler.java b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/service/DeviceMessageBusinessHandler.java index 0b19a0ad..44731cae 100644 --- a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/service/DeviceMessageBusinessHandler.java +++ b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/service/DeviceMessageBusinessHandler.java @@ -1,19 +1,15 @@ package org.jetlinks.community.device.service; -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; +import lombok.*; import lombok.extern.slf4j.Slf4j; import org.apache.commons.collections4.MapUtils; import org.hswebframework.ezorm.rdb.mapping.ReactiveRepository; import org.jetlinks.community.PropertyConstants; import org.jetlinks.community.buffer.PersistenceBuffer; -import org.jetlinks.community.configure.cluster.Cluster; import org.jetlinks.community.device.entity.DeviceInstanceEntity; 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.events.DeviceAutoRegisterEvent; import org.jetlinks.community.gateway.annotation.Subscribe; import org.jetlinks.community.utils.ErrorUtils; import org.jetlinks.core.device.DeviceConfigKey; @@ -23,21 +19,19 @@ import org.jetlinks.core.event.EventBus; import org.jetlinks.core.event.Subscription; import org.jetlinks.core.message.*; import org.jetlinks.core.metadata.DeviceMetadata; -import org.jetlinks.core.utils.FluxUtils; import org.jetlinks.core.utils.Reactors; import org.jetlinks.reactor.ql.utils.CastUtils; import org.jetlinks.supports.official.JetLinksDeviceMetadataCodec; +import org.springframework.boot.CommandLineRunner; +import org.springframework.boot.SpringApplication; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.dao.QueryTimeoutException; import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; -import org.springframework.util.StringUtils; import reactor.core.Disposable; import reactor.core.Disposables; -import reactor.core.publisher.BufferOverflowStrategy; -import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; -import reactor.core.scheduler.Schedulers; import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; @@ -46,16 +40,32 @@ import java.io.IOException; import java.io.ObjectInput; import java.io.ObjectOutput; import java.time.Duration; -import java.util.Date; import java.util.HashMap; -import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.function.Function; +/** + * 设备消息相关业务逻辑处理类.通过监听事件来进行处理: + *

+ *
+ * 同步设备的在线状态:{@link DeviceMessageBusinessHandler#init()}
+ * 处理自动注册:{@link DeviceMessageBusinessHandler#autoRegisterDevice(DeviceRegisterMessage)}
+ * 处理子设备注册:{@link DeviceMessageBusinessHandler#autoBindChildrenDevice(ChildDeviceMessage)}
+ * 处理子设备注销: {@link DeviceMessageBusinessHandler#autoUnbindChildrenDevice(ChildDeviceMessage)}
+ * 处理派生物模型:{@link DeviceMessageBusinessHandler#updateMetadata(DerivedMetadataMessage)}
+ *
+ * 
+ * + * @author zhouhao + * @since 1.5 + */ @Component -@AllArgsConstructor +@RequiredArgsConstructor @Slf4j -public class DeviceMessageBusinessHandler { +public class DeviceMessageBusinessHandler implements CommandLineRunner { + + private static final long[] metadataUpdateRetryDelay = new long[]{50, 100, 100, 250}; private final LocalDeviceInstanceService deviceService; @@ -67,12 +77,31 @@ public class DeviceMessageBusinessHandler { private final EventBus eventBus; + private final ApplicationEventPublisher eventPublisher; + private final Disposable.Composite disposable = Disposables.composite(); + /** * 自动注册设备信息 *

* 设备消息的header需要包含{@code deviceName},{@code productId}才会自动注册.、 + *

+ * 注册前会推送{@link DeviceAutoRegisterEvent}事件到spring,可以通过监听此事件来处理自定义逻辑,比如某些情况不允许自动注册. + *

{@code
+     *
+     * @EventListener
+     * public void handleAutoRegister(DeviceAutoRegisterEvent event){
+     *
+     *      event.async(
+     *          this
+     *          .checkAllowAutoRegister(event.getEntity())
+     *          .doOnNext(event::setAllowRegister)
+     *      )
+     *
+     * }
+     *
+     * }
* * @param message 注册消息 * @return 注册后的设备操作接口 @@ -93,7 +122,7 @@ public class DeviceMessageBusinessHandler { //T5. 配置信息 Mono.justOrEmpty(message.getHeader("configuration").map(Map.class::cast).orElse(new HashMap())) ).flatMap(tps -> { - DeviceInstanceEntity instance = new DeviceInstanceEntity(); + DeviceInstanceEntity instance = DeviceInstanceEntity.of(); instance.setId(tps.getT1()); instance.setName(tps.getT2()); instance.setProductId(tps.getT3()); @@ -102,11 +131,14 @@ public class DeviceMessageBusinessHandler { instance.setRegistryTime(message.getTimestamp()); instance.setCreateTimeNow(); instance.setCreatorId(tps.getT4().getCreatorId()); - instance.setOrgId(tps.getT4().getOrgId()); //网关ID message.getHeader(DeviceConfigKey.parentGatewayId.getKey()) .map(String::valueOf) .ifPresent(instance::setParentId); + if (Objects.equals(instance.getId(), instance.getParentId())) { + return Mono.error(new IllegalStateException("设备ID与网关ID不能相同:" + instance.getId())); + } + //设备自状态管理 //网关注册设备子设备时,设置自状态管理。 //在检查子设备状态时,将会发送ChildDeviceMessage到网关 @@ -114,25 +146,44 @@ public class DeviceMessageBusinessHandler { @SuppressWarnings("all") boolean selfManageState = CastUtils .castBoolean(tps.getT5().getOrDefault(DeviceConfigKey.selfManageState.getKey(), false)); - boolean offline = selfManageState || message.getHeaderOrDefault(Headers.ignoreSession); instance.setState(offline ? DeviceState.offline : DeviceState.online); //合并配置 instance.mergeConfiguration(tps.getT5()); - return deviceService - .save(instance) - .then(Mono.defer(() -> registry - .register(instance.toDeviceInfo() - .addConfig("state", offline - ? org.jetlinks.core.device.DeviceState.offline - : org.jetlinks.core.device.DeviceState.online)))); + DeviceAutoRegisterEvent event = new DeviceAutoRegisterEvent(instance); + //循环依赖检查 + event.async( + deviceService.checkCyclicDependency(instance) + ); + + return event + //先推送DeviceAutoRegisterEvent + .publish(eventPublisher) + .then(Mono.defer(() -> { + //允许注册 + if (event.isAllowRegister()) { + return deviceService + .save(instance) + .then(Mono.defer(() -> doRegister(instance))); + } else { + return Mono.empty(); + } + })); }); } + private Mono doRegister(DeviceInstanceEntity device) { + return registry + .register(device + .toDeviceInfo() + .addConfig("state", device.getState()==DeviceState.online + ? org.jetlinks.core.device.DeviceState.online + : org.jetlinks.core.device.DeviceState.offline)); + } - @Subscribe("/device/*/*/register") - @Transactional(propagation = Propagation.NEVER) + @Subscribe(value = "/device/*/*/register", priority = Integer.MIN_VALUE) + @Transactional(propagation = Propagation.REQUIRES_NEW) public Mono autoRegisterDevice(DeviceRegisterMessage message) { if (message.getHeader(Headers.force).orElse(false)) { return this @@ -161,13 +212,14 @@ public class DeviceMessageBusinessHandler { .then(); } + /** * 通过订阅子设备注册消息,自动绑定子设备到网关设备 * * @param message 子设备消息 * @return void */ - @Subscribe("/device/*/*/message/children/*/register") + @Subscribe(value = "/device/*/*/message/children/*/register", priority = Integer.MIN_VALUE) @Transactional(propagation = Propagation.REQUIRES_NEW) public Mono autoBindChildrenDevice(ChildDeviceMessage message) { @@ -175,14 +227,14 @@ public class DeviceMessageBusinessHandler { if (childMessage instanceof DeviceRegisterMessage) { String childId = ((DeviceRegisterMessage) childMessage).getDeviceId(); if (message.getDeviceId().equals(childId)) { - log.warn("子设备注册消息循环依赖:{}", message); - return Mono.empty(); + return Mono.error(new IllegalStateException("设备ID与网关ID不能相同:" + childId)); } //网关设备添加到header中 childMessage.addHeaderIfAbsent(DeviceConfigKey.parentGatewayId.getKey(), message.getDeviceId()); - return registry - .getDevice(childId) + return deviceService + .checkCyclicDependency(childId, message.getDeviceId()) + .then(registry.getDevice(childId)) .map(device -> device .getState() //更新数据库 @@ -214,9 +266,7 @@ public class DeviceMessageBusinessHandler { Message childMessage = message.getChildDeviceMessage(); if (childMessage instanceof DeviceUnRegisterMessage) { String childId = ((DeviceUnRegisterMessage) childMessage).getDeviceId(); - if(!StringUtils.hasText(childId)){ - return Mono.empty(); - } + //解除绑定关系 return deviceService .createUpdate() .setNull(DeviceInstanceEntity::getParentId) @@ -227,54 +277,10 @@ public class DeviceMessageBusinessHandler { return Mono.empty(); } - @Subscribe("/device/*/*/unregister") - @Transactional(propagation = Propagation.NEVER) - public Mono unRegisterDevice(DeviceUnRegisterMessage message) { - if(message.getHeader(Headers.ignore).orElse(false)){ - return Mono.empty(); - } - //注销设备 - return deviceService - .unregisterDevice(message.getDeviceId()) - .then(); - } - - @Subscribe("/device/*/*/message/tags/update") - public Mono updateDeviceTag(UpdateTagMessage message) { - Map tags = message.getTags(); - String deviceId = message.getDeviceId(); - - return registry - .getDevice(deviceId) - .flatMap(DeviceOperator::getMetadata) - .flatMapMany(metadata -> Flux - .fromIterable(tags.entrySet()) - .map(e -> { - DeviceTagEntity tagEntity = metadata - .getTag(e.getKey()) - .map(tagMeta -> DeviceTagEntity.of(tagMeta, e.getValue())) - .orElseGet(() -> { - DeviceTagEntity entity = new DeviceTagEntity(); - entity.setKey(e.getKey()); - entity.setType("string"); - entity.setName(e.getKey()); - entity.setCreateTime(new Date()); - entity.setDescription("设备上报"); - entity.setValue(String.valueOf(e.getValue())); - return entity; - }); - tagEntity.setDeviceId(deviceId); - tagEntity.setId(DeviceTagEntity.createTagId(deviceId, tagEntity.getKey())); - return tagEntity; - })) - .as(tagRepository::save) - .then(); - } - @Subscribe("/device/*/*/metadata/derived") public Mono updateMetadata(DerivedMetadataMessage message) { if (message.isAll()) { - return updateMedata(message.getDeviceId(), message.getMetadata()); + return updateMetadata(message.getDeviceId(), message.getMetadata()); } return Mono .zip( @@ -292,22 +298,69 @@ public class DeviceMessageBusinessHandler { //重新编码为字符串 .flatMap(JetLinksDeviceMetadataCodec.getInstance()::encode) //更新物模型 - .flatMap(metadata -> updateMedata(message.getDeviceId(), metadata)); + .flatMap(metadata -> updateMetadata(message.getDeviceId(), metadata)); } - private Mono updateMedata(String deviceId, String metadata) { - return deviceService - .createUpdate() - .set(DeviceInstanceEntity::getDeriveMetadata, metadata) - .where(DeviceInstanceEntity::getId, deviceId) - .execute() - .then(registry.getDevice(deviceId)) - .flatMap(device -> device.updateMetadata(metadata)) + /** + * 支持重试的物模型更新.通过重试来处理并行注册以及物模型更新时可能导致数据库的物模型与缓存中不一致的问题. + *

+ * 如果数据库中或者设备注册中心中没有设备则进行重试. + *

+ * 重试次数以及延迟使用{@link DeviceMessageBusinessHandler#metadataUpdateRetryDelay}定义. + *

+ * 如果返回结果为empty,说明整个重试都没有成功更新物模型. + * + * @param deviceId 设备ID + * @param metadata 物模型 + * @param currentRetries 当前重试次数 + * @return DeviceOperator + */ + private Mono retryableUpdateMetadata(String deviceId, String metadata, int currentRetries) { + if (currentRetries >= metadataUpdateRetryDelay.length) { + return registry.getDevice(deviceId); + } + + Mono delay; + if (currentRetries > 0) { + delay = Mono.delay(Duration.ofMillis(metadataUpdateRetryDelay[currentRetries])); + log.info("retry update device [{}] metadata : device not registered", deviceId); + } else { + delay = Mono.empty(); + } + + return delay + .then(this.doUpdateMetadata(deviceId, metadata)) + //设备不存在或者没注册,则重试 + .switchIfEmpty(Mono.defer(() -> retryableUpdateMetadata(deviceId, metadata, currentRetries + 1))); + } + + private Mono doUpdateMetadata(String deviceId, String metadata) { + return Mono + .zip( + //更新数据库 + deviceService + .createUpdate() + .set(DeviceInstanceEntity::getDeriveMetadata, metadata) + .where(DeviceInstanceEntity::getId, deviceId) + .execute() + .filter(i -> i >= 1), + //更新缓存 + registry + .getDevice(deviceId) + .filterWhen(device -> device.updateMetadata(metadata)), + (count, device) -> device + ); + } + + private Mono updateMetadata(String deviceId, String metadata) { + //更新数据库中以及注册中心中的物模型数据 + return this + .retryableUpdateMetadata(deviceId, metadata, 0) + .doOnNext(device -> log.info("update device [{}] metadata success", device.getDeviceId())) + .switchIfEmpty(Mono.fromRunnable(() -> log.warn("update device [{}] metadata failed: device not registered", deviceId))) .then(); } - - @AllArgsConstructor @NoArgsConstructor @Getter @@ -336,10 +389,7 @@ public class DeviceMessageBusinessHandler { } } - @PreDestroy - public void shutdown() { - disposable.dispose(); - } + private PersistenceBuffer buffer; @PostConstruct public void init() { @@ -352,7 +402,7 @@ public class DeviceMessageBusinessHandler { .build(); //缓冲同步设备上线信息,在突发大量上下线的情况,减少数据库的压力 - PersistenceBuffer buffer = + buffer = new PersistenceBuffer<>( "./data/device-state-buffer", "device-state.queue", @@ -374,7 +424,7 @@ public class DeviceMessageBusinessHandler { QueryTimeoutException.class)) .bufferSize(1000); - buffer.start(); + buffer.init(); disposable.add(eventBus .subscribe(subscription, DeviceMessage.class) @@ -384,4 +434,18 @@ public class DeviceMessageBusinessHandler { } + @PreDestroy + public void shutdown() { + buffer.stop(); + } + + @Override + public void run(String... args) throws Exception { + buffer.start(); + + //在所有bean之后dispose + SpringApplication + .getShutdownHandlers() + .add(disposable::dispose); + } } diff --git a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/service/DeviceMetadataMappingService.java b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/service/DeviceMetadataMappingService.java new file mode 100644 index 00000000..0c23425f --- /dev/null +++ b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/service/DeviceMetadataMappingService.java @@ -0,0 +1,154 @@ +package org.jetlinks.community.device.service; + +import lombok.AllArgsConstructor; +import org.hswebframework.web.crud.service.GenericReactiveCrudService; +import org.hswebframework.web.validator.CreateGroup; +import org.jetlinks.community.device.entity.DeviceInstanceEntity; +import org.jetlinks.community.device.entity.DeviceMetadataMappingDetail; +import org.jetlinks.community.device.entity.DeviceMetadataMappingEntity; +import org.jetlinks.core.device.DeviceRegistry; +import org.jetlinks.core.metadata.DeviceMetadata; +import org.jetlinks.core.things.ThingMetadata; +import org.jetlinks.supports.official.JetLinksDeviceMetadataCodec; +import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +@Service +@AllArgsConstructor +public class DeviceMetadataMappingService extends GenericReactiveCrudService { + + private final DeviceRegistry registry; + + private final LocalDeviceProductService productService; + + private final LocalDeviceInstanceService deviceService; + + public Flux getProductMappingDetail(String productId) { + return this + .getProductMetadata(productId) + .flatMapMany(metadata -> this + .convertDetail(metadata, + this + .createQuery() + .where(DeviceMetadataMappingEntity::getProductId, productId) + .isNull(DeviceMetadataMappingEntity::getDeviceId) + .fetch(), + () -> DeviceMetadataMappingDetail.ofProduct(productId) + )); + } + + public Flux getDeviceMappingDetail(String deviceId) { + return deviceService + .findById(deviceId) + .flatMapMany(device -> this + .getDeviceMetadata(device) + .flatMapMany(metadata -> this + .convertDetail( + metadata, + this + .createQuery() + //where product_id =? and (device_id is null or device_id = ?) + .where(DeviceMetadataMappingEntity::getProductId, device.getProductId()) + .nest() + .isNull(DeviceMetadataMappingEntity::getDeviceId) + .or() + .is(DeviceMetadataMappingEntity::getDeviceId, deviceId) + .end() + .fetch(), + () -> DeviceMetadataMappingDetail.ofDevice(device.getProductId(), deviceId)) + )); + } + + public Mono saveDeviceMapping(String deviceId, Flux mappings) { + + return mappings + .groupBy(e -> StringUtils.hasText(e.getOriginalId())) + .flatMap(group -> { + //bind + if (group.key()) { + return deviceService + .findById(deviceId) + .flatMap(device -> this.save( + group.doOnNext(e -> { + e.setDeviceId(deviceId); + e.setProductId(device.getProductId()); + e.generateId(); + e.tryValidate(CreateGroup.class); + }) + )); + } + //unbind + return group + .map(mapping -> DeviceMetadataMappingEntity + .generateIdByDevice(deviceId, mapping.getMetadataType(), mapping.getMetadataId())) + .as(this::deleteById) + .then(); + }) + .then(); + } + + public Mono saveProductMapping(String productId, Flux mappings) { + return mappings + .groupBy(e -> StringUtils.hasText(e.getOriginalId())) + .flatMap(group -> { + //bind + if (group.key()) { + return productService + .findById(productId) + .flatMap(device -> this.save( + group.doOnNext(e -> { + e.setDeviceId(null); + e.setProductId(productId); + e.generateId(); + e.tryValidate(CreateGroup.class); + }) + )); + } + //unbind + return group + .map(mapping -> DeviceMetadataMappingEntity + .generateIdByProduct(productId, mapping.getMetadataType(), mapping.getMetadataId())) + .as(this::deleteById) + .then(); + }) + .then(); + } + + private Mono getProductMetadata(String productId) { + //从数据库中获取物模型? + return productService + .findById(productId) + .flatMap(product -> JetLinksDeviceMetadataCodec.getInstance().decode(product.getMetadata())); + } + + private Mono getDeviceMetadata(DeviceInstanceEntity device) { + if (StringUtils.hasText(device.getDeriveMetadata())) { + return JetLinksDeviceMetadataCodec.getInstance().decode(device.getDeriveMetadata()); + } + return getProductMetadata(device.getProductId()); + } + + private Flux convertDetail(ThingMetadata metadata, + Flux mappings, + Supplier builder) { + + return mappings + .collect(Collectors.toMap(DeviceMetadataMappingEntity::getMetadataId, + Function.identity(), + //有设备ID则以设备配置的为准 + (left, right) -> StringUtils.hasText(left.getDeviceId()) ? left : right)) + .flatMapMany(mapping -> Flux + .fromIterable(metadata.getProperties()) + .map(property -> builder + .get() + .with(property) + .with(mapping.get(property.getId())))); + } + +} 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 index 70bf4341..6b7f01de 100644 --- 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 @@ -1,10 +1,16 @@ package org.jetlinks.community.device.service; import lombok.AllArgsConstructor; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.collections4.MapUtils; import org.hswebframework.web.bean.FastBeanCopier; import org.hswebframework.web.crud.events.EntityModifyEvent; +import org.hswebframework.web.crud.events.EntityPrepareModifyEvent; +import org.hswebframework.web.crud.events.EntityPrepareSaveEvent; import org.hswebframework.web.crud.events.EntitySavedEvent; +import org.hswebframework.web.exception.BusinessException; import org.jetlinks.core.device.DeviceRegistry; +import org.jetlinks.community.device.entity.DeviceInstanceEntity; import org.jetlinks.community.device.entity.DeviceProductEntity; import org.jetlinks.community.device.events.DeviceProductDeployEvent; import org.springframework.context.ApplicationEventPublisher; @@ -14,6 +20,7 @@ import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import java.util.List; +import java.util.Objects; /** * @author bestfeng @@ -24,6 +31,8 @@ public class DeviceProductHandler { private final LocalDeviceProductService productService; + private final LocalDeviceInstanceService instanceService; + private final DeviceRegistry deviceRegistry; private final ApplicationEventPublisher eventPublisher; @@ -50,11 +59,66 @@ public class DeviceProductHandler { .as(productService::findById) .filter(product -> product.getState() == 1) .flatMap(product -> deviceRegistry - .register(product.toProductInfo()) - .flatMap(i -> FastBeanCopier - .copy(product, new DeviceProductDeployEvent()) - .publish(eventPublisher)) - ) + .register(product.toProductInfo()) + .flatMap(i -> FastBeanCopier + .copy(product, new DeviceProductDeployEvent()) + .publish(eventPublisher)), + 8) .then(); } + + @EventListener + public void handleProductSaveEvent(EntityPrepareSaveEvent event) { + event.async( + Flux + .fromIterable(event.getEntity()) + .mapNotNull(DeviceProductEntity::getId) + .as(productService::findById) + .collectList() + .flatMap(before -> checkAccessIdChange(before, event.getEntity())) + ); + } + + @EventListener + public void handleProductSaveEvent(EntityPrepareModifyEvent event) { + event.async( + checkAccessIdChange(event.getBefore(), event.getAfter()) + ); + } + + /** + * 检查产品接入方式 + * 当产品下有设备时,禁止修改接入方式 + * + * @param before 修改前产品 + * @param after 修改后产品 + */ + private Mono checkAccessIdChange(List before, List after) { + return Flux + .fromIterable(before) + .collectMap(DeviceProductEntity::getId) + .filter(MapUtils::isNotEmpty) + .flatMap(beforeMap -> Flux + .fromIterable(after) + // 过滤出修改了接入方式的产品 + .filter(entity -> !Objects.equals(entity.getAccessId(), beforeMap.get(entity.getId()).getAccessId()) + || !Objects.equals(entity.getAccessProvider(), (beforeMap.get(entity.getId()).getAccessProvider()))) + .map(DeviceProductEntity::getId) + .collectList() + .filter(CollectionUtils::isNotEmpty) + .flatMap(productId -> instanceService + .createQuery() + .in(DeviceInstanceEntity::getProductId, productId) + .count() + .flatMap(deviceNum -> { + // 产品下有设备时,禁止修改接入方式 + if (deviceNum > 0) { + return Mono.error( + () -> new BusinessException("error.product_access_id_can_not_change_with_device") + ); + } + return Mono.empty(); + })) + .then()); + } } diff --git a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/service/DeviceProductNameSynchronizer.java b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/service/DeviceProductNameSynchronizer.java index 46512a03..add6afc8 100755 --- a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/service/DeviceProductNameSynchronizer.java +++ b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/service/DeviceProductNameSynchronizer.java @@ -1,11 +1,12 @@ package org.jetlinks.community.device.service; import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.apache.commons.collections4.CollectionUtils; -import org.hswebframework.web.crud.events.EntityModifyEvent; -import org.hswebframework.web.crud.events.EntityPrepareCreateEvent; -import org.hswebframework.web.crud.events.EntityPrepareSaveEvent; -import org.hswebframework.web.crud.events.EntitySavedEvent; +import org.hswebframework.web.crud.events.*; +import org.jetlinks.core.device.DeviceRegistry; +import org.jetlinks.core.trace.MonoTracer; +import org.jetlinks.community.PropertyConstants; import org.jetlinks.community.device.entity.DeviceInstanceEntity; import org.jetlinks.community.device.entity.DeviceProductEntity; import org.springframework.context.event.EventListener; @@ -28,12 +29,15 @@ import java.util.stream.Collectors; * @since 1.6 */ @Component +@Slf4j @AllArgsConstructor public class DeviceProductNameSynchronizer { private final LocalDeviceInstanceService instanceService; private final LocalDeviceProductService productService; + private final DeviceRegistry registry; + //自动更新产品名称 @EventListener public void autoUpdateProductName(EntityModifyEvent event) { @@ -47,7 +51,9 @@ public class DeviceProductNameSynchronizer { && before.get(product.getId()) != null && ( !Objects.equals(before.get(product.getId()).getName(), product.getName()) || - !Objects.equals(before.get(product.getId()).getDeviceType(), product.getDeviceType()) + !Objects.equals(before + .get(product.getId()) + .getDeviceType(), product.getDeviceType()) ) ) .flatMap(product -> instanceService @@ -55,8 +61,13 @@ public class DeviceProductNameSynchronizer { .set(DeviceInstanceEntity::getProductName, product.getName()) .set(DeviceInstanceEntity::getDeviceType, product.getDeviceType()) .where(DeviceInstanceEntity::getProductId, product.getId()) - .execute()) - ); + .execute() + //不触发事件,设备数量较多时,性能较差。 + .as(EntityEventHelper::setDoNotFireEvent) + .then(Objects.equals(before.get(product.getId()).getName(), product.getName()) + ? Mono.empty() + : syncDeviceProductName(product.getId(), product.getName())) + )); } //新增设备前填充产品名称和类型等信息 @@ -72,9 +83,7 @@ public class DeviceProductNameSynchronizer { @EventListener public void autoSetProductInfo(EntityPrepareSaveEvent event) { - event.async( - applyProductToDevice(event.getEntity()) - ); + event.async(applyProductToDevice(event.getEntity())); } protected Mono applyProductToDevice(Collection devices) { @@ -110,12 +119,34 @@ public class DeviceProductNameSynchronizer { Flux.fromIterable(event.getEntity()) .filter(product -> StringUtils.hasText(product.getName())) .flatMap(product -> instanceService - .createUpdate() - .set(DeviceInstanceEntity::getProductName, product.getName()) - .set(DeviceInstanceEntity::getDeviceType, product.getDeviceType()) - .where(DeviceInstanceEntity::getProductId, product.getId()) - .not(DeviceInstanceEntity::getProductName, product.getName()) - .execute()) + .createUpdate() + .set(DeviceInstanceEntity::getProductName, product.getName()) + .set(DeviceInstanceEntity::getDeviceType, product.getDeviceType()) + .where(DeviceInstanceEntity::getProductId, product.getId()) + .not(DeviceInstanceEntity::getProductName, product.getName()) + .execute() + //不触发事件,设备数量较多时,性能较差。 + .as(EntityEventHelper::setDoNotFireEvent) + .then(syncDeviceProductName(product.getId(), product.getName()) + ) + , 8) ); } + + private Mono syncDeviceProductName(String productId, String name) { + return Mono.fromRunnable(() -> syncDeviceProductNameAsync(productId, name)); + } + + @SuppressWarnings("all") + private void syncDeviceProductNameAsync(String productId, String name) { + registry + .getProduct(productId) + .flatMap(product -> product + .getDevices() + .flatMap(device -> device.setConfig(PropertyConstants.productName, name)) + .then()) + .as(MonoTracer.create("/product/" + productId + "/sync-device-name")) + .subscribe(null, + err -> log.warn("sync device product [{}] name error", productId, err)); + } } diff --git a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/service/DeviceTagHandler.java b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/service/DeviceTagHandler.java index 9a827eb7..9b70800e 100644 --- a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/service/DeviceTagHandler.java +++ b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/service/DeviceTagHandler.java @@ -3,18 +3,15 @@ package org.jetlinks.community.device.service; import lombok.AllArgsConstructor; import org.hswebframework.ezorm.core.dsl.Query; import org.hswebframework.ezorm.rdb.mapping.ReactiveRepository; -import org.hswebframework.web.crud.events.EntityCreatedEvent; import org.hswebframework.web.crud.events.EntityPrepareModifyEvent; import org.hswebframework.web.crud.events.EntityPrepareSaveEvent; -import org.hswebframework.web.crud.events.EntitySavedEvent; +import org.jetlinks.core.device.DeviceOperator; import org.jetlinks.core.device.DeviceProductOperator; import org.jetlinks.core.device.DeviceRegistry; -import org.jetlinks.core.device.DeviceThingType; 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.service.term.DeviceInstanceTerm; -import org.jetlinks.community.things.data.ThingsDataWriter; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.context.event.EventListener; import org.springframework.stereotype.Component; @@ -24,6 +21,7 @@ import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import java.util.List; +import java.util.Set; /** * 设备标签自动删除. @@ -39,8 +37,6 @@ public class DeviceTagHandler { private final ReactiveRepository tagRepository; - private final ThingsDataWriter dataWriter; - /** * 更新设备物模型时,若删除了标签,自动删除设备标签 */ @@ -69,15 +65,15 @@ public class DeviceTagHandler { .map(DeviceTagEntity::parseTagKey), // tp2:设备ID Mono.just(operator.getId()), - // tp3:新的标签 - Mono.justOrEmpty(device.getDeriveMetadata()) - // 设备物模型为空,则获取产品物模型 - .filter(StringUtils::hasText) - .map(metadata -> DeviceTagEntity.parseTagKey(device.getDeriveMetadata())) - .switchIfEmpty(operator - .getProduct() - .flatMap(DeviceProductOperator::getMetadata) - .map(DeviceTagEntity::parseTagKey)) + + Mono.zip( + getDeviceTags(device), + getProductTags(operator), + (deviceTags, productTags) -> { + deviceTags.addAll(productTags); + return deviceTags; + } + ) )) .flatMapMany(tp3 -> Flux .fromIterable(tp3.getT1()) @@ -88,6 +84,20 @@ public class DeviceTagHandler { .then(); } + private Mono> getDeviceTags(DeviceInstanceEntity device) { + return Mono.justOrEmpty(device.getDeriveMetadata()) + // 设备物模型为空,则获取产品物模型 + .filter(StringUtils::hasText) + .map(metadata -> DeviceTagEntity.parseTagKey(device.getDeriveMetadata())); + } + + private Mono> getProductTags(DeviceOperator operator){ + return operator + .getProduct() + .flatMap(DeviceProductOperator::getMetadata) + .map(DeviceTagEntity::parseTagKey); + } + /** * 更新产品物模型时,若删除了标签,自动删除设备标签 */ @@ -136,31 +146,5 @@ public class DeviceTagHandler { .then(); } - @EventListener - public void handleDeviceTagEvent(EntityCreatedEvent event) { - event.async(updateTag(event.getEntity())); - } - @EventListener - public void handleDeviceTagEvent(EntitySavedEvent event) { - event.async(updateTag(event.getEntity())); - } - - /** - * 更新标签消息 - * - * @param entityList 标签 - * @return Void - */ - private Mono updateTag(List entityList) { - return Flux - .fromIterable(entityList) - .flatMap(entity -> dataWriter - .updateTag(DeviceThingType.device.getId(), - entity.getDeviceId(), - entity.getKey(), - System.currentTimeMillis(), - entity.getValue())) - .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 index f470d8ae..c711e058 100755 --- 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 @@ -9,25 +9,18 @@ import org.hswebframework.ezorm.rdb.mapping.ReactiveUpdate; import org.hswebframework.ezorm.rdb.mapping.defaults.SaveResult; import org.hswebframework.web.api.crud.entity.PagerResult; import org.hswebframework.web.api.crud.entity.QueryParamEntity; +import org.hswebframework.web.api.crud.entity.TransactionManagers; 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.NotFoundException; 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.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.device.response.DeviceDetail; -import org.jetlinks.community.device.response.ResetDeviceConfigurationResult; -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.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; @@ -43,27 +36,45 @@ 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.ConfigPropertyMetadata; import org.jetlinks.core.metadata.DeviceMetadata; import org.jetlinks.core.metadata.MergeOption; +import org.jetlinks.core.trace.FluxTracer; +import org.jetlinks.core.trace.MonoTracer; +import org.jetlinks.core.utils.CompositeMap; 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.web.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.annotation.Transactional; import org.springframework.transaction.reactive.TransactionalOperator; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.core.scheduler.Schedulers; +import reactor.function.Function3; +import reactor.util.context.Context; import reactor.util.function.Tuple2; import reactor.util.function.Tuple3; import reactor.util.function.Tuples; import java.util.*; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.function.BiFunction; import java.util.function.Function; import java.util.stream.Collectors; @@ -108,7 +119,7 @@ public class LocalDeviceInstanceService extends GenericReactiveCrudService { instance.setState(null); - if (StringUtils.isEmpty(instance.getId())) { + if (!StringUtils.hasText(instance.getId())) { return handleCreateBefore(instance); } return registry @@ -119,51 +130,10 @@ public class LocalDeviceInstanceService extends GenericReactiveCrudService findByProductId(String productId) { - return createQuery() - .and(DeviceInstanceEntity::getProductId, productId) - .fetch(); - } - - private Set getProductConfigurationProperties(DeviceProductEntity product) { - if (MapUtils.isNotEmpty(product.getConfiguration())) { - return product.getConfiguration() - .keySet(); - } - return new HashSet<>(); - } - - private Mono> resetConfiguration(DeviceProductEntity product, DeviceInstanceEntity device) { - return metadataManager - .getProductConfigMetadataProperties(product.getId()) - .defaultIfEmpty(getProductConfigurationProperties(product)) - .flatMap(set -> { - if (set.size() > 0) { - if (MapUtils.isNotEmpty(device.getConfiguration())) { - set.forEach(device.getConfiguration()::remove); - } - //重置注册中心里的配置 - return registry - .getDevice(device.getId()) - .flatMap(opts -> opts.removeConfigs(set)) - .then(); - } - return Mono.empty(); - }) - .then( - //更新数据库 - createUpdate() - .set(device::getConfiguration) - .where(device::getId) - .execute() - ) - .then(Mono.fromSupplier(device::getConfiguration)); - } - /** * 重置设备配置 * @@ -178,37 +148,36 @@ public class LocalDeviceInstanceService extends GenericReactiveCrudService { DeviceProductEntity product = tp2.getT2(); DeviceInstanceEntity device = tp2.getT1(); - return resetConfiguration(product, device); + return Mono + .defer(() -> { + if (MapUtils.isNotEmpty(product.getConfiguration())) { + if (MapUtils.isNotEmpty(device.getConfiguration())) { + product.getConfiguration() + .keySet() + .forEach(device.getConfiguration()::remove); + } + //重置注册中心里的配置 + return registry + .getDevice(deviceId) + .flatMap(opts -> opts.removeConfigs(product.getConfiguration().keySet())) + .then(); + } + return Mono.empty(); + }) + .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(); + }) + ) + .then(Mono.fromSupplier(device::getConfiguration)); }) - .defaultIfEmpty(Collections.emptyMap()); - } - - public Mono resetConfiguration(Flux payload) { - return payload - .flatMap(deviceId -> resetConfiguration(deviceId)).count(); - } - - /** - * 重置设备配置信息(根据产品批量重置,性能欠佳,慎用) - * - * @param productId 产品ID - * @return 数量 - */ - public Flux resetConfigurationByProductId(String productId) { - return deviceProductService - .findById(productId) - .flatMapMany(product -> this - .findByProductId(productId) - .flatMap(device -> { - return resetConfiguration(product, device) - .thenReturn(ResetDeviceConfigurationResult - .success(SaveResult.of(0, 1))) - .onErrorResume(throwable -> { - String message = device.getId() + ":" + throwable.getMessage(); - return Mono.just(ResetDeviceConfigurationResult.error(message)); - }); - }) - ); + .defaultIfEmpty(Collections.emptyMap()) + ; } /** @@ -217,10 +186,12 @@ public class LocalDeviceInstanceService extends GenericReactiveCrudService deploy(String id) { - return findById(id) + return this + .findById(id) .flux() - .as(flux -> deploy(flux, Mono::error)) + .as(flux -> deploy(flux, (err, device) -> Flux.error(err))) .singleOrEmpty(); } @@ -233,7 +204,7 @@ public class LocalDeviceInstanceService extends GenericReactiveCrudService deploy(Flux flux) { return this - .deploy(flux, err -> Mono.empty()); + .deploy(flux, this::retryDeploy); // .contextWrite(TraceSourceException.deepTraceContext()); } @@ -243,19 +214,26 @@ public class LocalDeviceInstanceService extends GenericReactiveCrudService deploy(Flux flux, Function> fallback) { + public Flux deploy(Flux flux, + BiFunction, Flux> fallback) { //设备回滚 key: deviceId value: 操作 Map> rollback = new ConcurrentHashMap<>(); + List> fail = new CopyOnWriteArrayList<>(); return flux //添加回滚操作,用于再触发DeviceDeployedEvent事件执行失败时进行回滚. .flatMap(device -> registry .getDevice(device.getId()) + .onErrorResume(TraceSourceException.transfer("operation.device.deploy", device)) + .onErrorResume(err -> { + fail.add(deployFail(err, 1)); + return Mono.empty(); + }) .switchIfEmpty(Mono.fromRunnable(() -> { //设备之前没有注册的回滚操作(注销) rollback.put(device.getId(), registry.unregisterDevice(device.getId())); })) - .thenReturn(device)) + .thenReturn(device), 32, 32) //发布到注册中心 .flatMap(instance -> registry .register(instance.toDeviceInfo()) @@ -272,16 +250,22 @@ public class LocalDeviceInstanceService extends GenericReactiveCrudService success ? Mono.just(deviceOperator) : Mono.empty())) + .as(EntityEventHelper::setDoNotFireEvent) .thenReturn(instance) - //激活失败,忽略错误,继续处理其他设备 - .onErrorResume(e -> fallback.apply(e).then(Mono.empty())) - ) + //激活失败,返回错误,继续处理其他设备 + .onErrorResume(TraceSourceException.transfer("operation.device.deploy", instance)) + .onErrorResume(err -> { + fail.add(deployFail(err, 1)); + return Mono.empty(); + }), 16, 16) .buffer(200)//每200条数据批量更新 .publishOn(Schedulers.single()) .concatMap(all -> Flux .fromIterable(all) .groupBy(DeviceInstanceEntity::getState) .flatMap(group -> group + // 检查协议相关配置的必填项 + .flatMap(device -> validateConfiguration(device).thenReturn(device), 16, 16) .map(DeviceInstanceEntity::getId) .collectList() .flatMap(list -> createUpdate() @@ -292,8 +276,10 @@ public class LocalDeviceInstanceService extends GenericReactiveCrudService DeviceDeployResult.success(list.size())))) + .as(EntityEventHelper::setDoNotFireEvent) //推送激活事件 - .flatMap(res -> DeviceDeployedEvent.of(all).publish(eventPublisher).thenReturn(res)) + .flatMap(res -> DeviceDeployedEvent.of(all).publish(eventPublisher).thenReturn(res), + 16, 16) //传递国际化上下文 .as(LocaleUtils::transform) .as(transactionalOperator::transactional) @@ -301,24 +287,97 @@ public class LocalDeviceInstanceService extends GenericReactiveCrudService 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)) + .thenMany(fallback + .apply(err, all) + .switchIfEmpty(deployFail(err, all.size())) ) - .flatMap(res -> fallback.apply(err).thenReturn(res)) + .as(EntityEventHelper::setDoNotFireEvent) ) ) - //激活时不触发事件,单独处理DeviceDeployedEvent - .as(EntityEventHelper::setDoNotFireEvent) + .concatWith(Flux.fromIterable(fail).flatMap(Function.identity())) ; } + /** + * 重试启用 + * 将批量启用失败的设备,再次尝试单个启用 + * + * @param err 异常 + * @param device 设备列表 + * @return 结果 + */ + private Flux retryDeploy(Throwable err, + List device) { + if (device == null || device.size() == 1) { + // 单个启用错误时,直接返回错误结果 + return deployFail(err, 1); + } + return Flux.fromIterable(device) + // 启用单个设备 + .flatMap(d -> this.deploy(Flux.just(d), this::retryDeploy), 8) + .groupBy(DeviceDeployResult::isSuccess) + .flatMap(group -> { + if (group.key()) { + return group + .count() + .map(success -> DeviceDeployResult.success(success.intValue())); + } else { + return group; + } + }); + } + + private Flux deployFail(Throwable err, + int total) { + return Flux + .zip( + I18nSupportException.tryGetLocalizedMessageReactive(err), + TraceSourceException.tryGetOperationLocalizedReactive(err).defaultIfEmpty(""), + (msg, opt) -> new DeviceDeployResult(total, + false, + msg, + null, + TraceSourceException.tryGetSource(err), + opt) + ); + } + + private Mono validateConfiguration(DeviceInstanceEntity device) { + return metadataManager + .getDeviceConfigMetadata(device.getId()) + .flatMap(configMetadata -> { + List property = configMetadata + .getProperties() + .stream() + .map(ConfigPropertyMetadata::getProperty) + .collect(Collectors.toList()); + return registry + .getProduct(device.getProductId()) + .flatMap(product -> product.getConfigs(property)) + .map(Values::getAllValues) + .defaultIfEmpty(new HashMap<>()) + .map(conf -> { + if (MapUtils.isNotEmpty(device.getConfiguration())) { + return new CompositeMap<>(conf, device.getConfiguration()); + } + return conf; + }) + .zipWith(Mono.just(configMetadata)); + }) + .flatMap(tp2 -> DeviceConfigMetadataManager.validate(tp2.getT2(), tp2.getT1())) + .filter(validateResult -> !validateResult.isSuccess()) + .flatMap(validateResult -> { + if (log.isDebugEnabled()) { + log.debug(validateResult.getErrorMsg()); + } + return Mono + .error(() -> new TraceSourceException("error.device_configuration_required_must_not_be_null") + .withSource("operation.device.deploy", device)); + }) + .contextWrite(Context.of(DeviceInstanceEntity.class, device)) + .then(); + } + /** * 注销设备,取消后,设备无法再连接到服务. 注册中心也无法再获取到该设备信息. * @@ -338,6 +397,13 @@ public class LocalDeviceInstanceService extends GenericReactiveCrudService unregisterDevice(Publisher ids) { + return this + .handleUnregister(ids) + .count() + .map(Long::intValue); + } + + public Flux handleUnregister(Publisher ids) { return Flux .from(ids) .buffer(200) @@ -351,8 +417,10 @@ public class LocalDeviceInstanceService extends GenericReactiveCrudService registry @@ -360,11 +428,8 @@ public class LocalDeviceInstanceService extends GenericReactiveCrudService Mono.empty()) .then(registry.unregisterDevice(id)) - .onErrorResume(err -> Mono.empty()) - .thenReturn(id)) - .count() - .map(Long::intValue) - //注销不触发事件,单独处理DeviceDeployedEvent + .thenReturn(DeviceDeployResult.success(1)) + .onErrorResume(err -> Mono.just(DeviceDeployResult.error(err.getMessage(), id)))) .as(EntityEventHelper::setDoNotFireEvent); } @@ -478,9 +543,11 @@ public class LocalDeviceInstanceService extends GenericReactiveCrudService deviceIdList.indexOf(detail.getId()))) + .as(FluxTracer.create("/LocalDeviceInstanceService/convertDeviceInstanceToDetail")) ; } @@ -556,7 +623,7 @@ public class LocalDeviceInstanceService extends GenericReactiveCrudService { - log.warn("get device detail error", err); + log.warn("get device detail error:{}", err.getLocalizedMessage(), err); return Mono.just(detail); }); } @@ -590,7 +657,7 @@ public class LocalDeviceInstanceService extends GenericReactiveCrudService sender.readProperty(property).messageId(IDGenerator.SNOW_FLAKE_STRING.generate())) .flatMapMany(ReadPropertyMessageSender::send) @@ -608,7 +675,7 @@ public class LocalDeviceInstanceService extends GenericReactiveCrudService deviceOperator .messageSender() .readProperty(property) @@ -635,7 +702,7 @@ public class LocalDeviceInstanceService extends GenericReactiveCrudService operator .messageSender() .writeProperty() @@ -667,7 +734,7 @@ public class LocalDeviceInstanceService extends GenericReactiveCrudService operator .messageSender() .invokeFunction(functionId) @@ -685,18 +752,63 @@ public class LocalDeviceInstanceService extends GenericReactiveCrudService> 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; - }); + return registry + .getDevice(deviceId) + .switchIfEmpty(handleDeviceNotFoundInRegistry(deviceId)) + .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 Flux> prepareBatchSave(Flux> deviceStream) { + return deviceStream + .doOnNext(device -> { + if (!StringUtils.hasText(device.getT2().getProductId())) { + throw new IllegalArgumentException("error.productId_cannot_be_empty"); + } + }) + .collect(Collectors.groupingBy(tp2 -> tp2.getT2().getProductId())) + .flatMapMany(deviceMap -> { + Set productId = deviceMap.keySet(); + return deviceProductService + .findById(productId) + .doOnNext(product -> deviceMap + .getOrDefault(product.getId(), Collections.emptyList()) + .forEach(e -> e.getT2().setProductName(product.getName()))) + .thenMany( + Flux.fromIterable(deviceMap.values()) + .flatMap(Flux::fromIterable) + ); + }) + .doOnNext(device -> { + device.getT1().prepare(); + if (!StringUtils.hasText(device.getT2().getProductName())) { + throw new BusinessException("error.product_does_not_exist", 500, device.getT2().getProductId()); + } + }); + + } + + public Mono batchSave(Flux deviceStream) { + return this + .prepareBatchSave(deviceStream.map(detail -> Tuples.of(detail, detail.toInstance()))) + .collectList() + .flatMap(list -> Flux + .fromIterable(list) + .map(Tuple2::getT1) + .flatMapIterable(DeviceSaveDetail::getTags) + .as(tagRepository::save) + .then(Flux.fromIterable(list) + .map(Tuple2::getT2) + .as(this::save)) + ).map(SaveResult::getTotal); } private static Function> mapReply(Function function) { @@ -847,7 +959,12 @@ public class LocalDeviceInstanceService extends GenericReactiveCrudService insert(Publisher entityPublisher) { - return super.insert(Flux.from(entityPublisher).flatMap(this::handleCreateBefore)); + return super + .insert(Flux.from(entityPublisher) + .flatMap( + this::handleCreateBefore, + 16, + 16)); } @Override @@ -860,12 +977,13 @@ public class LocalDeviceInstanceService extends GenericReactiveCrudService handleCreateBefore(DeviceInstanceEntity instanceEntity) { return Mono .zip( - deviceProductService.findById(instanceEntity.getProductId()), + deviceProductService.concurrentFindById(instanceEntity.getProductId()), registry .getProduct(instanceEntity.getProductId()) .flatMap(DeviceProductOperator::getProtocol), (product, protocol) -> protocol.doBeforeDeviceCreate( - Transport.of(product.getTransportProtocol()), instanceEntity.toDeviceInfo(false)) + Transport.of(product.getTransportProtocol()), + instanceEntity.toDeviceInfo(false)) ) .flatMap(Function.identity()) .doOnNext(info -> { @@ -886,7 +1004,7 @@ public class LocalDeviceInstanceService extends GenericReactiveCrudService checkCyclicDependency(String id, String parentId) { - DeviceInstanceEntity instance = new DeviceInstanceEntity(); + DeviceInstanceEntity instance = DeviceInstanceEntity.of(); instance.setId(id); instance.setParentId(parentId); return checker.check(instance); @@ -922,4 +1040,33 @@ public class LocalDeviceInstanceService extends GenericReactiveCrudService handleDeviceNotFoundInRegistry(String deviceId) { + return Mono + .defer(() -> findById(deviceId) + .switchIfEmpty(Mono.error(() -> new NotFoundException.NoStackTrace("error.device_not_found", deviceId))) + .flatMap(ignore -> Mono.error(() -> new NotFoundException.NoStackTrace("error.device_not_activated", deviceId))) + ); + } + + public Mono handleUnbind(String gatewayId, List deviceIdList) { + return this + .createUpdate() + .setNull(DeviceInstanceEntity::getParentId) + .in(DeviceInstanceEntity::getId, deviceIdList) + .and(DeviceInstanceEntity::getParentId, gatewayId) + .execute() + .then(handleBindUnbind(gatewayId, Flux.fromIterable(deviceIdList), ProtocolSupport::onChildUnbind)); + } + + + private Mono handleBindUnbind(String gatewayId, + Flux childId, + Function3, Mono> operator) { + return registry + .getDevice(gatewayId) + .flatMap(gateway -> gateway + .getProtocol() + .flatMap(protocol -> operator.apply(protocol, gateway, childId.flatMap(registry::getDevice))) + ); + } } 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 d3b93828..986b0340 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 @@ -1,41 +1,140 @@ package org.jetlinks.community.device.service; +import com.github.benmanes.caffeine.cache.Caffeine; import lombok.extern.slf4j.Slf4j; import org.hswebframework.ezorm.rdb.mapping.ReactiveRepository; +import org.hswebframework.ezorm.rdb.mapping.defaults.SaveResult; +import org.hswebframework.web.api.crud.entity.PagerResult; +import org.hswebframework.web.api.crud.entity.QueryParamEntity; import org.hswebframework.web.bean.FastBeanCopier; import org.hswebframework.web.crud.events.EntityEventHelper; +import org.hswebframework.web.crud.events.EntityPrepareSaveEvent; 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.core.device.DeviceRegistry; +import org.jetlinks.core.metadata.DataType; +import org.jetlinks.core.metadata.types.ObjectType; +import org.jetlinks.community.device.entity.*; import org.jetlinks.community.device.enums.DeviceProductState; import org.jetlinks.community.device.events.DeviceProductDeployEvent; -import org.jetlinks.core.device.DeviceRegistry; +import org.jetlinks.community.device.timeseries.DeviceTimeSeriesMetric; +import org.jetlinks.community.timeseries.TimeSeriesManager; +import org.jetlinks.supports.official.JetLinksDeviceMetadataCodec; import org.reactivestreams.Publisher; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationEventPublisher; +import org.springframework.context.event.EventListener; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import reactor.util.function.Tuple2; +import reactor.util.function.Tuples; + +import java.time.Duration; +import java.util.*; @Service @Slf4j public class LocalDeviceProductService extends GenericReactiveCrudService { - @Autowired - private DeviceRegistry registry; + private final DeviceRegistry registry; - @Autowired - private ApplicationEventPublisher eventPublisher; + private final ApplicationEventPublisher eventPublisher; - @Autowired - private ReactiveRepository instanceRepository; + private final ReactiveRepository instanceRepository; + private final TimeSeriesManager timeSeriesManager; + private final DeviceConfigMetadataManager metadataManager; + + private final Map> productLoadCache = Caffeine + .newBuilder() + .expireAfterWrite(Duration.ofSeconds(2)) + .softValues() + .>build() + .asMap(); + + @SuppressWarnings("all") + public LocalDeviceProductService(DeviceRegistry registry, + ApplicationEventPublisher eventPublisher, + ReactiveRepository instanceRepository, + TimeSeriesManager timeSeriesManager, + DeviceConfigMetadataManager metadataManager) { + this.registry = registry; + this.eventPublisher = eventPublisher; + this.instanceRepository = instanceRepository; + this.timeSeriesManager = timeSeriesManager; + this.metadataManager = metadataManager; + } + + /** + * 并行支持的根据ID查询产品,用于在可能并行查询同一个产品id的场景,减少查询压力. + * + * @param id ID + * @return 产品实体 + */ + public Mono concurrentFindById(String id) { + return productLoadCache.computeIfAbsent(id, _id -> this + .findById(id) + .cache(val -> Duration.ofSeconds(2), err -> Duration.ZERO, () -> Duration.ofSeconds(1))); + } + + @EventListener + public void handlePrepareEvent(EntityPrepareSaveEvent event){ + event.async( + Flux + .fromIterable(event.getEntity()) + .doOnNext(product -> product.setState(null)) + ); + } + + @Override + public Mono save(Publisher entityPublisher) { + return Flux.from(entityPublisher) + .flatMap(product -> validateMetadata(product.getId(), product.getMetadata()).thenReturn(product)) + .as(super::save); + } + + @Override + public Mono updateById(String id, Mono entityPublisher) { + return super.updateById(id, entityPublisher + .doOnNext(product -> product.setState(null)) + .flatMap(product -> validateMetadata(product.getId(), product.getMetadata()).thenReturn(product)) + ); + } + + private Mono validateMetadata(String productId, String metadata) { + // FIXME: 2021/8/23 更好的校验方式 + JetLinksDeviceMetadataCodec.getInstance().doDecode(metadata); + return Mono.empty(); + } + + public Mono> queryProductDetail(QueryParamEntity entity) { + return this + .queryPager(entity) + .filter(e -> !CollectionUtils.isEmpty(e.getData())) + .flatMap(result -> this + .convertDetail(result.getData()) + .collectList() + .map(detailList -> PagerResult.of(result.getTotal(), detailList, entity))) + .defaultIfEmpty(PagerResult.empty()); + } + + public Flux queryProductDetailList(QueryParamEntity entity) { + return this + .query(entity) + .collectList() + .flatMapMany(this::convertDetail); + } + + @Transactional public Mono deploy(String id) { return findById(Mono.just(id)) .doOnNext(this::validateDeviceProduct) + // 检查协议相关配置的必填项 + .flatMap(product -> validateConfiguration(product).thenReturn(product)) .flatMap(product -> registry .register(product.toProductInfo()) .onErrorMap(e -> new BusinessException("error.unable_to_load_protocol_by_access_id", 404, product.getMessageProtocol())) @@ -60,6 +159,20 @@ public class LocalDeviceProductService extends GenericReactiveCrudService validateConfiguration(DeviceProductEntity product) { + return metadataManager + .getProductConfigMetadata(product.getId()) + .flatMap(configMetadata -> DeviceConfigMetadataManager.validate(configMetadata, product.getConfiguration())) + .filter(validateResult -> !validateResult.isSuccess()) + .flatMap(validateResult -> { + if (log.isDebugEnabled()) { + log.debug(validateResult.getErrorMsg()); + } + return Mono.error(() -> new BusinessException("error.product_configuration_required_must_not_be_null")); + }) + .then(); + } + public Mono cancelDeploy(String id) { return createUpdate() @@ -69,27 +182,146 @@ public class LocalDeviceProductService extends GenericReactiveCrudService registry .unregisterProduct(id) - .thenReturn(integer)); + .thenReturn(integer)) + ; } - @Override public Mono deleteById(Publisher idPublisher) { return Flux.from(idPublisher) - .collectList() - .flatMap(idList -> - instanceRepository.createQuery() - .where() - .in(DeviceInstanceEntity::getProductId, idList) - .count() - .flatMap(i -> { - if (i > 0) { - return Mono.error(new IllegalArgumentException("存在关联设备,无法删除!")); - } else { - return super.deleteById(Flux.fromIterable(idList)); - } - })); + .collectList() + .flatMap(idList -> + instanceRepository.createQuery() + .where() + .in(DeviceInstanceEntity::getProductId, idList) + .count() + .flatMap(i -> { + if (i > 0) { + return Mono.error(new IllegalArgumentException("error.cannot_deleted_because_device_is_associated_with_it")); + } else { + return super.deleteById(Flux.fromIterable(idList)); + } + })); } + @Deprecated + public Mono>> queryDeviceEvent(String productId, + String eventId, + QueryParamEntity entity, + boolean format) { + return registry + .getProduct(productId) + .flatMap(operator -> Mono.just(operator.getId()).zipWith(operator.getMetadata())) + .flatMap(tp -> timeSeriesManager + .getService(DeviceTimeSeriesMetric.deviceEventMetric(tp.getT1(), eventId)) + .queryPager(entity, data -> { + if (!format) { + return data.getData(); + } + Map formatData = new HashMap<>(data.getData()); + tp.getT2() + .getEvent(eventId) + .ifPresent(eventMetadata -> { + DataType type = eventMetadata.getType(); + if (type instanceof ObjectType) { + @SuppressWarnings("all") + Map val = (Map) type.format(formatData); + val.forEach((k, v) -> formatData.put(k + "_format", v)); + } else { + formatData.put("value_format", type.format(data.get("value"))); + } + }); + return formatData; + }) + ).defaultIfEmpty(PagerResult.empty()); + } + + /** + * 将产品的物模型合并到产品下设备的独立物模型中 + * + * @param productId 产品ID + * @return void + */ + public Mono mergeMetadataToDevice(String productId) { + return this + .findById(productId) + .flatMap(product -> JetLinksDeviceMetadataCodec.getInstance().decode(product.getMetadata())) + .flatMap(metadata -> instanceRepository + .createQuery() + //查询出产品下配置了独立物模型的设备 + .where(DeviceInstanceEntity::getProductId, productId) + .notNull(DeviceInstanceEntity::getDeriveMetadata) + .notEmpty(DeviceInstanceEntity::getDeriveMetadata) + .fetch() + //合并物模型 + .flatMap(instance -> instance.mergeMetadata(metadata).thenReturn(instance)) + //更新物模型 + .flatMap(instance -> instanceRepository + .createUpdate() + .set(instance::getDeriveMetadata) + .where(instance::getId) + .execute() + .then(registry.getDevice(instance.getId())) + .flatMap(device -> device.updateMetadata(instance.getDeriveMetadata())) + ) + .then()); + } + + @Deprecated + public Mono> queryDeviceProperties(String productId, QueryParamEntity entity) { + return timeSeriesManager + .getService(DeviceTimeSeriesMetric.devicePropertyMetric(productId)) + .queryPager(entity, data -> data.as(DevicePropertiesEntity.class)) + .defaultIfEmpty(PagerResult.empty()); + } + + @Deprecated + public Mono> queryDeviceLog(String productId, QueryParamEntity entity) { + return timeSeriesManager + .getService(DeviceTimeSeriesMetric.deviceLogMetric(productId)) + .queryPager(entity, data -> data.as(DeviceOperationLogEntity.class)) + .defaultIfEmpty(PagerResult.empty()); + } + + //转换为详情信息 + private Flux convertDetail(List list) { + return Flux + .fromIterable(list) + .index() + .flatMap( + indexTp2 -> Flux + .zip( + //产品所需配置信息 + metadataManager + .getProductConfigMetadata(indexTp2.getT2().getId()) + .collectList() + .onErrorReturn(Collections.emptyList()), + //产品特性 + metadataManager + .getProductFeatures(indexTp2.getT2().getId()) + .collectList() + .onErrorReturn(Collections.emptyList()), + //设备数量 + instanceRepository + .createQuery() + .where() + .and(DeviceInstanceEntity::getProductId, indexTp2.getT2().getId()) + .count() + ) + .map(tp3 -> Tuples + .of(indexTp2.getT1(), + ProductDetail + .of() + .with(indexTp2.getT2()) + .withConfigMetadatas(tp3.getT1()) + .withFeatures(tp3.getT2()) + .withDeviceCount(tp3.getT3())) + ) + , + 16) + //重新排序 + .sort(Comparator.comparingLong(Tuple2::getT1)) + .map(Tuple2::getT2); + } } diff --git a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/service/LocalProtocolSupportService.java b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/service/LocalProtocolSupportService.java deleted file mode 100644 index 236a06e5..00000000 --- a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/service/LocalProtocolSupportService.java +++ /dev/null @@ -1,55 +0,0 @@ -package org.jetlinks.community.device.service; - -import lombok.extern.slf4j.Slf4j; -import org.hswebframework.web.crud.service.GenericReactiveCrudService; -import org.hswebframework.web.exception.NotFoundException; -import org.jetlinks.community.device.entity.ProtocolSupportEntity; -import org.jetlinks.community.reference.DataReferenceManager; -import org.jetlinks.supports.protocol.management.ProtocolSupportManager; -import org.reactivestreams.Publisher; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Service; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; - -@Service -@Slf4j -public class LocalProtocolSupportService extends GenericReactiveCrudService { - - @Autowired - private ProtocolSupportManager supportManager; - - @Autowired - private DataReferenceManager referenceManager; - - @Override - public Mono deleteById(Publisher idPublisher) { - return Flux.from(idPublisher) - .flatMap(id -> supportManager.remove(id).thenReturn(id)) - .as(super::deleteById); - } - - public Mono deploy(String id) { - return findById(Mono.just(id)) - .switchIfEmpty(Mono.error(NotFoundException::new)) - .flatMap(r -> createUpdate() - .set(ProtocolSupportEntity::getState, 1) - .where(ProtocolSupportEntity::getId, id) - .execute()) - .map(i -> i > 0); - } - - public Mono unDeploy(String id) { - // 消息协议被使用时,不能禁用 - return referenceManager - .assertNotReferenced(DataReferenceManager.TYPE_PROTOCOL, id) - .then(findById(Mono.just(id))) - .switchIfEmpty(Mono.error(NotFoundException::new)) - .flatMap(r -> createUpdate() - .set(ProtocolSupportEntity::getState, 0) - .where(ProtocolSupportEntity::getId, id) - .execute()) - .map(i -> i > 0); - } - -} diff --git a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/service/ProductEventHandler.java b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/service/ProductEventHandler.java new file mode 100644 index 00000000..367c0ee9 --- /dev/null +++ b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/service/ProductEventHandler.java @@ -0,0 +1,94 @@ +package org.jetlinks.community.device.service; + +import lombok.AllArgsConstructor; +import org.hswebframework.web.crud.events.EntityPrepareModifyEvent; +import org.hswebframework.web.crud.events.EntityPrepareSaveEvent; +import org.jetlinks.core.device.DeviceRegistry; +import org.jetlinks.community.device.configuration.DeviceEventProperties; +import org.jetlinks.community.device.entity.DeviceInstanceEntity; +import org.jetlinks.community.device.entity.DeviceProductEntity; +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 wangsheng + */ +@Component +@AllArgsConstructor +public class ProductEventHandler { + + // 禁用 + private final static byte disabled = 0; + + // 启用 + private final static byte enabled = 1; + + private final DeviceEventProperties properties; + + private final LocalDeviceInstanceService deviceService; + + private final DeviceRegistry registry; + + + @EventListener + public void handleUndeploy(EntityPrepareModifyEvent event) { + if (properties.isOfflineWhenProductDisabled()) { + event.first( + handleUndeployProduct(event.getAfter()) + ); + } + } + + @EventListener + public void handleUndeploy(EntityPrepareSaveEvent event) { + if (properties.isOfflineWhenProductDisabled()) { + event.first( + handleUndeployProduct(event.getEntity()) + ); + } + } + + /** + * 产品禁用时使其下设备离线 + * + * @param productList 产品集合 + */ + private Flux handleUndeployProduct(List productList) { + return Flux + .fromIterable(productList) + .flatMap(this::disableDevice); + } + + private Mono disableDevice(DeviceProductEntity product) { + if (product.getState() != null && product.getState() == disabled) { + return findDeviceIdByProductId(product.getId()) + .flatMap(deviceId -> registry + .getDevice(deviceId) + .flatMap(operator -> operator + .isOnline() + .flatMap(isOnline -> { + // 设备在线则断开连接 + if (isOnline) { + return operator.disconnect(); + } + return Mono.empty(); + })) + ) + .then(); + } + return Mono.empty(); + } + + private Flux findDeviceIdByProductId(String productId) { + return deviceService + .createQuery() + .where() + .and(DeviceInstanceEntity::getProductId, productId) + .fetch() + .map(DeviceInstanceEntity::getId); + } +} diff --git a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/service/ProductReferenceDeviceGatewayProvider.java b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/service/ProductReferenceDeviceGatewayProvider.java index a423dd01..8622889c 100644 --- a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/service/ProductReferenceDeviceGatewayProvider.java +++ b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/service/ProductReferenceDeviceGatewayProvider.java @@ -29,7 +29,7 @@ public class ProductReferenceDeviceGatewayProvider implements DataReferenceProvi .fetch() .map(e -> DataReferenceInfo .of(e.getAccessId(), - DataReferenceManager.TYPE_DEVICE_GATEWAY, + DataReferenceManager.TYPE_PRODUCT, e.getId(), e.getName())); } @@ -42,7 +42,7 @@ public class ProductReferenceDeviceGatewayProvider implements DataReferenceProvi .filter(e -> StringUtils.hasText(e.getAccessId())) .map(e -> DataReferenceInfo .of(e.getAccessId(), - DataReferenceManager.TYPE_DEVICE_GATEWAY, + DataReferenceManager.TYPE_PRODUCT, e.getId(), e.getName())); } diff --git a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/service/ProtocolSupportHandler.java b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/service/ProtocolSupportHandler.java index 0fd269c9..a58cbb87 100644 --- a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/service/ProtocolSupportHandler.java +++ b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/service/ProtocolSupportHandler.java @@ -7,7 +7,7 @@ import org.hswebframework.web.crud.events.EntityModifyEvent; import org.hswebframework.web.crud.events.EntitySavedEvent; import org.hswebframework.web.exception.BusinessException; import org.hswebframework.web.i18n.LocaleUtils; -import org.jetlinks.community.device.entity.ProtocolSupportEntity; +import org.jetlinks.community.protocol.ProtocolSupportEntity; import org.jetlinks.community.reference.DataReferenceManager; import org.jetlinks.core.ProtocolSupport; import org.jetlinks.supports.protocol.management.ProtocolSupportLoader; 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 index 933b47bb..a8adf9d6 100755 --- 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 @@ -1,13 +1,9 @@ package org.jetlinks.community.device.service.data; +import com.google.common.collect.Lists; 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; @@ -20,6 +16,15 @@ 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.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.things.utils.ThingsDatabaseUtils; +import org.jetlinks.community.timeseries.query.Aggregation; +import org.jetlinks.community.timeseries.query.AggregationColumn; import org.jetlinks.core.event.Subscription; import org.jetlinks.core.message.DeviceMessage; import org.jetlinks.core.message.event.EventMessage; @@ -27,19 +32,10 @@ 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.metadata.types.ObjectType; 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.boot.CommandLineRunner; import org.springframework.boot.SpringApplication; @@ -112,17 +108,15 @@ public class DatabaseDeviceLatestDataService implements DeviceLatestDataService, //批量更新 .flatMap(sameTableData -> { Buffer first = sameTableData.get(0); - List> data = sameTableData - .stream() - .map(Buffer::getProperties) - .collect(Collectors.toList()); + List> data = Lists.transform(sameTableData, Buffer::getProperties); return this .doUpdateLatestData(first.table, data) .onErrorResume((err) -> { log.error("save device latest data error", err); return Mono.empty(); }); - })) + }, 8, 8) + ) .then(Reactors.ALWAYS_FALSE); } @@ -143,137 +137,6 @@ public class DatabaseDeviceLatestDataService implements DeviceLatestDataService, writer.stop(); } - static GeoCodec geoCodec = new GeoCodec(); - - static StringCodec stringCodec = new StringCodec(); - - @Override - public void run(String... args) throws Exception { - writer.start(); - SpringApplication - .getShutdownHandlers() - .add(writer::dispose); - } - - 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(() -> { @@ -299,13 +162,13 @@ public class DatabaseDeviceLatestDataService implements DeviceLatestDataService, table.addColumn(deviceName); for (PropertyMetadata property : metadata.getProperties()) { - table.addColumn(convertColumn(property)); + table.addColumn(ThingsDatabaseUtils.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); + RDBColumnMetadata column = ThingsDatabaseUtils.convertColumn(property); column.setName(getEventColumn(event.getId(), property.getId())); table.addColumn(column); } @@ -320,7 +183,6 @@ public class DatabaseDeviceLatestDataService implements DeviceLatestDataService, }); } - @Transactional(propagation = Propagation.NEVER) public Mono upgradeMetadata(String productId, DeviceMetadata metadata, boolean ddl) { return Mono .defer(() -> { @@ -335,13 +197,13 @@ public class DatabaseDeviceLatestDataService implements DeviceLatestDataService, .allowAlter(ddl); for (PropertyMetadata property : metadata.getProperties()) { - builder.addColumn(convertColumn(property)); + builder.addColumn(ThingsDatabaseUtils.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); + RDBColumnMetadata column = ThingsDatabaseUtils.convertColumn(property); column.setName(getEventColumn(event.getId(), property.getId())); builder.addColumn(column); } @@ -355,12 +217,14 @@ public class DatabaseDeviceLatestDataService implements DeviceLatestDataService, }); } + @Transactional(propagation = Propagation.REQUIRES_NEW) public Mono upgradeMetadata(String productId, DeviceMetadata metadata) { return upgradeMetadata(productId, metadata, true); } + @Override @Subscribe(topics = "/device/**", features = Subscription.Feature.local) - public void save(DeviceMessage message) { + public Mono saveAsync(DeviceMessage message) { try { Map properties = DeviceMessageUtils .tryGetProperties(message) @@ -380,7 +244,7 @@ public class DatabaseDeviceLatestDataService implements DeviceLatestDataService, return null; }); if (CollectionUtils.isEmpty(properties)) { - return; + return Mono.empty(); } String productId = message.getHeader("productId").map(String::valueOf).orElse("null"); String deviceName = message.getHeader("deviceName").map(String::valueOf).orElse(message.getDeviceId()); @@ -394,6 +258,19 @@ public class DatabaseDeviceLatestDataService implements DeviceLatestDataService, } catch (Exception e) { log.error(e.getMessage(), e); } + return Mono.empty(); + } + + public void save(DeviceMessage message) { + saveAsync(message).subscribe(); + } + + @Override + public void run(String... args) throws Exception { + writer.start(); + SpringApplication + .getShutdownHandlers() + .add(writer::dispose); } @Getter @@ -475,15 +352,15 @@ public class DatabaseDeviceLatestDataService implements DeviceLatestDataService, .getMetadata() .getCurrentSchema() .getTableReactive(table, false) - .flatMap(ignore -> { + .flatMap(_table -> { //没有deviceName,说明可能在同步表结构的时候发生了错误。 - if (!ignore.getColumn("deviceName").isPresent()) { + if (!_table.getColumn("deviceName").isPresent()) { log.warn("设备最新数据表[{}]结构错误", table); return Mono.empty(); } return databaseOperator .dml() - .upsert(table) + .upsert(_table) .ignoreUpdate("id") .values(properties) .execute() diff --git a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/service/data/DeviceDataManagerSupport.java b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/service/data/DeviceDataManagerSupport.java new file mode 100644 index 00000000..38a7791a --- /dev/null +++ b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/service/data/DeviceDataManagerSupport.java @@ -0,0 +1,106 @@ +package org.jetlinks.community.device.service.data; + +import lombok.AllArgsConstructor; +import org.hswebframework.web.api.crud.entity.QueryParamEntity; +import org.jetlinks.core.codec.Codec; +import org.jetlinks.core.codec.Codecs; +import org.jetlinks.core.event.EventBus; +import org.jetlinks.core.event.Subscription; +import org.jetlinks.core.message.ThingMessage; +import org.jetlinks.core.message.property.PropertyMessage; +import org.jetlinks.core.things.ThingProperty; +import org.jetlinks.core.things.ThingType; +import org.jetlinks.core.things.ThingsDataManagerSupport; +import org.jetlinks.community.things.ThingConstants; +import org.springframework.stereotype.Component; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +@AllArgsConstructor +@Component +public class DeviceDataManagerSupport implements ThingsDataManagerSupport { + private final DeviceDataService dataService; + + private final EventBus eventBus; + + @Override + public boolean isSupported(ThingType thingType) { + return true; + } + + @Override + public Mono getFirstProperty(ThingType thingType, String thingId) { + // FIXME: 2021/11/2 使用缓存 + // order by timestamp asc limit 0,1 + return QueryParamEntity + .newQuery() + .where() + .doPaging(0, 1) + .orderByAsc("timestamp") + .execute(param -> dataService.queryProperty(thingId, param)) + .take(1) + .map(data -> ThingProperty.of(data.getProperty(), data.getValue(), data.getTimestamp(), data.getState())) + .singleOrEmpty(); + } + + @Override + public Mono getAnyLastProperty(ThingType thingType, String thingId, long baseTime) { + // where timestamp < ? order by timestamp desc limit 0,1 + return QueryParamEntity + .newQuery() + .where() + .lt("timestamp", baseTime) + .doPaging(0, 1) + .orderByDesc("timestamp") + .execute(param -> dataService.queryProperty(thingId, param)) + .take(1) + .map(data -> ThingProperty.of(data.getProperty(), data.getValue(), data.getTimestamp(), data.getState())) + .singleOrEmpty(); + } + + @Override + public Mono getLastProperty(ThingType thingType, String thingId, String property, long baseTime) { + // where property = ? and timestamp < ? order by timestamp desc limit 0,1 + return QueryParamEntity + .newQuery() + .where() + .lt("timestamp", baseTime) + .doPaging(0, 1) + .orderByDesc("timestamp") + .execute(param -> dataService.queryProperty(thingId, param, property)) + .take(1) + .map(data -> ThingProperty.of(data.getProperty(), data.getValue(), data.getTimestamp(), data.getState())) + .singleOrEmpty(); + } + + @Override + public Mono getFirstProperty(ThingType thingType, String thingId, String property) { + // where property = ? order by timestamp asc limit 0,1 + return QueryParamEntity + .newQuery() + .where() + .doPaging(0, 1) + .orderByAsc("timestamp") + .execute(param -> dataService.queryProperty(thingId, param, property)) + .take(1) + .map(data -> ThingProperty.of(data.getProperty(), data.getValue(), data.getTimestamp(), data.getState())) + .singleOrEmpty(); + } + + private final static Codec CODEC = Codecs.lookup(ThingMessage.class); + + @Override + public Flux subscribeProperty(ThingType thingType, String thingId) { + return eventBus + .subscribe(Subscription + .builder() + .topics(ThingConstants.Topics.properties(thingType, thingId)) + .subscriberId("device-data:" + thingId) + .local() + .broker() + .build(), + CODEC) + .cast(PropertyMessage.class) + .flatMapIterable(PropertyMessage::getCompleteProperties); + } +} diff --git a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/service/data/DeviceDataRepository.java b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/service/data/DeviceDataRepository.java new file mode 100644 index 00000000..af707a3a --- /dev/null +++ b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/service/data/DeviceDataRepository.java @@ -0,0 +1,36 @@ +package org.jetlinks.community.device.service.data; + +import org.jetlinks.community.things.data.operations.SaveOperations; +import org.jetlinks.community.things.data.operations.TemplateOperations; +import org.jetlinks.community.things.data.operations.ThingOperations; +import reactor.core.publisher.Mono; + +/** + * 设备数据仓库,用户存储和查询设备相关历史数据 + * @author zhouhao + * @since 2.0 + * @see org.jetlinks.community.things.ThingsDataRepository + * @see org.jetlinks.community.things.data.ThingsDataRepositoryStrategy + */ +public interface DeviceDataRepository { + /** + * @return 返回保存操作接口, 用于对物数据进行保存 + */ + SaveOperations opsForSave(); + + /** + * 返回设备操作接口 + * + * @param deviceId 设备ID + * @return 操作接口 + */ + Mono opsForDevice(String deviceId); + + /** + * 返回产品操作接口. + * + * @param productId 产品ID + * @return 操作接口 + */ + Mono opsForProduct(String productId); +} 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 395a08b3..6403f34b 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 @@ -3,18 +3,24 @@ package org.jetlinks.community.device.service.data; import io.swagger.v3.oas.annotations.Hidden; import io.swagger.v3.oas.annotations.media.Schema; import lombok.*; +import org.hswebframework.ezorm.core.param.Term; +import org.hswebframework.ezorm.core.param.TermType; import org.hswebframework.web.api.crud.entity.PagerResult; import org.hswebframework.web.api.crud.entity.QueryParamEntity; -import org.jetlinks.community.Interval; -import org.jetlinks.community.device.entity.DeviceEvent; -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.hswebframework.web.validator.ValidatorUtils; import org.jetlinks.core.config.ConfigKey; import org.jetlinks.core.message.DeviceMessage; import org.jetlinks.core.metadata.DeviceMetadata; import org.jetlinks.core.metadata.EventMetadata; +import org.jetlinks.community.Interval; +import org.jetlinks.community.device.entity.DeviceEvent; +import org.jetlinks.community.device.entity.DeviceOperationLogEntity; +import org.jetlinks.community.device.entity.DeviceProperty; +import org.jetlinks.community.doc.QueryConditionOnly; +import org.jetlinks.community.timeseries.query.Aggregation; +import org.jetlinks.community.timeseries.query.AggregationData; +import org.jetlinks.community.utils.ConverterUtils; +import org.jetlinks.reactor.ql.utils.CastUtils; import org.joda.time.DateTime; import org.reactivestreams.Publisher; import org.springframework.util.StringUtils; @@ -22,19 +28,23 @@ import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; import java.util.Collection; import java.util.Date; +import java.util.List; import java.util.Map; /** - * 设备数据服务 + * 设备数据服务,本接口已不推荐使用,建议使用{@link DeviceDataRepository}. * * @author zhouhao * @since 1.5 + * @see DeviceDataRepository */ public interface DeviceDataService { - ConfigKey STORE_POLICY_CONFIG_KEY = ConfigKey.of("storePolicy", "存储策略", String.class); /** @@ -47,14 +57,22 @@ public interface DeviceDataService { Mono registerMetadata(@Nonnull String productId, @Nonnull DeviceMetadata metadata); - Mono reloadMetadata(@Nonnull String productId, @Nonnull DeviceMetadata metadata); + /** + * 重新加载物模型信息 + * + * @param productId 产品ID + * @param metadata 物模型 + * @return void + */ + Mono reloadMetadata(@Nonnull String productId, + @Nonnull DeviceMetadata metadata); /** * 批量保存消息 * * @param message 设备消息 * @return void - * @see this#saveDeviceMessage(Publisher) + * @see DeviceDataService#saveDeviceMessage(Publisher) */ @Nonnull default Mono saveDeviceMessage(@Nonnull Collection message) { @@ -108,7 +126,7 @@ public interface DeviceDataService { @Nonnull String... properties); /** - * 查询指定的设备属性列表 + * 查询指定的设备属性列表,通常用于查询单个属性,或者某个时间内的属性数据. * * @param deviceId 设备ID * @param query 查询条件 @@ -120,6 +138,33 @@ public interface DeviceDataService { @Nonnull QueryParamEntity query, @Nonnull String... property); + /** + * 根据产品ID查询设备列表 + * + * @param productId 产品ID + * @param query 查询条件 + * @param property 属性列表 + * @return 设备属性 + */ + @Nonnull + Flux queryPropertyByProductId(@Nonnull String productId, + @Nonnull QueryParamEntity query, + @Nonnull String... property); + + /** + * 根据时间聚合查询前n个属性值,{@link DeviceProperty#getFormatTime()}为所在时间区间 + * + * @param deviceId 设备ID + * @param request 聚合参数 + * @param properties 属性列表,空时查询全部属性 + * @return 属性值集合 + */ + @Nonnull + Flux queryTopProperty(@Nonnull String deviceId, + @Nonnull AggregationRequest request, + int numberOfTop, + @Nonnull String... properties); + /** * 根据产品ID聚合查询属性 * @@ -157,7 +202,6 @@ public interface DeviceDataService { @Nonnull String property, @Nonnull QueryParamEntity query); - /** * 分页查询属性 * @@ -171,6 +215,66 @@ public interface DeviceDataService { @Nonnull QueryParamEntity query, @Nonnull String... property); + /** + * 分页查询设备属性数据,一个属性为一列,仅支持部分存储策略。 + * + * @param deviceId 设备ID + * @param query 查询条件 + * @return 查询结果 + */ + @Nonnull + Mono> queryPropertiesPage(@Nonnull String deviceId, + @Nonnull QueryParamEntity query); + + /** + * 查询设备属性数据,但是不返回分页结果 + * + * @param deviceId 设备ID + * @param query 查询条件 + * @return 查询结果 + */ + @Nonnull + Flux queryProperties(@Nonnull String deviceId, + @Nonnull QueryParamEntity query); + + /** + * 根据产品分页查询属性数据,一个属性为一列,仅支持部分存储策略 + * + * @param productId 产品ID + * @param query 查询条件 + * @return 查询结果 + */ + @Nonnull + Mono> queryPropertiesPageByProduct(@Nonnull String productId, + @Nonnull QueryParamEntity query); + + /** + * 按产品ID分页查询属性 + * + * @param productId 产品ID + * @param query 查询条件 + * @return 分页查询结果 + * @since 1.8 + */ + @Nonnull + Mono> queryPropertyPageByProductId(@Nonnull String productId, + @Nonnull String property, + @Nonnull QueryParamEntity query); + + /** + * 按产品ID分页查询属性 + * + * @param productId 产品ID + * @param query 查询条件 + * @return 分页查询结果 + * @since 1.9 + */ + @Nonnull + Mono> queryPropertyPageByProductId(@Nonnull String productId, + @Nonnull QueryParamEntity query, + @Nonnull String... property); + + /** * 分页查询设备日志 * @@ -181,6 +285,12 @@ public interface DeviceDataService { Mono> queryDeviceMessageLog(@Nonnull String deviceId, @Nonnull QueryParamEntity query); + Flux queryDeviceMessageLogNoPaging(@Nonnull String deviceId, + @Nonnull QueryParamEntity query); + + Flux queryDeviceMessageLogNoPagingByProduct(@Nonnull String productId, + @Nonnull QueryParamEntity query); + /** * 查询设备事件,如果设置里format为true,将根据物模型对数据进行{@link org.jetlinks.core.metadata.DataType#format(Object)}. @@ -223,6 +333,21 @@ public interface DeviceDataService { @Nonnull QueryParamEntity query, boolean format); + /** + * 分页查询设备事件数据 + * + * @param productId 设备ID + * @param event 事件ID + * @param query 查询条件 + * @param format 是否对数据进行格式化 + * @return 分页查询结果 + */ + @Nonnull + Mono> queryEventPageByProductId(@Nonnull String productId, + @Nonnull String event, + @Nonnull QueryParamEntity query, + boolean format); + /** * 分页查询设备事件 * @@ -238,28 +363,6 @@ public interface DeviceDataService { return queryEventPage(deviceId, event, query, false); } - /** - * 查询设备属性数据,但是不返回分页结果 - * - * @param deviceId 设备ID - * @param query 查询条件 - * @return 查询结果 - */ - @Nonnull - Flux queryProperties(@Nonnull String deviceId, - @Nonnull QueryParamEntity query); - - /** - * 根据产品分页查询属性数据,一个属性为一列,仅支持部分存储策略 - * - * @param productId 产品ID - * @param query 查询条件 - * @return 查询结果 - */ - @Nonnull - Mono> queryPropertiesPageByProduct(@Nonnull String productId, - @Nonnull QueryParamEntity query); - @Getter @Setter @@ -267,20 +370,33 @@ public interface DeviceDataService { @NoArgsConstructor class DevicePropertyAggregation { @Schema(description = "属性ID") + @NotBlank private String property; //要聚合对字段 @Schema(description = "别名,默认和property一致") private String alias; //别名 @Schema(description = "聚合方式,支持(count,sum,max,min,avg)", type = "string") + @NotNull private Aggregation agg; //聚合函数 + @Schema(description = "聚合结果的数值精度") + Integer scale; + + public DevicePropertyAggregation(String property, String alias, Aggregation agg) { + this(property, alias, agg, null); + } + public String getAlias() { if (StringUtils.isEmpty(alias)) { return property; } return alias; } + + public void validate() { + ValidatorUtils.tryValidate(this); + } } @Getter @@ -290,13 +406,19 @@ public interface DeviceDataService { @NoArgsConstructor class AggregationRequest { //时间间隔 + //为空时,不按时间分组 @Schema(description = "间隔,如: 1d", type = "string", defaultValue = "1d") + @Nullable + @Builder.Default Interval interval = Interval.ofDays(1); + //时间格式 @Schema(defaultValue = "时间格式,如:yyyy-MM-dd", description = "yyyy-MM-dd") + @Builder.Default String format = "yyyy-MM-dd"; @Schema(description = "时间从,如: 2020-09-01 00:00:00,支持表达式: now-1d") + @Builder.Default Date from = new DateTime() .plusMonths(-1) .withHourOfDay(0) @@ -305,17 +427,20 @@ public interface DeviceDataService { .toDate(); @Schema(description = "时间到,如: 2020-09-30 00:00:00,支持表达式: now-1d") + @Builder.Default Date to = new DateTime() .withHourOfDay(23) .withMinuteOfHour(59) .withSecondOfMinute(59) .toDate(); - @Schema(description = "实例限制") + @Schema(description = "数量限制") + @Builder.Default int limit = 30; //过滤条件 - @Schema(description = "过滤条件") + @Schema(description = "过滤条件", implementation = QueryConditionOnly.class) + @Builder.Default QueryParamEntity filter = QueryParamEntity.of(); public AggregationRequest copy() { @@ -326,5 +451,28 @@ public interface DeviceDataService { public void setQuery(QueryParamEntity filter) { setFilter(filter); } + + public void prepareTimestampCondition() { + for (Term term : filter.getTerms()) { + if ("timestamp".equals(term.getColumn())) { + if (TermType.btw.equals(term.getTermType())) { + List values = ConverterUtils.convertToList(term.getValue()); + if (values.size() > 0) { + from = CastUtils.castDate(values.get(0)); + } + if (values.size() > 1) { + to = CastUtils.castDate(values.get(1)); + } + term.setValue(null); + } else if (TermType.gt.equals(term.getTermType()) || TermType.gte.equals(term.getTermType())) { + from = CastUtils.castDate(term.getValue()); + term.setValue(null); + } else if (TermType.lt.equals(term.getTermType()) || TermType.lte.equals(term.getTermType())) { + to = CastUtils.castDate(term.getValue()); + term.setValue(null); + } + } + } + } } } diff --git a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/service/data/DeviceDataStorageProperties.java b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/service/data/DeviceDataStorageProperties.java index 23ae8371..6c349974 100755 --- a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/service/data/DeviceDataStorageProperties.java +++ b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/service/data/DeviceDataStorageProperties.java @@ -2,9 +2,13 @@ package org.jetlinks.community.device.service.data; import lombok.Getter; import lombok.Setter; +import org.jetlinks.core.config.ConfigKey; import org.jetlinks.community.things.data.operations.DataSettings; import org.springframework.boot.context.properties.ConfigurationProperties; +import java.util.HashMap; +import java.util.Map; + @ConfigurationProperties(prefix = "jetlinks.device.storage") @Getter @Setter @@ -13,6 +17,48 @@ public class DeviceDataStorageProperties extends DataSettings { //默认数据存储策略,每个属性为一行数据 private String defaultPolicy = "default-row"; + //是否保存最新数据到数据库 + private boolean enableLastDataInDb = false; + + private Metric metric = new Metric(); + + @Getter + @Setter + public static class Metric { + /** + * 属性表前缀 {前缀}{产品ID} + */ + private String propertyPrefix = "properties_"; + + /** + * 事件表前缀 {前缀}{产品ID}_{事件ID} + *

+ * 如果设置了{@link Event#eventIsAllInOne()}, + * 则为 {前缀}{产品ID}_events + */ + private String eventPrefix = "event_"; + + /** + * 日志表前缀 {前缀}{产品ID} + */ + private String logPrefix = "device_log_"; + + /** + * 所有数据存储到日志表时的表名 + * + * @see DeviceDataStorageProperties#getLog() + * @see Log#isAllInOne() + */ + private String logAllInOne = "device_all_log"; + + /** + * 存储策略自定义配置 + * + * @see org.jetlinks.community.things.data.operations.MetricBuilder#option(ConfigKey) + */ + private Map options = new HashMap<>(); + + } public Log getLog() { return getLogFilter(); 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 index a205fe8f..2ab06308 100755 --- 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 @@ -49,8 +49,18 @@ public interface DeviceLatestDataService { * * @param message 设备消息 */ + @Deprecated void save(DeviceMessage message); + /** + * 保存消息数据 + * + * @param message 设备消息 + */ + default Mono saveAsync(DeviceMessage message){ + return Mono.fromRunnable(()->save(message)); + } + /** * 根据产品ID 查询最新的数据 * diff --git a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/service/data/DeviceThingsDataCustomizer.java b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/service/data/DeviceThingsDataCustomizer.java index 13282122..f01bc973 100644 --- a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/service/data/DeviceThingsDataCustomizer.java +++ b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/service/data/DeviceThingsDataCustomizer.java @@ -1,12 +1,15 @@ package org.jetlinks.community.device.service.data; import lombok.AllArgsConstructor; -import org.jetlinks.community.device.timeseries.DeviceTimeSeriesMetric; +import org.jetlinks.core.config.ConfigKey; import org.jetlinks.community.things.data.ThingsDataContext; import org.jetlinks.community.things.data.ThingsDataCustomizer; import org.jetlinks.community.things.data.operations.MetricBuilder; import org.springframework.stereotype.Component; +import javax.annotation.Nonnull; +import java.util.Optional; + @Component @AllArgsConstructor public class DeviceThingsDataCustomizer implements ThingsDataCustomizer { @@ -20,29 +23,57 @@ public class DeviceThingsDataCustomizer implements ThingsDataCustomizer { context.customMetricBuilder( ThingsBridgingDeviceDataService.thingType, new MetricBuilder() { + + @Override + public Optional option(ConfigKey key) { + Object value = properties.getMetric().getOptions().get(key.getKey()); + if (value == null) { + return Optional.empty(); + } + return Optional.of(key.convertValue(value)); + } + + @Override + public String getTemplateIdProperty() { + return "productId"; + } + @Override public String getThingIdProperty() { return "deviceId"; } @Override - public String createLogMetric(String thingType, String thingTemplateId, String thingId) { - return DeviceTimeSeriesMetric.deviceLogMetricId(thingTemplateId); + public String createLogMetric(@Nonnull String thingType, @Nonnull String thingTemplateId, String thingId) { + return properties.getMetric().getLogPrefix() + thingTemplateId; +// return DeviceTimeSeriesMetric.deviceLogMetricId(thingTemplateId); } @Override - public String createPropertyMetric(String thingType, String thingTemplateId, String thingId) { - return DeviceTimeSeriesMetric.devicePropertyMetricId(thingTemplateId); + public String createLogMetric(@Nonnull String thingType) { + return properties.getMetric().getLogAllInOne(); } @Override - public String createEventAllInOneMetric(String thingType, String thingTemplateId, String thingId) { - return MetricBuilder.super.createEventAllInOneMetric(thingType, thingTemplateId, thingId); + public String createPropertyMetric(@Nonnull String thingType, @Nonnull String thingTemplateId, String thingId) { + return properties.getMetric().getPropertyPrefix() + thingTemplateId; +// return DeviceTimeSeriesMetric.devicePropertyMetricId(thingTemplateId); } @Override - public String createEventMetric(String thingType, String thingTemplateId, String thingId, String eventId) { - return DeviceTimeSeriesMetric.deviceEventMetricId(thingTemplateId, eventId); + public String createEventAllInOneMetric(@Nonnull String thingType, @Nonnull String thingTemplateId, String thingId) { + return properties.getMetric().getEventPrefix() + thingTemplateId + "_events"; + + // return MetricBuilder.super.createEventAllInOneMetric(thingType, thingTemplateId, thingId); + } + + @Override + public String createEventMetric(@Nonnull String thingType, + @Nonnull String thingTemplateId, + String thingId, + @Nonnull String eventId) { + return properties.getMetric().getEventPrefix() + thingTemplateId + "_" + eventId; + // return DeviceTimeSeriesMetric.deviceEventMetricId(thingTemplateId, eventId); } } ); 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 index 6a15553d..4a8296dd 100755 --- 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 @@ -1,6 +1,7 @@ package org.jetlinks.community.device.service.data; import lombok.AllArgsConstructor; +import org.hswebframework.web.i18n.LocaleUtils; import org.jetlinks.core.Value; import org.jetlinks.core.device.DeviceRegistry; import org.jetlinks.core.metadata.*; @@ -14,6 +15,8 @@ import org.springframework.stereotype.Component; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import java.util.Locale; + @Component @AllArgsConstructor public class StorageDeviceConfigMetadataSupplier implements DeviceConfigMetadataSupplier { @@ -21,21 +24,47 @@ public class StorageDeviceConfigMetadataSupplier implements DeviceConfigMetadata 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 ConfigMetadata createObjectConf(Locale locale) { + return new DefaultConfigMetadata(LocaleUtils.resolveMessage("message.device.storage.config-metadata.name", locale, "存储配置"), "") + .scope(DeviceConfigScope.product) + .add(StorageConstants.propertyStorageType, LocaleUtils.resolveMessage("message.device.storage.config-metadata.type.name", locale, "存储方式"), getSaveEnumType(locale)); + } - 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, "不存储", "不存储此属性值")) - ); + private ConfigMetadata createAnotherConf(Locale locale) { + return new DefaultConfigMetadata(LocaleUtils.resolveMessage("message.device.storage.config-metadata.name", locale, "存储配置"), "") + .scope(DeviceConfigScope.product, DeviceConfigScope.device) + .add(StorageConstants.propertyStorageType, LocaleUtils.resolveMessage("message.device.storage.config-metadata.type.name", locale, "存储方式"), new EnumType() + .addElement(EnumType.Element.of("direct", LocaleUtils.resolveMessage("message.device.storage.config-metadata.type.direct.name", locale, "直接存储"), + LocaleUtils.resolveMessage("message.device.storage.config-metadata.type.direct.desc", locale, "将上报的属性值保存到配置到存储策略中"))) + .addElement(EnumType.Element.of(StorageConstants.propertyStorageTypeIgnore, LocaleUtils.resolveMessage("message.device.storage.config-metadata.type.ignore.name", locale, "不存储"), + LocaleUtils.resolveMessage("metadata.device.storage.config-metadata.type.ignore.desc", locale, "不存储此属性值")))); + } + private ConfigMetadata createDeviceObjectConf(Locale locale) { + return new DefaultConfigMetadata(LocaleUtils.resolveMessage("message.device.storage.config-metadata.name", locale, "存储配置"), "") + .scope(DeviceConfigScope.device) + .add(StorageConstants.propertyStorageType, LocaleUtils.resolveMessage("message.device.storage.config-metadata.type.name", locale, "存储方式"), getSaveEnumType(locale)); + } + + private ConfigMetadata createAnotherDeviceObjectConf(Locale locale) { + return new DefaultConfigMetadata(LocaleUtils.resolveMessage("message.device.storage.config-metadata.name", locale, "存储配置"), "") + .scope(DeviceConfigScope.device) + .add(StorageConstants.propertyStorageType, LocaleUtils.resolveMessage("message.device.storage.config-metadata.type.name", locale, "存储方式"), new EnumType() + .addElement(EnumType.Element.of(StorageConstants.propertyStorageTypeIgnore, LocaleUtils.resolveMessage("message.device.storage.config-metadata.type.ignore.name", locale, "不存储"), + LocaleUtils.resolveMessage("metadata.device.storage.config-metadata.type.ignore.desc", locale, "不存储此属性值"))) + .addElement(EnumType.Element.of(StorageConstants.propertyStorageTypeJson, LocaleUtils.resolveMessage("message.device.storage.config-metadata.type.json.name", locale, "JSON字符"), + LocaleUtils.resolveMessage("message.device.storage.config-metadata.type.json.desc", locale, "将数据序列化为JSON字符串进行存储")))); + } + + private EnumType getSaveEnumType(Locale locale) { + return new EnumType() + .addElement(EnumType.Element.of("direct", LocaleUtils.resolveMessage("message.device.storage.config-metadata.type.direct.name", locale, "直接存储"), + LocaleUtils.resolveMessage("message.device.storage.config-metadata.type.direct.desc", locale, "将上报的属性值保存到配置到存储策略中"))) + .addElement(EnumType.Element.of(StorageConstants.propertyStorageTypeIgnore, LocaleUtils.resolveMessage("message.device.storage.config-metadata.type.ignore.name", locale, "不存储"), + LocaleUtils.resolveMessage("metadata.device.storage.config-metadata.type.ignore.desc", locale, "不存储此属性值"))) + .addElement(EnumType.Element.of(StorageConstants.propertyStorageTypeJson, LocaleUtils.resolveMessage("message.device.storage.config-metadata.type.json.name", locale, "JSON字符"), + LocaleUtils.resolveMessage("message.device.storage.config-metadata.type.json.desc", locale, "将数据序列化为JSON字符串进行存储"))); + } @Override public Flux getDeviceConfigMetadata(String deviceId) { @@ -74,18 +103,36 @@ public class StorageDeviceConfigMetadataSupplier implements DeviceConfigMetadata 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 registry + .getProduct(productId) + .flatMap(prod -> prod + .getConfig(StorageConstants.storePolicyConfigKey) + .map(Value::asString)) + .defaultIfEmpty(properties.getDefaultPolicy()) + .flatMapMany(policy -> { + if ((ObjectType.ID.equals(typeId) || ArrayType.ID.equals(typeId)) && policy.startsWith("default-")) { + if ("default-row".equals(policy)) { + // ES行式存储时,只能存在一个对象类型的属性。设备仅支持保存为JSON字符 + return LocaleUtils + .currentReactive() + .flatMapMany(locale -> Flux + .just(createObjectConf(locale), createAnotherDeviceObjectConf(locale)) + ); + } + + return LocaleUtils + .currentReactive() + .flatMapMany(locale -> Flux + .just(createObjectConf(locale), createDeviceObjectConf(locale)) + );} + // 存储配置为不存储时,不返回存储配置的定义 + if (!policy.equals("none")) { + return LocaleUtils + .currentReactive() + .flatMapMany(locale -> Flux.just(createAnotherConf(locale))); + } + return Flux.empty(); + }); } return Flux.empty(); diff --git a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/service/data/ThingsBridgingDeviceDataService.java b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/service/data/ThingsBridgingDeviceDataService.java index 4385d93b..233d17e0 100644 --- a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/service/data/ThingsBridgingDeviceDataService.java +++ b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/service/data/ThingsBridgingDeviceDataService.java @@ -4,9 +4,6 @@ import lombok.AllArgsConstructor; import org.hswebframework.web.api.crud.entity.PagerResult; import org.hswebframework.web.api.crud.entity.QueryParamEntity; import org.hswebframework.web.bean.FastBeanCopier; -import org.jetlinks.community.things.data.AggregationRequest; -import org.jetlinks.community.things.data.operations.ColumnModeQueryOperations; -import org.jetlinks.community.things.data.operations.SaveOperations; import org.jetlinks.core.device.DeviceThingType; import org.jetlinks.core.message.DeviceMessage; import org.jetlinks.core.metadata.DeviceMetadata; @@ -15,6 +12,10 @@ import org.jetlinks.community.device.entity.DeviceOperationLogEntity; import org.jetlinks.community.device.entity.DeviceProperty; import org.jetlinks.community.things.ThingsDataRepository; import org.jetlinks.community.things.data.PropertyAggregation; +import org.jetlinks.community.things.data.operations.ColumnModeQueryOperations; +import org.jetlinks.community.things.data.operations.SaveOperations; +import org.jetlinks.community.things.data.operations.TemplateOperations; +import org.jetlinks.community.things.data.operations.ThingOperations; import org.jetlinks.community.timeseries.query.AggregationData; import org.reactivestreams.Publisher; import org.springframework.stereotype.Component; @@ -28,7 +29,7 @@ import java.util.stream.Stream; @Component @AllArgsConstructor -public class ThingsBridgingDeviceDataService implements DeviceDataService { +public class ThingsBridgingDeviceDataService implements DeviceDataService ,DeviceDataRepository{ private final ThingsDataRepository repository; static final String thingType = DeviceThingType.device.getId(); @@ -85,6 +86,7 @@ public class ThingsBridgingDeviceDataService implements DeviceDataService { } @Nonnull + @Override public Flux queryPropertyByProductId(@Nonnull String productId, @Nonnull QueryParamEntity query, @Nonnull String... property) { return repository .opsForTemplate(thingType, productId) @@ -93,6 +95,7 @@ public class ThingsBridgingDeviceDataService implements DeviceDataService { } @Nonnull + @Override public Flux queryTopProperty(@Nonnull String deviceId, @Nonnull AggregationRequest request, int numberOfTop, @@ -136,6 +139,7 @@ public class ThingsBridgingDeviceDataService implements DeviceDataService { } @Nonnull + @Override public Mono> queryPropertyPage(@Nonnull String deviceId, @Nonnull QueryParamEntity query, @Nonnull String... property) { return repository .opsForThing(thingType, deviceId) @@ -157,11 +161,46 @@ public class ThingsBridgingDeviceDataService implements DeviceDataService { } @Nonnull - public Mono> queryPropertyPageByProductId(@Nonnull String productId, @Nonnull String property, @Nonnull QueryParamEntity query) { - return queryPropertyPageByProductId(property, query, property); + @Override + public Mono> queryPropertiesPage(@Nonnull String deviceId, @Nonnull QueryParamEntity query) { + return repository + .opsForThing(thingType, deviceId) + .flatMap(opt -> opt.forQuery().unwrap(ColumnModeQueryOperations.class).queryAllPropertiesPage(query)) + .map(page -> PagerResult + .of(page.getTotal(), + page.getData().stream() + .map(DeviceProperties::new) + .collect(Collectors.toList()), + query + )); } @Nonnull + @Override + public Flux queryProperties(@Nonnull String deviceId, @Nonnull QueryParamEntity query) { + return repository + .opsForThing(thingType, deviceId) + .flatMapMany(opt -> opt.forQuery().unwrap(ColumnModeQueryOperations.class).queryAllProperties(query)) + .map(DeviceProperties::new); + } + + @Nonnull + @Override + public Mono> queryPropertiesPageByProduct(@Nonnull String productId, @Nonnull QueryParamEntity query) { + return repository + .opsForTemplate(thingType, productId) + .flatMap(opt -> opt.forQuery().unwrap(ColumnModeQueryOperations.class).queryAllPropertiesPage(query)) + .map(page -> convertPage(page,DeviceProperties::new)); + } + + @Nonnull + @Override + public Mono> queryPropertyPageByProductId(@Nonnull String productId, @Nonnull String property, @Nonnull QueryParamEntity query) { + return queryPropertyPageByProductId(productId, query, property); + } + + @Nonnull + @Override public Mono> queryPropertyPageByProductId(@Nonnull String productId, @Nonnull QueryParamEntity query, @Nonnull String... property) { return repository .opsForTemplate(thingType, productId) @@ -177,6 +216,7 @@ public class ThingsBridgingDeviceDataService implements DeviceDataService { .map(page -> convertPage(page,DeviceOperationLogEntity::of)); } + @Override public Flux queryDeviceMessageLogNoPaging(@Nonnull String deviceId, @Nonnull QueryParamEntity query) { return repository .opsForThing(thingType, deviceId) @@ -184,6 +224,7 @@ public class ThingsBridgingDeviceDataService implements DeviceDataService { .map(DeviceOperationLogEntity::of); } + @Override public Flux queryDeviceMessageLogNoPagingByProduct(@Nonnull String productId, @Nonnull QueryParamEntity query) { return repository .opsForTemplate(thingType, productId) @@ -210,6 +251,7 @@ public class ThingsBridgingDeviceDataService implements DeviceDataService { } @Nonnull + @Override public Mono> queryEventPageByProductId(@Nonnull String productId, @Nonnull String event, @Nonnull QueryParamEntity query, @@ -220,23 +262,18 @@ public class ThingsBridgingDeviceDataService implements DeviceDataService { .map(page ->convertPage(page,DeviceEvent::new)); } - - @Nonnull @Override - public Flux queryProperties(@Nonnull String deviceId, @Nonnull QueryParamEntity query) { - return repository - .opsForThing(thingType, deviceId) - .flatMapMany(opt -> opt.forQuery().unwrap(ColumnModeQueryOperations.class).queryAllProperties(query)) - .map(DeviceProperties::new); + public SaveOperations opsForSave() { + return repository.opsForSave(); } - @Nonnull @Override - public Mono> queryPropertiesPageByProduct(@Nonnull String productId, @Nonnull QueryParamEntity query) { - return repository - .opsForTemplate(thingType, productId) - .flatMap(opt -> opt.forQuery().unwrap(ColumnModeQueryOperations.class).queryAllPropertiesPage(query)) - .map(page -> convertPage(page,DeviceProperties::new)); + public Mono opsForDevice(String deviceId) { + return repository.opsForThing(thingType,deviceId); } + @Override + public Mono opsForProduct(String productId) { + return repository.opsForTemplate(thingType,productId); + } } diff --git a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/service/term/DeviceInstanceTerm.java b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/service/term/DeviceInstanceTerm.java index bee5e6d7..2c0be38c 100644 --- a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/service/term/DeviceInstanceTerm.java +++ b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/service/term/DeviceInstanceTerm.java @@ -53,7 +53,15 @@ public class DeviceInstanceTerm extends AbstractTermFragmentBuilder { if (term.getOptions().contains("not")) { sqlFragments.addSql("not"); } - sqlFragments.addSql("exists(select 1 from ", getTableName("dev_device_instance", column), " _dev where _dev.id = ", columnFullName); + if (term.getOptions().contains("productId")) { + // 根据产品ID关联 + sqlFragments + .addSql("exists(select 1 from ", getTableName("dev_device_instance", column), " _dev where _dev.product_id =", columnFullName); + } else { + // 根据设备ID关联 + sqlFragments + .addSql("exists(select 1 from ", getTableName("dev_device_instance", column), " _dev where _dev.id =", columnFullName); + } RDBTableMetadata metadata = column .getOwner() diff --git a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/service/term/DeviceLatestDataTerm.java b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/service/term/DeviceLatestDataTerm.java new file mode 100644 index 00000000..fb5d729d --- /dev/null +++ b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/service/term/DeviceLatestDataTerm.java @@ -0,0 +1,104 @@ +package org.jetlinks.community.device.service.term; + +import lombok.extern.slf4j.Slf4j; +import org.hswebframework.ezorm.core.param.Term; +import org.hswebframework.ezorm.rdb.metadata.RDBColumnMetadata; +import org.hswebframework.ezorm.rdb.metadata.RDBTableMetadata; +import org.hswebframework.ezorm.rdb.operator.builder.fragments.*; +import org.hswebframework.ezorm.rdb.operator.builder.fragments.term.AbstractTermFragmentBuilder; +import org.hswebframework.web.api.crud.entity.TermExpressionParser; +import org.jetlinks.community.device.service.data.DatabaseDeviceLatestDataService; +import org.springframework.stereotype.Component; + +import java.util.List; + +/** + * 根据设备最新的数据来查询设备或者与设备关联的数据,如: 查询温度大于30的设备列表. + *

+ * + * 注意: 查询时指定列名是和设备ID关联的列或者实体类属性名. + * 如: 查询设备列表时则使用id. + * 此条件仅支持关系型数据库中的查询. + * 要使用此功能,必须开启jetlinks.device.storage.latest.enabled=true + * + *

+ * 在通用查询接口中可以使用动态查询参数中的term.termType来使用此功能. + * 查看动态查询参数说明 + *

+ * 在内部通用条件中,可以使用DSL方式创建条件,例如: + *

+ *     createQuery()
+ *     .where()
+ *     .and("id","dev-latest$productId","temp > 10") //productId为具体的产品ID
+ *     .fetch()
+ * 
+ * + * @author zhouhao + * @since 1.6 + */ +@Slf4j +@Component +public class DeviceLatestDataTerm extends AbstractTermFragmentBuilder { + public DeviceLatestDataTerm() { + super("dev-latest", "按设备最新属性查询"); + } + + @Override + public SqlFragments createFragments(String columnFullName, RDBColumnMetadata column, Term term) { + PrepareSqlFragments sqlFragments = PrepareSqlFragments.of(); + + List options = term.getOptions(); + if (options.isEmpty()) { + log.warn("无法根据设备数据查询,请指定options,如: deviceId$dev-latest$demo-device"); + return sqlFragments.addSql("1=2"); + } + String tableName = DatabaseDeviceLatestDataService.getLatestTableTableName(options.get(0)); + + RDBTableMetadata metadata = column.getOwner().getSchema().getTable(tableName,false).orElse(null); + + if (metadata == null) { + log.warn("无法根据设备数据查询,不存在的表:{}", tableName); + return sqlFragments.addSql("1=2"); + } + + String value = String.valueOf(term.getValue()); + + List terms = TermExpressionParser.parse(value); + + SqlFragments fragments = builder.createTermFragments(metadata, terms); + + sqlFragments.addSql("exists(select 1 from ", metadata.getQuoteName(), "_tmp where _tmp.id =", columnFullName); + + if (fragments.isNotEmpty()) { + sqlFragments.addSql("and") + .addSql(fragments.getSql()) + .addParameter(fragments.getParameters()); + } + sqlFragments.addSql(")"); + + return sqlFragments; + } + + static WhereBuilder builder = new WhereBuilder(); + + static class WhereBuilder extends AbstractTermsFragmentBuilder { + + @Override + protected SqlFragments createTermFragments(RDBTableMetadata parameter, Term term) { + RDBColumnMetadata column = parameter.getColumn(term.getColumn()).orElse(null); + if (column == null) { + return EmptySqlFragments.INSTANCE; + } + return parameter + .findFeatureNow(TermFragmentBuilder.createFeatureId(term.getTermType())) + .createFragments(column.getFullName("_tmp"), column, term); + } + + @Override + protected SqlFragments createTermFragments(RDBTableMetadata parameter, List terms) { + return super.createTermFragments(parameter, terms); + } + } + + +} diff --git a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/service/term/DeviceTagTerm.java b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/service/term/DeviceTagTerm.java index b1dddd3f..6784d693 100644 --- a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/service/term/DeviceTagTerm.java +++ b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/service/term/DeviceTagTerm.java @@ -18,8 +18,29 @@ import java.util.Map; import java.util.stream.Collectors; import java.util.stream.Stream; + /** - * where("id$dev-tag$location","重庆") + * 根据设备标签查询设备或者与设备关联的数据. + *

+ * + * 注意: 查询时指定列名是和设备ID关联的列或者实体类属性名. + * 如: 查询设备列表时则使用id. + * 此条件仅支持关系型数据库中的查询. + * + *

+ * 在通用查询接口中可以使用动态查询参数中的term.termType来使用此功能. + * 查看动态查询参数说明 + *

+ * 在内部通用条件中,可以使用DSL方式创建条件,例如: + *

+ *     createQuery()
+ *     .where()
+ *     .and("id","dev-tag","tag1 = 1 or tag2 = 2")
+ *     .fetch()
+ * 
+ * + * @author zhouhao + * @since 1.6 */ @Component public class DeviceTagTerm extends AbstractTermFragmentBuilder { @@ -70,28 +91,44 @@ public class DeviceTagTerm extends AbstractTermFragmentBuilder { private void acceptTerm(boolean and, PrepareSqlFragments fragments, Collection tags) { + PrepareSqlFragments copy = PrepareSqlFragments.of(); + copy.addSql(fragments.getSql()); int len = 0; fragments.addSql("and ("); for (Object tag : tags) { - if (len++ > 0) { - fragments.addSql(and ? "and" : "or"); - } String key; String value; if (tag instanceof Map) { @SuppressWarnings("all") Map map = ((Map) tag); + if (map.get("type") != null) { + and = Term.Type.and.name().equals(map.get("type")); + } //key or column key = String.valueOf(map.getOrDefault("key", map.get("column"))); value = String.valueOf(map.get("value")); } else if (tag instanceof Term) { + Term.Type type = ((Term) tag).getType(); + if (type != null) { + and = Term.Type.and.equals(type); + } key = ((Term) tag).getColumn(); value = String.valueOf(((Term) tag).getValue()); } else { throw new IllegalArgumentException("illegal tag value format"); } - fragments.addSql("(d.key = ? and d.value like ?)") - .addParameter(key, value); + if (len++ > 0) { + // 组合多个exist语句:and (exists(...) and exist(...)) + fragments.addSql("))"); + fragments.addSql(and ? "and" : "or"); + fragments.add(copy); + fragments.addSql("and"); + fragments.addSql("(d.key = ? and d.value like ?)") + .addParameter(key, value); + } else { + fragments.addSql("(d.key = ? and d.value like ?)") + .addParameter(key, value); + } } if (tags.isEmpty()) { fragments.addSql("1=2"); @@ -104,6 +141,7 @@ public class DeviceTagTerm extends AbstractTermFragmentBuilder { PrepareSqlFragments fragments = PrepareSqlFragments.of(); + fragments.addSql("("); fragments.addSql("exists(select 1 from ",getTableName("dev_device_tags",column)," d where d.device_id =", columnFullName); Object value = term.getValue(); boolean and = term.getOptions().contains("and"); @@ -115,12 +153,13 @@ public class DeviceTagTerm extends AbstractTermFragmentBuilder { acceptTerm(and, column, fragments, String.valueOf(value)); } + fragments.addSql(")"); fragments.addSql(")"); return fragments; } - static DeviceTagTerm.WhereBuilder builder = new DeviceTagTerm.WhereBuilder(); + static WhereBuilder builder = new WhereBuilder(); static class WhereBuilder extends AbstractTermsFragmentBuilder { @@ -128,6 +167,7 @@ public class DeviceTagTerm extends AbstractTermFragmentBuilder { @Override protected SqlFragments createTermFragments(RDBColumnMetadata parameter, Term term) { + PrepareSqlFragments sqlFragments = PrepareSqlFragments.of(); sqlFragments.addSql("(d.key = ?") .addParameter(term.getColumn()) @@ -150,4 +190,4 @@ public class DeviceTagTerm extends AbstractTermFragmentBuilder { return super.createTermFragments(parameter, terms); } } -} \ No newline at end of file +} diff --git a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/service/term/DeviceTypeTerm.java b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/service/term/DeviceTypeTerm.java index 2b42a7e0..4dcf0790 100755 --- a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/service/term/DeviceTypeTerm.java +++ b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/service/term/DeviceTypeTerm.java @@ -25,9 +25,9 @@ public class DeviceTypeTerm extends AbstractTermFragmentBuilder { sqlFragments.addSql("not"); } sqlFragments - .addSql("exists(select 1 from ",getTableName("dev_product",column)," _product where _product.id = " + columnFullName); + .addSql("exists(select 1 from",getTableName("dev_product",column),"_product where _product.id =" , columnFullName); sqlFragments - .addSql(" and _product.device_type in("); + .addSql("and _product.device_type in("); sqlFragments.addSql(idList.stream().map(str -> "?").collect(Collectors.joining(","))) .addParameter(idList) .addSql("))"); 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 c62065e5..671ae3e8 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 @@ -2,6 +2,7 @@ package org.jetlinks.community.device.spi; import lombok.Generated; import org.jetlinks.core.metadata.ConfigMetadata; +import org.jetlinks.core.metadata.ConfigScope; import org.jetlinks.core.metadata.DeviceMetadataType; import org.jetlinks.core.metadata.Feature; import reactor.core.publisher.Flux; @@ -31,16 +32,6 @@ public interface DeviceConfigMetadataSupplier { */ Flux getProductConfigMetadata(String productId); - /** - * @see org.jetlinks.community.device.service.DeviceConfigMetadataManager#getMetadataExpandsConfig(String, DeviceMetadataType, String, String) - */ - default Flux getMetadataExpandsConfig(String productId, - DeviceMetadataType metadataType, - String metadataId, - String typeId) { - return Flux.empty(); - } - /** * @see org.jetlinks.community.device.service.DeviceConfigMetadataManager#getProductConfigMetadataByAccessId(String, String) */ @@ -49,6 +40,16 @@ public interface DeviceConfigMetadataSupplier { return Flux.empty(); } + /** + * @see org.jetlinks.community.device.service.DeviceConfigMetadataManager#getMetadataExpandsConfig(String, DeviceMetadataType, String, String, ConfigScope...) + */ + @Generated + default Flux getMetadataExpandsConfig(String productId, + DeviceMetadataType metadataType, + String metadataId, + String typeId) { + return Flux.empty(); + } /** * @see org.jetlinks.community.device.service.DeviceConfigMetadataManager#getProductFeatures(String) diff --git a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/timeseries/DeviceEventTimeSeriesMetadata.java b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/timeseries/DeviceEventTimeSeriesMetadata.java index 3cd6a672..4602f851 100644 --- a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/timeseries/DeviceEventTimeSeriesMetadata.java +++ b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/timeseries/DeviceEventTimeSeriesMetadata.java @@ -1,14 +1,15 @@ package org.jetlinks.community.device.timeseries; import org.apache.commons.collections.CollectionUtils; -import org.jetlinks.community.timeseries.TimeSeriesMetadata; -import org.jetlinks.community.timeseries.TimeSeriesMetric; import org.jetlinks.core.metadata.DataType; import org.jetlinks.core.metadata.EventMetadata; import org.jetlinks.core.metadata.PropertyMetadata; import org.jetlinks.core.metadata.SimplePropertyMetadata; +import org.jetlinks.core.metadata.types.DateTimeType; import org.jetlinks.core.metadata.types.ObjectType; import org.jetlinks.core.metadata.types.StringType; +import org.jetlinks.community.timeseries.TimeSeriesMetadata; +import org.jetlinks.community.timeseries.TimeSeriesMetric; import java.util.ArrayList; import java.util.List; @@ -20,28 +21,36 @@ class DeviceEventTimeSeriesMetadata implements TimeSeriesMetadata { private static final List defaultMetadata = new ArrayList<>(); static { + { + SimplePropertyMetadata property = new SimplePropertyMetadata(); + property.setId("id"); + property.setValueType(StringType.GLOBAL); + property.setName("id"); + defaultMetadata.add(property); + } { SimplePropertyMetadata property = new SimplePropertyMetadata(); property.setId("productId"); - property.setValueType(new StringType()); + property.setValueType(StringType.GLOBAL); property.setName("型号ID"); defaultMetadata.add(property); } { SimplePropertyMetadata property = new SimplePropertyMetadata(); - property.setId("orgId"); - property.setValueType(new StringType()); - property.setName("租户ID"); + property.setId("deviceId"); + property.setValueType(StringType.GLOBAL); + property.setName("设备ID"); defaultMetadata.add(property); } { SimplePropertyMetadata property = new SimplePropertyMetadata(); - property.setId("deviceId"); - property.setValueType(new StringType()); - property.setName("设备ID"); + property.setId("createTime"); + property.setValueType(DateTimeType.GLOBAL); + property.setName("创建时间"); defaultMetadata.add(property); } + } private final List metadata = new ArrayList<>(defaultMetadata); diff --git a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/timeseries/DeviceLogTimeSeriesMetadata.java b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/timeseries/DeviceLogTimeSeriesMetadata.java index 7f4bf11f..54705ba1 100644 --- a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/timeseries/DeviceLogTimeSeriesMetadata.java +++ b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/timeseries/DeviceLogTimeSeriesMetadata.java @@ -1,11 +1,12 @@ package org.jetlinks.community.device.timeseries; -import org.jetlinks.community.timeseries.TimeSeriesMetadata; -import org.jetlinks.community.timeseries.TimeSeriesMetric; import org.jetlinks.core.metadata.PropertyMetadata; import org.jetlinks.core.metadata.SimplePropertyMetadata; import org.jetlinks.core.metadata.types.DateTimeType; import org.jetlinks.core.metadata.types.StringType; +import org.jetlinks.community.ConfigMetadataConstants; +import org.jetlinks.community.timeseries.TimeSeriesMetadata; +import org.jetlinks.community.timeseries.TimeSeriesMetric; import java.util.ArrayList; import java.util.List; @@ -21,6 +22,7 @@ class DeviceLogTimeSeriesMetadata implements TimeSeriesMetadata { } static { + { SimplePropertyMetadata property = new SimplePropertyMetadata(); property.setId("id"); @@ -40,7 +42,9 @@ class DeviceLogTimeSeriesMetadata implements TimeSeriesMetadata { { SimplePropertyMetadata property = new SimplePropertyMetadata(); property.setId("content"); - property.setValueType(new StringType()); + property.setValueType(new StringType().expand( + ConfigMetadataConstants.maxLength, + Long.getLong("jetlinks.device.log.content.max-length",4096L))); property.setName("日志内容"); metadata.add(property); } @@ -48,7 +52,7 @@ class DeviceLogTimeSeriesMetadata implements TimeSeriesMetadata { { SimplePropertyMetadata property = new SimplePropertyMetadata(); property.setId("deviceId"); - property.setValueType(new StringType()); + property.setValueType(StringType.GLOBAL); property.setName("设备ID"); metadata.add(property); } @@ -56,20 +60,11 @@ class DeviceLogTimeSeriesMetadata implements TimeSeriesMetadata { { SimplePropertyMetadata property = new SimplePropertyMetadata(); property.setId("productId"); - property.setValueType(new StringType()); + property.setValueType(StringType.GLOBAL); property.setName("产品ID"); metadata.add(property); } - { - SimplePropertyMetadata property = new SimplePropertyMetadata(); - property.setId("orgId"); - property.setValueType(new StringType()); - property.setName("组织ID"); - metadata.add(property); - } - - { SimplePropertyMetadata property = new SimplePropertyMetadata(); property.setId("messageId"); @@ -77,13 +72,15 @@ class DeviceLogTimeSeriesMetadata implements TimeSeriesMetadata { property.setName("消息ID"); metadata.add(property); } + { SimplePropertyMetadata property = new SimplePropertyMetadata(); property.setId("createTime"); - property.setValueType(new DateTimeType()); - property.setName("创建事件"); + property.setValueType(DateTimeType.GLOBAL); + property.setName("创建时间"); metadata.add(property); } + { SimplePropertyMetadata property = new SimplePropertyMetadata(); property.setId("timestamp"); @@ -91,6 +88,7 @@ class DeviceLogTimeSeriesMetadata implements TimeSeriesMetadata { property.setName("数据时间"); metadata.add(property); } + } @Override diff --git a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/timeseries/DevicePropertiesTimeSeriesMetadata.java b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/timeseries/DevicePropertiesTimeSeriesMetadata.java index f515e21d..3018f47d 100644 --- a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/timeseries/DevicePropertiesTimeSeriesMetadata.java +++ b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/timeseries/DevicePropertiesTimeSeriesMetadata.java @@ -1,13 +1,10 @@ package org.jetlinks.community.device.timeseries; -import org.jetlinks.community.timeseries.TimeSeriesMetadata; -import org.jetlinks.community.timeseries.TimeSeriesMetric; import org.jetlinks.core.metadata.PropertyMetadata; import org.jetlinks.core.metadata.SimplePropertyMetadata; -import org.jetlinks.core.metadata.types.DateTimeType; -import org.jetlinks.core.metadata.types.DoubleType; -import org.jetlinks.core.metadata.types.ObjectType; -import org.jetlinks.core.metadata.types.StringType; +import org.jetlinks.core.metadata.types.*; +import org.jetlinks.community.timeseries.TimeSeriesMetadata; +import org.jetlinks.community.timeseries.TimeSeriesMetric; import java.util.ArrayList; import java.util.List; @@ -23,7 +20,13 @@ class DevicePropertiesTimeSeriesMetadata implements TimeSeriesMetadata { } static { - + { + SimplePropertyMetadata property = new SimplePropertyMetadata(); + property.setId("id"); + property.setValueType(StringType.GLOBAL); + property.setName("id"); + metadata.add(property); + } { SimplePropertyMetadata property = new SimplePropertyMetadata(); property.setId("property"); @@ -46,6 +49,14 @@ class DevicePropertiesTimeSeriesMetadata implements TimeSeriesMetadata { metadata.add(property); } + { + SimplePropertyMetadata property = new SimplePropertyMetadata(); + property.setId("geoValue"); + property.setValueType(new GeoType()); + property.setName("地理位置"); + metadata.add(property); + } + { SimplePropertyMetadata property = new SimplePropertyMetadata(); property.setId("objectValue"); @@ -84,28 +95,28 @@ class DevicePropertiesTimeSeriesMetadata implements TimeSeriesMetadata { property.setName("设备ID"); metadata.add(property); } +// +// { +// SimplePropertyMetadata property = new SimplePropertyMetadata(); +// property.setId("productId"); +// property.setValueType(new StringType()); +// property.setName("产品ID"); +// metadata.add(property); +// } { SimplePropertyMetadata property = new SimplePropertyMetadata(); - property.setId("productId"); + property.setId("type"); property.setValueType(new StringType()); - property.setName("型号ID"); - metadata.add(property); - } - - { - SimplePropertyMetadata property = new SimplePropertyMetadata(); - property.setId("orgId"); - property.setValueType(new StringType()); - property.setName("组织ID"); + property.setName("类型"); metadata.add(property); } { SimplePropertyMetadata property = new SimplePropertyMetadata(); property.setId("createTime"); - property.setValueType(new DateTimeType()); - property.setName("创建事件"); + property.setValueType(DateTimeType.GLOBAL); + property.setName("创建时间"); metadata.add(property); } } diff --git a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/timeseries/DeviceTimeSeriesMetadata.java b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/timeseries/DeviceTimeSeriesMetadata.java index 3fa40928..0d563135 100644 --- a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/timeseries/DeviceTimeSeriesMetadata.java +++ b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/timeseries/DeviceTimeSeriesMetadata.java @@ -1,8 +1,8 @@ package org.jetlinks.community.device.timeseries; -import org.jetlinks.community.timeseries.TimeSeriesMetadata; import org.jetlinks.core.metadata.EventMetadata; import org.jetlinks.core.metadata.PropertyMetadata; +import org.jetlinks.community.timeseries.TimeSeriesMetadata; import java.util.List; @@ -27,6 +27,8 @@ public interface DeviceTimeSeriesMetadata { static TimeSeriesMetadata properties(String productId, List properties) { return new FixedPropertiesTimeSeriesMetadata(productId, properties); } + + /** * 获取设备事件时序数据元数据 * diff --git a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/timeseries/DeviceTimeSeriesMetric.java b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/timeseries/DeviceTimeSeriesMetric.java index bfba5ac2..b129da4e 100644 --- a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/timeseries/DeviceTimeSeriesMetric.java +++ b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/timeseries/DeviceTimeSeriesMetric.java @@ -1,8 +1,8 @@ package org.jetlinks.community.device.timeseries; -import org.jetlinks.community.timeseries.TimeSeriesMetric; import org.jetlinks.core.device.DeviceProductOperator; import org.jetlinks.core.metadata.EventMetadata; +import org.jetlinks.community.timeseries.TimeSeriesMetric; /** * 设备时序数据度量标识 @@ -26,13 +26,6 @@ public interface DeviceTimeSeriesMetric { return TimeSeriesMetric.of(deviceEventMetricId(productId, eventId)); } - /** - * 构建事件指标ID - * - * @param productId 产品ID - * @param eventId 事件ID - * @return 事件指标ID - */ static String deviceEventMetricId(String productId, String eventId) { return "event_".concat(productId).concat("_").concat(eventId); } diff --git a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/timeseries/FixedPropertiesTimeSeriesMetadata.java b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/timeseries/FixedPropertiesTimeSeriesMetadata.java index 67e92d90..9ed1de12 100644 --- a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/timeseries/FixedPropertiesTimeSeriesMetadata.java +++ b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/timeseries/FixedPropertiesTimeSeriesMetadata.java @@ -1,11 +1,12 @@ package org.jetlinks.community.device.timeseries; -import org.jetlinks.community.timeseries.TimeSeriesMetadata; -import org.jetlinks.community.timeseries.TimeSeriesMetric; import org.jetlinks.core.metadata.PropertyMetadata; import org.jetlinks.core.metadata.SimplePropertyMetadata; import org.jetlinks.core.metadata.types.DateTimeType; import org.jetlinks.core.metadata.types.StringType; +import org.jetlinks.community.device.service.data.StorageConstants; +import org.jetlinks.community.timeseries.TimeSeriesMetadata; +import org.jetlinks.community.timeseries.TimeSeriesMetric; import java.util.ArrayList; import java.util.List; @@ -20,7 +21,18 @@ class FixedPropertiesTimeSeriesMetadata implements TimeSeriesMetadata { public FixedPropertiesTimeSeriesMetadata(String productId, List fixed) { this.metric = DeviceTimeSeriesMetric.devicePropertyMetric(productId); - this.fixed = new ArrayList<>(fixed); + this.fixed = new ArrayList<>(fixed.size() + metadata.size()); + for (PropertyMetadata propertyMetadata : fixed) { + SimplePropertyMetadata property = new SimplePropertyMetadata(); + property.setId(propertyMetadata.getId()); + property.setName(propertyMetadata.getName()); + if (StorageConstants.propertyIsJsonStringStorage(propertyMetadata)) { + property.setValueType(StringType.GLOBAL); + } else { + property.setValueType(propertyMetadata.getValueType()); + } + this.fixed.add(property); + } this.fixed.addAll(metadata); } @@ -42,13 +54,13 @@ class FixedPropertiesTimeSeriesMetadata implements TimeSeriesMetadata { metadata.add(property); } - { - SimplePropertyMetadata property = new SimplePropertyMetadata(); - property.setId("productId"); - property.setValueType(new StringType()); - property.setName("产品ID"); - metadata.add(property); - } +// { +// SimplePropertyMetadata property = new SimplePropertyMetadata(); +// property.setId("productId"); +// property.setValueType(new StringType()); +// property.setName("产品ID"); +// metadata.add(property); +// } { SimplePropertyMetadata property = new SimplePropertyMetadata(); diff --git a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/utils/DeviceCacheUtils.java b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/utils/DeviceCacheUtils.java new file mode 100644 index 00000000..7f3a51cd --- /dev/null +++ b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/utils/DeviceCacheUtils.java @@ -0,0 +1,62 @@ +package org.jetlinks.community.device.utils; + +import com.github.benmanes.caffeine.cache.Caffeine; +import org.jetlinks.community.device.entity.DeviceInstanceEntity; +import org.jetlinks.community.device.entity.DeviceProductEntity; +import reactor.core.publisher.Mono; + +import java.time.Duration; +import java.util.Map; +import java.util.Objects; +import java.util.function.Function; + +public class DeviceCacheUtils { + + private final static Map> productCache = Caffeine + .newBuilder() + .expireAfterAccess(Duration.ofSeconds(30)) + .>build() + .asMap(); + + private final static Map> deviceCache = Caffeine + .newBuilder() + .expireAfterAccess(Duration.ofSeconds(2)) + .>build() + .asMap(); + + + public static Mono getProductOrLoad(String productId, + Function> loader) { + return Mono.defer(() -> productCache.computeIfAbsent( + productId, + k -> loader + .apply(k) + .cache(val -> Duration.ofSeconds(10), + error -> Duration.ZERO, + () -> Duration.ofSeconds(5)))); + } + + public static void addDeviceCache(DeviceInstanceEntity entity) { + deviceCache.putIfAbsent(entity.getId(), Mono.just(entity)); + } + + public static Mono getDeviceOrLoad(String deviceId, + Function> loader) { + return Mono.deferContextual(ctx -> { + DeviceInstanceEntity inContext = ctx + .getOrEmpty(DeviceInstanceEntity.class).orElse(null); + + if (inContext != null && Objects.equals(inContext.getId(), deviceId)) { + return Mono.just(inContext); + } + return deviceCache.computeIfAbsent( + deviceId, + k -> loader + .apply(k) + .cache(val -> Duration.ofSeconds(1), + error -> Duration.ZERO, + () -> Duration.ofSeconds(1))); + }); + } + +} diff --git a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/web/DeviceCategoryController.java b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/web/DeviceCategoryController.java index 4a7b9b49..5fe85b07 100644 --- a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/web/DeviceCategoryController.java +++ b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/web/DeviceCategoryController.java @@ -1,6 +1,5 @@ package org.jetlinks.community.device.web; - import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.AllArgsConstructor; @@ -59,10 +58,10 @@ public class DeviceCategoryController implements ReactiveServiceCrudController getAllCategoryTreeByQueryParam(@RequestBody Mono query) { return this - .categoryService - .query(query) - .collectList() - .flatMapMany(all-> Flux.fromIterable(TreeSupportEntity.list2tree(all, DeviceCategoryEntity::setChildren))); + .categoryService + .query(query) + .collectList() + .flatMapMany(all-> Flux.fromIterable(TreeSupportEntity.list2tree(all, DeviceCategoryEntity::setChildren))); } @Override 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 b8e14f2f..0f63fea1 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 @@ -19,6 +19,7 @@ import org.hswebframework.web.authorization.Authentication; import org.hswebframework.web.authorization.Dimension; import org.hswebframework.web.authorization.annotation.*; import org.hswebframework.web.bean.FastBeanCopier; +import org.hswebframework.web.crud.query.QueryHelper; import org.hswebframework.web.crud.web.reactive.ReactiveServiceCrudController; import org.hswebframework.web.exception.BusinessException; import org.hswebframework.web.exception.NotFoundException; @@ -28,10 +29,6 @@ import org.hswebframework.web.id.IDGenerator; import org.jetlinks.community.PropertyMetric; import org.jetlinks.community.device.entity.*; 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.device.response.ImportDeviceInstanceResult; -import org.jetlinks.community.device.response.ResetDeviceConfigurationResult; import org.jetlinks.community.device.service.DeviceConfigMetadataManager; import org.jetlinks.community.device.service.LocalDeviceInstanceService; import org.jetlinks.community.device.service.LocalDeviceProductService; @@ -39,6 +36,8 @@ import org.jetlinks.community.device.service.data.DeviceDataService; import org.jetlinks.community.device.service.data.DeviceProperties; import org.jetlinks.community.device.web.excel.*; import org.jetlinks.community.device.web.request.AggRequest; +import org.jetlinks.community.device.web.response.DeviceDeployResult; +import org.jetlinks.community.device.web.response.ImportDeviceInstanceResult; import org.jetlinks.community.io.excel.AbstractImporter; import org.jetlinks.community.io.excel.ImportExportService; import org.jetlinks.community.io.file.FileManager; @@ -60,7 +59,6 @@ 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.DataBuffer; import org.springframework.core.io.buffer.DataBufferFactory; import org.springframework.core.io.buffer.DataBufferUtils; import org.springframework.core.io.buffer.DefaultDataBufferFactory; @@ -129,6 +127,8 @@ public class DeviceInstanceController implements private final DefaultPropertyMetricManager metricManager; + private final QueryHelper queryHelper; + @SuppressWarnings("all") public DeviceInstanceController(LocalDeviceInstanceService service, DeviceRegistry registry, @@ -142,7 +142,8 @@ public class DeviceInstanceController implements FileManager fileManager, WebClient.Builder builder, DeviceExcelFilterColumns filterColumns, - DefaultPropertyMetricManager metricManager) { + DefaultPropertyMetricManager metricManager, + QueryHelper queryHelper) { this.service = service; this.registry = registry; this.productService = productService; @@ -156,6 +157,7 @@ public class DeviceInstanceController implements this.webClient = builder.build(); this.filterColumns = filterColumns; this.metricManager = metricManager; + this.queryHelper = queryHelper; } @@ -222,7 +224,7 @@ public class DeviceInstanceController implements @PostMapping("/{deviceId:.+}/deploy") @SaveAction @Operation(summary = "激活指定ID设备") - public Mono deviceDeploy(@PathVariable @Parameter(description = "设备ID") String deviceId) { + public Mono deviceDeploy(@PathVariable @Parameter(description = "设备ID") String deviceId) { return service.deploy(deviceId); } @@ -234,22 +236,6 @@ public class DeviceInstanceController implements return service.resetConfiguration(deviceId); } - @PutMapping("/configuration/_reset/ids") - @SaveAction - @Operation(summary = "重置设备配置信息(根据设备ID批量重置,性能欠佳,慎用)") - public Mono resetConfigurationBatch(@RequestBody Flux payload) { - return service.resetConfiguration(payload); - } - - @GetMapping(value = "/configuration/_reset/{productId:.+}", produces = MediaType.TEXT_EVENT_STREAM_VALUE) - @ResourceAction( - id = "batchResetConf", - name = "批量重置设备配置信息" - ) - @Operation(summary = "重置设备配置信息(根据产品批量重置,性能欠佳,慎用)") - public Flux resetConfigurationBatch(@PathVariable @Parameter(description = "产品ID") String productId) { - return service.resetConfigurationByProductId(productId); - } //批量激活设备 @GetMapping(value = "/deploy", produces = MediaType.TEXT_EVENT_STREAM_VALUE) @@ -518,6 +504,16 @@ public class DeviceInstanceController implements .fetch(); } + @GetMapping("/tags/key") + @QueryAction + @Operation(summary = "获取全部标签key") + public Flux getTagKeys() { + return queryHelper + .select("select distinct key, name from dev_device_tags", DeviceTagEntity::new) + .fetch() + .distinct(DeviceTagEntity::getKey); + } + //保存设备标签 @PatchMapping("/{deviceId}/tag") @SaveAction @@ -582,7 +578,6 @@ public class DeviceInstanceController implements .map(info -> { DeviceInstanceEntity entity = FastBeanCopier.copy(info, new DeviceInstanceEntity()); entity.setProductId(productId); - entity.setOrgId(orgMapping.get(info.getOrgName())); if (StringUtils.isEmpty(entity.getId())) { throw new BusinessException("第" + (info.getRowNumber() + 1) + "行:设备ID不能为空"); } @@ -605,7 +600,7 @@ public class DeviceInstanceController implements .currentReactive() .flatMapMany(auth -> this .getDeviceProductDetail(productId) - .map(tp4 -> new DeviceExcelImporter(fileManager, webClient, tp4.getT1(), tp4.getT4(), auth)) + .map(tp4 -> new DeviceExcelImporter(fileManager, webClient, tp4.getT1(), tp4.getT4(), service, auth)) .flatMapMany(importer -> importer .doImport(fileUrl) .groupBy( @@ -636,8 +631,8 @@ public class DeviceInstanceController implements } private Flux handleImportDevice(Flux>> flux, - boolean autoDeploy, - int speed) { + boolean autoDeploy, + int speed) { return flux .buffer(100)//每100条数据保存一次 .map(Flux::fromIterable) @@ -730,7 +725,6 @@ public class DeviceInstanceController implements .query(parameter) .flatMap(entity -> { DeviceExcelInfo exportEntity = FastBeanCopier.copy(entity, new DeviceExcelInfo(), "state"); - exportEntity.setOrgName(orgMapping.get(entity.getOrgId())); exportEntity.setState(entity.getState().getText()); return registry .getDevice(entity.getId()) diff --git a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/web/DeviceMessageController.java b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/web/DeviceMessageController.java index 32fd4799..9d3f5a4b 100644 --- a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/web/DeviceMessageController.java +++ b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/web/DeviceMessageController.java @@ -1,35 +1,20 @@ package org.jetlinks.community.device.web; import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.Generated; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; 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.exception.BusinessException; -import org.hswebframework.web.id.IDGenerator; -import org.jetlinks.community.device.entity.DevicePropertiesEntity; -import org.jetlinks.community.utils.ErrorUtils; -import org.jetlinks.core.device.DeviceOperator; -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.function.FunctionInvokeMessageReply; -import org.jetlinks.core.message.property.ReadPropertyMessageReply; -import org.jetlinks.core.message.property.WritePropertyMessageReply; -import org.jetlinks.core.metadata.PropertyMetadata; -import org.jetlinks.core.metadata.types.StringType; +import org.jetlinks.community.device.entity.DeviceProperty; +import org.jetlinks.community.device.service.LocalDeviceInstanceService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; -import java.util.List; import java.util.Map; -import java.util.function.Function; @RestController @RequestMapping("/device") @@ -38,127 +23,71 @@ import java.util.function.Function; @Resource(id = "device-instance", name = "设备实例") @Tag(name = "设备指令API") @Deprecated +@Generated public class DeviceMessageController { @Autowired - private DeviceRegistry registry; + public LocalDeviceInstanceService instanceService; //获取设备属性 @GetMapping("/{deviceId}/property/{property:.+}") @SneakyThrows + @QueryAction @Deprecated + @Generated public Flux getProperty(@PathVariable String deviceId, @PathVariable String property) { - - return registry - .getDevice(deviceId) - .switchIfEmpty(ErrorUtils.notFound("设备不存在")) - .map(DeviceOperator::messageSender)//发送消息到设备 - .map(sender -> sender.readProperty(property).messageId(IDGenerator.SNOW_FLAKE_STRING.generate())) - .flatMapMany(ReadPropertyMessageSender::send) - .map(mapReply(ReadPropertyMessageReply::getProperties)); + return instanceService + .readProperty(deviceId, property) + .flux(); } //获取标准设备属性 @GetMapping("/standard/{deviceId}/property/{property:.+}") @SneakyThrows + @QueryAction @Deprecated - public Mono getStandardProperty(@PathVariable String deviceId, @PathVariable String property) { - return Mono.from(registry - .getDevice(deviceId) - .switchIfEmpty(ErrorUtils.notFound("设备不存在")) - .flatMapMany(deviceOperator -> deviceOperator.messageSender() - .readProperty(property).messageId(IDGenerator.SNOW_FLAKE_STRING.generate()) - .send() - .map(mapReply(ReadPropertyMessageReply::getProperties)) - .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)); - }))) - ; + @Generated + public Mono getStandardProperty(@PathVariable String deviceId, @PathVariable String property) { + return instanceService.readAndConvertProperty(deviceId, property); } //设置设备属性 @PostMapping("/setting/{deviceId}/property") @SneakyThrows + @QueryAction @Deprecated - public Flux settingProperties(@PathVariable String deviceId, @RequestBody Map properties) { - - return registry - .getDevice(deviceId) - .switchIfEmpty(ErrorUtils.notFound("设备不存在")) - .map(operator -> operator - .messageSender() - .writeProperty() - .messageId(IDGenerator.SNOW_FLAKE_STRING.generate()) - .write(properties) - ) - .flatMapMany(WritePropertyMessageSender::send) - .map(mapReply(WritePropertyMessageReply::getProperties)); + @Generated + public Flux writeProperties(@PathVariable String deviceId, @RequestBody Mono> properties) { + return properties.flatMapMany(props -> instanceService.writeProperties(deviceId, props)); } //设备功能调用 - @PostMapping("invoked/{deviceId}/function/{functionId}") + @PostMapping("/invoked/{deviceId}/function/{functionId}") @SneakyThrows + @QueryAction @Deprecated + @Generated public Flux invokedFunction(@PathVariable String deviceId, @PathVariable String functionId, - @RequestBody Map properties) { + @RequestBody Mono> properties) { + + return properties.flatMapMany(props -> instanceService.invokeFunction(deviceId, functionId, props)); + - return registry - .getDevice(deviceId) - .switchIfEmpty(ErrorUtils.notFound("设备不存在")) - .flatMap(operator -> operator - .messageSender() - .invokeFunction(functionId) - .messageId(IDGenerator.SNOW_FLAKE_STRING.generate()) - .setParameter(properties) - .validate() - ) - .flatMapMany(FunctionInvokeMessageSender::send) - .map(mapReply(FunctionInvokeMessageReply::getOutput)); } //获取设备所有属性 @PostMapping("/{deviceId}/properties") @SneakyThrows + @QueryAction @Deprecated + @Generated public Flux getProperties(@PathVariable String deviceId, - @RequestBody Mono> properties) { + @RequestBody Flux properties) { - return registry.getDevice(deviceId) - .switchIfEmpty(ErrorUtils.notFound("设备不存在")) - .map(DeviceOperator::messageSender) - .flatMapMany((sender) -> - properties.flatMapMany(list -> - sender.readProperty(list.toArray(new String[0])) - .messageId(IDGenerator.SNOW_FLAKE_STRING.generate()) - .send())) - .map(mapReply(ReadPropertyMessageReply::getProperties)); + return properties.collectList().flatMapMany(list -> instanceService.readProperties(deviceId, list)); } - 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()) { - throw new BusinessException(reply.getMessage(), reply.getCode()); - } - T mapped = function.apply(reply); - if (mapped == null) { - throw new BusinessException(reply.getMessage(), reply.getCode()); - } - return mapped; - }; - } } diff --git a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/web/DeviceMetadataMappingController.java b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/web/DeviceMetadataMappingController.java new file mode 100644 index 00000000..f4dc45a7 --- /dev/null +++ b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/web/DeviceMetadataMappingController.java @@ -0,0 +1,59 @@ +package org.jetlinks.community.device.web; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.AllArgsConstructor; +import org.hswebframework.web.authorization.annotation.QueryAction; +import org.hswebframework.web.authorization.annotation.Resource; +import org.hswebframework.web.authorization.annotation.SaveAction; +import org.jetlinks.community.device.entity.DeviceMetadataMappingDetail; +import org.jetlinks.community.device.entity.DeviceMetadataMappingEntity; +import org.jetlinks.community.device.service.DeviceMetadataMappingService; +import org.springframework.web.bind.annotation.*; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + + +@RestController +@RequestMapping("/device/metadata/mapping") +@Resource(id = "device-mapping", name = "设备物模型映射") +@Tag(name = "设备物模型映射") +@AllArgsConstructor +public class DeviceMetadataMappingController { + + public final DeviceMetadataMappingService mappingService; + + @PatchMapping("/device/{deviceId}") + @SaveAction + @Operation(summary = "保存设备映射信息") + public Mono saveDeviceMapping(@PathVariable @Parameter(description = "设备ID") String deviceId, + @RequestBody Flux mappings) { + return mappingService.saveDeviceMapping(deviceId, mappings); + } + + @PatchMapping("/product/{productId}") + @SaveAction + @Operation(summary = "保存产品映射信息") + public Mono saveProductMapping(@PathVariable @Parameter(description = "产品ID") String productId, + @RequestBody Flux mappings) { + return mappingService.saveProductMapping(productId, mappings); + } + + @GetMapping("/product/{productId}") + @QueryAction + @Operation(summary = "获取产品映射信息") + public Flux getProductMapping(@PathVariable + @Parameter(description = "产品ID") String productId) { + return mappingService.getProductMappingDetail(productId); + } + + @GetMapping("/device/{deviceId}") + @QueryAction + @Operation(summary = "获取设备映射信息") + public Flux getDeviceMapping(@PathVariable + @Parameter(description = "设备ID") String deviceId) { + return mappingService.getDeviceMappingDetail(deviceId); + } + +} 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 c6836f6b..fb36b4e9 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 @@ -1,5 +1,7 @@ package org.jetlinks.community.device.web; +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; @@ -8,11 +10,11 @@ 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.apache.commons.collections.CollectionUtils; import org.hswebframework.web.api.crud.entity.PagerResult; import org.hswebframework.web.api.crud.entity.QueryOperation; 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.authorization.annotation.SaveAction; @@ -20,72 +22,80 @@ 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.entity.ProductDetail; import org.jetlinks.community.device.service.DeviceConfigMetadataManager; +import org.jetlinks.community.device.service.LocalDeviceInstanceService; 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.community.device.service.data.DeviceProperties; 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.ExcelUtils; import org.jetlinks.community.io.excel.ImportExportService; import org.jetlinks.community.io.utils.FileUtils; +import org.jetlinks.community.things.data.ThingsDataRepositoryStrategies; import org.jetlinks.community.things.data.ThingsDataRepositoryStrategy; import org.jetlinks.community.timeseries.query.AggregationData; 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.ContentDisposition; 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"}) +@RequestMapping({"/device-product", "/device/product"}) @Resource(id = "device-product", name = "设备产品") -@Tag(name = "设备产品接口") +@Authorize +@Tag(name = "产品接口") @Slf4j public class DeviceProductController implements ReactiveServiceCrudController { private final LocalDeviceProductService productService; - private final List policies; private final DeviceDataService deviceDataService; + private final DeviceLatestDataService latestDataService; + private final DeviceConfigMetadataManager configMetadataManager; private final ObjectProvider metadataCodecs; private final DeviceMetadataCodec defaultCodec = new JetLinksDeviceMetadataCodec(); - - private final DataBufferFactory bufferFactory = new DefaultDataBufferFactory(); - private final ImportExportService importExportService; - public DeviceProductController(LocalDeviceProductService productService, - List policies, + private final LocalDeviceInstanceService deviceInstanceService; + + + private final String termName = "deviceId"; + + public DeviceProductController(DeviceLatestDataService latestDataService, + LocalDeviceProductService productService, DeviceDataService deviceDataService, DeviceConfigMetadataManager configMetadataManager, ObjectProvider metadataCodecs, - ImportExportService importExportService) { + ImportExportService importExportService, + LocalDeviceInstanceService deviceInstanceService + ) { + this.latestDataService = latestDataService; this.productService = productService; - this.policies = policies; this.deviceDataService = deviceDataService; this.configMetadataManager = configMetadataManager; this.metadataCodecs = metadataCodecs; this.importExportService = importExportService; + this.deviceInstanceService = deviceInstanceService; } @Override @@ -110,7 +120,6 @@ public class DeviceProductController implements ReactiveServiceCrudController convertMetadataTo(@RequestBody Mono metadata, - @PathVariable String id) { + public Mono convertMetadataTo(@RequestBody Mono metadata, + @PathVariable String id) { return metadata .flatMap(str -> Flux @@ -141,14 +151,15 @@ public class DeviceProductController implements ReactiveServiceCrudController defaultCodec .decode(str) - .flatMap(codec::encode))); + .flatMap(codec::encode)) + .map(JSON::parseObject)); } @PostMapping("/metadata/convert-from/{id}") @QueryAction @Operation(summary = "转换指定的物模型为平台的物模型格式") - public Mono convertMetadataFrom(@RequestBody Mono metadata, - @PathVariable String id) { + public Mono convertMetadataFrom(@RequestBody Mono metadata, + @PathVariable String id) { return metadata .flatMap(str -> Flux @@ -157,13 +168,54 @@ public class DeviceProductController implements ReactiveServiceCrudController codec .decode(str) - .flatMap(defaultCodec::encode))); + .flatMap(defaultCodec::encode)) + .map(JSON::parseObject)); + } + + @GetMapping("/{id:.+}/exists") + @QueryAction + @Operation(summary = "验证产品ID是否存在") + public Mono 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) { + DeviceProductEntity entity = new DeviceProductEntity(); + entity.setId(id); + entity.validateId(); + + return productService + .findById(id) + .flatMap(product -> LocaleUtils.resolveMessageReactive("error.product_ID_already_exists")) + .map(ValidationResult::error) + .defaultIfEmpty(ValidationResult.success()) + .onErrorResume(ValidationException.class, e -> Mono.just(e.getI18nCode()) + .map(ValidationResult::error)); + } + + @PostMapping("/detail/_query") + @QueryAction + @Operation(summary = "分页查询产品详情") + public Mono> queryProductDetail(@RequestBody Mono query) { + return query.flatMap(productService::queryProductDetail); + } + + @PostMapping("/detail/_query/no-paging") + @QueryAction + @Operation(summary = "查询产品详情列表") + public Flux queryProductDetailList(@RequestBody Mono query) { + return query.flatMapMany(productService::queryProductDetailList); } @PostMapping("/{productId:.+}/deploy") @SaveAction @Operation(summary = "激活产品") - public Mono deviceDeploy(@PathVariable @Parameter(description = "产品ID") String productId) { + public Mono deviceDeploy(@PathVariable + @Parameter(description = "产品ID") String productId) { return productService.deploy(productId); } @@ -177,8 +229,8 @@ public class DeviceProductController implements ReactiveServiceCrudController storePolicy() { - return Flux.fromIterable(policies) - .map(DeviceDataStorePolicyInfo::of); + return Flux.fromIterable(ThingsDataRepositoryStrategies.getAll()) + .map(DeviceDataStorePolicyInfo::of); } @PostMapping("/{productId:.+}/agg/_query") @@ -191,8 +243,10 @@ public class DeviceProductController implements ReactiveServiceCrudController deviceDataService .aggregationPropertiesByProduct(productId, - request.getQuery(), - request.getColumns().toArray(new DeviceDataService.DevicePropertyAggregation[0])) + request.getQuery(), + request + .getColumns() + .toArray(new DeviceDataService.DevicePropertyAggregation[0])) ) .map(AggregationData::values); } @@ -221,61 +275,43 @@ public class DeviceProductController implements ReactiveServiceCrudController deviceIdValidate(@PathVariable @Parameter(description = "产品ID") String id) { - return productService.findById(id) - .hasElement(); + @PostMapping("/{productId}/metadata/merge-to-device") + @SaveAction + @Operation(summary = "合并物模型到产品下的所有设备") + public Mono mergeMetadataToDevice(@PathVariable @Parameter(description = "产品ID") String productId) { + return productService.mergeMetadataToDevice(productId); } - @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 = "下载产品物模型属性导入模块") + @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()))); - + response.getHeaders() + .setContentDisposition( + ContentDisposition + .attachment() + .filename("物模型导入模板." + format, StandardCharsets.UTF_8) + .build() + ); 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())) + .flatMapMany(headers -> ExcelUtils.write(headers, PropertyMetadataExcelInfo.getTemplateContentMapping(), format)) .doOnError(err -> log.error(err.getMessage(), err)) - .map(bufferFactory::wrap) .as(response::writeWith) ; } @@ -304,4 +340,4 @@ public class DeviceProductController implements ReactiveServiceCrudController GatewayDeviceInfo.of(mapping.get(parentId), children)); }) - //收集所有有子设备的网关设备信息 - .collectMap(GatewayDeviceInfo::getId) + .collectMap(GatewayDeviceInfo::getId)//收集所有有子设备的网关设备信息 .defaultIfEmpty(Collections.emptyMap()) .flatMapMany(map -> Flux .fromIterable(mapping.values()) @@ -116,12 +116,13 @@ public class GatewayDeviceController { public Mono getGatewayInfo(@PathVariable String id) { return Mono.zip( instanceService.findById(id), - instanceService.createQuery() - .where() - .is(DeviceInstanceEntity::getParentId, id) - .fetch() - .collectList() - .defaultIfEmpty(Collections.emptyList()), + instanceService + .createQuery() + .where() + .is(DeviceInstanceEntity::getParentId, id) + .fetch() + .collectList() + .defaultIfEmpty(Collections.emptyList()), GatewayDeviceInfo::of); } @@ -129,6 +130,7 @@ public class GatewayDeviceController { @PostMapping("/{gatewayId}/bind/{deviceId}") @SaveAction @QueryOperation(summary = "绑定单个子设备到网关设备") + @Transactional public Mono bindDevice(@PathVariable @Parameter(description = "网关设备ID") String gatewayId, @PathVariable @Parameter(description = "子设备ID") String deviceId) { return instanceService @@ -139,13 +141,10 @@ public class GatewayDeviceController { .set(DeviceInstanceEntity::getParentId, gatewayId) .where(DeviceInstanceEntity::getId, deviceId) .execute() - .then(registry.getDevice(gatewayId) - .flatMap(gwOperator -> gwOperator.getProtocol() - .flatMap(protocolSupport -> protocolSupport.onChildBind(gwOperator, - Flux.from(registry.getDevice(deviceId))) - ) - ) - ) + ) + .then( + //触发绑定 + handleBindUnbind(gatewayId, Flux.just(deviceId), ProtocolSupport::onChildBind) ) .then(getGatewayInfo(gatewayId)); } @@ -153,6 +152,7 @@ public class GatewayDeviceController { @PostMapping("/{gatewayId}/bind") @SaveAction @QueryOperation(summary = "绑定多个子设备到网关设备") + @Transactional public Mono bindDevice(@PathVariable @Parameter(description = "网关设备ID") String gatewayId, @RequestBody @Parameter(description = "子设备ID集合") Mono> deviceId) { @@ -182,7 +182,8 @@ public class GatewayDeviceController { @PostMapping("/{gatewayId}/unbind/{deviceId}") @SaveAction - @QueryOperation(summary = "从网关设备中解绑子设备") + @QueryOperation(summary = "从网关设备中解绑单个子设备") + @Transactional public Mono unBindDevice(@PathVariable @Parameter(description = "网关设备ID") String gatewayId, @PathVariable @Parameter(description = "自设备ID") String deviceId) { return instanceService @@ -232,4 +233,5 @@ public class GatewayDeviceController { ); } + } diff --git a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/web/ProtocolSupportController.java b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/web/ProtocolSupportController.java index e07b1636..37870f92 100644 --- a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/web/ProtocolSupportController.java +++ b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/web/ProtocolSupportController.java @@ -6,6 +6,7 @@ import io.swagger.v3.oas.annotations.Hidden; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.Generated; import lombok.Getter; import org.apache.commons.codec.digest.DigestUtils; import org.hswebframework.utils.StringUtils; @@ -16,18 +17,16 @@ 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.BusinessException; -import org.jetlinks.community.device.entity.ProtocolSupportEntity; -import org.jetlinks.community.device.service.LocalProtocolSupportService; -import org.jetlinks.community.device.web.protocol.ProtocolDetail; -import org.jetlinks.community.device.web.protocol.ProtocolInfo; -import org.jetlinks.community.device.web.protocol.TransportInfo; import org.jetlinks.community.device.web.request.ProtocolDecodeRequest; import org.jetlinks.community.device.web.request.ProtocolEncodeRequest; import org.jetlinks.community.io.file.FileManager; +import org.jetlinks.community.protocol.ProtocolDetail; +import org.jetlinks.community.protocol.ProtocolInfo; +import org.jetlinks.community.protocol.ProtocolSupportEntity; import org.jetlinks.community.protocol.TransportDetail; +import org.jetlinks.community.protocol.service.LocalProtocolSupportService; import org.jetlinks.core.ProtocolSupport; import org.jetlinks.core.ProtocolSupports; -import org.jetlinks.core.message.codec.Transport; import org.jetlinks.core.metadata.ConfigMetadata; import org.jetlinks.core.metadata.unit.ValueUnit; import org.jetlinks.core.metadata.unit.ValueUnits; @@ -59,6 +58,7 @@ public class ProtocolSupportController @Autowired @Getter + @Generated private LocalProtocolSupportService service; @Autowired @@ -76,6 +76,7 @@ public class ProtocolSupportController @PostMapping("/{id}/_deploy") @SaveAction @Operation(summary = "发布协议") + @Deprecated public Mono deploy(@PathVariable String id) { return service.deploy(id); } @@ -83,10 +84,21 @@ public class ProtocolSupportController @PostMapping("/{id}/_un-deploy") @SaveAction @Operation(summary = "取消发布") + @Deprecated public Mono unDeploy(@PathVariable String id) { return service.unDeploy(id); } + @GetMapping("/{id:.+}/exists") + @QueryAction + @Operation(summary = "验证协议ID是否存在") + public Mono idValidate(@PathVariable @Parameter(description = "协议ID") String id) { + return Flux.merge(service.findById(id), + protocolSupports.getProtocols() + .filter(protocol -> protocol.getId().equals(id))) + .hasElements(); + } + //获取支持的协议类型 @GetMapping("/providers") @Authorize(merge = false) @@ -105,15 +117,24 @@ public class ProtocolSupportController .getProtocols() .collectMap(ProtocolSupport::getId) .flatMapMany(protocols -> service.createQuery() - .setParam(query) - .fetch() - .index() - .flatMap(tp2 -> Mono - .justOrEmpty(protocols.get(tp2.getT2().getId())) - .map(ProtocolInfo::of) - .map(protocolInfo -> Tuples.of(tp2.getT1(), protocolInfo)))) + .setParam(query) + .fetch() + .index() + .flatMap(tp2 -> Mono + .justOrEmpty(protocols.get(tp2.getT2().getId())) + .map(ignore -> ProtocolInfo.of(tp2.getT2())) + .map(protocolInfo -> Tuples.of(tp2.getT1(), protocolInfo)))) .sort(Comparator.comparingLong(Tuple2::getT1)) .map(Tuple2::getT2); + + } + + @GetMapping("/supports/{transport}") + @Authorize(merge = false) + @Operation(summary = "获取支持指定传输协议的消息协议") + public Flux getSupportTransportProtocols(@PathVariable String transport, + @Parameter(hidden = true) QueryParamEntity query) { + return service.getSupportTransportProtocols(transport,query); } @GetMapping("/{id}/{transport}/configuration") @@ -122,8 +143,7 @@ public class ProtocolSupportController @Operation(summary = "获取协议对应使用传输协议的配置元数据") public Mono getTransportConfiguration(@PathVariable @Parameter(description = "协议ID") String id, @PathVariable @Parameter(description = "传输协议") String transport) { - return protocolSupports.getProtocol(id) - .flatMap(support -> support.getConfigMetadata(Transport.of(transport))); + return service.getTransportConfiguration(id, transport); } @GetMapping("/{id}/{transport}/metadata") @@ -132,33 +152,50 @@ public class ProtocolSupportController @Operation(summary = "获取协议设置的默认物模型") public Mono getDefaultMetadata(@PathVariable @Parameter(description = "协议ID") String id, @PathVariable @Parameter(description = "传输协议") String transport) { - return protocolSupports - .getProtocol(id) - .flatMap(support ->support - .getDefaultMetadata(Transport.of(transport)) - .flatMap(metadata-> support.getMetadataCodec().encode(metadata)) - ).defaultIfEmpty("{}"); + return service.getDefaultMetadata(id,transport); } @GetMapping("/{id}/transports") @Authorize(merge = false) @Operation(summary = "获取协议支持的传输协议") - public Flux getAllTransport(@PathVariable @Parameter(description = "协议ID") String id) { + public Flux getAllTransport(@PathVariable @Parameter(description = "协议ID") String id) { return protocolSupports .getProtocol(id) - .flatMapMany(ProtocolSupport::getSupportedTransport) - .distinct() - .map(TransportInfo::of); + .onErrorMap(e -> new BusinessException("error.unable_to_load_protocol_by_access_id", 404, id)) + .flatMapMany(protocol -> protocol + .getSupportedTransport() + .distinct() + .flatMap(transport -> TransportDetail.of(protocol, transport))); + } + + @GetMapping("/{id}/transport/{transport}") + @Authorize(merge = false) + @Operation(summary = "获取消息协议对应的传输协议信息") + public Mono getTransportDetail(@PathVariable @Parameter(description = "协议ID") String id, + @PathVariable @Parameter(description = "传输协议") String transport) { + return service.getTransportDetail(id, transport); + } + + @PostMapping("/{id}/detail") + @QueryAction + @Operation(summary = "获取协议详情") + public Mono protocolDetail(@PathVariable String id) { + return protocolSupports + .getProtocol(id) + .onErrorMap(e -> new BusinessException("error.unable_to_load_protocol_by_access_id", 404, id)) + .flatMap(ProtocolDetail::of); } @PostMapping("/convert") @QueryAction @Hidden - public Mono convertToDetail(@RequestBody Mono entity) { + public Mono convertToDetail(@RequestParam(required = false) String transport, @RequestBody Mono entity) { return entity.map(ProtocolSupportEntity::toDeployDefinition) - .doOnNext(def -> def.setId("_debug")) - .flatMap(def -> supportLoader.load(def)) - .flatMap(ProtocolDetail::of); + .doOnNext(def -> def.setId("_debug")) + .flatMap(def -> supportLoader.load(def)) + .flatMap(support -> ProtocolDetail + .of(support, transport) + .doFinally(s -> support.dispose())); } @PostMapping("/decode") @@ -169,10 +206,14 @@ public class ProtocolSupportController .flatMapMany(request -> { ProtocolSupportDefinition supportEntity = request.getEntity().toDeployDefinition(); supportEntity.setId("_debug"); - return supportLoader.load(supportEntity) - .flatMapMany(protocol -> request - .getRequest() - .doDecode(protocol, null)); + return supportLoader + .load(supportEntity) + .flatMapMany(protocol -> Flux + .defer(() -> request + .getRequest() + .doDecode(protocol, null)) + .doFinally(s -> protocol.dispose()) + ); }) .collectList() .map(JSON::toJSONString) @@ -187,73 +228,27 @@ public class ProtocolSupportController .flatMapMany(request -> { ProtocolSupportDefinition supportEntity = request.getEntity().toDeployDefinition(); supportEntity.setId("_debug"); - return supportLoader.load(supportEntity) - .flatMapMany(protocol -> request - .getRequest() - .doEncode(protocol, null)); + return supportLoader + .load(supportEntity) + .flatMapMany(protocol -> Flux + .defer(() -> request + .getRequest() + .doEncode(protocol, null)) + .doFinally(s -> protocol.dispose())); }) .collectList() .map(JSON::toJSONString) .onErrorResume(err -> Mono.just(StringUtils.throwable2String(err))); } + @GetMapping("/units") @Authorize(merge = false) @Operation(summary = "获取单位数据") public Flux allUnits() { - return Flux.fromIterable(ValueUnits.getAllUnit()); - } - - - @GetMapping("/supports/{transport}") - @Authorize(merge = false) - @Operation(summary = "获取支持指定传输协议的消息协议") - public Flux getSupportTransportProtocols(@PathVariable String transport, - @Parameter(hidden = true) QueryParamEntity query) { - return protocolSupports - .getProtocols() - .collectMap(ProtocolSupport::getId) - .flatMapMany(protocols -> service.createQuery() - .setParam(query) - .fetch() - .index() - .flatMap(tp2 -> Mono - .justOrEmpty(protocols.get(tp2.getT2().getId())) - .filterWhen(support -> support - .getSupportedTransport() - .filter(t -> t.isSame(transport)) - .hasElements()) - .map(ProtocolInfo::of) - .map(protocolInfo -> Tuples.of(tp2.getT1(), protocolInfo)))) - .sort(Comparator.comparingLong(Tuple2::getT1)) - .map(Tuple2::getT2); - } - - @GetMapping("/{id}/transport/{transport}") - @Authorize(merge = false) - @Operation(summary = "获取消息协议对应的传输协议信息") - public Mono getTransportDetail(@PathVariable @Parameter(description = "协议ID") String id, - @PathVariable @Parameter(description = "传输协议") String transport) { - return protocolSupports - .getProtocol(id) - .onErrorMap(e -> new BusinessException("error.unable_to_load_protocol_by_access_id", 404, id)) - .flatMapMany(protocol -> protocol - .getSupportedTransport() - .filter(trans -> trans.isSame(transport)) - .distinct() - .flatMap(_transport -> TransportDetail.of(protocol, _transport))) - .singleOrEmpty(); - } - - - @PostMapping("/{id}/detail") - @QueryAction - @Operation(summary = "获取协议详情") - public Mono protocolDetail(@PathVariable String id) { - return protocolSupports - .getProtocol(id) - .onErrorMap(e -> new BusinessException("error.unable_to_load_protocol_by_access_id", 404, id)) - .flatMap(ProtocolDetail::of); + return Flux + .fromIterable(ValueUnits.getAllUnit()) + .distinct(ValueUnit::getId); } diff --git a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/web/excel/DeviceExcelConstants.java b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/web/excel/DeviceExcelConstants.java index 31ebae90..4ace1104 100644 --- a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/web/excel/DeviceExcelConstants.java +++ b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/web/excel/DeviceExcelConstants.java @@ -23,7 +23,7 @@ public interface DeviceExcelConstants { /** * 最大长度 */ - String maxLength = "macLength"; + String maxLength = "maxLength"; /** * 单位 @@ -39,4 +39,9 @@ public interface DeviceExcelConstants { * 标签 */ String tags = "tags"; + + /** + * 拓展配置 + */ + String expands = "expands"; } diff --git a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/web/excel/DeviceExcelImporter.java b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/web/excel/DeviceExcelImporter.java index a43b89e6..32c6053e 100644 --- a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/web/excel/DeviceExcelImporter.java +++ b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/web/excel/DeviceExcelImporter.java @@ -1,9 +1,11 @@ package org.jetlinks.community.device.web.excel; import lombok.Getter; +import org.hswebframework.web.api.crud.entity.EntityFactoryHolder; import org.hswebframework.web.authorization.Authentication; import org.hswebframework.web.validator.ValidatorUtils; import org.jetlinks.community.device.entity.DeviceProductEntity; +import org.jetlinks.community.device.service.LocalDeviceInstanceService; import org.jetlinks.community.io.excel.AbstractImporter; import org.jetlinks.community.io.excel.ImportHelper; import org.jetlinks.community.io.file.FileManager; @@ -20,7 +22,7 @@ import java.util.Map; /** * 设备导入. * - * @author zhangji 2023/6/28 + * @author zhangji 2023/6/25 * @since 2.1 */ public class DeviceExcelImporter extends AbstractImporter { @@ -34,13 +36,17 @@ public class DeviceExcelImporter extends AbstractImporter { private final Authentication auth; + private final LocalDeviceInstanceService deviceService; + public DeviceExcelImporter(FileManager fileManager, WebClient client, DeviceProductEntity product, List configs, + LocalDeviceInstanceService deviceService, Authentication auth) { super(fileManager, client); this.product = product; + this.deviceService = deviceService; this.auth = auth; List tags = product.parseMetadata().getTags(); for (PropertyMetadata tag : tags) { @@ -55,13 +61,13 @@ public class DeviceExcelImporter extends AbstractImporter { protected Mono handleData(Flux data) { return data .doOnNext(ValidatorUtils::tryValidate) - .map(deviceExcelInfo -> deviceExcelInfo.initDeviceInstance(product, auth)) + .map(deviceExcelInfo -> deviceExcelInfo.initDeviceInstance(product,auth)) .then(); } @Override protected DeviceExcelInfo newInstance() { - DeviceExcelInfo deviceExcelInfo = new DeviceExcelInfo(); + DeviceExcelInfo deviceExcelInfo = EntityFactoryHolder.newInstance(DeviceExcelInfo.class,DeviceExcelInfo::new); deviceExcelInfo.setTagMapping(tagMapping); deviceExcelInfo.setConfigMapping(configMapping); return deviceExcelInfo; diff --git a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/web/excel/DeviceExcelInfo.java b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/web/excel/DeviceExcelInfo.java index f3a697aa..c5a5588a 100644 --- a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/web/excel/DeviceExcelInfo.java +++ b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/web/excel/DeviceExcelInfo.java @@ -1,19 +1,25 @@ package org.jetlinks.community.device.web.excel; import com.alibaba.fastjson.JSONObject; +import lombok.Generated; import lombok.Getter; import lombok.Setter; import org.hswebframework.reactor.excel.CellDataType; import org.hswebframework.reactor.excel.ExcelHeader; +import org.hswebframework.web.api.crud.entity.EntityFactoryHolder; import org.hswebframework.web.authorization.Authentication; import org.hswebframework.web.bean.FastBeanCopier; +import org.hswebframework.web.validator.CreateGroup; import org.hswebframework.web.validator.ValidatorUtils; -import org.jetlinks.community.device.entity.DeviceInstanceEntity; -import org.jetlinks.community.device.entity.DeviceProductEntity; -import org.jetlinks.community.device.entity.DeviceTagEntity; import org.jetlinks.core.metadata.ConfigPropertyMetadata; import org.jetlinks.core.metadata.Jsonable; import org.jetlinks.core.metadata.PropertyMetadata; +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.DeviceType; +import org.jetlinks.community.io.excel.ExcelUtils; +import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; import javax.validation.constraints.NotBlank; @@ -22,6 +28,7 @@ import java.util.stream.Collectors; @Getter @Setter +@Generated public class DeviceExcelInfo implements Jsonable { @org.jetlinks.community.io.excel.annotation.ExcelHeader(value = "设备ID") @@ -32,13 +39,18 @@ public class DeviceExcelInfo implements Jsonable { @NotBlank(message = "设备名称不能为空") private String name; - private String orgName; - + @org.jetlinks.community.io.excel.annotation.ExcelHeader(value = "产品名称", ignoreRead = true) private String productName; - @org.jetlinks.community.io.excel.annotation.ExcelHeader(value = "父设备ID") + @org.jetlinks.community.io.excel.annotation.ExcelHeader(value = "设备类型", ignoreRead = true) + private DeviceType deviceType; + + @org.jetlinks.community.io.excel.annotation.ExcelHeader(value = "父设备ID", ignoreRead = true) private String parentId; + @org.jetlinks.community.io.excel.annotation.ExcelHeader(value = "状态", ignoreRead = true) + private String state; + private List tags = new ArrayList<>(); private DeviceInstanceEntity device; @@ -51,8 +63,6 @@ public class DeviceExcelInfo implements Jsonable { private long rowNumber; - private String state; - public void config(String key, Object value) { if (value == null || value instanceof String && !StringUtils.hasText((String) value)) { return; @@ -60,8 +70,12 @@ public class DeviceExcelInfo implements Jsonable { configuration.put(key, value); } + public static DeviceExcelInfo of(){ + return EntityFactoryHolder.newInstance(DeviceExcelInfo.class,DeviceExcelInfo::new); + } + public void tag(String key, String name, Object value, String type) { - if (value == null) { + if (ObjectUtils.isEmpty(value)) { return; } DeviceTagEntity entity = new DeviceTagEntity(); @@ -70,7 +84,7 @@ public class DeviceExcelInfo implements Jsonable { entity.setName(name); entity.setDeviceId(id); entity.setType(type); - entity.setId(DeviceTagEntity.createTagId(id,key)); + entity.setId(DeviceTagEntity.createTagId(id, key)); tags.add(entity); } @@ -78,7 +92,7 @@ public class DeviceExcelInfo implements Jsonable { this.id = id; for (DeviceTagEntity tag : tags) { tag.setDeviceId(id); - tag.setId(DeviceTagEntity.createTagId(tag.getDeviceId(),tag.getKey())); + tag.setId(DeviceTagEntity.createTagId(tag.getDeviceId(), tag.getKey())); } } @@ -86,61 +100,71 @@ public class DeviceExcelInfo implements Jsonable { FastBeanCopier.copy(Collections.singletonMap(key, value), this); } - public Map toMap(){ - Map val = FastBeanCopier.copy(this,new HashMap<>()); + public DeviceExcelInfo with(DeviceInstanceEntity device){ + FastBeanCopier.copy(device,this,"state"); + this.setState(device.getState().getText()); + return this; + } + + public void withConfiguration(Map configuration){ + this.configuration.putAll(configuration); + } + + public Map toMap() { + Map val = FastBeanCopier.copy(this, new HashMap<>()); for (DeviceTagEntity tag : tags) { - val.put(tag.getKey(),tag.getValue()); + val.put(tag.getKey(), tag.getValue()); } return val; } + @Override + public JSONObject toJson() { + return new JSONObject(toMap()); + } + public static List getTemplateHeaderMapping(DeviceExcelFilterColumns filterColumns, List tags, List configs) { List arr = - Arrays.stream(new ExcelHeader[]{ - new ExcelHeader("id", "设备ID", CellDataType.STRING), - new ExcelHeader("name", "设备名称", CellDataType.STRING), - new ExcelHeader("parentId", "父设备ID", CellDataType.STRING) - }) - .filter(a-> !filterColumns.getColumns().contains(a.getKey())) - .collect(Collectors.toList()); + ExcelUtils.getHeadersForRead( + EntityFactoryHolder.newInstance(DeviceExcelInfo.class,DeviceExcelInfo::new).getClass()) + .stream() + .filter(a -> !filterColumns.getColumns().contains(a.getKey())) + .collect(Collectors.toList()); + + return addExtHeaders(arr, tags, configs); + } + + public static List addExtHeaders(List headers, + List tags, + List configs) { for (PropertyMetadata tag : tags) { - arr.add(new ExcelHeader(tag.getId(), StringUtils.isEmpty(tag.getName()) ? tag.getId() : tag.getName(), CellDataType.STRING)); + headers.add(new ExcelHeader(tag.getId(), StringUtils.hasText(tag.getName()) ? tag.getName() : tag.getId(), CellDataType.STRING)); } for (ConfigPropertyMetadata config : configs) { - arr.add(new ExcelHeader("configuration." + config.getProperty(), StringUtils.isEmpty(config.getName()) ? config - .getProperty() : config.getName(), CellDataType.STRING)); + headers.add(new ExcelHeader("configuration." + config.getProperty(), + StringUtils.hasText(config.getName()) + ? config.getName() + : config.getProperty(), CellDataType.STRING)); } - return arr; + return headers; } public static List getExportHeaderMapping(DeviceExcelFilterColumns filterColumns, List tags, List configs) { List arr = - Arrays.stream(new ExcelHeader[]{ - new ExcelHeader("id", "设备ID", CellDataType.STRING), - new ExcelHeader("name", "设备名称", CellDataType.STRING), - new ExcelHeader("productName", "产品名称", CellDataType.STRING), - new ExcelHeader("parentId", "父设备ID", CellDataType.STRING), - new ExcelHeader("state", "状态", CellDataType.STRING) - }) - .filter(a-> !filterColumns.getColumns().contains(a.getKey())) - .collect(Collectors.toList()); + ExcelUtils.getHeadersForWrite(EntityFactoryHolder.newInstance(DeviceExcelInfo.class,DeviceExcelInfo::new).getClass()) + .stream() + .filter(a -> !filterColumns.getColumns().contains(a.getKey())) + .collect(Collectors.toList()); - for (PropertyMetadata tag : tags) { - arr.add(new ExcelHeader(tag.getId(), StringUtils.isEmpty(tag.getName()) ? tag.getId() : tag.getName(), CellDataType.STRING)); - } - for (ConfigPropertyMetadata config : configs) { - arr.add(new ExcelHeader("configuration." + config.getProperty(), - StringUtils.isEmpty(config.getName()) ? config.getProperty() : config.getName(), - CellDataType.STRING)); - } - return arr; + return addExtHeaders(arr, tags, configs); } + @Deprecated public static Map getImportHeaderMapping() { Map mapping = new HashMap<>(); @@ -148,14 +172,16 @@ public class DeviceExcelInfo implements Jsonable { mapping.put("设备名称", "name"); mapping.put("名称", "name"); -// mapping.put("所属机构", "orgName"); +// mapping.put("设备型号", "productName"); +// mapping.put("产品型号", "productName"); + mapping.put("父设备ID", "parentId"); return mapping; } public DeviceExcelInfo initDeviceInstance(DeviceProductEntity product, Authentication auth) { - DeviceInstanceEntity entity = FastBeanCopier.copy(this, new DeviceInstanceEntity()); + DeviceInstanceEntity entity = FastBeanCopier.copy(this, DeviceInstanceEntity.of()); entity.setProductId(product.getId()); entity.setProductName(product.getName()); @@ -168,7 +194,7 @@ public class DeviceExcelInfo implements Jsonable { entity.setModifierId(auth.getUser().getId()); entity.setModifierName(auth.getUser().getName()); - ValidatorUtils.tryValidate(entity); + ValidatorUtils.tryValidate(entity, CreateGroup.class); this.device = entity; return this; @@ -184,7 +210,7 @@ public class DeviceExcelInfo implements Jsonable { tag( maybeTag.getId(), entry.getKey(), - Optional.ofNullable(json.getString(maybeTag.getId())).orElse(null), + json.getString(maybeTag.getId()), maybeTag.getValueType().getId() ); } @@ -195,7 +221,7 @@ public class DeviceExcelInfo implements Jsonable { if (maybeConfig != null) { config( maybeConfig.getProperty(), - Optional.ofNullable(json.getString(maybeConfig.getProperty())).orElse(null) + json.getString(maybeConfig.getProperty()) ); } } diff --git a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/web/excel/DeviceWrapper.java b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/web/excel/DeviceWrapper.java index 494d7170..28ba5f96 100644 --- a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/web/excel/DeviceWrapper.java +++ b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/web/excel/DeviceWrapper.java @@ -1,5 +1,6 @@ package org.jetlinks.community.device.web.excel; +import lombok.Generated; import org.hswebframework.reactor.excel.Cell; import org.hswebframework.reactor.excel.converter.RowWrapper; import org.jetlinks.core.metadata.ConfigPropertyMetadata; @@ -14,8 +15,11 @@ import java.util.Map; * 设备数据导入包装器 * * @author zhouhao - * @see 1.0 + * @since 1.0 + * @deprecated 使用 {@link DeviceExcelImporter}导入 */ +@Generated +@Deprecated public class DeviceWrapper extends RowWrapper { Map tagMapping = new HashMap<>(); diff --git a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/web/excel/PropertyMetadataExcelImportInfo.java b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/web/excel/PropertyMetadataExcelImportInfo.java index 03dcd921..bdc152dd 100644 --- a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/web/excel/PropertyMetadataExcelImportInfo.java +++ b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/web/excel/PropertyMetadataExcelImportInfo.java @@ -11,6 +11,7 @@ import org.apache.commons.lang3.StringUtils; import org.hswebframework.web.bean.FastBeanCopier; import org.hswebframework.web.dict.EnumDict; import org.hswebframework.web.exception.BusinessException; +import org.hswebframework.web.validator.ValidatorUtils; import org.jetlinks.core.metadata.DataType; import org.jetlinks.core.metadata.PropertyMetadata; import org.jetlinks.core.metadata.SimplePropertyMetadata; @@ -19,6 +20,8 @@ import org.jetlinks.core.metadata.unit.ValueUnit; import org.jetlinks.core.metadata.unit.ValueUnits; import org.jetlinks.supports.official.JetLinksDataTypeCodecs; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Pattern; import java.util.*; import java.util.function.Supplier; import java.util.stream.Collectors; @@ -28,20 +31,26 @@ import java.util.stream.Collectors; @Slf4j public class PropertyMetadataExcelImportInfo { + @Pattern(regexp = "^[a-zA-Z0-9_-]+$",message = "error.import_property_metadata_id_validate_error") + @NotBlank(message = "error.import_property_metadata_id_not_null") private String property; + @NotBlank(message = "error.import_property_metadata_name_not_null") private String name; private String valueType; private Map expands = new HashMap<>(); + //数据类型 + @NotBlank(message = "error.import_property_metadata_data_type_not_null") private String dataType; //单位 private String unit; //精度 private String scale; //来源 + @NotBlank(message = "error.import_property_metadata_source_not_null") private String source; private String description; @@ -50,7 +59,12 @@ public class PropertyMetadataExcelImportInfo { private long rowNumber; //读写类型 - private List type; + @NotBlank(message = "error.import_property_metadata_type_not_null") + private String type; + + private List parseType(){ + return Arrays.stream(this.type.split(",")).collect(Collectors.toList()); + } /** * 单位 @@ -80,12 +94,20 @@ public class PropertyMetadataExcelImportInfo { public PropertyMetadata toMetadata() { SimplePropertyMetadata metadata = new SimplePropertyMetadata(); + try { + ValidatorUtils.tryValidate(this); metadata.setId(property); metadata.setName(name); metadata.setValueType(parseDataType()); metadata.setExpands(parseExpands()); metadata.setDescription(description); return metadata; + } catch (Throwable e) { + throw new BusinessException.NoStackTrace("error.import_property_metadata_error_index", + 400, + getRowNumber(), + e.getLocalizedMessage()); + } } protected DataType parseDataType() { @@ -113,9 +135,11 @@ public class PropertyMetadataExcelImportInfo { protected Map parseExpands() { // 处理系统默认的扩展信息(中文转换),合并到导入模板的expands中 expands.put(DeviceExcelConstants.source, PropertySource.getValue(source)); - expands.put(DeviceExcelConstants.storageType, PropertyStorage.getValue(storageType)); + Map storageTypeMap = new HashMap<>(); + storageTypeMap.put(DeviceExcelConstants.storageType, PropertyStorage.getValue(storageType)); + expands.put(DeviceExcelConstants.expands, storageTypeMap); expands.put(DeviceExcelConstants.tags, ""); - expands.put(DeviceExcelConstants.type, type.stream().map(PropertyType::getValue).collect(Collectors.toList())); + expands.put(DeviceExcelConstants.type, parseType().stream().map(PropertyType::getValue).collect(Collectors.toList())); return expands; } @@ -125,7 +149,8 @@ public class PropertyMetadataExcelImportInfo { setStorageType(PropertyStorage.getText(storageType)); setExpands(Collections.singletonMap(DeviceExcelConstants.storageType, storageType)); Map map = FastBeanCopier.copy(this, new HashMap<>(8)); - map.put(DeviceExcelConstants.type, type.stream() + map.put(DeviceExcelConstants.type, parseType() + .stream() .map(PropertyType::getText) .collect(Collectors.joining(","))); return map; 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 index 9ea68ce8..af7bd960 100644 --- 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 @@ -3,6 +3,7 @@ package org.jetlinks.community.device.web.excel; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONObject; import com.google.common.collect.Lists; +import io.netty.util.internal.ThreadLocalRandom; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.Setter; @@ -13,14 +14,12 @@ 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; @@ -33,7 +32,7 @@ import java.util.stream.Collectors; @Slf4j public class PropertyMetadataExcelInfo { - @NotBlank(message = "属性ID不能为空") + @NotBlank(message = "属性标识不能为空") private String property; @NotBlank(message = "属性名称不能为空") @@ -59,7 +58,22 @@ public class PropertyMetadataExcelInfo { private long rowNumber; //读写类型 - private List type; + @NotBlank(message = "读写类型不能为空") + private String type; + + public static String typeListToString(List type) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < type.size() - 1; i++) { + sb.append(type.get(i)); + sb.append(","); + } + sb.append(type.get(type.size() - 1)); + return sb.toString(); + } + + private List parseType(){ + return Arrays.stream(this.type.split(",")).collect(Collectors.toList()); + } /** * 单位 @@ -67,17 +81,24 @@ public class PropertyMetadataExcelInfo { private static final List idList = ValueUnits.getAllUnit(); /** - * 所有数据类型 + * 示例数据类型 */ - private static final List DATA_TYPES = Lists.newArrayList(ArrayType.ID, BooleanType.ID, + private static final List DATA_TYPES = Lists.newArrayList(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); + StringType.ID, GeoType.ID, FileType.ID, PasswordType.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); + private static final List numbers = Lists.newArrayList(IntType.ID, FloatType.ID, DoubleType.ID, LongType.ID); + + + /** + * 小数类型 + */ + private static final List decimalNumbers = Lists.newArrayList(FloatType.ID, DoubleType.ID); + public void with(String key, Object value) { FastBeanCopier.copy(Collections.singletonMap(key, value), this); @@ -87,9 +108,6 @@ public class PropertyMetadataExcelInfo { 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()); @@ -97,13 +115,13 @@ public class PropertyMetadataExcelInfo { metadata.setDescription(description); return metadata; } catch (Throwable e) { - throw new BusinessException("第" + this.getRowNumber() + "行错误:" + e.getMessage()); + throw new BusinessException("第" + this.getRowNumber() + "行错误:" + e.getMessage(),e); } } public static List getTemplateHeaderMapping(List configMetadataList) { List arr = new ArrayList<>(Arrays.asList( - new ExcelHeader("property", "属性ID", CellDataType.STRING), + new ExcelHeader("property", "属性标识", CellDataType.STRING), new ExcelHeader("name", "属性名称", CellDataType.STRING), new ExcelHeader("dataType", "数据类型", CellDataType.STRING), new ExcelHeader("unit", "单位", CellDataType.STRING), @@ -134,11 +152,11 @@ public class PropertyMetadataExcelInfo { //默认先使用json格式的数据解析物模型,没有json则使用简单模板,只支持int long double float if (!StringUtils.isEmpty(this.valueType)) { dataTypeJson = JSON.parseObject(this.valueType); - this.dataType = dataTypeJson.getString("type"); + this.dataType = dataTypeJson.getString(DeviceExcelConstants.type); } else { - dataTypeJson.put("type", this.dataType); - dataTypeJson.put("unit", this.unit); - dataTypeJson.put("scale", this.scale); + dataTypeJson.put(DeviceExcelConstants.type, this.dataType); + dataTypeJson.put(DeviceExcelConstants.unit, this.unit); + dataTypeJson.put(DeviceExcelConstants.scale, this.scale); } DataType dataType = Optional.ofNullable(this.dataType) .map(DataTypes::lookup) @@ -153,10 +171,12 @@ public class PropertyMetadataExcelInfo { 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())); + map.put(DeviceExcelConstants.source, PropertySource.getValue(source)); + Map storageTypeMap = new HashMap<>(); + storageTypeMap.put(DeviceExcelConstants.storageType, PropertyStorage.getValue(storageType)); + expands.put(DeviceExcelConstants.expands, storageTypeMap); + map.put(DeviceExcelConstants.tags, ""); + map.put(DeviceExcelConstants.type, parseType().stream().map(PropertyType::getValue).collect(Collectors.toList())); return map; } @@ -170,20 +190,23 @@ public class PropertyMetadataExcelInfo { 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"); + ThreadLocalRandom random = ThreadLocalRandom.current(); + excelInfo.setStorageType(random.nextBoolean() ? PropertyStorage.direct.getText() : PropertyStorage.ignore.getText()); + excelInfo.setSource(PropertySource.device.getText()); excelInfo.setDescription(excelInfo.getName() + "的说明"); - if (SIMPLE.contains(dt)) { + if (numbers.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四种"); + excelInfo.setDescription(excelInfo.getName() + "的说明。数据类型配置可以为空"); + if (decimalNumbers.contains(dt)){ + excelInfo.setScale(String.valueOf(random.nextInt(2) + 1)); + } } 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")); + excelInfo.setExpands(Collections.singletonMap(DeviceExcelConstants.storageType, excelInfo.getStorageType())); + List typeList = Arrays.asList(PropertyType.read.getText(), PropertyType.write.getText(), PropertyType.report.getText()); + excelInfo.setType(typeListToString(typeList)); return Flux.just(excelInfo); }).doOnError(e -> { log.error("填充模板异常:", e); @@ -228,10 +251,10 @@ public class PropertyMetadataExcelInfo { } break; case StringType.ID: - ((StringType) dataType).expand("maxLength", random.nextInt(2000)); + ((StringType) dataType).expand(DeviceExcelConstants.maxLength, random.nextInt(2000)); break; case PasswordType.ID: - ((PasswordType) dataType).expand("maxLength", random.nextInt(30)); + ((PasswordType) dataType).expand(DeviceExcelConstants.maxLength, random.nextInt(30)); break; default: break; @@ -243,14 +266,47 @@ public class PropertyMetadataExcelInfo { public Map toMap() { setSource(PropertySource.getText(source)); setStorageType(PropertyStorage.getText(storageType)); - setExpands(Collections.singletonMap("storageType", storageType)); + setExpands(Collections.singletonMap(DeviceExcelConstants.storageType, storageType)); Map map = FastBeanCopier.copy(this, new HashMap<>(8)); - map.put("type", type.stream() + map.put(DeviceExcelConstants.type, parseType() + .stream() .map(PropertyType::getText) .collect(Collectors.joining(","))); return map; } + public static List getExcelInfoContent(List properties) { + List excelInfoList = new ArrayList<>(); + for (PropertyMetadata property : properties) { + PropertyMetadataExcelInfo excelInfo = new PropertyMetadataExcelInfo(); + excelInfo.setProperty(property.getId()); + excelInfo.setName(property.getName()); + excelInfo.setDataType(property.getValueType().getId()); + if (PropertyMetadataExcelInfo.numbers.contains(property.getValueType().getType())) { + NumberType type = (NumberType) property.getValueType(); + excelInfo.setUnit(type.getUnit() == null ? "" : type.getUnit().getName()); + if (PropertyMetadataExcelInfo.decimalNumbers.contains(property.getValueType().getType())){ + excelInfo.setScale(type.getScale() == null ? "0" : String.valueOf(type.getScale().intValue())); + } + } else { + excelInfo.setUnit(""); + excelInfo.setScale(""); + } + property.getValueType(); + Map expands = property.getExpands(); + excelInfo.setSource(String.valueOf(expands.getOrDefault(DeviceExcelConstants.source, ""))); + excelInfo.setStorageType(String.valueOf(expands.getOrDefault(DeviceExcelConstants.storageType, ""))); + excelInfo.setDescription(property.getDescription()); + Map valueType = JetLinksDataTypeCodecs + .encode(property.getValueType()).orElse(Collections.emptyMap()); + excelInfo.setValueType(JSONObject.toJSONString(valueType)); + excelInfo.setExpands(Collections.singletonMap(DeviceExcelConstants.storageType, excelInfo.getStorageType())); + excelInfo.setType(property.getExpand(DeviceExcelConstants.type).orElse("").toString()); + excelInfoList.add(excelInfo); + } + return excelInfoList; + } + @AllArgsConstructor @Getter private enum PropertySource implements EnumDict { diff --git a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/web/excel/PropertyMetadataImportWrapper.java b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/web/excel/PropertyMetadataImportWrapper.java index b242904f..11111cb0 100644 --- a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/web/excel/PropertyMetadataImportWrapper.java +++ b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/web/excel/PropertyMetadataImportWrapper.java @@ -9,13 +9,13 @@ import java.util.HashMap; import java.util.List; import java.util.Map; - public class PropertyMetadataImportWrapper extends RowWrapper { +public class PropertyMetadataImportWrapper extends RowWrapper { private final Map propertyMapping = new HashMap<>(); private final Map expandsMapping = new HashMap<>(); public PropertyMetadataImportWrapper(List expands) { - propertyMapping.put("属性ID", "property"); + propertyMapping.put("属性标识", "property"); propertyMapping.put("属性名称", "name"); propertyMapping.put("数据类型", "dataType"); propertyMapping.put("单位", "unit"); diff --git a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/web/protocol/TransportSupportType.java b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/web/protocol/TransportSupportType.java index 65abe5f3..1abf7415 100644 --- a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/web/protocol/TransportSupportType.java +++ b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/web/protocol/TransportSupportType.java @@ -1,16 +1,19 @@ package org.jetlinks.community.device.web.protocol; import lombok.AllArgsConstructor; +import lombok.Generated; import lombok.Getter; import org.hswebframework.web.dict.EnumDict; @AllArgsConstructor @Getter +@Generated public enum TransportSupportType implements EnumDict { ENCODE("编码"), DECODE("解码"); private String text; @Override + @Generated public String getValue() { return name(); } diff --git a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/web/request/AggRequest.java b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/web/request/AggRequest.java index 65e4fa05..b76a6439 100644 --- a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/web/request/AggRequest.java +++ b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/web/request/AggRequest.java @@ -3,6 +3,7 @@ package org.jetlinks.community.device.web.request; import lombok.Getter; import lombok.Setter; import org.jetlinks.community.device.service.data.DeviceDataService; +import org.springframework.util.Assert; import java.util.List; @@ -12,4 +13,11 @@ public class AggRequest { private List columns; private DeviceDataService.AggregationRequest query; + + public void validate(){ + Assert.notNull(columns,"columns can not be null"); + Assert.notNull(query,"query can not be null"); + Assert.notEmpty(columns,"columns can not be empty"); + columns.forEach(DeviceDataService.DevicePropertyAggregation::validate); + } } \ No newline at end of file diff --git a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/web/request/ProtocolDecodePayload.java b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/web/request/ProtocolDecodePayload.java index 5fcc154d..50ddb988 100644 --- a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/web/request/ProtocolDecodePayload.java +++ b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/web/request/ProtocolDecodePayload.java @@ -1,6 +1,7 @@ package org.jetlinks.community.device.web.request; import com.alibaba.fastjson.JSON; +import lombok.Generated; import lombok.Getter; import lombok.Setter; import org.hswebframework.web.bean.FastBeanCopier; @@ -10,8 +11,9 @@ import org.jetlinks.core.message.Message; import org.jetlinks.core.message.codec.*; import org.jetlinks.core.server.session.DeviceSession; import org.jetlinks.rule.engine.executor.PayloadType; -import org.reactivestreams.Publisher; +import reactor.core.publisher.Flux; +import javax.annotation.Nonnull; import javax.annotation.Nullable; @Getter @@ -24,6 +26,8 @@ public class ProtocolDecodePayload { private String payload; + @SuppressWarnings("all") + @Generated public EncodedMessage toEncodedMessage() { if (transport == DefaultTransport.MQTT || transport == DefaultTransport.MQTT_TLS) { if (payload.startsWith("{")) { @@ -34,13 +38,15 @@ public class ProtocolDecodePayload { } else if (transport == DefaultTransport.CoAP || transport == DefaultTransport.CoAP_DTLS) { return DefaultCoapMessage.of(payload); } + return EncodedMessage.simple(payloadType.write(payload)); } - public Publisher doDecode(ProtocolSupport support, DeviceOperator deviceOperator) { + public Flux doDecode(ProtocolSupport support, DeviceOperator deviceOperator) { return support .getMessageCodec(getTransport()) .flatMapMany(codec -> codec.decode(new FromDeviceMessageContext() { + @Nonnull @Override public EncodedMessage getMessage() { return toEncodedMessage(); diff --git a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/web/request/ProtocolDecodeRequest.java b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/web/request/ProtocolDecodeRequest.java index ff78be6f..434af629 100644 --- a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/web/request/ProtocolDecodeRequest.java +++ b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/web/request/ProtocolDecodeRequest.java @@ -2,7 +2,7 @@ package org.jetlinks.community.device.web.request; import lombok.Getter; import lombok.Setter; -import org.jetlinks.community.device.entity.ProtocolSupportEntity; +import org.jetlinks.community.protocol.ProtocolSupportEntity; @Getter @Setter diff --git a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/web/request/ProtocolEncodePayload.java b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/web/request/ProtocolEncodePayload.java index d17801fe..a838b8b0 100644 --- a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/web/request/ProtocolEncodePayload.java +++ b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/web/request/ProtocolEncodePayload.java @@ -2,6 +2,7 @@ package org.jetlinks.community.device.web.request; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONObject; +import lombok.Generated; import lombok.Getter; import lombok.Setter; import org.jetlinks.core.ProtocolSupport; @@ -12,9 +13,10 @@ import org.jetlinks.core.message.codec.DefaultTransport; import org.jetlinks.core.message.codec.MessageEncodeContext; import org.jetlinks.core.message.codec.MqttMessage; import org.jetlinks.rule.engine.executor.PayloadType; -import org.reactivestreams.Publisher; +import reactor.core.publisher.Flux; import javax.annotation.Nullable; +import java.util.Optional; @Getter @Setter @@ -26,13 +28,18 @@ public class ProtocolEncodePayload { private PayloadType payloadType = PayloadType.STRING; + @Generated public Message toDeviceMessage() { - return MessageType.convertMessage(JSON.parseObject(payload)) - .orElseThrow(() -> new IllegalArgumentException("无法识别的消息")); + return Optional + .ofNullable(payload) + .map(JSON::parseObject) + .flatMap(MessageType::convertMessage) + .orElseThrow(() -> new IllegalArgumentException("error.unrecognized_message")); } - public Publisher doEncode(ProtocolSupport support, DeviceOperator operator) { - return support.getMessageCodec(getTransport()) + public Flux doEncode(ProtocolSupport support, DeviceOperator operator) { + return support + .getMessageCodec(getTransport()) .flatMapMany(codec -> codec.encode(new MessageEncodeContext() { @Override public Message getMessage() { diff --git a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/web/request/ProtocolEncodeRequest.java b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/web/request/ProtocolEncodeRequest.java index 8f37bca1..163d6563 100644 --- a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/web/request/ProtocolEncodeRequest.java +++ b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/web/request/ProtocolEncodeRequest.java @@ -2,7 +2,7 @@ package org.jetlinks.community.device.web.request; import lombok.Getter; import lombok.Setter; -import org.jetlinks.community.device.entity.ProtocolSupportEntity; +import org.jetlinks.community.protocol.ProtocolSupportEntity; @Getter @Setter diff --git a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/web/response/ChildrenDeviceInfo.java b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/web/response/ChildrenDeviceInfo.java index ebd00314..79c355ed 100644 --- a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/web/response/ChildrenDeviceInfo.java +++ b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/web/response/ChildrenDeviceInfo.java @@ -1,5 +1,6 @@ package org.jetlinks.community.device.web.response; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Getter; import lombok.Setter; import org.jetlinks.community.device.entity.DeviceInstanceEntity; @@ -9,16 +10,20 @@ import org.jetlinks.community.device.enums.DeviceState; @Setter public class ChildrenDeviceInfo { + @Schema(description = "子设备ID") private String id; + @Schema(description = "设备名称") private String name; + @Schema(description = "说明") private String description; + @Schema(description = "子设备状态") private DeviceState state; - public static ChildrenDeviceInfo of(DeviceInstanceEntity instance){ - ChildrenDeviceInfo deviceInfo=new ChildrenDeviceInfo(); + public static ChildrenDeviceInfo of(DeviceInstanceEntity instance) { + ChildrenDeviceInfo deviceInfo = new ChildrenDeviceInfo(); deviceInfo.setId(instance.getId()); deviceInfo.setName(instance.getName()); deviceInfo.setState(instance.getState()); 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/web/response/DeviceDeployResult.java similarity index 54% rename from jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/response/DeviceDeployResult.java rename to jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/web/response/DeviceDeployResult.java index 4d78dfd8..5de3b1dd 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/web/response/DeviceDeployResult.java @@ -1,4 +1,4 @@ -package org.jetlinks.community.device.response; +package org.jetlinks.community.device.web.response; import lombok.*; @@ -14,6 +14,8 @@ public class DeviceDeployResult { private String message; + private String sourceId; + //导致错误的源头 private Object source; @@ -22,11 +24,15 @@ public class DeviceDeployResult { @Generated public static DeviceDeployResult success(int total) { - return new DeviceDeployResult(total, true, null, null, null); + return new DeviceDeployResult(total, true, null,null, null, null); } @Generated public static DeviceDeployResult error(String message) { - return new DeviceDeployResult(0, false, message, null, null); + return new DeviceDeployResult(0, false, message, null,null, null); + } + + public static DeviceDeployResult error(String message, String sourceId) { + return new DeviceDeployResult(0, false, message, sourceId,null, null); } } diff --git a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/web/response/GatewayDeviceInfo.java b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/web/response/GatewayDeviceInfo.java index 9e73f180..e99cbc6f 100644 --- a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/web/response/GatewayDeviceInfo.java +++ b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/web/response/GatewayDeviceInfo.java @@ -1,7 +1,8 @@ package org.jetlinks.community.device.web.response; -import lombok.Getter; -import lombok.Setter; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.*; +import org.hswebframework.ezorm.rdb.mapping.defaults.SaveResult; import org.jetlinks.community.device.entity.DeviceInstanceEntity; import org.jetlinks.community.device.enums.DeviceState; @@ -15,17 +16,21 @@ import java.util.stream.Collectors; @Setter public class GatewayDeviceInfo { + @Schema(description = "网关设备ID") private String id; + @Schema(description = "网关设备名称") private String name; + @Schema(description = "说明") private String description; + @Schema(description = "网关设备状态") private DeviceState state; + @Schema(description = "子设备信息") private List children; - public static GatewayDeviceInfo of(DeviceInstanceEntity gateway, List children) { GatewayDeviceInfo info = new GatewayDeviceInfo(); diff --git a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/response/ImportDeviceInstanceResult.java b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/web/response/ImportDeviceInstanceResult.java similarity index 80% rename from jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/response/ImportDeviceInstanceResult.java rename to jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/web/response/ImportDeviceInstanceResult.java index 923f5907..9c2af2a4 100644 --- a/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/response/ImportDeviceInstanceResult.java +++ b/jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/web/response/ImportDeviceInstanceResult.java @@ -1,10 +1,6 @@ -package org.jetlinks.community.device.response; +package org.jetlinks.community.device.web.response; -import lombok.AllArgsConstructor; -import lombok.Generated; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; +import lombok.*; import org.hswebframework.ezorm.rdb.mapping.defaults.SaveResult; @Getter diff --git a/jetlinks-manager/device-manager/src/main/resources/i18n/device-manager/messages_en.properties b/jetlinks-manager/device-manager/src/main/resources/i18n/device-manager/messages_en.properties index a9ee52ed..63523c12 100644 --- a/jetlinks-manager/device-manager/src/main/resources/i18n/device-manager/messages_en.properties +++ b/jetlinks-manager/device-manager/src/main/resources/i18n/device-manager/messages_en.properties @@ -5,6 +5,9 @@ org.jetlinks.community.device.enums.DeviceState.notActive=Disabled org.jetlinks.community.device.enums.DeviceState.offline=Offline org.jetlinks.community.device.enums.DeviceState.online=Online +org.jetlinks.community.device.enums.DeviceType.device=Device +org.jetlinks.community.device.enums.DeviceType.childrenDevice=Child Device +org.jetlinks.community.device.enums.DeviceType.gateway=Gateway Device org.jetlinks.community.device.enums.DeviceProductState.unregistered=unpublished org.jetlinks.community.device.enums.DeviceProductState.registered=have published @@ -30,6 +33,11 @@ org.jetlinks.community.device.enums.FirmwareUpgradeState.failed=failed org.jetlinks.community.device.enums.FirmwareUpgradeState.success=success org.jetlinks.community.device.enums.FirmwareUpgradeState.canceled=canceled +org.jetlinks.community.device.enums.FirmwareUpgradeMode.pull=device pull +org.jetlinks.community.device.enums.FirmwareUpgradeMode.push=platform push + +operation.device.deploy=Deploy device + #message message.metadataType_cannot_be_empty=metadataType can not be empty message.productId_cannot_be_empty=productId can not be empty @@ -37,6 +45,8 @@ message.type_cannot_be_empty=type can not be empty message.deviceId_cannot_be_empty=deviceId can not be empty message.Id_cannot_be_empty=Id can not be empty +message.subscriber.provider.device-transparent-codec=abnormal parsing of device transmitted messages + #error error.function_parameter=Function parameter error:{0} error.unsupported_query=Unsupported query:{0} @@ -77,7 +87,140 @@ error.product_ID_already_exists=The product ID already exists error.product_ID_cannot_be_empty=The product ID cannot be empty error.gateway_cannot_be_bound_as_a_child_device=The gateway cannot be bound as a child device error.device_not_found_or_not_activated=device not fount or not activated +error.device_not_found=device not fount +error.device_not_activated=device not activated error.device_category_has_bean_use_by_product=The Category has bean used by the product error.storage_policy_unsupported_operation=The Storage policy does not support this operation error.message_protocol_can_not_be_empty=The messsage protocol can not be empty -error.please_select_the_access_mode_first=Please select the access mode first \ No newline at end of file +error.please_select_the_access_mode_first=Please select the access mode first +error.product_configuration_required_must_not_be_null=The required product configuration must not be null +error.device_configuration_required_must_not_be_null=The required device configuration must not be null +error.config_metadata_required_must_not_be_null=The required config metadata must not be null +error.unsupported_property_format=Unsupported property format +error.unsupported_upload_file_format=Unsupported upload file format +error.firmware_referenced=There is an upgrade task under the current firmware, which cannot be deleted +error.protocol_referenced=This protocol has been referenced by the device gateway and cannot be deleted +error.plugin_device_id_must_not_be_null=The plugin device id must not be null +error.device_id_and_parent_id_can_not_be_the_same=Device ID and gateway ID cannot be the same:{0} +error.product_device_gateway_must_not_be_null=The product is not configured with a device access gateway +error.product_access_id_can_not_change_with_device=When there are device instances under the product, the access id cannot be changed + +error.property_metadata_id_can_not_be_null = Property metadata [{0}] id can not be null +error.property_metadata_name_can_not_be_null = Property metadata [{0}] name can not be null +error.property_metadata_type_can_not_be_null = Property metadata [{0}] type can not be null +error.property_metadata_type_error = Property metadata [{0}] type error +error.property_metadata_time_type_format_can_not_be_null = Property metadata [{0}] time type format can not be null +error.property_metadata_enum_type_can_not_be_null = Property metadata [{0}] enum type can not be null +error.property_metadata_enum_type_value_can_not_be_null = Property metadata [{0}] enum type value can not be null + + +error.import_property_metadata_error_index = Line {0} is incorrect: {1} +error.import_property_metadata_id_not_null = Property metadata id can not be null +error.import_property_metadata_id_validate_error = Property metadata id contains only english or numbers or - or _ +error.import_property_metadata_name_not_null = Property metadata name can not be null +error.import_property_metadata_data_type_not_null = Property metadata date type can not be null +error.import_property_metadata_source_not_null = Property metadata source can not be null +error.import_property_metadata_type_not_null = Property metadata read and write type can not be null + + +hswebframework.web.system.permission.transparent-codec=Device Transparent Message Parsing Configuration +hswebframework.web.system.permission.device-mapping=Device Model Mapping +hswebframework.web.system.permission.device-group=Device Group +hswebframework.web.system.permission.device-msg-task=Device Batch Command Management +hswebframework.web.system.permission.things-instance=Thing Instance Management +hswebframework.web.system.permission.device-instance=Device Instance +hswebframework.web.system.permission.firmware-upgrade-task-manager=Firmware Upgrade Task Management +hswebframework.web.system.permission.firmware-manager=Firmware Management +hswebframework.web.system.permission.protocol-supports=Protocol Management +hswebframework.web.system.permission.device-gateway=Device Access Gateway +hswebframework.web.system.permission.device-product=Device Product +hswebframework.web.system.permission.device-firmware-manager=Device Firmware Information Management +hswebframework.web.system.permission.device-category=Product Category + +hswebframework.web.system.action.state=Status Test +hswebframework.web.system.action.enable=Enable +hswebframework.web.system.action.disable=Disable +hswebframework.web.system.action.import=Import +hswebframework.web.system.action.export=Export +hswebframework.web.system.action.publish=Push Update +hswebframework.web.system.action.deploy=Deploy Task +hswebframework.web.system.action.execute=Execute +hswebframework.web.system.action.bind=Bind +hswebframework.web.system.action.unbind=Unbind + +device.data.store.cassandra-column.name=Cassandra-column +device.data.store.cassandra-row.name=Cassandra-row +device.data.store.clickhouse-column.name=ClickHouse-column +device.data.store.clickhouse-row.name=ClickHouse-row +device.data.store.doris-column.name=Doris-column +device.data.store.doris-row.name=Doris-row +device.data.store.default-column.name=ElasticSearch-column +device.data.store.default-row.name=ElasticSearch-row +device.data.store.influxdb-column.name=Influxdb-column +device.data.store.influxdb-row.name=Influxdb-row +device.data.store.iotdb-column.name=IoTDB-column +device.data.store.iotdb-row.name=IoTDB-row +device.data.store.starrocks-column.name=StarRocks-column +device.data.store.starrocks-row.name=StarRocks-row +device.data.store.tdengine-column.name=TDengine-column +device.data.store.tdengine-row.name=TDengine-row +device.data.store.timescaledb-column.name=TimescaleDB-column +device.data.store.timescaledb-row.name=TimescaleDB-row +device.data.store.none.name=notStore + +#name +message.device.storage.config-metadata.name=storageConfiguration +message.device.storage.config-metadata.type.name=storageType +message.device.storage.config-metadata.type.direct.name=directStorage +message.device.storage.config-metadata.type.direct.desc=Save the reported attribute values to the storage policy +message.device.storage.config-metadata.type.ignore.name=notStorage +metadata.device.storage.config-metadata.type.ignore.desc=Not storage this metadata value +message.device.storage.config-metadata.type.json.name=JSON character +message.device.storage.config-metadata.type.json.desc=Serialize data as JSON strings for storage + +org.jetlinks.community.device.enums.DeviceLogType.event=Event report +org.jetlinks.community.device.enums.DeviceLogType.readProperty=Read property +org.jetlinks.community.device.enums.DeviceLogType.writeProperty=Write property +org.jetlinks.community.device.enums.DeviceLogType.writePropertyReply=Write property reply +org.jetlinks.community.device.enums.DeviceLogType.reportProperty=Property report +org.jetlinks.community.device.enums.DeviceLogType.readPropertyReply=Read property reply +org.jetlinks.community.device.enums.DeviceLogType.child=Child device message +org.jetlinks.community.device.enums.DeviceLogType.childReply=Child device message reply +org.jetlinks.community.device.enums.DeviceLogType.functionInvoke=Function invocation +org.jetlinks.community.device.enums.DeviceLogType.functionReply=Function reply +org.jetlinks.community.device.enums.DeviceLogType.register=Device registration +org.jetlinks.community.device.enums.DeviceLogType.unregister=Device unregistration +org.jetlinks.community.device.enums.DeviceLogType.readFirmware=Read firmware information +org.jetlinks.community.device.enums.DeviceLogType.readFirmwareReply=Read firmware information reply +org.jetlinks.community.device.enums.DeviceLogType.reportFirmware=Firmware information report +org.jetlinks.community.device.enums.DeviceLogType.pullFirmware=Pull firmware information +org.jetlinks.community.device.enums.DeviceLogType.pullFirmwareReply=Pull firmware information reply +org.jetlinks.community.device.enums.DeviceLogType.upgradeFirmware=Push firmware information +org.jetlinks.community.device.enums.DeviceLogType.upgradeFirmwareReply=Push firmware information reply +org.jetlinks.community.device.enums.DeviceLogType.upgradeFirmwareProgress=Firmware update progress +org.jetlinks.community.device.enums.DeviceLogType.log=Log +org.jetlinks.community.device.enums.DeviceLogType.tag=Tag update +org.jetlinks.community.device.enums.DeviceLogType.offline=Offline +org.jetlinks.community.device.enums.DeviceLogType.online=Online +org.jetlinks.community.device.enums.DeviceLogType.other=Other +org.jetlinks.community.device.enums.DeviceLogType.direct=Transparent transmission +org.jetlinks.community.device.enums.DeviceLogType.acknowledge=Acknowledge +org.jetlinks.community.device.enums.DeviceLogType.metadata=Report thing model +org.jetlinks.community.device.enums.DeviceLogType.stateCheck=Status check +org.jetlinks.community.device.enums.DeviceLogType.stateCheckReply=Status check reply +org.jetlinks.community.device.enums.DeviceLogType.disconnect=Disconnect +org.jetlinks.community.device.enums.DeviceLogType.disconnectReply=Disconnect reply +org.jetlinks.community.device.enums.DeviceLogType.reportCollectorData=Report data collection +org.jetlinks.community.device.enums.DeviceLogType.readCollectorData=Read data collection +org.jetlinks.community.device.enums.DeviceLogType.readCollectorDataReply=Read data collection reply +org.jetlinks.community.device.enums.DeviceLogType.writeCollectorData=Write data collection +org.jetlinks.community.device.enums.DeviceLogType.writeCollectorDataReply=Write data collection reply + +command.support.product.name=product related command support +command.support.product.description=product related command collection +command.support.device.name=device related command support +command.support.device.description=device related command collection +command.support.protocol.name=protocol related command support +command.support.protocol.description=protocol related command collection +command.support.firmware.name=firmware related command support +command.support.firmware.description=firmware related command collection 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 109c9401..c05df6a4 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 @@ -6,6 +6,10 @@ org.jetlinks.community.device.enums.DeviceState.notActive=\u7981\u7528 org.jetlinks.community.device.enums.DeviceState.offline=\u79BB\u7EBF org.jetlinks.community.device.enums.DeviceState.online=\u5728\u7EBF +org.jetlinks.community.device.enums.DeviceType.device=\u76F4\u8FDE\u8BBE\u5907 +org.jetlinks.community.device.enums.DeviceType.childrenDevice=\u7F51\u5173\u5B50\u8BBE\u5907 +org.jetlinks.community.device.enums.DeviceType.gateway=\u7F51\u5173\u8BBE\u5907 + org.jetlinks.community.device.enums.DeviceProductState.unregistered=\u6B63\u5E38 org.jetlinks.community.device.enums.DeviceProductState.registered=\u7981\u7528 @@ -31,6 +35,10 @@ org.jetlinks.community.device.enums.FirmwareUpgradeState.failed=\u5347\u7EA7\u59 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.FirmwareUpgradeMode.pull=\u8BBE\u5907\u62C9\u53D6 +org.jetlinks.community.device.enums.FirmwareUpgradeMode.push=\u5E73\u53F0\u63A8\u9001 + +operation.device.deploy=\u542F\u7528\u8BBE\u5907 #message message.metadataType_cannot_be_empty=metadataType\u4E0D\u80FD\u4E3A\u7A7A @@ -39,6 +47,8 @@ message.type_cannot_be_empty=type\u4E0D\u80FD\u4E3A\u7A7A message.deviceId_cannot_be_empty=deviceId\u4E0D\u80FD\u4E3A\u7A7A message.Id_cannot_be_empty=ID\u4E0D\u80FD\u4E3A\u7A7A +message.subscriber.provider.device-transparent-codec=\u8BBE\u5907\u900F\u4F20\u6D88\u606F\u89E3\u6790\u5F02\u5E38 + #error error.function_parameter=\u51FD\u6570\u53C2\u6570\u9519\u8BEF:{0} error.unsupported_query=\u4E0D\u652F\u6301\u7684\u67E5\u8BE2:{0} @@ -81,7 +91,137 @@ 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_not_found=\u8BBE\u5907\u4E0D\u5B58\u5728 +error.device_not_activated=\u8BBE\u5907\u672A\u542F\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 +error.please_select_the_access_mode_first=\u8BF7\u5148\u9009\u62E9\u63A5\u5165\u65B9\u5F0F +error.product_configuration_required_must_not_be_null=\u8BF7\u5148\u5B8C\u6210\u4EA7\u54C1\u63A5\u5165\u914D\u7F6E +error.device_configuration_required_must_not_be_null=\u8BF7\u5B8C\u5584\u5B9E\u4F8B\u4FE1\u606F\u9875\u9762\u4E2D\u7684\u914D\u7F6E +error.config_metadata_required_must_not_be_null=\u914D\u7F6E\u5FC5\u586B\u9879\u4E0D\u80FD\u4E3A\u7A7A:{0} +error.unsupported_property_format=\u8BF7\u8F93\u5165\u6B63\u786E\u683C\u5F0F\u7684\u5C5E\u6027 +error.unsupported_upload_file_format=\u4E0A\u4F20\u6587\u4EF6\u683C\u5F0F\u9519\u8BEF +error.firmware_referenced=\u5F53\u524D\u56FA\u4EF6\u4E0B\u5DF2\u6709\u5347\u7EA7\u4EFB\u52A1\uFF0C\u4E0D\u53EF\u5220\u9664 +error.protocol_referenced=\u8BE5\u6D88\u606F\u534F\u8BAE\u5DF2\u88AB\u8BBE\u5907\u63A5\u5165\u7F51\u5173\u5F15\u7528\uFF0C\u4E0D\u53EF\u5220\u9664 +error.plugin_device_id_must_not_be_null=\u8BBE\u5907ID\u4E0D\u80FD\u4E3A\u7A7A +error.device_id_and_parent_id_can_not_be_the_same=\u8BBE\u5907ID\u4E0E\u7F51\u5173ID\u4E0D\u80FD\u76F8\u540C:{0} +error.product_device_gateway_must_not_be_null=\u4EA7\u54C1\u672A\u914D\u7F6E\u8BBE\u5907\u63A5\u5165\u7F51\u5173 +error.product_access_id_can_not_change_with_device=\u4EA7\u54C1\u4E0B\u6709\u8BBE\u5907\u5B9E\u4F8B\u65F6\u4E0D\u80FD\u66F4\u6362\u63A5\u5165\u65B9\u5F0F + +error.property_metadata_id_can_not_be_null = \u7269\u6A21\u578B\u5C5E\u6027[{0}]ID\u4E0D\u80FD\u4E3A\u7A7A +error.property_metadata_name_can_not_be_null = \u7269\u6A21\u578B\u5C5E\u6027[{0}]\u540D\u79F0\u4E0D\u80FD\u4E3A\u7A7A +error.property_metadata_type_can_not_be_null = \u7269\u6A21\u578B\u5C5E\u6027[{0}]\u7C7B\u578B\u4E0D\u80FD\u4E3A\u7A7A +error.property_metadata_type_error = \u7269\u6A21\u578B\u5C5E\u6027[{0}]\u7C7B\u578B\u9519\u8BEF +error.property_metadata_time_type_format_can_not_be_null = "\u7269\u6A21\u578B\u5C5E\u6027[{0}]\u65F6\u95F4\u7C7B\u578B\u7684\u65F6\u95F4\u683C\u5F0F\u4E0D\u80FD\u4E3A\u7A7A" +error.property_metadata_enum_type_can_not_be_null = "\u7269\u6A21\u578B\u5C5E\u6027[{0}]\u679A\u4E3E\u7C7B\u578B\u4E0D\u80FD\u4E3A\u7A7A" +error.property_metadata_enum_type_value_can_not_be_null = "\u7269\u6A21\u578B\u5C5E\u6027[{0}]\u679A\u4E3E\u7C7B\u578B\u7684\u679A\u4E3E\u9879\u4E0D\u80FD\u4E3A\u7A7A" + + +error.import_property_metadata_error_index = \u7B2C{0}\u884C\u9519\u8BEF\uFF1A{1} +error.import_property_metadata_id_not_null = \u5C5E\u6027\u6807\u8BC6\u4E0D\u80FD\u4E3A\u7A7A +error.import_property_metadata_id_validate_error = \u5C5E\u6027\u6807\u8BC6\u4EC5\u5305\u542B\u82F1\u6587\u6216\u8005\u6570\u5B57\u6216\u8005-\u6216\u8005_ +error.import_property_metadata_name_not_null =\u5C5E\u6027\u540D\u79F0\u4E0D\u80FD\u4E3A\u7A7A +error.import_property_metadata_data_type_not_null = \u5C5E\u6027\u6570\u636E\u7C7B\u578B\u4E0D\u80FD\u4E3A\u7A7A +error.import_property_metadata_source_not_null = \u5C5E\u6027\u6570\u636E\u6765\u6E90\u4E0D\u80FD\u4E3A\u7A7A +error.import_property_metadata_type_not_null = \u5C5E\u6027\u8BFB\u5199\u7C7B\u578B\u4E0D\u80FD\u4E3A\u7A7A + +hswebframework.web.system.permission.transparent-codec=\u8BBE\u5907\u900F\u4F20\u6D88\u606F\u89E3\u6790\u914D\u7F6E +hswebframework.web.system.permission.device-mapping=\u8BBE\u5907\u6A21\u578B\u6620\u5C04 +hswebframework.web.system.permission.device-group=\u8BBE\u5907\u5206\u7EC4 +hswebframework.web.system.permission.device-msg-task=\u8BBE\u5907\u6279\u91CF\u6307\u4EE4\u7BA1\u7406 +hswebframework.web.system.permission.things-instance=\u7269\u5B9E\u4F8B\u7BA1\u7406 +hswebframework.web.system.permission.device-instance=\u8BBE\u5907\u5B9E\u4F8B +hswebframework.web.system.permission.firmware-upgrade-task-manager=\u56FA\u4EF6\u5347\u7EA7\u4EFB\u52A1\u7BA1\u7406 +hswebframework.web.system.permission.firmware-manager=\u56FA\u4EF6\u7BA1\u7406 +hswebframework.web.system.permission.protocol-supports=\u534F\u8BAE\u7BA1\u7406 +hswebframework.web.system.permission.device-gateway=\u8BBE\u5907\u63A5\u5165\u7F51\u5173 +hswebframework.web.system.permission.device-product=\u8BBE\u5907\u4EA7\u54C1 +hswebframework.web.system.permission.device-firmware-manager=\u8BBE\u5907\u56FA\u4EF6\u4FE1\u606F\u7BA1\u7406 +hswebframework.web.system.permission.device-category=\u4EA7\u54C1\u5206\u7C7B +device.data.store.cassandra-column.name=Cassandra-\u591A\u5217\u6A21\u5F0F +device.data.store.cassandra-row.name=Cassandra-\u5355\u5217\u6A21\u5F0F +device.data.store.clickhouse-column.name=ClickHouse-\u591A\u5217\u6A21\u5F0F +device.data.store.clickhouse-row.name=ClickHouse-\u5355\u5217\u6A21\u5F0F +device.data.store.doris-column.name=Doris-\u591A\u5217\u6A21\u5F0F +device.data.store.doris-row.name=Doris-\u5355\u5217\u6A21\u5F0F +device.data.store.default-column.name=ElasticSearch-\u591A\u5217\u6A21\u5F0F +device.data.store.default-row.name=ElasticSearch-\u5355\u5217\u6A21\u5F0F +device.data.store.influxdb-column.name=Influxdb-\u591A\u5217\u6A21\u5F0F +device.data.store.influxdb-row.name=Influxdb-\u5355\u5217\u6A21\u5F0F +device.data.store.iotdb-column.name=IoTDB-\u591A\u5217\u6A21\u5F0F +device.data.store.iotdb-row.name=IoTDB-\u5355\u5217\u6A21\u5F0F +device.data.store.starrocks-column.name=StarRocks-\u591A\u5217\u6A21\u5F0F +device.data.store.starrocks-row.name=StarRocks-\u5355\u5217\u6A21\u5F0F +device.data.store.tdengine-column.name=TDengine-\u591A\u5217\u6A21\u5F0F +device.data.store.tdengine-row.name=TDengine-\u5355\u5217\u6A21\u5F0F +device.data.store.timescaledb-column.name=TimescaleDB-\u591A\u5217\u6A21\u5F0F +device.data.store.timescaledb-row.name=TimescaleDB-\u5355\u5217\u6A21\u5F0F +device.data.store.none.name=\u4E0D\u5B58\u50A8 +#name +message.device.storage.config-metadata.name=\u5B58\u50A8\u914D\u7F6E +message.device.storage.config-metadata.type.name=\u5B58\u50A8\u65B9\u5F0F +message.device.storage.config-metadata.type.direct.name=\u76F4\u63A5\u5B58\u50A8 +message.device.storage.config-metadata.type.direct.desc=\u5C06\u4E0A\u62A5\u7684\u5C5E\u6027\u503C\u4FDD\u5B58\u5230\u914D\u7F6E\u5230\u5B58\u50A8\u7B56\u7565\u4E2D +message.device.storage.config-metadata.type.ignore.name=\u4E0D\u5B58\u50A8 +metadata.device.storage.config-metadata.type.ignore.desc=\u4E0D\u5B58\u50A8\u6B64\u5C5E\u6027\u503C +message.device.storage.config-metadata.type.json.name=JSON\u5B57\u7B26\u4E32 +message.device.storage.config-metadata.type.json.desc=\u5C06\u6570\u636E\u5E8F\u5217\u5316\u4E3AJSON\u5B57\u7B26\u4E32\u8FDB\u884C\u5B58\u50A8 + +hswebframework.web.system.action.state=\u72B6\u6001\u6D4B\u8BD5 +hswebframework.web.system.action.enable=\u542F\u7528 +hswebframework.web.system.action.disable=\u7981\u7528 +hswebframework.web.system.action.import=\u5BFC\u5165 +hswebframework.web.system.action.export=\u5BFC\u51FA +hswebframework.web.system.action.publish=\u63A8\u9001\u66F4\u65B0 +hswebframework.web.system.action.deploy=\u53D1\u5E03\u4EFB\u52A1 +hswebframework.web.system.action.execute=\u6267\u884C +hswebframework.web.system.action.bind=\u7ED1\u5B9A +hswebframework.web.system.action.unbind=\u89E3\u7ED1 + +org.jetlinks.community.device.enums.DeviceLogType.event=\u4E8B\u4EF6\u4E0A\u62A5 +org.jetlinks.community.device.enums.DeviceLogType.readProperty=\u8BFB\u53D6\u5C5E\u6027 +org.jetlinks.community.device.enums.DeviceLogType.writeProperty=\u4FEE\u6539\u5C5E\u6027 +org.jetlinks.community.device.enums.DeviceLogType.writePropertyReply=\u4FEE\u6539\u5C5E\u6027\u56DE\u590D +org.jetlinks.community.device.enums.DeviceLogType.reportProperty=\u5C5E\u6027\u4E0A\u62A5 +org.jetlinks.community.device.enums.DeviceLogType.readPropertyReply=\u8BFB\u53D6\u5C5E\u6027\u56DE\u590D +org.jetlinks.community.device.enums.DeviceLogType.child=\u5B50\u8BBE\u5907\u6D88\u606F +org.jetlinks.community.device.enums.DeviceLogType.childReply=\u5B50\u8BBE\u5907\u6D88\u606F\u56DE\u590D +org.jetlinks.community.device.enums.DeviceLogType.functionInvoke=\u8C03\u7528\u529F\u80FD +org.jetlinks.community.device.enums.DeviceLogType.functionReply=\u8C03\u7528\u529F\u80FD\u56DE\u590D +org.jetlinks.community.device.enums.DeviceLogType.register=\u8BBE\u5907\u6CE8\u518C +org.jetlinks.community.device.enums.DeviceLogType.unregister=\u8BBE\u5907\u6CE8\u9500 +org.jetlinks.community.device.enums.DeviceLogType.readFirmware=\u8BFB\u53D6\u56FA\u4EF6\u4FE1\u606F +org.jetlinks.community.device.enums.DeviceLogType.readFirmwareReply=\u8BFB\u53D6\u56FA\u4EF6\u4FE1\u606F\u56DE\u590D +org.jetlinks.community.device.enums.DeviceLogType.reportFirmware=\u4E0A\u62A5\u56FA\u4EF6\u4FE1\u606F +org.jetlinks.community.device.enums.DeviceLogType.pullFirmware=\u62C9\u53D6\u56FA\u4EF6\u4FE1\u606F +org.jetlinks.community.device.enums.DeviceLogType.pullFirmwareReply=\u62C9\u53D6\u56FA\u4EF6\u4FE1\u606F\u56DE\u590D +org.jetlinks.community.device.enums.DeviceLogType.upgradeFirmware=\u63A8\u9001\u56FA\u4EF6\u4FE1\u606F +org.jetlinks.community.device.enums.DeviceLogType.upgradeFirmwareReply=\u63A8\u9001\u56FA\u4EF6\u4FE1\u606F\u56DE\u590D +org.jetlinks.community.device.enums.DeviceLogType.upgradeFirmwareProgress=\u56FA\u4EF6\u66F4\u65B0\u8FDB\u5EA6 +org.jetlinks.community.device.enums.DeviceLogType.log=\u65E5\u5FD7 +org.jetlinks.community.device.enums.DeviceLogType.tag=\u6807\u7B7E\u66F4\u65B0 +org.jetlinks.community.device.enums.DeviceLogType.offline=\u79BB\u7EBF +org.jetlinks.community.device.enums.DeviceLogType.online=\u4E0A\u7EBF +org.jetlinks.community.device.enums.DeviceLogType.other=\u5176\u5B83 +org.jetlinks.community.device.enums.DeviceLogType.direct=\u900F\u4F20 +org.jetlinks.community.device.enums.DeviceLogType.acknowledge=\u5E94\u7B54 +org.jetlinks.community.device.enums.DeviceLogType.metadata=\u4E0A\u62A5\u7269\u6A21\u578B +org.jetlinks.community.device.enums.DeviceLogType.stateCheck=\u72B6\u6001\u68C0\u67E5 +org.jetlinks.community.device.enums.DeviceLogType.stateCheckReply=\u72B6\u6001\u68C0\u67E5\u56DE\u590D +org.jetlinks.community.device.enums.DeviceLogType.disconnect=\u65AD\u5F00\u8FDE\u63A5 +org.jetlinks.community.device.enums.DeviceLogType.disconnectReply=\u65AD\u5F00\u8FDE\u63A5\u56DE\u590D +org.jetlinks.community.device.enums.DeviceLogType.reportCollectorData=\u4E0A\u62A5\u6570\u91C7\u6570\u636E +org.jetlinks.community.device.enums.DeviceLogType.readCollectorData=\u8BFB\u53D6\u6570\u91C7\u6570\u636E +org.jetlinks.community.device.enums.DeviceLogType.readCollectorDataReply=\u8BFB\u53D6\u6570\u91C7\u6570\u636E\u56DE\u590D +org.jetlinks.community.device.enums.DeviceLogType.writeCollectorData=\u4FEE\u6539\u6570\u91C7\u6570\u636E +org.jetlinks.community.device.enums.DeviceLogType.writeCollectorDataReply=\u4FEE\u6539\u6570\u91C7\u6570\u636E\u56DE\u590D + +command.support.product.name=\u4EA7\u54C1\u76F8\u5173\u547D\u4EE4\u652F\u6301 +command.support.product.description=\u4EA7\u54C1\u76F8\u5173\u547D\u4EE4\u5408\u96C6 +command.support.device.name=\u8BBE\u5907\u76F8\u5173\u547D\u4EE4\u652F\u6301 +command.support.device.description=\u8BBE\u5907\u76F8\u5173\u547D\u4EE4\u5408\u96C6 +command.support.protocol.name=\u534F\u8BAE\u76F8\u5173\u547D\u4EE4\u652F\u6301 +command.support.protocol.description=\u534F\u8BAE\u76F8\u5173\u547D\u4EE4\u5408\u96C6 +command.support.firmware.name=\u56FA\u4EF6\u76F8\u5173\u547D\u4EE4\u652F\u6301 +command.support.firmware.description=\u56FA\u4EF6\u76F8\u5173\u547D\u4EE4\u5408\u96C6 \ No newline at end of file diff --git a/jetlinks-manager/network-manager/src/main/java/org/jetlinks/community/network/manager/entity/CertificateEntity.java b/jetlinks-manager/network-manager/src/main/java/org/jetlinks/community/network/manager/entity/CertificateEntity.java index 6883c8c4..b514a6b7 100755 --- a/jetlinks-manager/network-manager/src/main/java/org/jetlinks/community/network/manager/entity/CertificateEntity.java +++ b/jetlinks-manager/network-manager/src/main/java/org/jetlinks/community/network/manager/entity/CertificateEntity.java @@ -6,9 +6,11 @@ import lombok.Setter; import org.hswebframework.ezorm.rdb.mapping.annotation.*; import org.hswebframework.web.api.crud.entity.GenericEntity; import org.hswebframework.web.api.crud.entity.RecordCreationEntity; +import org.hswebframework.web.api.crud.entity.RecordModifierEntity; import org.hswebframework.web.crud.annotation.EnableEntityEvent; import org.hswebframework.web.crud.generator.Generators; import org.hswebframework.web.validator.CreateGroup; +import org.jetlinks.community.network.manager.enums.CertificateAuthenticationMethod; import org.jetlinks.community.network.manager.enums.CertificateFormat; import org.jetlinks.community.network.manager.enums.CertificateMode; import org.jetlinks.community.network.manager.enums.CertificateType; @@ -29,7 +31,7 @@ import java.sql.JDBCType; @Table(name = "certificate_info") @Comment("证书信息表") @EnableEntityEvent -public class CertificateEntity extends GenericEntity implements RecordCreationEntity { +public class CertificateEntity extends GenericEntity implements RecordCreationEntity, RecordModifierEntity { @Column @Schema(description = "证书名称") @@ -59,6 +61,14 @@ public class CertificateEntity extends GenericEntity implements RecordCr @NotNull(groups = CreateGroup.class) private CertificateMode mode; + @Column(length = 16) + @EnumCodec + @ColumnType(javaType = String.class) + @Schema(description = "证书认证方式,Single or Binomial") + @DefaultValue("single") + @NotNull(groups = CreateGroup.class) + private CertificateAuthenticationMethod authenticationMethod; + @Column @ColumnType(jdbcType = JDBCType.CLOB) @JsonCodec @@ -86,6 +96,26 @@ public class CertificateEntity extends GenericEntity implements RecordCr ) private Long createTime; + @Column(name = "creator_name", updatable = false) + @Schema( + description = "创建者名称(只读)" + , accessMode = Schema.AccessMode.READ_ONLY + ) + private String creatorName; + + @Column(length = 64) + @Schema(description = "修改人") + private String modifierId; + + @Column + @Schema(description = "修改时间") + @DefaultValue(generator = Generators.CURRENT_TIME) + private Long modifyTime; + + @Column(length = 64) + @Schema(description = "修改人名称") + private String modifierName; + @Getter @Setter public static class CertificateConfig { diff --git a/jetlinks-manager/network-manager/src/main/java/org/jetlinks/community/network/manager/entity/DeviceGatewayEntity.java b/jetlinks-manager/network-manager/src/main/java/org/jetlinks/community/network/manager/entity/DeviceGatewayEntity.java index ef408f2f..85388292 100755 --- a/jetlinks-manager/network-manager/src/main/java/org/jetlinks/community/network/manager/entity/DeviceGatewayEntity.java +++ b/jetlinks-manager/network-manager/src/main/java/org/jetlinks/community/network/manager/entity/DeviceGatewayEntity.java @@ -12,10 +12,10 @@ import org.hswebframework.web.bean.FastBeanCopier; import org.hswebframework.web.crud.annotation.EnableEntityEvent; import org.hswebframework.web.crud.generator.Generators; import org.hswebframework.web.validator.CreateGroup; -import org.jetlinks.core.ProtocolSupport; import org.jetlinks.community.gateway.supports.DeviceGatewayProperties; import org.jetlinks.community.gateway.supports.DeviceGatewayProvider; import org.jetlinks.community.network.manager.enums.DeviceGatewayState; +import org.jetlinks.core.ProtocolSupport; import javax.persistence.Column; import javax.persistence.Table; @@ -108,6 +108,13 @@ public class DeviceGatewayEntity extends GenericEntity implements Record ) private Long createTime; + @Column(name = "creator_name", updatable = false) + @Schema( + description = "创建者名称(只读)" + , accessMode = Schema.AccessMode.READ_ONLY + ) + private String creatorName; + @Column(length = 64) @Schema( description = "修改人ID" @@ -123,14 +130,18 @@ public class DeviceGatewayEntity extends GenericEntity implements Record ) private Long modifyTime; + @Column(length = 64) + @Schema(description = "修改人名称") + private String modifierName; + @Column @DefaultValue(generator = Generators.CURRENT_TIME) @Schema(description = "状态变更时间") private Long stateTime; - - public DeviceGatewayProperties toProperties(){ - - return FastBeanCopier.copy(this, new DeviceGatewayProperties()); + public DeviceGatewayProperties toProperties() { + DeviceGatewayProperties properties = FastBeanCopier.copy(this, new DeviceGatewayProperties()); + properties.setEnabled(DeviceGatewayState.enabled.equals(state)); + return properties; } } diff --git a/jetlinks-manager/network-manager/src/main/java/org/jetlinks/community/network/manager/entity/NetworkConfigEntity.java b/jetlinks-manager/network-manager/src/main/java/org/jetlinks/community/network/manager/entity/NetworkConfigEntity.java index 5bd12060..53b7b0c7 100755 --- a/jetlinks-manager/network-manager/src/main/java/org/jetlinks/community/network/manager/entity/NetworkConfigEntity.java +++ b/jetlinks-manager/network-manager/src/main/java/org/jetlinks/community/network/manager/entity/NetworkConfigEntity.java @@ -8,6 +8,7 @@ import org.apache.commons.collections4.CollectionUtils; import org.hswebframework.ezorm.rdb.mapping.annotation.*; import org.hswebframework.web.api.crud.entity.GenericEntity; import org.hswebframework.web.api.crud.entity.RecordCreationEntity; +import org.hswebframework.web.api.crud.entity.RecordModifierEntity; import org.hswebframework.web.crud.annotation.EnableEntityEvent; import org.hswebframework.web.crud.generator.Generators; import org.jetlinks.community.network.NetworkProperties; @@ -19,10 +20,7 @@ import javax.persistence.Table; import javax.validation.constraints.NotNull; import java.io.Serializable; import java.sql.JDBCType; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.Optional; +import java.util.*; import java.util.stream.Collectors; @Getter @@ -30,7 +28,7 @@ import java.util.stream.Collectors; @Table(name = "network_config") @Comment("网络组件信息表") @EnableEntityEvent -public class NetworkConfigEntity extends GenericEntity implements RecordCreationEntity { +public class NetworkConfigEntity extends GenericEntity implements RecordCreationEntity, RecordModifierEntity { @Column @NotNull(message = "名称不能为空") @@ -49,7 +47,7 @@ public class NetworkConfigEntity extends GenericEntity implements Record * @see NetworkType * @see org.jetlinks.community.network.NetworkTypes */ - @Schema(description="组件类型") + @Schema(description = "组件类型") @Generated @Column(nullable = false) @NotNull(message = "类型不能为空") @@ -80,6 +78,26 @@ public class NetworkConfigEntity extends GenericEntity implements Record @Generated private Long createTime; + @Column(name = "creator_name", updatable = false) + @Schema( + description = "创建者名称(只读)" + , accessMode = Schema.AccessMode.READ_ONLY + ) + private String creatorName; + + @Column(length = 64) + @Schema(description = "修改人") + private String modifierId; + + @Column + @Schema(description = "修改时间") + @DefaultValue(generator = Generators.CURRENT_TIME) + private Long modifyTime; + + @Column(length = 64) + @Schema(description = "修改人名称") + private String modifierName; + @Column @Generated @JsonCodec @@ -123,6 +141,7 @@ public class NetworkConfigEntity extends GenericEntity implements Record if (Boolean.FALSE.equals(shareCluster) && cluster != null) { return cluster .stream() + .filter(Objects::nonNull) .map(conf -> toNetworkProperties(conf.configuration)) .collect(Collectors.toList()); } else { @@ -135,10 +154,16 @@ public class NetworkConfigEntity extends GenericEntity implements Record @Generated public static class Configuration implements Serializable { private String serverId; - + private Map tags; private Map configuration; } + public Optional toNetworkProperties(String serverId) { + return this + .getConfig(serverId) + .map(this::toNetworkProperties); + } + public NetworkProperties toNetworkProperties(Map conf) { NetworkProperties properties = new NetworkProperties(); properties.setConfigurations(conf); @@ -153,5 +178,4 @@ public class NetworkConfigEntity extends GenericEntity implements Record return toNetworkProperties(configuration); } - } diff --git a/jetlinks-manager/network-manager/src/main/java/org/jetlinks/community/network/manager/enums/CertificateAuthenticationMethod.java b/jetlinks-manager/network-manager/src/main/java/org/jetlinks/community/network/manager/enums/CertificateAuthenticationMethod.java new file mode 100644 index 00000000..e6ff4f23 --- /dev/null +++ b/jetlinks-manager/network-manager/src/main/java/org/jetlinks/community/network/manager/enums/CertificateAuthenticationMethod.java @@ -0,0 +1,19 @@ +package org.jetlinks.community.network.manager.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.hswebframework.web.dict.I18nEnumDict; + +@Getter +@AllArgsConstructor +public enum CertificateAuthenticationMethod implements I18nEnumDict { + single("单向认证"), + binomial("双向认证"); + + private final String text; + + @Override + public String getValue() { + return name(); + } +} diff --git a/jetlinks-manager/network-manager/src/main/java/org/jetlinks/community/network/manager/enums/ConnectorState.java b/jetlinks-manager/network-manager/src/main/java/org/jetlinks/community/network/manager/enums/ConnectorState.java new file mode 100644 index 00000000..e395e2f6 --- /dev/null +++ b/jetlinks-manager/network-manager/src/main/java/org/jetlinks/community/network/manager/enums/ConnectorState.java @@ -0,0 +1,22 @@ +package org.jetlinks.community.network.manager.enums; + +import lombok.AllArgsConstructor; +import lombok.Generated; +import lombok.Getter; +import org.hswebframework.web.dict.Dict; +import org.hswebframework.web.dict.I18nEnumDict; + +@AllArgsConstructor +@Getter +@Dict +@Generated +public enum ConnectorState implements I18nEnumDict { + running("运行中", "running"), + paused("已暂停", "paused"), + stopped("已停止", "stopped"); + + private String text; + + private String value; + +} \ No newline at end of file diff --git a/jetlinks-manager/network-manager/src/main/java/org/jetlinks/community/network/manager/enums/NetworkConfigState.java b/jetlinks-manager/network-manager/src/main/java/org/jetlinks/community/network/manager/enums/NetworkConfigState.java index 060aaa04..fe922b0b 100644 --- a/jetlinks-manager/network-manager/src/main/java/org/jetlinks/community/network/manager/enums/NetworkConfigState.java +++ b/jetlinks-manager/network-manager/src/main/java/org/jetlinks/community/network/manager/enums/NetworkConfigState.java @@ -1,20 +1,22 @@ package org.jetlinks.community.network.manager.enums; import lombok.AllArgsConstructor; +import lombok.Generated; import lombok.Getter; import org.hswebframework.web.dict.Dict; -import org.hswebframework.web.dict.EnumDict; +import org.hswebframework.web.dict.I18nEnumDict; @AllArgsConstructor @Getter -@Dict("network-config-state") -public enum NetworkConfigState implements EnumDict { - enabled("已启动", "enabled"), +@Generated +@Dict("device-gateway-state") +public enum NetworkConfigState implements I18nEnumDict { + enabled("正常", "enabled"), paused("已暂停", "paused"), - disabled("已停止", "disabled"); + disabled("禁用", "disabled"); - private String text; + private final String text; - private String value; + private final String value; } diff --git a/jetlinks-manager/network-manager/src/main/java/org/jetlinks/community/network/manager/service/CertificateService.java b/jetlinks-manager/network-manager/src/main/java/org/jetlinks/community/network/manager/service/CertificateService.java index df1a9ffe..05f4d79b 100644 --- a/jetlinks-manager/network-manager/src/main/java/org/jetlinks/community/network/manager/service/CertificateService.java +++ b/jetlinks-manager/network-manager/src/main/java/org/jetlinks/community/network/manager/service/CertificateService.java @@ -15,16 +15,16 @@ import reactor.core.publisher.Mono; */ @Service public class CertificateService - extends GenericReactiveCrudService - implements CertificateManager { + extends GenericReactiveCrudService + implements CertificateManager { @Override public Mono getCertificate(String id) { - return createQuery() - .where(CertificateEntity::getId, id) - .fetchOne() - .map(entity -> { - DefaultCertificate defaultCertificate = new DefaultCertificate(entity.getId(), entity.getName()); - return entity.getFormat().init(defaultCertificate, entity.getConfigs()); - }); + return this + .findById(id) + .filter(cert -> cert.getFormat() != null && cert.getConfigs() != null) + .map(entity -> { + DefaultCertificate defaultCertificate = new DefaultCertificate(entity.getId(), entity.getName()); + return entity.getFormat().init(defaultCertificate, entity.getConfigs()); + }); } } diff --git a/jetlinks-manager/network-manager/src/main/java/org/jetlinks/community/network/manager/service/DeviceGatewayConfigService.java b/jetlinks-manager/network-manager/src/main/java/org/jetlinks/community/network/manager/service/DeviceGatewayConfigService.java index 76e9bfb0..f96b6564 100755 --- a/jetlinks-manager/network-manager/src/main/java/org/jetlinks/community/network/manager/service/DeviceGatewayConfigService.java +++ b/jetlinks-manager/network-manager/src/main/java/org/jetlinks/community/network/manager/service/DeviceGatewayConfigService.java @@ -1,9 +1,11 @@ package org.jetlinks.community.network.manager.service; +import org.hswebframework.ezorm.rdb.mapping.ReactiveRepository; import org.jetlinks.community.gateway.supports.DeviceGatewayProperties; import org.jetlinks.community.gateway.supports.DeviceGatewayPropertiesManager; import org.jetlinks.community.gateway.supports.DeviceGatewayProvider; import org.jetlinks.community.network.manager.entity.DeviceGatewayEntity; +import org.jetlinks.community.network.manager.enums.DeviceGatewayState; import org.jetlinks.community.reference.DataReferenceInfo; import org.jetlinks.community.reference.DataReferenceManager; import org.jetlinks.community.reference.DataReferenceProvider; @@ -13,54 +15,60 @@ import reactor.core.publisher.Mono; @Service public class DeviceGatewayConfigService implements DeviceGatewayPropertiesManager, DataReferenceProvider { - - - private final DeviceGatewayService deviceGatewayService; + private final ReactiveRepository repository; @Override public String getId() { return DataReferenceManager.TYPE_NETWORK; } + @Override + public Flux getAllProperties() { + return repository + .createQuery() + .where(DeviceGatewayEntity::getState, DeviceGatewayState.enabled) + .fetch() + .map(DeviceGatewayEntity::toProperties); + } + @Override public Flux getReference(String networkId) { - return deviceGatewayService + return repository .createQuery() .where() .and(DeviceGatewayEntity::getChannel, DeviceGatewayProvider.CHANNEL_NETWORK) .is(DeviceGatewayEntity::getChannelId, networkId) .fetch() - .map(e -> DataReferenceInfo.of(e.getId(),DataReferenceManager.TYPE_NETWORK, e.getChannelId(), e.getName())); + .map(e -> DataReferenceInfo.of(e.getChannelId(), DataReferenceManager.TYPE_DEVICE_GATEWAY, e.getId(), e.getName())); } @Override public Flux getReferences() { - return deviceGatewayService + return repository .createQuery() .where() .and(DeviceGatewayEntity::getChannel, DeviceGatewayProvider.CHANNEL_NETWORK) .notNull(DeviceGatewayEntity::getChannelId) .fetch() - .map(e -> DataReferenceInfo.of(e.getId(),DataReferenceManager.TYPE_NETWORK, e.getChannelId(), e.getName())); + .map(e -> DataReferenceInfo.of(e.getChannelId(), DataReferenceManager.TYPE_DEVICE_GATEWAY, e.getId(), e.getName())); } - public DeviceGatewayConfigService(DeviceGatewayService deviceGatewayService) { - this.deviceGatewayService = deviceGatewayService; + public DeviceGatewayConfigService(ReactiveRepository repository) { + this.repository = repository; } @Override public Mono getProperties(String id) { - return deviceGatewayService + return repository .findById(id) -// .switchIfEmpty(Mono.error(new NotFoundException("该设备网关不存在"))) .map(DeviceGatewayEntity::toProperties); } @Override public Flux getPropertiesByChannel(String channel) { - return deviceGatewayService + return repository .createQuery() .where() .and(DeviceGatewayEntity::getChannel, channel) diff --git a/jetlinks-manager/network-manager/src/main/java/org/jetlinks/community/network/manager/service/DeviceGatewayEventHandler.java b/jetlinks-manager/network-manager/src/main/java/org/jetlinks/community/network/manager/service/DeviceGatewayEventHandler.java index 8b4a0495..b4084087 100755 --- a/jetlinks-manager/network-manager/src/main/java/org/jetlinks/community/network/manager/service/DeviceGatewayEventHandler.java +++ b/jetlinks-manager/network-manager/src/main/java/org/jetlinks/community/network/manager/service/DeviceGatewayEventHandler.java @@ -3,13 +3,12 @@ package org.jetlinks.community.network.manager.service; import lombok.extern.slf4j.Slf4j; import org.hswebframework.web.crud.events.*; import org.hswebframework.web.id.IDGenerator; -import org.jetlinks.core.ProtocolSupport; -import org.jetlinks.community.gateway.DeviceGateway; import org.jetlinks.community.gateway.DeviceGatewayManager; import org.jetlinks.community.gateway.supports.DeviceGatewayProvider; import org.jetlinks.community.network.manager.entity.DeviceGatewayEntity; import org.jetlinks.community.network.manager.enums.DeviceGatewayState; import org.jetlinks.community.reference.DataReferenceManager; +import org.jetlinks.core.ProtocolSupport; import org.springframework.boot.CommandLineRunner; import org.springframework.context.event.EventListener; import org.springframework.core.annotation.Order; @@ -22,9 +21,8 @@ import java.time.Duration; import java.util.List; /** - * - * @author zhouhao - * @since 2.0 + * @author wangzheng + * @since 1.0 */ @Order(1) @Component @@ -72,7 +70,8 @@ public class DeviceGatewayEventHandler implements CommandLineRunner { //删除网关时检测是否已被使用 event.async( Flux.fromIterable(event.getEntity()) - .flatMap(gateway -> referenceManager.assertNotReferenced(DataReferenceManager.TYPE_DEVICE_GATEWAY, gateway.getId())) + .flatMap(gateway -> referenceManager + .assertNotReferenced(DataReferenceManager.TYPE_DEVICE_GATEWAY, gateway.getId(), "error.device_gateway_referenced")) ); } @@ -109,7 +108,7 @@ public class DeviceGatewayEventHandler implements CommandLineRunner { ); } - private Mono reloadGateway(Flux gatewayEntities) { + private Mono reloadGateway(Flux gatewayEntities) { return gatewayEntities .flatMap(gateway -> deviceGatewayManager.reload(gateway.getId())) .then(); @@ -119,14 +118,17 @@ public class DeviceGatewayEventHandler implements CommandLineRunner { for (DeviceGatewayEntity entity : entities) { DeviceGatewayProvider provider = deviceGatewayManager .getProvider(entity.getProvider()) - .orElseThrow(() -> new UnsupportedOperationException("error.unsupported_device_gateway_provider")); + .orElse(null); + if (provider == null) { + continue; + } if (!StringUtils.hasText(entity.getId())) { entity.setId(IDGenerator.SNOW_FLAKE_STRING.generate()); } //接入方式 entity.setChannel(provider.getChannel()); - //传输协议,如TCP,MQTT + //传输协议,如TCP,MQTT,ModBus if (!StringUtils.hasText(entity.getTransport())) { entity.setTransport(provider.getTransport().getId()); } @@ -148,35 +150,36 @@ public class DeviceGatewayEventHandler implements CommandLineRunner { .filter(entity -> entity.getConfiguration() != null) .flatMap(entity -> Mono.justOrEmpty(deviceGatewayManager.getProvider(entity.getProvider())) - .switchIfEmpty(Mono.error( - () -> new UnsupportedOperationException("error.unsupported_device_gateway_provider") - )) + //当分布式部署时,每个服务支持的网关可能不同. +// .switchIfEmpty(Mono.error( +// () -> new UnsupportedOperationException("error.unsupported_device_gateway_provider") +// )) .flatMap(gatewayProvider -> gatewayProvider.createDeviceGateway(entity.toProperties()))) .then(); } @Override public void run(String... args) { - log.debug("start device gateway in {} later", gatewayStartupDelay); - Mono.delay(gatewayStartupDelay) - .then( - Mono.defer(() -> deviceGatewayService - .createQuery() - .where() - .and(DeviceGatewayEntity::getState, DeviceGatewayState.enabled) - .fetch() - .map(DeviceGatewayEntity::getId) - .flatMap(id -> Mono - .defer(() -> deviceGatewayManager - .getGateway(id) - .flatMap(DeviceGateway::startup)) - .onErrorResume((err) -> { - log.error(err.getMessage(), err); - return Mono.empty(); - }) - ) - .then()) - ) - .subscribe(); +// log.debug("start device gateway in {} later", gatewayStartupDelay); +// Mono.delay(gatewayStartupDelay) +// .then( +// Mono.defer(() -> deviceGatewayService +// .createQuery() +// .where() +// .and(DeviceGatewayEntity::getState, DeviceGatewayState.enabled) +// .fetch() +// .map(DeviceGatewayEntity::getId) +// .flatMap(id -> Mono +// .defer(() -> deviceGatewayManager +// .getGateway(id) +// .flatMap(DeviceGateway::startup)) +// .onErrorResume((err) -> { +// log.error(err.getMessage(), err); +// return Mono.empty(); +// }) +// ) +// .then()) +// ) +// .subscribe(); } } diff --git a/jetlinks-manager/network-manager/src/main/java/org/jetlinks/community/network/manager/service/DeviceGatewayService.java b/jetlinks-manager/network-manager/src/main/java/org/jetlinks/community/network/manager/service/DeviceGatewayService.java index 2eaaec3e..55bc21d4 100644 --- a/jetlinks-manager/network-manager/src/main/java/org/jetlinks/community/network/manager/service/DeviceGatewayService.java +++ b/jetlinks-manager/network-manager/src/main/java/org/jetlinks/community/network/manager/service/DeviceGatewayService.java @@ -4,7 +4,6 @@ import org.hswebframework.web.crud.service.GenericReactiveCrudService; import org.hswebframework.web.exception.NotFoundException; import org.jetlinks.community.network.manager.entity.DeviceGatewayEntity; import org.jetlinks.community.network.manager.enums.DeviceGatewayState; -import org.jetlinks.community.network.manager.enums.NetworkConfigState; import org.reactivestreams.Publisher; import org.springframework.stereotype.Service; import reactor.core.publisher.Flux; @@ -15,7 +14,6 @@ import static org.jetlinks.community.network.manager.service.DeviceGatewayEventH /** * @author wangzheng - * @see * @since 1.0 */ @Service diff --git a/jetlinks-manager/network-manager/src/main/java/org/jetlinks/community/network/manager/service/LocalNetworkConfigManager.java b/jetlinks-manager/network-manager/src/main/java/org/jetlinks/community/network/manager/service/LocalNetworkConfigManager.java index 8390b172..33a32e61 100755 --- a/jetlinks-manager/network-manager/src/main/java/org/jetlinks/community/network/manager/service/LocalNetworkConfigManager.java +++ b/jetlinks-manager/network-manager/src/main/java/org/jetlinks/community/network/manager/service/LocalNetworkConfigManager.java @@ -29,7 +29,7 @@ public class LocalNetworkConfigManager implements NetworkConfigManager { } @Override - public Mono getConfig(NetworkType networkType,@Nonnull String id) { + public Mono getConfig(NetworkType networkType, @Nonnull String id) { return reactiveRepository .findById(id) .flatMap(conf -> Mono.justOrEmpty(conf.toNetworkProperties())); diff --git a/jetlinks-manager/network-manager/src/main/java/org/jetlinks/community/network/manager/service/NetworkChannelProvider.java b/jetlinks-manager/network-manager/src/main/java/org/jetlinks/community/network/manager/service/NetworkChannelProvider.java index 740ad5a4..8de46265 100644 --- a/jetlinks-manager/network-manager/src/main/java/org/jetlinks/community/network/manager/service/NetworkChannelProvider.java +++ b/jetlinks-manager/network-manager/src/main/java/org/jetlinks/community/network/manager/service/NetworkChannelProvider.java @@ -1,7 +1,6 @@ package org.jetlinks.community.network.manager.service; import lombok.AllArgsConstructor; -import org.jetlinks.core.utils.FluxUtils; import org.jetlinks.community.network.ClientNetworkConfig; import org.jetlinks.community.network.NetworkManager; import org.jetlinks.community.network.ServerNetworkConfig; @@ -36,7 +35,7 @@ public class NetworkChannelProvider implements ChannelProvider { .flatMap(this::toChannelInfo); } - public Mono toChannelInfo(NetworkConfigEntity entity){ + public Mono toChannelInfo(NetworkConfigEntity entity) { ChannelInfo info = new ChannelInfo(); info.setId(entity.getId()); info.setDescription(entity.getDescription()); @@ -46,7 +45,7 @@ public class NetworkChannelProvider implements ChannelProvider { .flatMap(provider -> Flux .fromIterable(entity.toNetworkPropertiesList()) .flatMap(provider::createConfig) - .as(FluxUtils.safeMap(conf -> { + .mapNotNull(conf -> { if (conf instanceof ClientNetworkConfig) { //客户端则返回远程地址 return ((ClientNetworkConfig) conf).getRemoteAddress(); @@ -56,7 +55,7 @@ public class NetworkChannelProvider implements ChannelProvider { return ((ServerNetworkConfig) conf).getPublicAddress(); } return null; - })) + }) .distinct() .map(address -> Address.of(address, //todo 真实状态检查? diff --git a/jetlinks-manager/network-manager/src/main/java/org/jetlinks/community/network/manager/service/NetworkConfigService.java b/jetlinks-manager/network-manager/src/main/java/org/jetlinks/community/network/manager/service/NetworkConfigService.java index e9598c51..453e5629 100644 --- a/jetlinks-manager/network-manager/src/main/java/org/jetlinks/community/network/manager/service/NetworkConfigService.java +++ b/jetlinks-manager/network-manager/src/main/java/org/jetlinks/community/network/manager/service/NetworkConfigService.java @@ -1,28 +1,21 @@ package org.jetlinks.community.network.manager.service; -import org.hswebframework.ezorm.rdb.mapping.defaults.SaveResult; import org.hswebframework.web.crud.service.GenericReactiveCrudService; import org.hswebframework.web.exception.NotFoundException; -import org.jetlinks.community.network.NetworkConfigManager; import org.jetlinks.community.network.NetworkManager; -import org.jetlinks.community.network.NetworkProperties; -import org.jetlinks.community.network.NetworkType; import org.jetlinks.community.network.manager.entity.NetworkConfigEntity; import org.jetlinks.community.network.manager.enums.NetworkConfigState; import org.reactivestreams.Publisher; import org.springframework.stereotype.Service; -import org.springframework.util.StringUtils; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; -import javax.annotation.Nonnull; - /** * @author zhouhao * @since 1.0 **/ @Service -public class NetworkConfigService extends GenericReactiveCrudService { +public class NetworkConfigService extends GenericReactiveCrudService { private final NetworkManager networkManager; @@ -45,14 +38,13 @@ public class NetworkConfigService extends GenericReactiveCrudService start(String id) { return this .findById(id) - .switchIfEmpty(Mono.error(() -> new NotFoundException("error.configuration_does_not_exist", id))) + .switchIfEmpty(Mono.error(() -> new NotFoundException("error.configuration_does_not_exist",id))) .flatMap(conf -> this .createUpdate() .set(NetworkConfigEntity::getState, NetworkConfigState.enabled) .where(conf::getId) - .execute() - .thenReturn(conf)) - .flatMap(conf -> networkManager.reload(conf.lookupNetworkType(), id)); + .execute()) + .then(); } public Mono shutdown(String id) { @@ -67,4 +59,5 @@ public class NetworkConfigService extends GenericReactiveCrudService networkManager.shutdown(conf.lookupNetworkType(), id)); } + } 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 index 9b39cd88..faa0f85c 100644 --- 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 @@ -55,7 +55,8 @@ public class NetworkEntityEventHandler { public void handleNetworkDelete(EntityBeforeDeleteEvent event) { event.async( Flux.fromIterable(event.getEntity()) - .flatMap(e -> referenceManager.assertNotReferenced(DataReferenceManager.TYPE_NETWORK, e.getId())) + .flatMap(e -> referenceManager + .assertNotReferenced(DataReferenceManager.TYPE_NETWORK, e.getId(), "error.network_referenced")) ); } 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 index fce0781d..4f6e65cf 100644 --- 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 @@ -32,7 +32,7 @@ public class ProtocolDataReferenceProvider implements DataReferenceProvider { .where() .is(DeviceGatewayEntity::getProtocol, protocolId) .fetch() - .map(e -> DataReferenceInfo.of(e.getId(),DataReferenceManager.TYPE_PROTOCOL, e.getProtocol(), e.getName())); + .map(e -> DataReferenceInfo.of(e.getProtocol(), DataReferenceManager.TYPE_DEVICE_GATEWAY, e.getId(), e.getName())); } @Override @@ -40,8 +40,8 @@ public class ProtocolDataReferenceProvider implements DataReferenceProvider { return deviceGatewayService .createQuery() .where() - .notNull(DeviceGatewayEntity::getChannelId) + .notNull(DeviceGatewayEntity::getProtocol) .fetch() - .map(e -> DataReferenceInfo.of(e.getId(),DataReferenceManager.TYPE_PROTOCOL, e.getChannelId(), e.getName())); + .map(e -> DataReferenceInfo.of(e.getProtocol(), DataReferenceManager.TYPE_DEVICE_GATEWAY, e.getId(), e.getName())); } } diff --git a/jetlinks-manager/network-manager/src/main/java/org/jetlinks/community/network/manager/session/DeviceSessionMeasurementProvider.java b/jetlinks-manager/network-manager/src/main/java/org/jetlinks/community/network/manager/session/DeviceSessionMeasurementProvider.java index 84a33de8..b36e26ca 100644 --- a/jetlinks-manager/network-manager/src/main/java/org/jetlinks/community/network/manager/session/DeviceSessionMeasurementProvider.java +++ b/jetlinks-manager/network-manager/src/main/java/org/jetlinks/community/network/manager/session/DeviceSessionMeasurementProvider.java @@ -2,15 +2,6 @@ package org.jetlinks.community.network.manager.session; import com.google.common.collect.Maps; import org.hswebframework.web.utils.DigestUtils; -import org.jetlinks.core.device.DeviceConfigKey; -import org.jetlinks.core.device.session.DeviceSessionManager; -import org.jetlinks.core.metadata.ConfigMetadata; -import org.jetlinks.core.metadata.DataType; -import org.jetlinks.core.metadata.DefaultConfigMetadata; -import org.jetlinks.core.metadata.SimplePropertyMetadata; -import org.jetlinks.core.metadata.types.*; -import org.jetlinks.core.server.session.DeviceSession; -import org.jetlinks.community.PropertyConstants; import org.jetlinks.community.dashboard.*; import org.jetlinks.community.dashboard.supports.StaticMeasurement; import org.jetlinks.community.dashboard.supports.StaticMeasurementProvider; @@ -21,6 +12,14 @@ import org.jetlinks.community.timeseries.TimeSeriesMetadata; import org.jetlinks.community.timeseries.TimeSeriesMetric; import org.jetlinks.community.timeseries.query.AggregationQueryParam; import org.jetlinks.community.utils.TimeUtils; +import org.jetlinks.core.device.DeviceConfigKey; +import org.jetlinks.core.device.session.DeviceSessionManager; +import org.jetlinks.core.metadata.ConfigMetadata; +import org.jetlinks.core.metadata.DataType; +import org.jetlinks.core.metadata.DefaultConfigMetadata; +import org.jetlinks.core.metadata.SimplePropertyMetadata; +import org.jetlinks.core.metadata.types.*; +import org.jetlinks.core.server.session.DeviceSession; import org.joda.time.format.DateTimeFormat; import org.joda.time.format.DateTimeFormatter; import org.reactivestreams.Publisher; @@ -226,7 +225,7 @@ public class DeviceSessionMeasurementProvider extends StaticMeasurementProvider } private long computeDuration(long timestamp) { - return System.currentTimeMillis() - timestamp; + return System.currentTimeMillis() - timestamp; } protected Mono reportDeviceSession(DeviceSession session, String type) { diff --git a/jetlinks-manager/network-manager/src/main/java/org/jetlinks/community/network/manager/web/CertificateController.java b/jetlinks-manager/network-manager/src/main/java/org/jetlinks/community/network/manager/web/CertificateController.java index 21012961..9682af27 100644 --- a/jetlinks-manager/network-manager/src/main/java/org/jetlinks/community/network/manager/web/CertificateController.java +++ b/jetlinks-manager/network-manager/src/main/java/org/jetlinks/community/network/manager/web/CertificateController.java @@ -14,8 +14,7 @@ import org.jetlinks.community.network.manager.entity.CertificateEntity; import org.jetlinks.community.network.manager.service.CertificateService; import org.jetlinks.community.network.security.Certificate; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.core.io.buffer.DataBufferFactory; -import org.springframework.core.io.buffer.DefaultDataBufferFactory; +import org.springframework.core.io.buffer.DataBufferUtils; import org.springframework.http.codec.multipart.FilePart; import org.springframework.http.codec.multipart.Part; import org.springframework.util.StreamUtils; @@ -24,7 +23,6 @@ import reactor.core.publisher.Mono; /** * @author wangzheng - * @see * @since 1.0 */ @RestController @@ -42,9 +40,6 @@ public class CertificateController implements ReactiveServiceCrudController Mono.fromCallable(() -> - Base64.encodeBase64String(StreamUtils.copyToByteArray(factory.join(all).asInputStream(true))))) + return DataBufferUtils + .join(part.content()) + .flatMap(buffer -> Mono + .fromCallable(() -> Base64.encodeBase64String(StreamUtils.copyToByteArray(buffer.asInputStream(true))))) ; } else { - return Mono.error(() -> new IllegalArgumentException("[file] part is not a file")); + return Mono.error(() -> new IllegalArgumentException("error.not_file")); } } diff --git a/jetlinks-manager/network-manager/src/main/java/org/jetlinks/community/network/manager/web/DeviceGatewayController.java b/jetlinks-manager/network-manager/src/main/java/org/jetlinks/community/network/manager/web/DeviceGatewayController.java index dda516fa..cd1e59e5 100755 --- a/jetlinks-manager/network-manager/src/main/java/org/jetlinks/community/network/manager/web/DeviceGatewayController.java +++ b/jetlinks-manager/network-manager/src/main/java/org/jetlinks/community/network/manager/web/DeviceGatewayController.java @@ -7,24 +7,20 @@ import lombok.AllArgsConstructor; import lombok.Generated; import org.hswebframework.web.api.crud.entity.PagerResult; 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.authorization.annotation.SaveAction; +import org.hswebframework.web.authorization.annotation.*; import org.hswebframework.web.crud.web.reactive.ReactiveServiceCrudController; import org.hswebframework.web.i18n.LocaleUtils; -import org.jetlinks.community.network.manager.enums.DeviceGatewayState; -import org.jetlinks.core.ProtocolSupports; -import org.jetlinks.core.device.session.DeviceSessionInfo; -import org.jetlinks.core.device.session.DeviceSessionManager; import org.jetlinks.community.gateway.DeviceGateway; import org.jetlinks.community.gateway.DeviceGatewayManager; import org.jetlinks.community.network.manager.entity.DeviceGatewayEntity; -import org.jetlinks.community.network.manager.enums.NetworkConfigState; +import org.jetlinks.community.network.manager.enums.DeviceGatewayState; import org.jetlinks.community.network.manager.service.DeviceGatewayService; import org.jetlinks.community.network.manager.web.response.DeviceGatewayDetail; import org.jetlinks.community.network.manager.web.response.DeviceGatewayProviderInfo; import org.jetlinks.community.utils.ReactorUtils; +import org.jetlinks.core.ProtocolSupports; +import org.jetlinks.core.device.session.DeviceSessionInfo; +import org.jetlinks.core.device.session.DeviceSessionManager; import org.springframework.web.bind.annotation.*; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -42,9 +38,9 @@ import java.util.Comparator; public class DeviceGatewayController implements ReactiveServiceCrudController { private final DeviceGatewayService deviceGatewayService; - private final ProtocolSupports protocolSupports; private final DeviceGatewayManager gatewayManager; private final DeviceSessionManager sessionManager; + private final ProtocolSupports protocolSupports; @Override @Generated @@ -58,9 +54,9 @@ public class DeviceGatewayController implements ReactiveServiceCrudController startup(@PathVariable @Parameter(description = "网关ID") String id) { - return gatewayManager - .start(id) - .then(deviceGatewayService.updateState(id, DeviceGatewayState.enabled)) + return deviceGatewayService + .updateState(id, DeviceGatewayState.enabled) + .then(gatewayManager.start(id)) .then(); } @@ -167,7 +163,7 @@ public class DeviceGatewayController implements ReactiveServiceCrudController removeSession(@PathVariable String deviceId) { - return sessionManager.remove(deviceId,false); + return sessionManager.remove(deviceId, false); } diff --git a/jetlinks-manager/network-manager/src/main/java/org/jetlinks/community/network/manager/web/request/MqttMessageRequest.java b/jetlinks-manager/network-manager/src/main/java/org/jetlinks/community/network/manager/web/request/MqttMessageRequest.java index 6dbb2177..88e3f8d3 100644 --- a/jetlinks-manager/network-manager/src/main/java/org/jetlinks/community/network/manager/web/request/MqttMessageRequest.java +++ b/jetlinks-manager/network-manager/src/main/java/org/jetlinks/community/network/manager/web/request/MqttMessageRequest.java @@ -17,18 +17,25 @@ import org.jetlinks.rule.engine.executor.PayloadType; @Setter public class MqttMessageRequest { + @Generated private String topic; + @Generated private int qosLevel; + @Generated private Object data; + @Generated private int messageId; + @Generated private boolean will; + @Generated private boolean dup; + @Generated private boolean retain; public static MqttMessage of(MqttMessageRequest request, PayloadType type) { diff --git a/jetlinks-manager/network-manager/src/main/java/org/jetlinks/community/network/manager/web/response/DeviceGatewayDetail.java b/jetlinks-manager/network-manager/src/main/java/org/jetlinks/community/network/manager/web/response/DeviceGatewayDetail.java index 84d40fc1..c6ae608a 100644 --- a/jetlinks-manager/network-manager/src/main/java/org/jetlinks/community/network/manager/web/response/DeviceGatewayDetail.java +++ b/jetlinks-manager/network-manager/src/main/java/org/jetlinks/community/network/manager/web/response/DeviceGatewayDetail.java @@ -4,6 +4,7 @@ import io.swagger.v3.oas.annotations.media.Schema; import lombok.Getter; import lombok.Setter; import org.hswebframework.web.bean.FastBeanCopier; +import org.hswebframework.web.i18n.LocaleUtils; import org.jetlinks.core.ProtocolSupport; import org.jetlinks.core.message.codec.Transport; import org.jetlinks.community.gateway.supports.DeviceGatewayProvider; @@ -15,6 +16,8 @@ import org.jetlinks.community.protocol.ProtocolDetail; import org.jetlinks.community.protocol.TransportDetail; import reactor.core.publisher.Mono; +import java.util.Map; + @Getter @Setter public class DeviceGatewayDetail { @@ -67,18 +70,25 @@ public class DeviceGatewayDetail { @Schema(description = "传输协议详情") private TransportDetail transportDetail; + @Schema(description = "配置信息(根据类型不同而不同)") + private Map configuration; + public static DeviceGatewayDetail of(DeviceGatewayEntity entity) { return FastBeanCopier.copy(entity, new DeviceGatewayDetail()); } public Mono with(ProtocolSupport protocol) { - this.protocolDetail = new ProtocolDetail(protocol.getId(), protocol.getName(), protocol.getDescription(), null); - - return TransportDetail - .of(protocol, Transport.of(transport)) - .doOnNext(this::setTransportDetail) - .thenReturn(this) - ; + return Mono + .zip( + TransportDetail + .of(protocol, Transport.of(transport)) + .doOnNext(this::setTransportDetail), + ProtocolDetail + .of(protocol) + .doOnNext(this::setProtocolDetail) + ) + .as(LocaleUtils::transform) + .thenReturn(this); } public DeviceGatewayDetail with(ChannelInfo channelInfo) { diff --git a/jetlinks-manager/network-manager/src/main/java/org/jetlinks/community/network/manager/web/response/NetworkTypeInfo.java b/jetlinks-manager/network-manager/src/main/java/org/jetlinks/community/network/manager/web/response/NetworkTypeInfo.java index 1a6cd8bf..63d9d1a3 100644 --- a/jetlinks-manager/network-manager/src/main/java/org/jetlinks/community/network/manager/web/response/NetworkTypeInfo.java +++ b/jetlinks-manager/network-manager/src/main/java/org/jetlinks/community/network/manager/web/response/NetworkTypeInfo.java @@ -1,13 +1,11 @@ package org.jetlinks.community.network.manager.web.response; -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; +import lombok.*; import org.jetlinks.community.network.NetworkType; @Getter @Setter +@Generated @AllArgsConstructor @NoArgsConstructor public class NetworkTypeInfo { diff --git a/jetlinks-manager/network-manager/src/main/resources/i18n/network-manager/messages_en.properties b/jetlinks-manager/network-manager/src/main/resources/i18n/network-manager/messages_en.properties index 66b9d740..9fb1fb39 100644 --- a/jetlinks-manager/network-manager/src/main/resources/i18n/network-manager/messages_en.properties +++ b/jetlinks-manager/network-manager/src/main/resources/i18n/network-manager/messages_en.properties @@ -22,7 +22,7 @@ error.network_has_bean_use_by_gateway=Network components are already in use error.certificate_has_bean_use_by_network=Certificate are already in use error.unsupported_network_type=Unsupported network type:{0} - +#description device.gateway.provider.websocket-server.description=It is applicable to the scenario of bidirectional data transmission between the device end and the server end. The device is accessed through websocket device.gateway.provider.coap-server-gateway.description=It is suitable for low-power devices with limited resources. The device subscribes and publishes messages through topic device.gateway.provider.tcp-server-gateway.description=It is suitable for scenarios with high reliability. The device uses TCP protocol to connect to the service and transmit messages @@ -30,9 +30,50 @@ device.gateway.provider.mqtt-server-gateway.description=It is suitable for light device.gateway.provider.http-server-gateway.description=It is applicable to the scenario where the device reports messages using HTTP protocol device.gateway.provider.mqtt-client-gateway.description=It is applicable to the scenario that the equipment is not directly connected to the platform, but accessed through the third-party mqtt service device.gateway.provider.udp-device-gateway.description=It is suitable for scenes with high speed requirements. The device uses UDP protocol to connect to the service and transmit messages -device.gateway.provider.child-device-gateway.description=Connect sub-devices through other gateway devices. +device.gateway.provider.plugin_gateway.description=Access third-party system device data by calling SDK or HTTP requests +device.gateway.provider.collector-gateway.description=It is suitable for scenarios where devices perform data uplink and downlink through data acquisition channels +device.gateway.provider.media-plugin.description=It is suitable for video devices connected using plugins +device.gateway.provider.OneNet-platform.description=It is suitable for scenarios where data access is required through the HTTP push service of the new OneNet IoT open platform +device.gateway.provider.child-device.description=It is applicable to access sub-devices through other gateway devices +device.gateway.provider.Ctwing.description=Data access through HTTP push service of CTWing platform +device.gateway.provider.agent-media-device-gateway.description=Connect the media device to JetLinks through Agent software proxy +device.gateway.provider.agent-device-gateway.description=Connect the device to JetLinks through Agent software proxy + + +#name +device.gateway.provider.websocket-server.name=Websocket server access +device.gateway.provider.coap-server-gateway.name=CoAP access +device.gateway.provider.tcp-server-gateway.name=TCP transparent transmission access +device.gateway.provider.mqtt-server-gateway.name=MQTT direct connection access +device.gateway.provider.http-server-gateway.name=HTTP push access +device.gateway.provider.mqtt-client-gateway.name=MQTT broker access +device.gateway.provider.udp-device-gateway.name=UDP device access +device.gateway.provider.plugin_gateway.name=Plugin device access +device.gateway.provider.collector-gateway.name=Collector device access +device.gateway.provider.media-plugin.name=Media plugin access +device.gateway.provider.OneNet-platform.name=OneNet platform access (new) +device.gateway.provider.child-device.name=Gateway child device access +device.gateway.provider.gb28181-2016.name=GB/T28181 media access +device.gateway.provider.fixed-media.name=Media device-Fixed address +device.gateway.provider.onvif.name=Onvif Media access +device.gateway.provider.Ctwing.name=Ctwing device access +device.gateway.provider.agent-media-device-gateway.name=Agent media device access +device.gateway.provider.agent-device-gateway.name=Agent device access +device.gateway.provider.OneNet.name=OneNet platform access (old) + + error.cert_configs_can_not_be_null=Certificate configuration cannot be empty error.pem_key_can_not_be_empty=PEM private key cannot be empty error.pem_cert_can_not_be_empty=PEM certificate cannot be empty error.pem_trust_can_not_be_empty=PEM trust certificate cannot be empty +error.device_gateway_referenced=This device gateway has been referenced by the product and cannot be deleted +error.network_referenced=This network component has been referenced by the device gateway and cannot be deleted + +org.jetlinks.community.network.manager.enums.DeviceGatewayState.enabled=Enabled +org.jetlinks.community.network.manager.enums.DeviceGatewayState.paused=Paused +org.jetlinks.community.network.manager.enums.DeviceGatewayState.disabled=Disabled + +hswebframework.web.system.permission.network-config=Network Component Configuration +hswebframework.web.system.permission.network-simulator=Network Simulator +hswebframework.web.system.permission.certificate=Certificate Management \ No newline at end of file diff --git a/jetlinks-manager/network-manager/src/main/resources/i18n/network-manager/messages_zh.properties b/jetlinks-manager/network-manager/src/main/resources/i18n/network-manager/messages_zh.properties index 34d07420..e0ef497e 100644 --- a/jetlinks-manager/network-manager/src/main/resources/i18n/network-manager/messages_zh.properties +++ b/jetlinks-manager/network-manager/src/main/resources/i18n/network-manager/messages_zh.properties @@ -5,14 +5,18 @@ error.device_gateway_enabled=\u8BE5\u8BBE\u5907\u7F51\u5173\u5DF2\u542F\u7528 error.configuration_does_not_exist=\u914D\u7F6E{0}\u4E0D\u5B58\u5728 error.not_file=[file]\u90E8\u5206\u4E0D\u662F\u4E00\u4E2A\u6587\u4EF6 error.simulator_does_not_exist=\u6A21\u62DF\u5668\u4E0D\u5B58\u5728 -error.unsupported_device_gateway_provider=\u4e0d\u652f\u6301\u7684\u8bbe\u5907\u63a5\u5165\u65b9\u5f0f +error.unsupported_device_gateway_provider=\u4E0D\u652F\u6301\u7684\u8BBE\u5907\u63A5\u5165\u65B9\u5F0F #enum org.jetlinks.community.network.manager.enums.ConnectorState.running=\u8FD0\u884C\u4E2D org.jetlinks.community.network.manager.enums.ConnectorState.paused=\u5DF2\u6682\u505C org.jetlinks.community.network.manager.enums.ConnectorState.stopped=\u5DF2\u505C\u6B62 -org.jetlinks.community.network.manager.enums.NetworkConfigState.enabled=\u6b63\u5e38 +org.jetlinks.community.network.manager.enums.DeviceGatewayState.enabled=\u6B63\u5E38 +org.jetlinks.community.network.manager.enums.DeviceGatewayState.paused=\u5DF2\u6682\u505C +org.jetlinks.community.network.manager.enums.DeviceGatewayState.disabled=\u7981\u7528 + +org.jetlinks.community.network.manager.enums.NetworkConfigState.enabled=\u6B63\u5E38 org.jetlinks.community.network.manager.enums.NetworkConfigState.paused=\u5DF2\u6682\u505C org.jetlinks.community.network.manager.enums.NetworkConfigState.disabled=\u7981\u7528 @@ -22,17 +26,56 @@ error.network_has_bean_use_by_gateway=\u7F51\u7EDC\u7EC4\u4EF6\u6B63\u5728\u88AB error.certificate_has_bean_use_by_network=\u8BC1\u4E66\u6B63\u5728\u88AB\u4F7F\u7528 error.unsupported_network_type=\u4E0D\u652F\u6301\u7684\u7F51\u7EDC\u7EC4\u4EF6\u7C7B\u578B:{0} -device.gateway.provider.websocket-server.description=\u5c06\u8bbe\u5907\u8fde\u63a5\u5230\u5e73\u53f0\u5185\u7f6eWebsocket\u670d\u52a1\u3002 -device.gateway.provider.coap-server-gateway.description=\u5c06\u8bbe\u5907\u8fde\u63a5\u5230\u5e73\u53f0\u5185\u7f6eCoAP\u670d\u52a1\u3002 -device.gateway.provider.tcp-server-gateway.description=\u5c06\u8bbe\u5907\u8fde\u63a5\u5230\u5e73\u53f0\u5185\u7f6eTCP\u670d\u52a1\u3002 -device.gateway.provider.mqtt-server-gateway.description=\u5c06\u8bbe\u5907\u8fde\u63a5\u5230\u5e73\u53f0\u5185\u7f6eMQTT\u670d\u52a1\u3002 -device.gateway.provider.http-server-gateway.description=\u5c06\u8bbe\u5907\u8fde\u63a5\u5230\u5e73\u53f0\u5185\u7f6eHTTP\u670d\u52a1\u3002 -device.gateway.provider.mqtt-client-gateway.description=\u5c06\u8bbe\u5907\u8fde\u63a5\u5230\u7b2c\u4e09\u65b9MQTT\u4ee3\u7406\u670d\u52a1\uff0c\u5e73\u53f0\u4ece\u7b2c\u4e09\u65b9\u8ba2\u9605\u8bbe\u5907\u6570\u636e\u3002 -device.gateway.provider.udp-device-gateway.description=\u5c06\u8bbe\u5907\u8fde\u63a5\u5230\u5e73\u53f0\u5185\u7f6eUDP\u670d\u52a1\uff0c\u9002\u7528\u4e8e\u5bf9\u4f20\u8f93\u901f\u5ea6\u6709\u9ad8\u8981\u6c42\u7684\u573a\u666f\u3002 -device.gateway.provider.child-device.description=\u9700\u8981\u901a\u8fc7\u7f51\u5173\u4e0e\u5e73\u53f0\u8fdb\u884c\u6570\u636e\u901a\u4fe1\u7684\u8bbe\u5907\uff0c\u5c06\u4f5c\u4e3a\u7f51\u5173\u5b50\u8bbe\u5907\u63a5\u5165\u5230\u5e73\u53f0\u3002 -device.gateway.provider.plugin_gateway.description=\u8c03\u7528SDK\u6216HTTP\u8bf7\u6c42\u5c06\u7b2c\u4e09\u65b9\u7cfb\u7edf\u8bbe\u5907\u6570\u636e\u63a5\u5165\u5230\u5e73\u53f0 +device.gateway.provider.websocket-server.description=\u5C06\u8BBE\u5907\u8FDE\u63A5\u5230\u5E73\u53F0\u5185\u7F6EWebsocket\u670D\u52A1\u3002 +device.gateway.provider.coap-server-gateway.description=\u5C06\u8BBE\u5907\u8FDE\u63A5\u5230\u5E73\u53F0\u5185\u7F6ECoAP\u670D\u52A1\u3002 +device.gateway.provider.tcp-server-gateway.description=\u5C06\u8BBE\u5907\u8FDE\u63A5\u5230\u5E73\u53F0\u5185\u7F6ETCP\u670D\u52A1\u3002 +device.gateway.provider.mqtt-server-gateway.description=\u5C06\u8BBE\u5907\u8FDE\u63A5\u5230\u5E73\u53F0\u5185\u7F6EMQTT\u670D\u52A1\u3002 +device.gateway.provider.http-server-gateway.description=\u5C06\u8BBE\u5907\u8FDE\u63A5\u5230\u5E73\u53F0\u5185\u7F6EHTTP\u670D\u52A1\u3002 +device.gateway.provider.mqtt-client-gateway.description=\u5C06\u8BBE\u5907\u8FDE\u63A5\u5230\u7B2C\u4E09\u65B9MQTT\u4EE3\u7406\u670D\u52A1\uFF0C\u5E73\u53F0\u4ECE\u7B2C\u4E09\u65B9\u8BA2\u9605\u8BBE\u5907\u6570\u636E\u3002 +device.gateway.provider.udp-device-gateway.description=\u5C06\u8BBE\u5907\u8FDE\u63A5\u5230\u5E73\u53F0\u5185\u7F6EUDP\u670D\u52A1\uFF0C\u9002\u7528\u4E8E\u5BF9\u4F20\u8F93\u901F\u5EA6\u6709\u9AD8\u8981\u6C42\u7684\u573A\u666F\u3002 +device.gateway.provider.plugin_gateway.description=\u8C03\u7528SDK\u6216HTTP\u8BF7\u6C42\u5C06\u7B2C\u4E09\u65B9\u7CFB\u7EDF\u8BBE\u5907\u6570\u636E\u63A5\u5165\u5230\u5E73\u53F0 +device.gateway.provider.collector-gateway.description=\u9002\u7528\u4E8E\u8BBE\u5907\u901A\u8FC7\u6570\u91C7\u901A\u9053\u8FDB\u884C\u6570\u636E\u4E0A\u4E0B\u884C\u7684\u573A\u666F +device.gateway.provider.media-plugin.description=\u9002\u7528\u4E8E\u4F7F\u7528\u63D2\u4EF6\u63A5\u5165\u7684\u89C6\u9891\u8BBE\u5907\u3002 +device.gateway.provider.OneNet-platform.description=\u9002\u7528\u4E8E\u9700\u8981\u901A\u8FC7\u65B0\u7248OneNet\u7269\u8054\u7F51\u5F00\u653E\u5E73\u53F0\u7684http\u63A8\u9001\u670D\u52A1\u8FDB\u884C\u6570\u636E\u63A5\u5165\u7684\u573A\u666F +device.gateway.provider.child-device.description=\u901A\u8FC7\u5176\u4ED6\u7F51\u5173\u8BBE\u5907\u6765\u63A5\u5165\u5B50\u8BBE\u5907 +device.gateway.provider.gb28181-2016.description=\u9002\u7528\u4E8E\u652F\u6301GB/T 28181 \u56FD\u6807\u534F\u8BAE\u7684\u89C6\u9891\u8BBE\u5907\u63A5\u5165\u3002 +device.gateway.provider.fixed-media.description=\u9002\u7528\u4E8E\u4F7F\u7528rtsp\u6216rtmp\u56FA\u5B9A\u5730\u5740\u63A5\u5165\u7684\u89C6\u9891\u8BBE\u5907\u3002 +device.gateway.provider.onvif.description=\u9002\u7528\u4E8E\u652F\u6301onvif\u534F\u8BAE\u7684\u89C6\u9891\u8BBE\u5907\u3002 +device.gateway.provider.Ctwing.description=\u9002\u7528\u4E8E\u9700\u8981\u901A\u8FC7CTWing\u5E73\u53F0\u7684http\u63A8\u9001\u670D\u52A1\u8FDB\u884C\u6570\u636E\u63A5\u5165\u7684\u573A\u666F\u3002 +device.gateway.provider.agent-media-device-gateway.description=\u5C06\u89C6\u9891\u8BBE\u5907\u901A\u8FC7Agent\u8F6F\u4EF6\u4EE3\u7406\u63A5\u5165\u5230\u5E73\u53F0 +device.gateway.provider.agent-device-gateway.description=\u5C06\u8BBE\u5907\u901A\u8FC7Agent\u8F6F\u4EF6\u4EE3\u7406\u63A5\u5165\u5230\u5E73\u53F0 +device.gateway.provider.OneNet.description=\u9002\u7528\u4E8E\u9700\u8981\u901A\u8FC7\u65E7\u7248OneNet\u5E73\u53F0\u7684http\u63A8\u9001\u670D\u52A1\u8FDB\u884C\u6570\u636E\u63A5\u5165\u7684\u573A\u666F\u3002 + +#name +device.gateway.provider.websocket-server.name=Websocket\u670D\u52A1\u63A5\u5165 +device.gateway.provider.coap-server-gateway.name=CoAP\u63A5\u5165 +device.gateway.provider.tcp-server-gateway.name=TCP\u900F\u4F20\u63A5\u5165 +device.gateway.provider.mqtt-server-gateway.name=MQTT\u76F4\u8FDE\u63A5\u5165 +device.gateway.provider.http-server-gateway.name=HTTP\u63A8\u9001\u63A5\u5165 +device.gateway.provider.mqtt-client-gateway.name=MQTT Broker\u63A5\u5165 +device.gateway.provider.udp-device-gateway.name=UDP\u63A5\u5165 +device.gateway.provider.plugin_gateway.name=\u63D2\u4EF6\u8BBE\u5907\u63A5\u5165 +device.gateway.provider.collector-gateway.name=\u6570\u91C7\u8BBE\u5907\u63A5\u5165 +device.gateway.provider.media-plugin.name=\u63D2\u4EF6\u89C6\u9891\u63A5\u5165 +device.gateway.provider.OneNet-platform.name=OneNet\u7269\u8054\u7F51\u5F00\u653E\u5E73\u53F0\u8BBE\u5907\u63A5\u5165\uFF08\u65B0\uFF09 +device.gateway.provider.child-device.name=\u7F51\u5173\u5B50\u8BBE\u5907\u63A5\u5165 +device.gateway.provider.gb28181-2016.name=GB/T28181\u56FD\u6807\u89C6\u9891\u63A5\u5165 +device.gateway.provider.fixed-media.name=\u89C6\u9891\u8BBE\u5907-\u56FA\u5B9A\u5730\u5740 +device.gateway.provider.onvif.name=onvif\u89C6\u9891\u63A5\u5165 +device.gateway.provider.Ctwing.name=Ctwing\u8BBE\u5907\u63A5\u5165 +device.gateway.provider.agent-media-device-gateway.name=Agent\u89C6\u9891\u8BBE\u5907\u63A5\u5165\u7F51\u5173 +device.gateway.provider.agent-device-gateway.name=Agent\u8BBE\u5907\u63A5\u5165\u7F51\u5173 +device.gateway.provider.OneNet.name=OneNet\u8BBE\u5907\u63A5\u5165\uFF08\u65E7\uFF09 + + error.cert_configs_can_not_be_null=\u8BC1\u4E66\u914D\u7F6E\u4E0D\u80FD\u4E3A\u7A7A error.pem_key_can_not_be_empty=PEM\u79C1\u94A5\u4E0D\u80FD\u4E3A\u7A7A error.pem_cert_can_not_be_empty=PEM\u8BC1\u4E66\u4E0D\u80FD\u4E3A\u7A7A error.pem_trust_can_not_be_empty=PEM\u4FE1\u4EFB\u8BC1\u4E66\u4E0D\u80FD\u4E3A\u7A7A +error.device_gateway_referenced=\u8BE5\u8BBE\u5907\u63A5\u5165\u7F51\u5173\u5DF2\u88AB\u4EA7\u54C1\u5F15\u7528\uFF0C\u4E0D\u53EF\u5220\u9664\u3002 +error.network_referenced=\u8BE5\u7F51\u7EDC\u7EC4\u4EF6\u5DF2\u88AB\u8BBE\u5907\u63A5\u5165\u7F51\u5173\u5F15\u7528\uFF0C\u4E0D\u53EF\u5220\u9664 + +hswebframework.web.system.permission.network-config=\u7F51\u7EDC\u7EC4\u4EF6\u914D\u7F6E +hswebframework.web.system.permission.network-simulator=\u7F51\u7EDC\u6A21\u62DF\u5668 +hswebframework.web.system.permission.certificate=\u8BC1\u4E66\u7BA1\u7406 \ No newline at end of file diff --git a/jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/configuration/NotificationProperties.java b/jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/configuration/NotificationProperties.java new file mode 100644 index 00000000..64194e0d --- /dev/null +++ b/jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/configuration/NotificationProperties.java @@ -0,0 +1,22 @@ +package org.jetlinks.community.notify.manager.configuration; + +import lombok.Getter; +import lombok.Setter; +import org.jetlinks.community.buffer.BufferProperties; +import org.springframework.boot.context.properties.ConfigurationProperties; + +import java.time.Duration; + +@ConfigurationProperties(prefix = "jetlinks.notification") +@Getter +@Setter +public class NotificationProperties { + + private BufferProperties buffer = new BufferProperties(); + + public NotificationProperties(){ + buffer.setFilePath("./data/notification-buffer"); + buffer.setSize(1000); + buffer.setTimeout(Duration.ofSeconds(1)); + } +} diff --git a/jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/configuration/NotifyConfiguration.java b/jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/configuration/NotifyConfiguration.java index c6d850c8..6e657c0b 100644 --- a/jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/configuration/NotifyConfiguration.java +++ b/jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/configuration/NotifyConfiguration.java @@ -10,7 +10,7 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @AutoConfiguration -@EnableConfigurationProperties(NotifySubscriberProperties.class) +@EnableConfigurationProperties({NotifySubscriberProperties.class, NotificationProperties.class}) public class NotifyConfiguration { @Bean diff --git a/jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/configuration/NotifySubscriberProperties.java b/jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/configuration/NotifySubscriberProperties.java index 666cd916..2cf7ccc9 100644 --- a/jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/configuration/NotifySubscriberProperties.java +++ b/jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/configuration/NotifySubscriberProperties.java @@ -4,8 +4,6 @@ import lombok.Getter; import lombok.Setter; import org.hswebframework.web.authorization.Authentication; import org.springframework.boot.context.properties.ConfigurationProperties; -import org.springframework.context.annotation.Configuration; - import java.util.Collections; import java.util.HashSet; import java.util.Set; diff --git a/jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/configuration/SubscriptionConfiguration.java b/jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/configuration/SubscriptionConfiguration.java new file mode 100644 index 00000000..6c5b61ff --- /dev/null +++ b/jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/configuration/SubscriptionConfiguration.java @@ -0,0 +1,23 @@ +package org.jetlinks.community.notify.manager.configuration; + +import org.jetlinks.community.notify.manager.subscriber.SubscriberProvider; +import org.jetlinks.community.notify.manager.subscriber.SubscriberProviders; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.context.ApplicationContextAware; +import org.springframework.context.annotation.Bean; + +@AutoConfiguration +public class SubscriptionConfiguration { + + @Bean + public ApplicationContextAware subscriberAutoRegister(ObjectProvider providers) { + + return applicationContext -> { + applicationContext.getBeanProvider(SubscriberProvider.class) + .forEach(SubscriberProviders::register); + }; + } + + +} diff --git a/jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/entity/Notification.java b/jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/entity/Notification.java index 9df48de7..4f44b289 100644 --- a/jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/entity/Notification.java +++ b/jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/entity/Notification.java @@ -54,7 +54,7 @@ public class Notification implements Serializable { public Notification copyWithMessage(Notify message) { Notification target = FastBeanCopier.copy(this, new Notification()); - target.setId(IDGenerator.SNOW_FLAKE_STRING.generate()); + target.setId(IDGenerator.RANDOM.generate()); target.setMessage(message.getMessage()); target.setDataId(message.getDataId()); target.setNotifyTime(message.getNotifyTime()); diff --git a/jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/entity/NotificationEntity.java b/jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/entity/NotificationEntity.java index 62cc0375..499c8394 100644 --- a/jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/entity/NotificationEntity.java +++ b/jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/entity/NotificationEntity.java @@ -6,6 +6,7 @@ import lombok.Getter; import lombok.Setter; import lombok.SneakyThrows; import org.hswebframework.ezorm.rdb.mapping.annotation.ColumnType; +import org.hswebframework.ezorm.rdb.mapping.annotation.Comment; import org.hswebframework.ezorm.rdb.mapping.annotation.DefaultValue; import org.hswebframework.ezorm.rdb.mapping.annotation.EnumCodec; import org.hswebframework.web.api.crud.entity.GenericEntity; @@ -24,17 +25,21 @@ import java.sql.JDBCType; indexes = @Index( name = "idx_ntfc_subscribe", columnList = "subscriber_type,subscriber" )) +@Comment("消息通知信息表") public class NotificationEntity extends GenericEntity { private static final long serialVersionUID = -1L; + @Schema(description = "订阅者ID") @Column(length = 64, nullable = false, updatable = false) @Hidden private String subscribeId; + @Schema(description = "订阅类型") @Column(length = 32, nullable = false, updatable = false) @Hidden private String subscriberType; + @Schema(description = "订阅者") @Column(length = 64, nullable = false, updatable = false) @Hidden private String subscriber; @@ -69,7 +74,6 @@ public class NotificationEntity extends GenericEntity { @ColumnType(jdbcType = JDBCType.CLOB, javaType = String.class) private String detailJson; - @Column(length = 32) @EnumCodec @DefaultValue("unread") diff --git a/jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/entity/NotifyHistoryEntity.java b/jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/entity/NotifyHistoryEntity.java index 70917380..1727e3a8 100644 --- a/jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/entity/NotifyHistoryEntity.java +++ b/jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/entity/NotifyHistoryEntity.java @@ -4,10 +4,7 @@ import com.alibaba.fastjson.JSON; 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.ezorm.rdb.mapping.annotation.*; import org.hswebframework.web.api.crud.entity.GenericEntity; import org.hswebframework.web.bean.FastBeanCopier; import org.hswebframework.web.crud.generator.Generators; @@ -23,15 +20,15 @@ import java.util.Date; import java.util.Map; @Table(name = "notify_history", indexes = { - @Index(name = "idx_nt_his_notifier_id", columnList = "notifier_id") + @Index(name = "idx_nt_his_notifier_id", columnList = "notifier_id,notify_time desc") }) +@Comment("消息通知记录表") @Getter @Setter public class NotifyHistoryEntity extends GenericEntity { private static final long serialVersionUID = -6849794470754667710L; - @Column(length = 32, nullable = false, updatable = false) @Schema(description = "通知ID") private String notifierId; @@ -87,7 +84,7 @@ public class NotifyHistoryEntity extends GenericEntity { private Integer retryTimes; public static NotifyHistoryEntity of(SerializableNotifierEvent event) { - NotifyHistoryEntity entity = FastBeanCopier.copy(event, new NotifyHistoryEntity()); + NotifyHistoryEntity entity = FastBeanCopier.copy(event, new NotifyHistoryEntity()); if (null != event.getTemplate()) { entity.setTemplate(JSON.toJSONString(event.getTemplate())); } @@ -103,4 +100,5 @@ public class NotifyHistoryEntity extends GenericEntity { public NotifyHistory toHistory(){ return FastBeanCopier.copy(this,new NotifyHistory()); } + } diff --git a/jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/entity/NotifySubscriberChannelEntity.java b/jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/entity/NotifySubscriberChannelEntity.java index efce2dcf..aec5fa93 100644 --- a/jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/entity/NotifySubscriberChannelEntity.java +++ b/jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/entity/NotifySubscriberChannelEntity.java @@ -10,9 +10,11 @@ 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.i18n.MultipleI18nSupportEntity; import org.hswebframework.web.validator.CreateGroup; import org.jetlinks.community.authorize.AuthenticationSpec; import org.jetlinks.community.notify.manager.enums.NotifyChannelState; +import org.jetlinks.community.notify.manager.subscriber.channel.NotifyChannelProvider; import javax.persistence.Column; import javax.persistence.Table; @@ -34,8 +36,11 @@ import java.util.Map; @Table(name = "notify_subscriber_channel") @Schema(description = "通知订阅通道") @EnableEntityEvent -public class NotifySubscriberChannelEntity extends GenericEntity implements RecordCreationEntity { +public class NotifySubscriberChannelEntity extends GenericEntity implements RecordCreationEntity, MultipleI18nSupportEntity { + /** + * @see NotifySubscriberProviderEntity#getId() + */ @Column(nullable = false, length = 64, updatable = false) @NotBlank(groups = CreateGroup.class) @Schema(description = "主题提供商标识") @@ -46,17 +51,24 @@ public class NotifySubscriberChannelEntity extends GenericEntity impleme @Schema(description = "名称") private String name; - @Column(nullable = false, length = 32) - @NotBlank(groups = CreateGroup.class) - @Schema(description = "通知类型") - private String channelProvider; - @Column @JsonCodec @ColumnType(jdbcType = JDBCType.LONGVARCHAR, javaType = String.class) @Schema(description = "权限范围") private AuthenticationSpec grant; + /** + * @see NotifyChannelProvider#getId() + */ + @Column(nullable = false, length = 32) + @NotBlank(groups = CreateGroup.class) + @Schema(description = "通知类型") + private String channelProvider; + + /** + * @see NotifyChannelProvider#createChannel(Map) + * @see org.jetlinks.community.notify.manager.subscriber.channel.notifiers.NotifierChannelProvider.NotifyChannelConfig + */ @Column @JsonCodec @ColumnType(jdbcType = JDBCType.LONGVARCHAR, javaType = String.class) @@ -77,4 +89,14 @@ public class NotifySubscriberChannelEntity extends GenericEntity impleme @Column(length = 64, updatable = false) @Schema(description = "创建时间", accessMode = Schema.AccessMode.READ_ONLY) private Long createTime; + + @Schema(title = "国际化信息定义") + @Column + @JsonCodec + @ColumnType(jdbcType = JDBCType.LONGVARCHAR, javaType = String.class) + private Map> i18nMessages; + + public String getI18nName() { + return getI18nMessage("name", name); + } } diff --git a/jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/entity/NotifySubscriberEntity.java b/jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/entity/NotifySubscriberEntity.java index e53b277c..4b187dcb 100644 --- a/jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/entity/NotifySubscriberEntity.java +++ b/jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/entity/NotifySubscriberEntity.java @@ -4,10 +4,12 @@ import io.swagger.v3.oas.annotations.Hidden; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Getter; import lombok.Setter; +import org.apache.commons.codec.digest.DigestUtils; import org.hswebframework.ezorm.rdb.mapping.annotation.*; import org.hswebframework.web.api.crud.entity.GenericEntity; import org.hswebframework.web.crud.annotation.EnableEntityEvent; import org.jetlinks.community.notify.manager.enums.SubscribeState; +import org.springframework.util.StringUtils; import javax.persistence.Column; import javax.persistence.Index; @@ -32,36 +34,35 @@ public class NotifySubscriberEntity extends GenericEntity { private static final long serialVersionUID = -1L; - @Comment("订阅者类型,如:user") + @Schema(description = "订阅者类型,如:user") @Column(length = 32, nullable = false, updatable = false) @Hidden private String subscriberType; - @Comment("订阅者ID") + @Schema(description = "订阅者ID") @Column(length = 32, nullable = false, updatable = false) @Hidden private String subscriber; - @Comment("主题提供商标识,如:device_alarm") @Column(length = 32, nullable = false, updatable = false) @Schema(description = "主题标识,如:device_alarm") private String topicProvider; + /** + * @see NotifySubscriberProviderEntity#getId() + */ @Column(length = 64) @Schema(description = "订阅提供商ID") private String providerId; - @Comment("订阅名称") @Column(length = 64, nullable = false) @Schema(description = "订阅名称") private String subscribeName; - @Comment("主题名称,如:设备告警") @Column(length = 64, nullable = false) @Schema(description = "主题名称") private String topicName; - @Comment("主题订阅配置") @Column(length = 3000) @JsonCodec @ColumnType(javaType = String.class) @@ -73,19 +74,17 @@ public class NotifySubscriberEntity extends GenericEntity { @Schema(description = "说明") private String description; - @Comment("状态:enabled,disabled") @Column(length = 32) @EnumCodec @ColumnType(javaType = String.class) @DefaultValue("enabled") - @Schema(description = "状态.") + @Schema(description = "订阅状态") private SubscribeState state; @Column(length = 32) @Schema(description = "订阅语言") private String locale; - /** * @see NotifySubscriberChannelEntity#getId() */ @@ -95,8 +94,18 @@ public class NotifySubscriberEntity extends GenericEntity { @ColumnType(javaType = String.class) private List notifyChannels; + public String generateId() { + if (super.getId() == null + && StringUtils.hasText(subscriberType) + && StringUtils.hasText(subscriber) + && StringUtils.hasText(topicProvider)) { + return DigestUtils.md5Hex(String.join(":", subscriberType, subscriber, topicProvider)); + } + return null; + } public Locale toLocale() { return locale == null ? Locale.getDefault() : Locale.forLanguageTag(locale); } + } diff --git a/jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/entity/NotifySubscriberProviderEntity.java b/jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/entity/NotifySubscriberProviderEntity.java index 071e50a1..62e8f68f 100644 --- a/jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/entity/NotifySubscriberProviderEntity.java +++ b/jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/entity/NotifySubscriberProviderEntity.java @@ -9,6 +9,7 @@ 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.authorization.Authentication; import org.hswebframework.web.crud.annotation.EnableEntityEvent; import org.hswebframework.web.validator.CreateGroup; import org.jetlinks.community.authorize.AuthenticationSpec; @@ -20,7 +21,13 @@ import javax.validation.constraints.NotBlank; import java.sql.JDBCType; import java.util.Map; - +/** + * 通知订阅提供商,用于定义用户支持订阅何种通知. + * + * @author zhouhao + * @see org.jetlinks.pro.notify.subscription.SubscriberProvider + * @since 2.0 + */ @Getter @Setter @Table(name = "notify_subscriber_provider") @@ -33,11 +40,17 @@ public class NotifySubscriberProviderEntity extends GenericEntity implem @Schema(description = "名称") private String name; + /** + * @see org.jetlinks.pro.notify.subscription.SubscriberProvider#getId() + */ @Column(length = 64, nullable = false, updatable = false) @Schema(description = "订阅提供商ID", accessMode = Schema.AccessMode.READ_ONLY) @NotBlank(groups = CreateGroup.class) private String provider; + /** + * @see org.jetlinks.pro.notify.subscription.SubscriberProvider#createSubscriber(String, Authentication, Map) + */ @Column @JsonCodec @ColumnType(jdbcType = JDBCType.LONGVARCHAR, javaType = String.class) diff --git a/jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/enums/NotificationState.java b/jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/enums/NotificationState.java index a0220e08..5852045a 100644 --- a/jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/enums/NotificationState.java +++ b/jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/enums/NotificationState.java @@ -2,11 +2,11 @@ package org.jetlinks.community.notify.manager.enums; import lombok.AllArgsConstructor; import lombok.Getter; -import org.hswebframework.web.dict.EnumDict; +import org.hswebframework.web.dict.I18nEnumDict; @Getter @AllArgsConstructor -public enum NotificationState implements EnumDict { +public enum NotificationState implements I18nEnumDict { unread("未读"), read("已读"); diff --git a/jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/enums/NotifyState.java b/jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/enums/NotifyState.java index 36b32506..cd4fce48 100644 --- a/jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/enums/NotifyState.java +++ b/jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/enums/NotifyState.java @@ -2,14 +2,16 @@ package org.jetlinks.community.notify.manager.enums; import lombok.AllArgsConstructor; import lombok.Getter; -import org.hswebframework.web.dict.EnumDict; +import org.hswebframework.web.dict.I18nEnumDict; @Getter @AllArgsConstructor -public enum NotifyState implements EnumDict { +public enum NotifyState implements I18nEnumDict { success("成功"), - error("失败"); + retrying("重试中"), + error("失败"), + cancel("已取消"); private final String text; diff --git a/jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/message/NotificationsPublishProvider.java b/jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/message/NotificationsPublishProvider.java index c4200e3b..e0026dd5 100644 --- a/jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/message/NotificationsPublishProvider.java +++ b/jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/message/NotificationsPublishProvider.java @@ -37,7 +37,7 @@ public class NotificationsPublishProvider implements SubscriptionProvider { .subscribe(Subscription.of( "notifications-publisher", "/notifications/user/" + request.getAuthentication().getUser().getId() + "/*/*", - Subscription.Feature.local, Subscription.Feature.broker + Subscription.Feature.local, Subscription.Feature.broker, Subscription.Feature.safetySerialization )) .map(msg -> Message.success(request.getId(), msg.getTopic(), msg.bodyToJson(true))); } diff --git a/jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/service/DefaultNotifyConfigManager.java b/jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/service/DefaultNotifyConfigManager.java index 37b6158e..1873187f 100644 --- a/jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/service/DefaultNotifyConfigManager.java +++ b/jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/service/DefaultNotifyConfigManager.java @@ -20,6 +20,6 @@ public class DefaultNotifyConfigManager implements NotifyConfigManager { @Override public Mono getNotifyConfig(@Nonnull NotifyType notifyType, @Nonnull String configId) { return configService.findById(configId) - .map(NotifyConfigEntity::toProperties); + .map(NotifyConfigEntity::toProperties); } } diff --git a/jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/service/DefaultTemplateManager.java b/jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/service/DefaultTemplateManager.java index 1cc141b7..f9db4f6e 100644 --- a/jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/service/DefaultTemplateManager.java +++ b/jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/service/DefaultTemplateManager.java @@ -8,7 +8,6 @@ import org.jetlinks.community.notify.template.TemplateProperties; import org.jetlinks.community.notify.template.TemplateProvider; import org.jetlinks.core.event.EventBus; import org.springframework.beans.BeansException; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.config.BeanPostProcessor; import org.springframework.stereotype.Service; import reactor.core.publisher.Mono; @@ -19,17 +18,16 @@ public class DefaultTemplateManager extends AbstractTemplateManager implements B private final NotifyTemplateService templateService; - public DefaultTemplateManager(NotifyTemplateService templateService) { + public DefaultTemplateManager(EventBus eventBus, NotifyTemplateService templateService) { + super(eventBus); this.templateService = templateService; } @Override protected Mono getProperties(NotifyType type, String id) { - return templateService - .findById(Mono.just(id)) - .map(NotifyTemplateEntity::toTemplateProperties); + return templateService.findById(Mono.just(id)) + .map(NotifyTemplateEntity::toTemplateProperties); } - @Override public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { if (bean instanceof TemplateProvider) { @@ -37,4 +35,5 @@ public class DefaultTemplateManager extends AbstractTemplateManager implements B } return bean; } + } diff --git a/jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/service/InDatabaseNotifyHistoryRepository.java b/jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/service/InDatabaseNotifyHistoryRepository.java index 85631b60..c196ae14 100644 --- a/jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/service/InDatabaseNotifyHistoryRepository.java +++ b/jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/service/InDatabaseNotifyHistoryRepository.java @@ -20,7 +20,7 @@ public class InDatabaseNotifyHistoryRepository implements NotifyHistoryRepositor public Mono handleNotify(SerializableNotifierEvent event) { return historyService - .insert(Mono.just(NotifyHistoryEntity.of(event))) + .save(NotifyHistoryEntity.of(event)) .then(); } diff --git a/jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/service/NotificationService.java b/jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/service/NotificationService.java index e8c20416..f542065a 100755 --- a/jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/service/NotificationService.java +++ b/jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/service/NotificationService.java @@ -1,21 +1,20 @@ package org.jetlinks.community.notify.manager.service; -import lombok.Getter; -import lombok.Setter; import lombok.extern.slf4j.Slf4j; import org.apache.commons.collections4.CollectionUtils; import org.hswebframework.web.api.crud.entity.QueryParamEntity; import org.hswebframework.web.crud.service.GenericReactiveCrudService; -import org.jetlinks.core.utils.Reactors; -import org.jetlinks.community.buffer.BufferProperties; import org.jetlinks.community.buffer.BufferSettings; import org.jetlinks.community.buffer.PersistenceBuffer; import org.jetlinks.community.gateway.annotation.Subscribe; +import org.jetlinks.community.notify.manager.configuration.NotificationProperties; import org.jetlinks.community.notify.manager.entity.Notification; import org.jetlinks.community.notify.manager.entity.NotificationEntity; import org.jetlinks.community.notify.manager.enums.NotificationState; import org.jetlinks.community.utils.ErrorUtils; -import org.springframework.boot.context.properties.ConfigurationProperties; +import org.jetlinks.core.utils.Reactors; +import org.springframework.boot.CommandLineRunner; +import org.springframework.boot.SpringApplication; import org.springframework.dao.NonTransientDataAccessException; import org.springframework.stereotype.Service; import org.springframework.transaction.CannotCreateTransactionException; @@ -25,32 +24,18 @@ import reactor.core.publisher.Mono; import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; import java.io.IOException; -import java.time.Duration; import java.util.concurrent.TimeoutException; import java.util.stream.Collectors; @Service @Slf4j -@ConfigurationProperties(prefix = "jetlinks.notification") -public class NotificationService extends GenericReactiveCrudService { +public class NotificationService extends GenericReactiveCrudService implements CommandLineRunner { - @Getter - @Setter - private BufferProperties buffer = new BufferProperties(); - - private PersistenceBuffer writer; - - public NotificationService() { - buffer.setFilePath("./data/notification-buffer"); - buffer.setSize(1000); - buffer.setTimeout(Duration.ofSeconds(1)); - } - - @PostConstruct - public void init() { + private final PersistenceBuffer writer; + public NotificationService(NotificationProperties properties) { writer = new PersistenceBuffer<>( - BufferSettings.create(buffer), + BufferSettings.create(properties.getBuffer()), NotificationEntity::new, flux -> this.save(flux).then(Reactors.ALWAYS_FALSE)) .retryWhenError(err -> ErrorUtils.hasException( @@ -60,39 +45,51 @@ public class NotificationService extends GenericReactiveCrudService subscribeNotifications(Notification notification) { - writer.write(NotificationEntity.from(notification)); - return Mono.empty(); + return writer.writeAsync(NotificationEntity.from(notification)); } + public Flux findAndMarkRead(QueryParamEntity query) { return this .query(query) - .collectList() + .buffer(200) .filter(CollectionUtils::isNotEmpty) - .flatMapMany(list -> this - .createUpdate() - .set(NotificationEntity::getState, NotificationState.read) - .where() - .in(NotificationEntity::getId, list - .stream() - .map(NotificationEntity::getId) - .collect(Collectors.toList())) - .and(NotificationEntity::getState, NotificationState.unread) - .execute() - .thenMany(Flux.fromIterable(list)) - .doOnNext(e -> e.setState(NotificationState.read))); + .flatMap(list -> this + .createUpdate() + .set(NotificationEntity::getState, NotificationState.read) + .where() + .in(NotificationEntity::getId, list + .stream() + .map(NotificationEntity::getId) + .collect(Collectors.toList())) + .and(NotificationEntity::getState, NotificationState.unread) + .execute() + .thenMany(Flux.fromIterable(list)) + .doOnNext(e -> e.setState(NotificationState.read)), + 8, + 8); } + @Override + public void run(String... args) throws Exception { + writer.start(); + SpringApplication + .getShutdownHandlers() + .add(writer::dispose); + } } diff --git a/jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/service/NotifierCacheManager.java b/jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/service/NotifierCacheManager.java index 74d494c6..cf97bd18 100644 --- a/jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/service/NotifierCacheManager.java +++ b/jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/service/NotifierCacheManager.java @@ -71,18 +71,18 @@ public class NotifierCacheManager { protected Mono reloadConfig(List entities) { return Flux.fromIterable(entities) - .map(NotifyConfigEntity::getId) - .doOnNext(id -> log.info("clear notifier config [{}] cache", id)) - .flatMap(notifierManager::reload) - .then(); + .map(NotifyConfigEntity::getId) + .doOnNext(id -> log.info("clear notifier config [{}] cache", id)) + .flatMap(notifierManager::reload) + .then(); } protected Mono reloadTemplate(List entities) { return Flux.fromIterable(entities) - .map(NotifyTemplateEntity::getId) - .doOnNext(id -> log.info("clear template [{}] cache", id)) - .flatMap(templateManager::reload) - .then(); + .map(NotifyTemplateEntity::getId) + .doOnNext(id -> log.info("clear template [{}] cache", id)) + .flatMap(templateManager::reload) + .then(); } } diff --git a/jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/service/NotifyConfigService.java b/jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/service/NotifyConfigService.java index cb22601a..54d9bca7 100644 --- a/jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/service/NotifyConfigService.java +++ b/jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/service/NotifyConfigService.java @@ -1,5 +1,6 @@ package org.jetlinks.community.notify.manager.service; +import lombok.Generated; import org.hswebframework.web.crud.service.GenericReactiveCacheSupportCrudService; import org.jetlinks.community.notify.manager.entity.NotifyConfigEntity; import org.springframework.stereotype.Service; @@ -8,6 +9,7 @@ import org.springframework.stereotype.Service; public class NotifyConfigService extends GenericReactiveCacheSupportCrudService { @Override + @Generated public String getCacheName() { return "notify_config"; } diff --git a/jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/service/NotifyHistory.java b/jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/service/NotifyHistory.java index aa26a7b4..9326f167 100644 --- a/jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/service/NotifyHistory.java +++ b/jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/service/NotifyHistory.java @@ -9,6 +9,7 @@ import org.hswebframework.web.bean.FastBeanCopier; import org.hswebframework.web.id.IDGenerator; import org.jetlinks.community.notify.event.SerializableNotifierEvent; import org.jetlinks.community.notify.manager.enums.NotifyState; +import org.jetlinks.community.utils.ObjectMappers; import java.util.Map; @@ -50,10 +51,12 @@ public class NotifyHistory { public static NotifyHistory of(SerializableNotifierEvent event) { NotifyHistory history = FastBeanCopier.copy(event, new NotifyHistory()); - history.setId(IDGenerator.SNOW_FLAKE_STRING.generate()); + if (null == event.getId()) { + history.setId(IDGenerator.RANDOM.generate()); + } history.setNotifyTime(System.currentTimeMillis()); if (null != event.getTemplate()) { - history.setTemplate(JSON.toJSONString(event.getTemplate())); + history.setTemplate(ObjectMappers.toJsonString(event.getTemplate())); } if (event.isSuccess()) { history.setState(NotifyState.success); @@ -65,8 +68,8 @@ public class NotifyHistory { } public JSONObject toJson() { - JSONObject obj = FastBeanCopier.copy(this,new JSONObject()); - obj.put("state",state.getValue()); + JSONObject obj = FastBeanCopier.copy(this, new JSONObject()); + obj.put("state", state.getValue()); obj.put("context", JSON.toJSONString(context)); return obj; } diff --git a/jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/service/NotifyHistoryService.java b/jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/service/NotifyHistoryService.java index b5de315b..02d23fc3 100644 --- a/jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/service/NotifyHistoryService.java +++ b/jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/service/NotifyHistoryService.java @@ -1,22 +1,12 @@ package org.jetlinks.community.notify.manager.service; import org.hswebframework.web.crud.service.GenericReactiveCrudService; -import org.jetlinks.community.gateway.annotation.Subscribe; -import org.jetlinks.community.notify.event.SerializableNotifierEvent; import org.jetlinks.community.notify.manager.entity.NotifyHistoryEntity; import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Propagation; -import org.springframework.transaction.annotation.Transactional; -import reactor.core.publisher.Mono; @Service public class NotifyHistoryService extends GenericReactiveCrudService { - @Subscribe("/notify/**") - @Transactional(propagation = Propagation.NEVER) - public Mono handleNotify(SerializableNotifierEvent event) { - return insert(Mono.just(NotifyHistoryEntity.of(event))).then(); - } } diff --git a/jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/service/NotifySubscriberProviderService.java b/jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/service/NotifySubscriberProviderService.java index 6b93bee3..79deb3d4 100644 --- a/jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/service/NotifySubscriberProviderService.java +++ b/jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/service/NotifySubscriberProviderService.java @@ -1,11 +1,101 @@ package org.jetlinks.community.notify.manager.service; +import lombok.AllArgsConstructor; +import org.apache.commons.collections4.MapUtils; +import org.hswebframework.ezorm.rdb.mapping.ReactiveRepository; import org.hswebframework.web.crud.service.GenericReactiveCacheSupportCrudService; +import org.jetlinks.community.notify.manager.entity.NotifySubscriberChannelEntity; import org.jetlinks.community.notify.manager.entity.NotifySubscriberProviderEntity; +import org.jetlinks.community.notify.manager.subscriber.SubscriberProvider; +import org.jetlinks.community.notify.manager.subscriber.SubscriberProviders; +import org.jetlinks.community.notify.manager.web.NotifyChannelController; import org.springframework.stereotype.Service; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.util.function.Tuple2; +import reactor.util.function.Tuples; + +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; @Service +@AllArgsConstructor public class NotifySubscriberProviderService extends GenericReactiveCacheSupportCrudService { + private final ReactiveRepository repository; + public Mono saveInfo(Flux infoFlux) { + Flux>> + cache = infoFlux + .map(pro -> Tuples.of(pro.toProviderEntity(), pro.toChannelEntities())) + .cache(); + + return this + .save(cache.map(Tuple2::getT1)) + .then(repository + .save(cache.flatMapIterable(tp2 -> { + //provider保存后再回填ID + for (NotifySubscriberChannelEntity entity : tp2.getT2()) { + entity.setProviderId(tp2.getT1().getId()); + } + return tp2.getT2(); + }))) + .then(); + } + + + //获取所有通道配置 + public Flux channels() { + + Map info = SubscriberProviders + .getProviders() + .stream() + .collect(Collectors.toMap( + SubscriberProvider::getId, + NotifyChannelController.SubscriberProviderInfo::of)); + + Map notSaveInfoMap = new HashMap<>(info); + return createQuery() + .fetch() + .collectList() + .flatMap(providers -> { + Map providerInfoMap = new HashMap<>(); + for (NotifySubscriberProviderEntity provider : providers) { + NotifyChannelController.SubscriberProviderInfo channelInfo = info.get(provider.getProvider()); + if (channelInfo != null) { + channelInfo.with(provider); + providerInfoMap.put(channelInfo.getId(), channelInfo); + } + if (info.get(provider.getProvider()) != null) { + notSaveInfoMap.remove(provider.getProvider()); + } + } + if (!MapUtils.isEmpty(notSaveInfoMap)) { + List providerList = notSaveInfoMap + .values() + .stream() + .map(NotifyChannelController.SubscriberProviderInfo::toProviderEntity) + .collect(Collectors.toList()); + return save(providerList) + .thenReturn(providerInfoMap); + } + return Mono.just(providerInfoMap); + }) + .filter(MapUtils::isNotEmpty) + .flatMapMany(mapping -> repository + .createQuery() + .in(NotifySubscriberChannelEntity::getProviderId, mapping.keySet()) + .fetch() + .doOnNext(channel -> { + NotifyChannelController.SubscriberProviderInfo channelInfo = mapping.get(channel.getProviderId()); + if (channelInfo != null) { + channelInfo.with(channel); + } + })) + .thenMany(Flux.fromIterable(info.values())) + .sort(Comparator.comparing(NotifyChannelController.SubscriberProviderInfo::getOrder)); + } } diff --git a/jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/service/NotifySubscriberService.java b/jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/service/NotifySubscriberService.java index 514c545a..39a467f9 100644 --- a/jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/service/NotifySubscriberService.java +++ b/jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/service/NotifySubscriberService.java @@ -10,10 +10,6 @@ import org.hswebframework.web.crud.events.*; import org.hswebframework.web.crud.service.GenericReactiveCrudService; import org.hswebframework.web.exception.BusinessException; import org.hswebframework.web.i18n.LocaleUtils; -import org.jetlinks.core.event.EventBus; -import org.jetlinks.core.event.Subscription; -import org.jetlinks.core.metadata.ConfigMetadata; -import org.jetlinks.core.utils.CompositeMap; import org.jetlinks.community.gateway.annotation.Subscribe; import org.jetlinks.community.notify.manager.configuration.NotifySubscriberProperties; import org.jetlinks.community.notify.manager.entity.Notification; @@ -27,6 +23,10 @@ import org.jetlinks.community.notify.manager.subscriber.SubscriberProvider; import org.jetlinks.community.notify.manager.subscriber.SubscriberProviders; import org.jetlinks.community.topic.Topics; import org.jetlinks.community.utils.ReactorUtils; +import org.jetlinks.core.event.EventBus; +import org.jetlinks.core.event.Subscription; +import org.jetlinks.core.metadata.ConfigMetadata; +import org.jetlinks.core.utils.CompositeMap; import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.CommandLineRunner; import org.springframework.context.ApplicationEventPublisher; @@ -82,10 +82,10 @@ public class NotifySubscriberService extends GenericReactiveCrudService entity) { entity.async( Flux.fromIterable(entity.getEntity()) - .map(providerEntity -> - providerChannels - .computeIfAbsent(providerEntity.getProviderId(), ignore -> new NotifySubscriberProviderCache()) - .addChannel(providerEntity) + .flatMap(providerEntity -> + providerChannels + .computeIfAbsent(providerEntity.getProviderId(), ignore -> new NotifySubscriberProviderCache()) + .addChannel(providerEntity) ) ); } @@ -94,10 +94,10 @@ public class NotifySubscriberService extends GenericReactiveCrudService entity) { entity.async( Flux.fromIterable(entity.getAfter()) - .map(providerEntity -> - providerChannels - .computeIfAbsent(providerEntity.getProviderId(), ignore -> new NotifySubscriberProviderCache()) - .addChannel(providerEntity) + .flatMap(providerEntity -> + providerChannels + .computeIfAbsent(providerEntity.getProviderId(), ignore -> new NotifySubscriberProviderCache()) + .addChannel(providerEntity) ) ); } @@ -106,10 +106,10 @@ public class NotifySubscriberService extends GenericReactiveCrudService entity) { entity.async( Flux.fromIterable(entity.getEntity()) - .map(providerEntity -> - providerChannels - .computeIfAbsent(providerEntity.getProviderId(), ignore -> new NotifySubscriberProviderCache()) - .addChannel(providerEntity) + .flatMap(providerEntity -> + providerChannels + .computeIfAbsent(providerEntity.getProviderId(), ignore -> new NotifySubscriberProviderCache()) + .addChannel(providerEntity) ) ); } @@ -118,10 +118,10 @@ public class NotifySubscriberService extends GenericReactiveCrudService entity) { entity.async( Flux.fromIterable(entity.getEntity()) - .map(providerEntity -> - providerChannels - .computeIfAbsent(providerEntity.getProviderId(), ignore -> new NotifySubscriberProviderCache()) - .removeChannel(providerEntity) + .flatMap(providerEntity -> + providerChannels + .computeIfAbsent(providerEntity.getProviderId(), ignore -> new NotifySubscriberProviderCache()) + .removeChannel(providerEntity) ) ); } @@ -130,10 +130,10 @@ public class NotifySubscriberService extends GenericReactiveCrudService entity) { entity.async( Flux.fromIterable(entity.getEntity()) - .map(providerEntity -> - providerChannels - .computeIfAbsent(providerEntity.getId(), ignore -> new NotifySubscriberProviderCache()) - .update(providerEntity) + .flatMap(providerEntity -> + providerChannels + .computeIfAbsent(providerEntity.getId(), ignore -> new NotifySubscriberProviderCache()) + .update(providerEntity) ) ); } @@ -142,10 +142,10 @@ public class NotifySubscriberService extends GenericReactiveCrudService entity) { entity.async( Flux.fromIterable(entity.getAfter()) - .map(providerEntity -> - providerChannels - .computeIfAbsent(providerEntity.getId(), ignore -> new NotifySubscriberProviderCache()) - .update(providerEntity) + .flatMap(providerEntity -> + providerChannels + .computeIfAbsent(providerEntity.getId(), ignore -> new NotifySubscriberProviderCache()) + .update(providerEntity) ) ); } @@ -154,10 +154,10 @@ public class NotifySubscriberService extends GenericReactiveCrudService entity) { entity.async( Flux.fromIterable(entity.getEntity()) - .map(providerEntity -> - providerChannels - .computeIfAbsent(providerEntity.getId(), ignore -> new NotifySubscriberProviderCache()) - .update(providerEntity) + .flatMap(providerEntity -> + providerChannels + .computeIfAbsent(providerEntity.getId(), ignore -> new NotifySubscriberProviderCache()) + .update(providerEntity) ) ); } @@ -166,7 +166,7 @@ public class NotifySubscriberService extends GenericReactiveCrudService entity) { entity.async( Flux.fromIterable(entity.getEntity()) - .map(providerEntity -> { + .flatMap(providerEntity -> { ReactorUtils.dispose(providerChannels.remove(providerEntity.getId())); return Mono.empty(); }) @@ -178,7 +178,7 @@ public class NotifySubscriberService extends GenericReactiveCrudService entity) { entity.async( Flux.fromIterable(entity.getEntity()) - .map(providerEntity -> { + .flatMap(providerEntity -> { providerEntity.setState(SubscribeState.disabled); handleSubscribe(providerEntity); return Mono.empty(); @@ -190,7 +190,7 @@ public class NotifySubscriberService extends GenericReactiveCrudService entity) { entity.async( Flux.fromIterable(entity.getEntity()) - .map(providerEntity -> { + .flatMap(providerEntity -> { handleSubscribe(providerEntity); return Mono.empty(); }) @@ -201,7 +201,7 @@ public class NotifySubscriberService extends GenericReactiveCrudService entity) { entity.async( Flux.fromIterable(entity.getAfter()) - .map(providerEntity -> { + .flatMap(providerEntity -> { handleSubscribe(providerEntity); return Mono.empty(); }) @@ -212,7 +212,7 @@ public class NotifySubscriberService extends GenericReactiveCrudService entity) { entity.async( Flux.fromIterable(entity.getEntity()) - .map(providerEntity -> { + .flatMap(providerEntity -> { handleSubscribe(providerEntity); return Mono.empty(); }) @@ -434,8 +434,13 @@ public class NotifySubscriberService extends GenericReactiveCrudService subs.remove(entity.getId(), this))) .flatMap(subscriber -> subscriber .subscribe(entity.toLocale()) @@ -551,7 +559,7 @@ public class NotifySubscriberService extends GenericReactiveCrudService newChannels = new HashSet<>(effectNotifyChannel); //通道被禁用或者没有权限则删除此通道 if (e.getState() == NotifyChannelState.disabled - || (!properties.isAllowAllNotify(auth) && e.getGrant() != null && !e.getGrant().isGranted(auth))) { + || (!properties.isAllowAllNotify(auth) && e.getGrant() != null && !e.getGrant().isGranted(auth))) { newChannels.remove(e.getId()); } else { if (userConfigureNotifyChannels.contains(e.getId())) { @@ -585,11 +593,17 @@ public class NotifySubscriberService extends GenericReactiveCrudService(newChannels)); } + @Override + public boolean isDisposed() { + return disposable == null || disposable.isDisposed(); + } + @Override public void dispose() { + Disposable disposable = this.disposable; + this.disposable = null; if (null != disposable) { disposable.dispose(); - disposable = null; } } } @@ -600,6 +614,10 @@ public class NotifySubscriberService extends GenericReactiveCrudService channels = new ConcurrentHashMap<>(); + public NotifySubscriberProviderEntity getProvider(){ + return provider; + } + public Mono update(NotifySubscriberProviderEntity entity) { provider = entity; return resubscribe(entity); @@ -652,6 +670,10 @@ public class NotifySubscriberService extends GenericReactiveCrudService table.resubscribe(channel)) .then(); +// return Flux +// .fromIterable(subscribers.values()) +// .flatMap(table -> table.resubscribe(channel)) +// .then(); } diff --git a/jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/service/NotifyTemplateService.java b/jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/service/NotifyTemplateService.java index 737f748d..ae07b882 100644 --- a/jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/service/NotifyTemplateService.java +++ b/jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/service/NotifyTemplateService.java @@ -6,7 +6,6 @@ import org.springframework.stereotype.Service; /** * @author wangzheng - * @see * @since 1.0 */ @Service diff --git a/jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/subscriber/Subscriber.java b/jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/subscriber/Subscriber.java index 02479964..61941dd5 100644 --- a/jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/subscriber/Subscriber.java +++ b/jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/subscriber/Subscriber.java @@ -6,8 +6,19 @@ import java.util.Locale; public interface Subscriber { + /** + * 指定本地化语言发起订阅 + * + * @param locale Locale + * @return 通知事件流 + */ Flux subscribe(Locale locale); + /** + * 使用默认语言进行订阅 + * + * @return 通知事件流 + */ default Flux subscribe() { return subscribe(Locale.getDefault()); } 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 f1d2d088..51bbcacb 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 @@ -1,6 +1,9 @@ package org.jetlinks.community.notify.manager.subscriber; import org.hswebframework.web.authorization.Authentication; +import org.jetlinks.community.notify.enums.SubscriberTypeEnum; +import org.jetlinks.community.notify.subscription.SubscribeType; +import org.jetlinks.core.Wrapper; import org.jetlinks.core.metadata.ConfigMetadata; import org.jetlinks.core.metadata.PropertyMetadata; import reactor.core.publisher.Flux; @@ -8,16 +11,64 @@ import reactor.core.publisher.Mono; import java.util.Map; -public interface SubscriberProvider { +/** + * 个人订阅提供商,用于提供对不同类型的个人订阅的支持. 如: 设备告警等. + * + * @author zhouhao + * @since 2.2 + */ +public interface SubscriberProvider extends Wrapper { + /** + * @return 唯一标识 + */ String getId(); + /** + * @return 名称 + */ String getName(); + /** + * return 排序 + */ + default Integer getOrder(){ + return Integer.MAX_VALUE; + }; + + /** + * 订阅类型 + * + * @see SubscriberTypeEnum + */ + default SubscribeType getType(){ + return SubscriberTypeEnum.other; + } + + /** + * 基于配置信息创建订阅器 + * + * @param id 订阅ID + * @param authentication 用户权限信息 + * @param config 订阅配置信息 + * @return 订阅器 + */ Mono createSubscriber(String id, Authentication authentication, Map config); + /** + * 获取通过该订阅提供商收到的通知中的{@link Notify#getDetail()}的数据结构, + * 通常用于前端进行可视化配置: 如和消息通知中的变量进行绑定. + * + * @param config 配置信息 + * @return PropertyMetadata + */ default Flux getDetailProperties(Map config) { return Flux.empty(); } + /** + * 获取需要的配置结构定义,通常用于前端进行动态渲染配置. + * + * @return ConfigMetadata + */ ConfigMetadata getConfigMetadata(); } diff --git a/jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/subscriber/channel/InsideMailChannelProvider.java b/jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/subscriber/channel/InsideMailChannelProvider.java index 03acafee..9b3eb525 100644 --- a/jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/subscriber/channel/InsideMailChannelProvider.java +++ b/jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/subscriber/channel/InsideMailChannelProvider.java @@ -1,8 +1,9 @@ package org.jetlinks.community.notify.manager.subscriber.channel; import lombok.AllArgsConstructor; -import org.jetlinks.community.notify.manager.entity.Notification; +import org.hswebframework.web.i18n.LocaleUtils; import org.jetlinks.core.event.EventBus; +import org.jetlinks.community.notify.manager.entity.Notification; import org.springframework.core.Ordered; import org.springframework.stereotype.Component; import reactor.core.publisher.Mono; @@ -33,7 +34,8 @@ public class InsideMailChannelProvider implements NotifyChannelProvider, NotifyC @Override public String getName() { - return "站内信"; + return LocaleUtils + .resolveMessage("message.subscriber.provider.inside-mail", "站内信"); } @Override diff --git a/jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/subscriber/channel/NotificationDispatcher.java b/jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/subscriber/channel/NotificationDispatcher.java index 4d96760a..8c2979ea 100644 --- a/jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/subscriber/channel/NotificationDispatcher.java +++ b/jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/subscriber/channel/NotificationDispatcher.java @@ -6,14 +6,13 @@ import org.hswebframework.web.crud.events.EntityCreatedEvent; import org.hswebframework.web.crud.events.EntityDeletedEvent; import org.hswebframework.web.crud.events.EntityModifyEvent; import org.hswebframework.web.crud.events.EntitySavedEvent; -import org.jetlinks.community.gateway.annotation.Subscribe; -import org.jetlinks.community.notify.manager.entity.Notification; -import org.jetlinks.community.notify.manager.entity.NotifySubscriberChannelEntity; -import org.jetlinks.community.notify.manager.entity.NotifySubscriberChannelEntity; -import org.jetlinks.community.notify.manager.enums.NotifyChannelState; import org.jetlinks.core.cache.ReactiveCacheContainer; import org.jetlinks.core.event.EventBus; import org.jetlinks.core.event.Subscription; +import org.jetlinks.community.gateway.annotation.Subscribe; +import org.jetlinks.community.notify.manager.entity.Notification; +import org.jetlinks.community.notify.manager.entity.NotifySubscriberChannelEntity; +import org.jetlinks.community.notify.manager.enums.NotifyChannelState; import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.CommandLineRunner; import org.springframework.context.event.EventListener; @@ -35,6 +34,7 @@ import java.util.Map; @Component @Slf4j public class NotificationDispatcher implements CommandLineRunner { + private final EventBus eventBus; private final ReactiveCacheContainer channels = ReactiveCacheContainer.create(); diff --git a/jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/subscriber/providers/AlarmDeviceProvider.java b/jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/subscriber/providers/AlarmDeviceProvider.java index 64c7a14b..833a14f2 100644 --- a/jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/subscriber/providers/AlarmDeviceProvider.java +++ b/jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/subscriber/providers/AlarmDeviceProvider.java @@ -2,12 +2,13 @@ package org.jetlinks.community.notify.manager.subscriber.providers; import lombok.extern.slf4j.Slf4j; import org.hswebframework.web.authorization.Authentication; +import org.hswebframework.web.i18n.LocaleUtils; +import org.jetlinks.community.notify.manager.subscriber.Subscriber; +import org.jetlinks.community.topic.Topics; import org.jetlinks.core.event.EventBus; import org.jetlinks.core.metadata.PropertyMetadata; import org.jetlinks.core.metadata.SimplePropertyMetadata; import org.jetlinks.core.metadata.types.StringType; -import org.jetlinks.community.notify.manager.subscriber.Subscriber; -import org.jetlinks.community.topic.Topics; import org.springframework.stereotype.Component; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -29,7 +30,13 @@ public class AlarmDeviceProvider extends AlarmProvider { @Override public String getName() { - return "设备告警"; + return LocaleUtils + .resolveMessage("message.subscriber.provider.alarm-device", "设备告警"); + } + + @Override + public Integer getOrder() { + return 100; } @Override diff --git a/jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/subscriber/providers/AlarmProductProvider.java b/jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/subscriber/providers/AlarmProductProvider.java index 51a8eff0..bf81a1ad 100644 --- a/jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/subscriber/providers/AlarmProductProvider.java +++ b/jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/subscriber/providers/AlarmProductProvider.java @@ -2,12 +2,13 @@ package org.jetlinks.community.notify.manager.subscriber.providers; import lombok.extern.slf4j.Slf4j; import org.hswebframework.web.authorization.Authentication; +import org.hswebframework.web.i18n.LocaleUtils; +import org.jetlinks.community.notify.manager.subscriber.Subscriber; +import org.jetlinks.community.topic.Topics; import org.jetlinks.core.event.EventBus; import org.jetlinks.core.metadata.PropertyMetadata; import org.jetlinks.core.metadata.SimplePropertyMetadata; import org.jetlinks.core.metadata.types.StringType; -import org.jetlinks.community.notify.manager.subscriber.Subscriber; -import org.jetlinks.community.topic.Topics; import org.springframework.stereotype.Component; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -29,7 +30,13 @@ public class AlarmProductProvider extends AlarmProvider { @Override public String getName() { - return "产品告警"; + return LocaleUtils + .resolveMessage("message.subscriber.provider.alarm-product", "产品告警"); + } + + @Override + public Integer getOrder() { + return -100; } @Override 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 index 0568dae5..94991ec8 100755 --- 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 @@ -7,9 +7,11 @@ import lombok.extern.slf4j.Slf4j; import org.hswebframework.web.authorization.Authentication; import org.hswebframework.web.i18n.LocaleUtils; import org.jetlinks.community.ValueObject; +import org.jetlinks.community.notify.enums.SubscriberTypeEnum; 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.notify.subscription.SubscribeType; import org.jetlinks.community.topic.Topics; import org.jetlinks.core.event.EventBus; import org.jetlinks.core.event.Subscription; @@ -21,7 +23,6 @@ import org.jetlinks.core.metadata.types.IntType; import org.jetlinks.core.metadata.types.LongType; import org.jetlinks.core.metadata.types.StringType; import org.jetlinks.core.utils.FluxUtils; -import org.springframework.stereotype.Component; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -30,7 +31,6 @@ import java.util.Locale; import java.util.Map; import java.util.Objects; -@Component @Slf4j public class AlarmProvider implements SubscriberProvider { @@ -47,7 +47,19 @@ public class AlarmProvider implements SubscriberProvider { @Override public String getName() { - return "告警"; + return LocaleUtils + .resolveMessage("message.subscriber.provider.alarm", "告警"); + } + + @Override + public SubscribeType getType() { + return SubscriberTypeEnum.alarm; + } + + @Override + public Mono createSubscriber(String id, Authentication authentication, Map config) { + String topic = Topics.alarm("*", "*", getAlarmId(config)); + return doCreateSubscriber(id, authentication, topic); } @Override @@ -56,15 +68,9 @@ public class AlarmProvider implements SubscriberProvider { .add("alarmConfigId", "告警规则", "告警规则,支持通配符:*", StringType.GLOBAL); } - @Override - public Mono createSubscriber(String id, Authentication authentication, Map config) { - - String topic = Topics.alarm("*", "*", getAlarmId(config)); - - return Mono.just(locale -> createSubscribe(locale, id, new String[]{topic}) - //有效期内去重,防止同一个用户所在多个部门推送同一个告警 - .as(FluxUtils.distinct(Notify::getDataId, Duration.ofSeconds(10)))); - + protected String getAlarmId(Map config) { + ValueObject configs = ValueObject.of(config); + return configs.getString("alarmConfigId").orElse("*"); } protected Mono doCreateSubscriber(String id, @@ -75,11 +81,6 @@ public class AlarmProvider implements SubscriberProvider { .as(FluxUtils.distinct(Notify::getDataId, Duration.ofSeconds(10)))); } - protected String getAlarmId(Map config) { - ValueObject configs = ValueObject.of(config); - return configs.getString("alarmConfigId").orElse("*"); - } - private Flux createSubscribe(Locale locale, String id, String[] topic) { @@ -116,11 +117,7 @@ public class AlarmProvider implements SubscriberProvider { TargetType targetType = TargetType.of(json.getString("targetType")); String targetName = json.getString("targetName"); String alarmName = json.getString("alarmConfigName"); - if (targetType == TargetType.scene) { - message = String.format("[%s]发生告警:[%s]!", targetName, alarmName); - } else { - message = String.format("%s[%s]发生告警:[%s]!", targetType.getText(), targetName, alarmName); - } + message = String.format("%s[%s]发生告警:[%s]!", targetType.getText(), targetName, alarmName); return LocaleUtils.resolveMessage("message.alarm.notify." + targetType.name(), locale, message, targetName, alarmName); } @@ -144,7 +141,6 @@ public class AlarmProvider implements SubscriberProvider { device("设备"), product("产品"), scene("场景"); - private final String text; public static TargetType of(String name) { diff --git a/jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/subscriber/providers/AlarmSceneProvider.java b/jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/subscriber/providers/AlarmSceneProvider.java index be80a91d..74bde6ef 100644 --- a/jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/subscriber/providers/AlarmSceneProvider.java +++ b/jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/subscriber/providers/AlarmSceneProvider.java @@ -2,9 +2,10 @@ package org.jetlinks.community.notify.manager.subscriber.providers; import lombok.extern.slf4j.Slf4j; import org.hswebframework.web.authorization.Authentication; -import org.jetlinks.core.event.EventBus; +import org.hswebframework.web.i18n.LocaleUtils; import org.jetlinks.community.notify.manager.subscriber.Subscriber; import org.jetlinks.community.topic.Topics; +import org.jetlinks.core.event.EventBus; import org.springframework.stereotype.Component; import reactor.core.publisher.Mono; @@ -25,7 +26,13 @@ public class AlarmSceneProvider extends AlarmProvider { @Override public String getName() { - return "场景告警"; + return LocaleUtils + .resolveMessage("message.subscriber.provider.alarm-other", "场景告警"); + } + + @Override + public Integer getOrder() { + return 300; } @Override diff --git a/jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/web/NotificationController.java b/jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/web/NotificationController.java index fbfbc29e..0ce4e169 100644 --- a/jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/web/NotificationController.java +++ b/jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/web/NotificationController.java @@ -12,18 +12,24 @@ import org.hswebframework.web.api.crud.entity.QueryParamEntity; import org.hswebframework.web.authorization.Authentication; import org.hswebframework.web.authorization.annotation.Authorize; import org.hswebframework.web.authorization.exception.UnAuthorizedException; +import org.hswebframework.web.i18n.LocaleUtils; +import org.jetlinks.community.notify.manager.subscriber.SubscriberProvider; +import org.jetlinks.community.notify.manager.subscriber.SubscriberProviders; +import org.jetlinks.core.metadata.ConfigMetadata; +import org.jetlinks.community.notify.manager.configuration.NotifySubscriberProperties; import org.jetlinks.community.notify.manager.entity.NotificationEntity; import org.jetlinks.community.notify.manager.entity.NotifySubscriberEntity; import org.jetlinks.community.notify.manager.enums.NotificationState; import org.jetlinks.community.notify.manager.enums.SubscribeState; import org.jetlinks.community.notify.manager.service.NotificationService; +import org.jetlinks.community.notify.manager.service.NotifySubscriberProviderService; import org.jetlinks.community.notify.manager.service.NotifySubscriberService; -import org.jetlinks.community.notify.manager.subscriber.SubscriberProvider; -import org.jetlinks.core.metadata.ConfigMetadata; +import org.jetlinks.community.notify.subscription.SubscribeType; import org.springframework.web.bind.annotation.*; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import java.util.Comparator; import java.util.List; @RestController @@ -35,14 +41,18 @@ public class NotificationController { private final NotifySubscriberService subscriberService; - private final List providers; + private final NotifySubscriberProviderService providerService; + + private final NotifySubscriberProperties properties; public NotificationController(NotificationService notificationService, NotifySubscriberService subscriberService, - List providers) { + NotifySubscriberProviderService providerService, + NotifySubscriberProperties properties) { this.notificationService = notificationService; this.subscriberService = subscriberService; - this.providers = providers; + this.providerService = providerService; + this.properties = properties; } @GetMapping("/subscriptions/_query") @@ -105,11 +115,11 @@ public class NotificationController { @PatchMapping("/subscribe") @Authorize(ignore = true) @Operation(summary = "订阅通知") - public Mono doSubscribe(@RequestBody Mono subscribe) { + public Flux doSubscribe(@RequestBody Flux subscribe) { return Authentication .currentReactive() .switchIfEmpty(Mono.error(UnAuthorizedException::new)) - .flatMap(auth -> subscribe + .flatMapMany(auth -> subscribe .doOnNext(e -> { e.setSubscriberType("user"); e.setSubscriber(auth.getUser().getId()); @@ -119,15 +129,66 @@ public class NotificationController { .thenReturn(e))); } + /** + * @see NotifyChannelController#getChannelProviders() + * @deprecated + */ @GetMapping("/providers") @Authorize(merge = false) @Operation(summary = "获取全部订阅支持") public Flux getProviders() { return Flux - .fromIterable(providers) + .fromIterable(SubscriberProviders.getProviders()) .map(SubscriberProviderInfo::of); } + /** + * @see NotifyChannelController#getChannelProviders() + * @deprecated + */ + @GetMapping("/current/providers") + @Authorize(merge = false) + @Operation(summary = "获取当前用户可用的订阅支持") + public Flux getCurrentProviders() { + return Authentication + .currentReactive() + .switchIfEmpty(Mono.error(UnAuthorizedException::new)) + .flatMapMany(auth -> providerService + .channels() + .mapNotNull(info -> info.copyToProvidedUser(auth, properties))) + .filter(p -> CollectionUtils.isNotEmpty(p.getChannels())) + .map(NotifyChannelController.SubscriberProviderInfo::getProvider) + .collectList() + .flatMapMany(providers -> Flux + .fromIterable(SubscriberProviders.getProviders()) + .filter(provider -> providers.contains(provider.getId())) + .map(SubscriberProviderInfo::of)); + } + + /** + * @see NotifyChannelController#getChannelProviders() + * @deprecated + */ + @GetMapping("/current/{type}/providers") + @Authorize(merge = false) + @Operation(summary = "根据订阅类型获取当前用户可用的订阅支持") + public Flux getCurrentProviders(@PathVariable String type) { + return Authentication + .currentReactive() + .switchIfEmpty(Mono.error(UnAuthorizedException::new)) + .flatMapMany(auth -> providerService + .channels() + .mapNotNull(info -> info.copyToProvidedUser(auth, properties))) + .filter(p -> CollectionUtils.isNotEmpty(p.getChannels())) + .map(NotifyChannelController.SubscriberProviderInfo::getProvider) + .collectList() + .flatMapMany(providers -> Flux + .fromIterable(SubscriberProviders.getProviders()) + .filter(provider -> provider.getType().getId().equals(type) && providers.contains(provider.getId())) + .sort(Comparator.comparing(SubscriberProvider::getOrder)) + .map(SubscriberProviderInfo::of)); + } + @GetMapping("/_query") @Authorize(ignore = true) @QueryOperation(summary = "查询通知记录") @@ -190,7 +251,7 @@ public class NotificationController { @PostMapping("/_{state}/provider") @Authorize(ignore = true) - @QueryOperation(summary = "按订阅类型修改通知状态") + @QueryOperation(summary = "按订阅具体类型修改通知状态") public Mono readNotificationByType(@RequestBody Mono> providerList, @PathVariable NotificationState state) { return Authentication @@ -198,12 +259,13 @@ public class NotificationController { .switchIfEmpty(Mono.error(UnAuthorizedException::new)) .flatMap(auth -> providerList .filter(CollectionUtils::isNotEmpty) - .flatMap(list -> notificationService.createUpdate() - .set(NotificationEntity::getState, state) - .where(NotificationEntity::getSubscriberType, "user") - .and(NotificationEntity::getSubscriber, auth.getUser().getId()) - .in(NotificationEntity::getTopicProvider, list) - .execute()) + .flatMap(list -> notificationService + .createUpdate() + .set(NotificationEntity::getState, state) + .where(NotificationEntity::getSubscriberType, "user") + .and(NotificationEntity::getSubscriber, auth.getUser().getId()) + .in(NotificationEntity::getTopicProvider, list) + .execute()) ); } @@ -214,14 +276,38 @@ public class NotificationController { private String name; + private SubscribeTypeInfo type; + private ConfigMetadata metadata; + public String getName() { + return LocaleUtils.resolveMessage("message.subscriber.provider." + id, name); + } + public static SubscriberProviderInfo of(SubscriberProvider provider) { SubscriberProviderInfo info = new SubscriberProviderInfo(); info.id = provider.getId(); info.name = provider.getName(); + info.type = SubscribeTypeInfo.of(provider.getType()); info.setMetadata(provider.getConfigMetadata()); return info; } } + + @Getter + @Setter + public static class SubscribeTypeInfo { + + private String id; + + private String name; + + public static SubscribeTypeInfo of(SubscribeType type) { + SubscribeTypeInfo info = new SubscribeTypeInfo(); + info.id = type.getId(); + info.name = type.getName(); + return info; + } + } + } diff --git a/jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/web/NotifyChannelController.java b/jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/web/NotifyChannelController.java index 10ec44bb..483da89d 100644 --- a/jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/web/NotifyChannelController.java +++ b/jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/web/NotifyChannelController.java @@ -1,5 +1,6 @@ package org.jetlinks.community.notify.manager.web; +import com.fasterxml.jackson.annotation.JsonIgnore; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.AllArgsConstructor; @@ -7,13 +8,11 @@ import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; import org.apache.commons.collections4.CollectionUtils; -import org.apache.commons.collections4.MapUtils; import org.hswebframework.ezorm.rdb.mapping.ReactiveRepository; import org.hswebframework.web.authorization.Authentication; import org.hswebframework.web.authorization.annotation.Authorize; import org.hswebframework.web.authorization.annotation.DeleteAction; import org.hswebframework.web.authorization.annotation.Resource; - import org.hswebframework.web.authorization.annotation.SaveAction; import org.hswebframework.web.authorization.exception.UnAuthorizedException; import org.hswebframework.web.id.IDGenerator; @@ -26,15 +25,13 @@ import org.jetlinks.community.notify.manager.service.NotifySubscriberProviderSer import org.jetlinks.community.notify.manager.subscriber.SubscriberProvider; import org.jetlinks.community.notify.manager.subscriber.SubscriberProviders; import org.jetlinks.community.notify.manager.subscriber.channel.NotifyChannelProvider; +import org.jetlinks.community.notify.subscription.SubscribeType; import org.jetlinks.core.metadata.PropertyMetadata; import org.springframework.web.bind.annotation.*; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; -import reactor.util.function.Tuple2; -import reactor.util.function.Tuples; import java.util.*; -import java.util.function.Function; import java.util.stream.Collectors; @RestController @@ -44,6 +41,7 @@ import java.util.stream.Collectors; @AllArgsConstructor public class NotifyChannelController { + @SuppressWarnings("all") private final ReactiveRepository repository; private final NotifySubscriberProviderService providerService; @@ -66,22 +64,7 @@ public class NotifyChannelController { @SaveAction @Operation(summary = "保存通道配置") public Mono save(@RequestBody Flux infoFlux) { - Flux>> - cache = infoFlux - .map(pro -> Tuples.of(pro.toProviderEntity(), pro.toChannelEntities())) - .cache(); - - return providerService - .save(cache.map(Tuple2::getT1)) - .then(repository - .save(cache.flatMapIterable(tp2 -> { - //provider保存后再回填ID - for (NotifySubscriberChannelEntity entity : tp2.getT2()) { - entity.setProviderId(tp2.getT1().getId()); - } - return tp2.getT2(); - }))) - .then(); + return providerService.saveInfo(infoFlux); } @PatchMapping("/{providerId}") @@ -143,55 +126,7 @@ public class NotifyChannelController { @SaveAction //有保存权限的才能查看全部 @Operation(summary = "获取所有通道配置") public Flux channels() { - - Map info = SubscriberProviders - .getProviders() - .stream() - .collect(Collectors.toMap( - SubscriberProvider::getId, - SubscriberProviderInfo::of)); - - Map notSaveInfoMap = new HashMap<>(info); - return providerService - .createQuery() - .fetch() - .collectList() - .flatMap(providers -> { - Map providerInfoMap = new HashMap<>(); - for (NotifySubscriberProviderEntity provider : providers) { - SubscriberProviderInfo channelInfo = info.get(provider.getProvider()); - if (channelInfo != null) { - channelInfo.with(provider); - providerInfoMap.put(channelInfo.getId(), channelInfo); - } - if (info.get(provider.getProvider()) != null) { - notSaveInfoMap.remove(provider.getProvider()); - } - } - if (!MapUtils.isEmpty(notSaveInfoMap)) { - List providerList = notSaveInfoMap - .values() - .stream() - .map(SubscriberProviderInfo::toProviderEntity) - .collect(Collectors.toList()); - return providerService - .save(providerList) - .thenReturn(providerInfoMap); - } - return Mono.just(providerInfoMap); - }) - .filter(MapUtils::isNotEmpty) - .flatMapMany(mapping -> repository - .createQuery() - .in(NotifySubscriberChannelEntity::getProviderId, mapping.keySet()) - .fetch() - .doOnNext(channel -> { - SubscriberProviderInfo channelInfo = mapping.get(channel.getProviderId()); - if (channelInfo != null) { - channelInfo.with(channel); - } - })) - .thenMany(Flux.fromIterable(info.values())); + return providerService.channels(); } @GetMapping("/all") @@ -228,6 +163,8 @@ public class NotifyChannelController { private String provider; + private SubscribeTypeInfo type; + private Map configuration; private AuthenticationSpec grant; @@ -236,6 +173,26 @@ public class NotifyChannelController { private List channels = new ArrayList<>(); + private int order; + + public SubscriberProviderInfo(String id, + String name, + String provider, + SubscribeTypeInfo type, + Map configuration, + AuthenticationSpec grant, + NotifyChannelState state, + List channels) { + this.id = id; + this.name = name; + this.provider = provider; + this.type = type; + this.configuration = configuration; + this.grant = grant; + this.state = state; + this.channels = channels; + } + public SubscriberProviderInfo copyToProvidedUser(Authentication auth, NotifySubscriberProperties properties) { if (id == null || (state == null || state == NotifyChannelState.disabled) @@ -247,11 +204,17 @@ public class NotifyChannelController { info.setId(id); info.setName(name); info.setProvider(provider); + info.setType(type); info.setChannels( channels .stream() - .filter(e -> e.getId() != null && - (properties.isAllowAllNotify(auth) || e.getGrant() == null || e.getGrant().isGranted(auth))) + .filter(channel -> ( + (channel.getId() != null) + && ( + properties.isAllowAllNotify(auth) + || (channel.getGrant() != null && channel.getGrant().isGranted(auth)) + ) + )) .collect(Collectors.toList()) ); return info; @@ -262,10 +225,13 @@ public class NotifyChannelController { IDGenerator.RANDOM.generate(), info.getName(), info.getId(), + SubscribeTypeInfo.of(info.getType()), null, null, NotifyChannelState.disabled, - new ArrayList<>()); + new ArrayList<>(), + info.getOrder() + ); } public SubscriberProviderInfo with(List provider) { @@ -296,9 +262,8 @@ public class NotifyChannelController { public SubscriberProviderInfo with(NotifySubscriberProviderEntity provider) { this.id = provider.getId(); - this.name = provider.getName(); - this.provider = provider.getProvider(); this.grant = provider.getGrant(); + this.provider = provider.getProvider(); this.configuration = provider.getConfiguration(); this.state = provider.getState(); return this; @@ -320,6 +285,24 @@ public class NotifyChannelController { } } + @Getter + @Setter + @AllArgsConstructor + @NoArgsConstructor + public static class SubscribeTypeInfo { + private String id; + @JsonIgnore + SubscribeType type; + + public static SubscribeTypeInfo of(SubscribeType type) { + return new SubscribeTypeInfo(type.getId(), type); + } + + public String getName() { + return type.getName(); + } + } + @Getter @Setter @AllArgsConstructor diff --git a/jetlinks-manager/notify-manager/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/jetlinks-manager/notify-manager/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports index c0938886..cdf49c77 100644 --- a/jetlinks-manager/notify-manager/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +++ b/jetlinks-manager/notify-manager/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -1 +1,2 @@ org.jetlinks.community.notify.manager.configuration.NotifyConfiguration +org.jetlinks.community.notify.manager.configuration.SubscriptionConfiguration \ No newline at end of file diff --git a/jetlinks-manager/notify-manager/src/main/resources/i18n/notify-manager/messages_en.properties b/jetlinks-manager/notify-manager/src/main/resources/i18n/notify-manager/messages_en.properties new file mode 100644 index 00000000..abf87a77 --- /dev/null +++ b/jetlinks-manager/notify-manager/src/main/resources/i18n/notify-manager/messages_en.properties @@ -0,0 +1,27 @@ +#enum +org.jetlinks.community.notify.manager.enums.NotificationState.unread=unread +org.jetlinks.community.notify.manager.enums.NotificationState.read=read + +#enum +org.jetlinks.community.notify.manager.enums.NotifyState.success=success +org.jetlinks.community.notify.manager.enums.NotifyState.retrying=retrying +org.jetlinks.community.notify.manager.enums.NotifyState.error=error +org.jetlinks.community.notify.manager.enums.NotifyState.cancel=cancel + +#error +error.unsupported_topics=Unsupported topics:{0} +error.notifier_does_not_exist=Notifiers :{0} does not exist +error.bulletin_is_published=The announcement has been issued +error.bulletin_is_unpublished=The announcement has been withdrawn + +hswebframework.web.system.action.send=Send Notification +hswebframework.web.system.permission.template=Notification Template +hswebframework.web.system.permission.notifier=Notification Management +hswebframework.web.system.permission.notify-channel=Notification Channel Configuration + +#message +message.subscriber.provider.inside-mail=Inside mail +message.notify.system.bulletin.dict.name=System bulletin type +message.notify.dict.item.default.text=default +message.subscriber.provider.system-bulletin=system button + diff --git a/jetlinks-manager/notify-manager/src/main/resources/i18n/notify-manager/messages_zh.properties b/jetlinks-manager/notify-manager/src/main/resources/i18n/notify-manager/messages_zh.properties new file mode 100644 index 00000000..0bf1366f --- /dev/null +++ b/jetlinks-manager/notify-manager/src/main/resources/i18n/notify-manager/messages_zh.properties @@ -0,0 +1,24 @@ +#enum +org.jetlinks.community.notify.manager.enums.NotificationState.unread=\u672A\u8BFB +org.jetlinks.community.notify.manager.enums.NotificationState.read=\u5DF2\u8BFB +org.jetlinks.community.notify.manager.enums.NotifyState.success=\u6210\u529F +org.jetlinks.community.notify.manager.enums.NotifyState.retrying=\u91CD\u8BD5\u4E2D +org.jetlinks.community.notify.manager.enums.NotifyState.error=\u5931\u8D25 +org.jetlinks.community.notify.manager.enums.NotifyState.cancel=\u5DF2\u53D6\u6D88 +#error +error.unsupported_topics=\u4E0D\u652F\u6301\u7684\u4E3B\u9898:{0} +error.notifier_does_not_exist=\u901A\u77E5\u5668:{0}\u4E0D\u5B58\u5728 +error.bulletin_is_published=\u516C\u544A\u5DF2\u53D1\u5E03 +error.bulletin_is_unpublished=\u516C\u544A\u5DF2\u64A4\u56DE + +hswebframework.web.system.action.send=\u53D1\u9001\u901A\u77E5 +hswebframework.web.system.permission.template=\u901A\u77E5\u6A21\u677F +hswebframework.web.system.permission.notifier=\u901A\u77E5\u7BA1\u7406 +hswebframework.web.system.permission.notify-channel=\u901A\u77E5\u901A\u9053\u914D\u7F6E + +#message +message.subscriber.provider.inside-mail=\u7AD9\u5185\u4FE1 +message.notify.system.bulletin.dict.name=\u7CFB\u7EDF\u516C\u544A\u7C7B\u578B +message.notify.dict.item.default.text=\u9ED8\u8BA4 +message.subscriber.provider.system-bulletin=\u7CFB\u7EDF\u516C\u544A + diff --git a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/alarm/AbstractAlarmTarget.java b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/alarm/AbstractAlarmTarget.java index 5837b8b6..8e16877f 100644 --- a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/alarm/AbstractAlarmTarget.java +++ b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/alarm/AbstractAlarmTarget.java @@ -36,7 +36,7 @@ public abstract class AbstractAlarmTarget implements AlarmTarget { protected abstract Flux doConvert(AlarmData data); - static Optional getFromOutput(String key, Map output) { + protected static Optional getFromOutput(String key, Map output) { //优先从场景输出中获取 Object sceneOutput = output.get(SceneRule.CONTEXT_KEY_SCENE_OUTPUT); if (sceneOutput instanceof Map) { diff --git a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/alarm/AlarmData.java b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/alarm/AlarmData.java index 5773b1fd..30e2b899 100644 --- a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/alarm/AlarmData.java +++ b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/alarm/AlarmData.java @@ -22,4 +22,6 @@ public class AlarmData implements Serializable { private String ruleName; private Map output; + + private String creatorId; } diff --git a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/alarm/AlarmHandleInfo.java b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/alarm/AlarmHandleInfo.java index 09120e91..73c28266 100644 --- a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/alarm/AlarmHandleInfo.java +++ b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/alarm/AlarmHandleInfo.java @@ -3,8 +3,9 @@ package org.jetlinks.community.rule.engine.alarm; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Getter; import lombok.Setter; -import org.jetlinks.community.rule.engine.enums.AlarmHandleType; +import org.jetlinks.community.rule.engine.enums.AlarmHandleState; import org.jetlinks.community.rule.engine.enums.AlarmRecordState; +import org.jetlinks.community.terms.TermSpec; import javax.validation.constraints.NotBlank; import javax.validation.constraints.NotNull; @@ -38,8 +39,46 @@ public class AlarmHandleInfo { private String type; @Schema(description = "处理后的状态") - @NotBlank private AlarmRecordState state; + @Schema(description = "告警处理状态") + private AlarmHandleState handleState; + + @Schema(description = "告警记录创建者ID") + private String recordCreatorId; + + @Schema(description = "告警级别") + private int level; + + @Schema(description = "告警目标类型") + private String targetType; + + @Schema(description = "告警目标名称") + private String targetName; + + @Schema(description = "告警目标Id") + private String targetId; + + @Schema(description = "告警源类型") + private String sourceType; + + @Schema(description = "告警源Id") + private String sourceId; + + @Schema(description = "告警源名称") + private String sourceName; + + @Schema(description = "告警配置源") + private String alarmConfigSource; + + @Schema(description = "触发条件") + private TermSpec termSpec; + + @Schema(description = "触发条件描述") + private String triggerDesc; + + @Schema(description = "告警原因描述") + private String actualDesc; + } diff --git a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/alarm/AlarmHandler.java b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/alarm/AlarmHandler.java index 2259c696..1ca783fc 100644 --- a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/alarm/AlarmHandler.java +++ b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/alarm/AlarmHandler.java @@ -15,6 +15,7 @@ public interface AlarmHandler { /** * 触发告警 * @see AlarmTaskExecutorProvider + * @see org.jetlinks.community.things.impl.preprocessor.internal.AbstractMessageAlarmPreprocessor * * @return 告警触发结果 */ diff --git a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/alarm/AlarmLevelInfo.java b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/alarm/AlarmLevelInfo.java index aac1b331..da18fa5e 100644 --- a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/alarm/AlarmLevelInfo.java +++ b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/alarm/AlarmLevelInfo.java @@ -3,6 +3,8 @@ package org.jetlinks.community.rule.engine.alarm; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Getter; import lombok.Setter; +import org.hswebframework.ezorm.rdb.mapping.annotation.JsonCodec; +import org.hswebframework.web.i18n.SingleI18nSupportEntity; import java.util.Map; @@ -11,7 +13,7 @@ import java.util.Map; */ @Getter @Setter -public class AlarmLevelInfo { +public class AlarmLevelInfo implements SingleI18nSupportEntity { @Schema(description = "级别") private Integer level; @@ -19,6 +21,11 @@ public class AlarmLevelInfo { @Schema(description = "名称") private String title; - @Schema(description = "拓展") - private Map expands; + @JsonCodec + @Schema(description = "国际化信息") + private Map i18nMessages; + + public String getTitle() { + return getI18nMessage("title", title); + } } diff --git a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/alarm/AlarmProperties.java b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/alarm/AlarmProperties.java new file mode 100644 index 00000000..424ef9f3 --- /dev/null +++ b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/alarm/AlarmProperties.java @@ -0,0 +1,25 @@ +package org.jetlinks.community.rule.engine.alarm; + +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * @author bestfeng + */ +@Getter +@Setter +@ConfigurationProperties(prefix = "jetlinks.alarm") +public class AlarmProperties { + + private AlarmHandleHistory handleHistory = new AlarmHandleHistory(); + + + @Getter + @Setter + public static class AlarmHandleHistory { + + //创建默认告警记录 + private boolean createWhenAlarm; + } +} diff --git a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/alarm/AlarmSceneHandler.java b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/alarm/AlarmSceneHandler.java index 336161e9..78407738 100644 --- a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/alarm/AlarmSceneHandler.java +++ b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/alarm/AlarmSceneHandler.java @@ -1,11 +1,8 @@ package org.jetlinks.community.rule.engine.alarm; import lombok.AllArgsConstructor; -import org.hswebframework.web.authorization.ReactiveAuthenticationHolder; import org.hswebframework.web.crud.events.*; import org.hswebframework.web.exception.BusinessException; -import org.jetlinks.core.event.EventBus; -import org.jetlinks.core.event.Subscription; import org.jetlinks.community.gateway.annotation.Subscribe; import org.jetlinks.community.rule.engine.entity.AlarmConfigEntity; import org.jetlinks.community.rule.engine.entity.AlarmHistoryInfo; @@ -18,6 +15,8 @@ import org.jetlinks.community.rule.engine.service.AlarmConfigService; import org.jetlinks.community.rule.engine.service.AlarmHistoryService; import org.jetlinks.community.rule.engine.service.AlarmRecordService; import org.jetlinks.community.topic.Topics; +import org.jetlinks.core.event.EventBus; +import org.jetlinks.core.event.Subscription; import org.springframework.boot.CommandLineRunner; import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.event.EventListener; @@ -68,7 +67,7 @@ public class AlarmSceneHandler implements SceneFilter, CommandLineRunner { .of(alarmConfig.getTargetType()) .convert(AlarmData.of(alarmConfig.getId(), alarmConfig.getName(), data.getRule().getId(), data .getRule() - .getName(), data.getOutput())) + .getName(), data.getOutput(), alarmConfig.getCreatorId())) .flatMap(targetInfo -> { AlarmRecordEntity record = ofRecord(targetInfo, alarmConfig); diff --git a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/alarm/AlarmTarget.java b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/alarm/AlarmTarget.java index 3075c88c..7e06e788 100644 --- a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/alarm/AlarmTarget.java +++ b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/alarm/AlarmTarget.java @@ -14,6 +14,10 @@ public interface AlarmTarget { String getName(); + default Integer getOrder(){ + return Integer.MAX_VALUE; + }; + Flux convert(AlarmData data); default boolean isSupported(String trigger) { diff --git a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/alarm/AlarmTargetInfo.java b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/alarm/AlarmTargetInfo.java index 31a3ef76..46210163 100644 --- a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/alarm/AlarmTargetInfo.java +++ b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/alarm/AlarmTargetInfo.java @@ -26,8 +26,14 @@ public class AlarmTargetInfo { private String sourceName; + private String creatorId; + public static AlarmTargetInfo of(String targetId, String targetName, String targetType) { - return AlarmTargetInfo.of(targetId, targetName, targetType, null, null, null); + return AlarmTargetInfo.of(targetId, targetName, targetType, null, null, null, null); + } + + public static AlarmTargetInfo of(String targetId, String targetName, String targetType, String creatorId) { + return AlarmTargetInfo.of(targetId, targetName, targetType, null, null, null, creatorId); } public AlarmTargetInfo withTarget(String type, String id, String name) { diff --git a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/alarm/AlarmTaskExecutorProvider.java b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/alarm/AlarmTaskExecutorProvider.java index 9e3f52ea..397d5a9f 100644 --- a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/alarm/AlarmTaskExecutorProvider.java +++ b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/alarm/AlarmTaskExecutorProvider.java @@ -14,7 +14,6 @@ import org.jetlinks.rule.engine.api.task.TaskExecutor; import org.jetlinks.rule.engine.api.task.TaskExecutorProvider; import org.jetlinks.rule.engine.defaults.FunctionTaskExecutor; import org.reactivestreams.Publisher; -import org.springframework.stereotype.Component; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; diff --git a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/alarm/DefaultAlarmHandler.java b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/alarm/DefaultAlarmHandler.java index b1c4c9da..0fa465d6 100644 --- a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/alarm/DefaultAlarmHandler.java +++ b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/alarm/DefaultAlarmHandler.java @@ -1,41 +1,61 @@ package org.jetlinks.community.rule.engine.alarm; import lombok.AllArgsConstructor; +import lombok.Getter; import lombok.extern.slf4j.Slf4j; -import org.hswebframework.ezorm.rdb.mapping.ReactiveRepository; import org.hswebframework.web.bean.FastBeanCopier; import org.hswebframework.web.i18n.LocaleUtils; import org.hswebframework.web.id.IDGenerator; -import org.jetlinks.core.config.ConfigStorageManager; -import org.jetlinks.core.event.EventBus; -import org.jetlinks.core.utils.Reactors; +import org.jetlinks.community.PropertyConstants; import org.jetlinks.community.command.rule.data.AlarmInfo; import org.jetlinks.community.command.rule.data.AlarmResult; import org.jetlinks.community.command.rule.data.RelieveInfo; import org.jetlinks.community.command.rule.data.RelieveResult; +import org.jetlinks.community.lock.ReactiveLock; +import org.jetlinks.community.lock.ReactiveLockHolder; import org.jetlinks.community.rule.engine.entity.AlarmHandleHistoryEntity; import org.jetlinks.community.rule.engine.entity.AlarmHistoryInfo; import org.jetlinks.community.rule.engine.entity.AlarmRecordEntity; +import org.jetlinks.community.rule.engine.enums.AlarmHandleState; +import org.jetlinks.community.rule.engine.enums.AlarmHandleType; import org.jetlinks.community.rule.engine.enums.AlarmRecordState; +import org.jetlinks.community.rule.engine.service.AlarmHandleHistoryService; import org.jetlinks.community.rule.engine.service.AlarmHistoryService; import org.jetlinks.community.rule.engine.service.AlarmRecordService; import org.jetlinks.community.topic.Topics; import org.jetlinks.community.utils.ObjectMappers; +import org.jetlinks.community.utils.TopicUtils; +import org.jetlinks.core.config.ConfigStorage; +import org.jetlinks.core.config.ConfigStorageManager; +import org.jetlinks.core.event.EventBus; +import org.jetlinks.core.utils.Reactors; +import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; +import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; + +import java.io.Externalizable; +import java.io.IOException; +import java.io.ObjectInput; +import java.io.ObjectOutput; +import java.util.HashMap; +import java.util.Map; import java.util.function.Function; @Slf4j -@AllArgsConstructor @Component +@EnableConfigurationProperties(AlarmProperties.class) public class DefaultAlarmHandler implements AlarmHandler { + static final String CACHE_ID = "alarm-records"; + private final AlarmRecordService alarmRecordService; private final AlarmHistoryService historyService; - private final ReactiveRepository handleHistoryRepository; + private final AlarmHandleHistoryService alarmHandleHistoryService; private final EventBus eventBus; @@ -43,13 +63,44 @@ public class DefaultAlarmHandler implements AlarmHandler { private final ApplicationEventPublisher eventPublisher; + private final AlarmProperties alarmProperties; + + public DefaultAlarmHandler(AlarmRecordService alarmRecordService, + AlarmHistoryService historyService, + AlarmHandleHistoryService alarmHandleHistoryService, + EventBus eventBus, + ConfigStorageManager storageManager, + ApplicationEventPublisher eventPublisher, + AlarmProperties alarmProperties) { + this.alarmRecordService = alarmRecordService; + this.historyService = historyService; + this.alarmHandleHistoryService = alarmHandleHistoryService; + this.eventBus = eventBus; + this.storageManager = storageManager; + this.eventPublisher = eventPublisher; + this.alarmProperties = alarmProperties; + } + @Override public Mono triggerAlarm(AlarmInfo alarmInfo) { - return getRecordCache(createRecordId(alarmInfo)) - .map(this::ofRecordCache) - .defaultIfEmpty(new AlarmResult()) - .flatMap(result -> { - AlarmRecordEntity record = ofRecord(result, alarmInfo); + String recordId = createRecordId(alarmInfo); + ReactiveLock lock = ReactiveLockHolder + .getLock("triggerAlarm:" + recordId); + return this + .getRecordCache(recordId) + .flatMap(cache -> { + if (alarmInfo.getAlarmTime() == null) { + alarmInfo.setAlarmTime(System.currentTimeMillis()); + } + AlarmRecordEntity record = ofRecord(cache, alarmInfo); + AlarmResult result = ofRecordCache(cache); + AlarmHistoryInfo historyInfo = createHistory(record, alarmInfo); + //时间滞后的告警数据,仅记录日志. + if (!cache.isEffectiveTrigger(alarmInfo.getAlarmTime())) { + return historyService + .save(historyInfo) + .thenReturn(result); + } //更新告警状态. return alarmRecordService .createUpdate() @@ -63,8 +114,6 @@ public class DefaultAlarmHandler implements AlarmHandler { return Reactors.ALWAYS_ZERO; }) .flatMap(total -> { - AlarmHistoryInfo historyInfo = createHistory(record, alarmInfo); - //更新结果返回0 说明是新产生的告警数据 if (total == 0) { result.setFirstAlarm(true); @@ -73,26 +122,161 @@ public class DefaultAlarmHandler implements AlarmHandler { record.setAlarmTime(historyInfo.getAlarmTime()); record.setHandleTime(null); record.setHandleType(null); - return this .saveAlarmRecord(record) .then(historyService.save(historyInfo)) .then(publishAlarmRecord(historyInfo, alarmInfo)) - .then(publishEvent(historyInfo)) + //初始化告警处理记录 + .then(Mono.defer(() -> { + AlarmHandleInfo alarmHandleInfo = createAlarmHandleInfo(record); + if (alarmProperties.getHandleHistory().isCreateWhenAlarm()) { + return alarmHandleHistoryService + .save(AlarmHandleHistoryEntity.of(alarmHandleInfo)) + .then(publishAlarmHandleHistory(alarmInfo, alarmHandleInfo)); + } + return Mono.empty(); + })) + .then(publishAlarmLog(historyInfo, alarmInfo)) .then(saveAlarmCache(result, record)); } result.setFirstAlarm(false); result.setAlarming(true); + result.setAlarmTime(historyInfo.getAlarmTime()); return historyService .save(historyInfo) - .then(publishEvent(historyInfo)) + .then(publishAlarmLog(historyInfo, alarmInfo)) .then(saveAlarmCache(result, record)); }); - }); - + }) + .as(lock::lock); } - private Mono saveAlarmRecord(AlarmRecordEntity record){ + @Override + public Mono relieveAlarm(RelieveInfo relieveInfo) { + return this + .getRecordCache(createRecordId(relieveInfo)) + .flatMap(cache -> { + if (relieveInfo.getRelieveTime() == null) { + relieveInfo.setRelieveTime(System.currentTimeMillis()); + } + long relieveTime = relieveInfo.getRelieveTime(); + AlarmRecordEntity record = this.ofRecord(cache, relieveInfo); + AlarmHistoryInfo historyInfo = this.createHistory(record, relieveInfo); + RelieveResult relieveResult = createRelieveResult(record, relieveInfo); + //无效解除告警,更新解除告警时间 + if (!cache.isEffectiveRelieve(relieveInfo.getRelieveTime())) { + return this.updateRecordCache( + record.getId(), + relieveTime, + _cache -> _cache.withRelievedTime(relieveTime), + false, true) + .thenReturn(relieveResult); + } + return Mono + .zip(alarmRecordService.changeRecordState( + StringUtils.hasText(relieveInfo.getAlarmRelieveType()) + ? relieveInfo.getAlarmRelieveType() + : AlarmHandleType.system.getValue(), + AlarmRecordState.normal, + record.getId()), + this.updateRecordCache(record.getId(), + relieveTime, + _cache -> _cache.relieved(relieveTime), + false, true), + (total, ignore) -> total) + .flatMap(total -> { + //如果有数据被更新说明是正在告警中 + if (total > 0) { + AlarmHandleInfo alarmHandleInfo = createRelieveAlarmHandleInfo(relieveInfo, record); + return alarmHandleHistoryService + .save(AlarmHandleHistoryEntity.of(alarmHandleInfo)) + .then(publishAlarmHandleHistory(relieveInfo, alarmHandleInfo)) + .then(publishAlarmRelieve(historyInfo, relieveInfo)) + .thenReturn(relieveResult); + } + return Mono.empty(); + }); + }); + } + + public Mono publishAlarmHandleHistory(AlarmInfo alarmInfo, AlarmHandleInfo info) { + String topic = Topics.alarmHandleHistory(info.getTargetType(), info.getTargetId(), info.getAlarmConfigId()); + Map configs = new HashMap<>(); + configs.put(PropertyConstants.creatorId.getKey(), info.getRecordCreatorId()); + + return Flux + .fromIterable(TopicUtils.refactorTopic(configs, topic)) + .flatMap(assetTopic -> eventBus.publish(assetTopic, info)) + .then(); + } + + public Mono publishAlarmRecord(AlarmHistoryInfo historyInfo, AlarmInfo alarmInfo) { + String topic = Topics.alarm(historyInfo.getTargetType(), historyInfo.getTargetId(), historyInfo.getAlarmConfigId()); + + return doPublishAlarmInfo(topic, historyInfo, alarmInfo); + } + + public Mono doPublishAlarmInfo(String topic, AlarmHistoryInfo historyInfo, AlarmInfo alarmInfo) { +// Set userId = new HashSet<>(); +// for (Map binding : alarmInfo.getBindings()) { +// if (DefaultDimensionType.user.getId().equals(binding.get("type"))) { +// userId.add(binding.get("id").toString()); +// } +// } + + Map configs = new HashMap<>(); + configs.put(PropertyConstants.creatorId.getKey(), historyInfo.getCreatorId()); + + return Flux + .fromIterable(TopicUtils.refactorTopic(configs, topic)) + .flatMap(assetTopic -> eventBus.publish(assetTopic, historyInfo)) + .then(); + } + + + public Mono publishAlarmRelieve(AlarmHistoryInfo historyInfo, AlarmInfo alarmInfo) { + String topic = Topics.alarmRelieve(historyInfo.getTargetType(), historyInfo.getTargetId(), historyInfo.getAlarmConfigId()); + + return this.doPublishAlarmInfo(topic, historyInfo, alarmInfo); + } + + protected AlarmHistoryInfo createHistory(AlarmRecordEntity record, AlarmInfo alarmInfo) { + AlarmHistoryInfo info = new AlarmHistoryInfo(); + info.setId(IDGenerator.RANDOM.generate()); + info.setAlarmConfigId(record.getAlarmConfigId()); + info.setAlarmConfigName(record.getAlarmName()); + info.setDescription(record.getDescription()); + info.setAlarmRecordId(record.getId()); + info.setLevel(record.getLevel()); + info.setAlarmTime(alarmInfo.getAlarmTime()); + info.setTriggerDesc(record.getTriggerDesc()); + info.setAlarmConfigSource(alarmInfo.getAlarmConfigSource()); + info.setActualDesc(record.getActualDesc()); + + info.setTargetName(record.getTargetName()); + info.setTargetId(record.getTargetId()); + info.setTargetType(record.getTargetType()); + + info.setSourceType(record.getSourceType()); + info.setSourceName(record.getSourceName()); + info.setSourceId(record.getSourceId()); + + info.setAlarmInfo(ObjectMappers.toJsonString(alarmInfo.getData())); + info.setCreatorId(record.getCreatorId()); + return info; + } + + private RelieveResult createRelieveResult(AlarmRecordEntity record, RelieveInfo relieveInfo) { + AlarmHistoryInfo historyInfo = this.createHistory(record, relieveInfo); + RelieveResult relieveResult = FastBeanCopier.copy(record, new RelieveResult()); + relieveResult.setRelieveTime(relieveInfo.getRelieveTime()); + relieveResult.setActualDesc(historyInfo.getActualDesc()); + relieveResult.setRelieveReason(relieveInfo.getRelieveReason()); + relieveResult.setDescribe(relieveInfo.getDescription()); + return relieveResult; + } + + private Mono saveAlarmRecord(AlarmRecordEntity record) { return alarmRecordService .createUpdate() .set(record) @@ -109,43 +293,11 @@ public class DefaultAlarmHandler implements AlarmHandler { }); } - @Override - public Mono relieveAlarm(RelieveInfo relieveInfo) { - return getRecordCache(createRecordId(relieveInfo)) - .map(this::ofRecordCache) - .defaultIfEmpty(new AlarmResult()) - .flatMap(result -> { - AlarmRecordEntity record = this.ofRecord(result, relieveInfo); - return Mono - .zip(alarmRecordService.changeRecordState( - relieveInfo.getAlarmRelieveType() - , AlarmRecordState.normal, - record.getId()), - this.updateRecordCache(record.getId(), DefaultAlarmRuleHandler.RecordCache::withNormal), - (total, ignore) -> total) - .flatMap(total -> { - //如果有数据被更新说明是正在告警中 - if (total > 0) { - result.setAlarming(true); - AlarmHistoryInfo historyInfo = this.createHistory(record, relieveInfo); - RelieveResult relieveResult = FastBeanCopier.copy(record, new RelieveResult()); - relieveResult.setRelieveTime(System.currentTimeMillis()); - relieveResult.setActualDesc(historyInfo.getActualDesc()); - relieveResult.setRelieveReason(relieveInfo.getRelieveReason()); - relieveResult.setDescribe(relieveInfo.getDescription()); - return saveAlarmHandleHistory(relieveInfo, record) - .then(publishAlarmRelieve(historyInfo, relieveInfo)) - .thenReturn(relieveResult); - } - return Mono.empty(); - }); - }); - } - - public AlarmRecordEntity ofRecord(AlarmResult result, AlarmInfo alarmData) { + private AlarmRecordEntity ofRecord(RecordCache cache, AlarmInfo alarmData) { AlarmRecordEntity entity = new AlarmRecordEntity(); entity.setAlarmConfigId(alarmData.getAlarmConfigId()); - entity.setAlarmTime(result.getAlarmTime()); + entity.setAlarmTime(cache.getTrigger().alarmTime); + entity.setLastAlarmTime(alarmData.getAlarmTime()); entity.setState(AlarmRecordState.warning); entity.setLevel(alarmData.getLevel()); entity.setTargetType(alarmData.getTargetType()); @@ -159,6 +311,7 @@ public class DefaultAlarmHandler implements AlarmHandler { entity.setAlarmName(alarmData.getAlarmName()); entity.setDescription(alarmData.getDescription()); entity.setAlarmConfigSource(alarmData.getAlarmConfigSource()); + entity.setCreatorId(alarmData.getSourceCreatorId()); if (alarmData.getTermSpec() != null) { entity.setTermSpec(alarmData.getTermSpec()); entity.setTriggerDesc(alarmData.getTermSpec().getTriggerDesc()); @@ -168,73 +321,48 @@ public class DefaultAlarmHandler implements AlarmHandler { return entity; } - public AlarmHistoryInfo createHistory(AlarmRecordEntity record, AlarmInfo alarmInfo) { - AlarmHistoryInfo info = new AlarmHistoryInfo(); - info.setId(IDGenerator.RANDOM.generate()); - info.setAlarmConfigId(record.getAlarmConfigId()); - info.setAlarmConfigName(record.getAlarmName()); - info.setDescription(record.getDescription()); - info.setAlarmRecordId(record.getId()); - info.setLevel(record.getLevel()); - info.setAlarmTime(System.currentTimeMillis()); - info.setTriggerDesc(record.getTriggerDesc()); - info.setAlarmConfigSource(alarmInfo.getAlarmConfigSource()); - info.setActualDesc(record.getActualDesc()); + private Mono publishAlarmLog(AlarmHistoryInfo historyInfo, AlarmInfo alarmInfo) { + String topic = Topics.alarmLog(historyInfo.getTargetType(), + historyInfo.getTargetId(), + historyInfo.getAlarmConfigId(), + historyInfo.getAlarmRecordId()); - info.setTargetName(record.getTargetName()); - info.setTargetId(record.getTargetId()); - info.setTargetType(record.getTargetType()); - - info.setSourceType(record.getSourceType()); - info.setSourceName(record.getSourceName()); - info.setSourceId(record.getSourceId()); - - info.setAlarmInfo(ObjectMappers.toJsonString(alarmInfo.getData())); - return info; - } - - public Mono publishAlarmRecord(AlarmHistoryInfo historyInfo, AlarmInfo alarmInfo) { - String topic = Topics.alarm(historyInfo.getTargetType(), historyInfo.getTargetId(), historyInfo.getAlarmConfigId()); - - return doPublishAlarmHistoryInfo(topic, historyInfo, alarmInfo); - } - - public Mono doPublishAlarmHistoryInfo(String topic, AlarmHistoryInfo historyInfo, AlarmInfo alarmInfo) { - return Mono.just(topic) - .flatMap(assetTopic -> eventBus.publish(assetTopic, historyInfo)) - .then(); - } - - private Mono publishEvent(AlarmHistoryInfo historyInfo) { - return Mono.fromRunnable(() -> eventPublisher.publishEvent(historyInfo)); + eventPublisher.publishEvent(historyInfo); + return doPublishAlarmInfo(topic, historyInfo, alarmInfo); } private Mono saveAlarmCache(AlarmResult result, AlarmRecordEntity record) { return this - .updateRecordCache(record.getId(), cache -> cache.with(result)) + .updateRecordCache(record.getId(), + result.getAlarmTime(), + cache -> cache.with(result), + true, false) .thenReturn(result); } - public Mono publishAlarmRelieve(AlarmHistoryInfo historyInfo, AlarmInfo alarmInfo) { - String topic = Topics.alarmRelieve(historyInfo.getTargetType(), historyInfo.getTargetId(), historyInfo.getAlarmConfigId()); - return this.doPublishAlarmHistoryInfo(topic, historyInfo, alarmInfo); + private AlarmHandleInfo createRelieveAlarmHandleInfo(RelieveInfo relieveInfo, AlarmRecordEntity record) { + AlarmHandleInfo alarmHandleInfo = createAlarmHandleInfo(record); + alarmHandleInfo.setHandleTime(relieveInfo.getRelieveTime() == null + ? System.currentTimeMillis() : relieveInfo.getRelieveTime()); + alarmHandleInfo.setState(AlarmRecordState.normal); + alarmHandleInfo.setType(relieveInfo.getAlarmRelieveType() == null ? + AlarmHandleType.system.getValue() : relieveInfo.getAlarmRelieveType()); + alarmHandleInfo.setDescribe(StringUtils.hasText(relieveInfo.getDescribe()) + ? relieveInfo.getDescribe() + : getLocaleDescribe()); + alarmHandleInfo.setHandleState(AlarmHandleState.processed); + return alarmHandleInfo; } - private Mono saveAlarmHandleHistory(RelieveInfo relieveInfo, AlarmRecordEntity record) { - AlarmHandleInfo alarmHandleInfo = new AlarmHandleInfo(); - alarmHandleInfo.setHandleTime(System.currentTimeMillis()); + + private AlarmHandleInfo createAlarmHandleInfo(AlarmRecordEntity record) { + AlarmHandleInfo alarmHandleInfo = FastBeanCopier.copy(record, new AlarmHandleInfo()); alarmHandleInfo.setAlarmRecordId(record.getId()); - alarmHandleInfo.setAlarmConfigId(record.getAlarmConfigId()); - alarmHandleInfo.setAlarmTime(record.getAlarmTime()); - alarmHandleInfo.setState(AlarmRecordState.normal); - alarmHandleInfo.setType(relieveInfo.getAlarmRelieveType()); - alarmHandleInfo.setDescribe(getLocaleDescribe()); - // TODO: 2022/12/22 批量缓冲保存 - return handleHistoryRepository - .save(AlarmHandleHistoryEntity.of(alarmHandleInfo)) - .then(); + alarmHandleInfo.setState(AlarmRecordState.warning); + alarmHandleInfo.setRecordCreatorId(record.getCreatorId()); + return alarmHandleInfo; } private String getLocaleDescribe() { @@ -246,32 +374,182 @@ public class DefaultAlarmHandler implements AlarmHandler { return AlarmRecordEntity.generateId(alarmInfo.getTargetId(), alarmInfo.getTargetType(), alarmInfo.getAlarmConfigId()); } - private Mono getRecordCache(String recordId) { + private Mono getRecordCache(String recordId) { return storageManager - .getStorage("alarm-records") - .flatMap(store -> store - .getConfig(recordId) - .map(val -> val.as(DefaultAlarmRuleHandler.RecordCache.class))); + .getStorage(CACHE_ID) + .flatMap(store -> getCache0(store, recordId)); } - private Mono updateRecordCache(String recordId, Function handler) { - return storageManager - .getStorage("alarm-records") - .flatMap(store -> store - .getConfig(recordId) - .map(val -> val.as(DefaultAlarmRuleHandler.RecordCache.class)) - .switchIfEmpty(Mono.fromSupplier(DefaultAlarmRuleHandler.RecordCache::new)) - .mapNotNull(handler) - .flatMap(cache -> store.setConfig(recordId, cache) - .thenReturn(cache))); + Mono getCache0(ConfigStorage storage, String recordId) { + String reliveId = recordId + ":relieve"; + return storage + .getConfigs(recordId, reliveId) + .map(caches -> new RecordCache( + caches + .getValue(recordId) + .map(val -> val.as(TriggerCache.class)) + .orElseGet(TriggerCache::new), + caches + .getValue(reliveId) + .map(val -> val.as(RelieveCache.class)) + .orElseGet(RelieveCache::new) + )); } + private Mono updateRecordCache( + String recordId, + Long timestamp, + Function handler, + boolean includeTrigger, + boolean includeRelieve) { + String reliveId = recordId + ":relieve"; + return storageManager + .getStorage(CACHE_ID) + .flatMap(store -> getCache0(store, recordId) + .filter(cache -> includeTrigger ? cache.isEffectiveTrigger(timestamp) : cache.isEffectiveTime(timestamp)) + .map(handler) + .flatMap(cache -> { + Map configs = new HashMap<>(2); + if (includeTrigger) { + configs.put(recordId, cache.getTrigger()); + } + if (includeRelieve) { + configs.put(reliveId, cache.getRelieve()); + } + return store + .setConfigs(configs) + .thenReturn(cache); + })); + } - private AlarmResult ofRecordCache(DefaultAlarmRuleHandler.RecordCache cache) { + @Getter + @AllArgsConstructor + static class RecordCache { + private TriggerCache trigger; + + private RelieveCache relieve; + + public RecordCache withNormal() { + trigger.withNormal(); + return this; + } + + public RecordCache relieved(long time) { + trigger.alarmTime = 0; + relieve.reliveTime = time; + return withNormal(); + } + + public RecordCache withRelievedTime(long time) { + relieve.reliveTime = time; + return this; + } + + public RecordCache with(AlarmResult result) { + //触发的时间滞后了? + if (!isEffectiveTrigger(result.getAlarmTime())) { + return this; + } + trigger.with(result); + return this; + } + + //告警在解除最近一次告警之后才算有效 + public boolean isEffectiveTrigger(long timestamp) { + //只获取时间不可行或者告警仍在运行也需要解除 + return timestamp > relieve.reliveTime || !trigger.isAlarming(); + } + + //解除告警在最近一次告警时间之后 并且 当前告警正在告警中才算有效 + public boolean isEffectiveRelieve(long timestamp) { + return timestamp > trigger.alarmTime && trigger.isAlarming(); + } + + //有效的触发时间 + public boolean isEffectiveTime(long timestamp) { + return timestamp > relieve.reliveTime && timestamp > trigger.alarmTime; + } + } + + public static class TriggerCache implements Externalizable { + + static final byte stateNormal = 0x01; + static final byte stateAlarming = 0x02; + + byte state; + long alarmTime; + long lastAlarmTime; + + + public boolean isAlarming() { + return state == stateAlarming; + } + + public TriggerCache withNormal() { + this.state = stateNormal; + return this; + } + + public TriggerCache withAlarming() { + this.state = stateAlarming; + return this; + } + + public TriggerCache with(AlarmResult result) { + + if (result.isFirstAlarm()) { + this.alarmTime = result.getAlarmTime(); + } else { + this.alarmTime = this.alarmTime == 0 ? result.getAlarmTime() : this.alarmTime; + } + + this.lastAlarmTime = result.getAlarmTime(); + + if (result.isAlarming() || result.isFirstAlarm()) { + + this.state = stateAlarming; + + } else { + this.state = stateNormal; + } + return this; + } + + @Override + public void writeExternal(ObjectOutput out) throws IOException { + out.writeByte(state); + out.writeLong(alarmTime); + out.writeLong(lastAlarmTime); + } + + @Override + public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { + state = in.readByte(); + alarmTime = in.readLong(); + lastAlarmTime = in.readLong(); + } + } + + public static class RelieveCache implements Externalizable { + private long reliveTime; + + @Override + public void writeExternal(ObjectOutput out) throws IOException { + out.writeLong(reliveTime); + } + + @Override + public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { + reliveTime = in.readLong(); + } + } + + private AlarmResult ofRecordCache(RecordCache cache) { AlarmResult result = new AlarmResult(); - result.setAlarmTime(cache.alarmTime); - result.setLastAlarmTime(cache.lastAlarmTime); - result.setAlarming(cache.isAlarming()); + result.setAlarmTime(cache.trigger.alarmTime); + result.setLastAlarmTime(cache.trigger.lastAlarmTime); + result.setAlarming(cache.trigger.isAlarming()); return result; } -} + +} \ No newline at end of file diff --git a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/alarm/DefaultAlarmRuleHandler.java b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/alarm/DefaultAlarmRuleHandler.java index b9f744c0..1c0f1517 100644 --- a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/alarm/DefaultAlarmRuleHandler.java +++ b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/alarm/DefaultAlarmRuleHandler.java @@ -11,11 +11,6 @@ import org.hswebframework.web.crud.events.EntityCreatedEvent; import org.hswebframework.web.crud.events.EntityDeletedEvent; import org.hswebframework.web.crud.events.EntityModifyEvent; import org.hswebframework.web.crud.events.EntitySavedEvent; -import org.jetlinks.core.config.ConfigStorage; -import org.jetlinks.core.config.ConfigStorageManager; -import org.jetlinks.core.event.EventBus; -import org.jetlinks.core.event.Subscription; -import org.jetlinks.core.utils.CompositeSet; import org.jetlinks.community.command.rule.data.AlarmResult; import org.jetlinks.community.command.rule.data.RelieveInfo; import org.jetlinks.community.gateway.annotation.Subscribe; @@ -30,6 +25,11 @@ import org.jetlinks.community.rule.engine.service.AlarmConfigService; import org.jetlinks.community.rule.engine.service.AlarmRecordService; import org.jetlinks.community.terms.TermSpec; import org.jetlinks.community.utils.ConverterUtils; +import org.jetlinks.core.config.ConfigStorage; +import org.jetlinks.core.config.ConfigStorageManager; +import org.jetlinks.core.event.EventBus; +import org.jetlinks.core.event.Subscription; +import org.jetlinks.core.utils.CompositeSet; import org.jetlinks.reactor.ql.utils.CastUtils; import org.jetlinks.rule.engine.api.RuleData; import org.jetlinks.rule.engine.api.RuleDataHelper; @@ -48,7 +48,6 @@ import java.io.ObjectInput; import java.io.ObjectOutput; import java.util.*; import java.util.concurrent.ConcurrentHashMap; - @Slf4j @AllArgsConstructor @Component @@ -84,27 +83,43 @@ public class DefaultAlarmRuleHandler implements AlarmRuleHandler, CommandLineRun public Flux triggered(ExecutionContext context, RuleData data) { return this .parseAlarmInfo(context, data) - .flatMap(alarmInfo -> alarmHandler - .triggerAlarm(FastBeanCopier - .copy(alarmInfo, new org.jetlinks.community.command.rule.data.AlarmInfo())) - .map(info -> { - Result result = FastBeanCopier.copy(alarmInfo, new Result()); - FastBeanCopier.copy(info, result); - return result; - })); + .flatMap(alarmInfo -> { + org.jetlinks.community.command.rule.data.AlarmInfo copy = FastBeanCopier + .copy(alarmInfo, new org.jetlinks.community.command.rule.data.AlarmInfo()); + copy.setAlarmTime(getTimestampFromData(data)); + return alarmHandler + .triggerAlarm(copy) + .map(info -> { + Result result = FastBeanCopier.copy(alarmInfo, new Result()); + FastBeanCopier.copy(info, result); + return result; + }); + }); + } + + + private long getTimestampFromData(RuleData data) { + Object _data = data.getData(); + if (_data instanceof Map) { + @SuppressWarnings("all") + Map map = (Map) _data; + return CastUtils + .castNumber(map.getOrDefault("timestamp", System.currentTimeMillis())) + .longValue(); + } + return System.currentTimeMillis(); } @Override public Flux relieved(ExecutionContext context, RuleData data) { return this .parseAlarmInfo(context, data) - .flatMap(alarmInfo -> { - // 已经被解除不重复更新 - if (alarmInfo.isCached() && !alarmInfo.isAlarming()) { - return Mono.empty(); - } + .concatMap(alarmInfo -> { + RelieveInfo relieveInfo = FastBeanCopier.copy(alarmInfo, new RelieveInfo()); + relieveInfo.setRelieveTime(getTimestampFromData(data)); + return alarmHandler - .relieveAlarm(FastBeanCopier.copy(alarmInfo, new RelieveInfo())) + .relieveAlarm(relieveInfo) .map(info -> { Result result = FastBeanCopier.copy(alarmInfo, new Result()); FastBeanCopier.copy(info, result); @@ -172,22 +187,21 @@ public class DefaultAlarmRuleHandler implements AlarmRuleHandler, CommandLineRun } } - + @SuppressWarnings("all") private Flux parseAlarm(ExecutionContext context, ConfigStorage alarm, Map contextMap) { return this .getAlarmInfo(alarm) .flatMapMany(result -> { - String ruleName = RuleEngineConstants .getRuleName(context) .orElse(result.getAlarmName()); - AlarmData alarmData = AlarmData.of( result.getAlarmConfigId(), result.getAlarmName(), context.getInstanceId(), ruleName, - contextMap); + contextMap, + result.ownerId); result.setData(alarmData); result.setTermSpec(AlarmInfo.parseAlarmTrigger(context, contextMap)); @@ -198,6 +212,7 @@ public class DefaultAlarmRuleHandler implements AlarmRuleHandler, CommandLineRun }); } + private Mono getAlarmInfo(ConfigStorage alarm) { return alarm .getConfigs(configInfoKey) @@ -357,9 +372,9 @@ public class DefaultAlarmRuleHandler implements AlarmRuleHandler, CommandLineRun .doOnNext(this::handleBind) //加载告警配置数据到缓存 .thenMany(alarmConfigService - .createQuery() - .fetch() - .doOnNext(this::handleAlarmConfig) + .createQuery() + .fetch() + .flatMap(this::handleAlarmConfig) ) .subscribe(); @@ -373,6 +388,11 @@ public class DefaultAlarmRuleHandler implements AlarmRuleHandler, CommandLineRun */ private String ownerId; + /** + * 告警触发源创建者用户ID,表示告警是属于哪个用户的资产触发的,用于进行数据权限控制 + */ + private String sourceCreatorId; + private AlarmData data; /** @@ -382,21 +402,21 @@ public class DefaultAlarmRuleHandler implements AlarmRuleHandler, CommandLineRun private boolean cached; - private List> bindings; - public static TermSpec parseAlarmTrigger(ExecutionContext context, Map contextMap) { TermSpec termSpec = new TermSpec(); Map configuration = context.getJob().getConfiguration(); Object termSpecs = configuration.get(AlarmConstants.ConfigKey.alarmFilterTermSpecs); if (termSpecs != null) { - termSpec.setChildren(ConverterUtils.convertToList(termSpecs,o -> FastBeanCopier.copy(o, TermSpec.class))); - return termSpec.apply(contextMap); - + termSpec.setChildren(ConverterUtils.convertToList(termSpecs, o -> FastBeanCopier.copy(o, TermSpec.class))); + Map actualContext = new HashMap<>(contextMap); + if (contextMap.get(SceneRule.CONTEXT_KEY_SCENE_OUTPUT) != null) { + FastBeanCopier.copy(contextMap.get(SceneRule.CONTEXT_KEY_SCENE_OUTPUT), actualContext); + } + return termSpec.apply(actualContext); } return termSpec; } - @Override public AlarmInfo copyWith(AlarmTargetInfo targetInfo) { AlarmInfo result = FastBeanCopier.copy(this, new AlarmInfo()); @@ -407,12 +427,13 @@ public class DefaultAlarmRuleHandler implements AlarmRuleHandler, CommandLineRun result.setSourceId(targetInfo.getSourceId()); result.setSourceType(targetInfo.getSourceType()); result.setSourceName(targetInfo.getSourceName()); + result.setSourceCreatorId(targetInfo.getCreatorId()); return result; } } - + @Deprecated public static class RecordCache implements Externalizable { static final byte stateNormal = 0x01; @@ -467,5 +488,4 @@ public class DefaultAlarmRuleHandler implements AlarmRuleHandler, CommandLineRun lastAlarmTime = in.readLong(); } } - } diff --git a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/alarm/DeviceAlarmTarget.java b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/alarm/DeviceAlarmTarget.java index b353fd67..b64c358d 100644 --- a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/alarm/DeviceAlarmTarget.java +++ b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/alarm/DeviceAlarmTarget.java @@ -1,10 +1,16 @@ package org.jetlinks.community.rule.engine.alarm; +import org.hswebframework.web.i18n.LocaleUtils; +import org.jetlinks.community.PropertyConstants; import org.jetlinks.community.rule.engine.scene.internal.triggers.DeviceTriggerProvider; +import org.jetlinks.community.things.holder.ThingsRegistryHolder; +import org.jetlinks.core.device.DeviceThingType; import org.springframework.stereotype.Component; import reactor.core.publisher.Flux; +import java.util.HashSet; import java.util.Map; +import java.util.Set; /** * @author bestfeng @@ -12,14 +18,28 @@ import java.util.Map; @Component public class DeviceAlarmTarget extends AbstractAlarmTarget { + public static final String TYPE = "device"; + @Override public String getType() { - return "device"; + return TYPE; } @Override public String getName() { - return "设备"; + return LocaleUtils + .resolveMessage("message.rule_engine_alarm_device", "设备"); + } + + @Override + public Integer getOrder() { + return 200; + } + + private final Set configKeys = new HashSet<>(); + + public DeviceAlarmTarget() { + configKeys.add(PropertyConstants.creatorId.getKey()); } @Override @@ -27,12 +47,17 @@ public class DeviceAlarmTarget extends AbstractAlarmTarget { Map output = data.getOutput(); String deviceId = AbstractAlarmTarget.getFromOutput("deviceId", output).map(String::valueOf).orElse(null); String deviceName = AbstractAlarmTarget.getFromOutput("deviceName", output).map(String::valueOf).orElse(deviceId); - if (deviceId == null) { return Flux.empty(); } - - return Flux.just(AlarmTargetInfo.of(deviceId, deviceName, getType())); + return ThingsRegistryHolder + .registry() + .getThing(DeviceThingType.device, deviceId) + .flatMap(thing -> thing.getConfigs(configKeys)) + .flatMapMany(values -> { + String creatorId = values.getValue(PropertyConstants.creatorId).orElse(null); + return Flux.just(AlarmTargetInfo.of(deviceId, deviceName, getType(), creatorId)); + }); } @Override diff --git a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/alarm/ProductAlarmTarget.java b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/alarm/ProductAlarmTarget.java index 0c8f7e0f..86eb9851 100644 --- a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/alarm/ProductAlarmTarget.java +++ b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/alarm/ProductAlarmTarget.java @@ -1,10 +1,16 @@ package org.jetlinks.community.rule.engine.alarm; +import org.hswebframework.web.i18n.LocaleUtils; +import org.jetlinks.community.PropertyConstants; import org.jetlinks.community.rule.engine.scene.internal.triggers.DeviceTriggerProvider; +import org.jetlinks.community.things.holder.ThingsRegistryHolder; +import org.jetlinks.core.device.DeviceThingType; import org.springframework.stereotype.Component; import reactor.core.publisher.Flux; +import java.util.HashSet; import java.util.Map; +import java.util.Set; /** * @author bestfeng @@ -12,14 +18,29 @@ import java.util.Map; @Component public class ProductAlarmTarget extends AbstractAlarmTarget { + public static final String TYPE = "product"; + @Override public String getType() { - return "product"; + return TYPE; } @Override public String getName() { - return "产品"; + return LocaleUtils + .resolveMessage("message.rule_engine_alarm_product", "产品"); + } + + @Override + public Integer getOrder() { + return 100; + } + + private final Set configKeys = new HashSet<>(); + + + public ProductAlarmTarget() { + configKeys.add(PropertyConstants.creatorId.getKey()); } @Override @@ -27,13 +48,24 @@ public class ProductAlarmTarget extends AbstractAlarmTarget { Map output = data.getOutput(); String productId = AbstractAlarmTarget.getFromOutput("productId", output).map(String::valueOf).orElse(null); String productName = AbstractAlarmTarget.getFromOutput("productName", output).map(String::valueOf).orElse(productId); - - return Flux.just(AlarmTargetInfo.of(productId, productName, getType())); + String deviceId = AbstractAlarmTarget.getFromOutput("deviceId", output).map(String::valueOf).orElse(null); + if (deviceId == null || productId == null) { + return Flux.empty(); + } + return ThingsRegistryHolder + .registry() + .getThing(DeviceThingType.device, deviceId) + .flatMap(thing -> thing + .getTemplate() + .flatMap(template-> template.getConfigs(configKeys))) + .flatMapMany(values -> { + String creatorId = values.getValue(PropertyConstants.creatorId).orElse(null); + return Flux.just(AlarmTargetInfo.of(productId, productName, getType(), creatorId)); + }); } @Override public boolean isSupported(String trigger) { return DeviceTriggerProvider.PROVIDER.equals(trigger); }; - } diff --git a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/alarm/SceneAlarmTarget.java b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/alarm/SceneAlarmTarget.java index 3101b3bb..7a105d37 100644 --- a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/alarm/SceneAlarmTarget.java +++ b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/alarm/SceneAlarmTarget.java @@ -1,5 +1,7 @@ package org.jetlinks.community.rule.engine.alarm; + +import org.hswebframework.web.i18n.LocaleUtils; import org.springframework.stereotype.Component; import reactor.core.publisher.Flux; @@ -7,7 +9,7 @@ import reactor.core.publisher.Flux; * @author bestfeng */ @Component -public class SceneAlarmTarget implements AlarmTarget { +public class SceneAlarmTarget extends AbstractAlarmTarget { public static final String TYPE = "scene"; @@ -18,16 +20,23 @@ public class SceneAlarmTarget implements AlarmTarget { @Override public String getName() { - return "场景"; + return LocaleUtils + .resolveMessage("message.rule_engine_alarm_scene", "场景"); } @Override - public Flux convert(AlarmData data) { + public Integer getOrder() { + return 400; + } + + @Override + public Flux doConvert(AlarmData data) { return Flux.just(AlarmTargetInfo - .of(data.getRuleId(), - data.getRuleName(), - getType()) - .withSource(TYPE, data.getRuleId(), data.getRuleName())); + .of(data.getRuleId(), + data.getRuleName(), + getType(), + data.getCreatorId()) + .withSource(TYPE, data.getRuleId(), data.getRuleName())); } } diff --git a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/cmd/AlarmCommandSupport.java b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/cmd/AlarmCommandSupport.java new file mode 100644 index 00000000..8641387e --- /dev/null +++ b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/cmd/AlarmCommandSupport.java @@ -0,0 +1,34 @@ +package org.jetlinks.community.rule.engine.cmd; + +import org.jetlinks.core.command.AbstractCommandSupport; +import org.jetlinks.core.command.CommandHandler; +import org.jetlinks.community.command.rule.RelievedAlarmCommand; +import org.jetlinks.community.command.rule.TriggerAlarmCommand; +import org.jetlinks.community.rule.engine.alarm.AlarmHandler; + +public class AlarmCommandSupport extends AbstractCommandSupport { + + public AlarmCommandSupport(AlarmHandler alarmHandler) { + //触发告警 + registerHandler( + TriggerAlarmCommand.class, + CommandHandler.of( + TriggerAlarmCommand::metadata, + (cmd, ignore) -> alarmHandler + .triggerAlarm(cmd.getAlarmInfo()), + TriggerAlarmCommand::new + ) + ); + + // 解除告警 + registerHandler( + RelievedAlarmCommand.class, + CommandHandler.of( + RelievedAlarmCommand::metadata, + (cmd, ignore) -> alarmHandler + .relieveAlarm(cmd.getRelieveInfo()), + RelievedAlarmCommand::new + ) + ); + } +} diff --git a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/cmd/AlarmConfigCommandSupport.java b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/cmd/AlarmConfigCommandSupport.java new file mode 100644 index 00000000..acfafd81 --- /dev/null +++ b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/cmd/AlarmConfigCommandSupport.java @@ -0,0 +1,106 @@ +package org.jetlinks.community.rule.engine.cmd; + +import org.hswebframework.ezorm.rdb.mapping.ReactiveRepository; +import org.hswebframework.web.bean.FastBeanCopier; +import org.hswebframework.web.validator.ValidatorUtils; +import org.jetlinks.community.command.CrudCommandSupport; +import org.jetlinks.core.command.AbstractCommand; +import org.jetlinks.core.command.CommandHandler; +import org.jetlinks.core.command.CommandUtils; +import org.jetlinks.core.metadata.SimpleFunctionMetadata; +import org.jetlinks.community.rule.engine.alarm.AlarmHandleInfo; +import org.jetlinks.community.rule.engine.entity.AlarmConfigDetail; +import org.jetlinks.community.rule.engine.entity.AlarmConfigEntity; +import org.jetlinks.community.rule.engine.entity.AlarmLevelEntity; +import org.jetlinks.community.rule.engine.service.AlarmConfigService; +import org.jetlinks.community.rule.engine.service.AlarmLevelService; +import org.jetlinks.sdk.server.commons.cmd.DisabledCommand; +import org.jetlinks.sdk.server.commons.cmd.EnabledCommand; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.util.function.Function; + +/** + * @author liusq + * @date 2024/4/12 + */ +public class AlarmConfigCommandSupport extends CrudCommandSupport { + public AlarmConfigCommandSupport(AlarmConfigService service, + ReactiveRepository alarmLevelRepository) { + super(service); + //分页查询 + registerHandler( + org.jetlinks.sdk.server.commons.cmd.QueryPagerCommand + .createHandler( + metadata -> {}, + cmd -> service.queryDetailPager(cmd.asQueryParam())) + ); + + + // 启用 + registerHandler( + EnabledCommand + .createHandler(cmd -> Flux + .fromIterable(cmd.getIds()) + .flatMap(service::enable) + .then()) + ); + + // 禁用 + registerHandler( + DisabledCommand + .createHandler(cmd -> Flux + .fromIterable(cmd.getIds()) + .flatMap(service::disable) + .then()) + ); + + // 处理告警 + registerHandler( + HandleAlarmCommand + .createHandler(cmd -> { + AlarmHandleInfo info = FastBeanCopier.copy(cmd.readable(), new AlarmHandleInfo()); + ValidatorUtils.tryValidate(info); + return service.handleAlarm(info); + }) + ); + + // 获取告警级别 + registerHandler( + QueryAlarmLevelCommand + .createHandler(cmd -> alarmLevelRepository.findById(AlarmLevelService.DEFAULT_ALARM_ID)) + ); + + } + + public static class HandleAlarmCommand extends AbstractCommand, HandleAlarmCommand> { + public static CommandHandler> createHandler(Function> handler) { + return CommandHandler.of( + () -> { + SimpleFunctionMetadata metadata = new SimpleFunctionMetadata(); + metadata.setId(CommandUtils.getCommandIdByType(HandleAlarmCommand.class)); + metadata.setName("处理告警"); + return metadata; + }, + (cmd, ignore) -> handler.apply(cmd), + HandleAlarmCommand::new + ); + } + } + + public static class QueryAlarmLevelCommand extends AbstractCommand, QueryAlarmLevelCommand> { + public static CommandHandler> createHandler(Function> handler) { + return CommandHandler.of( + () -> { + SimpleFunctionMetadata metadata = new SimpleFunctionMetadata(); + metadata.setId(CommandUtils.getCommandIdByType(QueryAlarmLevelCommand.class)); + metadata.setName("获取告警级别"); + return metadata; + }, + (cmd, ignore) -> handler.apply(cmd), + QueryAlarmLevelCommand::new + ); + } + } +} diff --git a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/cmd/AlarmHistoryCommandSupport.java b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/cmd/AlarmHistoryCommandSupport.java new file mode 100644 index 00000000..e8877dfc --- /dev/null +++ b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/cmd/AlarmHistoryCommandSupport.java @@ -0,0 +1,23 @@ +package org.jetlinks.community.rule.engine.cmd; + +import org.jetlinks.core.command.AbstractCommandSupport; +import org.jetlinks.community.rule.engine.entity.AlarmHistoryInfo; +import org.jetlinks.community.rule.engine.service.AlarmHistoryService; +import org.jetlinks.sdk.server.commons.cmd.QueryPagerCommand; + +/** + * @author liusq + * @date 2024/4/12 + */ +public class AlarmHistoryCommandSupport extends AbstractCommandSupport { + public AlarmHistoryCommandSupport(AlarmHistoryService service) { + //分页查询 + registerHandler( + QueryPagerCommand + .createHandler( + metadata -> { + }, + cmd -> service.queryPager(cmd.asQueryParam())) + ); + } +} diff --git a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/cmd/AlarmRecordCommandSupport.java b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/cmd/AlarmRecordCommandSupport.java new file mode 100644 index 00000000..d0f50a4d --- /dev/null +++ b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/cmd/AlarmRecordCommandSupport.java @@ -0,0 +1,46 @@ +package org.jetlinks.community.rule.engine.cmd; + +import org.hswebframework.web.api.crud.entity.PagerResult; +import org.jetlinks.core.command.CommandHandler; +import org.jetlinks.core.command.CommandUtils; +import org.jetlinks.core.metadata.SimpleFunctionMetadata; +import org.jetlinks.community.command.CrudCommandSupport; +import org.jetlinks.community.rule.engine.entity.AlarmHandleHistoryEntity; +import org.jetlinks.community.rule.engine.entity.AlarmRecordEntity; +import org.jetlinks.community.rule.engine.service.AlarmHandleHistoryService; +import org.jetlinks.community.rule.engine.service.AlarmRecordService; +import org.jetlinks.sdk.server.commons.cmd.QueryPagerCommand; +import reactor.core.publisher.Mono; + +import java.util.function.Function; + +/** + * @author liusq + * @date 2024/4/12 + */ +public class AlarmRecordCommandSupport extends CrudCommandSupport { + public AlarmRecordCommandSupport(AlarmRecordService service, + AlarmHandleHistoryService historyService) { + super(service); + // 查询告警处理历史 + registerHandler( + QueryHandleHistoryPagerCommand + .createHandler(cmd -> historyService.queryPager(cmd.asQueryParam())) + ); + } + + public static class QueryHandleHistoryPagerCommand extends QueryPagerCommand { + public static CommandHandler>> createHandler(Function>> handler) { + return CommandHandler.of( + () -> { + SimpleFunctionMetadata metadata = new SimpleFunctionMetadata(); + metadata.setId(CommandUtils.getCommandIdByType(QueryHandleHistoryPagerCommand.class)); + metadata.setName("获取告警处理历史"); + return metadata; + }, + (cmd, ignore) -> handler.apply(cmd), + QueryHandleHistoryPagerCommand::new + ); + } + } +} diff --git a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/cmd/AlarmRuleBindCommandSupport.java b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/cmd/AlarmRuleBindCommandSupport.java new file mode 100644 index 00000000..39d2d6fd --- /dev/null +++ b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/cmd/AlarmRuleBindCommandSupport.java @@ -0,0 +1,15 @@ +package org.jetlinks.community.rule.engine.cmd; + +import org.jetlinks.community.command.CrudCommandSupport; +import org.jetlinks.community.rule.engine.entity.AlarmRuleBindEntity; +import org.jetlinks.community.rule.engine.service.AlarmRuleBindService; + +/** + * @author liusq + * @date 2024/4/12 + */ +public class AlarmRuleBindCommandSupport extends CrudCommandSupport { + public AlarmRuleBindCommandSupport(AlarmRuleBindService ruleBindService) { + super(ruleBindService); + } +} diff --git a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/cmd/RuleCommandSupport.java b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/cmd/RuleCommandSupport.java new file mode 100644 index 00000000..d656f609 --- /dev/null +++ b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/cmd/RuleCommandSupport.java @@ -0,0 +1,33 @@ +package org.jetlinks.community.rule.engine.cmd; + +/** + * @author liusq + * @date 2024/4/17 + */ +@Deprecated +public interface RuleCommandSupport { + /** + * 场景 + */ + String sceneService = "sceneService"; + + /** + * 告警配置 + */ + String alarmConfigService = "alarmConfigService"; + + /** + * 告警记录 + */ + String alarmRecordService = "alarmRecordService"; + + /** + * 告警历史 + */ + String alarmHistoryService = "alarmHistoryService"; + + /** + * 告警规则绑定 + */ + String alarmRuleBindService = "alarmRuleBindService"; +} diff --git a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/cmd/RuleCommandSupportManager.java b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/cmd/RuleCommandSupportManager.java new file mode 100644 index 00000000..327f4002 --- /dev/null +++ b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/cmd/RuleCommandSupportManager.java @@ -0,0 +1,27 @@ +package org.jetlinks.community.rule.engine.cmd; + +import org.jetlinks.community.command.InternalSdkServices; +import org.jetlinks.community.command.StaticCommandSupportManagerProvider; +import org.jetlinks.community.command.rule.RuleCommandServices; + +/** + * @author liusq + * @date 2024/4/11 + */ +public class RuleCommandSupportManager extends StaticCommandSupportManagerProvider { + + public RuleCommandSupportManager(SceneCommandSupport sceneCommandSupport, + AlarmHistoryCommandSupport alarmHistoryCommandSupport, + AlarmConfigCommandSupport alarmConfigCommandSupport, + AlarmRecordCommandSupport alarmRecordCommandSupport, + AlarmRuleBindCommandSupport alarmRuleBindCommandSupport, + AlarmCommandSupport alarmCommandSupport) { + super(InternalSdkServices.ruleService); + register(RuleCommandServices.sceneService, sceneCommandSupport); + register(RuleCommandServices.alarmHistoryService, alarmHistoryCommandSupport); + register(RuleCommandServices.alarmConfigService, alarmConfigCommandSupport); + register(RuleCommandServices.alarmRecordService, alarmRecordCommandSupport); + register(RuleCommandServices.alarmRuleBindService, alarmRuleBindCommandSupport); + register(RuleCommandServices.alarm, alarmCommandSupport); + } +} diff --git a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/cmd/SceneCommandSupport.java b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/cmd/SceneCommandSupport.java new file mode 100644 index 00000000..b03df350 --- /dev/null +++ b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/cmd/SceneCommandSupport.java @@ -0,0 +1,148 @@ +package org.jetlinks.community.rule.engine.cmd; + +import org.hswebframework.web.bean.FastBeanCopier; +import org.jetlinks.core.command.AbstractCommand; +import org.jetlinks.core.command.CommandHandler; +import org.jetlinks.core.command.CommandUtils; +import org.jetlinks.core.metadata.SimpleFunctionMetadata; +import org.jetlinks.core.metadata.SimplePropertyMetadata; +import org.jetlinks.core.metadata.types.ArrayType; +import org.jetlinks.community.command.CrudCommandSupport; +import org.jetlinks.community.rule.engine.entity.SceneEntity; +import org.jetlinks.community.rule.engine.scene.SceneRule; +import org.jetlinks.community.rule.engine.scene.SceneUtils; +import org.jetlinks.community.rule.engine.scene.Variable; +import org.jetlinks.community.rule.engine.scene.term.TermColumn; +import org.jetlinks.community.rule.engine.service.SceneService; +import org.jetlinks.sdk.server.commons.cmd.AddCommand; +import org.jetlinks.sdk.server.commons.cmd.DisabledCommand; +import org.jetlinks.sdk.server.commons.cmd.EnabledCommand; +import org.jetlinks.sdk.server.commons.cmd.QueryByIdCommand; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.util.Collections; +import java.util.HashMap; +import java.util.function.Function; + +/** + * @author liusq + * @date 2024/4/11 + */ +public class SceneCommandSupport extends CrudCommandSupport { + public SceneCommandSupport(SceneService service) { + super(service); + //新增 + registerHandler( + AddCommand + .createHandler( + metadata -> metadata + .setInputs(Collections.singletonList( + SimplePropertyMetadata.of("data", "数据列表", new ArrayType().elementType(createEntityType())) + )), + cmd -> Flux + .fromIterable(cmd.dataList((data) -> FastBeanCopier.copy(data, new SceneRule()))) + .flatMap(rule -> service.createScene(rule).thenReturn(rule))) + ); + + // 修改场景联动 + registerHandler( + UpdateSceneByIdCommand + .createHandler(cmd -> { + Object data = cmd.readable().getOrDefault("data", new HashMap<>()); + return service.updateScene(cmd.getId(), FastBeanCopier.copy(data, new SceneRule())).then(); + }) + ); + + + // 启用 + registerHandler( + EnabledCommand + .createHandler(cmd -> Flux + .fromIterable(cmd.getIds()) + .flatMap(service::enable) + .then()) + ); + + // 禁用 + registerHandler( + DisabledCommand + .createHandler(cmd -> Flux + .fromIterable(cmd.getIds()) + .flatMap(service::disabled) + .then()) + ); + + // 根据触发器解析出支持的条件列 + registerHandler( + ParseTermColumnCommand + .createHandler(cmd -> { + SceneRule rule = FastBeanCopier.copy(cmd + .readable() + .getOrDefault("data", new HashMap<>()), new SceneRule()); + return SceneUtils.parseTermColumns(rule); + }) + ); + + // 解析规则中输出的变量 + registerHandler( + ParseVariablesCommand + .createHandler(cmd -> { + SceneRule rule = FastBeanCopier.copy(cmd + .readable() + .getOrDefault("data", new HashMap<>()), new SceneRule()); + + Integer branch = (Integer) cmd.readable().getOrDefault("branch", 0); + Integer branchGroup = (Integer) cmd.readable().getOrDefault("branchGroup", 0); + Integer action = (Integer) cmd.readable().getOrDefault("action", 0); + + return SceneUtils.parseVariables(Mono.just(rule), branch, branchGroup, action); + }) + ); + } + + public static class UpdateSceneByIdCommand extends QueryByIdCommand> { + public static CommandHandler> createHandler(Function> handler) { + return CommandHandler.of( + () -> { + SimpleFunctionMetadata metadata = new SimpleFunctionMetadata(); + metadata.setId(CommandUtils.getCommandIdByType(UpdateSceneByIdCommand.class)); + metadata.setName("更新场景"); + return metadata; + }, + (cmd, ignore) -> handler.apply(cmd), + UpdateSceneByIdCommand::new + ); + } + } + + public static class ParseTermColumnCommand extends AbstractCommand, ParseTermColumnCommand> { + public static CommandHandler> createHandler(Function> handler) { + return CommandHandler.of( + () -> { + SimpleFunctionMetadata metadata = new SimpleFunctionMetadata(); + metadata.setId(CommandUtils.getCommandIdByType(ParseTermColumnCommand.class)); + metadata.setName("根据触发器解析出支持的条件列"); + return metadata; + }, + (cmd, ignore) -> handler.apply(cmd), + ParseTermColumnCommand::new + ); + } + } + + public static class ParseVariablesCommand extends AbstractCommand, ParseVariablesCommand> { + public static CommandHandler> createHandler(Function> handler) { + return CommandHandler.of( + () -> { + SimpleFunctionMetadata metadata = new SimpleFunctionMetadata(); + metadata.setId(CommandUtils.getCommandIdByType(ParseTermColumnCommand.class)); + metadata.setName("根据触发器解析出支持的条件列"); + return metadata; + }, + (cmd, ignore) -> handler.apply(cmd), + ParseVariablesCommand::new + ); + } + } +} diff --git a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/configuration/AlarmConfiguration.java b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/configuration/AlarmConfiguration.java new file mode 100644 index 00000000..7bc06c23 --- /dev/null +++ b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/configuration/AlarmConfiguration.java @@ -0,0 +1,15 @@ +package org.jetlinks.community.rule.engine.configuration; + +import org.jetlinks.community.rule.engine.alarm.AlarmProperties; +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.context.properties.EnableConfigurationProperties; + +/** + * @author bestfeng + */ +@AutoConfiguration +@EnableConfigurationProperties({AlarmProperties.class}) +public class AlarmConfiguration { + + +} diff --git a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/configuration/AlarmTargetConfiguration.java b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/configuration/AlarmTargetConfiguration.java index 2a1ab621..853a66cf 100644 --- a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/configuration/AlarmTargetConfiguration.java +++ b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/configuration/AlarmTargetConfiguration.java @@ -10,7 +10,7 @@ import org.springframework.context.annotation.Bean; /** * - * @author zhangji 2025/1/22 + * @author zhangji 2025/1/15 * @since 2.3 */ @AutoConfiguration 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 532c8110..bd2ca623 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,9 +1,15 @@ package org.jetlinks.community.rule.engine.configuration; +import org.hswebframework.ezorm.rdb.mapping.ReactiveRepository; import org.jetlinks.community.elastic.search.index.ElasticSearchIndexManager; +import org.jetlinks.community.elastic.search.service.AggregationService; import org.jetlinks.community.elastic.search.service.ElasticSearchService; +import org.jetlinks.community.rule.engine.alarm.AlarmHandler; +import org.jetlinks.community.rule.engine.cmd.*; +import org.jetlinks.community.rule.engine.entity.AlarmLevelEntity; +import org.jetlinks.community.rule.engine.entity.AlarmRuleBindEntity; import org.jetlinks.community.rule.engine.scene.*; -import org.jetlinks.community.rule.engine.service.ElasticSearchAlarmHistoryService; +import org.jetlinks.community.rule.engine.service.*; import org.jetlinks.core.event.EventBus; import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.autoconfigure.AutoConfiguration; @@ -25,14 +31,35 @@ public class RuleEngineManagerConfiguration { SceneFilter.composite(filters)); } + @Bean + @ConditionalOnClass(AlarmRuleBindEntity.class) + public RuleCommandSupportManager alarmRuleBindCommandSupportManager(SceneService sceneService, + AlarmConfigService configService, + ReactiveRepository alarmLevelRepository, + AlarmRecordService recordService, + AlarmHandleHistoryService handleHistoryService, + AlarmRuleBindService ruleBindService, + AlarmHistoryService historyService, + AlarmHandler alarmHandler) { + return new RuleCommandSupportManager( + new SceneCommandSupport(sceneService), + new AlarmHistoryCommandSupport(historyService), + new AlarmConfigCommandSupport(configService, alarmLevelRepository), + new AlarmRecordCommandSupport(recordService, handleHistoryService), + new AlarmRuleBindCommandSupport(ruleBindService), + new AlarmCommandSupport(alarmHandler) + ); + } + @Configuration(proxyBeanMethods = false) @ConditionalOnClass(ElasticSearchService.class) static class ElasticSearchAlarmHistoryConfiguration { @Bean(initMethod = "init") public ElasticSearchAlarmHistoryService alarmHistoryService(ElasticSearchService elasticSearchService, - ElasticSearchIndexManager indexManager) { - return new ElasticSearchAlarmHistoryService(indexManager, elasticSearchService); + ElasticSearchIndexManager indexManager, + AggregationService aggregationService) { + return new ElasticSearchAlarmHistoryService(indexManager, elasticSearchService, aggregationService); } } } diff --git a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/entity/AlarmConfigDetail.java b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/entity/AlarmConfigDetail.java index 2a2ee176..38ac3dc2 100644 --- a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/entity/AlarmConfigDetail.java +++ b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/entity/AlarmConfigDetail.java @@ -70,9 +70,6 @@ public class AlarmConfigDetail { @Schema(description = "更新时间") private Long modifyTime; - @Schema(description = "修改人名称") - private String modifierName; - public static AlarmConfigDetail of(AlarmConfigEntity entity) { return FastBeanCopier.copy(entity, new AlarmConfigDetail(), "sceneTriggerType"); } diff --git a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/entity/AlarmConfigEntity.java b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/entity/AlarmConfigEntity.java index 162aff97..3bc6a494 100755 --- a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/entity/AlarmConfigEntity.java +++ b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/entity/AlarmConfigEntity.java @@ -117,6 +117,7 @@ public class AlarmConfigEntity extends GenericEntity implements RecordCr configs.put(AlarmConstants.ConfigKey.targetType, getTargetType()); configs.put(AlarmConstants.ConfigKey.state, getState().name()); configs.put(AlarmConstants.ConfigKey.description, getDescription()); + return configs; } } diff --git a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/entity/AlarmHandleHistoryEntity.java b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/entity/AlarmHandleHistoryEntity.java index 6ecceeb6..82c4fb8e 100755 --- a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/entity/AlarmHandleHistoryEntity.java +++ b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/entity/AlarmHandleHistoryEntity.java @@ -3,23 +3,24 @@ package org.jetlinks.community.rule.engine.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.Comment; -import org.hswebframework.ezorm.rdb.mapping.annotation.DefaultValue; -import org.hswebframework.ezorm.rdb.mapping.annotation.EnumCodec; +import org.hswebframework.ezorm.rdb.mapping.annotation.*; import org.hswebframework.web.api.crud.entity.GenericEntity; import org.hswebframework.web.api.crud.entity.RecordCreationEntity; +import org.hswebframework.web.bean.FastBeanCopier; import org.hswebframework.web.crud.generator.Generators; import org.hswebframework.web.dict.EnumDict; +import org.hswebframework.web.utils.DigestUtils; import org.jetlinks.community.dictionary.Dictionary; import org.jetlinks.community.rule.engine.alarm.AlarmHandleInfo; -import org.jetlinks.community.rule.engine.enums.AlarmHandleType; +import org.jetlinks.community.rule.engine.enums.AlarmHandleState; import org.jetlinks.community.rule.engine.service.AlarmHandleTypeDictInit; +import org.jetlinks.community.terms.TermSpec; import org.springframework.util.StringUtils; import javax.persistence.Column; import javax.persistence.Index; import javax.persistence.Table; +import java.sql.JDBCType; @Getter @Setter @@ -43,11 +44,11 @@ public class AlarmHandleHistoryEntity extends GenericEntity implements R @ColumnType(javaType = String.class) private EnumDict handleType; - @Column(length = 256, nullable = false, updatable = false) + @Column(length = 256, nullable = false) @Schema(description = "说明") private String description; - @Column(updatable = false) + @Column @Schema(description = "处理时间") private Long handleTime; @@ -55,6 +56,63 @@ public class AlarmHandleHistoryEntity extends GenericEntity implements R @Schema(description = "告警时间") private Long alarmTime; + @Column(length = 32) + @Schema(description = "告警处理状态") + @EnumCodec + @ColumnType(javaType = String.class) + @DefaultValue("unprocessed") + private AlarmHandleState state; + + @Column(length = 32, updatable = false) + @Schema(description = "告警目标类型") + private String targetType; + + @Column(length = 64, updatable = false) + @Schema(description = "告警目标Id") + private String targetId; + + @Column + @Schema(description = "告警目标名称") + private String targetName; + + @Column(length = 32) + @Schema(description = "告警源类型") + private String sourceType; + + @Column(length = 64) + @Schema(description = "告警源Id") + private String sourceId; + + @Column + @Schema(description = "告警源名称") + private String sourceName; + + @Column + @Schema(description = "告警级别") + private Integer level; + + @Column + @ColumnType(jdbcType = JDBCType.LONGVARCHAR) + @JsonCodec + @Schema(description = "触发条件") + private TermSpec termSpec; + + @Column(length = 1024) + @Schema(description = "触发条件描述") + private String triggerDesc; + + @Column(length = 1024) + @Schema(description = "告警原因描述") + private String actualDesc; + + /** + * 告警流水。每次告警到结束告警唯一 + */ + @Column + @Schema(description = "告警流水号") + private String serialNumber; + + @Column(updatable = false) @Schema( description = "创建者ID(只读)" @@ -77,17 +135,28 @@ public class AlarmHandleHistoryEntity extends GenericEntity implements R ) private String creatorName; - @SuppressWarnings("all") - public static AlarmHandleHistoryEntity of(AlarmHandleInfo handleInfo) { - AlarmHandleHistoryEntity entity = new AlarmHandleHistoryEntity(); - entity.setAlarmId(handleInfo.getAlarmConfigId()); - entity.setAlarmRecordId(handleInfo.getAlarmRecordId()); - entity.setAlarmTime(handleInfo.getAlarmTime()); - String type = handleInfo.getType(); - entity.setHandleType(StringUtils.hasText(type) ? EnumDict.create(type) : AlarmHandleType.system); - entity.setDescription(handleInfo.getDescribe()); - entity.setHandleTime(handleInfo.getHandleTime() == null ? System.currentTimeMillis() : handleInfo.getHandleTime()); - return entity; + + public static String generateId(String... args) { + return DigestUtils.md5Hex(String.join("-", args)); } + public void generateId() { + if (alarmTime != null) { + setId(generateId(alarmRecordId, alarmTime.toString())); + } + } + + @SuppressWarnings("all") + public static AlarmHandleHistoryEntity of(AlarmHandleInfo handleInfo) { + AlarmHandleHistoryEntity entity = FastBeanCopier.copy(handleInfo, new AlarmHandleHistoryEntity()); + entity.setAlarmId(handleInfo.getAlarmConfigId()); + String type = handleInfo.getType(); + if (StringUtils.hasText(type)) { + entity.setHandleType(EnumDict.create(type)); + } + entity.setDescription(handleInfo.getDescribe() == null ? "" : handleInfo.getDescribe()); + entity.setState(handleInfo.getHandleState()); + entity.generateId(); + return entity; + } } diff --git a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/entity/AlarmHistoryInfo.java b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/entity/AlarmHistoryInfo.java index 97a22083..ccd01fda 100755 --- a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/entity/AlarmHistoryInfo.java +++ b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/entity/AlarmHistoryInfo.java @@ -9,7 +9,7 @@ import org.jetlinks.community.rule.engine.scene.SceneData; import org.jetlinks.community.terms.TermSpec; import java.io.Serializable; -import java.util.*; + @Getter @Setter @@ -106,29 +106,4 @@ public class AlarmHistoryInfo implements Serializable { info.setDescription(alarmConfig.getDescription()); return info; } - - @SuppressWarnings("all") - @Deprecated - static List> convertBindings(AlarmTargetInfo targetInfo, - SceneData data, - AlarmConfigEntity alarmConfig) { - List> bindings = new ArrayList<>(); - - bindings.addAll((List) data.getOutput().getOrDefault("_bindings", Collections.emptyList())); - - //添加告警配置创建人到bindings中。作为用户维度信息 - Map userDimension = new HashMap<>(2); - userDimension.put("type", "user"); - userDimension.put("id", alarmConfig.getCreatorId()); - bindings.add(userDimension); - //添加组织纬度信息 - if ("org".equals(alarmConfig.getTargetType())) { - Map orgDimension = new HashMap<>(2); - userDimension.put("type", targetInfo.getTargetType()); - userDimension.put("id", targetInfo.getTargetId()); - bindings.add(userDimension); - } - return bindings; - } - } diff --git a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/entity/AlarmLevelEntity.java b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/entity/AlarmLevelEntity.java index 5aac19b1..ec9b3dfd 100755 --- a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/entity/AlarmLevelEntity.java +++ b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/entity/AlarmLevelEntity.java @@ -35,7 +35,6 @@ public class AlarmLevelEntity extends GenericEntity { @Schema(description = "说明") private String description; - public static AlarmLevelEntity of(List levels){ AlarmLevelEntity entity = new AlarmLevelEntity(); entity.setLevels(levels); diff --git a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/entity/AlarmRecordEntity.java b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/entity/AlarmRecordEntity.java index 30b65fe2..a0a5f480 100755 --- a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/entity/AlarmRecordEntity.java +++ b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/entity/AlarmRecordEntity.java @@ -5,6 +5,8 @@ import lombok.Getter; import lombok.Setter; import org.hswebframework.ezorm.rdb.mapping.annotation.*; import org.hswebframework.web.api.crud.entity.GenericEntity; +import org.hswebframework.web.api.crud.entity.RecordCreationEntity; +import org.hswebframework.web.crud.generator.Generators; import org.hswebframework.web.dict.EnumDict; import org.hswebframework.web.utils.DigestUtils; import org.jetlinks.community.dictionary.Dictionary; @@ -19,12 +21,14 @@ import java.sql.JDBCType; @Getter @Setter -@Table(name = "alarm_record",indexes = { - @Index(name = "idx_alarm_rec_t_type",columnList = "targetType"), - @Index(name = "idx_alarm_rec_t_key",columnList = "targetKey") +@Table(name = "alarm_record", indexes = { + @Index(name = "idx_alarm_rec_t_idtype", columnList = "targetId,targetType"), + @Index(name = "idx_alarm_rec_t_id", columnList = "targetId"), + @Index(name = "idx_alarm_rec_t_type", columnList = "targetType"), + @Index(name = "idx_alarm_rec_t_key", columnList = "targetKey") }) @Comment("告警记录") -public class AlarmRecordEntity extends GenericEntity { +public class AlarmRecordEntity extends GenericEntity implements RecordCreationEntity { @Column(length = 64, nullable = false, updatable = false) @@ -78,9 +82,13 @@ public class AlarmRecordEntity extends GenericEntity { private String actualDesc; @Column - @Schema(description = "最近一次告警时间") + @Schema(description = "当前首次告警时间") private Long alarmTime; + @Column + @Schema(description = "最近一次告警时间") + private Long lastAlarmTime; + @Column @Schema(description = "处理时间") private Long handleTime; @@ -96,7 +104,6 @@ public class AlarmRecordEntity extends GenericEntity { @DefaultValue("normal") private AlarmRecordState state; - @Column(length = 32) @Dictionary(AlarmHandleTypeDictInit.DICT_ID) @Schema(description = "告警处理类型") @@ -134,4 +141,18 @@ public class AlarmRecordEntity extends GenericEntity { } + @Column(updatable = false) + @DefaultValue(generator = Generators.CURRENT_TIME) + @Schema( + description = "创建时间(只读)" + , accessMode = Schema.AccessMode.READ_ONLY + ) + private Long createTime; + + @Column(length = 64, updatable = false) + @Schema( + description = "创建者ID(只读)" + , accessMode = Schema.AccessMode.READ_ONLY + ) + private String creatorId; } diff --git a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/entity/RuleInstanceEntity.java b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/entity/RuleInstanceEntity.java index 0e0ca459..865752ed 100755 --- a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/entity/RuleInstanceEntity.java +++ b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/entity/RuleInstanceEntity.java @@ -68,13 +68,19 @@ public class RuleInstanceEntity extends GenericEntity implements RecordC @DefaultValue("1") private Integer modelVersion; - @Column(name = "create_time") - @Schema(description = "创建时间") + @Column(name = "create_time", updatable = false) @DefaultValue(generator = Generators.CURRENT_TIME) + @Schema( + description = "创建时间(只读)" + , accessMode = Schema.AccessMode.READ_ONLY + ) private Long createTime; - @Column(name = "creator_id") - @Schema(description = "创建者ID") + @Column(name = "creator_id", updatable = false) + @Schema( + description = "创建者ID(只读)" + , accessMode = Schema.AccessMode.READ_ONLY + ) private String creatorId; @Column(length = 64) diff --git a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/enums/AlarmHandleState.java b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/enums/AlarmHandleState.java new file mode 100644 index 00000000..20d6b017 --- /dev/null +++ b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/enums/AlarmHandleState.java @@ -0,0 +1,20 @@ +package org.jetlinks.community.rule.engine.enums; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.hswebframework.web.dict.I18nEnumDict; + +@AllArgsConstructor +@Getter +public enum AlarmHandleState implements I18nEnumDict { + + processed("已处理"), + unprocessed("未处理"); + + private final String text; + + @Override + public String getValue() { + return name(); + } +} diff --git a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/enums/AlarmHandleType.java b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/enums/AlarmHandleType.java index 6dd6e2f6..31360031 100755 --- a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/enums/AlarmHandleType.java +++ b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/enums/AlarmHandleType.java @@ -2,14 +2,14 @@ package org.jetlinks.community.rule.engine.enums; import lombok.AllArgsConstructor; import lombok.Getter; -import org.hswebframework.web.dict.EnumDict; +import org.hswebframework.web.dict.I18nEnumDict; /** * @author bestfeng */ @AllArgsConstructor @Getter -public enum AlarmHandleType implements EnumDict { +public enum AlarmHandleType implements I18nEnumDict { system("系统"), user("人工"); 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 index 7b7029ec..6065b123 100644 --- 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 @@ -44,11 +44,12 @@ public class AlarmRecordMeasurementProvider extends StaticMeasurementProvider { public String[] getTags(AlarmHistoryInfo info) { Map tagMap = Maps.newLinkedHashMap(); - tagMap.put(AlarmConstants.ConfigKey.targetId, info.getTargetId()); + // tagMap.put(AlarmConstants.ConfigKey.targetId, info.getTargetId()); + //只需要记录targetType,用于统计 设备,产品等告警数量. tagMap.put(AlarmConstants.ConfigKey.targetType, info.getTargetType()); - tagMap.put(AlarmConstants.ConfigKey.targetName, info.getTargetName()); - - tagMap.put(AlarmConstants.ConfigKey.alarmConfigId, info.getAlarmConfigId()); + //tagMap.put(AlarmConstants.ConfigKey.targetName, info.getTargetName()); +// tagMap.put(AlarmConstants.ConfigKey.alarmConfigId, info.getAlarmConfigId()); + tagMap.put(PropertyConstants.creatorId.getKey(), info.getCreatorId()); return ConverterUtils.convertMapToTags(tagMap); } -} \ No newline at end of file +} 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 index 739d32fa..739b43b9 100644 --- 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 @@ -15,6 +15,7 @@ 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 org.springframework.util.StringUtils; import reactor.core.publisher.Flux; import java.time.LocalDateTime; @@ -69,14 +70,18 @@ public class AlarmRecordRankMeasurement extends StaticMeasurement { } public AggregationQueryParam createQueryParam(MeasurementParameter parameter) { - return AggregationQueryParam - .of() + AggregationQueryParam aggregationQueryParam = AggregationQueryParam.of(); + aggregationQueryParam.setTimeProperty("alarmTime"); + String targetType = parameter.getString("targetType").orElse(null); + String targetId = parameter.getString("targetId").orElse(null); + return aggregationQueryParam .groupBy(parameter.getString("group", "targetId")) - .sum("count", "count") + .count("targetId", "count") .agg("targetId", Aggregation.TOP) + .agg("targetName", Aggregation.TOP) .filter(query -> query - .where("name", "record-agg") - .where("targetType", parameter.getString("targetType", null)) + .when(StringUtils.hasText(targetType), q -> q.and("targetType",targetType)) + .when(StringUtils.hasText(targetId), q -> q.and("targetId",targetId)) ) .limit(parameter.getInt("limit").orElse(1)) .from(parameter 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 index 58d0f804..2fa2535c 100644 --- 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 @@ -9,6 +9,7 @@ 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 org.springframework.util.StringUtils; import reactor.core.publisher.Flux; import java.time.LocalDateTime; @@ -39,8 +40,7 @@ public class AlarmRecordTrendMeasurement extends StaticMeasurement { .add("to", "时间至", "", StringType.GLOBAL); - - class AggRecordTrendDimension implements MeasurementDimension{ + class AggRecordTrendDimension implements MeasurementDimension { @Override public DimensionDefinition getDefinition() { @@ -63,16 +63,16 @@ public class AlarmRecordTrendMeasurement extends StaticMeasurement { } public AggregationQueryParam createQueryParam(MeasurementParameter parameter) { + String targetType = parameter.getString("targetType").orElse(null); + String targetId = parameter.getString("targetId").orElse(null); 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)) + .when(StringUtils.hasText(targetType), q -> q.and("targetType", targetType)) + .when(StringUtils.hasText(targetId), q -> q.and("targetId",targetId)) ) .limit(parameter.getInt("limit").orElse(1)) .from(parameter @@ -90,12 +90,12 @@ public class AlarmRecordTrendMeasurement extends StaticMeasurement { 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()); + .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 index fa176201..fd61302b 100755 --- 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 @@ -18,6 +18,6 @@ public interface AlarmTimeSeriesMetric { * @return 度量标识 */ static TimeSeriesMetric alarmStreamMetrics() { - return TimeSeriesMetric.of("alarm_metrics"); + return TimeSeriesMetric.of("alarm_history"); } } diff --git a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/scene/DeviceOperation.java b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/scene/DeviceOperation.java index ab5d6658..6771e0ee 100644 --- a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/scene/DeviceOperation.java +++ b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/scene/DeviceOperation.java @@ -5,7 +5,6 @@ import lombok.AllArgsConstructor; import lombok.Getter; import lombok.Setter; import org.hswebframework.web.i18n.LocaleUtils; -import org.jetlinks.community.rule.engine.utils.TermColumnUtils; import org.jetlinks.core.message.function.FunctionInvokeMessage; import org.jetlinks.core.message.function.FunctionParameter; import org.jetlinks.core.message.property.ReadPropertyMessage; @@ -14,17 +13,18 @@ import org.jetlinks.core.metadata.DataType; import org.jetlinks.core.metadata.PropertyMetadata; import org.jetlinks.core.metadata.types.BooleanType; import org.jetlinks.core.metadata.types.DateTimeType; -import org.jetlinks.core.metadata.types.ObjectType; import org.jetlinks.core.metadata.types.UnknownType; import org.jetlinks.core.things.ThingMetadata; import org.jetlinks.community.TimerSpec; import org.jetlinks.community.rule.engine.scene.term.TermColumn; +import org.jetlinks.community.rule.engine.utils.TermColumnUtils; import org.springframework.util.Assert; import javax.validation.constraints.NotNull; -import java.util.*; -import java.util.function.BiConsumer; -import java.util.stream.Collectors; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; import static org.jetlinks.core.metadata.SimplePropertyMetadata.of; diff --git a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/scene/SceneAction.java b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/scene/SceneAction.java index 1fc29c95..6dab1303 100644 --- a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/scene/SceneAction.java +++ b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/scene/SceneAction.java @@ -8,13 +8,15 @@ import org.apache.commons.collections4.MapUtils; import org.hswebframework.ezorm.core.param.Term; import org.hswebframework.web.bean.FastBeanCopier; import org.hswebframework.web.i18n.LocaleUtils; -import org.jetlinks.core.metadata.DataType; -import org.jetlinks.core.metadata.PropertyMetadata; -import org.jetlinks.core.metadata.types.ObjectType; import org.jetlinks.community.reactorql.term.TermTypes; import org.jetlinks.community.rule.engine.scene.internal.actions.*; +import org.jetlinks.community.terms.I18nSpec; import org.jetlinks.community.terms.TermSpec; import org.jetlinks.community.utils.ConverterUtils; +import org.jetlinks.core.metadata.DataType; +import org.jetlinks.core.metadata.PropertyMetadata; +import org.jetlinks.core.metadata.types.EnumType; +import org.jetlinks.core.metadata.types.ObjectType; import org.jetlinks.rule.engine.api.model.RuleNodeModel; import reactor.core.publisher.Flux; @@ -29,6 +31,11 @@ import java.util.function.Consumer; import static org.hswebframework.web.i18n.LocaleUtils.resolveMessage; import static org.jetlinks.community.rule.engine.scene.SceneRule.createBranchActionId; +/** + * @see org.jetlinks.community.rule.engine.executor.TimerTaskExecutorProvider + * @see org.jetlinks.community.rule.engine.executor.DelayTaskExecutorProvider + * @see org.jetlinks.community.rule.engine.executor.DeviceMessageSendTaskExecutorProvider + */ @Getter @Setter public class SceneAction implements Serializable { @@ -121,9 +128,9 @@ public class SceneAction implements Serializable { .createVariable(actionConfig()) .collectList() .filter(CollectionUtils::isNotEmpty) + .as(LocaleUtils::transform) .map(list -> SceneAction.createVariable(branchIndex, group, index, list)) - .flux() - .as(LocaleUtils::transform); + .flux(); } @@ -216,6 +223,8 @@ public class SceneAction implements Serializable { variable.setType(dataType.getType()); variable.setTermTypes(TermTypes.lookup(dataType)); variable.setColumn(id); + variable.setFullNameCode(I18nSpec.of(null, variable.getName())); + if (dataType instanceof ObjectType) { List children = new ArrayList<>(); for (PropertyMetadata property : ((ObjectType) dataType).getProperties()) { @@ -230,6 +239,9 @@ public class SceneAction implements Serializable { } variable.setChildren(children); } + if (dataType instanceof EnumType) { + variable.withOption("elements", ((EnumType) dataType).getElements()); + } return variable; diff --git a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/scene/SceneActionProvider.java b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/scene/SceneActionProvider.java index 9ccf73e8..43484668 100644 --- a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/scene/SceneActionProvider.java +++ b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/scene/SceneActionProvider.java @@ -1,15 +1,22 @@ package org.jetlinks.community.rule.engine.scene; -import org.jetlinks.core.utils.Reactors; -import org.jetlinks.core.utils.SerializeUtils; import org.jetlinks.community.rule.engine.alarm.AlarmConstants; import org.jetlinks.community.terms.TermSpec; +import org.jetlinks.core.utils.Reactors; +import org.jetlinks.core.utils.SerializeUtils; import org.jetlinks.rule.engine.api.model.RuleNodeModel; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import java.util.List; +/** + * 场景动作提供商,用于提供场景动作的配置,变量,规则节点等信息 + * + * @param 配置类型 + * @author zhouhao + * @since 2.1 + */ public interface SceneActionProvider { /** @@ -63,6 +70,16 @@ public interface SceneActionProvider { ); } + /** + * 判断当前服务是否支持此触发器 + * + * @return 是否支持 + * @since 2.3 + */ + default Mono isSupported() { + return Reactors.ALWAYS_TRUE; + } + /** * 获取详细类型, * 用于区分同一个类型支持的多个执行动作 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 index c32ad470..e16fd6c4 100644 --- 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 @@ -10,12 +10,18 @@ import org.jetlinks.community.rule.engine.commons.ShakeLimit; import java.io.Serializable; import java.util.ArrayList; import java.util.List; +import java.util.Map; import java.util.stream.Collectors; @Getter @Setter public class SceneConditionAction implements Serializable { + /** + * @see org.jetlinks.community.rule.engine.scene.term.TermColumn + * @see org.jetlinks.community.reactorql.term.TermType + * @see org.jetlinks.community.reactorql.term.TermValue + */ @Schema(description = "条件") private List when; @@ -31,6 +37,12 @@ public class SceneConditionAction implements Serializable { @Schema(description = "分支ID") private Integer branchId; + @Schema(description = "分支名称") + private String branchName; + + @Schema(description = "拓展信息") + private Map options; + //仅用于设置到reactQl sql的column中 public List createContextTerm() { List contextTerm = new ArrayList<>(); @@ -50,6 +62,16 @@ public class SceneConditionAction implements Serializable { if (CollectionUtils.isNotEmpty(when)) { contextTerm.addAll(when); } + // 分支触发条件需要查询的列 + contextTerm.addAll(SceneAction + .parseColumnFromOptions(options) + .stream() + .map(column -> { + Term term = new Term(); + term.setColumn(column); + return term; + }) + .collect(Collectors.toList())); return contextTerm; } } 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 5fed42b5..93cfddac 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 @@ -35,6 +35,7 @@ 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 org.springframework.util.StringUtils; import reactor.core.Disposable; import reactor.core.Disposables; import reactor.core.publisher.Flux; @@ -68,6 +69,7 @@ public class SceneRule implements Serializable { public static final String SOURCE_ID_KEY = "sourceId"; public static final String SOURCE_NAME_KEY = "sourceName"; + public static final String TRIGGER_TYPE = "triggerType"; @Schema(description = "场景ID") @NotBlank(message = "error.scene_rule_id_cannot_be_blank") @@ -81,6 +83,11 @@ public class SceneRule implements Serializable { @NotNull(message = "error.scene_rule_trigger_cannot_be_null") private Trigger trigger; + /** + * @see org.jetlinks.community.rule.engine.scene.term.TermColumn + * @see org.jetlinks.community.reactorql.term.TermType + * @see org.jetlinks.community.reactorql.term.TermValue + */ @Schema(description = "触发条件") private List terms; @@ -241,6 +248,8 @@ public class SceneRule implements Serializable { }); //满足条件后的输出操作 List, Mono>> outs = new ArrayList<>(); + //不满足时执行 + Function, Mono> nonMatch = null; List groups = branch.getThen(); int thenIndex = 0; @@ -283,6 +292,20 @@ public class SceneRule implements Serializable { .unicast() .onBackpressureBuffer(Queues.>unboundedMultiproducer().get()); + Flux> resetSignal = Flux.never(); + //连续满足条件才触发,不满足则立即重置 + if (shakeLimit.isContinuous()) { + Sinks.Many> resetSinks = Sinks + .many() + .multicast() + .directBestEffort(); + resetSignal = resetSinks.asFlux(); + nonMatch = data -> { + resetSinks.tryEmitNext(data); + return Mono.empty(); + }; + } + //动作输出 Flux, Mono>> _outs = Flux.fromIterable(new ArrayList<>(actionOuts)); Function, Mono> handler = @@ -292,9 +315,11 @@ public class SceneRule implements Serializable { disposable.add( trigger .provider() - .shakeLimit(DigestUtils.md5Hex(id + ":" + _branchIndex), - sinks.asFlux(), - shakeLimit) + .shakeLimit( + DigestUtils.md5Hex(id + ":" + _branchIndex), + sinks.asFlux(), + shakeLimit, + resetSignal) .flatMap(res -> { res.getElement().put("_total", res.getTimes()); return handler.apply(res.getElement()); @@ -317,7 +342,7 @@ public class SceneRule implements Serializable { Flux, Mono>> outFlux = Flux.fromIterable(outs); Function, Mono> fOut = out -> outFlux.flatMap(fun -> fun.apply(out)).then(); - + Function, Mono> fNonMatch = nonMatch; Function, Mono> handler = data -> filter @@ -325,7 +350,14 @@ public class SceneRule implements Serializable { .flatMap(match -> { // 满足条件后执行输出 if (match) { - return fOut.apply(data).thenReturn(true); + return fOut + .apply(data) + .then(Reactors.ALWAYS_TRUE); + } + if (fNonMatch != null) { + return fNonMatch + .apply(data) + .then(Reactors.ALWAYS_FALSE); } return Reactors.ALWAYS_FALSE; }); @@ -364,10 +396,13 @@ public class SceneRule implements Serializable { disposable.add( sourceData - .flatMap(data -> fLast - .apply(data) - .as(tracer) - .contextWrite(ctx -> TraceHolder.readToContext(ctx, data))) + .flatMap( + data -> fLast + .apply(data) + .as(tracer) + .contextWrite(ctx -> TraceHolder.readToContext(ctx, data)), + 8, + 8) .subscribe() ); @@ -520,7 +555,7 @@ public class SceneRule implements Serializable { .expand(var -> var.getChildren() == null ? Flux.empty() : Flux.fromIterable(var.getChildren())) - .collectMap(Variable::getColumn, Function.identity()); + .collectMap(Variable::getId, Function.identity()); RuleNodeModel actionNode = new RuleNodeModel(); actionNode.setId(createBranchActionId(branchIndex, groupIndex, actionIndex)); @@ -633,11 +668,17 @@ public class SceneRule implements Serializable { } if (var != null) { - spec.setColumn(var.getId()); + spec.setColumn(var.getColumn()); spec.setMetadata(var.isMetadata()); spec.setDisplayCode(var.getFullNameCode()); } + // 重构条件字段名,用于后续获取告警原因触发字段取不到 + if (CollectionUtils.isEmpty(spec.getChildren()) && StringUtils.hasText(spec.getColumn())) { + spec.setColumn(newTerm.getColumn().substring(newTerm.getColumn().indexOf("['") + 2, + newTerm.getColumn().length() - 2)); + } + } private List getTermList() { 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 e94fdcd7..8afec89e 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 @@ -6,15 +6,15 @@ 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; +import org.jetlinks.community.PropertyConstants; import org.jetlinks.core.event.EventBus; import org.jetlinks.core.event.Subscription; import org.jetlinks.core.trace.TraceHolder; import org.jetlinks.core.utils.FluxUtils; -import org.jetlinks.community.PropertyConstants; import org.jetlinks.reactor.ql.ReactorQL; import org.jetlinks.reactor.ql.ReactorQLContext; -import org.jetlinks.reactor.ql.ReactorQLRecord; import org.jetlinks.rule.engine.api.RuleData; +import org.jetlinks.rule.engine.api.RuleDataHelper; import org.jetlinks.rule.engine.api.task.ExecutionContext; import org.jetlinks.rule.engine.api.task.TaskExecutor; import org.jetlinks.rule.engine.api.task.TaskExecutorProvider; @@ -25,7 +25,8 @@ import reactor.core.publisher.Mono; import java.time.Duration; import java.util.Map; -import java.util.function.Consumer; + +import static org.jetlinks.community.rule.engine.scene.SceneRule.TRIGGER_TYPE; @Slf4j @AllArgsConstructor @@ -126,7 +127,7 @@ public class SceneTaskExecutorProvider implements TaskExecutorProvider { return context .getInput() .accept() - .flatMap(RuleData::dataToMap); + .concatMap(RuleData::dataToMap, 0); } }); } @@ -147,7 +148,7 @@ public class SceneTaskExecutorProvider implements TaskExecutorProvider { source = context .getInput() .accept() - .flatMap(RuleData::dataToMap); + .concatMap(RuleData::dataToMap, 0); } else { if (log.isInfoEnabled()) { log.info("init scene [{}:{}], sql:{}", ruleId, ruleName, request.toNativeSql()); @@ -164,7 +165,16 @@ public class SceneTaskExecutorProvider implements TaskExecutorProvider { .sql(request.getSql()) .build() .start(qlContext) - .map(ReactorQLRecord::asMap); + .map(record -> RuleDataHelper.toContextMap(context.newRuleData(record.asMap()))) + .onErrorContinue((err, val) -> { + context + .logger() + .error("reactor ql execute failed", err); + @SuppressWarnings("all") + Disposable disp = context + .onError(err, null) + .subscribe(); + }); } // 分支条件 @@ -254,6 +264,7 @@ public class SceneTaskExecutorProvider implements TaskExecutorProvider { SceneData sceneData = new SceneData(); sceneData.setId(IDGenerator.RANDOM.generate()); sceneData.setRule(rule); + map.put(TRIGGER_TYPE, rule.getTrigger().getType()); sceneData.setOutput(map); return sceneData; } @@ -264,6 +275,12 @@ public class SceneTaskExecutorProvider implements TaskExecutorProvider { @Override public Mono execute(RuleData ruleData) { + Object data = ruleData.getData(); + if (data instanceof Map) { + @SuppressWarnings("all") + Map map = (Map) data; + map.put(TRIGGER_TYPE, rule.getTrigger().getType()); + } //分支 if (useBranch) { if (log.isDebugEnabled()) { diff --git a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/scene/SceneTriggerProvider.java b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/scene/SceneTriggerProvider.java index 2b06af87..f7457c04 100644 --- a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/scene/SceneTriggerProvider.java +++ b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/scene/SceneTriggerProvider.java @@ -4,20 +4,27 @@ import org.hswebframework.ezorm.core.param.Term; import org.hswebframework.ezorm.rdb.executor.SqlRequest; import org.hswebframework.ezorm.rdb.operator.builder.fragments.SqlFragments; import org.hswebframework.web.bean.FastBeanCopier; +import org.jetlinks.core.event.EventBus; +import org.jetlinks.core.event.Subscription; +import org.jetlinks.core.event.TopicPayload; +import org.jetlinks.core.utils.Reactors; import org.jetlinks.community.rule.engine.commons.ShakeLimit; import org.jetlinks.community.rule.engine.commons.ShakeLimitResult; import org.jetlinks.community.rule.engine.commons.impl.SimpleShakeLimitProvider; import org.jetlinks.community.rule.engine.scene.term.TermColumn; import org.jetlinks.community.terms.TermSpec; -import org.jetlinks.core.utils.Reactors; import org.jetlinks.rule.engine.api.model.RuleModel; import org.jetlinks.rule.engine.api.model.RuleNodeModel; +import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.function.Function; import static org.jetlinks.community.rule.engine.scene.SceneRule.SOURCE_ID_KEY; @@ -58,12 +65,62 @@ public interface SceneTriggerProvider terms, boolean hasFilter); + /** + * 订阅触发场景的原始数据流 + * + * @param eventBus 事件总线 + * @param subscriber 订阅者标识 + * @param userId 用户ID + * @param topic topic + * @return 数据流 + * @since 2.3 + */ + default Flux> subscribe(EventBus eventBus, + String subscriber, + String userId, + String topic) { + return Flux.error(new UnsupportedOperationException("not support")); + } + + + /** + * 判断当前服务是否支持此触发器 + * + * @return 是否支持 + * @since 2.3 + */ + default Mono isSupported() { + return Reactors.ALWAYS_TRUE; + } + + /** + * 处理SQL执行收到的数据, + * 通过{@link SceneTriggerProvider#createSql(TriggerConfig, List, boolean)}和{@link EventBus} + * 订阅到的数据将调用此方法处理. + * + *
{@code
+     *  select * from "/device/p1/d1/event/e1"
+     * }
+ * + * @param payload 事件数据 + * @param output 处理结果接收器 + * @return void + * @see SceneTriggerProvider#createSql(TriggerConfig, List, boolean) + * @see EventBus#subscribe(Subscription, Function) + * @since 2.3 + */ + default Mono handleSqlResult(TopicPayload payload, + Consumer> output) { + return Mono.fromRunnable(() -> output.accept(payload.bodyToJson(true))); + } + /** * 创建过滤条件.不含where前缀. * * @param config 配置 * @param terms 条件 * @return 条件SQL语句 + * @see org.jetlinks.community.utils.ReactorUtils#createFilter(List) */ SqlFragments createFilter(E config, List terms); @@ -122,18 +179,36 @@ public interface SceneTriggerProvider parseTermColumns(E config); /** - * 配置信息 + * 防抖 */ default Flux>> shakeLimit(String key, Flux> source, ShakeLimit limit) { + return shakeLimit(key, + source, + limit, + Mono.never()); + } + + + default Flux>> shakeLimit(String key, + Flux> source, + ShakeLimit limit, + Publisher> resetSignal) { return SimpleShakeLimitProvider .GLOBAL - .shakeLimit(key, - source.groupBy( - data -> String.valueOf(data.getOrDefault(SOURCE_ID_KEY, "null")), - Integer.MAX_VALUE), - limit); + .shakeLimit( + key, + source.groupBy( + data -> String.valueOf(data.getOrDefault(SOURCE_ID_KEY, "null")), + Integer.MAX_VALUE), + limit, + groupKey -> Flux + .from(resetSignal) + .filter(map -> Objects.equals( + String.valueOf(map.getOrDefault(SOURCE_ID_KEY, "null")), + groupKey + ))); } interface TriggerConfig { diff --git a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/scene/SceneUtils.java b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/scene/SceneUtils.java index 100ab5e2..b3010e3f 100644 --- a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/scene/SceneUtils.java +++ b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/scene/SceneUtils.java @@ -1,27 +1,28 @@ package org.jetlinks.community.rule.engine.scene; +import com.google.common.collect.Lists; import org.apache.commons.collections4.CollectionUtils; import org.hswebframework.ezorm.core.param.Term; -import org.hswebframework.ezorm.rdb.executor.SqlRequest; -import org.hswebframework.ezorm.rdb.operator.builder.fragments.NativeSql; import org.jetlinks.community.PropertyMetric; -import org.jetlinks.community.reactorql.function.FunctionSupport; -import org.jetlinks.community.reactorql.term.TermType; +import org.jetlinks.community.reactorql.term.TermUtils; +import org.jetlinks.community.reactorql.term.TermValue; +import org.jetlinks.community.relation.utils.VariableSource; import org.jetlinks.community.rule.engine.executor.device.DeviceSelectorProviders; +import org.jetlinks.community.rule.engine.executor.device.DeviceSelectorSpec; import org.jetlinks.community.rule.engine.scene.term.TermColumn; -import org.jetlinks.community.rule.engine.scene.value.TermValue; import org.jetlinks.community.rule.engine.web.response.SelectorInfo; import org.springframework.util.StringUtils; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import java.util.*; -import java.util.function.BiFunction; import java.util.function.Function; import java.util.stream.Collectors; public class SceneUtils { + //关键字,用于重构别名时条件判断 + private final static List keys = Lists.newArrayList("current", "recent", "last", "lastTime"); public static String createColumnAlias(String prefix, String column, boolean wrapColumn) { if (!column.contains(".")) { @@ -29,15 +30,28 @@ public class SceneUtils { } String[] arr = column.split("[.]"); String alias; - //prefix.temp.current - if (prefix.equals(arr[0])) { - String property = arr[1]; - alias = property + "_" + arr[arr.length - 1]; - } else { - if (arr.length > 1) { - alias = String.join("_", Arrays.copyOfRange(arr, 1, arr.length)); + //prefix.temp.current -> temp_current + if (StringUtils.hasText(prefix) && prefix.equals(arr[0])) { + if (arr.length == 2) { + alias = arr[1]; } else { - alias = column.replace(".", "_"); + alias = String.join("_", Arrays.copyOfRange(arr, 1, arr.length)); + } + } else { + if (!isContainThis(arr)) { + //someObj.obj.column -> obj_column + if (arr.length > 1 && !isContainKey(arr, keys)) { + alias = String.join("_", Arrays.copyOfRange(arr, 1, arr.length)); + } else { + //someObj.obj.column.recent -> someObj_obj_column_recent + alias = column.replace(".", "_"); + } + } else { + //直接将 xx.this.recent 解析中间的this忽略解析为 xx_recent, latest同理 + String[] array = Arrays.stream(arr) + .filter(str -> !"this".equals(str)) + .toArray(String[]::new); + alias = String.join("_", array); } } return wrapColumn ? wrapColumnName(alias) : alias; @@ -71,7 +85,7 @@ public class SceneUtils { public static List parseVariable(List terms, List columns) { //平铺条件 - Map> termCache = expandTerm(terms); + Map> termCache = TermUtils.expandTerm(terms); //解析变量 List variables = new ArrayList<>(termCache.size()); @@ -82,34 +96,6 @@ public class SceneUtils { return variables; } - public static Map> expandTerm(List terms) { - Map> termCache = new LinkedHashMap<>(); - expandTerm(terms, termCache); - return termCache; - } - - private static void expandTerm(List terms, Map> container) { - if (terms == null) { - return; - } - for (Term term : terms) { - if (StringUtils.hasText(term.getColumn())) { - List termList = container.get(term.getColumn()); - if (termList == null){ - List list = new ArrayList<>(); - list.add(term); - container.put(term.getColumn(),list); - } else { - termList.add(term); - container.put(term.getColumn(), termList); - } - } - if (term.getTerms() != null) { - expandTerm(term.getTerms(), container); - } - } - } - private static List columnToVariable(String prefixName, TermColumn column, Function> termSupplier) { @@ -119,7 +105,7 @@ public class SceneUtils { if (CollectionUtils.isEmpty(column.getChildren())) { List termList = termSupplier.apply(column.getColumn()); variables.add(Variable.of(column.getVariable("_"), variableName) - .with(column) + .with(column) ); if (termList != null && !termList.isEmpty()) { for (Term term : termList) { @@ -130,13 +116,13 @@ public class SceneUtils { if (property != null && metric != null && termValue.getSource() == TermValue.Source.metric) { // temp_metric variables.add(Variable.of( - property + "_metric_" + termValue.getMetric(), - (prefixName == null ? column.getName() : prefixName) + "_指标_" + metric.getName()) - .withTermType(column.getTermTypes()) - .withColumn(column.getColumn()) - .withCode(column.getCode()) - .withFullNameCode(column.getFullNameCode().copy()) - .withMetadata(column.isMetadata()) + property + "_metric_" + termValue.getMetric(), + (prefixName == null ? column.getName() : prefixName) + "_指标_" + metric.getName()) + .withTermType(column.getTermTypes()) + .withColumn(column.getColumn()) + .withCode(column.getCode()) + .withFullNameCode(column.getFullNameCode().copy()) + .withMetadata(column.isMetadata()) ); } } @@ -158,14 +144,17 @@ public class SceneUtils { } public static Flux> getSupportTriggers() { - return Flux.fromIterable(SceneProviders.triggerProviders()); + return Flux + .fromIterable(SceneProviders.triggerProviders()) + .filterWhen(SceneTriggerProvider::isSupported); } public static Flux> getSupportActions() { - return Flux.fromIterable(SceneProviders.actionProviders()); + return Flux + .fromIterable(SceneProviders.actionProviders()) + .filterWhen(SceneActionProvider::isSupported); } - public static Flux parseTermColumns(SceneRule ruleMono) { Trigger trigger = ruleMono.getTrigger(); if (trigger != null) { @@ -182,9 +171,9 @@ public class SceneUtils { cache, (columns, rule) -> rule .createVariables(columns, - branch, - branchGroup, - action)) + branch, + branchGroup, + action)) .flatMapMany(Function.identity()); } @@ -196,68 +185,8 @@ public class SceneUtils { .map(SelectorInfo::of); } - public static Term refactorTerm(String tableName, - Term term, - BiFunction columnRefactor) { - if (term.getColumn() == null) { - return term; - } - String[] arr = term.getColumn().split("[.]"); - - List values = TermValue.of(term); - if (values.isEmpty()) { - return term; - } - - Function parser = value -> { - //上游变量 - if (value.getSource() == TermValue.Source.variable - || value.getSource() == TermValue.Source.upper) { - term.getOptions().add(TermType.OPTIONS_NATIVE_SQL); - return tableName + "['" + value.getValue() + "']"; - } - //指标 - else if (value.getSource() == TermValue.Source.metric) { - term.getOptions().add(TermType.OPTIONS_NATIVE_SQL); - return tableName + "['" + arr[1] + "_metric_" + value.getMetric() + "']"; - } - //函数, 如: array_len() , device_prop() - else if (value.getSource() == TermValue.Source.function) { - SqlRequest request = FunctionSupport - .supports - .getNow(value.getFunction()) - .createSql(columnRefactor.apply(tableName, value.getColumn()), value.getArgs()) - .toRequest(); - return NativeSql.of(request.getSql(), request.getParameters()); - } - //手动设置值 - else { - return value.getValue(); - } - }; - Object val; - if (values.size() == 1) { - val = parser.apply(values.get(0)); - } else { - val = values - .stream() - .map(parser) - .collect(Collectors.toList()); - } - - if (term.getOptions().contains(TermType.OPTIONS_NATIVE_SQL) && !(val instanceof NativeSql)) { - val = NativeSql.of(String.valueOf(val)); - } - - term.setColumn(columnRefactor.apply(tableName, term.getColumn())); - - term.setValue(val); - - return term; - } - public static Term refactorTerm(String tableName, Term term) { - return refactorTerm(tableName, term, SceneUtils::refactorColumn); + return TermUtils.refactorTerm(tableName, term, SceneUtils::refactorColumn); } private static String refactorColumn(String tableName, String column) { @@ -265,22 +194,55 @@ public class SceneUtils { // fixme 重构 条件列解析逻辑 // properties.xxx.last的场景 if (arr.length > 3 && arr[0].equals("properties")) { - return tableName + "['" + createColumnAlias("properties", column, false) - + "." + String.join(".", Arrays.copyOfRange(arr, 2, arr.length - 1)) + "']"; + String refactorColumn = tableName + "['" + createColumnAlias("properties", column, false); + // 上一次上报时间不需要计算 + if (!DeviceOperation.PropertyValueType.lastTime.name().equals(arr[arr.length -1])) { + refactorColumn += "." + String.join(".", Arrays.copyOfRange(arr, 2, arr.length - 1)); + } + refactorColumn += "']"; + return refactorColumn; } else if (!isDirectTerm(arr[0])) { return tableName + "['" + createColumnAlias(arr[0], column, false) + "']"; } else { // scene.obj1.xx.val1.current => t['scene.obj1_current.val1'] - if (arr.length > 3 && isSceneTerm(column)) { - return tableName + "['" + arr[0] + "." + createColumnAlias(arr[0], column, false) + - "." + String.join(".", Arrays.copyOfRange(arr, 2, arr.length - 1)) - + "']"; + if (isSceneTerm(column)) { + String refactorColumn = tableName + "['" + arr[0] + "." + createColumnAlias(arr[0], column, false); + if (arr.length > 3) { + refactorColumn = refactorColumn + + "." + String.join(".", Arrays.copyOfRange(arr, 2, arr.length - 1)); + } + refactorColumn += "']"; + return refactorColumn; } else { return tableName + "['" + column + "']"; } } } + public static void refactorUpperKey(DeviceSelectorSpec deviceSelectorSpec) { + // 将变量格式改为与查询的别名一致 + if (VariableSource.Source.upper.equals(deviceSelectorSpec.getSource())) { + // scene.xx.current -> scene.scene_xx_current + if (deviceSelectorSpec.getUpperKey().startsWith("scene.")) { + String alias = SceneUtils.createColumnAlias("properties", deviceSelectorSpec.getUpperKey(), false); + deviceSelectorSpec.setUpperKey("scene." + alias); + } + } + } + + private static boolean isContainThis(String[] arr) { + return Arrays.stream(arr) + .collect(Collectors.toList()) + .contains("this") + ; + } + + private static boolean isContainKey(String[] arr, List keys) { + return !Collections.disjoint(Arrays.stream(arr) + .collect(Collectors.toList()), keys) + + ; + } private static boolean isDirectTerm(String column) { //直接term,构建Condition输出条件时使用 diff --git a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/scene/Trigger.java b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/scene/Trigger.java index 17a76d8d..7aa7a0e2 100644 --- a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/scene/Trigger.java +++ b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/scene/Trigger.java @@ -41,6 +41,7 @@ public class Trigger implements Serializable { private String type; /** + * @see SceneTriggerProvider#shakeLimit(String) * @deprecated {@link SceneConditionAction#getShakeLimit()} */ @Deprecated @@ -57,9 +58,10 @@ public class Trigger implements Serializable { private Map configuration; - public String getTypeName(){ + public String getTypeName() { return provider().getName(); } + /** * 重构查询条件,替换为实际将要输出的变量. * @@ -103,8 +105,8 @@ public class Trigger implements Serializable { return config == null ? EmptySqlFragments.INSTANCE : provider().createFilter(config, terms); } - public Mono> createFilterSpec(List terms, BiConsumer customizer){ - return provider().createFilterSpec(triggerConfig(), terms,customizer); + public Mono> createFilterSpec(List terms, BiConsumer customizer) { + return provider().createFilterSpec(triggerConfig(), terms, customizer); } public Flux parseTermColumns() { diff --git a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/scene/Variable.java b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/scene/Variable.java index 627ecafe..dcb3fbef 100644 --- a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/scene/Variable.java +++ b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/scene/Variable.java @@ -21,6 +21,8 @@ import java.util.Map; @Setter public class Variable { public static final String OPTION_PRODUCT_ID = "productId"; + //用于标记前端从后续请求接口(如告警)内取参名称 + public static final String OPTION_PARAMETER = "parameter"; @Schema(description = "变量ID") private String id; diff --git a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/scene/internal/actions/AlarmAction.java b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/scene/internal/actions/AlarmAction.java index a970a463..5af147ff 100644 --- a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/scene/internal/actions/AlarmAction.java +++ b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/scene/internal/actions/AlarmAction.java @@ -3,21 +3,29 @@ package org.jetlinks.community.rule.engine.scene.internal.actions; import lombok.Getter; import lombok.Setter; import org.hswebframework.web.i18n.LocaleUtils; +import org.jetlinks.community.rule.engine.alarm.AlarmConstants; +import org.jetlinks.community.rule.engine.alarm.AlarmTaskExecutorProvider; +import org.jetlinks.community.rule.engine.scene.Variable; import org.jetlinks.core.metadata.types.BooleanType; import org.jetlinks.core.metadata.types.DateTimeType; import org.jetlinks.core.metadata.types.IntType; import org.jetlinks.core.metadata.types.StringType; -import org.jetlinks.community.rule.engine.alarm.AlarmConstants; -import org.jetlinks.community.rule.engine.alarm.AlarmTaskExecutorProvider; -import org.jetlinks.community.rule.engine.scene.Variable; import java.util.ArrayList; import java.util.List; +import static org.jetlinks.community.rule.engine.scene.Variable.OPTION_PARAMETER; + @Getter @Setter public class AlarmAction extends AlarmTaskExecutorProvider.Config { + public static final String OPTION_ALARM_ID = "id"; + public static final String OPTION_ALARM_NAME = "name"; + public static final String OPTION_ALARM_LEVEL = "level"; + /** + * @see org.jetlinks.community.rule.engine.alarm.AlarmRuleHandler.Result + */ public List createVariables() { List variables = new ArrayList<>(); @@ -26,18 +34,22 @@ public class AlarmAction extends AlarmTaskExecutorProvider.Config { Variable.of(AlarmConstants.ConfigKey.alarmConfigId, LocaleUtils.resolveMessage("message.alarm_config_id", "告警配置ID")) .withType(StringType.GLOBAL) + //用于前端 + .withOption(OPTION_PARAMETER,OPTION_ALARM_ID) ); variables.add( Variable.of(AlarmConstants.ConfigKey.alarmName, LocaleUtils.resolveMessage("message.alarm_config_name", "告警配置名称")) .withType(StringType.GLOBAL) + .withOption(OPTION_PARAMETER,OPTION_ALARM_NAME) ); variables.add( Variable.of(AlarmConstants.ConfigKey.level, LocaleUtils.resolveMessage("message.alarm_level", "告警级别")) .withType(IntType.GLOBAL) + .withOption(OPTION_PARAMETER,OPTION_ALARM_LEVEL) ); // variables.add( diff --git a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/scene/internal/actions/AlarmActionProvider.java b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/scene/internal/actions/AlarmActionProvider.java index c300177f..9b1a087c 100644 --- a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/scene/internal/actions/AlarmActionProvider.java +++ b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/scene/internal/actions/AlarmActionProvider.java @@ -1,6 +1,7 @@ package org.jetlinks.community.rule.engine.scene.internal.actions; import org.hswebframework.web.bean.FastBeanCopier; +import org.hswebframework.web.i18n.LocaleUtils; import org.jetlinks.community.rule.engine.alarm.AlarmTaskExecutorProvider; import org.jetlinks.community.rule.engine.enums.AlarmMode; import org.jetlinks.community.rule.engine.scene.SceneActionProvider; @@ -34,7 +35,8 @@ public class AlarmActionProvider implements SceneActionProvider { @Override public Flux createVariable(AlarmAction config) { - return Flux.fromIterable(config.createVariables()); + return LocaleUtils + .transform(Flux.defer(() -> Flux.fromIterable(config.createVariables()))); } @Override diff --git a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/scene/internal/actions/DeviceAction.java b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/scene/internal/actions/DeviceAction.java index 9b972d8b..d5f5537b 100644 --- a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/scene/internal/actions/DeviceAction.java +++ b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/scene/internal/actions/DeviceAction.java @@ -59,7 +59,7 @@ public class DeviceAction extends DeviceSelectorSpec { variables.add(Variable .of("success", resolveMessage( - "message.action_execute_success", + "message.scene.variable.execute_success", "执行是否成功" )) .withType(BooleanType.ID) @@ -71,7 +71,7 @@ public class DeviceAction extends DeviceSelectorSpec { variables.add(Variable .of("deviceId", resolveMessage( - "message.device_id", + "message.device.variable.device_id", "设备ID" )) .withType(StringType.ID) @@ -130,6 +130,12 @@ public class DeviceAction extends DeviceSelectorSpec { } DeviceMessage msg = (DeviceMessage) MessageType.convertMessage(message).orElse(null); + List columns = new ArrayList<>(); + // 按变量获取设备的条件列 + if (Source.upper.equals(getSource())) { + columns.add(getUpperKey()); + } + Collection readyToParse; if (msg instanceof WritePropertyMessage) { @@ -137,13 +143,14 @@ public class DeviceAction extends DeviceSelectorSpec { } else if (msg instanceof FunctionInvokeMessage) { readyToParse = Lists.transform(((FunctionInvokeMessage) msg).getInputs(), FunctionParameter::getValue); } else { - return Collections.emptyList(); + readyToParse = Collections.emptyList(); } - return readyToParse + columns.addAll(readyToParse .stream() .flatMap(val -> parseColumnFromOptions(VariableSource.of(val).getOptions()).stream()) - .collect(Collectors.toList()); + .collect(Collectors.toList())); + return columns; } } \ No newline at end of file diff --git a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/scene/internal/actions/DeviceActionProvider.java b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/scene/internal/actions/DeviceActionProvider.java index 3ff3a92b..ea4705c7 100644 --- a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/scene/internal/actions/DeviceActionProvider.java +++ b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/scene/internal/actions/DeviceActionProvider.java @@ -4,14 +4,18 @@ import lombok.AllArgsConstructor; import org.hswebframework.web.bean.FastBeanCopier; import org.hswebframework.web.i18n.LocaleUtils; import org.jetlinks.core.things.ThingsRegistry; +import org.jetlinks.community.command.CommandSupportManagerProvider; import org.jetlinks.community.rule.engine.executor.DeviceMessageSendTaskExecutorProvider; import org.jetlinks.community.rule.engine.executor.device.DeviceSelectorProviders; import org.jetlinks.community.rule.engine.executor.device.DeviceSelectorSpec; import org.jetlinks.community.rule.engine.scene.SceneActionProvider; +import org.jetlinks.community.rule.engine.scene.SceneUtils; import org.jetlinks.community.rule.engine.scene.Variable; import org.jetlinks.rule.engine.api.model.RuleNodeModel; +import org.jetlinks.sdk.server.SdkServices; import org.springframework.stereotype.Component; import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; import java.util.List; @@ -53,6 +57,7 @@ public class DeviceActionProvider implements SceneActionProvider { config.setMessage(device.getMessage()); if (DeviceSelectorProviders.isFixed(device)) { + SceneUtils.refactorUpperKey(device); config.setSelectorSpec(FastBeanCopier.copy(device, new DeviceSelectorSpec())); } else { config.setSelectorSpec( diff --git a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/scene/internal/actions/DeviceDataActionProvider.java b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/scene/internal/actions/DeviceDataActionProvider.java index 5bbd59cc..38e1313a 100644 --- a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/scene/internal/actions/DeviceDataActionProvider.java +++ b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/scene/internal/actions/DeviceDataActionProvider.java @@ -4,27 +4,26 @@ import lombok.AllArgsConstructor; import lombok.Getter; import lombok.Setter; import org.hswebframework.web.bean.FastBeanCopier; +import org.jetlinks.core.metadata.PropertyMetadata; +import org.jetlinks.core.things.ThingsRegistry; +import org.jetlinks.community.command.CommandSupportManagerProvider; import org.jetlinks.community.rule.engine.executor.device.DeviceDataTaskExecutorProvider; import org.jetlinks.community.rule.engine.executor.device.DeviceSelectorProviders; import org.jetlinks.community.rule.engine.executor.device.DeviceSelectorSpec; import org.jetlinks.community.rule.engine.scene.SceneAction; import org.jetlinks.community.rule.engine.scene.SceneActionProvider; +import org.jetlinks.community.rule.engine.scene.SceneUtils; import org.jetlinks.community.rule.engine.scene.Variable; -import org.jetlinks.core.metadata.PropertyMetadata; -import org.jetlinks.core.things.ThingsRegistry; import org.jetlinks.rule.engine.api.model.RuleNodeModel; +import org.jetlinks.sdk.server.SdkServices; import org.springframework.stereotype.Component; import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; import java.util.ArrayList; import java.util.Collections; import java.util.List; -/** - * - * @author zhangji 2025/1/22 - * @since 2.3 - */ @Component @AllArgsConstructor public class DeviceDataActionProvider implements SceneActionProvider { @@ -44,8 +43,8 @@ public class DeviceDataActionProvider implements SceneActionProvider parseColumns(DeviceDataAction config) { - - return Collections.emptyList(); + String upperKey = config.getSelector().getUpperKey(); + return upperKey == null ? Collections.emptyList() : Collections.singletonList(upperKey); } @Override @@ -63,7 +62,7 @@ public class DeviceDataActionProvider implements SceneActionProvider terms, boolean hasWhere) { + return createSql(terms, Collections.emptySet(), hasWhere); + } - Map> termsMap = SceneUtils.expandTerm(terms); - List termList = new ArrayList<>(); - for (List values : termsMap.values()) { - termList.addAll(values); - } + public SqlRequest createSql(List terms, Set headers, boolean hasWhere) { + + List termList = TermUtils.expandTermToList(terms); // select * from ( // select // this.deviceId deviceId, @@ -97,6 +98,10 @@ public class DeviceTrigger extends DeviceSelectorSpec implements SceneTriggerPro selectColumns.add("this.headers.bindings \"_bindings\""); //链路追踪ID selectColumns.add("this.headers.traceparent \"traceparent\""); + //自定义填充header + for (String header : headers) { + selectColumns.add("this.headers['" + header + "'] \"" + header + "\""); + } switch (this.operation.getOperator()) { case readProperty: @@ -162,7 +167,6 @@ public class DeviceTrigger extends DeviceSelectorSpec implements SceneTriggerPro } return PrepareSqlRequest.of(builder.toString(), new Object[0]); - } String createFilterDescription(List terms) { @@ -252,8 +256,9 @@ public class DeviceTrigger extends DeviceSelectorSpec implements SceneTriggerPro } } - public static Term refactorTermValue(String tableName, Term term) { + + return SceneUtils.refactorTerm(tableName, term); } @@ -272,7 +277,7 @@ public class DeviceTrigger extends DeviceSelectorSpec implements SceneTriggerPro } String[] arr = column.split("[.]"); //properties.temp.current - if ("properties".equals(arr[0])) { + if ("properties".equals(arr[0]) || "scene".equals(arr[0])) { try { DeviceOperation.PropertyValueType valueType = DeviceOperation.PropertyValueType.valueOf(arr[arr.length - 1]); String property = arr[1]; @@ -304,22 +309,26 @@ public class DeviceTrigger extends DeviceSelectorSpec implements SceneTriggerPro public List createDefaultVariable() { return Arrays.asList( - Variable.of("deviceId", "设备ID") + Variable.of("deviceId", getVariableI18nName("device_id","设备ID")) .withOption(Variable.OPTION_PRODUCT_ID, productId) .withTermType(TermTypes.lookup(StringType.GLOBAL)) .withColumn("deviceId"), - Variable.of("deviceName", "设备名称") + Variable.of("deviceName", getVariableI18nName("device_name","设备名称")) .withTermType(TermTypes.lookup(StringType.GLOBAL)) .withColumn("deviceName"), - Variable.of("productId", "产品ID") + Variable.of("productId", getVariableI18nName("product_id","产品ID")) .withTermType(TermTypes.lookup(StringType.GLOBAL)) .withColumn("productId"), - Variable.of("productName", "产品名称") + Variable.of("productName", getVariableI18nName("product_name","产品名称")) .withTermType(TermTypes.lookup(StringType.GLOBAL)) .withColumn("productName") ); } + private String getVariableI18nName(String id, String defaultName) { + return LocaleUtils.resolveMessage("message.device.variable." + id, defaultName); + } + public Flux parseTermColumns(ThingsRegistry registry) { if (!StringUtils.hasText(productId)) { @@ -401,4 +410,5 @@ public class DeviceTrigger extends DeviceSelectorSpec implements SceneTriggerPro operation.validate(); } + } diff --git a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/scene/internal/triggers/DeviceTriggerProvider.java b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/scene/internal/triggers/DeviceTriggerProvider.java index 437a0a01..90e2f30b 100644 --- a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/scene/internal/triggers/DeviceTriggerProvider.java +++ b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/scene/internal/triggers/DeviceTriggerProvider.java @@ -1,30 +1,58 @@ package org.jetlinks.community.rule.engine.scene.internal.triggers; +import lombok.Getter; import lombok.RequiredArgsConstructor; +import lombok.Setter; import org.hswebframework.ezorm.core.param.Term; import org.hswebframework.ezorm.rdb.executor.SqlRequest; import org.hswebframework.ezorm.rdb.operator.builder.fragments.SqlFragments; -import org.jetlinks.core.things.ThingsRegistry; +import org.hswebframework.web.i18n.LocaleUtils; +import org.jetlinks.community.command.CommandSupportManagerProvider; import org.jetlinks.community.rule.engine.scene.AbstractSceneTriggerProvider; import org.jetlinks.community.rule.engine.scene.Variable; import org.jetlinks.community.rule.engine.scene.term.TermColumn; +import org.jetlinks.core.things.ThingsRegistry; import org.jetlinks.rule.engine.api.model.RuleModel; import org.jetlinks.rule.engine.api.model.RuleNodeModel; +import org.jetlinks.sdk.server.SdkServices; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import java.util.HashSet; import java.util.List; +import java.util.Set; @Component @RequiredArgsConstructor @ConfigurationProperties(prefix = "rule.scene.trigger.device") public class DeviceTriggerProvider extends AbstractSceneTriggerProvider { + private static boolean sharedSupported; + + static { + try { + Class.forName("org.jetlinks.community.device.entity.DeviceInstanceEntity"); + sharedSupported = false; + } catch (ClassNotFoundException e) { + //当前不在设备模块,默认使用共享订阅 + sharedSupported = true; + } + } + public static final String PROVIDER = "device"; private final ThingsRegistry registry; + @Getter + @Setter + private boolean sharedMod = sharedSupported; + + @Getter + @Setter + private Set customHeaders = new HashSet<>(); + @Override public String getProvider() { return PROVIDER; @@ -32,7 +60,8 @@ public class DeviceTriggerProvider extends AbstractSceneTriggerProvider terms, boolean hasFilter) { - return config.createSql(terms, hasFilter); + return config.createSql(terms, customHeaders, hasFilter); } @Override diff --git a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/scene/internal/triggers/ManualTriggerProvider.java b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/scene/internal/triggers/ManualTriggerProvider.java index 69d76dde..02f7700d 100644 --- a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/scene/internal/triggers/ManualTriggerProvider.java +++ b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/scene/internal/triggers/ManualTriggerProvider.java @@ -7,18 +7,25 @@ import org.hswebframework.ezorm.rdb.operator.builder.fragments.EmptySqlFragments import org.hswebframework.ezorm.rdb.operator.builder.fragments.SqlFragments; import org.hswebframework.web.i18n.LocaleUtils; import org.jetlinks.core.metadata.types.DateTimeType; +import org.jetlinks.community.reactorql.term.FixedTermTypeSupport; import org.jetlinks.community.reactorql.term.TermTypes; import org.jetlinks.community.rule.engine.scene.AbstractSceneTriggerProvider; import org.jetlinks.community.rule.engine.scene.Variable; import org.jetlinks.community.rule.engine.scene.term.TermColumn; +import org.jetlinks.community.terms.I18nSpec; +import org.jetlinks.community.terms.TermSpec; import org.jetlinks.rule.engine.api.model.RuleModel; import org.jetlinks.rule.engine.api.model.RuleNodeModel; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; import java.util.Collections; import java.util.List; +import java.util.function.BiConsumer; + +import static org.jetlinks.community.rule.engine.scene.SceneRule.TRIGGER_TYPE; @Component @ConfigurationProperties(prefix = "rule.scene.trigger.manual") @@ -33,7 +40,8 @@ public class ManualTriggerProvider extends AbstractSceneTriggerProvider> createFilterSpec(ManualTrigger config, + List terms, + BiConsumer customizer) { + TermSpec spec = new TermSpec(); + spec.setColumn(TRIGGER_TYPE); + spec.setTermType(FixedTermTypeSupport.eq.name()); + spec.setTriggerSpec( + I18nSpec.of("message.term_type_scene_manual_trigger_desc", "系统在接收到手动触发指令时,触发场景") + ); + spec.setActualSpec( + I18nSpec.of("message.term_type_scene_manual_actual_desc", "手动触发告警") + ); + spec.setDisplayCode(I18nSpec.of("message.scene_trigger_type", "场景触发类型")); + spec.setExpected(PROVIDER); + spec.setActual(PROVIDER); + spec.setMatched(true); + + if (!terms.isEmpty()) { + spec.setChildren(TermSpec.of(terms, customizer)); + } + + return Mono.just(Collections.singletonList(spec)); + } + @Override public List createDefaultVariable(ManualTrigger config) { return Collections.singletonList( @@ -59,9 +92,14 @@ public class ManualTriggerProvider extends AbstractSceneTriggerProvider { @@ -32,7 +39,8 @@ public class TimerTriggerProvider implements SceneTriggerProvider @Override public String getName() { - return "定时触发"; + return LocaleUtils + .resolveMessage("message.scene_trigger_name_timer", "定时触发"); } @Override @@ -50,6 +58,31 @@ public class TimerTriggerProvider implements SceneTriggerProvider return EmptySqlFragments.INSTANCE; } + @Override + public Mono> createFilterSpec(TimerTrigger config, + List terms, + BiConsumer customizer) { + TermSpec spec = new TermSpec(); + spec.setColumn(TRIGGER_TYPE); + spec.setTermType(FixedTermTypeSupport.eq.name()); + spec.setTriggerSpec( + I18nSpec.of("message.term_type_scene_timer_trigger_desc", config.toString()) + ); + spec.setActualSpec( + I18nSpec.of("message.term_type_scene_timer_actual_desc", "定时触发告警") + ); + spec.setDisplayCode(I18nSpec.of("message.scene_trigger_type", "场景触发类型")); + spec.setExpected(PROVIDER); + spec.setActual(PROVIDER); + spec.setMatched(true); + + if (!terms.isEmpty()) { + spec.setChildren(TermSpec.of(terms, customizer)); + } + + return Mono.just(Collections.singletonList(spec)); + } + @Override public List createDefaultVariable(TimerTrigger config) { return Collections.singletonList( @@ -58,9 +91,14 @@ public class TimerTriggerProvider implements SceneTriggerProvider LocaleUtils.resolveMessage( "message.scene_term_column_now", "服务器时间")) + .withDescription( + LocaleUtils.resolveMessage( + "message.scene_term_column_now_desc", + "服务器时间")) .withType(DateTimeType.ID) .withTermType(TermTypes.lookup(DateTimeType.GLOBAL)) .withColumn("_now") + .withFullNameCode(I18nSpec.of("message.scene_term_column_now", "服务器时间")) ); } diff --git a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/scene/term/TermColumn.java b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/scene/term/TermColumn.java index 72b5f0ac..7143a8ad 100644 --- a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/scene/term/TermColumn.java +++ b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/scene/term/TermColumn.java @@ -8,20 +8,21 @@ import org.apache.commons.collections4.CollectionUtils; import org.hswebframework.ezorm.core.param.Term; import org.hswebframework.web.bean.FastBeanCopier; import org.hswebframework.web.i18n.LocaleUtils; -import org.jetlinks.community.reactorql.function.FunctionInfo; import org.jetlinks.core.metadata.DataType; +import org.jetlinks.core.metadata.MetadataConstants; import org.jetlinks.core.metadata.PropertyMetadata; import org.jetlinks.core.metadata.types.BooleanType; import org.jetlinks.core.metadata.types.EnumType; import org.jetlinks.community.PropertyMetadataConstants; import org.jetlinks.community.PropertyMetric; +import org.jetlinks.community.reactorql.function.FunctionInfo; +import org.jetlinks.community.reactorql.function.FunctionSupport; +import org.jetlinks.community.reactorql.term.TermType; +import org.jetlinks.community.reactorql.term.TermTypes; import org.jetlinks.community.rule.engine.scene.DeviceOperation; import org.jetlinks.community.terms.I18nSpec; import org.springframework.util.StringUtils; -import org.jetlinks.community.reactorql.term.TermType; -import org.jetlinks.community.reactorql.term.TermTypes; - import java.util.*; import java.util.function.Function; import java.util.function.Predicate; @@ -130,7 +131,7 @@ public class TermColumn { if (column == null) { return null; } - String[] arr = column.split("[.]"); + String[] arr = column.split("[.]",2); if (arr.length == 1) { return arr[0]; } @@ -167,6 +168,12 @@ public class TermColumn { setDataType(metadata.getValueType().getId()); withMetrics(metadata); setTermTypes(TermTypes.lookup(metadata.getValueType())); + setFunctions(FunctionSupport.lookup(metadata.getValueType())); + if (!StringUtils.hasText(this.getCode())) { + String localeName = MetadataConstants.Expand.getLocaleName(metadata, LocaleUtils.current()); + setFullNameCode(I18nSpec.of(null, localeName)); + setFullName(fullNameCode.resolveI18nMessage()); + } return this; } @@ -184,7 +191,7 @@ public class TermColumn { } public static TermColumn of(String column, String code, String defaultName, DataType type) { - TermColumn termColumn = of(column, defaultName, type, null); + TermColumn termColumn = of(column, resolveI18n(code, defaultName), type, null); termColumn.setCode(code); return termColumn; } @@ -195,7 +202,9 @@ public class TermColumn { String defaultName, DataType type, String defaultDescription) { - TermColumn termColumn = of(column, defaultName, type, resolveI18n(getDescriptionByCode(code), defaultDescription)); + TermColumn termColumn = of( + column, resolveI18n(code, defaultName), type, resolveI18n(getDescriptionByCode(code), defaultDescription) + ); termColumn.setCode(code); termColumn.setCodeDesc(getDescriptionByCode(code)); return termColumn; @@ -209,6 +218,7 @@ public class TermColumn { termColumn.setDataType(type.getId()); termColumn.setDescription(description); termColumn.setTermTypes(TermTypes.lookup(type)); + termColumn.setFunctions(FunctionSupport.lookup(type)); if (type instanceof EnumType) { List elements = ((EnumType) type).getElements(); if (CollectionUtils.isNotEmpty(elements)) { @@ -295,12 +305,8 @@ public class TermColumn { } else { this.fullName = name; if (CollectionUtils.isEmpty(children)) { - if (StringUtils.hasText(code)) { - this.fullNameCode = I18nSpec.of(code, name); - this.fullName = fullNameCode.resolveI18nMessage(); - } else { - this.fullName = name; - } + this.fullNameCode = I18nSpec.of(code, name); + this.fullName = fullNameCode.resolveI18nMessage(); } } if (CollectionUtils.isNotEmpty(children)) { diff --git a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/scene/value/TermValue.java b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/scene/value/TermValue.java index 5cf105af..a84b60dc 100644 --- a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/scene/value/TermValue.java +++ b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/scene/value/TermValue.java @@ -13,8 +13,12 @@ import java.util.List; import java.util.Map; import java.util.stream.Collectors; +/** + * @see org.jetlinks.community.reactorql.term.TermValue + */ @Getter @Setter +@Deprecated public class TermValue implements Serializable { private static final long serialVersionUID = 1; diff --git a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/service/AlarmConfigService.java b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/service/AlarmConfigService.java index 1b723835..7c61ec4c 100755 --- a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/service/AlarmConfigService.java +++ b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/service/AlarmConfigService.java @@ -8,8 +8,11 @@ import org.hswebframework.web.crud.events.EntityModifyEvent; import org.hswebframework.web.crud.events.EntitySavedEvent; import org.hswebframework.web.crud.service.GenericReactiveCrudService; import org.hswebframework.web.exception.BusinessException; +import org.jetlinks.community.rule.engine.entity.AlarmHandleHistoryEntity; import org.jetlinks.community.rule.engine.alarm.AlarmHandleInfo; -import org.jetlinks.community.rule.engine.entity.*; +import org.jetlinks.community.rule.engine.entity.AlarmConfigDetail; +import org.jetlinks.community.rule.engine.entity.AlarmConfigEntity; +import org.jetlinks.community.rule.engine.entity.SceneEntity; import org.jetlinks.community.rule.engine.enums.AlarmState; import org.springframework.context.event.EventListener; import org.springframework.stereotype.Service; @@ -55,7 +58,6 @@ public class AlarmConfigService extends GenericReactiveCrudService> queryDetailPager(QueryParamEntity query) { return this .queryPager(query) diff --git a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/service/AlarmHandleHistoryService.java b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/service/AlarmHandleHistoryService.java index f40023a3..60aff350 100755 --- a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/service/AlarmHandleHistoryService.java +++ b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/service/AlarmHandleHistoryService.java @@ -9,6 +9,4 @@ import org.springframework.stereotype.Service; @AllArgsConstructor public class AlarmHandleHistoryService extends GenericReactiveCrudService { - - } diff --git a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/service/AlarmHandleTypeDictInit.java b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/service/AlarmHandleTypeDictInit.java index 03beb8d7..98ba096f 100644 --- a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/service/AlarmHandleTypeDictInit.java +++ b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/service/AlarmHandleTypeDictInit.java @@ -5,6 +5,7 @@ import lombok.extern.slf4j.Slf4j; import org.apache.commons.codec.digest.DigestUtils; import org.hswebframework.web.dictionary.entity.DictionaryEntity; import org.hswebframework.web.dictionary.entity.DictionaryItemEntity; +import org.hswebframework.web.i18n.LocaleUtils; import org.jetlinks.community.dictionary.DictionaryConstants; import org.jetlinks.community.dictionary.DictionaryInitInfo; import org.jetlinks.community.rule.engine.enums.AlarmHandleType; @@ -29,6 +30,7 @@ public class AlarmHandleTypeDictInit implements DictionaryInitInfo { entity.setId(DICT_ID); entity.setName("告警处理类型"); entity.setClassified(DictionaryConstants.CLASSIFIED_SYSTEM); + entity.putI18nName("message.handle.type.dict.name", LocaleUtils.getSupportLocales()); entity.setStatus((byte) 1); List items = new ArrayList<>(); @@ -37,8 +39,10 @@ public class AlarmHandleTypeDictInit implements DictionaryInitInfo { for (AlarmHandleType type : AlarmHandleType.values()) { DictionaryItemEntity item = new DictionaryItemEntity(); item.setId(DigestUtils.md5Hex(DICT_ID + type.getValue())); + item.setName(type.getValue()); item.setValue(type.getValue()); item.setText(type.getText()); + item.putI18nText("message.handle.type.dict.item." + type.name(), LocaleUtils.getSupportLocales()); item.setDictId(DICT_ID); item.setStatus((byte) 1); item.setOrdinal(index++); diff --git a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/service/AlarmHistoryService.java b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/service/AlarmHistoryService.java index e4ec0e38..93fd7a90 100644 --- a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/service/AlarmHistoryService.java +++ b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/service/AlarmHistoryService.java @@ -3,20 +3,73 @@ package org.jetlinks.community.rule.engine.service; import org.hswebframework.ezorm.core.param.QueryParam; import org.hswebframework.web.api.crud.entity.PagerResult; import org.jetlinks.community.rule.engine.entity.AlarmHistoryInfo; +import org.jetlinks.community.timeseries.query.AggregationData; +import org.jetlinks.community.timeseries.query.AggregationQueryParam; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; /** + * 告警历史记录服务 + * * @author bestfeng + * @author zhouhao + * @since 2.0 */ public interface AlarmHistoryService { - + /** + * 分页查询告警记录 + * + * @param queryParam 查询参数 + * @return 分页结果 + */ Mono> queryPager(QueryParam queryParam); + /** + * 聚合查询 + * + * @param param 查询参数 + * @return 聚合结果 + */ + Flux aggregation(AggregationQueryParam param); + + /** + * 查询告警记录数量 + * + * @param queryParam 查询参数 + * @return 数量 + */ + Mono count(QueryParam queryParam); + + /** + * 不分页查询告警记录 + * + * @param param 查询参数 + * @return 告警记录 + */ + Flux query(QueryParam param); + + /** + * 保存告警记录 + * + * @param historyInfo 告警记录 + * @return void + */ Mono save(AlarmHistoryInfo historyInfo); + /** + * 保存告警记录 + * + * @param historyInfo 告警记录 + * @return void + */ Mono save(Flux historyInfo); + /** + * 保存告警记录 + * + * @param historyInfo 告警记录 + * @return void + */ Mono save(Mono historyInfo); } diff --git a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/service/ElasticSearchAlarmHistoryService.java b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/service/ElasticSearchAlarmHistoryService.java index 1d7173e3..245cf71b 100644 --- a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/service/ElasticSearchAlarmHistoryService.java +++ b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/service/ElasticSearchAlarmHistoryService.java @@ -2,19 +2,21 @@ package org.jetlinks.community.rule.engine.service; import com.alibaba.fastjson.JSONObject; import lombok.AllArgsConstructor; -import org.apache.commons.collections4.CollectionUtils; import org.hswebframework.ezorm.core.param.QueryParam; import org.hswebframework.web.api.crud.entity.PagerResult; import org.hswebframework.web.bean.FastBeanCopier; +import org.jetlinks.community.elastic.search.index.DefaultElasticSearchIndexMetadata; +import org.jetlinks.community.elastic.search.index.ElasticSearchIndexManager; +import org.jetlinks.community.elastic.search.service.AggregationService; +import org.jetlinks.community.elastic.search.service.ElasticSearchService; +import org.jetlinks.community.rule.engine.entity.AlarmHistoryInfo; +import org.jetlinks.community.timeseries.query.AggregationData; +import org.jetlinks.community.timeseries.query.AggregationQueryParam; import org.jetlinks.core.metadata.types.ArrayType; import org.jetlinks.core.metadata.types.DateTimeType; import org.jetlinks.core.metadata.types.IntType; import org.jetlinks.core.metadata.types.StringType; -import org.jetlinks.community.PropertyConstants; -import org.jetlinks.community.elastic.search.index.DefaultElasticSearchIndexMetadata; -import org.jetlinks.community.elastic.search.index.ElasticSearchIndexManager; -import org.jetlinks.community.elastic.search.service.ElasticSearchService; -import org.jetlinks.community.rule.engine.entity.AlarmHistoryInfo; +import org.springframework.stereotype.Component; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -34,12 +36,19 @@ public class ElasticSearchAlarmHistoryService implements AlarmHistoryService { private final ElasticSearchIndexManager indexManager; private final ElasticSearchService elasticSearchService; - + private final AggregationService aggregationService; public Mono> queryPager(QueryParam queryParam) { return elasticSearchService.queryPager(ALARM_HISTORY_INDEX, queryParam, AlarmHistoryInfo.class); } + @Override + public Flux aggregation(AggregationQueryParam param) { + return aggregationService + .aggregation(ALARM_HISTORY_INDEX, param) + .map(AggregationData::of); + } + public Mono save(AlarmHistoryInfo historyInfo) { return elasticSearchService.commit(ALARM_HISTORY_INDEX, createData(historyInfo)); } @@ -52,6 +61,17 @@ public class ElasticSearchAlarmHistoryService implements AlarmHistoryService { return elasticSearchService.save(ALARM_HISTORY_INDEX, historyInfo.map(this::createData)); } + @Override + public Flux query(QueryParam param) { + return elasticSearchService + .query(ALARM_HISTORY_INDEX,param,AlarmHistoryInfo.class); + } + + @Override + public Mono count(QueryParam queryParam) { + return elasticSearchService.count(ALARM_HISTORY_INDEX,queryParam); + } + private Map createData(AlarmHistoryInfo info) { Map data = FastBeanCopier.copy(info, new HashMap<>(16), "termSpec"); if (info.getTermSpec() != null) { @@ -84,7 +104,6 @@ public class ElasticSearchAlarmHistoryService implements AlarmHistoryService { .addProperty("triggerDesc", StringType.GLOBAL) .addProperty("actualDesc", StringType.GLOBAL) .addProperty("alarmConfigSource", StringType.GLOBAL) - .addProperty("bindings", new ArrayType().elementType(StringType.GLOBAL)) ).block(Duration.ofSeconds(10)); } } diff --git a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/service/LocalRuleInstanceRepository.java b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/service/LocalRuleInstanceRepository.java index 548ef82a..0e523a74 100644 --- a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/service/LocalRuleInstanceRepository.java +++ b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/service/LocalRuleInstanceRepository.java @@ -72,10 +72,10 @@ public class LocalRuleInstanceRepository implements RuleInstanceRepository, Comm .and(SceneEntity::getState, RuleInstanceState.started) .fetch() .flatMap(SceneEntity::toRule) - ) - ; + ); } + @Override public void run(String... args) throws Exception { this diff --git a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/service/SceneService.java b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/service/SceneService.java index 7ea4e560..940575eb 100644 --- a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/service/SceneService.java +++ b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/service/SceneService.java @@ -22,7 +22,6 @@ import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import java.util.Collection; -import java.util.Collections; import java.util.HashMap; import java.util.Map; @@ -91,35 +90,25 @@ public class SceneService extends GenericReactiveCrudService enable(String id) { - return enable(Collections.singletonList(id)); - } - - @Transactional(rollbackFor = Throwable.class) - public Mono enable(Collection id) { - Assert.notEmpty(id, "id can not be empty"); + Assert.hasText(id, "id can not be empty"); long now = System.currentTimeMillis(); return this .createUpdate() .set(SceneEntity::getState, RuleInstanceState.started) .set(SceneEntity::getModifyTime, now) .set(SceneEntity::getStartTime, now) - .in(SceneEntity::getId, id) + .where(SceneEntity::getId, id) .execute() .then(); } @Transactional public Mono disabled(String id) { - return disabled(Collections.singletonList(id)); - } - - @Transactional - public Mono disabled(Collection id) { - Assert.notEmpty(id, "id can not be empty"); + Assert.hasText(id, "id can not be empty"); return this .createUpdate() .set(SceneEntity::getState, RuleInstanceState.disable) - .in(SceneEntity::getId, id) + .where(SceneEntity::getId, id) .execute() .then(); } diff --git a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/service/terms/AlarmBindRuleTerm.java b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/service/terms/AlarmBindRuleTerm.java index 989e816d..1ba891fa 100644 --- a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/service/terms/AlarmBindRuleTerm.java +++ b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/service/terms/AlarmBindRuleTerm.java @@ -2,17 +2,17 @@ package org.jetlinks.community.rule.engine.service.terms; import org.hswebframework.ezorm.core.param.Term; import org.hswebframework.ezorm.rdb.metadata.RDBColumnMetadata; -import org.hswebframework.ezorm.rdb.operator.builder.fragments.PrepareSqlFragments; +import org.hswebframework.ezorm.rdb.operator.builder.fragments.BatchSqlFragments; import org.hswebframework.ezorm.rdb.operator.builder.fragments.SqlFragments; import org.hswebframework.ezorm.rdb.operator.builder.fragments.term.AbstractTermFragmentBuilder; +import org.hswebframework.ezorm.rdb.utils.SqlUtils; import org.springframework.stereotype.Component; import java.util.List; -import java.util.stream.Collectors; /** * 根据告警配置查询规则. - * + *

* 例如:查询告警ID为alarm-id绑定的场景联动 *

  *     {
@@ -35,23 +35,26 @@ public class AlarmBindRuleTerm extends AbstractTermFragmentBuilder {
     public SqlFragments createFragments(String columnFullName,
                                         RDBColumnMetadata column,
                                         Term term) {
+        boolean not = term.getOptions().contains("not");
 
-        PrepareSqlFragments sqlFragments = PrepareSqlFragments.of();
-        if (term.getOptions().contains("not")) {
-            sqlFragments.addSql("not");
+        BatchSqlFragments sqlFragments = new BatchSqlFragments(not ? 7 : 6, 1);
+
+        if (not) {
+            sqlFragments.add(SqlFragments.NOT);
         }
+
         sqlFragments
             .addSql("exists(select 1 from ", getTableName("s_alarm_rule_bind", column), " _bind where _bind.rule_id =", columnFullName);
 
         List alarmId = convertList(column, term);
         sqlFragments
-            .addSql(
-                "and _bind.alarm_id in (",
-                alarmId.stream().map(r -> "?").collect(Collectors.joining(",")),
-                ")")
+            .addSql("and _bind.alarm_id in (")
+            .add(SqlUtils.createQuestionMarks(alarmId.size()))
+            //  )
+            .add(SqlFragments.RIGHT_BRACKET)
             .addParameter(alarmId);
 
-        sqlFragments.addSql(")");
+        sqlFragments.add(SqlFragments.RIGHT_BRACKET);
 
         return sqlFragments;
     }
diff --git a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/service/terms/AlarmRecordTerm.java b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/service/terms/AlarmRecordTerm.java
new file mode 100644
index 00000000..a6287029
--- /dev/null
+++ b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/service/terms/AlarmRecordTerm.java
@@ -0,0 +1,48 @@
+package org.jetlinks.community.rule.engine.service.terms;
+
+import org.jetlinks.community.terms.SubTableTermFragmentBuilder;
+import org.springframework.stereotype.Component;
+
+/**
+ * 根据告警记录信息条件查询.
+ * 

+ * 例如:查询具有设备类型的告警记录的设备 + *

{@code
+ *     {
+ *          "column":"id",
+ *          "termType":"alarm-record",
+ *          "value": [
+ *              {
+ *                  "column": "target_type",
+ *                  "termType": "eq",
+ *                  "value": "device"
+ *              }
+ *          ]
+ *     }
+ * }
+ * + * @author zhangji 2023/08/16 + */ +@Component +public class AlarmRecordTerm extends SubTableTermFragmentBuilder { + + public AlarmRecordTerm() { + super("alarm-record", "根据告警记录查询"); + } + + @Override + protected String getSubTableName() { + return "alarm_record"; + } + + @Override + protected String getTableAlias() { + return "_record"; + } + + @Override + protected String getSubTableColumn() { + return "target_id"; + } + +} diff --git a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/service/terms/RuleBindAlarmTerm.java b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/service/terms/RuleBindAlarmTerm.java index a7e84700..33ff9b10 100644 --- a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/service/terms/RuleBindAlarmTerm.java +++ b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/service/terms/RuleBindAlarmTerm.java @@ -54,7 +54,6 @@ public class RuleBindAlarmTerm extends AbstractTermFragmentBuilder { public SqlFragments createFragments(String columnFullName, RDBColumnMetadata column, Term term) { - AlarmRuleBindTerm bindTerm = AlarmRuleBindTerm.of(term.getValue()); if (CollectionUtils.isEmpty(bindTerm.ruleId)) { throw new IllegalArgumentException("illegal term [rule-bind-alarm] value :" + term); diff --git a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/utils/TermColumnUtils.java b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/utils/TermColumnUtils.java index 3e80f36c..26b79f5d 100644 --- a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/utils/TermColumnUtils.java +++ b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/utils/TermColumnUtils.java @@ -9,7 +9,6 @@ import org.jetlinks.core.metadata.DataType; import org.jetlinks.core.metadata.PropertyMetadata; import org.jetlinks.core.metadata.SimplePropertyMetadata; import org.jetlinks.core.metadata.types.ArrayType; -import org.jetlinks.core.metadata.types.IntType; import org.jetlinks.core.metadata.types.LongType; import org.jetlinks.core.metadata.types.ObjectType; @@ -23,9 +22,7 @@ import java.util.stream.Collectors; import static org.jetlinks.core.metadata.SimplePropertyMetadata.of; /** - * - * @author zhangji 2025/1/22 - * @since 2.3 + * @author bestfeng */ public class TermColumnUtils { @@ -48,8 +45,14 @@ public class TermColumnUtils { of("this", resolveI18n("message.term_element_of_array", "数组元素"), - (arrayType.getElementType() instanceof ArrayType) ? IntType.GLOBAL : arrayType.getElementType()); + arrayType.getElementType()); columns.addAll(TermColumnUtils.createTermColumn(prefix, prop, false)); + // 移除第二层嵌套数组的复杂条件 + if (arrayType.getElementType() instanceof ArrayType) { + for (TermColumn column : columns) { + column.getTermTypes().removeIf(termType -> ComplexExistsFunction.function.equals(termType.getId())); + } + } } return columns; 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 ee6743c1..841a23cc 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 @@ -13,12 +13,13 @@ import org.hswebframework.web.authorization.annotation.SaveAction; import org.hswebframework.web.crud.service.ReactiveCrudService; import org.hswebframework.web.crud.web.reactive.ReactiveServiceCrudController; import org.jetlinks.community.rule.engine.alarm.AlarmLevelInfo; +import org.jetlinks.community.rule.engine.alarm.AlarmTarget; import org.jetlinks.community.rule.engine.alarm.AlarmTargetSupplier; import org.jetlinks.community.rule.engine.entity.AlarmConfigDetail; import org.jetlinks.community.rule.engine.entity.AlarmConfigEntity; import org.jetlinks.community.rule.engine.entity.AlarmLevelEntity; -import org.jetlinks.community.rule.engine.scene.SceneUtils; import org.jetlinks.community.rule.engine.scene.SceneTriggerProvider; +import org.jetlinks.community.rule.engine.scene.SceneUtils; import org.jetlinks.community.rule.engine.service.AlarmConfigService; import org.jetlinks.community.rule.engine.service.AlarmLevelService; import org.jetlinks.community.rule.engine.web.response.AlarmTargetTypeInfo; @@ -26,6 +27,8 @@ import org.springframework.web.bind.annotation.*; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import java.util.Comparator; + @RestController @RequestMapping(value = "/alarm/config") @Resource(id = "alarm-config", name = "告警配置") @@ -66,7 +69,8 @@ public class AlarmConfigController implements ReactiveServiceCrudController triggerCache + .sort(Comparator.comparing(AlarmTarget::getOrder)) + .concatMap(alarmTarget -> triggerCache .filter(alarmTarget::isSupported) .collectList() .map(supportTriggers -> AlarmTargetTypeInfo diff --git a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/web/AlarmHistoryController.java b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/web/AlarmHistoryController.java index 9118bf17..e1463316 100755 --- a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/web/AlarmHistoryController.java +++ b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/web/AlarmHistoryController.java @@ -28,23 +28,25 @@ public class AlarmHistoryController { @Operation(summary = "告警历史查询") @QueryAction @Deprecated + // 已弃用,改为通过告警记录的数据权限查询 public Mono> queryHandleHistoryPager(@RequestBody Mono query) { return query.flatMap(alarmHistoryService::queryPager); } @PostMapping("/{alarmConfigId}/_query") - @Operation(summary = "告警历史查询") + @Operation(summary = "按告警配置查询告警历史") @QueryAction + @Deprecated + // 已弃用,改为通过告警记录的数据权限查询 public Mono> queryHandleHistoryPager( - @PathVariable @Parameter(description = "告警配置ID") String alarmConfigId, - @RequestBody Mono query - ) { + @PathVariable @Parameter(description = "告警配置ID") String alarmConfigId, + @RequestBody Mono query) { return query - .map(q -> q - .toNestQuery() - .and(AlarmHistoryInfo::getAlarmConfigId, alarmConfigId) - .getParam()) - .flatMap(alarmHistoryService::queryPager); + .map(q -> q + .toNestQuery() + .and(AlarmHistoryInfo::getAlarmConfigId, alarmConfigId) + .getParam()) + .flatMap(alarmHistoryService::queryPager); } @PostMapping("/alarm-record/{recordId}/_query") diff --git a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/web/AlarmRecordController.java b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/web/AlarmRecordController.java index 4cfa4557..db16f1c4 100755 --- a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/web/AlarmRecordController.java +++ b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/web/AlarmRecordController.java @@ -10,18 +10,21 @@ 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.bean.FastBeanCopier; import org.hswebframework.web.crud.web.reactive.ReactiveServiceQueryController; import org.hswebframework.web.exception.NotFoundException; +import org.jetlinks.community.command.rule.data.RelieveInfo; import org.jetlinks.community.rule.engine.alarm.AlarmHandleInfo; +import org.jetlinks.community.rule.engine.alarm.AlarmHandler; import org.jetlinks.community.rule.engine.entity.AlarmHandleHistoryEntity; import org.jetlinks.community.rule.engine.entity.AlarmRecordEntity; -import org.jetlinks.community.rule.engine.service.AlarmConfigService; import org.jetlinks.community.rule.engine.service.AlarmHandleHistoryService; import org.jetlinks.community.rule.engine.service.AlarmRecordService; import org.springframework.web.bind.annotation.*; import reactor.core.publisher.Mono; +import java.util.Collections; + @RestController @RequestMapping(value = "/alarm/record") @Resource(id = "alarm-record", name = "告警记录") @@ -30,53 +33,58 @@ import reactor.core.publisher.Mono; @AllArgsConstructor public class AlarmRecordController implements ReactiveServiceQueryController { - private final AlarmRecordService recordService; - - private final AlarmConfigService configService; + private final AlarmRecordService alarmRecordService; private final AlarmHandleHistoryService handleHistoryService; + private final AlarmHandler alarmHandler; + @Override - public ReactiveCrudService getService() { - return recordService; + public AlarmRecordService getService() { + return alarmRecordService; } + @PostMapping("/{dimensionType}/_query") @Operation(summary = "按不同维度查询告警记录") @QueryAction public Mono> queryPagerByDimensionType(@PathVariable String dimensionType, @RequestBody Mono query) { - return query - .doOnNext(queryParamEntity -> queryParamEntity - .toNestQuery(q -> q.and(AlarmRecordEntity::getTargetType, TermType.eq, dimensionType))) - .flatMap(this::queryPager1); + return query.doOnNext(queryParamEntity -> { + queryParamEntity.toNestQuery(q -> q.and("targetType", TermType.eq, dimensionType)); + }) + .flatMap(this::queryPager1); } + @PostMapping("/_handle") @Operation(summary = "处理告警") @SaveAction - public Mono handleAlarm(@RequestBody Mono handleInfo) { - return handleInfo - .flatMap(configService::handleAlarm); + public Mono handleAlarm(@RequestBody Mono handleInfoMono) { + return handleInfoMono + .flatMap(info -> alarmRecordService + .findById(info.getAlarmRecordId()) + .flatMap(record -> handleAlarm(record, AlarmHandleHistoryEntity.of(info))) + ); } @PostMapping("/handle-history/_query") @Operation(summary = "告警处理历史查询") @QueryAction + @Deprecated public Mono> queryHandleHistoryPager(@RequestBody Mono query) { return query.flatMap(handleHistoryService::queryPager); } + @PostMapping("/{id}/handle-history/_query") @Operation(summary = "按告警记录查询告警处理历史") @QueryAction - public Mono> queryHandleHistoryPager( - @PathVariable String id, - @RequestBody Mono query) { + public Mono> queryHandleHistoryPager(@PathVariable String id, @RequestBody Mono query) { return query - .doOnNext(queryParamEntity -> queryParamEntity - .toNestQuery(q -> q.and(AlarmHandleHistoryEntity::getAlarmRecordId, TermType.eq, id))) - .flatMap(handleHistoryService::queryPager); + .doOnNext(queryParamEntity -> queryParamEntity + .toNestQuery(q -> q.and("alarmRecordId", TermType.eq, id))) + .flatMap(handleHistoryService::queryPager); } @PostMapping("/{dimensionType}/_handle") @@ -94,7 +102,7 @@ public class AlarmRecordController implements ReactiveServiceQueryController> queryHandleHistoryPager(@PathVariable String dimensionType, @PathVariable String recordId, @RequestBody Mono query) { - return recordService + return alarmRecordService .createQuery() .where(AlarmRecordEntity::getId, recordId) .fetchOne() @@ -102,15 +110,25 @@ public class AlarmRecordController implements ReactiveServiceQueryController query.flatMap(handleHistoryService::queryPager)); } + private Mono handleAlarm(AlarmRecordEntity record, AlarmHandleHistoryEntity entity) { + RelieveInfo relieveInfo = FastBeanCopier.copy(record, new RelieveInfo()); + relieveInfo.setRelieveTime(entity.getHandleTime()); + relieveInfo.setRelieveReason(entity.getDescription()); + relieveInfo.setData(Collections.emptyMap()); + relieveInfo.setAlarmRelieveType(entity.getHandleType().getValue()); + relieveInfo.setDescribe(entity.getDescription()); + return alarmHandler + .relieveAlarm(relieveInfo) + .then(); + } + private Mono> queryPager1(QueryParamEntity query) { if (query.getTotal() != null) { - return getService() - .createQuery() - .setParam(query.rePaging(query.getTotal())) - .fetch() + return this + .query(query.rePaging(query.getTotal())) .collectList() .map(list -> PagerResult.of(query.getTotal(), list, query)); } - return getService().queryPager(query); + return queryPager(query); } } 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 338e1c4c..74fe565f 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 @@ -1,40 +1,22 @@ package org.jetlinks.community.rule.engine.web; import io.swagger.v3.oas.annotations.Operation; -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.ezorm.core.param.Term; import org.hswebframework.web.authorization.annotation.DeleteAction; -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.ReactiveServiceQueryController; -import org.hswebframework.web.i18n.LocaleUtils; -import org.jetlinks.community.reactorql.aggregation.AggregationSupport; -import org.jetlinks.community.rule.engine.service.SceneService; -import org.jetlinks.community.rule.engine.utils.TermColumnUtils; -import org.jetlinks.community.rule.engine.web.request.SceneExecuteRequest; -import org.jetlinks.community.rule.engine.web.response.SceneActionInfo; -import org.jetlinks.community.rule.engine.web.response.SceneAggregationInfo; -import org.jetlinks.community.rule.engine.web.response.SceneTriggerInfo; -import org.jetlinks.core.device.DeviceRegistry; import org.jetlinks.community.rule.engine.entity.SceneEntity; -import org.jetlinks.community.rule.engine.executor.device.DeviceSelectorProvider; -import org.jetlinks.community.rule.engine.executor.device.DeviceSelectorProviders; -import org.jetlinks.community.rule.engine.scene.*; -import org.jetlinks.community.rule.engine.scene.term.TermColumn; -import org.jetlinks.core.metadata.SimplePropertyMetadata; +import org.jetlinks.community.rule.engine.scene.SceneRule; +import org.jetlinks.community.rule.engine.service.SceneService; +import org.jetlinks.community.rule.engine.web.request.SceneExecuteRequest; import org.springframework.web.bind.annotation.*; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; -import java.util.List; import java.util.Map; -import java.util.function.Function; - @RestController @RequestMapping("/scene") @@ -58,7 +40,7 @@ public class SceneController implements ReactiveServiceQueryController update(@PathVariable String id, - @RequestBody Mono sceneRuleMono) { + @RequestBody Mono sceneRuleMono) { return sceneRuleMono .flatMap(sceneRule -> service.updateScene(id, sceneRule)) .then(); @@ -71,13 +53,6 @@ public class SceneController implements ReactiveServiceQueryController disableSceneBatch(@RequestBody Mono> id) { - return id.flatMap(service::disabled); - } - @PutMapping("/{id}/_enable") @Operation(summary = "启用场景") @SaveAction @@ -85,13 +60,6 @@ public class SceneController implements ReactiveServiceQueryController enabledSceneBatch(@RequestBody Mono> id) { - return id.flatMap(service::enable); - } - @PostMapping("/{id}/_execute") @Operation(summary = "手动执行场景") @SaveAction @@ -116,109 +84,4 @@ public class SceneController implements ReactiveServiceQueryController getSupportTriggers() { - return SceneUtils - .getSupportTriggers() - .map(SceneTriggerInfo::of); - } - - @GetMapping("/action/supports") - @Operation(summary = "获取支持的动作类型") - public Flux getSupportActions() { - return SceneUtils - .getSupportActions() - .flatMap(provider -> SceneActionInfo.of(provider)); - } - - @GetMapping("/aggregation/supports") - @Operation(summary = "获取支持的聚合函数") - public Flux getSupportAggregations() { - return LocaleUtils - .currentReactive() - .flatMapMany(locale -> Flux - .fromIterable(AggregationSupport.supports.getAll()) - .map(aggregation -> SceneAggregationInfo.of(aggregation, locale))); - } - - @PostMapping("/parse-term-column") - @Operation(summary = "根据触发器解析出支持的条件列") - @QueryAction - public Flux parseTermColumns(@RequestBody Mono ruleMono) { - return ruleMono - .flatMapMany(rule -> { - Trigger trigger = rule.getTrigger(); - if (trigger != null) { - return trigger.parseTermColumns(); - } - return Flux.empty(); - }); - } - - @PostMapping("/parse-array-child-term-column") - @Operation(summary = "解析数组需要的子元素支持的条件列") - @QueryAction - public Flux parseArrayChildTermColumns(@RequestBody Mono metadataMono) { - return metadataMono - .flatMapMany(metadata -> Flux - .fromIterable(TermColumnUtils.parseArrayChildTermColumns(metadata.getValueType()))); - } - - @PostMapping("/parse-variables") - @Operation(summary = "解析规则中输出的变量") - @QueryAction - public Flux parseVariables(@RequestBody Mono ruleMono, - @RequestParam(required = false) Integer branch, - @RequestParam(required = false) Integer branchGroup, - @RequestParam(required = false) Integer action) { - Mono cache = ruleMono.cache(); - return Mono - .zip( - parseTermColumns(cache).collectList(), - cache, - (columns, rule) -> rule - .createVariables(columns, - branch, - branchGroup, - action)) - .flatMapMany(Function.identity()); - } - - @GetMapping("/device-selectors") - @Operation(summary = "获取支持的设备选择器") - @QueryAction - public Flux getDeviceSelectors() { - return Flux - .fromIterable(DeviceSelectorProviders.allProvider()) - //场景联动的设备动作必须选择一个产品,不再列出产品 - .filter(provider -> !"product".equals(provider.getProvider())) - .map(SelectorInfo::of); - } - - @Getter - @Setter - public static class SelectorInfo { - @Schema(description = "ID") - private String id; - - @Schema(description = "名称") - private String name; - - @Schema(description = "说明") - private String description; - - public static SelectorInfo of(DeviceSelectorProvider provider) { - SelectorInfo info = new SelectorInfo(); - info.setId(provider.getProvider()); - - info.setName(LocaleUtils - .resolveMessage("message.device_selector_" + provider.getProvider(), provider.getName())); - - info.setDescription(LocaleUtils - .resolveMessage("message.device_selector_" + provider.getProvider() + "_desc", provider.getName())); - return info; - } - } - } diff --git a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/web/SceneUtilsController.java b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/web/SceneUtilsController.java new file mode 100644 index 00000000..ebf1c8b8 --- /dev/null +++ b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/web/SceneUtilsController.java @@ -0,0 +1,91 @@ +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 org.hswebframework.web.authorization.annotation.QueryAction; +import org.hswebframework.web.authorization.annotation.Resource; +import org.hswebframework.web.i18n.LocaleUtils; +import org.jetlinks.community.reactorql.aggregation.AggregationSupport; +import org.jetlinks.community.rule.engine.scene.SceneRule; +import org.jetlinks.community.rule.engine.scene.SceneUtils; +import org.jetlinks.community.rule.engine.scene.Variable; +import org.jetlinks.community.rule.engine.scene.term.TermColumn; +import org.jetlinks.community.rule.engine.utils.TermColumnUtils; +import org.jetlinks.community.rule.engine.web.response.SceneActionInfo; +import org.jetlinks.community.rule.engine.web.response.SceneAggregationInfo; +import org.jetlinks.community.rule.engine.web.response.SceneTriggerInfo; +import org.jetlinks.community.rule.engine.web.response.SelectorInfo; +import org.jetlinks.core.metadata.SimplePropertyMetadata; +import org.springframework.web.bind.annotation.*; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +@RestController +@RequestMapping("/scene") +@Tag(name = "场景管理") +@AllArgsConstructor +@Resource(id = "rule-scene", name = "场景管理") +public class SceneUtilsController { + @GetMapping("/trigger/supports") + @Operation(summary = "获取支持的触发器类型") + public Flux getSupportTriggers() { + return SceneUtils + .getSupportTriggers() + .map(SceneTriggerInfo::of); + } + + @GetMapping("/action/supports") + @Operation(summary = "获取支持的动作类型") + public Flux getSupportActions() { + return SceneUtils + .getSupportActions() + .flatMap(provider -> SceneActionInfo.of(provider)); + } + + @GetMapping("/aggregation/supports") + @Operation(summary = "获取支持的聚合函数") + public Flux getSupportAggregations() { + return LocaleUtils + .currentReactive() + .flatMapMany(locale -> Flux + .fromIterable(AggregationSupport.supports.getAll()) + .map(aggregation -> SceneAggregationInfo.of(aggregation, locale))); + } + + @PostMapping("/parse-term-column") + @Operation(summary = "根据触发器解析出支持的条件列") + @QueryAction + public Flux parseTermColumns(@RequestBody Mono ruleMono) { + return ruleMono.flatMapMany(SceneUtils::parseTermColumns); + } + + @PostMapping("/parse-array-child-term-column") + @Operation(summary = "解析数组需要的子元素支持的条件列") + @QueryAction + public Flux parseArrayChildTermColumns(@RequestBody Mono metadataMono) { + return metadataMono + .flatMapMany(metadata -> Flux + .fromIterable(TermColumnUtils.parseArrayChildTermColumns(metadata.getValueType()))); + } + + + @PostMapping("/parse-variables") + @Operation(summary = "解析规则中输出的变量") + @QueryAction + public Flux parseVariables(@RequestBody Mono ruleMono, + @RequestParam(required = false) Integer branch, + @RequestParam(required = false) Integer branchGroup, + @RequestParam(required = false) Integer action) { + return SceneUtils.parseVariables(ruleMono, branch, branchGroup, action); + } + + @GetMapping("/device-selectors") + @Operation(summary = "获取支持的设备选择器") + @QueryAction + public Flux getDeviceSelectors() { + return SceneUtils.getDeviceSelectors(); + } + + +} diff --git a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/web/response/SceneActionInfo.java b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/web/response/SceneActionInfo.java index 431b97cd..068cbc50 100644 --- a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/web/response/SceneActionInfo.java +++ b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/web/response/SceneActionInfo.java @@ -13,7 +13,7 @@ import java.util.function.Function; /** * 执行动作类型. * - * @author zhangji 2025/1/22 + * @author zhangji 2025/1/8 * @since 2.3 */ @Getter diff --git a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/web/response/SceneAggregationInfo.java b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/web/response/SceneAggregationInfo.java index 0ab05e0d..ec125b0a 100644 --- a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/web/response/SceneAggregationInfo.java +++ b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/web/response/SceneAggregationInfo.java @@ -11,7 +11,7 @@ import java.util.Locale; /** * 聚合函数类型. * - * @author zhangji 2025/1/22 + * @author zhangji 2025/1/10 * @since 2.3 */ @Getter @@ -29,10 +29,10 @@ public class SceneAggregationInfo { SceneAggregationInfo aggregationInfo = new SceneAggregationInfo(); aggregationInfo.setId(id); aggregationInfo.setName(LocaleUtils - .resolveMessage("message.scene_aggregation_name_" + id, - locale, - support.getName())); + .resolveMessage("message.scene_aggregation_name_" + id, + locale, + support.getName())); return aggregationInfo; } -} +} \ No newline at end of file diff --git a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/web/response/SceneRuleInfo.java b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/web/response/SceneRuleInfo.java index 53dc8691..430ee829 100644 --- a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/web/response/SceneRuleInfo.java +++ b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/web/response/SceneRuleInfo.java @@ -1,11 +1,10 @@ package org.jetlinks.community.rule.engine.web.response; -import com.alibaba.fastjson.JSON; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Getter; import lombok.Setter; import org.hswebframework.web.bean.FastBeanCopier; -import org.jetlinks.community.rule.engine.entity.RuleInstanceEntity; +import org.jetlinks.community.rule.engine.entity.SceneEntity; import org.jetlinks.community.rule.engine.enums.RuleInstanceState; import org.jetlinks.community.rule.engine.scene.SceneRule; @@ -23,9 +22,9 @@ public class SceneRuleInfo extends SceneRule { @Schema(description = "创建时间") private long createTime; - public static SceneRuleInfo of(RuleInstanceEntity instance) { + public static SceneRuleInfo of(SceneEntity instance) { - SceneRuleInfo info = FastBeanCopier.copy(JSON.parseObject(instance.getModelMeta()), new SceneRuleInfo()); + SceneRuleInfo info = FastBeanCopier.copy(instance, new SceneRuleInfo()); info.setState(instance.getState()); info.setId(instance.getId()); info.setCreateTime(info.getCreateTime()); diff --git a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/web/response/SceneTriggerInfo.java b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/web/response/SceneTriggerInfo.java index c54aaef8..6fd6c5f9 100644 --- a/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/web/response/SceneTriggerInfo.java +++ b/jetlinks-manager/rule-engine-manager/src/main/java/org/jetlinks/community/rule/engine/web/response/SceneTriggerInfo.java @@ -9,7 +9,7 @@ import org.jetlinks.community.rule.engine.scene.SceneTriggerProvider; /** * 触发器类型. * - * @author zhangji 2025/1/22 + * @author zhangji 2025/1/8 * @since 2.3 */ @Getter @@ -41,4 +41,4 @@ public class SceneTriggerInfo { return triggerInfo; } -} +} \ No newline at end of file diff --git a/jetlinks-manager/rule-engine-manager/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/jetlinks-manager/rule-engine-manager/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports index f7f7a176..30529d90 100644 --- a/jetlinks-manager/rule-engine-manager/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +++ b/jetlinks-manager/rule-engine-manager/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -1,2 +1,3 @@ org.jetlinks.community.rule.engine.configuration.RuleEngineManagerConfiguration -org.jetlinks.community.rule.engine.configuration.AlarmTargetConfiguration \ No newline at end of file +org.jetlinks.community.rule.engine.configuration.AlarmTargetConfiguration +org.jetlinks.community.rule.engine.configuration.AlarmConfiguration \ No newline at end of file diff --git a/jetlinks-manager/rule-engine-manager/src/main/resources/i18n/rule-engine-manager/messages_en.properties b/jetlinks-manager/rule-engine-manager/src/main/resources/i18n/rule-engine-manager/messages_en.properties index 516aa155..56ca3b05 100644 --- a/jetlinks-manager/rule-engine-manager/src/main/resources/i18n/rule-engine-manager/messages_en.properties +++ b/jetlinks-manager/rule-engine-manager/src/main/resources/i18n/rule-engine-manager/messages_en.properties @@ -19,19 +19,21 @@ error.scene_id_cannot_be_empty=The scene id cannot be empty error.scene_rule_id_cannot_be_blank=The id cannot be empty error.scene_rule_name_cannot_be_blank=The name cannot be empty error.scene_rule_trigger_cannot_be_null=The trigger cannot be null -error.scene_rule_trigger_device_cannot_be_null=The device trigger cannot be null -error.scene_rule_trigger_timer_cannot_be_null=The timer trigger cannot be null +error.scene_rule_trigger_device_cannot_be_null=Please configure the device trigger rule first +error.scene_rule_trigger_timer_cannot_be_null=Please configure the timer trigger rule first +error.scene_action_rule_cannot_be_null=Please configure scene action rules first error.scene_rule_trigger_device_product_cannot_be_null=The product cannot be null error.scene_rule_trigger_device_operation_cannot_be_null=The device trigger operation cannot be null error.scene_rule_trigger_device_operation_event_id_cannot_be_null=The event id operation cannot be null error.scene_rule_trigger_device_operation_read_property_cannot_be_empty=The read property cannot be empty error.scene_rule_trigger_device_operation_write_property_cannot_be_empty=The write property cannot be empty error.scene_rule_trigger_device_operation_function_id_cannot_be_null=The function id cannot be null -error.scene_rule_trigger_device_operation_function_parameter_cannot_be_empty=The function parameter cannot be empty error.scene_rule_actions_notify_type_cannot_be_empty=The notify type cannot be null error.scene_rule_actions_notify_id_cannot_be_empty=The notify id cannot be null error.scene_rule_actions_notify_template_cannot_be_blank=The notify template cannot be null error.scene_rule_actions_notify_variables_cannot_be_blank=The notify variables cannot be null +error.alarm_record_not_fount=[{0}] alarm record not found +error.the_alarm_record_has_been_processed=the alarm record has been processed #entity-package error.not_set_alarm_rule=The alarm rules are not set @@ -57,23 +59,37 @@ error.rule_nodes_cannot_be_used=Rule nodes cannot be used:{0} error.cannot_delete_alarm_config_with_warnning_record=The alarm config has warnning record and cannot be deleted #enmus -org.jetlinks.community.rule.engine.enums.AlarmState.running=running -org.jetlinks.community.rule.engine.enums.AlarmState.stopped=stopped +org.jetlinks.community.rule.engine.enums.AlarmDataExchangeState.enabled=Active +org.jetlinks.community.rule.engine.enums.AlarmDataExchangeState.disable=Disabled -org.jetlinks.community.rule.engine.enums.RuleInstanceState.started=started -org.jetlinks.community.rule.engine.enums.RuleInstanceState.disable=disable +org.jetlinks.community.rule.engine.enums.AlarmState.enabled=Active +org.jetlinks.community.rule.engine.enums.AlarmState.disabled=Disabled +org.jetlinks.community.rule.engine.enums.AlarmState.running=Running +org.jetlinks.community.rule.engine.enums.AlarmState.stopped=Stopped -org.jetlinks.community.rule.engine.device.DeviceAlarmRule.TriggerType.device=device -org.jetlinks.community.rule.engine.device.DeviceAlarmRule.TriggerType.timer=timer +org.jetlinks.community.rule.engine.enums.RuleInstanceState.started=Active +org.jetlinks.community.rule.engine.enums.RuleInstanceState.disable=Disabled -org.jetlinks.community.rule.engine.device.SceneRule.TriggerType.manual=manual -org.jetlinks.community.rule.engine.device.SceneRule.TriggerType.timer=timer -org.jetlinks.community.rule.engine.device.SceneRule.TriggerType.device=device -org.jetlinks.community.rule.engine.device.SceneRule.TriggerType.scene=scene -org.jetlinks.community.rule.engine.device.SceneRule.TriggerType.other=other +org.jetlinks.community.rule.engine.device.DeviceAlarmRule.TriggerType.device=Device +org.jetlinks.community.rule.engine.device.DeviceAlarmRule.TriggerType.timer=Timer -org.jetlinks.community.rule.engine.tenant.RuleEngineAssetType.ruleModel=ruleModel -org.jetlinks.community.rule.engine.tenant.RuleEngineAssetType.ruleInstance=ruleInstance +org.jetlinks.community.rule.engine.device.SceneRule.TriggerType.manual=Manual +org.jetlinks.community.rule.engine.device.SceneRule.TriggerType.timer=Timer +org.jetlinks.community.rule.engine.device.SceneRule.TriggerType.device=Device +org.jetlinks.community.rule.engine.device.SceneRule.TriggerType.scene=Scene +org.jetlinks.community.rule.engine.device.SceneRule.TriggerType.other=Other + +org.jetlinks.community.rule.engine.assets.RuleEngineAssetType.ruleModel=Rule model +org.jetlinks.community.rule.engine.assets.RuleEngineAssetType.ruleInstance=Rule instance + +org.jetlinks.community.rule.engine.enums.AlarmRecordState.warning=Alarming +org.jetlinks.community.rule.engine.enums.AlarmRecordState.normal=No alarm + +org.jetlinks.community.rule.engine.enums.AlarmHandleType.system=System +org.jetlinks.community.rule.engine.enums.AlarmHandleType.user=Manual + +org.jetlinks.community.rule.engine.enums.AlarmHandleState.processed=Processed +org.jetlinks.community.rule.engine.enums.AlarmHandleState.unprocessed=Unprocessed error.can_not_delete_stated_scene=The scene has been started and cannot be deleted @@ -82,6 +98,18 @@ message.scene_term_column_now=Server time message.scene_term_column_now_desc=Server time when device data is received message.scene_term_column_timestamp=Data reporting time message.scene_term_column_timestamp_desc=The time specified in the data reported by the device +message.scene_term_column_event_success=Whether the scene trigger was successful +message.device_metadata_property=property +message.device_metadata_event=event +message.device_metadata_function=function + +message.scene_term_write_parseData=Write the [{0}] value of point +message.scene_term_read_parseData=Read point [{0}] value +message.collector_points_data=Point data + + +message.scene_term_column_execute_success=Whether the execution is successful +message.scene_term_column_execute_success_desc=Execute result message.property_value_type_current=Current value message.property_value_type_current_desc=Data value reported by the device this time @@ -97,7 +125,7 @@ message.property_value_type_last_time_desc=Last reported data time message.property_value_type_last_time_nest_desc=Data time in last [{0}] message.property_value_type_last_desc=Last reported data value message.property_value_type_last_nest_desc=Data value in last [{0}] -message.scene_triggered_relieve_alarm=scene triggered relieve alarm +message.scene_triggered_relieve_alarm=Scene triggered relieve alarm message.scene_term_column_full_name={1} of {0} @@ -107,40 +135,79 @@ message.scene_trigger_type=Scene trigger type message.term_type_scene_manual_desc=When the system receives a manual trigger command, it triggers the scene message.term_type_scene_manual_actual_desc=Manual trigger message.term_type_scene_timer_actual_desc=Timer trigger -message.term_size_of_array=array size -message.term_origin_of_array=array origin -message.term_element_of_array=array element +message.term_size_of_array=Array size +message.term_origin_of_array=Array origin +message.term_element_of_array=Array element + +message.scene_trigger_collector_output=Collector trigger output data +message.scene_trigger_device_output=Device trigger output data +message.scene_trigger_manual_output=Manual trigger output data +message.scene.variable.execute_success=Whether the execution is successful + +message.scene.action.device-data.properties=Device[{0}] +message.scene.action.device-data.configs=Device[{0}] +message.scene.action.device-data.events=Device[{0}] +message.scene.action.device-data.tags=Device[{0}] + +message.device.properties.name=Properties +message.device.value.name=Properties value + +message.action_var_index=Action[{0}] +message.action_var_index_full=Action[{0}] output +message.action_var_output_description=Output result of action[{0}] execution + +message.device.variable.device_id=Device id +message.device.variable.device_name=Device name +message.device.variable.product_id=Product id +message.device.variable.product_name=Product name + +message.alarm_config_id=Alarm config id +message.alarm_config_name=Alarm config name +message.alarm_level=Alarm level + +message.first_alarm=Is it the first alarm +message.first_alarm_description=Whether it is the first alarm or the first alarm after being cleared +message.alarm_time=First alarm time +message.alarm_time_description=The time of the first alarm or the first alarm after being cleared +message.last_alarm_time=Last alarm time +message.last_alarm_time_description=The time when the last alarm was triggered +message.action_var_read_property=The return value of readding property [{0}] +message.action_var_write_property=The return value of writing property [{0}] +message.action_var_function=The return value of calling function [{0}] + +message.rule_engine_alarm_collector=Collector +message.rule_engine_alarm_aiModel=AiModel +message.rule_engine_alarm_aiTask=AiTask +message.rule_engine_alarm_device=Device +message.rule_engine_alarm_organization=Org +message.rule_engine_alarm_product=Product +message.rule_engine_alarm_scene=Scene + +message.subscriber.provider.alarm-other=Scene alarm +message.subscriber.provider.alarm-product=Product alarm +message.subscriber.provider.alarm-device=Device alarm +message.subscriber.provider.alarm-org=Org alarm +message.subscriber.provider.alarm-collector=Collector alarm +message.subscriber.provider.alarm=Alarm -message.rule_engine_alarm_collector=collector -message.rule_engine_alarm_aiModel=aiModel -message.rule_engine_alarm_aiTask=aiTask -message.rule_engine_alarm_device=device -message.rule_engine_alarm_organization=org -message.rule_engine_alarm_product=product -message.rule_engine_alarm_scene=scene -message.rule_engine_alarm_subscriber_provider_alarm-other=scene alarm -message.rule_engine_alarm_subscriber_provider_alarm-product=product alarm -message.rule_engine_alarm_subscriber_provider_alarm-device=device alarm -message.rule_engine_alarm_subscriber_provider_alarm-org=org alarm -message.rule_engine_alarm_subscriber_provider_alarm=alarm #scene-trigger -message.scene_trigger_name_manual=manual trigger -message.scene_trigger_name_device=device trigger -message.scene_trigger_name_timer=timer trigger -message.scene_trigger_name_collector=collector trigger +message.scene_trigger_name_manual=Manual trigger +message.scene_trigger_name_device=Device trigger +message.scene_trigger_name_timer=Timer trigger +message.scene_trigger_name_collector=Collector trigger message.scene_trigger_desc_manual=Suitable for third-party platforms issuing instructions to IoT platforms to control devices message.scene_trigger_desc_device=Suitable for executing specified actions when device data or behavior meets triggering conditions message.scene_trigger_desc_timer=Suitable for regularly executing fixed tasks message.scene_trigger_desc_collector=Suitable for executing specified actions when the collector point meets the triggering conditions #scene-action -message.scene_action_name_device=device output -message.scene_action_name_device-data=device data -message.scene_action_name_notify=message notification -message.scene_action_name_delay=delay execution -message.scene_action_name_trigger=trigger alarm -message.scene_action_name_relieve=relieve alarm -message.scene_action_name_collector=collector output +message.scene_action_name_device=Device output +message.scene_action_name_device-data=Device data +message.scene_action_name_notify=Message notification +message.scene_action_name_delay=Delay execution +message.scene_action_name_trigger=Trigger alarm +message.scene_action_name_relieve=Relieve alarm +message.scene_action_name_collector=Collector output message.scene_action_desc_device=Configure device invocation function, read attributes, set attribute rules message.scene_action_desc_device-data=Get basic information, attributes, tags, events, etc. of the device message.scene_action_desc_notify=Configure notifications to be sent to designated users via email, DingTalk, WeChat, SMS, etc @@ -159,4 +226,41 @@ message.scene_aggregation_name_FIRST=FIRST message.scene_aggregation_name_LAST=LAST message.scene_aggregation_name_MEDIAN=MEDIAN message.scene_aggregation_name_SPREAD=SPREAD -message.scene_aggregation_name_STDDEV=STDDEV \ No newline at end of file +message.scene_aggregation_name_STDDEV=STDDEV + +hswebframework.web.system.permission.alarm-config=Alarm configuration +hswebframework.web.system.permission.rule-instance=Rule engine instance +hswebframework.web.system.permission.rule-scene=Scenario management +hswebframework.web.system.permission.alarm-record=Alarm record + +#dict +message.handle.type.dict.name=Alarm handle type +message.handle.type.dict.item.user=Manual +message.handle.type.dict.item.system=System + +#alarm-notify +message.alarm.notify.device=Device [{0}] alarm occurs:{1} +message.alarm.notify.product=Product [{0}] alarm occurs:{1} +message.alarm.notify.organization=Organization [{0}] alarm occurs:{1} +message.alarm.notify.scene=Scene [{0}] alarm occurs:{1} +message.alarm.notify.collector=Collector [{0}] alarm occurs:{1} + +#execute action +message.thing.info.spec.property.value=Value +message.thing.info.spec.property.state=State +message.thing.info.spec.property.timestamp=Timestamp +message.thing.info.spec.tag.value=Tag +message.thing.info.spec.tag.timestamp=Timestamp +message.thing.info.spec.event.data=Data +message.thing.info.spec.event.timestamp=Timestamp +message.thing.info.spec.configs=Basic +message.thing.info.spec.properties=Properties +message.thing.info.spec.tags=Tags +message.thing.info.spec.events=Event +message.thing.info.spec.config.state=Online Status +message.thing.info.spec.config.state-online=Online +message.thing.info.spec.config.state-offline=Offline +message.thing.info.spec.config.onlineTime=Last Online Time +message.thing.info.spec.config.offlineTime=Last Offline Time +message.thing.info.spec.config.deviceName=Device Name +message.thing.info.spec.config.productName=Product Name \ No newline at end of file diff --git a/jetlinks-manager/rule-engine-manager/src/main/resources/i18n/rule-engine-manager/messages_zh.properties b/jetlinks-manager/rule-engine-manager/src/main/resources/i18n/rule-engine-manager/messages_zh.properties index 822f828b..5520e465 100644 --- a/jetlinks-manager/rule-engine-manager/src/main/resources/i18n/rule-engine-manager/messages_zh.properties +++ b/jetlinks-manager/rule-engine-manager/src/main/resources/i18n/rule-engine-manager/messages_zh.properties @@ -19,8 +19,9 @@ error.scene_id_cannot_be_empty=\u573A\u666FID\u4E0D\u80FD\u4E3A\u7A7A error.scene_rule_id_cannot_be_blank=id\u4E0D\u80FD\u4E3A\u7A7A error.scene_rule_name_cannot_be_blank=\u540D\u79F0\u4E0D\u80FD\u4E3A\u7A7A error.scene_rule_trigger_cannot_be_null=\u89E6\u53D1\u65B9\u5F0F\u4E0D\u80FD\u4E3A\u7A7A -error.scene_rule_trigger_device_cannot_be_null=\u8BBE\u5907\u89E6\u53D1\u914D\u7F6E\u4E0D\u80FD\u4E3A\u7A7A -error.scene_rule_trigger_timer_cannot_be_null=\u5B9A\u65F6\u89E6\u53D1\u914D\u7F6E\u4E0D\u80FD\u4E3A\u7A7A +error.scene_rule_trigger_device_cannot_be_null=\u8BF7\u5148\u914D\u7F6E\u8BBE\u5907\u89E6\u53D1\u89C4\u5219 +error.scene_rule_trigger_timer_cannot_be_null=\u8BF7\u5148\u914D\u7F6E\u5B9A\u65F6\u89E6\u53D1\u89C4\u5219 +error.scene_action_rule_cannot_be_null=\u8BF7\u5148\u914D\u7F6E\u573A\u666F\u6267\u884C\u89C4\u5219 error.scene_rule_trigger_device_product_cannot_be_null=\u4EA7\u54C1\u4E0D\u80FD\u4E3A\u7A7A error.scene_rule_trigger_device_operation_cannot_be_null=\u89E6\u53D1\u7C7B\u578B\u4E0D\u80FD\u4E3A\u7A7A error.scene_rule_trigger_device_operation_event_id_cannot_be_null=\u8BBE\u5907\u4E8B\u4EF6\u4E0D\u80FD\u4E3A\u7A7A @@ -58,9 +59,15 @@ error.rule_nodes_cannot_be_used=\u4E0D\u80FD\u4F7F\u7528\u89C4\u5219\u8282\u70B9 error.cannot_delete_alarm_config_with_warnning_record=\u5F53\u524D\u544A\u8B66\u6709\u544A\u8B66\u4E2D\u7684\u6570\u636E\uFF0C\u4E0D\u53EF\u5220\u9664 #enums +org.jetlinks.community.rule.engine.enums.AlarmDataExchangeState.enabled=\u6B63\u5E38 +org.jetlinks.community.rule.engine.enums.AlarmDataExchangeState.disable=\u7981\u7528 + org.jetlinks.community.rule.engine.enums.AlarmState.running=\u8FD0\u884C\u4E2D org.jetlinks.community.rule.engine.enums.AlarmState.stopped=\u5DF2\u505C\u6B62 +org.jetlinks.community.rule.engine.enums.AlarmState.enabled=\u6B63\u5E38 +org.jetlinks.community.rule.engine.enums.AlarmState.disabled=\u7981\u7528 + org.jetlinks.community.rule.engine.enums.RuleInstanceState.started=\u6B63\u5E38 org.jetlinks.community.rule.engine.enums.RuleInstanceState.disable=\u7981\u7528 @@ -76,8 +83,17 @@ org.jetlinks.community.rule.engine.device.SceneRule.TriggerType.device=\u8BBE\u5 org.jetlinks.community.rule.engine.device.SceneRule.TriggerType.scene=\u573A\u666F org.jetlinks.community.rule.engine.device.SceneRule.TriggerType.other=\u5176\u4ED6 -org.jetlinks.community.rule.engine.tenant.RuleEngineAssetType.ruleModel=\u89C4\u5219\u6A21\u578B -org.jetlinks.community.rule.engine.tenant.RuleEngineAssetType.ruleInstance=\u89C4\u5219\u5B9E\u4F8B +org.jetlinks.community.rule.engine.assets.RuleEngineAssetType.ruleModel=\u89C4\u5219\u6A21\u578B +org.jetlinks.community.rule.engine.assets.RuleEngineAssetType.ruleInstance=\u89C4\u5219\u5B9E\u4F8B + +org.jetlinks.community.rule.engine.enums.AlarmRecordState.warning=\u544A\u8B66\u4E2D +org.jetlinks.community.rule.engine.enums.AlarmRecordState.normal=\u65E0\u544A\u8B66 + +org.jetlinks.community.rule.engine.enums.AlarmHandleType.system=\u7CFB\u7EDF +org.jetlinks.community.rule.engine.enums.AlarmHandleType.user=\u4EBA\u5DE5 + +org.jetlinks.community.rule.engine.enums.AlarmHandleState.processed=\u5DF2\u5904\u7406 +org.jetlinks.community.rule.engine.enums.AlarmHandleState.unprocessed=\u672A\u5904\u7406 error.can_not_delete_stated_scene=\u573A\u666F\u5DF2\u542F\u52A8,\u65E0\u6CD5\u5220\u9664 @@ -85,8 +101,17 @@ message.scene_term_column_now=\u7CFB\u7EDF\u65F6\u95F4 message.scene_term_column_now_desc=\u670D\u52A1\u5668\u65F6\u95F4 message.scene_term_column_timestamp=\u4E0A\u62A5\u65F6\u95F4 message.scene_term_column_timestamp_desc=\u6570\u636E\u4E0A\u62A5\u65F6\u6307\u5B9A\u7684\u65F6\u95F4 +message.scene_term_column_event_success=\u573A\u666F\u89E6\u53D1\u662F\u5426\u6210\u529F +message.device_metadata_property=\u5C5E\u6027 +message.device_metadata_event=\u4E8B\u4EF6 +message.device_metadata_function=\u529F\u80FD +message.scene_term_write_parseData=\u5199\u5165\u70B9\u4F4D\u3010{0}\u3011\u7684\u503C +message.scene_term_read_parseData=\u8BFB\u53D6\u70B9\u4F4D\u3010{0}\u3011\u7684\u503C +message.collector_points_data=\u70B9\u4F4D\u6570\u636E +message.scene_term_column_execute_success=\u662F\u5426\u6267\u884C\u6210\u529F +message.scene_term_column_execute_success_desc=\u6267\u884C\u7ED3\u679C message.property_value_type_current=\u5F53\u524D\u503C message.property_value_type_current_param=\u5C5E\u6027/\u5C5E\u6027\u5BF9\u8C61/\u7B2C\u4E00\u4E2A\u503C/ \u5F53\u524D\u503C @@ -100,7 +125,7 @@ message.property_value_type_recent_nest_desc=\u5F53\u524D\u503C\u4E3A\u7A7A\u65F message.property_value_type_last=\u4E0A\u4E00\u503C message.property_value_type_last_time=\u4E0A\u4E00\u6B21\u4E0A\u62A5\u65F6\u95F4 message.property_value_type_last_time_desc=\u4E0A\u4E00\u6B21\u4E0A\u62A5\u6570\u636E\u7684\u65F6\u95F4 -message.property_value_type_last_time_nest_desc=\u4e0a\u4e00\u6b21\u3010{0}\u3011\u4e2d\u4e0a\u62a5\u6570\u636e\u7684\u65f6\u95f4 +message.property_value_type_last_time_nest_desc=\u4E0A\u4E00\u6B21\u3010{0}\u3011\u4E2D\u4E0A\u62A5\u6570\u636E\u7684\u65F6\u95F4 message.property_value_type_last_desc=\u4E0A\u4E00\u6B21\u4E0A\u62A5\u7684\u6570\u636E\u503C message.property_value_type_last_nest_desc=\u4E0A\u4E00\u6B21\u3010{0}\u3011\u4E2D\u7684\u6570\u636E\u503C message.scene_triggered_relieve_alarm=\u573A\u666F\u89E6\u53D1\u89E3\u9664\u544A\u8B66 @@ -115,7 +140,47 @@ message.term_type_scene_manual_actual_desc=\u624B\u52A8\u89E6\u53D1\u544A\u8B66 message.term_type_scene_timer_actual_desc=\u5B9A\u65F6\u89E6\u53D1\u544A\u8B66 message.term_size_of_array=\u6570\u7EC4\u957F\u5EA6 message.term_origin_of_array=\u539F\u59CB\u503C -message.term_element_of_array=\u6570\u503C\u5143\u7D20 +message.term_element_of_array=\u6570\u7EC4\u5143\u7D20 + +message.scene_trigger_collector_output=\u91C7\u96C6\u5668\u89E6\u53D1\u8F93\u51FA\u7684\u6570\u636E +message.scene_trigger_device_output=\u8BBE\u5907\u89E6\u53D1\u8F93\u51FA\u7684\u6570\u636E +message.scene_trigger_manual_output=\u624B\u52A8\u89E6\u53D1\u8F93\u51FA\u7684\u6570\u636E +message.scene.variable.execute_success=\u662F\u5426\u6267\u884C\u6210\u529F + +message.scene.action.device-data.properties=\u8BBE\u5907[{0}]\u4FE1\u606F +message.scene.action.device-data.configs=\u8BBE\u5907[{0}]\u4FE1\u606F +message.scene.action.device-data.events=\u8BBE\u5907[{0}]\u4FE1\u606F +message.scene.action.device-data.tags=\u8BBE\u5907[{0}]\u4FE1\u606F +message.device.properties.name=\u7269\u6A21\u578B\u5C5E\u6027 +message.device.value.name=\u5C5E\u6027\u503C + +message.action_var_index=\u52A8\u4F5C[{0}] +message.action_var_index_full=\u52A8\u4F5C[{0}]\u8F93\u51FA +message.action_var_output_description=\u52A8\u4F5C[{0}]\u6267\u884C\u7684\u8F93\u51FA\u7ED3\u679C + +message.device.variable.device_id=\u8BBE\u5907ID +message.device.variable.device_name=\u8BBE\u5907\u540D\u79F0 +message.device.variable.product_id=\u4EA7\u54C1ID +message.device.variable.product_name=\u4EA7\u54C1\u540D\u79F0 + +message.alarm_config_id=\u544A\u8B66\u914D\u7F6EID +message.alarm_config_name=\u544A\u8B66\u914D\u7F6E\u540D\u79F0 +message.alarm_level=\u544A\u8B66\u7EA7\u522B + +message.first_alarm=\u662F\u5426\u9996\u6B21\u544A\u8B66 +message.first_alarm_description=\u662F\u5426\u4E3A\u9996\u6B21\u544A\u8B66\u6216\u8005\u89E3\u9664\u540E\u7684\u7B2C\u4E00\u6B21\u544A\u8B66 +message.alarm_time=\u9996\u6B21\u544A\u8B66\u65F6\u95F4 +message.alarm_time_description=\u9996\u6B21\u544A\u8B66\u6216\u8005\u89E3\u9664\u544A\u8B66\u540E\u7684\u7B2C\u4E00\u6B21\u544A\u8B66\u65F6\u95F4 +message.last_alarm_time=\u4E0A\u4E00\u6B21\u544A\u8B66\u65F6\u95F4 +message.last_alarm_time_description=\u4E0A\u4E00\u6B21\u89E6\u53D1\u544A\u8B66\u7684\u65F6\u95F4 +message.action_var_read_property=\u8BFB\u53D6\u5C5E\u6027[{0}]\u8FD4\u56DE\u503C +message.action_var_write_property=\u8BBE\u7F6E\u5C5E\u6027[{0}]\u8FD4\u56DE\u503C +message.action_var_function=\u529F\u80FD\u8C03\u7528[{0}]\u8FD4\u56DE\u503C + +hswebframework.web.system.permission.alarm-config=\u544A\u8B66\u914D\u7F6E +hswebframework.web.system.permission.rule-instance=\u89C4\u5219\u5F15\u64CE\u5B9E\u4F8B +hswebframework.web.system.permission.rule-scene=\u573A\u666F\u7BA1\u7406 +hswebframework.web.system.permission.alarm-record=\u544A\u8B66\u8BB0\u5F55 message.rule_engine_alarm_collector=\u91C7\u96C6\u5668 message.rule_engine_alarm_aiModel=AI\u6A21\u578B @@ -124,44 +189,79 @@ message.rule_engine_alarm_device=\u8BBE\u5907 message.rule_engine_alarm_organization=\u7EC4\u7EC7 message.rule_engine_alarm_product=\u4EA7\u54C1 message.rule_engine_alarm_scene=\u573A\u666F -message.rule_engine_alarm_subscriber_provider_alarm-other=\u573A\u666F\u544A\u8B66 -message.rule_engine_alarm_subscriber_provider_alarm-product=\u4EA7\u54C1\u544A\u8B66 -message.rule_engine_alarm_subscriber_provider_alarm-device=\u8BBE\u5907\u544A\u8B66 -message.rule_engine_alarm_subscriber_provider_alarm-org=\u7EC4\u7EC7\u544A\u8B66 -message.rule_engine_alarm_subscriber_provider_alarm=\u544A\u8B66 + +message.subscriber.provider.alarm-other=\u573A\u666F\u544A\u8B66 +message.subscriber.provider.alarm-product=\u4EA7\u54C1\u544A\u8B66 +message.subscriber.provider.alarm-device=\u8BBE\u5907\u544A\u8B66 +message.subscriber.provider.alarm-org=\u7EC4\u7EC7\u544A\u8B66 +message.subscriber.provider.alarm-collector=\u91C7\u96C6\u5668\u544A\u8B66 +message.subscriber.provider.alarm=\u544A\u8B66 + #scene-trigger -message.scene_trigger_name_manual=\u624b\u52a8\u89e6\u53d1 -message.scene_trigger_name_device=\u8bbe\u5907\u89e6\u53d1 -message.scene_trigger_name_timer=\u5b9a\u65f6\u89e6\u53d1 -message.scene_trigger_name_collector=\u91c7\u96c6\u5668\u89e6\u53d1 -message.scene_trigger_desc_manual=\u9002\u7528\u4e8e\u7b2c\u4e09\u65b9\u5e73\u53f0\u5411\u7269\u8054\u7f51\u5e73\u53f0\u4e0b\u53d1\u6307\u4ee4\u63a7\u5236\u8bbe\u5907 -message.scene_trigger_desc_device=\u9002\u7528\u4e8e\u8bbe\u5907\u6570\u636e\u6216\u884c\u4e3a\u6ee1\u8db3\u89e6\u53d1\u6761\u4ef6\u65f6\uff0c\u6267\u884c\u6307\u5b9a\u7684\u52a8\u4f5c -message.scene_trigger_desc_timer=\u9002\u7528\u4e8e\u5b9a\u671f\u6267\u884c\u56fa\u5b9a\u4efb\u52a1 -message.scene_trigger_desc_collector=\u9002\u7528\u4e8e\u91c7\u96c6\u5668\u70b9\u4f4d\u6ee1\u8db3\u89e6\u53d1\u6761\u4ef6\u65f6\uff0c\u6267\u884c\u6307\u5b9a\u7684\u52a8\u4f5c +message.scene_trigger_name_manual=\u624B\u52A8\u89E6\u53D1 +message.scene_trigger_name_device=\u8BBE\u5907\u89E6\u53D1 +message.scene_trigger_name_timer=\u5B9A\u65F6\u89E6\u53D1 +message.scene_trigger_name_collector=\u91C7\u96C6\u5668\u89E6\u53D1 +message.scene_trigger_desc_manual=\u9002\u7528\u4E8E\u7B2C\u4E09\u65B9\u5E73\u53F0\u5411\u7269\u8054\u7F51\u5E73\u53F0\u4E0B\u53D1\u6307\u4EE4\u63A7\u5236\u8BBE\u5907 +message.scene_trigger_desc_device=\u9002\u7528\u4E8E\u8BBE\u5907\u6570\u636E\u6216\u884C\u4E3A\u6EE1\u8DB3\u89E6\u53D1\u6761\u4EF6\u65F6\uFF0C\u6267\u884C\u6307\u5B9A\u7684\u52A8\u4F5C +message.scene_trigger_desc_timer=\u9002\u7528\u4E8E\u5B9A\u671F\u6267\u884C\u56FA\u5B9A\u4EFB\u52A1 +message.scene_trigger_desc_collector=\u9002\u7528\u4E8E\u91C7\u96C6\u5668\u70B9\u4F4D\u6EE1\u8DB3\u89E6\u53D1\u6761\u4EF6\u65F6\uFF0C\u6267\u884C\u6307\u5B9A\u7684\u52A8\u4F5C #scene-action -message.scene_action_name_device=\u8bbe\u5907\u8f93\u51fa -message.scene_action_name_device-data=\u8bbe\u5907\u4fe1\u606f -message.scene_action_name_notify=\u6d88\u606f\u901a\u77e5 -message.scene_action_name_delay=\u5ef6\u8fdf\u6267\u884c -message.scene_action_name_trigger=\u89e6\u53d1\u544a\u8b66 -message.scene_action_name_relieve=\u89e3\u9664\u544a\u8b66 -message.scene_action_name_collector=\u91c7\u96c6\u5668\u8f93\u51fa -message.scene_action_desc_device=\u914d\u7f6e\u8bbe\u5907\u8c03\u7528\u529f\u80fd\u3001\u8bfb\u53d6\u5c5e\u6027\u3001\u8bbe\u7f6e\u5c5e\u6027\u89c4\u5219 -message.scene_action_desc_device-data=\u83b7\u53d6\u8bbe\u5907\u7684\u57fa\u672c\u4fe1\u606f\u3001\u5c5e\u6027\u3001\u6807\u7b7e\u3001\u4e8b\u4ef6\u7b49 -message.scene_action_desc_notify=\u914d\u7f6e\u5411\u6307\u5b9a\u7528\u6237\u53d1\u90ae\u4ef6\u3001\u9489\u9489\u3001\u5fae\u4fe1\u3001\u77ed\u4fe1\u7b49\u901a\u77e5 -message.scene_action_desc_delay=\u7b49\u5f85\u4e00\u6bb5\u65f6\u95f4\u540e\uff0c\u518d\u6267\u884c\u540e\u7eed\u52a8\u4f5c -message.scene_action_desc_trigger=\u914d\u7f6e\u89e6\u53d1\u544a\u8b66\u89c4\u5219\uff0c\u9700\u914d\u5408\u201c\u544a\u8b66\u914d\u7f6e\u201d\u4f7f\u7528 -message.scene_action_desc_relieve=\u914d\u7f6e\u89e3\u9664\u544a\u8b66\u89c4\u5219\uff0c\u9700\u914d\u5408\u201c\u544a\u8b66\u914d\u7f6e\u201d\u4f7f\u7528 -message.scene_action_desc_collector=\u914d\u7f6e\u8bfb\u53d6\u70b9\u4f4d\u3001\u8bbe\u7f6e\u70b9\u4f4d\u89c4\u5219 +message.scene_action_name_device=\u8BBE\u5907\u8F93\u51FA +message.scene_action_name_device-data=\u8BBE\u5907\u4FE1\u606F +message.scene_action_name_notify=\u6D88\u606F\u901A\u77E5 +message.scene_action_name_delay=\u5EF6\u8FDF\u6267\u884C +message.scene_action_name_trigger=\u89E6\u53D1\u544A\u8B66 +message.scene_action_name_relieve=\u89E3\u9664\u544A\u8B66 +message.scene_action_name_collector=\u91C7\u96C6\u5668\u8F93\u51FA +message.scene_action_desc_device=\u914D\u7F6E\u8BBE\u5907\u8C03\u7528\u529F\u80FD\u3001\u8BFB\u53D6\u5C5E\u6027\u3001\u8BBE\u7F6E\u5C5E\u6027\u89C4\u5219 +message.scene_action_desc_device-data=\u83B7\u53D6\u8BBE\u5907\u7684\u57FA\u672C\u4FE1\u606F\u3001\u5C5E\u6027\u3001\u6807\u7B7E\u3001\u4E8B\u4EF6\u7B49 +message.scene_action_desc_notify=\u914D\u7F6E\u5411\u6307\u5B9A\u7528\u6237\u53D1\u90AE\u4EF6\u3001\u9489\u9489\u3001\u5FAE\u4FE1\u3001\u77ED\u4FE1\u7B49\u901A\u77E5 +message.scene_action_desc_delay=\u7B49\u5F85\u4E00\u6BB5\u65F6\u95F4\u540E\uFF0C\u518D\u6267\u884C\u540E\u7EED\u52A8\u4F5C +message.scene_action_desc_trigger=\u914D\u7F6E\u89E6\u53D1\u544A\u8B66\u89C4\u5219\uFF0C\u9700\u914D\u5408\u201C\u544A\u8B66\u914D\u7F6E\u201D\u4F7F\u7528 +message.scene_action_desc_relieve=\u914D\u7F6E\u89E3\u9664\u544A\u8B66\u89C4\u5219\uFF0C\u9700\u914D\u5408\u201C\u544A\u8B66\u914D\u7F6E\u201D\u4F7F\u7528 +message.scene_action_desc_collector=\u914D\u7F6E\u8BFB\u53D6\u70B9\u4F4D\u3001\u8BBE\u7F6E\u70B9\u4F4D\u89C4\u5219 #aggregation -message.scene_aggregation_name_COUNT=\u603b\u6570 -message.scene_aggregation_name_DISTINCT_COUNT=\u603b\u6570(\u53bb\u91cd) -message.scene_aggregation_name_MIN=\u6700\u5c0f\u503c -message.scene_aggregation_name_MAX=\u6700\u5927\u503c -message.scene_aggregation_name_AVG=\u5e73\u5747\u503c -message.scene_aggregation_name_SUM=\u603b\u548c -message.scene_aggregation_name_FIRST=\u7b2c\u4e00\u4e2a\u503c -message.scene_aggregation_name_LAST=\u6700\u540e\u4e00\u4e2a\u503c -message.scene_aggregation_name_MEDIAN=\u4e2d\u4f4d\u6570 -message.scene_aggregation_name_SPREAD=\u6781\u5dee -message.scene_aggregation_name_STDDEV=\u6807\u51c6\u5dee \ No newline at end of file +message.scene_aggregation_name_COUNT=\u603B\u6570 +message.scene_aggregation_name_DISTINCT_COUNT=\u603B\u6570(\u53BB\u91CD) +message.scene_aggregation_name_MIN=\u6700\u5C0F\u503C +message.scene_aggregation_name_MAX=\u6700\u5927\u503C +message.scene_aggregation_name_AVG=\u5E73\u5747\u503C +message.scene_aggregation_name_SUM=\u603B\u548C +message.scene_aggregation_name_FIRST=\u7B2C\u4E00\u4E2A\u503C +message.scene_aggregation_name_LAST=\u6700\u540E\u4E00\u4E2A\u503C +message.scene_aggregation_name_MEDIAN=\u4E2D\u4F4D\u6570 +message.scene_aggregation_name_SPREAD=\u6781\u5DEE +message.scene_aggregation_name_STDDEV=\u6807\u51C6\u5DEE + +#dict +message.handle.type.dict.name=\u544A\u8B66\u5904\u7406\u7C7B\u578B +message.handle.type.dict.item.user=\u4EBA\u5DE5 +message.handle.type.dict.item.system=\u7CFB\u7EDF + +#alarm-notify +message.alarm.notify.device=\u8BBE\u5907[{0}]\u53D1\u751F\u544A\u8B66:{1} +message.alarm.notify.product=\u4EA7\u54C1[{0}]\u53D1\u751F\u544A\u8B66:{1} +message.alarm.notify.organization=\u7EC4\u7EC7[{0}]\u53D1\u751F\u544A\u8B66:{1} +message.alarm.notify.scene=\u573A\u666F[{0}]\u53D1\u751F\u544A\u8B66:{1} +message.alarm.notify.collector=\u91C7\u96C6\u5668[{0}]\u53D1\u751F\u544A\u8B66:{1} + +#execute action +message.thing.info.spec.property.value=\u5C5E\u6027\u503C +message.thing.info.spec.property.state=\u5C5E\u6027\u72B6\u6001 +message.thing.info.spec.property.timestamp=\u65F6\u95F4\u6233 +message.thing.info.spec.tag.value=\u6807\u7B7E\u503C +message.thing.info.spec.tag.timestamp=\u65F6\u95F4\u6233 +message.thing.info.spec.event.data=\u4E8B\u4EF6\u6570\u636E +message.thing.info.spec.event.timestamp=\u65F6\u95F4\u6233 +message.thing.info.spec.configs=\u57FA\u672C\u4FE1\u606F +message.thing.info.spec.properties=\u7269\u6A21\u578B\u5C5E\u6027 +message.thing.info.spec.tags=\u6807\u7B7E\u4FE1\u606F +message.thing.info.spec.events=\u4E8B\u4EF6\u4FE1\u606F +message.thing.info.spec.config.state=\u5728\u7EBF\u72B6\u6001 +message.thing.info.spec.config.state-online=\u5728\u7EBF +message.thing.info.spec.config.state-offline=\u79BB\u7EBF +message.thing.info.spec.config.onlineTime=\u4E0A\u4E00\u6B21\u5728\u7EBF\u65F6\u95F4 +message.thing.info.spec.config.offlineTime=\u4E0A\u4E00\u6B21\u79BB\u7EBF\u65F6\u95F4 +message.thing.info.spec.config.deviceName=\u8BBE\u5907\u540D\u79F0 +message.thing.info.spec.config.productName=\u4EA7\u54C1\u540D\u79F0 \ No newline at end of file diff --git a/jetlinks-standalone/src/main/resources/application.yml b/jetlinks-standalone/src/main/resources/application.yml index ff54e0f1..d100ca2f 100644 --- a/jetlinks-standalone/src/main/resources/application.yml +++ b/jetlinks-standalone/src/main/resources/application.yml @@ -24,7 +24,7 @@ spring: pool: max-active: 1024 timeout: 20s - # database: 3 + # database: 3 # max-wait: 10s r2dbc: # 需要手动创建数据库,启动会自动创建表,修改了配置easyorm相关配置也要修改 diff --git a/pom.xml b/pom.xml index 11a36f06..7c9bb263 100644 --- a/pom.xml +++ b/pom.xml @@ -22,31 +22,32 @@ ${java.version} - 4.0.18-SNAPSHOT + 4.0.18 - 4.1.3-SNAPSHOT + 4.1.3 - 1.2.3-SNAPSHOT + 1.2.4-SNAPSHOT 1.0.6 - 1.0.18-SNAPSHOT + 1.0.18 - 1.0.2 + 1.0.3 - 1.0.0 + 1.0.1 Borca-SR2 3.0.2 - 4.1.104.Final + 4.1.111.Final 7.17.23 3.7.0 1.2.83 - 2020.0.38 - 4.3.8 + 2020.0.46 + 4.5.10 2.18.0 - 1.2.11 + 1.7.36 + 1.2.13 1.6.11 2.14.3 2.9.1 @@ -55,10 +56,9 @@ 5.12.1 4.5.2 2.4.1 - 2021.0.3 2.57 - 1.53.0 - 1.1.4 + 1.70.0 + 1.1.5 1.11.8 @@ -672,6 +672,7 @@ org.slf4j slf4j-api + ${slf4j.version}