sms4j-oa-plugin支持钉钉、飞书、微信的webhook普通信息发送---支持yaml配置和java代码配置两种方式

This commit is contained in:
zhangyang 2023-10-23 10:20:04 +08:00
parent 2d65ffe0e1
commit a73f5c6d11
41 changed files with 1159 additions and 232 deletions

View File

@ -65,6 +65,19 @@
</dependency> </dependency>
<!--配置文件提示-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<version>${spring.boot.version}</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<version>${spring.boot.version}</version>
</dependency>
<!-- <dependency>--> <!-- <dependency>-->
<!-- <groupId>com.feishu.openplatform</groupId>--> <!-- <groupId>com.feishu.openplatform</groupId>-->
<!-- <artifactId>feishu-sdk-java</artifactId>--> <!-- <artifactId>feishu-sdk-java</artifactId>-->

View File

@ -5,5 +5,19 @@ import org.dromara.oa.comm.entity.Response;
import org.dromara.oa.comm.enums.MessageType; import org.dromara.oa.comm.enums.MessageType;
public interface OaSender { public interface OaSender {
/**
* 获取通知webhook实例唯一标识
*/
String getConfigId();
/**
* 获取供应商标识
*/
String getSupplier();
/**
* 发送消息
*/
Response sender(Request request, MessageType messageType); Response sender(Request request, MessageType messageType);
} }

View File

@ -18,6 +18,11 @@
</properties> </properties>
<dependencies> <dependencies>
<!-- <dependency>-->
<!-- <groupId>org.slf4j</groupId>-->
<!-- <artifactId>slf4j-api</artifactId>-->
<!-- </dependency>-->
<dependency> <dependency>
<groupId>cn.hutool</groupId> <groupId>cn.hutool</groupId>
<artifactId>hutool-cron</artifactId> <artifactId>hutool-cron</artifactId>

View File

@ -1,20 +0,0 @@
package org.dromara.oa.comm.config;
import lombok.Builder;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.ToString;
@Builder
@ToString
@Getter
@EqualsAndHashCode
public class OaConfig {
private String OaType;
private String tokenId;
private String sign;
}

View File

@ -0,0 +1,23 @@
package org.dromara.oa.comm.config;
/**
* @author dongfeng
* @date 2023-10-19 13:36
*/
public interface OaSupplierConfig {
/**
* 获取配置标识名(唯一)
*/
String getConfigId();
/**
* 获取供应商
*/
String getSupplier();
/**
* 获取是否使用
*/
Boolean getIsEnable();
}

View File

@ -0,0 +1,26 @@
package org.dromara.oa.comm.content;
/**
* @author dongfeng
* @date 2023-10-22 13:50
*/
public class OaContent {
/**
* 供应商配置键名
*/
public static final String SUPPLIER_KEY = "supplier";
/**
* 钉钉
*/
public static final String DINGTALK = "dingding";
/**
* 飞书
*/
public static final String BYTETALK = "feishu";
/**
* 微信
*/
public static final String WETALK = "wetalk";
}

View File

@ -13,7 +13,13 @@ public class Request {
// 消息内容 // 消息内容
private String content; private String content;
private List<String> phones; private List<String> phoneList;
private List<String> userIdList;
private List<String> userNamesList;
private Boolean isNoticeAll = false;
// oa类型 // oa类型
private String oaType; private String oaType;

View File

@ -1,19 +1,23 @@
package org.dromara.oa.comm.entity; package org.dromara.oa.comm.entity;
import lombok.AllArgsConstructor;
import lombok.Data; import lombok.Data;
@Data @Data
@AllArgsConstructor
public class Response { public class Response {
/** /**
* 响应码 * 是否成功
*/ */
private String code; private boolean success;
/** /**
* 响应消息 * 厂商原返回体
*/ */
private String message; private Object data;
/** /**
* 响应数据 * 配置标识名 如未配置取对应渠道名例如
*/ */
private String data; private String oaConfigId;
} }

View File

@ -13,6 +13,7 @@ public enum MessageType {
MessageType(String name) { MessageType(String name) {
this.name = name; this.name = name;
} }
private final String name; private final String name;
public String getName() { public String getName() {

View File

@ -22,7 +22,10 @@
<groupId>org.dromara.sms4j</groupId> <groupId>org.dromara.sms4j</groupId>
<artifactId>sms4j-oa-api</artifactId> <artifactId>sms4j-oa-api</artifactId>
</dependency> </dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</dependency>
<dependency> <dependency>
<groupId>com.sun.activation</groupId> <groupId>com.sun.activation</groupId>
<artifactId>javax.activation</artifactId> <artifactId>javax.activation</artifactId>
@ -48,6 +51,20 @@
<!-- <artifactId>feishu-sdk-java</artifactId>--> <!-- <artifactId>feishu-sdk-java</artifactId>-->
<!-- </dependency>--> <!-- </dependency>-->
<!--配置文件提示-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<version>${spring.boot.version}</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
</dependencies> </dependencies>

View File

@ -0,0 +1,18 @@
package org.dromara.oa.core.byteTalk.config;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.dromara.oa.comm.enums.OaType;
import org.dromara.oa.core.provider.config.OaBaseConfig;
@Data
@EqualsAndHashCode(callSuper = true)
public class ByteTalkConfig extends OaBaseConfig {
private final String requestUrl = OaType.BYTETALK.getUrl();
@Override
public String getSupplier() {
return OaType.BYTETALK.getType();
}
}

View File

@ -0,0 +1,40 @@
package org.dromara.oa.core.byteTalk.config;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import org.dromara.oa.comm.content.OaContent;
import org.dromara.oa.core.byteTalk.service.ByteTalkOaImpl;
import org.dromara.oa.core.provider.factory.AbstractProviderFactory;
/**
* @author dongfeng
* @description 飞书通知对象建造
* @date 2023-10-22 21:00
*/
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class ByteTalkFactory extends AbstractProviderFactory<ByteTalkOaImpl, ByteTalkConfig> {
private static final ByteTalkFactory INSTANCE = new ByteTalkFactory();
/**
* 建造一个飞书通知对象实现
*/
@Override
public ByteTalkOaImpl createSmsOa(ByteTalkConfig byteTalkConfig) {
return new ByteTalkOaImpl(byteTalkConfig);
}
@Override
public String getSupplier() {
return OaContent.BYTETALK;
}
/**
* 获取建造者实例
*
* @return 建造者实例
*/
public static ByteTalkFactory instance() {
return INSTANCE;
}
}

View File

@ -0,0 +1,70 @@
package org.dromara.oa.core.byteTalk.service;
import cn.hutool.json.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.dromara.oa.comm.entity.Request;
import org.dromara.oa.comm.entity.Response;
import org.dromara.oa.comm.enums.MessageType;
import org.dromara.oa.comm.enums.OaType;
import org.dromara.oa.comm.errors.OaException;
import org.dromara.oa.core.byteTalk.config.ByteTalkConfig;
import org.dromara.oa.core.byteTalk.utils.ByteTalkBuilder;
import org.dromara.oa.core.provider.service.AbstractOaBlend;
import org.dromara.oa.core.support.HttpClientImpl;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import static org.dromara.oa.comm.enums.OaType.BYTETALK;
/**
* @author dongfeng
* @date 2023-10-22 21:01
*/
@Slf4j
public class ByteTalkOaImpl extends AbstractOaBlend<ByteTalkConfig> {
private HttpClientImpl httpClient = new HttpClientImpl();
public ByteTalkOaImpl(ByteTalkConfig config) {
super(config);
}
@Override
public String getSupplier() {
return OaType.BYTETALK.getType();
}
@Override
public Response sender(Request request, MessageType messageType) {
if (Objects.isNull(request.getContent())) {
throw new OaException("消息体content不能为空");
}
StringBuilder webhook = new StringBuilder();
JSONObject message = null;
ByteTalkConfig config = getConfig();
webhook.append(BYTETALK.getUrl());
webhook.append(config.getTokenId());
long now = System.currentTimeMillis() / 1000;
String sign = ByteTalkBuilder.byteTalkSign(config.getSign(), now);
message = ByteTalkBuilder.createByteTalkMessage(request, messageType, sign, now);
String post;
try {
post = httpClient.post(webhook, getHeaders(), message);
log.info("请求返回结果:" + post);
} catch (Exception e) {
log.warn("请求失败问题:" + e.getMessage());
throw new OaException(e.getMessage());
}
// 后续解析响应体提取errorCode判断是否成功
return new Response(true, post, config.getConfigId());
}
public static Map<String, String> getHeaders() {
Map<String, String> headers = new HashMap<>();
headers.put("Content-Type", "application/json");
return headers;
}
}

View File

@ -0,0 +1,62 @@
package org.dromara.oa.core.byteTalk.utils;
import cn.hutool.json.JSONObject;
import org.dromara.oa.comm.entity.Request;
import org.dromara.oa.comm.enums.MessageType;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;
import java.util.List;
import static org.dromara.oa.comm.enums.MessageType.TEXT;
/**
* @author dongfeng
* @description 飞书通知签名和信息构建
* @date 2023-10-19 13:07
*/
public class ByteTalkBuilder {
public static String byteTalkSign(String secret, Long timestamp) {
//把timestamp+"\n"+密钥当做签名字符串
String stringToSign = timestamp + "\n" + secret;
//使用HmacSHA256算法计算签名
Mac mac = null;
try {
mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(stringToSign.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
throw new RuntimeException(e);
}
byte[] signData = mac.doFinal(new byte[]{});
return new String(Base64.getEncoder().encode(signData));
}
public static JSONObject createByteTalkMessage(Request request, MessageType messageType, String sign, Long timestamp) {
JSONObject message = new JSONObject();
if (messageType == TEXT) {
message.set("msg_type", "text");
message.set("timestamp", timestamp);
message.set("sign", sign);
StringBuilder content = new StringBuilder();
List<String> userNamesList = request.getUserNamesList();
Boolean isNoticeAll = request.getIsNoticeAll();
if (isNoticeAll) {
content.append("<at user_id=\"all\">所有人</at>");
}
userNamesList.forEach(l -> {
content.append("<at user_id=\"ou_xxx\">").append(l).append("</at>");
});
content.append(request.getContent());
JSONObject text = new JSONObject();
text.set("text", content);
message.set("content", text);
}
return message;
}
}

View File

@ -0,0 +1,69 @@
package org.dromara.oa.core.config;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import lombok.extern.slf4j.Slf4j;
import org.dromara.oa.api.OaSender;
import org.dromara.oa.comm.config.OaSupplierConfig;
import org.dromara.oa.comm.content.OaContent;
import org.dromara.oa.core.byteTalk.config.ByteTalkFactory;
import org.dromara.oa.core.dingTalk.config.DingTalkFactory;
import org.dromara.oa.core.provider.factory.BaseProviderFactory;
import org.dromara.oa.core.provider.factory.OaFactory;
import org.dromara.oa.core.provider.factory.ProviderFactoryHolder;
import org.dromara.oa.core.weTalk.config.WeTalkFactory;
import java.util.List;
import java.util.Map;
/**
* @author dongfeng
* @description 注册工厂, 读取yaml配置并根据配置生成对象
* @date 2023-10-22 12:39
*/
@Slf4j
public class OaBlendsInitializer {
private List<BaseProviderFactory<? extends OaSender, ? extends OaSupplierConfig>> factoryList;
private final Map<String, Map<String, Object>> blends;
public OaBlendsInitializer(Map<String, Map<String, Object>> oas
) {
this.blends = oas;
onApplicationEvent();
}
public void onApplicationEvent() {
registerDefaultFactory();
// 解析供应商配置
for (String configId : blends.keySet()) {
Map<String, Object> configMap = blends.get(configId);
if (Boolean.FALSE.equals(configMap.get("isEnable"))) {
continue;
}
Object supplierObj = configMap.get(OaContent.SUPPLIER_KEY);
String supplier = supplierObj == null ? "" : String.valueOf(supplierObj);
supplier = StrUtil.isEmpty(supplier) ? configId : supplier;
BaseProviderFactory<OaSender, OaSupplierConfig> providerFactory = (BaseProviderFactory<OaSender, OaSupplierConfig>) ProviderFactoryHolder.requireForSupplier(supplier);
if (providerFactory == null) {
log.warn("创建\"{}\"的通知webhook服务失败未找到供应商为\"{}\"的服务", configId, supplier);
continue;
}
configMap.put("configId", configId);
JSONObject configJson = new JSONObject(configMap);
OaSupplierConfig supplierConfig = JSONUtil.toBean(configJson, providerFactory.getConfigClass());
OaFactory.createAndRegisterOaSender(supplierConfig);
}
}
/**
* 注册默认工厂实例
*/
private void registerDefaultFactory() {
ProviderFactoryHolder.registerFactory(DingTalkFactory.instance());
ProviderFactoryHolder.registerFactory(ByteTalkFactory.instance());
ProviderFactoryHolder.registerFactory(WeTalkFactory.instance());
}
}

View File

@ -0,0 +1,31 @@
package org.dromara.oa.core.config;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* @author dongfeng
* @date 2023-10-22 12:50
*/
public class OaSupplierConfig {
/**
* 注入配置
*/
@Bean
@ConfigurationProperties(prefix = "sms.oas")
@ConditionalOnProperty(prefix = "sms", name = "config-type", havingValue = "yaml")
protected Map<String, Map<String, Object>> oas() {
return new LinkedHashMap<>();
}
@Bean
protected OaBlendsInitializer smsOasInitializer(
Map<String, Map<String, Object>> oas) {
return new OaBlendsInitializer(oas);
}
}

View File

@ -0,0 +1,22 @@
package org.dromara.oa.core.dingTalk.config;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.dromara.oa.comm.enums.OaType;
import org.dromara.oa.core.provider.config.OaBaseConfig;
/**
* @author dongfeng
* @date 2023-10-19 13:38
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class DingTalkConfig extends OaBaseConfig {
private final String requestUrl = OaType.DINGTALK.getUrl();
@Override
public String getSupplier() {
return OaType.DINGTALK.getType();
}
}

View File

@ -0,0 +1,40 @@
package org.dromara.oa.core.dingTalk.config;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import org.dromara.oa.comm.content.OaContent;
import org.dromara.oa.core.dingTalk.service.DingTalkOaImpl;
import org.dromara.oa.core.provider.factory.AbstractProviderFactory;
/**
* @author dongfeng
* @description 钉钉通知对象建造
* @date 2023-10-22 21:00
*/
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class DingTalkFactory extends AbstractProviderFactory<DingTalkOaImpl, DingTalkConfig> {
private static final DingTalkFactory INSTANCE = new DingTalkFactory();
/**
* 建造一个钉钉通知实现
*/
@Override
public DingTalkOaImpl createSmsOa(DingTalkConfig dingTalkConfig) {
return new DingTalkOaImpl(dingTalkConfig);
}
@Override
public String getSupplier() {
return OaContent.DINGTALK;
}
/**
* 获取建造者实例
*
* @return 建造者实例
*/
public static DingTalkFactory instance() {
return INSTANCE;
}
}

View File

@ -0,0 +1,76 @@
package org.dromara.oa.core.dingTalk.service;
import cn.hutool.json.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.dromara.oa.comm.entity.Request;
import org.dromara.oa.comm.entity.Response;
import org.dromara.oa.comm.enums.MessageType;
import org.dromara.oa.comm.enums.OaType;
import org.dromara.oa.comm.errors.OaException;
import org.dromara.oa.core.dingTalk.config.DingTalkConfig;
import org.dromara.oa.core.dingTalk.utils.DingTalkBuilder;
import org.dromara.oa.core.provider.service.AbstractOaBlend;
import org.dromara.oa.core.support.HttpClientImpl;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import static org.dromara.oa.comm.enums.OaType.DINGTALK;
/**
* @author dongfeng
* @date 2023-10-22 21:01
*/
@Slf4j
public class DingTalkOaImpl extends AbstractOaBlend<DingTalkConfig> {
private HttpClientImpl httpClient = new HttpClientImpl();
/**
* 建造一个微信通知对象服务
*/
public DingTalkOaImpl(DingTalkConfig config) {
super(config);
}
@Override
public String getSupplier() {
return OaType.DINGTALK.getType();
}
@Override
public Response sender(Request request, MessageType messageType) {
if (Objects.isNull(request.getContent())) {
throw new OaException("消息体content不能为空");
}
StringBuilder webhook = new StringBuilder();
JSONObject message = null;
DingTalkConfig config = getConfig();
webhook.append(DINGTALK.getUrl());
webhook.append(config.getTokenId());
String sign = config.getSign();
if (!Objects.isNull(sign)) {
sign = DingTalkBuilder.sign(sign);
webhook.append(sign);
}
message = DingTalkBuilder.createMessage(request, messageType);
String post;
try {
post = httpClient.post(webhook, getHeaders(), message);
log.info("请求返回结果:" + post);
} catch (Exception e) {
log.warn("请求失败问题:" + e.getMessage());
throw new OaException(e.getMessage());
}
// 后续解析响应体提取errorCode判断是否成功
return new Response(true, post, config.getConfigId());
}
public static Map<String, String> getHeaders() {
Map<String, String> headers = new HashMap<>();
headers.put("Content-Type", "application/json");
return headers;
}
}

View File

@ -0,0 +1,70 @@
package org.dromara.oa.core.dingTalk.utils;
import cn.hutool.json.JSONObject;
import org.dromara.oa.comm.entity.Request;
import org.dromara.oa.comm.enums.MessageType;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;
import static org.dromara.oa.comm.enums.MessageType.MARKDOWN;
import static org.dromara.oa.comm.enums.MessageType.TEXT;
/**
* @author dongfeng
* @description 钉钉通知签名和信息构建
* @date 2023-10-19 13:07
*/
public class DingTalkBuilder {
public static String sign(String secret) {
Long timestamp = System.currentTimeMillis();
String stringToSign = timestamp + "\n" + secret;
Mac mac = null;
try {
mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
throw new RuntimeException(e);
}
byte[] signData = mac.doFinal(stringToSign.getBytes(StandardCharsets.UTF_8));
String sign = null;
try {
sign = URLEncoder.encode(new String(Base64.getEncoder().encode(signData)), StandardCharsets.UTF_8.toString());
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
return "&timestamp=" + timestamp + "&sign=" + sign;
}
public static JSONObject createMessage(Request request, MessageType messageType) {
JSONObject message = new JSONObject();
if (messageType == TEXT) {
message.set("msgtype", "text");
JSONObject text = new JSONObject();
text.set("content", request.getContent());
JSONObject at = new JSONObject();
at.set("atMobiles", request.getPhoneList());
at.set("isAtAll", request.getIsNoticeAll());
message.set("at", at);
message.set("text", text);
} else if (messageType == MARKDOWN) {
message.set("msgtype", "markdown");
JSONObject markdown = new JSONObject();
markdown.set("text", request.getContent());
markdown.set("title", request.getTitle());
JSONObject at = new JSONObject();
at.set("atMobiles", request.getPhoneList());
message.set("at", at);
message.set("markdown", markdown);
}
return message;
}
}

View File

@ -1,38 +0,0 @@
package org.dromara.oa.core.factory;
import org.dromara.oa.comm.config.OaConfig;
import org.dromara.oa.comm.errors.OaException;
import org.dromara.oa.core.service.OaBuild;
import org.dromara.oa.core.service.SenderImpl;
import java.util.HashMap;
import java.util.Map;
public class OaFactory {
private static final Map<Object, OaConfig> configs = new HashMap<>();
/**
* createMailClient
* <p>从工厂获取一个OA发送实例
* @param key 配置的标识key
*/
public static SenderImpl createSender(Object key) {
try {
return OaBuild.build(configs.get(key));
} catch (Exception e) {
throw new OaException(e.getMessage());
}
}
/**
* set
* <p>将一个配置对象交给工厂
* @param key 标识
* @param config 配置对象
*/
public static void put(Object key, OaConfig config){
configs.put(key, config);
}
}

View File

@ -0,0 +1,26 @@
package org.dromara.oa.core.provider.config;
import lombok.Data;
import org.dromara.oa.comm.config.OaSupplierConfig;
@Data
public abstract class OaBaseConfig implements OaSupplierConfig {
/**
* 供应商
*/
private String supplier;
/**
* 获取配置标识名(唯一)
*/
private String configId;
private String tokenId;
private String sign;
private Boolean isEnable = true;
}

View File

@ -0,0 +1,33 @@
package org.dromara.oa.core.provider.factory;
import org.dromara.oa.api.OaSender;
import org.dromara.oa.comm.config.OaSupplierConfig;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
public abstract class AbstractProviderFactory<S extends OaSender, C extends OaSupplierConfig> implements BaseProviderFactory<S, C> {
private Class<C> configClass;
public AbstractProviderFactory() {
Type genericSuperclass = getClass().getGenericSuperclass();
if (genericSuperclass instanceof ParameterizedType) {
ParameterizedType paramType = (ParameterizedType) genericSuperclass;
Type[] typeArguments = paramType.getActualTypeArguments();
if (typeArguments.length > 1 && typeArguments[1] instanceof Class) {
configClass = (Class<C>) typeArguments[1];
}
}
}
/**
* 获取配置类
*
* @return 配置类
*/
public Class<C> getConfigClass() {
return configClass;
}
}

View File

@ -0,0 +1,30 @@
package org.dromara.oa.core.provider.factory;
import org.dromara.oa.api.OaSender;
import org.dromara.oa.comm.config.OaSupplierConfig;
public interface BaseProviderFactory<S extends OaSender, C extends OaSupplierConfig> {
/**
* 创建通知webhook实现对象
*
* @param c 通知webhook配置对象
* @return 通知webhook实现对象
*/
S createSmsOa(C c);
/**
* 获取配置类
*
* @return 配置类
*/
Class<C> getConfigClass();
/**
* 获取供应商
*
* @return 供应商
*/
String getSupplier();
}

View File

@ -0,0 +1,53 @@
package org.dromara.oa.core.provider.factory;
import org.dromara.oa.api.OaSender;
import org.dromara.oa.comm.config.OaSupplierConfig;
import org.dromara.oa.comm.errors.OaException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class OaFactory {
private final static Map<String, OaSender> configs = new ConcurrentHashMap<>();
/**
* <p>创建各个厂商的实现类
*
* @param config 通知webhook配置
*/
public static void createAndRegisterOaSender(OaSupplierConfig config) {
OaSender oaSender = createAndGetOa(config);
register(oaSender);
}
/**
* 注册通知webhook服务对象
*
* @param smsBlend 通知webhook服务对象
*/
public static void register(OaSender smsBlend) {
if (smsBlend == null) {
throw new OaException("通知webhook服务对象不能为空");
}
configs.put(smsBlend.getConfigId(), smsBlend);
}
public static OaSender createAndGetOa(OaSupplierConfig config) {
BaseProviderFactory factory = ProviderFactoryHolder.requireForSupplier(config.getSupplier());
if (factory == null) {
throw new OaException("不支持当前供应商配置");
}
return factory.createSmsOa(config);
}
/**
* 通过configId获取通知webhook服务对象
*
* @param configId 唯一标识
* @return 返回通知webhook服务对象如果未找到则返回null
*/
public static OaSender getSmsOaBlend(String configId) {
return configs.get(configId);
}
}

View File

@ -0,0 +1,43 @@
package org.dromara.oa.core.provider.factory;
import cn.hutool.core.collection.CollUtil;
import org.dromara.oa.api.OaSender;
import org.dromara.oa.comm.config.OaSupplierConfig;
import org.dromara.oa.comm.errors.OaException;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* @author dongfeng
* @date 2023-10-22 21:12
*/
public class ProviderFactoryHolder {
private static final Map<String, BaseProviderFactory<? extends OaSender, ? extends OaSupplierConfig>> factories = new ConcurrentHashMap<>();
public static void registerFactory(BaseProviderFactory<? extends OaSender, ? extends OaSupplierConfig> factory) {
if (factory == null) {
throw new OaException("注册供应商工厂失败,工厂实例不能为空");
}
factories.put(factory.getSupplier(), factory);
}
public static void registerFactory(List<BaseProviderFactory<? extends OaSender, ? extends OaSupplierConfig>> factoryList) {
if (CollUtil.isEmpty(factoryList)) {
return;
}
for (BaseProviderFactory<? extends OaSender, ? extends OaSupplierConfig> factory : factoryList) {
if (factory == null) {
continue;
}
registerFactory(factory);
}
}
public static BaseProviderFactory<? extends OaSender, ? extends OaSupplierConfig> requireForSupplier(String supplier) {
return factories.getOrDefault(supplier, null);
}
}

View File

@ -0,0 +1,27 @@
package org.dromara.oa.core.provider.service;
import cn.hutool.core.util.StrUtil;
import lombok.Getter;
import org.dromara.oa.api.OaSender;
import org.dromara.oa.comm.config.OaSupplierConfig;
/**
* @author dongfeng
* @date 2023-10-22 21:03
*/
public abstract class AbstractOaBlend<C extends OaSupplierConfig> implements OaSender {
@Getter
private final String configId;
private final C config;
protected AbstractOaBlend(C config) {
this.configId = StrUtil.isEmpty(config.getConfigId()) ? getSupplier() : config.getConfigId();
this.config = config;
}
protected C getConfig() {
return config;
}
}

View File

@ -1,22 +0,0 @@
package org.dromara.oa.core.service;
import lombok.Data;
import org.dromara.oa.comm.config.OaConfig;
@Data
public class OaBuild {
private String OaType;
private OaConfig config;
public OaBuild(OaConfig config) {
this.config = config;
this.OaType = getOaType();
}
public static SenderImpl build(OaConfig config) {
return SenderImpl.NewSender(new OaBuild(config));
}
}

View File

@ -1,120 +0,0 @@
package org.dromara.oa.core.service;
import org.dromara.oa.api.OaSender;
import org.dromara.oa.comm.entity.Request;
import org.dromara.oa.comm.entity.Response;
import org.dromara.oa.comm.enums.MessageType;
import org.dromara.oa.comm.errors.OaException;
import org.dromara.oa.core.support.HttpClientImpl;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
import java.util.logging.Logger;
import cn.hutool.json.JSONObject;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import static org.dromara.oa.comm.enums.MessageType.*;
import static org.dromara.oa.comm.enums.OaType.DINGTALK;
public class SenderImpl implements OaSender {
private static Logger logger = Logger.getLogger("oaLog");
private OaBuild oaBuild;
private HttpClientImpl httpClient = new HttpClientImpl();
public SenderImpl() {
}
public SenderImpl(OaBuild oaBuild) {
this.oaBuild = oaBuild;
}
public static SenderImpl NewSender(OaBuild oaBuild){
return new SenderImpl(oaBuild);
}
@Override
public Response sender(Request request, MessageType messageType) {
StringBuilder webhook = new StringBuilder();
webhook.append(DINGTALK.getUrl());
webhook.append(oaBuild.getConfig().getTokenId());
if (request.getOaType().equals(DINGTALK.getType())) {
// todo 等待完善钉钉和飞书的sign
webhook.append(sign(oaBuild.getConfig().getSign()));
}
JSONObject message = createMessage(request, messageType);
try {
String post = httpClient.post(webhook, getHeaders(), message);
logger.info("请求返回结果:" + post);
} catch (Exception e) {
logger.warning("请求失败问题:" + e.getMessage());
throw new OaException(e.getMessage());
}
return new Response();
}
public static Map<String, String> getHeaders() {
Map<String, String> headers = new HashMap<>();
headers.put("Content-Type", "application/json");
return headers;
}
public static String sign(String secret) {
Long timestamp = System.currentTimeMillis();
String stringToSign = timestamp + "\n" + secret;
Mac mac = null;
try {
mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
throw new RuntimeException(e);
}
byte[] signData = mac.doFinal(stringToSign.getBytes(StandardCharsets.UTF_8));
String sign = null;
try {
sign = URLEncoder.encode(new String(Base64.getEncoder().encode(signData)),StandardCharsets.UTF_8.toString());
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
return "&timestamp=" + timestamp + "&sign=" + sign;
}
public static JSONObject createMessage(Request request, MessageType messageType) {
JSONObject message = new JSONObject();
if (messageType == TEXT){
message.set("msgtype", "text");
JSONObject text = new JSONObject();
text.set("content", request.getContent());
JSONObject at = new JSONObject();
at.set("atMobiles", request.getPhones());
message.set("at", at);
message.set("text", text);
} else if (messageType == LINK) {
message.set("msgtype", "link");
JSONObject link = new JSONObject();
link.set("text", request.getContent());
link.set("title", request.getTitle());
message.set("link", link);
} else if (messageType == MARKDOWN) {
message.set("msgtype", "markdown");
JSONObject markdown = new JSONObject();
markdown.set("text", request.getContent());
markdown.set("title", request.getTitle());
JSONObject at = new JSONObject();
at.set("atMobiles", request.getPhones());
message.set("at", at);
message.set("markdown", markdown);
}
return message;
}
}

View File

@ -9,7 +9,6 @@ public class HttpClientImpl extends AbstractHttpClient{
@Override @Override
public <T> String post(StringBuilder url, Map<String, String> headers, T message) throws Exception { public <T> String post(StringBuilder url, Map<String, String> headers, T message) throws Exception {
// 构建请求体 // 构建请求体
String payload = "{\"msgtype\":\"text\",\"text\":{\"content\":\"This HertzBeat 通知\"},\"at\":{\"isAtAll\":false}}";
// 发送POST请求 // 发送POST请求
HttpResponse response = HttpRequest.post(url.toString()) HttpResponse response = HttpRequest.post(url.toString())
.headerMap(headers, true) .headerMap(headers, true)

View File

@ -0,0 +1,18 @@
package org.dromara.oa.core.weTalk.config;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.dromara.oa.comm.enums.OaType;
import org.dromara.oa.core.provider.config.OaBaseConfig;
@Data
@EqualsAndHashCode(callSuper = true)
public class WeTalkConfig extends OaBaseConfig {
private final String requestUrl = OaType.WETALK.getUrl();
@Override
public String getSupplier() {
return OaType.WETALK.getType();
}
}

View File

@ -0,0 +1,40 @@
package org.dromara.oa.core.weTalk.config;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import org.dromara.oa.comm.content.OaContent;
import org.dromara.oa.core.provider.factory.AbstractProviderFactory;
import org.dromara.oa.core.weTalk.service.WeTalkOaImpl;
/**
* @author dongfeng
* @description 微信通知对象建造
* @date 2023-10-22 21:00
*/
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class WeTalkFactory extends AbstractProviderFactory<WeTalkOaImpl, WeTalkConfig> {
private static final WeTalkFactory INSTANCE = new WeTalkFactory();
/**
* 建造一个微信通知服务
*/
@Override
public WeTalkOaImpl createSmsOa(WeTalkConfig weTalkConfig) {
return new WeTalkOaImpl(weTalkConfig);
}
@Override
public String getSupplier() {
return OaContent.WETALK;
}
/**
* 获取建造者实例
*
* @return 建造者实例
*/
public static WeTalkFactory instance() {
return INSTANCE;
}
}

View File

@ -0,0 +1,68 @@
package org.dromara.oa.core.weTalk.service;
import cn.hutool.json.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.dromara.oa.comm.entity.Request;
import org.dromara.oa.comm.entity.Response;
import org.dromara.oa.comm.enums.MessageType;
import org.dromara.oa.comm.enums.OaType;
import org.dromara.oa.comm.errors.OaException;
import org.dromara.oa.core.provider.service.AbstractOaBlend;
import org.dromara.oa.core.support.HttpClientImpl;
import org.dromara.oa.core.weTalk.config.WeTalkConfig;
import org.dromara.oa.core.weTalk.utils.WeTalkBuilder;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import static org.dromara.oa.comm.enums.OaType.WETALK;
/**
* @author dongfeng
* @date 2023-10-22 21:01
*/
@Slf4j
public class WeTalkOaImpl extends AbstractOaBlend<WeTalkConfig> {
private HttpClientImpl httpClient = new HttpClientImpl();
public WeTalkOaImpl(WeTalkConfig config) {
super(config);
}
@Override
public String getSupplier() {
return OaType.WETALK.getType();
}
@Override
public Response sender(Request request, MessageType messageType) {
if (Objects.isNull(request.getContent())) {
throw new OaException("消息体content不能为空");
}
StringBuilder webhook = new StringBuilder();
JSONObject message = null;
WeTalkConfig config = getConfig();
webhook.append(WETALK.getUrl());
webhook.append(config.getTokenId());
message = WeTalkBuilder.createWeTalkMessage(request, messageType);
String post;
try {
post = httpClient.post(webhook, getHeaders(), message);
log.info("请求返回结果:" + post);
} catch (Exception e) {
log.warn("请求失败问题:" + e.getMessage());
throw new OaException(e.getMessage());
}
// 后续解析响应体提取errorCode判断是否成功
return new Response(true, post, config.getConfigId());
}
public static Map<String, String> getHeaders() {
Map<String, String> headers = new HashMap<>();
headers.put("Content-Type", "application/json");
return headers;
}
}

View File

@ -0,0 +1,56 @@
package org.dromara.oa.core.weTalk.utils;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.json.JSONObject;
import org.dromara.oa.comm.entity.Request;
import org.dromara.oa.comm.enums.MessageType;
import java.util.List;
import static org.dromara.oa.comm.enums.MessageType.MARKDOWN;
import static org.dromara.oa.comm.enums.MessageType.TEXT;
/**
* @author dongfeng
* @description 微信通知签名和信息构建
* @date 2023-10-19 13:07
*/
public class WeTalkBuilder {
public static JSONObject createWeTalkMessage(Request request, MessageType messageType) {
JSONObject message = new JSONObject();
if (messageType == TEXT) {
message.set("msgtype", "text");
JSONObject text = new JSONObject();
text.set("content", request.getContent());
boolean isContain = false;
List<String> userIdList = request.getUserIdList();
List<String> phoneList = request.getPhoneList();
Boolean isNoticeAll = request.getIsNoticeAll();
if (!ObjectUtil.isNull(userIdList)) {
if (isNoticeAll) {
userIdList.add("@all");
isContain = true;
}
text.set("mentioned_list", userIdList.toArray());
}
if (!ObjectUtil.isNull(phoneList)) {
if (isNoticeAll && !isContain) {
phoneList.add("@all");
}
text.set("mentioned_mobile_list", phoneList.toArray());
}
message.set("text", text);
} else if (messageType == MARKDOWN) {
message.set("msgtype", "markdown");
JSONObject markdown = new JSONObject();
markdown.set("content", request.getContent());
markdown.set("title", request.getTitle());
message.set("markdown", markdown);
}
return message;
}
}

View File

@ -0,0 +1,2 @@
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.dromara.oa.core.config.OaSupplierConfig

View File

@ -60,3 +60,17 @@ sms:
template-id: pub_verif_short template-id: pub_verif_short
# 模版名称 # 模版名称
templateName: code templateName: code
oas:
oaDingTalkByYaml: # configId
isEnable: true # 表示该配置是否生效(默认生效,false表示不生效)
supplier: dingding # 厂商标识
tokenId: 您的accessKey
sign: 您的sign
oaByteTalkByYaml: # configId
supplier: feishu # 厂商标识
tokenId: 您的accessKey
sign: 您的sign
oaWeTalkByYaml:
supplier: wetalk # 厂商标识
tokenId: 您的sign

View File

@ -1,12 +1,14 @@
package org.dromara.sms4j.example; package org.dromara.sms4j.example;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.dromara.oa.comm.config.OaConfig; import org.dromara.oa.api.OaSender;
import org.dromara.oa.comm.entity.Request; import org.dromara.oa.comm.entity.Request;
import org.dromara.oa.comm.enums.MessageType; import org.dromara.oa.comm.enums.MessageType;
import org.dromara.oa.comm.enums.OaType; import org.dromara.oa.comm.enums.OaType;
import org.dromara.oa.core.factory.OaFactory; import org.dromara.oa.core.byteTalk.config.ByteTalkConfig;
import org.dromara.oa.core.service.SenderImpl; import org.dromara.oa.core.dingTalk.config.DingTalkConfig;
import org.dromara.oa.core.provider.factory.OaFactory;
import org.dromara.oa.core.weTalk.config.WeTalkConfig;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest;
@ -28,27 +30,134 @@ public class SmsOaTest {
* 填secret * 填secret
*/ */
private static final String SIGN = ""; private static final String SIGN = "";
/**
* 填oa的key
* value是一个OaConfig
*/
private static final String oaKey = "";
@Test @Test
public void oaDingTalkTest() { public void oaDingTalkTest() {
OaFactory.put(oaKey, OaConfig.builder() String key = "oaDingTalk";
.OaType(OaType.DINGTALK.getType()) DingTalkConfig dingTalkConfig = new DingTalkConfig();
.tokenId(TOKENID) dingTalkConfig.setConfigId(key);
.sign(SIGN).build()); dingTalkConfig.setSign(SIGN);
SenderImpl alarm = OaFactory.createSender(oaKey); dingTalkConfig.setTokenId(TOKENID);
// OaFactory.createAndRegisterOaSender(dingTalkConfig);
OaSender alarm = OaFactory.createAndGetOa(dingTalkConfig);
Request request = new Request(); Request request = new Request();
request.setOaType(OaType.DINGTALK.getType());
ArrayList<String> phones = new ArrayList<>(); ArrayList<String> phones = new ArrayList<>();
phones.add(PHONE); phones.add(PHONE);
request.setPhones(phones); request.setPhoneList(phones);
request.setIsNoticeAll(true);
request.setContent("测试消息"); request.setContent("测试消息");
request.setTitle("测试消息标题"); request.setTitle("测试消息标题");
alarm.sender(request, MessageType.TEXT); alarm.sender(request, MessageType.TEXT);
// 测试markdown,无法@
// request.setContent("#### 杭州天气 @150XXXXXXXX \n > 9度西北风1级空气良89相对温度73%\n > ![screenshot](https://img.alicdn.com/tfs/TB1NwmBEL9TBuNjy1zbXXXpepXa-2400-1218.png)\n > ###### 10点20分发布 [天气](https://www.dingtalk.com) \n");
// request.setTitle("杭州天气");
// alarm.sender(request, MessageType.MARKDOWN);
}
@Test
public void oaDingTalkByYamlTest() {
String configId = "oaDingTalkByYaml";
OaSender alarm = OaFactory.getSmsOaBlend(configId);
Request request = new Request();
ArrayList<String> phones = new ArrayList<>();
phones.add(PHONE);
request.setPhoneList(phones);
request.setIsNoticeAll(false);
request.setContent("HertzBeat");
request.setTitle("HertzBeat");
alarm.sender(request, MessageType.TEXT);
}
@Test
public void oaByteTalkTest() {
String configId = "oaByteTalk";
ByteTalkConfig dingTalkConfig = new ByteTalkConfig();
dingTalkConfig.setConfigId(configId);
dingTalkConfig.setSign(SIGN);
dingTalkConfig.setTokenId(TOKENID);
OaSender alarm = OaFactory.createAndGetOa(dingTalkConfig);
Request request = new Request();
ArrayList<String> userNameList = new ArrayList<>();
userNameList.add("user1");
userNameList.add("user2");
request.setUserNamesList(userNameList);
request.setContent("测试消息");
request.setIsNoticeAll(true);
// request.setTitle("测试消息标题");
alarm.sender(request, MessageType.TEXT);
}
@Test
public void oaByteTalkByYamlTest() {
String configId = "oaByteTalkByYaml";
OaSender alarm = OaFactory.getSmsOaBlend(configId);
Request request = new Request();
request.setOaType(OaType.BYTETALK.getType());
ArrayList<String> userNameList = new ArrayList<>();
userNameList.add("user1");
userNameList.add("user2");
request.setUserNamesList(userNameList);
request.setContent("测试消息");
request.setIsNoticeAll(true);
// request.setTitle("测试消息标题");
alarm.sender(request, MessageType.TEXT);
}
@Test
public void oaWeTalkTest() {
String configId = "oaWeTalk";
WeTalkConfig weTalkConfig = new WeTalkConfig();
weTalkConfig.setConfigId(configId);
weTalkConfig.setTokenId(TOKENID);
OaSender alarm = OaFactory.createAndGetOa(weTalkConfig);
Request request = new Request();
ArrayList<String> phones = new ArrayList<>();
phones.add(PHONE);
phones.add("131");
request.setPhoneList(phones);
ArrayList<String> userIds = new ArrayList<>();
userIds.add("123");
request.setUserIdList(userIds);
// request.setIsNoticeAll(true);
request.setContent("测试消息");
request.setTitle("测试消息标题");
alarm.sender(request, MessageType.TEXT);
// 测试markdown,无法@
// 企业微信的markdown直接是content没有title
// request.setContent("#### 杭州天气 @150XXXXXXXX \n > 9度西北风1级空气良89相对温度73%\n > ![screenshot](https://img.alicdn.com/tfs/TB1NwmBEL9TBuNjy1zbXXXpepXa-2400-1218.png)\n > ###### 10点20分发布 [天气](https://www.dingtalk.com) \n");
// request.setTitle("杭州天气");
// alarm.sender(request, MessageType.MARKDOWN);
}
@Test
public void oaWeTalkByYamlTest() {
String configId = "oaWeTalkByYaml";
OaSender alarm = OaFactory.getSmsOaBlend(configId);
Request request = new Request();
request.setOaType(OaType.WETALK.getType());
ArrayList<String> phones = new ArrayList<>();
phones.add(PHONE);
phones.add("131");
request.setPhoneList(phones);
ArrayList<String> userIds = new ArrayList<>();
userIds.add("123");
request.setUserIdList(userIds);
// request.setIsNoticeAll(true);
request.setContent("测试消息");
request.setTitle("测试消息标题");
alarm.sender(request, MessageType.TEXT);
// 测试markdown,无法@
// 企业微信的markdown直接是content没有title
// request.setContent("#### 杭州天气 @150XXXXXXXX \n > 9度西北风1级空气良89相对温度73%\n > ![screenshot](https://img.alicdn.com/tfs/TB1NwmBEL9TBuNjy1zbXXXpepXa-2400-1218.png)\n > ###### 10点20分发布 [天气](https://www.dingtalk.com) \n");
// request.setTitle("杭州天气");
// alarm.sender(request, MessageType.MARKDOWN);
} }
} }