新增通知管理
This commit is contained in:
parent
4c26b30032
commit
31af39258d
|
|
@ -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>
|
||||||
|
|
@ -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));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
package org.jetlinks.community.notify;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通知类型.通常使用枚举实现此接口
|
||||||
|
*
|
||||||
|
* @author zhouhao
|
||||||
|
* @see DefaultNotifyType
|
||||||
|
* @since 1.0
|
||||||
|
*/
|
||||||
|
public interface NotifyType {
|
||||||
|
|
||||||
|
String getId();
|
||||||
|
|
||||||
|
String getName();
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
package org.jetlinks.community.notify;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 服务商标识,通常使用枚举实现此接口
|
||||||
|
*
|
||||||
|
* @author zhouhao
|
||||||
|
* @since 1.0
|
||||||
|
*/
|
||||||
|
public interface Provider {
|
||||||
|
/**
|
||||||
|
* @return 唯一标识
|
||||||
|
*/
|
||||||
|
String getId();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return 名称
|
||||||
|
*/
|
||||||
|
String getName();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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");
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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 {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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();
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -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);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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 "测试";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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 |
|
||||||
|
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}*/
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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;
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -19,6 +19,7 @@
|
||||||
<module>timeseries-component</module>
|
<module>timeseries-component</module>
|
||||||
<module>dashboard-component</module>
|
<module>dashboard-component</module>
|
||||||
<module>common-component</module>
|
<module>common-component</module>
|
||||||
|
<module>notify-component</module>
|
||||||
</modules>
|
</modules>
|
||||||
|
|
||||||
<artifactId>jetlinks-components</artifactId>
|
<artifactId>jetlinks-components</artifactId>
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -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,10 +4,7 @@ import org.hswebframework.web.api.crud.entity.QueryParamEntity;
|
||||||
import org.jetlinks.core.message.property.ReadPropertyMessageReply;
|
import org.jetlinks.core.message.property.ReadPropertyMessageReply;
|
||||||
import org.jetlinks.core.message.property.ReportPropertyMessage;
|
import org.jetlinks.core.message.property.ReportPropertyMessage;
|
||||||
import org.jetlinks.core.message.property.WritePropertyMessageReply;
|
import org.jetlinks.core.message.property.WritePropertyMessageReply;
|
||||||
import org.jetlinks.core.metadata.ConfigMetadata;
|
import org.jetlinks.core.metadata.*;
|
||||||
import org.jetlinks.core.metadata.DataType;
|
|
||||||
import org.jetlinks.core.metadata.DefaultConfigMetadata;
|
|
||||||
import org.jetlinks.core.metadata.PropertyMetadata;
|
|
||||||
import org.jetlinks.community.dashboard.*;
|
import org.jetlinks.community.dashboard.*;
|
||||||
import org.jetlinks.community.dashboard.supports.StaticMeasurement;
|
import org.jetlinks.community.dashboard.supports.StaticMeasurement;
|
||||||
import org.jetlinks.community.device.message.DeviceMessageUtils;
|
import org.jetlinks.community.device.message.DeviceMessageUtils;
|
||||||
|
|
@ -43,8 +40,10 @@ class DevicePropertyMeasurement extends StaticMeasurement {
|
||||||
|
|
||||||
Map<String, Object> createValue(Object value) {
|
Map<String, Object> createValue(Object value) {
|
||||||
Map<String, Object> values = new HashMap<>();
|
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("value", value);
|
||||||
values.put("formatValue", metadata.getValueType().format(value));
|
values.put("formatValue", type.format(value));
|
||||||
return values;
|
return values;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
Notify Manager
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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> {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -15,6 +15,7 @@
|
||||||
<module>authentication-manager</module>
|
<module>authentication-manager</module>
|
||||||
<module>device-manager</module>
|
<module>device-manager</module>
|
||||||
<module>network-manager</module>
|
<module>network-manager</module>
|
||||||
|
<module>notify-manager</module>
|
||||||
</modules>
|
</modules>
|
||||||
|
|
||||||
</project>
|
</project>
|
||||||
|
|
@ -123,6 +123,12 @@
|
||||||
<version>${project.version}</version>
|
<version>${project.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>${project.groupId}</groupId>
|
||||||
|
<artifactId>notify-manager</artifactId>
|
||||||
|
<version>${project.version}</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.jetlinks</groupId>
|
<groupId>org.jetlinks</groupId>
|
||||||
<artifactId>jetlinks-supports</artifactId>
|
<artifactId>jetlinks-supports</artifactId>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue