zhouhao 5 rokov pred
rodič
commit
31af39258d
71 zmenil súbory, kde vykonal 3394 pridanie a 88 odobranie
  1. 41 0
      jetlinks-components/notify-component/notify-core/pom.xml
  2. 26 0
      jetlinks-components/notify-component/notify-core/src/main/java/org/jetlinks/community/notify/AbstractNotifier.java
  3. 71 0
      jetlinks-components/notify-component/notify-core/src/main/java/org/jetlinks/community/notify/DefaultNotifierManager.java
  4. 24 0
      jetlinks-components/notify-component/notify-core/src/main/java/org/jetlinks/community/notify/DefaultNotifyType.java
  5. 76 0
      jetlinks-components/notify-component/notify-core/src/main/java/org/jetlinks/community/notify/Notifier.java
  6. 39 0
      jetlinks-components/notify-component/notify-core/src/main/java/org/jetlinks/community/notify/NotifierManager.java
  7. 59 0
      jetlinks-components/notify-component/notify-core/src/main/java/org/jetlinks/community/notify/NotifierProperties.java
  8. 53 0
      jetlinks-components/notify-component/notify-core/src/main/java/org/jetlinks/community/notify/NotifierProvider.java
  9. 28 0
      jetlinks-components/notify-component/notify-core/src/main/java/org/jetlinks/community/notify/NotifyConfigManager.java
  10. 15 0
      jetlinks-components/notify-component/notify-core/src/main/java/org/jetlinks/community/notify/NotifyType.java
  11. 20 0
      jetlinks-components/notify-component/notify-core/src/main/java/org/jetlinks/community/notify/Provider.java
  12. 48 0
      jetlinks-components/notify-component/notify-core/src/main/java/org/jetlinks/community/notify/rule/NotifierRuleNode.java
  13. 37 0
      jetlinks-components/notify-component/notify-core/src/main/java/org/jetlinks/community/notify/rule/RuleNotifierProperties.java
  14. 50 0
      jetlinks-components/notify-component/notify-core/src/main/java/org/jetlinks/community/notify/template/AbstractTemplateManager.java
  15. 15 0
      jetlinks-components/notify-component/notify-core/src/main/java/org/jetlinks/community/notify/template/Template.java
  16. 57 0
      jetlinks-components/notify-component/notify-core/src/main/java/org/jetlinks/community/notify/template/TemplateManager.java
  17. 19 0
      jetlinks-components/notify-component/notify-core/src/main/java/org/jetlinks/community/notify/template/TemplateProperties.java
  18. 19 0
      jetlinks-components/notify-component/notify-core/src/main/java/org/jetlinks/community/notify/template/TemplateProvider.java
  19. 32 0
      jetlinks-components/notify-component/notify-dingtalk/pom.xml
  20. 86 0
      jetlinks-components/notify-component/notify-dingtalk/src/main/java/org/jetlinks/community/notify/dingtalk/DingTalkMessageTemplate.java
  21. 120 0
      jetlinks-components/notify-component/notify-dingtalk/src/main/java/org/jetlinks/community/notify/dingtalk/DingTalkNotifier.java
  22. 80 0
      jetlinks-components/notify-component/notify-dingtalk/src/main/java/org/jetlinks/community/notify/dingtalk/DingTalkNotifierProvider.java
  23. 19 0
      jetlinks-components/notify-component/notify-dingtalk/src/main/java/org/jetlinks/community/notify/dingtalk/DingTalkProperties.java
  24. 20 0
      jetlinks-components/notify-component/notify-dingtalk/src/main/java/org/jetlinks/community/notify/dingtalk/DingTalkProvider.java
  25. 39 0
      jetlinks-components/notify-component/notify-dingtalk/src/test/java/org/jetlinks/community/notify/dingtalk/DingTalkNotifierTest.java
  26. 44 0
      jetlinks-components/notify-component/notify-email/pom.xml
  27. 20 0
      jetlinks-components/notify-component/notify-email/src/main/java/org/jetlinks/community/notify/email/EmailProvider.java
  28. 198 0
      jetlinks-components/notify-component/notify-email/src/main/java/org/jetlinks/community/notify/email/embedded/DefaultEmailNotifier.java
  29. 122 0
      jetlinks-components/notify-component/notify-email/src/main/java/org/jetlinks/community/notify/email/embedded/DefaultEmailNotifierProvider.java
  30. 49 0
      jetlinks-components/notify-component/notify-email/src/main/java/org/jetlinks/community/notify/email/embedded/DefaultEmailProperties.java
  31. 30 0
      jetlinks-components/notify-component/notify-email/src/main/java/org/jetlinks/community/notify/email/embedded/EmailTemplate.java
  32. 29 0
      jetlinks-components/notify-component/notify-email/src/main/java/org/jetlinks/community/notify/email/embedded/ParsedEmailTemplate.java
  33. 41 0
      jetlinks-components/notify-component/notify-sms/pom.xml
  34. 168 0
      jetlinks-components/notify-component/notify-sms/src/main/java/org/jetlinks/community/notify/sms/provider/Hy2046SmsSenderProvider.java
  35. 40 0
      jetlinks-components/notify-component/notify-sms/src/main/java/org/jetlinks/community/notify/sms/provider/PlainTextSmsTemplate.java
  36. 81 0
      jetlinks-components/notify-component/notify-sms/src/main/java/org/jetlinks/community/notify/sms/provider/TestSmsProvider.java
  37. 46 0
      jetlinks-components/notify-component/notify-voice/README.md
  38. 27 0
      jetlinks-components/notify-component/notify-voice/pom.xml
  39. 19 0
      jetlinks-components/notify-component/notify-voice/src/main/java/org/jetlinks/community/notify/voice/VoiceNotifierConfiguration.java
  40. 20 0
      jetlinks-components/notify-component/notify-voice/src/main/java/org/jetlinks/community/notify/voice/VoiceProvider.java
  41. 79 0
      jetlinks-components/notify-component/notify-voice/src/main/java/org/jetlinks/community/notify/voice/aliyun/AliyunNotifierProvider.java
  42. 109 0
      jetlinks-components/notify-component/notify-voice/src/main/java/org/jetlinks/community/notify/voice/aliyun/AliyunVoiceNotifier.java
  43. 37 0
      jetlinks-components/notify-component/notify-voice/src/main/java/org/jetlinks/community/notify/voice/aliyun/AliyunVoiceTemplate.java
  44. 67 0
      jetlinks-components/notify-component/notify-voice/src/test/java/org/jetlinks/community/notify/voice/supports/aliyun/AliyunVoiceNotifierTest.java
  45. 28 0
      jetlinks-components/notify-component/notify-wechat/pom.xml
  46. 19 0
      jetlinks-components/notify-component/notify-wechat/src/main/java/org/jetlinks/community/notify/wechat/WechatCorpProperties.java
  47. 102 0
      jetlinks-components/notify-component/notify-wechat/src/main/java/org/jetlinks/community/notify/wechat/WechatMessageTemplate.java
  48. 77 0
      jetlinks-components/notify-component/notify-wechat/src/main/java/org/jetlinks/community/notify/wechat/WechatNotifierProvider.java
  49. 20 0
      jetlinks-components/notify-component/notify-wechat/src/main/java/org/jetlinks/community/notify/wechat/WechatProvider.java
  50. 120 0
      jetlinks-components/notify-component/notify-wechat/src/main/java/org/jetlinks/community/notify/wechat/WeixinCorpNotifier.java
  51. 38 0
      jetlinks-components/notify-component/notify-wechat/src/test/java/org/jetlinks/community/notify/wechat/WeixinCorpNotifierTest.java
  52. 25 0
      jetlinks-components/notify-component/pom.xml
  53. 1 0
      jetlinks-components/pom.xml
  54. 0 27
      jetlinks-manager/authentication-manager/.gitignore
  55. 0 27
      jetlinks-manager/device-manager/.gitignore
  56. 4 5
      jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/measurements/DevicePropertyMeasurement.java
  57. 0 29
      jetlinks-manager/network-manager/.gitignore
  58. 1 0
      jetlinks-manager/notify-manager/README.md
  59. 125 0
      jetlinks-manager/notify-manager/pom.xml
  60. 64 0
      jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/entity/NotifyConfigEntity.java
  61. 51 0
      jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/entity/NotifyTemplateEntity.java
  62. 25 0
      jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/service/DefaultNotifyConfigManager.java
  63. 35 0
      jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/service/DefaultTemplateManager.java
  64. 65 0
      jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/service/NotifierCacheManager.java
  65. 14 0
      jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/service/NotifyConfigService.java
  66. 17 0
      jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/service/NotifyTemplateService.java
  67. 112 0
      jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/web/NotifierConfigController.java
  68. 69 0
      jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/web/NotifierController.java
  69. 56 0
      jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/web/NotifierTemplateController.java
  70. 1 0
      jetlinks-manager/pom.xml
  71. 6 0
      jetlinks-standalone/pom.xml

+ 41 - 0
jetlinks-components/notify-component/notify-core/pom.xml

@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <artifactId>notify-component</artifactId>
+        <groupId>org.jetlinks.community</groupId>
+        <version>1.0-SNAPSHOT</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>notify-core</artifactId>
+    <dependencies>
+        <dependency>
+            <groupId>org.hswebframework</groupId>
+            <artifactId>hsweb-easy-orm-rdb</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.jetlinks</groupId>
+            <artifactId>rule-engine-support</artifactId>
+            <version>${jetlinks.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.hswebframework.web</groupId>
+            <artifactId>hsweb-starter</artifactId>
+            <version>${hsweb.framework.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.jetlinks</groupId>
+            <artifactId>jetlinks-core</artifactId>
+            <version>${jetlinks.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>${project.groupId}</groupId>
+            <artifactId>common-component</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+
+    </dependencies>
+
+</project>

+ 26 - 0
jetlinks-components/notify-component/notify-core/src/main/java/org/jetlinks/community/notify/AbstractNotifier.java

@@ -0,0 +1,26 @@
+package org.jetlinks.community.notify;
+
+import lombok.AllArgsConstructor;
+import org.jetlinks.core.Values;
+import org.jetlinks.community.notify.template.Template;
+import org.jetlinks.community.notify.template.TemplateManager;
+import reactor.core.publisher.Mono;
+
+import javax.annotation.Nonnull;
+
+@AllArgsConstructor
+public abstract class AbstractNotifier<T extends Template> implements Notifier<T> {
+
+    private TemplateManager templateManager;
+
+    @Override
+    @Nonnull
+    public Mono<Void> send(@Nonnull String templateId, @Nonnull Values context) {
+        return templateManager
+                .getTemplate(getType(), templateId)
+                .switchIfEmpty(Mono.error(new UnsupportedOperationException("模版不存在:" + templateId)))
+                .flatMap(tem -> send((T) tem, context));
+    }
+
+
+}

+ 71 - 0
jetlinks-components/notify-component/notify-core/src/main/java/org/jetlinks/community/notify/DefaultNotifierManager.java

@@ -0,0 +1,71 @@
+package org.jetlinks.community.notify;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.BeansException;
+import org.springframework.beans.factory.config.BeanPostProcessor;
+import org.springframework.stereotype.Component;
+import reactor.core.publisher.Mono;
+
+import javax.annotation.Nonnull;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+@Slf4j
+@Component
+@SuppressWarnings("all")
+public class DefaultNotifierManager implements NotifierManager, BeanPostProcessor {
+
+    private final Map<String, Map<String, NotifierProvider>> providers = new ConcurrentHashMap<>();
+
+    private Map<String, Notifier> notifiers = new ConcurrentHashMap<>();
+
+    private NotifyConfigManager configManager;
+
+    public DefaultNotifierManager(NotifyConfigManager manager) {
+        this.configManager = manager;
+    }
+
+    protected Mono<NotifierProperties> getProperties(NotifyType notifyType,
+                                                     String id) {
+        return configManager.getNotifyConfig(notifyType, id);
+    }
+
+    public Mono<Void> reload(String id) {
+        // TODO: 2019/12/20 集群支持
+        return Mono.justOrEmpty(notifiers.remove(id))
+            .flatMap(Notifier::close);
+    }
+
+    protected Mono<Notifier> createNotifier(NotifierProperties properties) {
+        return Mono.justOrEmpty(providers.get(properties.getType()))
+            .switchIfEmpty(Mono.error(new UnsupportedOperationException("不支持的通知类型:" + properties.getType())))
+            .flatMap(map -> Mono.justOrEmpty(map.get(properties.getProvider())))
+            .switchIfEmpty(Mono.error(new UnsupportedOperationException("不支持的服务商:" + properties.getProvider())))
+            .flatMap(notifierProvider -> notifierProvider.createNotifier(properties))
+            .flatMap(notifier -> Mono.justOrEmpty(notifiers.put(properties.getId(), notifier))
+                .flatMap(Notifier::close)//如果存在旧的通知器则关掉之
+                .onErrorContinue((err, obj) -> log.error(err.getMessage(), err))//忽略异常
+                .thenReturn(notifier));
+    }
+
+    @Override
+    @Nonnull
+    public Mono<Notifier> getNotifier(@Nonnull NotifyType type,
+                                      @Nonnull String id) {
+        return Mono.justOrEmpty(notifiers.get(id))
+            .switchIfEmpty(Mono.defer(() -> getProperties(type, id)).flatMap(this::createNotifier));
+    }
+
+    protected void registerProvider(NotifierProvider provider) {
+        providers.computeIfAbsent(provider.getType().getId(), ignore -> new ConcurrentHashMap<>())
+            .put(provider.getProvider().getId(), provider);
+    }
+
+    @Override
+    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
+        if (bean instanceof NotifierProvider) {
+            registerProvider(((NotifierProvider) bean));
+        }
+        return bean;
+    }
+}

+ 24 - 0
jetlinks-components/notify-component/notify-core/src/main/java/org/jetlinks/community/notify/DefaultNotifyType.java

@@ -0,0 +1,24 @@
+package org.jetlinks.community.notify;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+@Getter
+@AllArgsConstructor
+public enum DefaultNotifyType implements NotifyType {
+
+    sms("短信"),
+    email("邮件"),
+    voice("语音"),
+    dingTalk("钉钉"),
+    weixin("微信"),
+
+    ;
+
+    private String name;
+
+    @Override
+    public String getId() {
+        return name();
+    }
+}

+ 76 - 0
jetlinks-components/notify-component/notify-core/src/main/java/org/jetlinks/community/notify/Notifier.java

@@ -0,0 +1,76 @@
+package org.jetlinks.community.notify;
+
+import org.jetlinks.core.Values;
+import org.jetlinks.community.notify.template.Template;
+import reactor.core.publisher.Mono;
+
+import javax.annotation.Nonnull;
+import java.util.function.Consumer;
+
+/**
+ * 通知器,用于发送通知,如: 短信,邮件,语音,微信等s
+ *
+ * @author zhouhao
+ * @see NotifierManager
+ * @see NotifierProvider
+ * @since 1.0
+ */
+public interface Notifier<T extends Template> {
+
+    /**
+     * 获取通知类型,如: 语音通知
+     *
+     * @return 通知类型
+     * @see DefaultNotifyType
+     */
+    @Nonnull
+    NotifyType getType();
+
+    /**
+     * 获取通知服务提供商,如: aliyun 等
+     *
+     * @return 通知服务提供商
+     */
+    @Nonnull
+    Provider getProvider();
+
+    /**
+     * 指定模版ID进行发送.
+     * 发送失败或者模版不存在将返回{@link Mono#error(Throwable)}.
+     *
+     * @param templateId 模版ID
+     * @param context    上下文
+     * @return 异步发送结果
+     * @see Mono#doOnError(Consumer)
+     * @see Mono#doOnSuccess(Consumer)
+     * @see Template
+     */
+    @Nonnull
+    Mono<Void> send(@Nonnull String templateId, Values context);
+
+    /**
+     * 指定模版{@link Template}并发送.
+     * <p>
+     * 注意:不同等服务商使用的模版实现不同.
+     * <p>
+     * 发送失败返回{@link Mono#error(Throwable)}.
+     *
+     * @param template 模版
+     * @param context  上下文
+     * @return 异步发送结果
+     * @see Mono#doOnError(Consumer)
+     * @see Mono#doOnSuccess(Consumer)
+     * @see Template
+     */
+    @Nonnull
+    Mono<Void> send(@Nonnull T template, @Nonnull Values context);
+
+    /**
+     * 关闭通知器,以释放相关资源
+     *
+     * @return 关闭结果
+     */
+    @Nonnull
+    Mono<Void> close();
+
+}

+ 39 - 0
jetlinks-components/notify-component/notify-core/src/main/java/org/jetlinks/community/notify/NotifierManager.java

@@ -0,0 +1,39 @@
+package org.jetlinks.community.notify;
+
+import org.jetlinks.community.notify.template.Template;
+import reactor.core.publisher.Mono;
+
+import javax.annotation.Nonnull;
+
+/**
+ * 通知管理器,用于获取获取通知器.
+ *
+ * @author zhouhao
+ * @see 1.0
+ * @see Notifier
+ * @see DefaultNotifyType
+ * @see Template
+ * @see NotifyConfigManager
+ */
+public interface NotifierManager {
+
+    /**
+     * 获取通知器
+     *
+     * @param type 通知类型 {@link DefaultNotifyType}
+     * @param id   唯一标识
+     * @param <T>  模版类型
+     * @return 异步获取结果
+     * @see NotifierProvider
+     */
+    @Nonnull
+    <T extends Template> Mono<Notifier<T>> getNotifier(@Nonnull NotifyType type, @Nonnull String id);
+
+    /**
+     * 重新加载通知管理器
+     *
+     * @param id 通知管理器ID
+     * @return 加载结果
+     */
+    Mono<Void> reload(String id);
+}

+ 59 - 0
jetlinks-components/notify-component/notify-core/src/main/java/org/jetlinks/community/notify/NotifierProperties.java

@@ -0,0 +1,59 @@
+package org.jetlinks.community.notify;
+
+import lombok.Getter;
+import lombok.Setter;
+
+import java.io.Serializable;
+import java.util.Map;
+import java.util.Optional;
+
+/**
+ * 通知配置属性
+ *
+ * @author zhouhao
+ * @see NotifyConfigManager
+ * @since 1.0
+ */
+@Getter
+@Setter
+public class NotifierProperties implements Serializable {
+
+    private static final long serialVersionUID = -6849794470754667710L;
+
+    /**
+     * 配置全局唯一标识
+     */
+    private String id;
+
+    /**
+     * 通知类型标识
+     * @see NotifyType
+     */
+    private String type;
+
+    /**
+     * 通知服务提供商标识,如: aliyun ...
+     */
+    private String provider;
+
+    /**
+     * 配置名称
+     */
+    private String name;
+
+    /**
+     * 配置内容,不同的服务提供商,配置不同.
+     * @see NotifierProvider
+     */
+    private Map<String, Object> configuration;
+
+    public Optional<Object> getConfig(String key){
+        return Optional.ofNullable(configuration)
+                .map(conf->conf.get(key));
+    }
+    public Object getConfigOrNull(String key){
+        return Optional.ofNullable(configuration)
+                .map(conf->conf.get(key))
+                .orElse(null);
+    }
+}

+ 53 - 0
jetlinks-components/notify-component/notify-core/src/main/java/org/jetlinks/community/notify/NotifierProvider.java

@@ -0,0 +1,53 @@
+package org.jetlinks.community.notify;
+
+import org.jetlinks.core.metadata.ConfigMetadata;
+import org.jetlinks.community.notify.template.Template;
+import reactor.core.publisher.Mono;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+/**
+ * 通知服务提供商
+ *
+ * @author zhouhao
+ * @see org.jetlinks.community.notify.template.TemplateProvider
+ * @see NotifierManager
+ * @since 1.0
+ */
+public interface NotifierProvider {
+
+    /**
+     * 获取通知类型
+     *
+     * @return 通知类型
+     * @see DefaultNotifyType
+     */
+    @Nonnull
+    NotifyType getType();
+
+    /**
+     * @return 服务商
+     */
+    @Nonnull
+    Provider getProvider();
+
+    /**
+     * 根据配置创建通知器
+     *
+     * @param properties 通知配置
+     * @return 创建结果
+     */
+    @Nonnull
+    Mono<? extends Notifier<? extends Template>> createNotifier(@Nonnull NotifierProperties properties);
+
+    /**
+     * 获取通知配置元数据,通过元数据可以知道此通知所需要的配置信息
+     *
+     * @return 配置元数据
+     */
+    @Nullable
+    default ConfigMetadata getNotifierConfigMetadata() {
+        return null;
+    }
+}

+ 28 - 0
jetlinks-components/notify-component/notify-core/src/main/java/org/jetlinks/community/notify/NotifyConfigManager.java

@@ -0,0 +1,28 @@
+package org.jetlinks.community.notify;
+
+import reactor.core.publisher.Mono;
+
+import javax.annotation.Nonnull;
+
+/**
+ * 通知配置管理器,用于统一管理通知配置
+ *
+ * @author zhouhao
+ * @version 1.0
+ * @since 1.0
+ */
+public interface NotifyConfigManager {
+
+    /**
+     * 根据类型和配置ID获取通知器配置
+     * <p>
+     * 如果配置不存在则返回{@link Mono#empty()},可通过{@link Mono#switchIfEmpty(Mono)}进行处理
+     *
+     * @param notifyType 类型 {@link DefaultNotifyType}
+     * @param configId   配置ID
+     * @return 配置
+     */
+    @Nonnull
+    Mono<NotifierProperties> getNotifyConfig(@Nonnull NotifyType notifyType, @Nonnull String configId);
+
+}

+ 15 - 0
jetlinks-components/notify-component/notify-core/src/main/java/org/jetlinks/community/notify/NotifyType.java

@@ -0,0 +1,15 @@
+package org.jetlinks.community.notify;
+
+/**
+ * 通知类型.通常使用枚举实现此接口
+ *
+ * @author zhouhao
+ * @see DefaultNotifyType
+ * @since 1.0
+ */
+public interface NotifyType {
+
+    String getId();
+
+    String getName();
+}

+ 20 - 0
jetlinks-components/notify-component/notify-core/src/main/java/org/jetlinks/community/notify/Provider.java

@@ -0,0 +1,20 @@
+package org.jetlinks.community.notify;
+
+/**
+ * 服务商标识,通常使用枚举实现此接口
+ *
+ * @author zhouhao
+ * @since 1.0
+ */
+public interface Provider {
+    /**
+     * @return 唯一标识
+     */
+    String getId();
+
+    /**
+     * @return 名称
+     */
+    String getName();
+
+}

+ 48 - 0
jetlinks-components/notify-component/notify-core/src/main/java/org/jetlinks/community/notify/rule/NotifierRuleNode.java

@@ -0,0 +1,48 @@
+package org.jetlinks.community.notify.rule;
+
+import lombok.AllArgsConstructor;
+import org.jetlinks.core.Values;
+import org.jetlinks.community.notify.NotifierManager;
+import org.jetlinks.rule.engine.api.RuleData;
+import org.jetlinks.rule.engine.api.RuleDataHelper;
+import org.jetlinks.rule.engine.api.executor.ExecutionContext;
+import org.jetlinks.rule.engine.executor.CommonExecutableRuleNodeFactoryStrategy;
+import org.reactivestreams.Publisher;
+import org.springframework.stereotype.Component;
+import reactor.core.publisher.Mono;
+
+import java.util.function.Function;
+
+@Component
+@AllArgsConstructor
+public class NotifierRuleNode extends CommonExecutableRuleNodeFactoryStrategy<RuleNotifierProperties> {
+
+    private NotifierManager notifierManager;
+
+    @Override
+    public String getSupportType() {
+        return "notifier";
+    }
+
+    @Override
+    public Function<RuleData, ? extends Publisher<?>> createExecutor(ExecutionContext context, RuleNotifierProperties config) {
+        return rule -> notifierManager
+                .getNotifier(config.getNotifyType(), config.getNotifierId())
+                .switchIfEmpty(Mono.fromRunnable(() -> {
+                    context.logger().warn("通知器[{}-{}]不存在", config.getNodeType(), config.getNotifierId());
+                }))
+                .flatMap(notifier -> notifier.send(config.getTemplateId(), Values.of(RuleDataHelper.toContextMap(rule))))
+                .doOnError(err -> {
+                    context.logger().error("发送[{}]通知[{}-{}]失败",
+                            config.getNotifyType().getName(),
+                            config.getNotifierId(),
+                            config.getTemplateId(), err);
+                })
+                .doOnSuccess(ignore -> {
+                    context.logger().info("发送[{}]通知[{}-{}]完成",
+                            config.getNotifyType().getName(),
+                            config.getNotifierId(),
+                            config.getTemplateId());
+                });
+    }
+}

+ 37 - 0
jetlinks-components/notify-component/notify-core/src/main/java/org/jetlinks/community/notify/rule/RuleNotifierProperties.java

@@ -0,0 +1,37 @@
+package org.jetlinks.community.notify.rule;
+
+import lombok.Getter;
+import lombok.Setter;
+import org.jetlinks.community.notify.DefaultNotifyType;
+import org.jetlinks.rule.engine.api.model.NodeType;
+import org.jetlinks.rule.engine.executor.node.RuleNodeConfig;
+import org.springframework.util.Assert;
+
+@Getter
+@Setter
+public class RuleNotifierProperties implements RuleNodeConfig {
+
+    private DefaultNotifyType notifyType;
+
+    private String notifierId;
+
+    private String templateId;
+
+    @Override
+    public NodeType getNodeType() {
+        return NodeType.PEEK;
+    }
+
+    @Override
+    public void setNodeType(NodeType nodeType) {
+
+    }
+
+    @Override
+    public void validate() {
+        Assert.notNull(notifyType,"notifyType can not be null");
+        Assert.hasText(notifierId,"notifierId can not be empty");
+        Assert.hasText(templateId,"templateId can not be empty");
+
+    }
+}

+ 50 - 0
jetlinks-components/notify-component/notify-core/src/main/java/org/jetlinks/community/notify/template/AbstractTemplateManager.java

@@ -0,0 +1,50 @@
+package org.jetlinks.community.notify.template;
+
+import org.jetlinks.community.notify.NotifyType;
+import reactor.core.publisher.Mono;
+
+import javax.annotation.Nonnull;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+public abstract class AbstractTemplateManager implements TemplateManager {
+
+    protected Map<String, Map<String, TemplateProvider>> providers = new ConcurrentHashMap<>();
+
+    protected Map<String, Template> templates = new ConcurrentHashMap<>();
+
+    protected abstract Mono<TemplateProperties> getProperties(NotifyType type, String id);
+
+    protected void register(TemplateProvider provider) {
+        providers.computeIfAbsent(provider.getType().getId(), ignore -> new ConcurrentHashMap<>())
+                .put(provider.getProvider().getId(), provider);
+    }
+
+    @Override
+    @Nonnull
+    public Mono<? extends Template> createTemplate(@Nonnull NotifyType type,@Nonnull TemplateProperties prop) {
+        return Mono.justOrEmpty(providers.get(type.getId()))
+                .switchIfEmpty(Mono.error(() -> new UnsupportedOperationException("不支持的通知类型:" + prop.getType())))
+                .flatMap(map -> Mono.justOrEmpty(map.get(prop.getProvider()))
+                        .switchIfEmpty(Mono.error(() -> new UnsupportedOperationException("不支持的服务商:" + prop.getProvider())))
+                        .flatMap(provider -> provider.createTemplate(prop)));
+    }
+
+    @Nonnull
+    @Override
+    public Mono<? extends Template> getTemplate(@Nonnull NotifyType type, @Nonnull String id) {
+        return Mono.justOrEmpty(templates.get(id))
+                .switchIfEmpty(Mono.defer(() ->
+                        getProperties(type, id)
+                                .flatMap(prop -> this.createTemplate(type, prop))
+                                .switchIfEmpty(Mono.error(() -> new UnsupportedOperationException("通知类型不支持:" + type.getId())))
+                ));
+    }
+
+    @Override
+    @Nonnull
+    public Mono<Void> reload(String templateId) {
+        // TODO: 2019/12/20 集群支持
+        return Mono.fromRunnable(() -> templates.remove(templateId));
+    }
+}

+ 15 - 0
jetlinks-components/notify-component/notify-core/src/main/java/org/jetlinks/community/notify/template/Template.java

@@ -0,0 +1,15 @@
+package org.jetlinks.community.notify.template;
+
+
+import java.io.Serializable;
+
+/**
+ * 通知模版,不同的服务商{@link org.jetlinks.community.notify.NotifierProvider},{@link TemplateProvider}需要实现不同的模版
+ *
+ * @author zhouhao
+ * @version 1.0
+ * @since 1.0
+ */
+public interface Template extends Serializable {
+
+}

+ 57 - 0
jetlinks-components/notify-component/notify-core/src/main/java/org/jetlinks/community/notify/template/TemplateManager.java

@@ -0,0 +1,57 @@
+package org.jetlinks.community.notify.template;
+
+import org.jetlinks.community.notify.NotifyType;
+import reactor.core.publisher.Mono;
+
+import javax.annotation.Nonnull;
+
+/**
+ * 模版管理器,用于统一管理通知模版. 模版的转换由不同的通知服务商{@link TemplateProvider}去实现.
+ *
+ * @author zhouhao
+ * @see Template
+ * @see TemplateProvider
+ * @since 1.0
+ */
+public interface TemplateManager {
+
+    /**
+     * 根据通知类型和模版ID获取模版
+     * <p>
+     * 如果模版不存在将返回{@link Mono#empty()},可通过{@link Mono#switchIfEmpty(Mono)}进行处理.
+     * <p>
+     * 如果通知类型或者通知服务商不支持,将会返回{@link Mono#error(Throwable)}. {@link UnsupportedOperationException}
+     * <p>
+     * 请根据不同的通知类型处理对应的模版.
+     *
+     * @param type 通知类型
+     * @param id   模版ID
+     * @return 模版
+     */
+    @Nonnull
+    Mono<? extends Template> getTemplate(@Nonnull NotifyType type, @Nonnull String id);
+
+    /**
+     * 根据通知类型和配置对象创建一个模版
+     * <p>
+     * 如果通知类型或者通知服务商不支持,将会返回{@link Mono#error(Throwable)}. {@link UnsupportedOperationException}
+     * <p>
+     * 请根据不同的通知类型处理对应的模版.
+     *
+     * @param type       通知类型
+     * @param properties 模版配置
+     * @return 模版
+     * @see TemplateProvider
+     */
+    @Nonnull
+    Mono<? extends Template> createTemplate(@Nonnull NotifyType type, @Nonnull TemplateProperties properties);
+
+    /**
+     * 重新加载模版
+     *
+     * @param templateId 模版ID
+     * @return 异步结果
+     */
+    @Nonnull
+    Mono<Void> reload(String templateId);
+}

+ 19 - 0
jetlinks-components/notify-component/notify-core/src/main/java/org/jetlinks/community/notify/template/TemplateProperties.java

@@ -0,0 +1,19 @@
+package org.jetlinks.community.notify.template;
+
+import lombok.Getter;
+import lombok.Setter;
+
+import java.io.Serializable;
+import java.util.Map;
+
+@Getter
+@Setter
+public class TemplateProperties implements Serializable {
+    private static final long serialVersionUID = -6849794470754667710L;
+
+    private String type;
+
+    private String provider;
+
+    private String template;
+}

+ 19 - 0
jetlinks-components/notify-component/notify-core/src/main/java/org/jetlinks/community/notify/template/TemplateProvider.java

@@ -0,0 +1,19 @@
+package org.jetlinks.community.notify.template;
+
+import org.jetlinks.core.metadata.ConfigMetadata;
+import org.jetlinks.community.notify.NotifyType;
+import org.jetlinks.community.notify.Provider;
+import reactor.core.publisher.Mono;
+
+public interface TemplateProvider {
+
+    NotifyType getType();
+
+    Provider getProvider();
+
+    Mono<? extends Template> createTemplate(TemplateProperties properties);
+
+    default ConfigMetadata getTemplateConfigMetadata() {
+        return null;
+    }
+}

+ 32 - 0
jetlinks-components/notify-component/notify-dingtalk/pom.xml

@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <artifactId>notify-component</artifactId>
+        <groupId>org.jetlinks.community</groupId>
+        <version>1.0-SNAPSHOT</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>notify-dingtalk</artifactId>
+
+
+    <dependencies>
+
+        <dependency>
+            <groupId>org.jetlinks.community</groupId>
+            <artifactId>notify-core</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-webflux</artifactId>
+        </dependency>
+
+
+
+    </dependencies>
+
+</project>

+ 86 - 0
jetlinks-components/notify-component/notify-dingtalk/src/main/java/org/jetlinks/community/notify/dingtalk/DingTalkMessageTemplate.java

@@ -0,0 +1,86 @@
+package org.jetlinks.community.notify.dingtalk;
+
+import com.alibaba.fastjson.JSONObject;
+import lombok.Getter;
+import lombok.Setter;
+import lombok.SneakyThrows;
+import org.hswebframework.web.utils.ExpressionUtils;
+import org.jetlinks.core.Values;
+import org.jetlinks.community.notify.template.Template;
+import org.springframework.util.StringUtils;
+import org.springframework.web.reactive.function.BodyInserters;
+import org.springframework.web.util.UriComponentsBuilder;
+
+import javax.validation.constraints.NotBlank;
+import java.util.Collections;
+
+@Getter
+@Setter
+public class DingTalkMessageTemplate implements Template {
+
+    /**
+     * 应用ID
+     */
+    @NotBlank(message = "[agentId]不能为空")
+    private String agentId;
+
+    private String userIdList;
+
+    private String departmentIdList;
+
+    private boolean toAllUser;
+
+    @NotBlank(message = "[message]不能为空")
+    private String message;
+
+
+    @SneakyThrows
+    public BodyInserters.FormInserter<String> createFormInserter(BodyInserters.FormInserter<String> inserter, Values context) {
+        inserter.with("agent_id", this.getAgentId())
+                .with("to_all_user", String.valueOf(toAllUser))
+                .with("msg",this.createMessage(context));
+        if (StringUtils.hasText(userIdList)) {
+            inserter.with("userid_list", this.createUserIdList(context));
+        }
+        if (StringUtils.hasText(departmentIdList)) {
+            inserter.with("dept_id_list", this.createDepartmentIdList(context));
+        }
+        return inserter;
+
+    }
+
+    public UriComponentsBuilder createUriParameter(UriComponentsBuilder builder, Values context){
+        builder.queryParam("agent_id", this.getAgentId())
+                .queryParam("to_all_user", String.valueOf(toAllUser))
+                .queryParam("msg",this.createMessage(context));
+        if (StringUtils.hasText(userIdList)) {
+            builder.queryParam("userid_list", this.createUserIdList(context));
+        }
+        if (StringUtils.hasText(departmentIdList)) {
+            builder.queryParam("dept_id_list", this.createDepartmentIdList(context));
+        }
+        return builder;
+    }
+
+    public String createUserIdList(Values context) {
+        if (StringUtils.isEmpty(userIdList)) {
+            return userIdList;
+        }
+        return ExpressionUtils.analytical(userIdList, context.getAllValues(), "spel");
+    }
+
+    public String createDepartmentIdList(Values context) {
+        if (StringUtils.isEmpty(departmentIdList)) {
+            return departmentIdList;
+        }
+        return ExpressionUtils.analytical(departmentIdList, context.getAllValues(), "spel");
+    }
+
+    public String createMessage(Values context) {
+        JSONObject json = new JSONObject();
+        json.put("msgtype", "text");
+        json.put("text", Collections.singletonMap("content",ExpressionUtils.analytical(message, context.getAllValues(), "spel")));
+        return json.toJSONString();
+    }
+
+}

+ 120 - 0
jetlinks-components/notify-component/notify-dingtalk/src/main/java/org/jetlinks/community/notify/dingtalk/DingTalkNotifier.java

@@ -0,0 +1,120 @@
+package org.jetlinks.community.notify.dingtalk;
+
+import lombok.extern.slf4j.Slf4j;
+import org.hswebframework.web.exception.BusinessException;
+import org.jetlinks.core.Values;
+import org.jetlinks.community.notify.AbstractNotifier;
+import org.jetlinks.community.notify.DefaultNotifyType;
+import org.jetlinks.community.notify.NotifyType;
+import org.jetlinks.community.notify.Provider;
+import org.jetlinks.community.notify.template.TemplateManager;
+import org.springframework.http.MediaType;
+import org.springframework.web.reactive.function.BodyInserters;
+import org.springframework.web.reactive.function.client.WebClient;
+import org.springframework.web.util.UriComponentsBuilder;
+import reactor.core.publisher.Mono;
+
+import javax.annotation.Nonnull;
+import java.time.Duration;
+import java.util.HashMap;
+import java.util.concurrent.atomic.AtomicReference;
+
+@Slf4j
+public class DingTalkNotifier extends AbstractNotifier<DingTalkMessageTemplate> {
+
+    private AtomicReference<String> accessToken = new AtomicReference<>();
+
+    private long refreshTokenTime;
+
+    private long tokenTimeOut = Duration.ofSeconds(7000).toMillis();
+
+    private WebClient client;
+
+    private static final String tokenApi = "https://oapi.dingtalk.com/gettoken";
+
+    private static final String notify = "https://oapi.dingtalk.com/topapi/message/corpconversation/asyncsend_v2";
+
+    private DingTalkProperties properties;
+
+    public DingTalkNotifier(WebClient client, DingTalkProperties properties, TemplateManager templateManager) {
+        super(templateManager);
+        this.client = client;
+        this.properties = properties;
+    }
+
+    @Nonnull
+    @Override
+    public NotifyType getType() {
+        return DefaultNotifyType.dingTalk;
+    }
+
+    @Nonnull
+    @Override
+    public Provider getProvider() {
+        return DingTalkProvider.dingTalkMessage;
+    }
+
+    @Nonnull
+    @Override
+    public Mono<Void> send(@Nonnull DingTalkMessageTemplate template, @Nonnull Values context) {
+        return getToken()
+                .flatMap(token ->
+                        client.post()
+                                .uri(notify)
+                                .contentType(MediaType.APPLICATION_FORM_URLENCODED)
+                                .body(template.createFormInserter(BodyInserters.fromFormData("access_token", token),context))
+                                .exchange()
+                                .flatMap(clientResponse -> clientResponse.bodyToMono(HashMap.class))
+                                .as(this::checkResult))
+                .then();
+    }
+
+    private Mono<HashMap> checkResult(Mono<HashMap> msg) {
+        return msg.doOnNext(map -> {
+            String code = String.valueOf(map.get("errcode"));
+            if ("0".equals(code)) {
+                log.info("发送钉钉通知成功");
+            } else {
+                log.warn("发送钉钉通知失败:{}", map);
+                throw new BusinessException("发送钉钉通知失败:" + map.get("errmsg"), code);
+            }
+        });
+    }
+
+    private Mono<String> getToken() {
+        if (System.currentTimeMillis() - refreshTokenTime > tokenTimeOut || accessToken.get() == null) {
+            return requestToken();
+        }
+        return Mono.just(accessToken.get());
+    }
+
+    private Mono<String> requestToken() {
+        return client
+                .get()
+                .uri(UriComponentsBuilder.fromUriString(tokenApi)
+                        .queryParam("appkey", properties.getAppKey())
+                        .queryParam("appsecret", properties.getAppSecret())
+                        .build().toUri())
+                .exchange()
+                .flatMap(resp -> resp.bodyToMono(HashMap.class))
+                .map(map -> {
+                    if (map.containsKey("access_token")) {
+                        return map.get("access_token");
+                    }
+                    throw new BusinessException("获取Token失败:" + map.get("errmsg"), String.valueOf(map.get("errcode")));
+                })
+                .cast(String.class)
+                .doOnNext((r) -> {
+                    refreshTokenTime = System.currentTimeMillis();
+                    accessToken.set(r);
+                });
+    }
+
+    @Nonnull
+    @Override
+    public Mono<Void> close() {
+        accessToken.set(null);
+        refreshTokenTime = 0;
+        return Mono.empty();
+    }
+}

+ 80 - 0
jetlinks-components/notify-component/notify-dingtalk/src/main/java/org/jetlinks/community/notify/dingtalk/DingTalkNotifierProvider.java

@@ -0,0 +1,80 @@
+package org.jetlinks.community.notify.dingtalk;
+
+import com.alibaba.fastjson.JSON;
+import org.hswebframework.web.bean.FastBeanCopier;
+import org.hswebframework.web.validator.ValidatorUtils;
+import org.jetlinks.core.metadata.ConfigMetadata;
+import org.jetlinks.core.metadata.DefaultConfigMetadata;
+import org.jetlinks.core.metadata.types.BooleanType;
+import org.jetlinks.core.metadata.types.StringType;
+import org.jetlinks.community.ConfigMetadataConstants;
+import org.jetlinks.community.notify.*;
+import org.jetlinks.community.notify.template.TemplateManager;
+import org.jetlinks.community.notify.template.TemplateProperties;
+import org.jetlinks.community.notify.template.TemplateProvider;
+import org.springframework.stereotype.Component;
+import org.springframework.web.reactive.function.client.WebClient;
+import reactor.core.publisher.Mono;
+
+import javax.annotation.Nonnull;
+
+@Component
+public class DingTalkNotifierProvider implements NotifierProvider, TemplateProvider {
+
+    private WebClient client = WebClient.create();
+
+    private final TemplateManager templateManager;
+
+    public DingTalkNotifierProvider(TemplateManager templateManager) {
+        this.templateManager = templateManager;
+    }
+
+    public static final DefaultConfigMetadata notifierConfig = new DefaultConfigMetadata("通知配置", "")
+            .add("appKey", "appKey", "", new StringType().expand(ConfigMetadataConstants.required.value(true)))
+            .add("appSecret", "appSecret", "", new StringType());
+
+    public static final DefaultConfigMetadata templateConfig = new DefaultConfigMetadata("模版配置", "")
+            .add("agentId", "应用ID", "", new StringType().expand(ConfigMetadataConstants.required.value(true)))
+            .add("userIdList", "收信人ID", "与部门ID不能同时为空", new StringType())
+            .add("departmentIdList", "收信部门ID", "与收信人ID不能同时为空", new StringType())
+            .add("toAllUser", "全部用户", "推送到全部用户", new BooleanType())
+            .add("message", "内容", "最大不超过500字", new StringType().expand(ConfigMetadataConstants.maxLength.value(500L)));
+
+    @Nonnull
+    @Override
+    public NotifyType getType() {
+        return DefaultNotifyType.dingTalk;
+    }
+
+    @Nonnull
+    @Override
+    public Provider getProvider() {
+        return DingTalkProvider.dingTalkMessage;
+    }
+
+    @Override
+    public Mono<DingTalkMessageTemplate> createTemplate(TemplateProperties properties) {
+        return Mono.fromSupplier(() -> {
+            return ValidatorUtils.tryValidate(JSON.parseObject(properties.getTemplate(), DingTalkMessageTemplate.class));
+        });
+    }
+
+    @Nonnull
+    @Override
+    public Mono<DingTalkNotifier> createNotifier(@Nonnull NotifierProperties properties) {
+        return Mono.defer(() -> {
+            DingTalkProperties dingTalkProperties = FastBeanCopier.copy(properties.getConfiguration(), new DingTalkProperties());
+            return Mono.just(new DingTalkNotifier(client, ValidatorUtils.tryValidate(dingTalkProperties), templateManager));
+        });
+    }
+
+    @Override
+    public ConfigMetadata getNotifierConfigMetadata() {
+        return notifierConfig;
+    }
+
+    @Override
+    public ConfigMetadata getTemplateConfigMetadata() {
+        return templateConfig;
+    }
+}

+ 19 - 0
jetlinks-components/notify-component/notify-dingtalk/src/main/java/org/jetlinks/community/notify/dingtalk/DingTalkProperties.java

@@ -0,0 +1,19 @@
+package org.jetlinks.community.notify.dingtalk;
+
+import lombok.Getter;
+import lombok.Setter;
+
+import javax.validation.constraints.NotBlank;
+
+@Getter
+@Setter
+public class DingTalkProperties {
+
+    @NotBlank(message = "appKey不能为空")
+    private String appKey;
+
+    @NotBlank(message = "appSecret不能为空")
+    private String appSecret;
+
+
+}

+ 20 - 0
jetlinks-components/notify-component/notify-dingtalk/src/main/java/org/jetlinks/community/notify/dingtalk/DingTalkProvider.java

@@ -0,0 +1,20 @@
+package org.jetlinks.community.notify.dingtalk;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import org.jetlinks.community.notify.Provider;
+
+@Getter
+@AllArgsConstructor
+public enum DingTalkProvider implements Provider {
+    dingTalkMessage("钉钉消息通知")
+    ;
+
+    private String name;
+
+    @Override
+    public String getId() {
+        return name();
+    }
+
+}

+ 39 - 0
jetlinks-components/notify-component/notify-dingtalk/src/test/java/org/jetlinks/community/notify/dingtalk/DingTalkNotifierTest.java

@@ -0,0 +1,39 @@
+package org.jetlinks.community.notify.dingtalk;
+
+import org.jetlinks.core.Values;
+import org.junit.jupiter.api.Test;
+import org.springframework.web.reactive.function.client.WebClient;
+import reactor.test.StepVerifier;
+
+import java.util.HashMap;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+class DingTalkNotifierTest {
+
+
+    @Test
+    void test(){
+        DingTalkProperties properties=new DingTalkProperties();
+        properties.setAppKey("dingd2rgqrqnbvgbvi9xZQ");
+        properties.setAppSecret("rfSNLse4SI1CeAo6aV8cPRzig8HZACwRR6XGS_feOje-3M7rE68WjL9LKWTgko2R");
+
+        DingTalkMessageTemplate messageTemplate=new DingTalkMessageTemplate();
+
+        messageTemplate.setAgentId("335474263");
+        messageTemplate.setMessage("test"+System.currentTimeMillis());
+        messageTemplate.setUserIdList("0458215455697857");
+
+        DingTalkNotifier notifier=new DingTalkNotifier(
+                WebClient.builder().build(),properties,null
+        );
+
+        notifier.send(messageTemplate, Values.of(new HashMap<>()))
+                .as(StepVerifier::create)
+                .expectComplete()
+                .verify();
+
+
+    }
+
+}

+ 44 - 0
jetlinks-components/notify-component/notify-email/pom.xml

@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <artifactId>notify-component</artifactId>
+        <groupId>org.jetlinks.community</groupId>
+        <version>1.0-SNAPSHOT</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>notify-email</artifactId>
+    <dependencies>
+        <dependency>
+            <groupId>org.jetlinks</groupId>
+            <artifactId>rule-engine-support</artifactId>
+            <version>${jetlinks.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.jetlinks.community</groupId>
+            <artifactId>notify-core</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework</groupId>
+            <artifactId>spring-context-support</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>javax.mail</groupId>
+            <artifactId>mail</artifactId>
+            <version>1.4.7</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.jsoup</groupId>
+            <artifactId>jsoup</artifactId>
+            <version>1.11.3</version>
+        </dependency>
+
+    </dependencies>
+
+</project>

+ 20 - 0
jetlinks-components/notify-component/notify-email/src/main/java/org/jetlinks/community/notify/email/EmailProvider.java

@@ -0,0 +1,20 @@
+package org.jetlinks.community.notify.email;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import org.jetlinks.community.notify.Provider;
+
+@Getter
+@AllArgsConstructor
+public enum EmailProvider implements Provider {
+
+    embedded("默认")
+    ;
+
+    private String name;
+
+    @Override
+    public String getId() {
+        return name();
+    }
+}

+ 198 - 0
jetlinks-components/notify-component/notify-email/src/main/java/org/jetlinks/community/notify/email/embedded/DefaultEmailNotifier.java

@@ -0,0 +1,198 @@
+package org.jetlinks.community.notify.email.embedded;
+
+import com.alibaba.fastjson.JSONObject;
+import io.vavr.control.Try;
+import lombok.Getter;
+import lombok.Setter;
+import lombok.extern.slf4j.Slf4j;
+import org.hswebframework.web.exception.BusinessException;
+import org.hswebframework.web.id.IDGenerator;
+import org.hswebframework.web.utils.ExpressionUtils;
+import org.hswebframework.web.validator.ValidatorUtils;
+import org.jetlinks.core.Values;
+import org.jetlinks.community.notify.*;
+import org.jetlinks.community.notify.email.EmailProvider;
+import org.jetlinks.community.notify.template.TemplateManager;
+import org.jsoup.Jsoup;
+import org.jsoup.nodes.Document;
+import org.jsoup.nodes.Element;
+import org.springframework.core.io.InputStreamResource;
+import org.springframework.core.io.InputStreamSource;
+import org.springframework.core.io.Resource;
+import org.springframework.http.MediaType;
+import org.springframework.mail.javamail.JavaMailSender;
+import org.springframework.mail.javamail.JavaMailSenderImpl;
+import org.springframework.mail.javamail.MimeMessageHelper;
+import org.springframework.util.StringUtils;
+import org.springframework.web.reactive.function.client.WebClient;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+import reactor.core.scheduler.Scheduler;
+import reactor.core.scheduler.Schedulers;
+
+import javax.annotation.Nonnull;
+import javax.mail.internet.MimeMessage;
+import javax.mail.internet.MimeUtility;
+import java.io.*;
+import java.nio.charset.StandardCharsets;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Function;
+
+/**
+ * 使用javax.mail进行邮件发送
+ *
+ * @author bsetfeng
+ * @author zhouhao
+ * @since 1.0
+ **/
+@Slf4j
+public class DefaultEmailNotifier extends AbstractNotifier<EmailTemplate> {
+
+
+    @Getter
+    @Setter
+    private JavaMailSender javaMailSender;
+
+    @Getter
+    @Setter
+    private String sender;
+
+    public static Scheduler scheduler = Schedulers.newElastic("email-notifier");
+
+    public DefaultEmailNotifier(NotifierProperties properties, TemplateManager templateManager) {
+        super(templateManager);
+
+        DefaultEmailProperties emailProperties = new JSONObject(properties.getConfiguration())
+            .toJavaObject(DefaultEmailProperties.class);
+        ValidatorUtils.tryValidate(emailProperties);
+
+        JavaMailSenderImpl mailSender = new JavaMailSenderImpl();
+        mailSender.setHost(emailProperties.getHost());
+        mailSender.setPort(emailProperties.getPort());
+        mailSender.setUsername(emailProperties.getUsername());
+        mailSender.setPassword(emailProperties.getPassword());
+        mailSender.setJavaMailProperties(emailProperties.createJavaMailProperties());
+        this.sender = emailProperties.getSender();
+        this.javaMailSender = mailSender;
+    }
+
+    @Nonnull
+    @Override
+    public Mono<Void> send(@Nonnull EmailTemplate template, @Nonnull Values context) {
+        return Mono.just(template)
+            .map(temp -> convert(temp, context.getAllValues()))
+            .flatMap(temp -> doSend(temp, template.getSendTo()));
+    }
+
+    @Nonnull
+    @Override
+    public Mono<Void> close() {
+        return Mono.empty();
+    }
+
+    @Nonnull
+    @Override
+    public NotifyType getType() {
+        return DefaultNotifyType.email;
+    }
+
+    @Nonnull
+    @Override
+    public Provider getProvider() {
+        return EmailProvider.embedded;
+    }
+
+    protected Mono<Void> doSend(ParsedEmailTemplate template, List<String> sendTo) {
+        return Mono
+            .fromCallable(() -> {
+                MimeMessage mimeMessage = this.javaMailSender.createMimeMessage();
+                MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, true, "utf-8");
+
+                helper.setFrom(this.sender);
+                helper.setTo(sendTo.toArray(new String[0]));
+                helper.setSubject(template.getSubject());
+                helper.setText(new String(template.getText().getBytes(), StandardCharsets.UTF_8), true);
+
+                return Flux.fromIterable(template.getAttachments().entrySet())
+                    .flatMap(entry -> Mono.zip(Mono.just(entry.getKey()), convertResource(entry.getValue())))
+                    .doOnNext(tp -> Try.run(() -> helper.addAttachment(MimeUtility.encodeText(tp.getT1()), tp.getT2())).get())
+                    .then(
+                        Flux.fromIterable(template.getImages().entrySet())
+                            .flatMap(entry -> Mono.zip(Mono.just(entry.getKey()), convertResource(entry.getValue())))
+                            .doOnNext(tp -> Try.run(() -> helper.addInline(tp.getT1(), tp.getT2(), MediaType.APPLICATION_OCTET_STREAM_VALUE)).get())
+                            .then()
+                    ).thenReturn(mimeMessage)
+                    ;
+
+            })
+            .publishOn(scheduler)
+            .flatMap(Function.identity())
+            .doOnNext(message -> this.javaMailSender.send(message))
+            .then()
+            ;
+    }
+
+
+    protected Mono<InputStreamSource> convertResource(String resource) {
+        if (resource.startsWith("http")) {
+            return WebClient.create()
+                .get()
+                .uri(resource)
+                .accept(MediaType.APPLICATION_OCTET_STREAM)
+                .exchange()
+                .flatMap(rep -> rep.bodyToMono(Resource.class));
+        } else {
+            try {
+                return Mono.just(new InputStreamResource(new FileInputStream(resource)));
+            } catch (FileNotFoundException e) {
+                return Mono.error(e);
+            }
+        }
+    }
+
+
+    protected ParsedEmailTemplate convert(EmailTemplate template, Map<String, Object> context) {
+        String subject = template.getSubject();
+        String text = template.getText();
+        if (StringUtils.isEmpty(subject) || StringUtils.isEmpty(text)) {
+            throw new BusinessException("模板内容错误,text 或者 subject 不能为空.");
+        }
+        String sendText = render(text, context);
+        List<EmailTemplate.Attachment> tempAttachments = template.getAttachments();
+        Map<String, String> attachments = new HashMap<>();
+        if (tempAttachments != null) {
+            for (EmailTemplate.Attachment tempAttachment : tempAttachments) {
+                attachments.put(tempAttachment.getName(), render(tempAttachment.getLocation(), context));
+            }
+        }
+        return ParsedEmailTemplate.builder()
+            .attachments(attachments)
+            .images(extractSendTextImage(sendText))
+            .text(sendText)
+            .subject(render(subject, context))
+            .build();
+    }
+
+
+    private Map<String, String> extractSendTextImage(String sendText) {
+        Map<String, String> images = new HashMap<>();
+        Document doc = Jsoup.parse(sendText);
+        for (Element src : doc.getElementsByTag("img")) {
+            String s = src.attr("src");
+            if (s.startsWith("http")) {
+                continue;
+            }
+            String tempKey = IDGenerator.MD5.generate();
+            src.attr("src", "cid:".concat(tempKey));
+            images.put(tempKey, s);
+        }
+        return images;
+    }
+
+    private String render(String str, Map<String, Object> context) {
+        return ExpressionUtils.analytical(str, context, "spel");
+    }
+
+}

+ 122 - 0
jetlinks-components/notify-component/notify-email/src/main/java/org/jetlinks/community/notify/email/embedded/DefaultEmailNotifierProvider.java

@@ -0,0 +1,122 @@
+package org.jetlinks.community.notify.email.embedded;
+
+import com.alibaba.fastjson.JSON;
+import org.jetlinks.core.metadata.ConfigMetadata;
+import org.jetlinks.core.metadata.DefaultConfigMetadata;
+import org.jetlinks.core.metadata.SimplePropertyMetadata;
+import org.jetlinks.core.metadata.types.*;
+import org.jetlinks.community.notify.*;
+import org.jetlinks.community.notify.email.EmailProvider;
+import org.jetlinks.community.notify.template.TemplateManager;
+import org.jetlinks.community.notify.template.TemplateProperties;
+import org.jetlinks.community.notify.template.TemplateProvider;
+import org.springframework.stereotype.Component;
+import reactor.core.publisher.Mono;
+
+import javax.annotation.Nonnull;
+
+import static org.jetlinks.community.ConfigMetadataConstants.*;
+
+@Component
+public class DefaultEmailNotifierProvider implements NotifierProvider, TemplateProvider {
+
+    private final TemplateManager templateManager;
+
+    public DefaultEmailNotifierProvider(TemplateManager templateManager) {
+        this.templateManager = templateManager;
+    }
+
+    @Nonnull
+    @Override
+    public NotifyType getType() {
+        return DefaultNotifyType.email;
+    }
+
+    @Nonnull
+    @Override
+    public Provider getProvider() {
+        return EmailProvider.embedded;
+    }
+
+    public static final DefaultConfigMetadata templateConfig;
+
+    public static final DefaultConfigMetadata notifierConfig;
+
+    static {
+        {
+            SimplePropertyMetadata name = new SimplePropertyMetadata();
+            name.setId("name");
+            name.setName("文件名");
+            name.setValueType(new StringType());
+
+            SimplePropertyMetadata location = new SimplePropertyMetadata();
+            location.setId("location");
+            location.setName("文件地址");
+            location.setValueType(new FileType()
+                    .bodyType(FileType.BodyType.url)
+                    .expand(allowInput.value(true)));
+
+            templateConfig = new DefaultConfigMetadata("邮件模版", "")
+                    .add("subject", "标题", "标题,可使用变量", new StringType().expand(maxLength.value(255L)))
+                    .add("text", "内容", "", new StringType().expand(maxLength.value(5120L), isRichText.value(true)))
+                    .add("sendTo", "收件人", "", new ArrayType().elementType(new StringType()))
+                    .add("attachments", "附件列表", "", new ArrayType()
+                            .elementType(new ObjectType()
+                                    .addPropertyMetadata(name)
+                                    .addPropertyMetadata(location)));
+        }
+
+        {
+            SimplePropertyMetadata name = new SimplePropertyMetadata();
+            name.setId("name");
+            name.setName("配置名称");
+            name.setValueType(new StringType());
+
+            SimplePropertyMetadata value = new SimplePropertyMetadata();
+            value.setId("value");
+            value.setName("配置值");
+            value.setValueType(new StringType());
+
+            SimplePropertyMetadata description = new SimplePropertyMetadata();
+            description.setId("description");
+            description.setName("说明");
+            description.setValueType(new StringType());
+
+            notifierConfig = new DefaultConfigMetadata("邮件配置", "")
+                    .add("host", "服务器地址", "例如: pop3.qq.com", new StringType().expand(maxLength.value(255L)))
+                    .add("port", "端口", "", new IntType().min(0).max(65536))
+                    .add("sender", "发件人", "默认和用户名相同", new StringType())
+                    .add("username", "用户名", "", new StringType())
+                    .add("password", "密码", "", new PasswordType())
+                    .add("properties", "其他配置", "", new ArrayType()
+                            .elementType(new ObjectType()
+                                    .addPropertyMetadata(name)
+                                    .addPropertyMetadata(value)
+                                    .addPropertyMetadata(description)));
+        }
+
+
+    }
+
+    @Override
+    public ConfigMetadata getNotifierConfigMetadata() {
+        return notifierConfig;
+    }
+
+    @Override
+    public ConfigMetadata getTemplateConfigMetadata() {
+        return templateConfig;
+    }
+
+    @Nonnull
+    @Override
+    public Mono<DefaultEmailNotifier> createNotifier(@Nonnull NotifierProperties properties) {
+        return Mono.fromSupplier(() -> new DefaultEmailNotifier(properties, templateManager));
+    }
+
+    @Override
+    public Mono<EmailTemplate> createTemplate(TemplateProperties properties) {
+
+        return Mono.fromSupplier(() -> JSON.parseObject(properties.getTemplate(), EmailTemplate.class));
+    }
+}

+ 49 - 0
jetlinks-components/notify-component/notify-email/src/main/java/org/jetlinks/community/notify/email/embedded/DefaultEmailProperties.java

@@ -0,0 +1,49 @@
+package org.jetlinks.community.notify.email.embedded;
+
+import lombok.Getter;
+import lombok.Setter;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+
+@Getter
+@Setter
+public class DefaultEmailProperties {
+    private String host;
+
+    private int port;
+
+    private String username;
+
+    private String password;
+
+    private String sender;
+
+    private List<ConfigProperty> properties;
+
+    @Getter
+    @Setter
+    public static class ConfigProperty {
+
+        private String name;
+
+        private String value;
+
+        private String description;
+    }
+
+    public Properties createJavaMailProperties() {
+
+        Properties properties = new Properties();
+
+        if (this.properties != null) {
+            for (ConfigProperty property : this.properties) {
+                properties.put(property.getName(), property.getValue());
+            }
+        }
+
+        return properties;
+    }
+
+}

+ 30 - 0
jetlinks-components/notify-component/notify-email/src/main/java/org/jetlinks/community/notify/email/embedded/EmailTemplate.java

@@ -0,0 +1,30 @@
+package org.jetlinks.community.notify.email.embedded;
+
+import lombok.Getter;
+import lombok.Setter;
+import org.jetlinks.community.notify.template.Template;
+
+import java.util.List;
+
+@Getter
+@Setter
+public class EmailTemplate implements Template {
+
+    private String subject;
+
+    private String text;
+
+    private List<Attachment> attachments;
+
+    private List<String> sendTo;
+
+    @Getter
+    @Setter
+    public static class Attachment {
+
+        private String name;
+
+        private String location;
+
+    }
+}

+ 29 - 0
jetlinks-components/notify-component/notify-email/src/main/java/org/jetlinks/community/notify/email/embedded/ParsedEmailTemplate.java

@@ -0,0 +1,29 @@
+package org.jetlinks.community.notify.email.embedded;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.util.Map;
+
+/**
+ * @author bsetfeng
+ * @since 1.0
+ **/
+@Builder
+@AllArgsConstructor
+@NoArgsConstructor
+@Data
+public class ParsedEmailTemplate {
+
+    //附件 key:附件名称 value:附件uri
+    private Map<String, String> attachments;
+
+    //图片 key:text中图片占位符 value:图片uri
+    private Map<String, String> images;
+
+    private String subject;
+
+    private String text;
+}

+ 41 - 0
jetlinks-components/notify-component/notify-sms/pom.xml

@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <artifactId>notify-component</artifactId>
+        <groupId>org.jetlinks.community</groupId>
+        <version>1.0-SNAPSHOT</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>notify-sms</artifactId>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.hswebframework</groupId>
+            <artifactId>hsweb-easy-orm-rdb</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.hswebframework.web</groupId>
+            <artifactId>hsweb-starter</artifactId>
+            <version>${hsweb.framework.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.jetlinks</groupId>
+            <artifactId>rule-engine-support</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework</groupId>
+            <artifactId>spring-context-indexer</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.jetlinks.community</groupId>
+            <artifactId>notify-core</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+    </dependencies>
+</project>

+ 168 - 0
jetlinks-components/notify-component/notify-sms/src/main/java/org/jetlinks/community/notify/sms/provider/Hy2046SmsSenderProvider.java

@@ -0,0 +1,168 @@
+package org.jetlinks.community.notify.sms.provider;
+
+import com.alibaba.fastjson.JSON;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.codec.digest.DigestUtils;
+import org.jetlinks.core.Values;
+import org.jetlinks.core.metadata.ConfigMetadata;
+import org.jetlinks.core.metadata.DefaultConfigMetadata;
+import org.jetlinks.core.metadata.types.PasswordType;
+import org.jetlinks.core.metadata.types.StringType;
+import org.jetlinks.community.notify.*;
+import org.jetlinks.community.notify.template.Template;
+import org.jetlinks.community.notify.template.TemplateManager;
+import org.jetlinks.community.notify.template.TemplateProperties;
+import org.jetlinks.community.notify.template.TemplateProvider;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Profile;
+import org.springframework.http.MediaType;
+import org.springframework.stereotype.Component;
+import org.springframework.util.Assert;
+import org.springframework.util.LinkedMultiValueMap;
+import org.springframework.util.MultiValueMap;
+import org.springframework.web.reactive.function.BodyInserters;
+import org.springframework.web.reactive.function.client.WebClient;
+import reactor.core.publisher.Mono;
+
+import javax.annotation.Nonnull;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.Map;
+
+@Component
+@Slf4j
+@Profile({"dev","test"})
+public class Hy2046SmsSenderProvider implements NotifierProvider, TemplateProvider, Provider {
+
+
+    private WebClient webClient = WebClient.builder()
+            .baseUrl("http://sms10692.com/v2sms.aspx")
+            .build();
+
+    @Autowired
+    private TemplateManager templateManager;
+
+    @Nonnull
+    @Override
+    public NotifyType getType() {
+        return DefaultNotifyType.sms;
+    }
+
+    @Nonnull
+    @Override
+    public Provider getProvider() {
+        return this;
+    }
+
+    static DefaultConfigMetadata notifierConfig = new DefaultConfigMetadata("宏衍2046短信配置", "")
+            .add("userId", "userId", "用户ID", new StringType())
+            .add("username", "用户名", "用户名", new StringType())
+            .add("password", "密码", "密码", new PasswordType());
+
+    @Override
+    public ConfigMetadata getNotifierConfigMetadata() {
+        return notifierConfig;
+    }
+
+    @Override
+    public ConfigMetadata getTemplateConfigMetadata() {
+        return PlainTextSmsTemplate.templateConfig;
+    }
+
+    @Override
+    public Mono<? extends Template> createTemplate(TemplateProperties properties) {
+        return Mono.fromSupplier(() -> JSON.parseObject(properties.getTemplate(), PlainTextSmsTemplate.class));
+    }
+
+    @Nonnull
+    @Override
+    public Mono<Hy2046SmsSender> createNotifier(@Nonnull NotifierProperties properties) {
+        return Mono.defer(() -> {
+            String userId = (String) properties.getConfigOrNull("userId");
+            String username = (String) properties.getConfigOrNull("username");
+            String password = (String) properties.getConfigOrNull("password");
+            Assert.hasText(userId, "短信配置错误,缺少userId");
+            Assert.hasText(username, "短信配置错误,缺少username");
+            Assert.hasText(password, "短信配置错误,缺少password");
+            return Mono.just(new Hy2046SmsSender(userId, username, password));
+        });
+    }
+
+    @Override
+    public String getId() {
+        return "hy2046";
+    }
+
+    @Override
+    public String getName() {
+        return "宏衍2046";
+    }
+
+    class Hy2046SmsSender extends AbstractNotifier<PlainTextSmsTemplate> {
+
+        String userId;
+        String username;
+        String password;
+
+        public Hy2046SmsSender(String userId, String username, String password) {
+            super(templateManager);
+            this.userId = userId;
+            this.username = username;
+            this.password = password;
+        }
+
+        @Nonnull
+        @Override
+        public NotifyType getType() {
+            return DefaultNotifyType.sms;
+        }
+
+        @Nonnull
+        @Override
+        public Provider getProvider() {
+            return Hy2046SmsSenderProvider.this;
+        }
+
+        @Nonnull
+        @Override
+        public Mono<Void> close() {
+            return Mono.empty();
+        }
+
+
+        @Nonnull
+        @Override
+        public Mono<Void> send(@Nonnull PlainTextSmsTemplate template, @Nonnull Values context) {
+            return Mono.defer(() -> {
+                String ts = DateTimeFormatter.ofPattern("yyyyMMddHHmmss").format(LocalDateTime.now());
+                String sign = DigestUtils.md5Hex(username.concat(password).concat(ts));
+                String[] sendTo = template.getSendTo(context.getAllValues());
+
+                String mobile = String.join(",", sendTo);
+                MultiValueMap<String, String> formData = new LinkedMultiValueMap<>();
+                formData.add("userid", userId);
+                formData.add("timestamp", ts);
+                formData.add("sign", sign);
+                formData.add("mobile", mobile);
+                formData.add("content", template.getTextSms(context.getAllValues()));
+                formData.add("action", "send");
+                formData.add("rt", "json");
+
+                return webClient.post()
+                        .contentType(MediaType.APPLICATION_FORM_URLENCODED)
+                        .body(BodyInserters.fromFormData(formData))
+                        .retrieve()
+                        .bodyToMono(Map.class)
+                        .map(map -> {
+                            if (Integer.valueOf(sendTo.length).equals(map.get("SuccessCounts"))) {
+                                return true;
+                            }
+                            throw new RuntimeException("发送短信失败:" + map.get("Message"));
+                        });
+
+            }).then();
+        }
+
+    }
+}
+

+ 40 - 0
jetlinks-components/notify-component/notify-sms/src/main/java/org/jetlinks/community/notify/sms/provider/PlainTextSmsTemplate.java

@@ -0,0 +1,40 @@
+package org.jetlinks.community.notify.sms.provider;
+
+import lombok.Getter;
+import lombok.Setter;
+import org.hswebframework.web.utils.ExpressionUtils;
+import org.jetlinks.core.metadata.DefaultConfigMetadata;
+import org.jetlinks.core.metadata.types.ArrayType;
+import org.jetlinks.core.metadata.types.StringType;
+import org.jetlinks.community.ConfigMetadataConstants;
+import org.jetlinks.community.notify.template.Template;
+
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Stream;
+
+@Getter
+@Setter
+public class PlainTextSmsTemplate implements Template {
+
+    public static final DefaultConfigMetadata templateConfig = new DefaultConfigMetadata("模版配置", "")
+            .add("text", "短信内容", "短信内容,支持使用变量:${ }", new StringType()
+                    .expand(ConfigMetadataConstants.maxLength.value(512L)))
+            .add("sendTo", "收件人", "", new ArrayType().elementType(new StringType()));
+
+    private String text;
+
+    private List<String> sendTo;
+
+    public String getTextSms(Map<String, Object> context) {
+        return ExpressionUtils.analytical(text, context, "spel");
+    }
+
+    public String[] getSendTo(Map<String, Object> context) {
+
+        return sendTo.stream()
+                .map(str -> ExpressionUtils.analytical(str, context, "spel")).toArray(String[]::new);
+
+    }
+
+}

+ 81 - 0
jetlinks-components/notify-component/notify-sms/src/main/java/org/jetlinks/community/notify/sms/provider/TestSmsProvider.java

@@ -0,0 +1,81 @@
+package org.jetlinks.community.notify.sms.provider;
+
+import com.alibaba.fastjson.JSON;
+import lombok.extern.slf4j.Slf4j;
+import org.jetlinks.core.Values;
+import org.jetlinks.core.metadata.ConfigMetadata;
+import org.jetlinks.community.notify.*;
+import org.jetlinks.community.notify.template.Template;
+import org.jetlinks.community.notify.template.TemplateManager;
+import org.jetlinks.community.notify.template.TemplateProperties;
+import org.jetlinks.community.notify.template.TemplateProvider;
+import org.springframework.context.annotation.Profile;
+import org.springframework.stereotype.Component;
+import reactor.core.publisher.Mono;
+
+import javax.annotation.Nonnull;
+
+@Slf4j
+@Component
+@Profile({"dev", "test"})
+public class TestSmsProvider extends AbstractNotifier<PlainTextSmsTemplate> implements NotifierProvider, TemplateProvider, Provider {
+
+    public TestSmsProvider(TemplateManager templateManager) {
+        super(templateManager);
+    }
+
+    @Override
+    @Nonnull
+    public NotifyType getType() {
+        return DefaultNotifyType.sms;
+    }
+
+    @Override
+    @Nonnull
+    public Provider getProvider() {
+        return this;
+    }
+
+    @Override
+    public Mono<? extends Template> createTemplate(TemplateProperties properties) {
+        return Mono.fromSupplier(() -> JSON.parseObject(properties.getTemplate(), PlainTextSmsTemplate.class));
+    }
+
+    @Override
+    public ConfigMetadata getTemplateConfigMetadata() {
+        return PlainTextSmsTemplate.templateConfig;
+    }
+
+    @Override
+    public ConfigMetadata getNotifierConfigMetadata() {
+        return null;
+    }
+
+    @Nonnull
+    @Override
+    public Mono<Void> send(@Nonnull PlainTextSmsTemplate template, @Nonnull Values context) {
+        return Mono.fromRunnable(() -> log.info("send sms [{}] message:{}", template.getSendTo(context.getAllValues()), template.getTextSms(context.getAllValues())));
+    }
+
+    @Nonnull
+    @Override
+    public Mono<Void> close() {
+        return Mono.empty();
+    }
+
+    @Nonnull
+    @Override
+    public Mono<TestSmsProvider> createNotifier(@Nonnull NotifierProperties properties) {
+        return Mono.just(this);
+    }
+
+    @Override
+    public String getId() {
+        return "test";
+    }
+
+    @Override
+    public String getName() {
+        return "测试";
+    }
+}

+ 46 - 0
jetlinks-components/notify-component/notify-voice/README.md

@@ -0,0 +1,46 @@
+# 语音通知
+
+用于发送电话语音通知.
+
+
+
+## API
+
+1. org.jetlinks.community.notify.voice.VoiceNotifierManager
+
+例子:
+```java
+
+motifierManager.getNotifier(notifierId) //获取通知器
+            .flatMap(notifier->notifier.send(templateId,contextMap))//发送
+            .doOnError(err->log.error("发送失败",err)) //失败
+            .subscribe(ignore->log.info("发送成功")); //成功
+
+```
+ 
+## 拓展自定义服务商
+
+实现接口: `org.jetlinks.community.notify.voice.provider.VoiceNotifierProvider`
+
+
+## 阿里云(`aliyun`)
+
+系统已实现阿里云语音通知`org.jetlinks.community.notify.voice.supports.aliyun.AliyunVoiceNotifierProvider`
+
+配置(`NotifierProperties.configuration`):
+
+|  Key   |  示例 |
+|  ----  | ----  |
+| regionId  |  cn-hangzhou |
+| accessKeyId | LTAI4Dj2oThQnZYMAwTSj1F8 |
+| secret  |  FeZ2nQZKM635IsPDudZOaQ5aDHMFeT |
+
+模版配置(`TemplateProperties.template`)
+
+|  Key   |  示例 |
+|  ----  | ----  |
+| ttsCode  |  TTS_123456 |
+| calledShowNumbers | 02387673801 |
+| calledNumber  |  18502114022 |
+| playTimes  |  1 |
+

+ 27 - 0
jetlinks-components/notify-component/notify-voice/pom.xml

@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <artifactId>notify-component</artifactId>
+        <groupId>org.jetlinks.community</groupId>
+        <version>1.0-SNAPSHOT</version>
+        <relativePath>../pom.xml</relativePath>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>notify-voice</artifactId>
+
+    <dependencies>
+        <dependency>
+            <groupId>com.aliyun</groupId>
+            <artifactId>aliyun-java-sdk-core</artifactId>
+            <version>4.1.0</version>
+        </dependency>
+        <dependency>
+            <groupId>${project.groupId}</groupId>
+            <artifactId>notify-core</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+    </dependencies>
+</project>

+ 19 - 0
jetlinks-components/notify-component/notify-voice/src/main/java/org/jetlinks/community/notify/voice/VoiceNotifierConfiguration.java

@@ -0,0 +1,19 @@
+package org.jetlinks.community.notify.voice;
+
+import org.jetlinks.community.notify.template.TemplateManager;
+import org.jetlinks.community.notify.voice.aliyun.AliyunNotifierProvider;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+public class VoiceNotifierConfiguration {
+
+
+    @Bean
+    @ConditionalOnBean(TemplateManager.class)
+    public AliyunNotifierProvider aliyunNotifierProvider(TemplateManager templateManager) {
+        return new AliyunNotifierProvider(templateManager);
+    }
+
+}

+ 20 - 0
jetlinks-components/notify-component/notify-voice/src/main/java/org/jetlinks/community/notify/voice/VoiceProvider.java

@@ -0,0 +1,20 @@
+package org.jetlinks.community.notify.voice;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import org.jetlinks.community.notify.Provider;
+
+@Getter
+@AllArgsConstructor
+public enum VoiceProvider implements Provider {
+
+    aliyun("阿里云")
+    ;
+
+    private String name;
+
+    @Override
+    public String getId() {
+        return name();
+    }
+}

+ 79 - 0
jetlinks-components/notify-component/notify-voice/src/main/java/org/jetlinks/community/notify/voice/aliyun/AliyunNotifierProvider.java

@@ -0,0 +1,79 @@
+package org.jetlinks.community.notify.voice.aliyun;
+
+import com.alibaba.fastjson.JSON;
+import lombok.AllArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.hswebframework.web.validator.ValidatorUtils;
+import org.jetlinks.core.metadata.ConfigMetadata;
+import org.jetlinks.core.metadata.DefaultConfigMetadata;
+import org.jetlinks.core.metadata.types.IntType;
+import org.jetlinks.core.metadata.types.StringType;
+import org.jetlinks.community.notify.*;
+import org.jetlinks.community.notify.template.TemplateManager;
+import org.jetlinks.community.notify.template.TemplateProperties;
+import org.jetlinks.community.notify.template.TemplateProvider;
+import org.jetlinks.community.notify.voice.VoiceProvider;
+import reactor.core.publisher.Mono;
+
+import javax.annotation.Nonnull;
+
+/**
+ * <a href="https://help.aliyun.com/document_detail/114035.html?spm=a2c4g.11186623.6.561.3d1b3c2dGMXAmk">
+ * 阿里云语音通知服务
+ * </a>
+ *
+ * @author zhouhao
+ * @since 1.0
+ */
+@Slf4j
+@AllArgsConstructor
+public class AliyunNotifierProvider implements NotifierProvider, TemplateProvider {
+
+    private TemplateManager templateManager;
+
+    @Nonnull
+    @Override
+    public Provider getProvider() {
+        return VoiceProvider.aliyun;
+    }
+
+    public static final DefaultConfigMetadata templateConfig = new DefaultConfigMetadata("阿里云语音模版",
+            "https://help.aliyun.com/document_detail/114035.html?spm=a2c4g.11186623.6.561.3d1b3c2dGMXAmk")
+            .add("ttsCode", "模版ID", "ttsCode", new StringType())
+            .add("calledShowNumbers", "被叫显号", "", new StringType())
+            .add("CalledNumber", "被叫号码", "", new StringType())
+            .add("PlayTimes", "播放次数", "", new IntType());
+
+    public static final DefaultConfigMetadata notifierConfig = new DefaultConfigMetadata("阿里云通知配置",
+            "https://help.aliyun.com/document_detail/114035.html?spm=a2c4g.11186623.6.561.3d1b3c2dGMXAmk")
+            .add("regionId", "regionId", "regionId", new StringType())
+            .add("accessKeyId", "accessKeyId", "", new StringType())
+            .add("secret", "secret", "", new StringType());
+
+    @Override
+    public ConfigMetadata getTemplateConfigMetadata() {
+        return templateConfig;
+    }
+
+    @Override
+    public ConfigMetadata getNotifierConfigMetadata() {
+        return notifierConfig;
+    }
+
+    @Override
+    public Mono<AliyunVoiceTemplate> createTemplate(TemplateProperties properties) {
+        return Mono.fromCallable(() -> ValidatorUtils.tryValidate(JSON.parseObject(properties.getTemplate(), AliyunVoiceTemplate.class)));
+    }
+
+    @Nonnull
+    @Override
+    public NotifyType getType() {
+        return DefaultNotifyType.voice;
+    }
+
+    @Nonnull
+    @Override
+    public Mono<AliyunVoiceNotifier> createNotifier(@Nonnull NotifierProperties properties) {
+        return Mono.fromSupplier(() -> new AliyunVoiceNotifier(properties, templateManager));
+    }
+}

+ 109 - 0
jetlinks-components/notify-component/notify-voice/src/main/java/org/jetlinks/community/notify/voice/aliyun/AliyunVoiceNotifier.java

@@ -0,0 +1,109 @@
+package org.jetlinks.community.notify.voice.aliyun;
+
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONObject;
+import com.aliyuncs.CommonRequest;
+import com.aliyuncs.CommonResponse;
+import com.aliyuncs.DefaultAcsClient;
+import com.aliyuncs.IAcsClient;
+import com.aliyuncs.http.MethodType;
+import com.aliyuncs.profile.DefaultProfile;
+import com.aliyuncs.profile.IClientProfile;
+import lombok.extern.slf4j.Slf4j;
+import org.hswebframework.web.exception.BusinessException;
+import org.hswebframework.web.logger.ReactiveLogger;
+import org.jetlinks.core.Values;
+import org.jetlinks.community.notify.*;
+import org.jetlinks.community.notify.template.TemplateManager;
+import org.jetlinks.community.notify.voice.VoiceProvider;
+import reactor.core.publisher.Mono;
+
+import javax.annotation.Nonnull;
+import java.util.Map;
+import java.util.Objects;
+
+@Slf4j
+public class AliyunVoiceNotifier extends AbstractNotifier<AliyunVoiceTemplate> {
+
+    private IAcsClient client;
+    private String domain = "dyvmsapi.aliyuncs.com";
+    private String regionId = "cn-hangzhou";
+    private int connectTimeout = 1000;
+    private int readTimeout = 5000;
+
+    public AliyunVoiceNotifier(NotifierProperties profile, TemplateManager templateManager) {
+        super(templateManager);
+        Map<String, Object> config = profile.getConfiguration();
+        DefaultProfile defaultProfile = DefaultProfile.getProfile(
+                this.regionId = (String) Objects.requireNonNull(config.get("regionId"), "regionId不能为空"),
+                (String) Objects.requireNonNull(config.get("accessKeyId"), "accessKeyId不能为空"),
+                (String) Objects.requireNonNull(config.get("secret"), "secret不能为空")
+        );
+        this.client = new DefaultAcsClient(defaultProfile);
+        this.domain = (String) config.getOrDefault("domain", "dyvmsapi.aliyuncs.com");
+    }
+
+    public AliyunVoiceNotifier(IClientProfile profile, TemplateManager templateManager) {
+        this(new DefaultAcsClient(profile), templateManager);
+    }
+
+    public AliyunVoiceNotifier(IAcsClient client, TemplateManager templateManager) {
+        super(templateManager);
+        this.client = client;
+    }
+
+    @Override
+    @Nonnull
+    public NotifyType getType() {
+        return DefaultNotifyType.voice;
+    }
+
+    @Nonnull
+    @Override
+    public Provider getProvider() {
+        return VoiceProvider.aliyun;
+    }
+
+    @Override
+    @Nonnull
+    public Mono<Void> send(@Nonnull AliyunVoiceTemplate template, @Nonnull Values context) {
+
+        return Mono.<Void>defer(() -> {
+            try {
+                CommonRequest request = new CommonRequest();
+                request.setMethod(MethodType.POST);
+                request.setDomain(domain);
+                request.setVersion("2017-05-25");
+                request.setAction("SingleCallByTts");
+                request.setConnectTimeout(connectTimeout);
+                request.setReadTimeout(readTimeout);
+                request.putQueryParameter("RegionId", regionId);
+                request.putQueryParameter("CalledShowNumber", template.getCalledShowNumbers());
+                request.putQueryParameter("CalledNumber", template.getCalledNumber());
+                request.putQueryParameter("TtsCode", template.getTtsCode());
+                request.putQueryParameter("PlayTimes", String.valueOf(template.getPlayTimes()));
+                request.putQueryParameter("TtsParam", template.createTtsParam(context.getAllValues()));
+
+                CommonResponse response = client.getCommonResponse(request);
+
+                log.info("发起语音通知完成 {}:{}", response.getHttpResponse().getStatus(), response.getData());
+
+                JSONObject json = JSON.parseObject(response.getData());
+                if (!"ok".equalsIgnoreCase(json.getString("Code"))) {
+                    return Mono.error(new BusinessException(json.getString("Message"), json.getString("Code")));
+                }
+            } catch (Exception e) {
+                return Mono.error(e);
+            }
+            return Mono.empty();
+        }).doOnEach(ReactiveLogger.onError(err -> {
+            log.info("发起语音通知失败", err);
+        }));
+    }
+
+    @Override
+    @Nonnull
+    public Mono<Void> close() {
+        return Mono.fromRunnable(client::shutdown);
+    }
+}

+ 37 - 0
jetlinks-components/notify-component/notify-voice/src/main/java/org/jetlinks/community/notify/voice/aliyun/AliyunVoiceTemplate.java

@@ -0,0 +1,37 @@
+package org.jetlinks.community.notify.voice.aliyun;
+
+import com.alibaba.fastjson.JSON;
+import lombok.Getter;
+import lombok.Setter;
+import org.jetlinks.community.notify.template.Template;
+
+import javax.validation.constraints.NotBlank;
+import java.util.Map;
+
+/**
+ * 阿里云通知模版
+ *
+ * https://help.aliyun.com/document_detail/114035.html?spm=a2c4g.11186623.6.561.3d1b3c2dGMXAmk
+ */
+@Getter
+@Setter
+public class AliyunVoiceTemplate implements Template {
+
+    @NotBlank(message = "[ttsCode]不能为空")
+    private String ttsCode;
+
+    @NotBlank(message = "[calledShowNumbers]不能为空")
+    private String calledShowNumbers;
+
+    @NotBlank(message = "[calledNumber]不能为空")
+    private String calledNumber;
+
+    private int playTimes = 1;
+
+    private Map<String,String> ttsParam;
+
+    public String createTtsParam(Map<String,Object> ctx){
+
+        return JSON.toJSONString(ctx);
+    }
+}

+ 67 - 0
jetlinks-components/notify-component/notify-voice/src/test/java/org/jetlinks/community/notify/voice/supports/aliyun/AliyunVoiceNotifierTest.java

@@ -0,0 +1,67 @@
+/*
+package org.jetlinks.community.notify.voice.supports.aliyun;
+
+import org.jetlinks.core.Values;
+import org.jetlinks.community.notify.NotifierProperties;
+import org.jetlinks.community.notify.NotifyType;
+import org.jetlinks.community.notify.template.Template;
+import org.jetlinks.community.notify.template.TemplateManager;
+import org.jetlinks.community.notify.template.TemplateProperties;
+import org.jetlinks.community.notify.voice.aliyun.AliyunVoiceNotifier;
+import org.jetlinks.community.notify.voice.aliyun.AliyunVoiceTemplate;
+import org.junit.jupiter.api.Test;
+import reactor.core.publisher.Mono;
+import reactor.test.StepVerifier;
+
+import javax.annotation.Nonnull;
+import java.util.HashMap;
+
+class AliyunVoiceNotifierTest {
+
+    @Test
+    void test() {
+        NotifierProperties properties = new NotifierProperties();
+        properties.setId("test");
+        properties.setName("test");
+        properties.setProvider("aliyun");
+        properties.setConfiguration(new HashMap<String, Object>() {{
+            put("regionId", "cn-hangzhou");
+            put("accessKeyId", "LTAI4Fj2oYhjnYYMAwTSj1F8");
+            put("secret", "tea2nKEKM635IsPDud0OaZ5aIHM8eG");
+        }});
+
+        AliyunVoiceNotifier notifier = new AliyunVoiceNotifier(
+                properties, new TemplateManager() {
+            @Nonnull
+            @Override
+            public Mono<? extends Template> getTemplate(@Nonnull NotifyType type, @Nonnull String id) {
+                return Mono.empty();
+            }
+
+            @Nonnull
+            @Override
+            public Mono<? extends Template> createTemplate(@Nonnull NotifyType type, @Nonnull TemplateProperties properties) {
+                return Mono.empty();
+            }
+
+            @Override
+            public Mono<Void> reload(String templateId) {
+                return Mono.empty();
+            }
+        }
+        );
+        AliyunVoiceTemplate template = new AliyunVoiceTemplate();
+        template.setCalledShowNumbers("02566040637");
+        template.setPlayTimes(1);
+        template.setTtsCode("TTS_176535661");
+        template.setCalledNumber("18502314099");
+        notifier.send(template, Values.of(new HashMap<String, Object>() {
+            {
+                put("busi","测试");
+                put("address","测试");
+
+            }
+        })).as(StepVerifier::create)
+                .verifyComplete();
+    }
+}*/

+ 28 - 0
jetlinks-components/notify-component/notify-wechat/pom.xml

@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <artifactId>notify-component</artifactId>
+        <groupId>org.jetlinks.community</groupId>
+        <version>1.0-SNAPSHOT</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>notify-wechat</artifactId>
+
+    <dependencies>
+
+        <dependency>
+            <groupId>org.jetlinks.community</groupId>
+            <artifactId>notify-core</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-webflux</artifactId>
+        </dependency>
+
+    </dependencies>
+</project>

+ 19 - 0
jetlinks-components/notify-component/notify-wechat/src/main/java/org/jetlinks/community/notify/wechat/WechatCorpProperties.java

@@ -0,0 +1,19 @@
+package org.jetlinks.community.notify.wechat;
+
+import lombok.Getter;
+import lombok.Setter;
+
+import javax.validation.constraints.NotBlank;
+
+@Getter
+@Setter
+public class WechatCorpProperties {
+
+    @NotBlank(message = "corpId不能为空")
+    private String corpId;
+
+    @NotBlank(message = "corpSecret不能为空")
+    private String corpSecret;
+
+
+}

+ 102 - 0
jetlinks-components/notify-component/notify-wechat/src/main/java/org/jetlinks/community/notify/wechat/WechatMessageTemplate.java

@@ -0,0 +1,102 @@
+package org.jetlinks.community.notify.wechat;
+
+import com.alibaba.fastjson.JSONObject;
+import lombok.Getter;
+import lombok.Setter;
+import lombok.SneakyThrows;
+import org.hswebframework.web.utils.ExpressionUtils;
+import org.jetlinks.core.Values;
+import org.jetlinks.community.notify.template.Template;
+import org.springframework.util.StringUtils;
+import org.springframework.web.reactive.function.BodyInserters;
+import org.springframework.web.util.UriComponentsBuilder;
+
+import javax.validation.constraints.NotBlank;
+import java.util.Collections;
+
+@Getter
+@Setter
+public class WechatMessageTemplate implements Template {
+
+    /**
+     * 应用ID
+     */
+    @NotBlank(message = "[agentId]不能为空")
+    private String agentId;
+
+    private String toUser;
+
+    private String toParty;
+
+    private String toTag;
+
+    @NotBlank(message = "[message]不能为空")
+    private String message;
+
+
+    @SneakyThrows
+    public BodyInserters.FormInserter<String> createFormInserter(BodyInserters.FormInserter<String> inserter, Values context) {
+        inserter.with("agentid", this.getAgentId())
+                .with("msgtype","text")
+                .with("text",this.createMessage(context));
+        if (StringUtils.hasText(toUser)) {
+            inserter.with("touser", this.createUserIdList(context));
+        }
+        if (StringUtils.hasText(toParty)) {
+            inserter.with("toparty", this.createDepartmentIdList(context));
+        }
+        return inserter;
+
+    }
+
+    public String createJsonRequest(Values context){
+        JSONObject json=new JSONObject();
+        json.put("agentid",getAgentId());
+        json.put("msgtype","text");
+        json.put("text",Collections.singletonMap("content",ExpressionUtils.analytical(message, context.getAllValues(), "spel")));
+
+        if (StringUtils.hasText(toUser)) {
+            json.put("touser", this.createUserIdList(context));
+        }
+        if (StringUtils.hasText(toParty)) {
+            json.put("toparty", this.createDepartmentIdList(context));
+        }
+
+        return json.toJSONString();
+    }
+
+
+    public UriComponentsBuilder createUriParameter(UriComponentsBuilder builder, Values context){
+        builder.queryParam("agentid", this.getAgentId())
+                .queryParam("msgtype","text")
+                .queryParam("text",this.createMessage(context));
+        if (StringUtils.hasText(toUser)) {
+            builder.queryParam("touser", this.createUserIdList(context));
+        }
+        if (StringUtils.hasText(toParty)) {
+            builder.queryParam("toparty", this.createDepartmentIdList(context));
+        }
+        return builder;
+    }
+
+    public String createUserIdList(Values context) {
+        if (StringUtils.isEmpty(toUser)) {
+            return toUser;
+        }
+        return ExpressionUtils.analytical(toUser, context.getAllValues(), "spel");
+    }
+
+    public String createDepartmentIdList(Values context) {
+        if (StringUtils.isEmpty(toParty)) {
+            return toParty;
+        }
+        return ExpressionUtils.analytical(toParty, context.getAllValues(), "spel");
+    }
+
+    public String createMessage(Values context) {
+        JSONObject json = new JSONObject();
+        json.put("content", ExpressionUtils.analytical(message, context.getAllValues(), "spel"));
+        return json.toJSONString();
+    }
+
+}

+ 77 - 0
jetlinks-components/notify-component/notify-wechat/src/main/java/org/jetlinks/community/notify/wechat/WechatNotifierProvider.java

@@ -0,0 +1,77 @@
+package org.jetlinks.community.notify.wechat;
+
+import com.alibaba.fastjson.JSON;
+import org.hswebframework.web.bean.FastBeanCopier;
+import org.hswebframework.web.validator.ValidatorUtils;
+import org.jetlinks.core.metadata.ConfigMetadata;
+import org.jetlinks.core.metadata.DefaultConfigMetadata;
+import org.jetlinks.core.metadata.types.StringType;
+import org.jetlinks.community.ConfigMetadataConstants;
+import org.jetlinks.community.notify.*;
+import org.jetlinks.community.notify.template.TemplateManager;
+import org.jetlinks.community.notify.template.TemplateProperties;
+import org.jetlinks.community.notify.template.TemplateProvider;
+import org.springframework.stereotype.Component;
+import org.springframework.web.reactive.function.client.WebClient;
+import reactor.core.publisher.Mono;
+
+import javax.annotation.Nonnull;
+
+@Component
+public class WechatNotifierProvider implements NotifierProvider, TemplateProvider {
+
+    private WebClient client = WebClient.create();
+
+    private final TemplateManager templateManager;
+
+    public WechatNotifierProvider(TemplateManager templateManager) {
+        this.templateManager = templateManager;
+    }
+
+    public static final DefaultConfigMetadata notifierConfig = new DefaultConfigMetadata("通知配置", "")
+            .add("corpId", "corpId", "", new StringType().expand(ConfigMetadataConstants.required.value(true)))
+            .add("corpSecret", "corpSecret", "", new StringType());
+
+    public static final DefaultConfigMetadata templateConfig = new DefaultConfigMetadata("模版配置", "")
+            .add("agentId", "应用ID", "", new StringType().expand(ConfigMetadataConstants.required.value(true)))
+            .add("toUser", "收信人ID", "与部门ID不能同时为空", new StringType())
+            .add("toParty", "收信部门ID", "与收信人ID不能同时为空", new StringType())
+            .add("toTag", "按标签推送", "", new StringType())
+            .add("message", "内容", "最大不超过500字", new StringType().expand(ConfigMetadataConstants.maxLength.value(500L)));
+
+    @Nonnull
+    @Override
+    public NotifyType getType() {
+        return DefaultNotifyType.weixin;
+    }
+
+    @Nonnull
+    @Override
+    public Provider getProvider() {
+        return WechatProvider.corpMessage;
+    }
+
+    @Override
+    public Mono<WechatMessageTemplate> createTemplate(TemplateProperties properties) {
+        return Mono.fromSupplier(() -> ValidatorUtils.tryValidate(JSON.parseObject(properties.getTemplate(), WechatMessageTemplate.class)));
+    }
+
+    @Nonnull
+    @Override
+    public Mono<WeixinCorpNotifier> createNotifier(@Nonnull NotifierProperties properties) {
+        return Mono.defer(() -> {
+            WechatCorpProperties wechatCorpProperties = FastBeanCopier.copy(properties.getConfiguration(), new WechatCorpProperties());
+            return Mono.just(new WeixinCorpNotifier(client, ValidatorUtils.tryValidate(wechatCorpProperties), templateManager));
+        });
+    }
+
+    @Override
+    public ConfigMetadata getNotifierConfigMetadata() {
+        return notifierConfig;
+    }
+
+    @Override
+    public ConfigMetadata getTemplateConfigMetadata() {
+        return templateConfig;
+    }
+}

+ 20 - 0
jetlinks-components/notify-component/notify-wechat/src/main/java/org/jetlinks/community/notify/wechat/WechatProvider.java

@@ -0,0 +1,20 @@
+package org.jetlinks.community.notify.wechat;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import org.jetlinks.community.notify.Provider;
+
+@Getter
+@AllArgsConstructor
+public enum WechatProvider implements Provider {
+    corpMessage("微信企业消息通知")
+    ;
+
+    private String name;
+
+    @Override
+    public String getId() {
+        return name();
+    }
+
+}

+ 120 - 0
jetlinks-components/notify-component/notify-wechat/src/main/java/org/jetlinks/community/notify/wechat/WeixinCorpNotifier.java

@@ -0,0 +1,120 @@
+package org.jetlinks.community.notify.wechat;
+
+import lombok.extern.slf4j.Slf4j;
+import org.hswebframework.web.exception.BusinessException;
+import org.jetlinks.core.Values;
+import org.jetlinks.community.notify.AbstractNotifier;
+import org.jetlinks.community.notify.DefaultNotifyType;
+import org.jetlinks.community.notify.NotifyType;
+import org.jetlinks.community.notify.Provider;
+import org.jetlinks.community.notify.template.TemplateManager;
+import org.springframework.http.MediaType;
+import org.springframework.web.reactive.function.BodyInserters;
+import org.springframework.web.reactive.function.client.WebClient;
+import org.springframework.web.util.UriComponentsBuilder;
+import reactor.core.publisher.Mono;
+
+import javax.annotation.Nonnull;
+import java.time.Duration;
+import java.util.HashMap;
+import java.util.concurrent.atomic.AtomicReference;
+
+@Slf4j
+public class WeixinCorpNotifier extends AbstractNotifier<WechatMessageTemplate> {
+
+    private AtomicReference<String> accessToken = new AtomicReference<>();
+
+    private long refreshTokenTime;
+
+    private long tokenTimeOut = Duration.ofSeconds(7000).toMillis();
+
+    private WebClient client;
+
+    private static final String tokenApi = "https://qyapi.weixin.qq.com/cgi-bin/gettoken";
+
+    private static final String notify = "https://qyapi.weixin.qq.com/cgi-bin/message/send";
+
+    private WechatCorpProperties properties;
+
+    public WeixinCorpNotifier(WebClient client, WechatCorpProperties properties, TemplateManager templateManager) {
+        super(templateManager);
+        this.client = client;
+        this.properties = properties;
+    }
+
+    @Nonnull
+    @Override
+    public NotifyType getType() {
+        return DefaultNotifyType.weixin;
+    }
+
+    @Nonnull
+    @Override
+    public Provider getProvider() {
+        return WechatProvider.corpMessage;
+    }
+
+    @Nonnull
+    @Override
+    public Mono<Void> send(@Nonnull WechatMessageTemplate template, @Nonnull Values context) {
+        return getToken()
+                .flatMap(token ->
+                        client.post()
+                                .uri(UriComponentsBuilder.fromUriString(notify).queryParam("access_token",token).toUriString())
+                                .contentType(MediaType.APPLICATION_JSON)
+                                .body(BodyInserters.fromValue(template.createJsonRequest(context)))
+                                .exchange()
+                                .flatMap(clientResponse -> clientResponse.bodyToMono(HashMap.class))
+                                .as(this::checkResult))
+                .then();
+    }
+
+    private Mono<HashMap> checkResult(Mono<HashMap> msg) {
+        return msg.doOnNext(map -> {
+            String code = String.valueOf(map.get("errcode"));
+            if ("0".equals(code)) {
+                log.info("发送微信企业通知成功");
+            } else {
+                log.warn("发送微信企业通知失败:{}", map);
+                throw new BusinessException("发送微信企业通知失败:" + map.get("errmsg"), code);
+            }
+        });
+    }
+
+    private Mono<String> getToken() {
+        if (System.currentTimeMillis() - refreshTokenTime > tokenTimeOut || accessToken.get() == null) {
+            return requestToken();
+        }
+        return Mono.just(accessToken.get());
+    }
+
+    private Mono<String> requestToken() {
+        return client
+                .get()
+                .uri(UriComponentsBuilder.fromUriString(tokenApi)
+                        .queryParam("corpid", properties.getCorpId())
+                        .queryParam("corpsecret", properties.getCorpSecret())
+                        .build().toUri())
+                .exchange()
+                .flatMap(resp -> resp.bodyToMono(HashMap.class))
+                .map(map -> {
+                    if (map.containsKey("access_token")) {
+                        return map.get("access_token");
+                    }
+                    throw new BusinessException("获取Token失败:" + map.get("errmsg"), String.valueOf(map.get("errcode")));
+                })
+                .cast(String.class)
+                .doOnNext((r) -> {
+                    refreshTokenTime = System.currentTimeMillis();
+                    accessToken.set(r);
+                });
+    }
+
+    @Nonnull
+    @Override
+    public Mono<Void> close() {
+        accessToken.set(null);
+        refreshTokenTime = 0;
+        return Mono.empty();
+    }
+}

+ 38 - 0
jetlinks-components/notify-component/notify-wechat/src/test/java/org/jetlinks/community/notify/wechat/WeixinCorpNotifierTest.java

@@ -0,0 +1,38 @@
+package org.jetlinks.community.notify.wechat;
+
+import org.jetlinks.core.Values;
+import org.junit.jupiter.api.Test;
+import org.springframework.web.reactive.function.client.WebClient;
+import reactor.test.StepVerifier;
+
+import java.util.HashMap;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+class WeixinCorpNotifierTest {
+
+
+    @Test
+    void test(){
+        WechatCorpProperties properties=new WechatCorpProperties();
+        properties.setCorpId("wwd7e935e2867897122");
+        properties.setCorpSecret("c0qeMSJK2pJee47Bg4kguBmd1JCBt2OFfsNFVGjc1i0");
+
+        WechatMessageTemplate messageTemplate=new WechatMessageTemplate();
+
+        messageTemplate.setAgentId("1000002");
+        messageTemplate.setMessage("test"+System.currentTimeMillis());
+        messageTemplate.setToUser("zhouhao");
+
+        WeixinCorpNotifier notifier=new WeixinCorpNotifier(
+                WebClient.builder().build(),properties,null
+        );
+
+        notifier.send(messageTemplate, Values.of(new HashMap<>()))
+                .as(StepVerifier::create)
+                .expectComplete()
+                .verify();
+
+    }
+
+}

+ 25 - 0
jetlinks-components/notify-component/pom.xml

@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <artifactId>jetlinks-components</artifactId>
+        <groupId>org.jetlinks.community</groupId>
+        <version>1.0-SNAPSHOT</version>
+        <relativePath>../pom.xml</relativePath>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>notify-component</artifactId>
+    <packaging>pom</packaging>
+    <modules>
+        <module>notify-core</module>
+        <module>notify-sms</module>
+        <module>notify-email</module>
+        <module>notify-wechat</module>
+        <module>notify-dingtalk</module>
+        <module>notify-voice</module>
+    </modules>
+
+
+</project>

+ 1 - 0
jetlinks-components/pom.xml

@@ -19,6 +19,7 @@
         <module>timeseries-component</module>
         <module>dashboard-component</module>
         <module>common-component</module>
+        <module>notify-component</module>
     </modules>
 
     <artifactId>jetlinks-components</artifactId>

+ 0 - 27
jetlinks-manager/authentication-manager/.gitignore

@@ -1,27 +0,0 @@
-**/pom.xml.versionsBackup
-**/target/
-**/out/
-*.class
-# Mobile Tools for Java (J2ME)
-.mtj.tmp/
-.idea/
-/nbproject
-*.ipr
-*.iws
-*.iml
-
-# Package Files #
-*.jar
-*.war
-*.ear
-*.log
-# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
-hs_err_pid*
-**/transaction-logs/
-!/.mvn/wrapper/maven-wrapper.jar
-/data/
-*.db
-/static/
-/upload
-/ui/upload/
-docker/data

+ 0 - 27
jetlinks-manager/device-manager/.gitignore

@@ -1,27 +0,0 @@
-**/pom.xml.versionsBackup
-**/target/
-**/out/
-*.class
-# Mobile Tools for Java (J2ME)
-.mtj.tmp/
-.idea/
-/nbproject
-*.ipr
-*.iws
-*.iml
-
-# Package Files #
-*.jar
-*.war
-*.ear
-*.log
-# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
-hs_err_pid*
-**/transaction-logs/
-!/.mvn/wrapper/maven-wrapper.jar
-/data/
-*.db
-/static/
-/upload
-/ui/upload/
-docker/data

+ 4 - 5
jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/measurements/DevicePropertyMeasurement.java

@@ -4,10 +4,7 @@ import org.hswebframework.web.api.crud.entity.QueryParamEntity;
 import org.jetlinks.core.message.property.ReadPropertyMessageReply;
 import org.jetlinks.core.message.property.ReportPropertyMessage;
 import org.jetlinks.core.message.property.WritePropertyMessageReply;
-import org.jetlinks.core.metadata.ConfigMetadata;
-import org.jetlinks.core.metadata.DataType;
-import org.jetlinks.core.metadata.DefaultConfigMetadata;
-import org.jetlinks.core.metadata.PropertyMetadata;
+import org.jetlinks.core.metadata.*;
 import org.jetlinks.community.dashboard.*;
 import org.jetlinks.community.dashboard.supports.StaticMeasurement;
 import org.jetlinks.community.device.message.DeviceMessageUtils;
@@ -43,8 +40,10 @@ class DevicePropertyMeasurement extends StaticMeasurement {
 
     Map<String, Object> createValue(Object value) {
         Map<String, Object> values = new HashMap<>();
+        DataType type = metadata.getValueType();
+        value = type instanceof Converter ? ((Converter<?>) type).convert(value) : value;
         values.put("value", value);
-        values.put("formatValue", metadata.getValueType().format(value));
+        values.put("formatValue", type.format(value));
         return values;
     }
 

+ 0 - 29
jetlinks-manager/network-manager/.gitignore

@@ -1,29 +0,0 @@
-**/pom.xml.versionsBackup
-**/target/
-**/out/
-*.class
-# Mobile Tools for Java (J2ME)
-.mtj.tmp/
-.idea/
-/nbproject
-*.ipr
-*.iws
-*.iml
-
-# Package Files #
-*.jar
-*.war
-*.ear
-*.log
-# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
-hs_err_pid*
-**/transaction-logs/
-!/.mvn/wrapper/maven-wrapper.jar
-/data/
-*.db
-/static/
-/upload
-/ui/upload/
-docker/data
-!ip2region.db
-!device-simulator.jar

+ 1 - 0
jetlinks-manager/notify-manager/README.md

@@ -0,0 +1 @@
+Notify Manager

+ 125 - 0
jetlinks-manager/notify-manager/pom.xml

@@ -0,0 +1,125 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <groupId>org.jetlinks.community</groupId>
+        <artifactId>jetlinks-manager</artifactId>
+        <version>1.0-SNAPSHOT</version>
+        <relativePath>../pom.xml</relativePath>
+    </parent>
+    <artifactId>notify-manager</artifactId>
+
+    <properties>
+        <hsweb.framework.version>4.0.0-SNAPSHOT</hsweb.framework.version>
+    </properties>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.jetlinks.community</groupId>
+            <artifactId>common-component</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.hswebframework.web</groupId>
+            <artifactId>hsweb-authorization-api</artifactId>
+            <version>${hsweb.framework.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.jetlinks</groupId>
+            <artifactId>jetlinks-supports</artifactId>
+            <version>${jetlinks.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.jetlinks</groupId>
+            <artifactId>jetlinks-core</artifactId>
+            <version>${jetlinks.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.hswebframework.web</groupId>
+            <artifactId>hsweb-starter</artifactId>
+            <version>${hsweb.framework.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>io.r2dbc</groupId>
+            <artifactId>r2dbc-h2</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.hswebframework</groupId>
+            <artifactId>hsweb-easy-orm-rdb</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.jetlinks.community</groupId>
+            <artifactId>notify-email</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.jetlinks.community</groupId>
+            <artifactId>notify-voice</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.jetlinks.community</groupId>
+            <artifactId>notify-sms</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+
+    </dependencies>
+
+    <repositories>
+        <repository>
+            <id>aliyun-nexus</id>
+            <name>aliyun</name>
+            <url>http://maven.aliyun.com/nexus/content/groups/public/</url>
+        </repository>
+
+        <repository>
+            <id>hsweb-nexus</id>
+            <name>Nexus Release Repository</name>
+            <url>http://nexus.hsweb.me/content/groups/public/</url>
+            <snapshots>
+                <enabled>true</enabled>
+                <updatePolicy>always</updatePolicy>
+            </snapshots>
+        </repository>
+
+        <repository>
+            <id>spring.io</id>
+            <name>spring</name>
+            <url>https://repo.spring.io/milestone</url>
+        </repository>
+
+    </repositories>
+
+    <distributionManagement>
+        <repository>
+            <id>releases</id>
+            <name>Nexus Release Repository</name>
+            <url>http://nexus.hsweb.me/content/repositories/releases/</url>
+        </repository>
+        <snapshotRepository>
+            <id>snapshots</id>
+            <name>Nexus Snapshot Repository</name>
+            <url>http://nexus.hsweb.me/content/repositories/snapshots/</url>
+        </snapshotRepository>
+    </distributionManagement>
+
+    <pluginRepositories>
+        <pluginRepository>
+            <id>aliyun-nexus</id>
+            <name>aliyun</name>
+            <url>http://maven.aliyun.com/nexus/content/groups/public/</url>
+        </pluginRepository>
+    </pluginRepositories>
+</project>

+ 64 - 0
jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/entity/NotifyConfigEntity.java

@@ -0,0 +1,64 @@
+package org.jetlinks.community.notify.manager.entity;
+
+import lombok.Getter;
+import lombok.Setter;
+import org.hswebframework.ezorm.rdb.mapping.annotation.ColumnType;
+import org.hswebframework.ezorm.rdb.mapping.annotation.JsonCodec;
+import org.hswebframework.web.api.crud.entity.GenericEntity;
+import org.hswebframework.web.crud.annotation.EnableEntityEvent;
+import org.jetlinks.community.notify.NotifierProperties;
+
+import javax.persistence.Column;
+import javax.persistence.Table;
+import java.sql.JDBCType;
+import java.util.HashMap;
+import java.util.Map;
+
+@Table(name = "notify_config")
+@Getter
+@Setter
+@EnableEntityEvent
+public class NotifyConfigEntity extends GenericEntity<String> {
+
+    /**
+     * 配置名称
+     */
+    @Column
+    private String name;
+
+    /**
+     * 通知类型
+     */
+    @Column
+    private String type;
+
+    /**
+     * 服务提供商
+     */
+    @Column
+    private String provider;
+
+    /**
+     * 描述
+     */
+    @Column
+    private String description;
+
+    /**
+     * 配置详情
+     */
+    @Column
+    @JsonCodec
+    @ColumnType(jdbcType = JDBCType.CLOB)
+    private Map<String, Object> configuration;
+
+    public NotifierProperties toProperties() {
+        NotifierProperties properties = new NotifierProperties();
+        properties.setProvider(provider);
+        properties.setId(getId());
+        properties.setType(type);
+        properties.setConfiguration(configuration == null ? new HashMap<>() : configuration);
+        properties.setName(name);
+        return properties;
+    }
+}

+ 51 - 0
jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/entity/NotifyTemplateEntity.java

@@ -0,0 +1,51 @@
+package org.jetlinks.community.notify.manager.entity;
+
+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.web.api.crud.entity.GenericEntity;
+import org.hswebframework.web.crud.annotation.EnableEntityEvent;
+import org.jetlinks.community.notify.template.TemplateProperties;
+
+import javax.persistence.Column;
+import javax.persistence.Table;
+import java.sql.JDBCType;
+
+/**
+ * @author wangzheng
+ * @author zhouhao
+ * @since 1.0
+ */
+@Setter
+@Getter
+@Table(name = "notify_template")
+@EnableEntityEvent
+public class NotifyTemplateEntity extends GenericEntity<String> {
+
+    @Column
+    @Comment("通知类型")
+    private String type;
+
+    @Column
+    @Comment("通知服务商")
+    private String provider;
+
+    @Column
+    @Comment("模板名称")
+    private String name;
+
+    @Comment("模板内容")
+    @Column
+    @ColumnType(jdbcType = JDBCType.CLOB)
+    private String template;
+
+
+    public TemplateProperties toTemplateProperties() {
+        TemplateProperties properties = new TemplateProperties();
+        properties.setProvider(provider);
+        properties.setType(type);
+        properties.setTemplate(template);
+        return properties;
+    }
+}

+ 25 - 0
jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/service/DefaultNotifyConfigManager.java

@@ -0,0 +1,25 @@
+package org.jetlinks.community.notify.manager.service;
+
+import org.jetlinks.community.notify.NotifierProperties;
+import org.jetlinks.community.notify.NotifyConfigManager;
+import org.jetlinks.community.notify.NotifyType;
+import org.jetlinks.community.notify.manager.entity.NotifyConfigEntity;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import reactor.core.publisher.Mono;
+
+import javax.annotation.Nonnull;
+
+@Service
+public class DefaultNotifyConfigManager implements NotifyConfigManager {
+
+    @Autowired
+    private NotifyConfigService configService;
+
+    @Nonnull
+    @Override
+    public Mono<NotifierProperties> getNotifyConfig(@Nonnull NotifyType notifyType, @Nonnull String configId) {
+        return configService.findById(configId)
+                .map(NotifyConfigEntity::toProperties);
+    }
+}

+ 35 - 0
jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/service/DefaultTemplateManager.java

@@ -0,0 +1,35 @@
+package org.jetlinks.community.notify.manager.service;
+
+import lombok.extern.slf4j.Slf4j;
+import org.jetlinks.community.notify.NotifyType;
+import org.jetlinks.community.notify.manager.entity.NotifyTemplateEntity;
+import org.jetlinks.community.notify.template.AbstractTemplateManager;
+import org.jetlinks.community.notify.template.TemplateProperties;
+import org.jetlinks.community.notify.template.TemplateProvider;
+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;
+
+@Service
+@Slf4j
+public class DefaultTemplateManager extends AbstractTemplateManager implements BeanPostProcessor {
+
+    @Autowired
+    private NotifyTemplateService templateService;
+
+    @Override
+    protected Mono<TemplateProperties> getProperties(NotifyType type, String id) {
+        return templateService.findById(Mono.just(id))
+                .map(NotifyTemplateEntity::toTemplateProperties);
+    }
+    @Override
+    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
+        if (bean instanceof TemplateProvider) {
+            register(((TemplateProvider) bean));
+        }
+        return bean;
+    }
+
+}

+ 65 - 0
jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/service/NotifierCacheManager.java

@@ -0,0 +1,65 @@
+package org.jetlinks.community.notify.manager.service;
+
+import lombok.extern.slf4j.Slf4j;
+import org.hswebframework.web.crud.events.EntityDeletedEvent;
+import org.hswebframework.web.crud.events.EntityModifyEvent;
+import org.jetlinks.community.notify.NotifierManager;
+import org.jetlinks.community.notify.manager.entity.NotifyConfigEntity;
+import org.jetlinks.community.notify.manager.entity.NotifyTemplateEntity;
+import org.jetlinks.community.notify.template.TemplateManager;
+import org.springframework.context.event.EventListener;
+import org.springframework.stereotype.Component;
+import reactor.core.publisher.Flux;
+
+import java.util.List;
+
+@Component
+@Slf4j
+public class NotifierCacheManager {
+
+    private final TemplateManager templateManager;
+
+    private final NotifierManager notifierManager;
+
+    public NotifierCacheManager(TemplateManager templateManager, NotifierManager notifierManager) {
+        this.templateManager = templateManager;
+        this.notifierManager = notifierManager;
+    }
+
+    @EventListener
+    public void handleTemplateModify(EntityModifyEvent<NotifyTemplateEntity> event) {
+        reloadTemplate(event.getBefore());
+    }
+
+    @EventListener
+    public void handleTemplateDelete(EntityDeletedEvent<NotifyTemplateEntity> event) {
+        reloadTemplate(event.getEntity());
+    }
+
+    @EventListener
+    public void handleConfigModify(EntityModifyEvent<NotifyConfigEntity> event) {
+        reloadConfig(event.getBefore());
+    }
+
+    @EventListener
+    public void handleConfigDelete(EntityDeletedEvent<NotifyConfigEntity> event) {
+        reloadConfig(event.getEntity());
+    }
+
+    protected void reloadConfig(List<NotifyConfigEntity> entities) {
+        Flux.fromIterable(entities)
+                .map(NotifyConfigEntity::getId)
+                .doOnNext(id -> log.info("clear notifier config [{}] cache", id))
+                .flatMap(notifierManager::reload)
+                .subscribe();
+    }
+
+    protected void reloadTemplate(List<NotifyTemplateEntity> entities) {
+        Flux.fromIterable(entities)
+                .map(NotifyTemplateEntity::getId)
+                .doOnNext(id -> log.info("clear template [{}] cache", id))
+                .flatMap(templateManager::reload)
+                .subscribe();
+    }
+
+}

+ 14 - 0
jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/service/NotifyConfigService.java

@@ -0,0 +1,14 @@
+package org.jetlinks.community.notify.manager.service;
+
+import org.hswebframework.web.crud.service.GenericReactiveCacheSupportCrudService;
+import org.jetlinks.community.notify.manager.entity.NotifyConfigEntity;
+import org.springframework.stereotype.Service;
+
+@Service
+public class NotifyConfigService extends GenericReactiveCacheSupportCrudService<NotifyConfigEntity, String> {
+
+    @Override
+    public String getCacheName() {
+        return "notify_config";
+    }
+}

+ 17 - 0
jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/service/NotifyTemplateService.java

@@ -0,0 +1,17 @@
+package org.jetlinks.community.notify.manager.service;
+
+import org.hswebframework.web.crud.service.GenericReactiveCrudService;
+import org.jetlinks.community.notify.manager.entity.NotifyTemplateEntity;
+import org.springframework.stereotype.Service;
+
+/**
+ * @author wangzheng
+ * @see
+ * @since 1.0
+ */
+@Service
+public class NotifyTemplateService extends GenericReactiveCrudService<NotifyTemplateEntity, String> {
+
+
+
+}

+ 112 - 0
jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/web/NotifierConfigController.java

@@ -0,0 +1,112 @@
+package org.jetlinks.community.notify.manager.web;
+
+import lombok.AllArgsConstructor;
+import lombok.EqualsAndHashCode;
+import lombok.Getter;
+import lombok.Setter;
+import org.hswebframework.web.authorization.annotation.QueryAction;
+import org.hswebframework.web.authorization.annotation.Resource;
+import org.hswebframework.web.crud.web.reactive.ReactiveServiceCrudController;
+import org.jetlinks.core.metadata.ConfigMetadata;
+import org.jetlinks.community.notify.NotifierProvider;
+import org.jetlinks.community.notify.NotifyType;
+import org.jetlinks.community.notify.manager.entity.NotifyConfigEntity;
+import org.jetlinks.community.notify.manager.service.NotifyConfigService;
+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.stream.Collectors;
+
+@RestController
+@RequestMapping("/notifier/config")
+@Resource(id = "notifier", name = "通知管理")
+public class NotifierConfigController implements ReactiveServiceCrudController<NotifyConfigEntity, String> {
+
+    private final NotifyConfigService notifyConfigService;
+
+    private final List<NotifierProvider> providers;
+
+
+    public NotifierConfigController(NotifyConfigService notifyConfigService,
+                                    List<NotifierProvider> providers) {
+        this.notifyConfigService = notifyConfigService;
+        this.providers = providers;
+    }
+
+
+    @Override
+    public NotifyConfigService getService() {
+        return notifyConfigService;
+    }
+
+    @GetMapping("/{type}/{provider}/metadata")
+    @QueryAction
+    public Mono<ConfigMetadata> getAllTypes(@PathVariable String type,
+                                            @PathVariable String provider) {
+        return Flux.fromIterable(providers)
+            .filter(prov -> prov.getType().getId().equalsIgnoreCase(type) && prov.getProvider().getId().equalsIgnoreCase(provider))
+            .flatMap(prov -> Mono.justOrEmpty(prov.getNotifierConfigMetadata()))
+            .next();
+    }
+
+
+    @GetMapping("/types")
+    @QueryAction
+    public Flux<NotifyTypeInfo> getAllTypes() {
+        return Flux.fromIterable(providers)
+            .collect(Collectors.groupingBy(NotifierProvider::getType))
+            .flatMapIterable(Map::entrySet)
+            .map(en -> {
+                NotifyTypeInfo typeInfo = new NotifyTypeInfo();
+                typeInfo.setId(en.getKey().getId());
+                typeInfo.setName(en.getKey().getName());
+                typeInfo.setProviderInfos(en.getValue().stream().map(ProviderInfo::of).collect(Collectors.toList()));
+                return typeInfo;
+            });
+    }
+
+    /**
+     * 根据类型获取服务商信息
+     *
+     * @param type 类型标识 {@link NotifyType#getId()}
+     * @return 服务商信息
+     */
+    @GetMapping("/type/{type}/providers")
+    @QueryAction
+    public Flux<ProviderInfo> getTypeProviders(@PathVariable String type) {
+        return Flux.fromIterable(providers)
+            .filter(provider -> provider.getType().getId().equals(type))
+            .map(ProviderInfo::of);
+    }
+
+    @Getter
+    @Setter
+    @EqualsAndHashCode(of = "id")
+    public static class NotifyTypeInfo {
+        private String id;
+
+        private String name;
+
+        private List<ProviderInfo> providerInfos;
+
+    }
+
+    @AllArgsConstructor
+    @Getter
+    public static class ProviderInfo {
+        private String type;
+
+        private String id;
+
+        private String name;
+
+        public static ProviderInfo of(NotifierProvider provider) {
+            return new ProviderInfo(provider.getType().getId(), provider.getProvider().getId(), provider.getProvider().getName());
+        }
+
+    }
+
+}

+ 69 - 0
jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/web/NotifierController.java

@@ -0,0 +1,69 @@
+package org.jetlinks.community.notify.manager.web;
+
+import lombok.Getter;
+import lombok.Setter;
+import org.hswebframework.web.authorization.annotation.Resource;
+import org.hswebframework.web.authorization.annotation.ResourceAction;
+import org.hswebframework.web.exception.NotFoundException;
+import org.jetlinks.core.Values;
+import org.jetlinks.community.notify.DefaultNotifyType;
+import org.jetlinks.community.notify.NotifierManager;
+import org.jetlinks.community.notify.NotifyType;
+import org.jetlinks.community.notify.manager.entity.NotifyTemplateEntity;
+import org.jetlinks.community.notify.template.TemplateManager;
+import org.springframework.web.bind.annotation.*;
+import reactor.core.publisher.Mono;
+
+import javax.validation.constraints.NotNull;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.function.Function;
+
+@RestController
+@RequestMapping("/notifier")
+@Resource(id = "notifier", name = "通知管理")
+public class NotifierController {
+
+
+    private final NotifierManager notifierManager;
+
+    private final TemplateManager templateManager;
+
+    public NotifierController(NotifierManager notifierManager, TemplateManager templateManager) {
+        this.notifierManager = notifierManager;
+        this.templateManager = templateManager;
+    }
+
+    /**
+     * 指定通知器以及模版.发送通知.
+     *
+     * @param notifierId 通知器ID
+     * @param mono       发送请求
+     * @return 发送结果
+     */
+    @PostMapping("/{notifierId}/_send")
+    @ResourceAction(id = "send", name = "发送通知")
+    public Mono<Void> sendNotify(@PathVariable String notifierId,
+                                 @RequestBody Mono<SendNotifyRequest> mono) {
+        return mono.flatMap(tem -> {
+            NotifyType type = DefaultNotifyType.valueOf(tem.getTemplate().getType());
+            return Mono.zip(
+                    notifierManager.getNotifier(type, notifierId)
+                            .switchIfEmpty(Mono.error(() -> new NotFoundException("通知器[" + notifierId + "]不存在"))),
+                    templateManager.createTemplate(type, tem.getTemplate().toTemplateProperties()),
+                    (notifier, template) -> notifier.send(template, Values.of(tem.getContext())))
+                    .flatMap(Function.identity());
+        });
+    }
+
+    @Getter
+    @Setter
+    public static class SendNotifyRequest {
+
+        @NotNull
+        private NotifyTemplateEntity template;
+
+        private Map<String, Object> context = new HashMap<>();
+    }
+
+}

+ 56 - 0
jetlinks-manager/notify-manager/src/main/java/org/jetlinks/community/notify/manager/web/NotifierTemplateController.java

@@ -0,0 +1,56 @@
+package org.jetlinks.community.notify.manager.web;
+
+import org.hswebframework.web.authorization.annotation.Authorize;
+import org.hswebframework.web.authorization.annotation.QueryAction;
+import org.hswebframework.web.authorization.annotation.Resource;
+import org.hswebframework.web.crud.service.ReactiveCrudService;
+import org.hswebframework.web.crud.web.reactive.ReactiveServiceCrudController;
+import org.jetlinks.core.metadata.ConfigMetadata;
+import org.jetlinks.community.notify.manager.entity.NotifyTemplateEntity;
+import org.jetlinks.community.notify.manager.service.NotifyTemplateService;
+import org.jetlinks.community.notify.template.TemplateProvider;
+import org.springframework.web.bind.annotation.*;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+import java.util.List;
+
+/**
+ * @author wangzheng
+ * @author zhouhao
+ * @since 1.0
+ */
+@RestController
+@RequestMapping("/notifier/template")
+@Authorize
+@Resource(id = "template", name = "通知模板")
+public class NotifierTemplateController implements ReactiveServiceCrudController<NotifyTemplateEntity, String> {
+
+    private final NotifyTemplateService templateService;
+
+    private final List<TemplateProvider> providers;
+
+
+    public NotifierTemplateController(NotifyTemplateService templateService, List<TemplateProvider> providers) {
+        this.templateService = templateService;
+        this.providers = providers;
+    }
+
+    @Override
+    public ReactiveCrudService<NotifyTemplateEntity, String> getService() {
+        return templateService;
+    }
+
+
+
+    @GetMapping("/{type}/{provider}/config/metadata")
+    @QueryAction
+    public Mono<ConfigMetadata> getAllTypes(@PathVariable String type,
+                                            @PathVariable String provider) {
+        return Flux.fromIterable(providers)
+                .filter(prov -> prov.getType().getId().equalsIgnoreCase(type) && prov.getProvider().getId().equalsIgnoreCase(provider))
+                .flatMap(prov -> Mono.justOrEmpty(prov.getTemplateConfigMetadata()))
+                .next();
+    }
+
+}

+ 1 - 0
jetlinks-manager/pom.xml

@@ -15,6 +15,7 @@
         <module>authentication-manager</module>
         <module>device-manager</module>
         <module>network-manager</module>
+        <module>notify-manager</module>
     </modules>
 
 </project>

+ 6 - 0
jetlinks-standalone/pom.xml

@@ -123,6 +123,12 @@
             <version>${project.version}</version>
         </dependency>
 
+        <dependency>
+            <groupId>${project.groupId}</groupId>
+            <artifactId>notify-manager</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+
         <dependency>
             <groupId>org.jetlinks</groupId>
             <artifactId>jetlinks-supports</artifactId>