新增通知管理

This commit is contained in:
zhouhao 2020-02-19 15:03:38 +08:00
parent 4c26b30032
commit 31af39258d
71 changed files with 3394 additions and 88 deletions

View File

@ -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>

View File

@ -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));
}
}

View File

@ -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;
}
}

View File

@ -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();
}
}

View File

@ -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();
}

View File

@ -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);
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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);
}

View File

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

View File

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

View File

@ -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());
});
}
}

View File

@ -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");
}
}

View File

@ -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));
}
}

View File

@ -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 {
}

View File

@ -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);
}

View File

@ -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;
}

View File

@ -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;
}
}

View File

@ -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>

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -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;
}
}

View File

@ -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;
}

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -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>

View File

@ -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();
}
}

View File

@ -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");
}
}

View File

@ -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));
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}

View File

@ -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>

View File

@ -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();
}
}
}

View File

@ -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);
}
}

View File

@ -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 "测试";
}
}

View File

@ -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 |

View File

@ -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>

View File

@ -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);
}
}

View File

@ -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();
}
}

View File

@ -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));
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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();
}
}*/

View File

@ -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>

View File

@ -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;
}

View File

@ -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();
}
}

View File

@ -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;
}
}

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -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>

View File

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

View File

@ -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

View File

@ -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

View File

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

View File

@ -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

View File

@ -0,0 +1 @@
Notify Manager

View File

@ -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>

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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();
}
}

View File

@ -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";
}
}

View File

@ -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> {
}

View File

@ -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());
}
}
}

View File

@ -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<>();
}
}

View File

@ -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();
}
}

View File

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

View File

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