mirror of
				https://gitee.com/binary/weixin-java-tools.git
				synced 2025-10-31 10:38:42 +08:00 
			
		
		
		
	🆕 #1720 增加企业微信群机器人消息发送接口
* #1720 增加群机器人的消息类型 * #1720 增加文件流生成base64方法,用于图片转base64,群机器人图片消息发送测试 * #1720 增加群机器人消息推送地址webhook/send * #1720 增加群机器人webhook_key配置属性 * #1720 增加群机器人消息推送接口服务、不需要自动带accessToken的post请求接口 * #1720 新增微信群机器人消息发送api * #1720 新增微信群机器人消息发送api单元测试 * #1720 新增微信群机器人消息发送api单元测试配置、新增属性webhook配置 Co-authored-by: yang ran <yangran@xytdt.com>
This commit is contained in:
		| @ -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<String> mentionedList, List<String> 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<NewArticle> articleList) throws WxErrorException; | ||||
| } | ||||
| @ -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; | ||||
|  | ||||
|   /** | ||||
|    * <pre> | ||||
|    * Service没有实现某个API的时候,可以用这个, | ||||
| @ -328,6 +336,13 @@ public interface WxCpService { | ||||
|  | ||||
|   WxCpOaService getOAService(); | ||||
|  | ||||
|   /** | ||||
|    * 获取群机器人消息推送服务 | ||||
|    * | ||||
|    * @return 群机器人消息推送服务 | ||||
|    */ | ||||
|   WxCpGroupRobotService getGroupRobotService(); | ||||
|  | ||||
|   /** | ||||
|    * http请求对象 | ||||
|    */ | ||||
|  | ||||
| @ -51,6 +51,7 @@ public abstract class BaseWxCpServiceImpl<H, P> 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 abstract class BaseWxCpServiceImpl<H, P> implements WxCpService, RequestH | ||||
|     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 @@ public abstract class BaseWxCpServiceImpl<H, P> implements WxCpService, RequestH | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * 普通请求,不自动带accessToken | ||||
|    */ | ||||
|   private <T, E> T executeNormal(RequestExecutor<T, E> 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 abstract class BaseWxCpServiceImpl<H, P> implements WxCpService, RequestH | ||||
|     return oaService; | ||||
|   } | ||||
|  | ||||
|   @Override | ||||
|   public WxCpGroupRobotService getGroupRobotService() { | ||||
|     return groupRobotService; | ||||
|   } | ||||
|  | ||||
|   @Override | ||||
|   public WxCpTaskCardService getTaskCardService() { | ||||
|     return taskCardService; | ||||
|  | ||||
| @ -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<String> mentionedList, List<String> 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<NewArticle> articleList) throws WxErrorException { | ||||
|     WxCpGroupRobotMessage message = new WxCpGroupRobotMessage() | ||||
|       .setMsgType(WxConsts.GroupRobotMsgType.NEWS) | ||||
|       .setArticles(articleList); | ||||
|     cpService.postWithoutToken(this.getApiUrl(), message.toJson()); | ||||
|   } | ||||
|  | ||||
| } | ||||
| @ -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<String> mentionedList; | ||||
|   /** | ||||
|    * 手机号列表,提醒手机号对应的群成员(@某个成员),@all表示提醒所有人 | ||||
|    */ | ||||
|   private List<String> mentionedMobileList; | ||||
|   /** | ||||
|    * 图片内容的base64编码 | ||||
|    */ | ||||
|   private String base64; | ||||
|   /** | ||||
|    * 图片内容(base64编码前)的md5值 | ||||
|    */ | ||||
|   private String md5; | ||||
|   /** | ||||
|    * 图文消息,一个图文消息支持1到8条图文 | ||||
|    */ | ||||
|   private List<NewArticle> 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(); | ||||
|   } | ||||
| } | ||||
| @ -107,7 +107,15 @@ public interface WxCpConfigStorage { | ||||
|  | ||||
|   /** | ||||
|    * 是否自动刷新token | ||||
|    * | ||||
|    * @return . | ||||
|    */ | ||||
|   boolean autoRefreshToken(); | ||||
|  | ||||
|   /** | ||||
|    * 获取群机器人webhook的key | ||||
|    * | ||||
|    * @return key | ||||
|    */ | ||||
|   String getWebhookKey(); | ||||
| } | ||||
|  | ||||
| @ -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 class WxCpDefaultConfigImpl implements WxCpConfigStorage, Serializable { | ||||
|     return true; | ||||
|   } | ||||
|  | ||||
|   @Override | ||||
|   public String getWebhookKey() { | ||||
|     return this.webhookKey; | ||||
|   } | ||||
|  | ||||
|   public void setApacheHttpClientBuilder(ApacheHttpClientBuilder apacheHttpClientBuilder) { | ||||
|     this.apacheHttpClientBuilder = apacheHttpClientBuilder; | ||||
|   } | ||||
|  | ||||
| @ -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 class WxCpRedisConfigImpl implements WxCpConfigStorage { | ||||
|     return true; | ||||
|   } | ||||
|  | ||||
|   @Override | ||||
|   public String getWebhookKey() { | ||||
|     return this.getWebhookKey(); | ||||
|   } | ||||
|  | ||||
|   public void setApacheHttpClientBuilder(ApacheHttpClientBuilder apacheHttpClientBuilder) { | ||||
|     this.apacheHttpClientBuilder = apacheHttpClientBuilder; | ||||
|   } | ||||
|  | ||||
| @ -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"; | ||||
|  | ||||
| @ -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 = "实时新增用户反馈<font color=\"warning\">132例</font>,请相关同事注意。\n" + | ||||
|       ">类型:<font color=\"comment\">用户反馈</font> \n" + | ||||
|       ">普通用户反馈:<font color=\"comment\">117例</font> \n" + | ||||
|       ">VIP用户反馈:<font color=\"comment\">15例</font>"; | ||||
|     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())); | ||||
|   } | ||||
| } | ||||
| @ -10,4 +10,5 @@ | ||||
|   <departmentId>企业号通讯录的某个部门id</departmentId> | ||||
|   <tagId>企业号通讯录里的某个tagid</tagId> | ||||
|   <oauth2redirectUri>网页授权获取用户信息回调地址</oauth2redirectUri> | ||||
|   <webhookKey>webhook链接地址的key值</webhookKey> | ||||
| </xml> | ||||
|  | ||||
| @ -7,6 +7,7 @@ | ||||
|       <class name="me.chanjar.weixin.cp.api.WxCpBaseAPITest"/> | ||||
|       <class name="me.chanjar.weixin.cp.api.WxCpMessageAPITest"/> | ||||
|       <class name="me.chanjar.weixin.cp.api.WxCpMessageRouterTest"/> | ||||
|       <class name="me.chanjar.weixin.cp.api.WxCpGroupRobotServiceTest"/> | ||||
|     </classes> | ||||
|   </test> | ||||
|  | ||||
|  | ||||
		Reference in New Issue
	
	Block a user
	 xyz9025
					xyz9025