diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/api/WxConsts.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/api/WxConsts.java index 7f1dcdf023..2ba0f93213 100644 --- a/weixin-java-common/src/main/java/me/chanjar/weixin/common/api/WxConsts.java +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/api/WxConsts.java @@ -105,6 +105,31 @@ public static class KefuMsgType { public static final String MINIPROGRAM_NOTICE = "miniprogram_notice"; } + /** + * 群机器人的消息类型. + */ + public static class GroupRobotMsgType { + /** + * 文本消息. + */ + public static final String TEXT = "text"; + + /** + * 图片消息. + */ + public static final String IMAGE = "image"; + + /** + * markdown消息. + */ + public static final String MARKDOWN = "markdown"; + + /** + * 图文消息(点击跳转到外链). + */ + public static final String NEWS = "news"; + } + /** * 表示是否是保密消息,0表示否,1表示是,默认0. */ diff --git a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/fs/FileUtils.java b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/fs/FileUtils.java index 1b051a4718..a00c9cbade 100644 --- a/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/fs/FileUtils.java +++ b/weixin-java-common/src/main/java/me/chanjar/weixin/common/util/fs/FileUtils.java @@ -4,6 +4,7 @@ import java.io.IOException; import java.io.InputStream; import java.nio.file.Files; +import java.util.Base64; public class FileUtils { @@ -34,4 +35,32 @@ public static File createTmpFile(InputStream inputStream, String name, String ex return createTmpFile(inputStream, name, ext, Files.createTempDirectory("weixin-java-tools-temp").toFile()); } + /** + * 文件流生成base64 + * + * @param in 文件流 + * @return base64编码 + */ + public static String imageToBase64ByStream(InputStream in) { + byte[] data = null; + // 读取图片字节数组 + try { + data = new byte[in.available()]; + in.read(data); + // 返回Base64编码过的字节数组字符串 + return Base64.getEncoder().encodeToString(data); + } catch (IOException e) { + e.printStackTrace(); + } finally { + if (in != null) { + try { + in.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + return null; + } + } diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpGroupRobotService.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpGroupRobotService.java new file mode 100644 index 0000000000..007dff78fc --- /dev/null +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpGroupRobotService.java @@ -0,0 +1,52 @@ +package me.chanjar.weixin.cp.api; + +import me.chanjar.weixin.common.error.WxErrorException; +import me.chanjar.weixin.cp.bean.article.NewArticle; + +import java.util.List; + +/** + * 微信群机器人消息发送api + * 文档地址:https://work.weixin.qq.com/help?doc_id=13376 + * 调用地址:https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key= + * + * @author yr + * @date 2020-8-20 + */ +public interface WxCpGroupRobotService { + + /** + * 发送text类型的消息 + * + * @param content 文本内容,最长不超过2048个字节,必须是utf8编码 + * @param mentionedList userId的列表,提醒群中的指定成员(@某个成员),@all表示提醒所有人,如果开发者获取不到userId,可以使用mentioned_mobile_list + * @param mobileList 手机号列表,提醒手机号对应的群成员(@某个成员),@all表示提醒所有人 + * @throws WxErrorException 异常 + */ + void sendText(String content, List mentionedList, List mobileList) throws WxErrorException; + + /** + * 发送markdown类型的消息 + * + * @param content markdown内容,最长不超过4096个字节,必须是utf8编码 + * @throws WxErrorException 异常 + */ + void sendMarkDown(String content) throws WxErrorException; + + /** + * 发送image类型的消息 + * + * @param base64 图片内容的base64编码 + * @param md5 图片内容(base64编码前)的md5值 + * @throws WxErrorException 异常 + */ + void sendImage(String base64, String md5) throws WxErrorException; + + /** + * 发送news类型的消息 + * + * @param articleList 图文消息,支持1到8条图文 + * @throws WxErrorException 异常 + */ + void sendNews(List articleList) throws WxErrorException; +} diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpService.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpService.java index 73776228ed..036265815b 100644 --- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpService.java +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpService.java @@ -173,6 +173,14 @@ public interface WxCpService { */ String post(String url, String postData) throws WxErrorException; + /** + * 当不需要自动带accessToken的时候,可以用这个发起post请求 + * + * @param url 接口地址 + * @param postData 请求body字符串 + */ + String postWithoutToken(String url, String postData) throws WxErrorException; + /** *
    * Service没有实现某个API的时候,可以用这个,
@@ -328,6 +336,13 @@ public interface WxCpService {
 
   WxCpOaService getOAService();
 
+  /**
+   * 获取群机器人消息推送服务
+   *
+   * @return 群机器人消息推送服务
+   */
+  WxCpGroupRobotService getGroupRobotService();
+
   /**
    * http请求对象
    */
diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/BaseWxCpServiceImpl.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/BaseWxCpServiceImpl.java
index 75cf92aa9c..f2d96cf5cb 100644
--- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/BaseWxCpServiceImpl.java
+++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/BaseWxCpServiceImpl.java
@@ -51,6 +51,7 @@ public abstract class BaseWxCpServiceImpl implements WxCpService, RequestH
   private WxCpOaService oaService = new WxCpOaServiceImpl(this);
   private WxCpTaskCardService taskCardService = new WxCpTaskCardServiceImpl(this);
   private WxCpExternalContactService externalContactService = new WxCpExternalContactServiceImpl(this);
+  private WxCpGroupRobotService groupRobotService = new WxCpGroupRobotServiceImpl(this);
 
   /**
    * 全局的是否正在刷新access token的锁.
@@ -217,6 +218,11 @@ public String post(String url, String postData) throws WxErrorException {
     return execute(SimplePostRequestExecutor.create(this), url, postData);
   }
 
+  @Override
+  public String postWithoutToken(String url, String postData) throws WxErrorException {
+    return this.executeNormal(SimplePostRequestExecutor.create(this), url, postData);
+  }
+
   /**
    * 向微信端发送请求,在这里执行的策略是当发生access_token过期时才去刷新,然后重新执行请求,而不是全局定时请求.
    */
@@ -296,6 +302,27 @@ protected  T executeInternal(RequestExecutor executor, String uri, E
     }
   }
 
+  /**
+   * 普通请求,不自动带accessToken
+   */
+  private  T executeNormal(RequestExecutor executor, String uri, E data) throws WxErrorException {
+    try {
+      T result = executor.execute(uri, data, WxType.CP);
+      log.debug("\n【请求地址】: {}\n【请求参数】:{}\n【响应数据】:{}", uri, data, result);
+      return result;
+    } catch (WxErrorException e) {
+      WxError error = e.getError();
+      if (error.getErrorCode() != 0) {
+        log.error("\n【请求地址】: {}\n【请求参数】:{}\n【错误信息】:{}", uri, data, error);
+        throw new WxErrorException(error, e);
+      }
+      return null;
+    } catch (IOException e) {
+      log.error("\n【请求地址】: {}\n【请求参数】:{}\n【异常信息】:{}", uri, data, e.getMessage());
+      throw new RuntimeException(e);
+    }
+  }
+
   @Override
   public void setWxCpConfigStorage(WxCpConfigStorage wxConfigProvider) {
     this.configStorage = wxConfigProvider;
@@ -412,6 +439,11 @@ public WxCpOaService getOAService() {
     return oaService;
   }
 
+  @Override
+  public WxCpGroupRobotService getGroupRobotService() {
+    return groupRobotService;
+  }
+
   @Override
   public WxCpTaskCardService getTaskCardService() {
     return taskCardService;
diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpGroupRobotServiceImpl.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpGroupRobotServiceImpl.java
new file mode 100644
index 0000000000..ed4d8a108e
--- /dev/null
+++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpGroupRobotServiceImpl.java
@@ -0,0 +1,65 @@
+package me.chanjar.weixin.cp.api.impl;
+
+import lombok.RequiredArgsConstructor;
+import me.chanjar.weixin.common.api.WxConsts;
+import me.chanjar.weixin.common.error.WxErrorException;
+import me.chanjar.weixin.cp.api.WxCpGroupRobotService;
+import me.chanjar.weixin.cp.api.WxCpService;
+import me.chanjar.weixin.cp.bean.WxCpGroupRobotMessage;
+import me.chanjar.weixin.cp.bean.article.NewArticle;
+import me.chanjar.weixin.cp.config.WxCpConfigStorage;
+import me.chanjar.weixin.cp.constant.WxCpApiPathConsts;
+
+import java.util.List;
+
+/**
+ * 微信群机器人消息发送api 实现
+ *
+ * @author yr
+ * @date 2020-08-20
+ */
+@RequiredArgsConstructor
+public class WxCpGroupRobotServiceImpl implements WxCpGroupRobotService {
+  private final WxCpService cpService;
+
+  private String getApiUrl() {
+    WxCpConfigStorage wxCpConfigStorage = cpService.getWxCpConfigStorage();
+    return wxCpConfigStorage.getApiUrl(WxCpApiPathConsts.WEBHOOK_SEND) + wxCpConfigStorage.getWebhookKey();
+  }
+
+  @Override
+  public void sendText(String content, List mentionedList, List mobileList) throws WxErrorException {
+    WxCpGroupRobotMessage message = new WxCpGroupRobotMessage()
+      .setMsgType(WxConsts.GroupRobotMsgType.TEXT)
+      .setContent(content)
+      .setMentionedList(mentionedList)
+      .setMentionedMobileList(mobileList);
+    cpService.postWithoutToken(this.getApiUrl(), message.toJson());
+  }
+
+  @Override
+  public void sendMarkDown(String content) throws WxErrorException {
+    WxCpGroupRobotMessage message = new WxCpGroupRobotMessage()
+      .setMsgType(WxConsts.GroupRobotMsgType.MARKDOWN)
+      .setContent(content);
+    cpService.postWithoutToken(this.getApiUrl(), message.toJson());
+  }
+
+  @Override
+  public void sendImage(String base64, String md5) throws WxErrorException {
+    WxCpGroupRobotMessage message = new WxCpGroupRobotMessage()
+      .setMsgType(WxConsts.GroupRobotMsgType.IMAGE)
+      .setBase64(base64)
+      .setMd5(md5);
+    cpService.postWithoutToken(this.getApiUrl(), message.toJson());
+  }
+
+  @Override
+  public void sendNews(List articleList) throws WxErrorException {
+    WxCpGroupRobotMessage message = new WxCpGroupRobotMessage()
+      .setMsgType(WxConsts.GroupRobotMsgType.NEWS)
+      .setArticles(articleList);
+    cpService.postWithoutToken(this.getApiUrl(), message.toJson());
+  }
+
+}
diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpGroupRobotMessage.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpGroupRobotMessage.java
new file mode 100644
index 0000000000..937a88cb09
--- /dev/null
+++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpGroupRobotMessage.java
@@ -0,0 +1,118 @@
+package me.chanjar.weixin.cp.bean;
+
+import com.google.gson.JsonArray;
+import com.google.gson.JsonObject;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import lombok.experimental.Accessors;
+import me.chanjar.weixin.cp.bean.article.NewArticle;
+
+import java.util.List;
+
+import static me.chanjar.weixin.common.api.WxConsts.GroupRobotMsgType.*;
+
+/**
+ * 微信群机器人消息
+ *
+ * @author yr
+ * @date 2020-08-20
+ */
+@AllArgsConstructor
+@NoArgsConstructor
+@Accessors(chain = true)
+@Data
+public class WxCpGroupRobotMessage {
+  /**
+   * 消息类型
+   */
+  private String msgType;
+
+  /**
+   * 文本内容,最长不超过2048个字节,markdown内容,最长不超过4096个字节,必须是utf8编码
+   * 必填
+   */
+  private String content;
+  /**
+   * userid的列表,提醒群中的指定成员(@某个成员),@all表示提醒所有人,如果开发者获取不到userid,可以使用mentioned_mobile_list
+   */
+  private List mentionedList;
+  /**
+   * 手机号列表,提醒手机号对应的群成员(@某个成员),@all表示提醒所有人
+   */
+  private List mentionedMobileList;
+  /**
+   * 图片内容的base64编码
+   */
+  private String base64;
+  /**
+   * 图片内容(base64编码前)的md5值
+   */
+  private String md5;
+  /**
+   * 图文消息,一个图文消息支持1到8条图文
+   */
+  private List articles;
+
+  public String toJson() {
+    JsonObject messageJson = new JsonObject();
+    messageJson.addProperty("msgtype", this.getMsgType());
+
+    switch (this.getMsgType()) {
+      case TEXT: {
+        JsonObject text = new JsonObject();
+        JsonArray uidJsonArray = new JsonArray();
+        JsonArray mobileJsonArray = new JsonArray();
+
+        text.addProperty("content", this.getContent());
+
+        if (this.getMentionedList() != null) {
+          for (String item : this.getMentionedList()) {
+            uidJsonArray.add(item);
+          }
+        }
+        if (this.getMentionedMobileList() != null) {
+          for (String item : this.getMentionedMobileList()) {
+            mobileJsonArray.add(item);
+          }
+        }
+        text.add("mentioned_list", uidJsonArray);
+        text.add("mentioned_mobile_list", mobileJsonArray);
+        messageJson.add("text", text);
+        break;
+      }
+      case MARKDOWN: {
+        JsonObject text = new JsonObject();
+        text.addProperty("content", this.getContent());
+        messageJson.add("markdown", text);
+        break;
+      }
+      case IMAGE: {
+        JsonObject text = new JsonObject();
+        text.addProperty("base64", this.getBase64());
+        text.addProperty("md5", this.getMd5());
+        messageJson.add("image", text);
+        break;
+      }
+      case NEWS: {
+        JsonObject text = new JsonObject();
+        JsonArray array = new JsonArray();
+        for (NewArticle article : this.getArticles()) {
+          JsonObject articleJson = new JsonObject();
+          articleJson.addProperty("title", article.getTitle());
+          articleJson.addProperty("description", article.getDescription());
+          articleJson.addProperty("url", article.getUrl());
+          articleJson.addProperty("picurl", article.getPicUrl());
+          array.add(articleJson);
+        }
+        text.add("articles", array);
+        messageJson.add("news", text);
+        break;
+      }
+      default:
+
+    }
+
+    return messageJson.toString();
+  }
+}
diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/WxCpConfigStorage.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/WxCpConfigStorage.java
index 19c04f4be1..e81870ebe2 100644
--- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/WxCpConfigStorage.java
+++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/WxCpConfigStorage.java
@@ -107,7 +107,15 @@ public interface WxCpConfigStorage {
 
   /**
    * 是否自动刷新token
+   *
    * @return .
    */
   boolean autoRefreshToken();
+
+  /**
+   * 获取群机器人webhook的key
+   *
+   * @return key
+   */
+  String getWebhookKey();
 }
diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/impl/WxCpDefaultConfigImpl.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/impl/WxCpDefaultConfigImpl.java
index bd407af6db..a9b449530a 100644
--- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/impl/WxCpDefaultConfigImpl.java
+++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/impl/WxCpDefaultConfigImpl.java
@@ -50,6 +50,8 @@ public class WxCpDefaultConfigImpl implements WxCpConfigStorage, Serializable {
 
   private volatile String baseApiUrl;
 
+  private volatile String webhookKey;
+
   @Override
   public void setBaseApiUrl(String baseUrl) {
     this.baseApiUrl = baseUrl;
@@ -287,6 +289,11 @@ public boolean autoRefreshToken() {
     return true;
   }
 
+  @Override
+  public String getWebhookKey() {
+    return this.webhookKey;
+  }
+
   public void setApacheHttpClientBuilder(ApacheHttpClientBuilder apacheHttpClientBuilder) {
     this.apacheHttpClientBuilder = apacheHttpClientBuilder;
   }
diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/impl/WxCpRedisConfigImpl.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/impl/WxCpRedisConfigImpl.java
index 9133a6ce3e..a92af02d1e 100644
--- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/impl/WxCpRedisConfigImpl.java
+++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/impl/WxCpRedisConfigImpl.java
@@ -46,6 +46,8 @@ public class WxCpRedisConfigImpl implements WxCpConfigStorage {
 
   protected volatile String baseApiUrl;
 
+  private volatile String webhookKey;
+
   @Override
   public void setBaseApiUrl(String baseUrl) {
     this.baseApiUrl = baseUrl;
@@ -344,6 +346,11 @@ public boolean autoRefreshToken() {
     return true;
   }
 
+  @Override
+  public String getWebhookKey() {
+    return this.getWebhookKey();
+  }
+
   public void setApacheHttpClientBuilder(ApacheHttpClientBuilder apacheHttpClientBuilder) {
     this.apacheHttpClientBuilder = apacheHttpClientBuilder;
   }
diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/constant/WxCpApiPathConsts.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/constant/WxCpApiPathConsts.java
index 6b5e9444c4..eaaf937d08 100644
--- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/constant/WxCpApiPathConsts.java
+++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/constant/WxCpApiPathConsts.java
@@ -21,6 +21,7 @@ public final class WxCpApiPathConsts {
   public static final String BATCH_GET_RESULT = "/cgi-bin/batch/getresult?jobid=";
   public static final String JSCODE_TO_SESSION = "/cgi-bin/miniprogram/jscode2session";
   public static final String GET_TOKEN = "/cgi-bin/gettoken?corpid=%s&corpsecret=%s";
+  public static final String WEBHOOK_SEND = "/cgi-bin/webhook/send?key=";
 
   public static class Agent {
     public static final String AGENT_GET = "/cgi-bin/agent/get?agentid=%d";
diff --git a/weixin-java-cp/src/test/java/me/chanjar/weixin/cp/api/WxCpGroupRobotServiceTest.java b/weixin-java-cp/src/test/java/me/chanjar/weixin/cp/api/WxCpGroupRobotServiceTest.java
new file mode 100644
index 0000000000..126a215b78
--- /dev/null
+++ b/weixin-java-cp/src/test/java/me/chanjar/weixin/cp/api/WxCpGroupRobotServiceTest.java
@@ -0,0 +1,66 @@
+package me.chanjar.weixin.cp.api;
+
+import com.google.inject.Inject;
+import lombok.extern.slf4j.Slf4j;
+import me.chanjar.weixin.common.error.WxErrorException;
+import me.chanjar.weixin.common.util.fs.FileUtils;
+import me.chanjar.weixin.cp.bean.article.NewArticle;
+import org.testng.annotations.BeforeTest;
+import org.testng.annotations.Guice;
+import org.testng.annotations.Test;
+
+import java.io.InputStream;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import static org.testng.Assert.*;
+
+/**
+ * 微信群机器人消息发送api 单元测试
+ *
+ * @author yr
+ * @date 2020-08-20
+ */
+@Slf4j
+@Guice(modules = ApiTestModule.class)
+public class WxCpGroupRobotServiceTest {
+
+  @Inject
+  protected WxCpService wxService;
+
+  private WxCpGroupRobotService robotService;
+
+  @BeforeTest
+  public void setup() {
+    robotService = wxService.getGroupRobotService();
+  }
+
+  @Test
+  public void testSendText() throws WxErrorException {
+    robotService.sendText("Hello World", null, null);
+  }
+
+  @Test
+  public void testSendMarkDown() throws WxErrorException {
+    String content = "实时新增用户反馈132例,请相关同事注意。\n" +
+      ">类型:用户反馈 \n" +
+      ">普通用户反馈:117例 \n" +
+      ">VIP用户反馈:15例";
+    robotService.sendMarkDown(content);
+  }
+
+  @Test
+  public void testSendImage() throws WxErrorException {
+    InputStream inputStream = getClass().getClassLoader().getResourceAsStream("mm.jpeg");
+    assert inputStream != null;
+    String base64 = FileUtils.imageToBase64ByStream(inputStream);
+    String md5 = "1cb2e787063d66e24f5f89e7fc267a4d";
+    robotService.sendImage(base64, md5);
+  }
+
+  @Test
+  public void testSendNews() throws WxErrorException {
+    NewArticle article = new NewArticle("图文消息测试","hello world","http://www.baidu.com","http://res.mail.qq.com/node/ww/wwopenmng/images/independent/doc/test_pic_msg1.png");
+    robotService.sendNews(Stream.of(article).collect(Collectors.toList()));
+  }
+}
diff --git a/weixin-java-cp/src/test/resources/test-config.sample.xml b/weixin-java-cp/src/test/resources/test-config.sample.xml
index 6f98ebafc1..76a74a25a3 100644
--- a/weixin-java-cp/src/test/resources/test-config.sample.xml
+++ b/weixin-java-cp/src/test/resources/test-config.sample.xml
@@ -10,4 +10,5 @@
   企业号通讯录的某个部门id
   企业号通讯录里的某个tagid
   网页授权获取用户信息回调地址
+  webhook链接地址的key值
 
diff --git a/weixin-java-cp/src/test/resources/testng.xml b/weixin-java-cp/src/test/resources/testng.xml
index ffa8a2edff..4b94015661 100644
--- a/weixin-java-cp/src/test/resources/testng.xml
+++ b/weixin-java-cp/src/test/resources/testng.xml
@@ -7,6 +7,7 @@
       
       
       
+