> 技术文档 > (JAVA)自建应用调用企业微信API接口,实现消息推送_企业微信消息推送接口

(JAVA)自建应用调用企业微信API接口,实现消息推送_企业微信消息推送接口

建议先简单了解企业微信开发者中心文档:开发前必读 - 文档 - 企业微信开发者中心

了解一下企业微信调用接口的基础参数:基本概念介绍 - 文档 - 企业微信开发者中心

本篇每个步骤都会跟着官网文档走,都会贴上相关链接,看完本篇文章,可以获得技能:

1、学会查看各应用的开发文档

2、学会调用接口,对接口的处理

3、学会redis和redisson的使用

4、学会封装数据传输对象(DTO)来调用接口和获取返回值

5、学会使用Spring WebFlux的非阻塞HTTP客户端的使用

实现所需以下步骤:

1、获取企业的IDSecretID

2、根据ID和SecretID从而获取access_token

3、对access_token进行缓存

4、调用接口发送信息

1、获取企业的ID和SecretID

操作:先注册好你的企业 -> 点击头像 -> 管理企业

进入管理企业页面后

操作:应用管理 - > 创建应用

进入创建应用后

填写你的企业信息,点击创建应用

创建应用完成后

操作:返回应用管理 - > 点击你新创建的应用

1.1、获取到企业的SecretID

操作:点击查看就能获取SecretID(会发送到你的企业微信)

1.2、获取到企业的ID

操作 :我的企业 - > 企业信息 - > 拉到最底下能看到企业ID

2、根据ID和SecretID从而获取access_token

文档对应位置:获取access_token - 文档 - 企业微信开发者中心

注意点:

请求方式: GET(HTTPS
请求地址: https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=ID&corpsecret=SECRET

参数 必须 说明 corpid 是 企业ID,获取方式参考:术语说明-corpid corpsecret 是 应用的凭证密钥,注意应用需要是启用状态,获取方式参考:术语说明-secret

2.1、获取access_token

1、使用postman获取

返回结果:

{ \"errcode\": 0, \"errmsg\": \"ok\", \"access_token\": \"accesstoken000001\", \"expires_in\": 7200 }

翻译:

errcode 出错返回码,为0表示成功,非0表示调用失败 errmsg 返回码提示语 access_token 获取到的凭证,最长为512字节 expires_in 凭证的有效时间(秒)

注意事项:(在文中第3点会贴出代码,来实现如何通过redis来对access_token进行缓存
开发者需要缓存access_token,用于后续接口的调用(注意:不能频繁调用gettoken接口,否则会受到频率拦截)。当access_token失效或过期时,需要重新获取。

access_token的有效期通过返回的expires_in来传达,正常情况下为7200秒(2小时)。
由于企业微信每个应用的access_token是彼此独立的,所以进行缓存时需要区分应用来进行存储。
access_token至少保留512字节的存储空间。
企业微信可能会出于运营需要,提前使access_token失效,开发者应实现access_token失效时重新获取的逻辑。

2、springboot单元测试获取

wxService代码在后面讲到redis缓存access_token时会贴上

2.2、获取失败解决 - 新版本需要加入企业可信IP

意思是不允许你当前的ip进行访问(你的ip不可信)

新版本企业微信需要配置企业可信IP才能使用

配置IP地址:

如果出现以下情况:配置企业可信IP前,请先 设置可信域名 或 设置接收消息服务器URL

详情解决办法请看我另外一篇文章:

(JAVA)自建应用调用企业微信API接口,设置企业可信IP-CSDN博客

3、对access_token进行缓存

使用redis+redisson分布式锁,对access_token进行相关缓存操作。

代码实现:(思路逻辑都已经备注在代码里)

public String getAccessTokenByRedis() { //从redis中获取wx_access_token Object cacheObject = redisCache.getCacheObject(\"wx_access_token\"); //如果存在,直接返回access_token if(cacheObject != null){ return cacheObject.toString(); } //如果不存在,获取分布式锁 RLock lock = redissonClient.getLock(\"wx_access_token_lock\"); //默认未上锁 boolean locked = false; try { //尝试获取锁,最多等待3秒,上锁后10秒自动释放 locked = lock.tryLock(3,10, TimeUnit.SECONDS); //如果获取到锁,再次从redis中获取access_token,防止在上锁期间,其他线程已经获取到锁并更新了access_token。 if (locked) { cacheObject = redisCache.getCacheObject(\"wx_access_token\"); if(cacheObject != null){  return cacheObject.toString(); } String accessToken = getAccessTokenByApi(); //企业微信接口的返回值access_token有效期为7200秒,这里存入redis设置为7000秒,防止临界值过期问题。 redisCache.setCacheObject(\"wx_access_token\", accessToken,7000, TimeUnit.SECONDS); return accessToken; }else{ //未取到锁 throw new RuntimeException(\"获取 access_token 超时,请稍后再试\"); } }catch (Exception e){ throw new RuntimeException(\"Redisson 锁被中断\", e); }finally { if(locked && lock.isHeldByCurrentThread()){ lock.unlock(); } } }

代码中的:redisCache是ruoyi框架封装好的redis工具类,调用的是redis的redisTemplate,可自行封装。

getCacheObject:

setCacheObject:

4、调用接口发送信息

了解调用消息推送的传输过程:概述 - 文档 - 企业微信开发者中心

4.1、查看发送应用消息接口文档

发送应用消息 - 文档 - 企业微信开发者中心

4.2、接口地址与请求方式

请求方式:POST(HTTPS
请求地址: https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token=ACCESS_TOKEN

参数 是否必须 说明 access_token 是 调用接口凭证

4.3、返回示例

{
  \"errcode\" : 0,
  \"errmsg\" : \"ok\",
  \"invaliduser\" : \"userid1|userid2\",
  \"invalidparty\" : \"partyid1|partyid2\",
  \"invalidtag\": \"tagid1|tagid2\",
  \"unlicenseduser\" : \"userid3|userid4\",
  \"msgid\": \"xxxx\",
  \"response_code\": \"xyzxyz\"
}

返回结果说明:

参数 说明 errcode 返回码 errmsg 对返回码的文本描述内容 invaliduser 不合法的userid,不区分大小写,统一转为小写 invalidparty 不合法的partyid invalidtag 不合法的标签id unlicenseduser 没有基础接口许可(包含已过期)的userid msgid 消息id,用于撤回应用消息 response_code 仅消息类型为“按钮交互型”,“投票选择型”和“多项选择型”的模板卡片消息返回,应用可使用response_code调用更新模版卡片消息接口,72小时内有效,且只能使用一次/4、

4.4、请求示例

4.4.1请求消息类型(文本

{
   \"touser\" : \"UserID1|UserID2|UserID3\",
   \"toparty\" : \"PartyID1|PartyID2\",
   \"totag\" : \"TagID1 | TagID2\",
   \"msgtype\" : \"text\",
   \"agentid\" : 1,
   \"text\" : {
       \"content\" : \"你的快递已到,请携带工卡前往邮件中心领取。\\n出发前可查看邮件中心视频实况,聪明避开排队。\"
   },
   \"safe\":0,
   \"enable_id_trans\": 0,
   \"enable_duplicate_check\": 0,
   \"duplicate_check_interval\": 1800
}

参数说明:

参数 是否必须 说明 touser 否 指定接收消息的成员,成员ID列表(多个接收者用‘|’分隔,最多支持1000个)。
特殊情况:指定为\"@all\",则向该企业应用的全部成员发送 toparty 否 指定接收消息的部门,部门ID列表,多个接收者用‘|’分隔,最多支持100个。
当touser为\"@all\"时忽略本参数 totag 否 指定接收消息的标签,标签ID列表,多个接收者用‘|’分隔,最多支持100个。
当touser为\"@all\"时忽略本参数 msgtype 是 消息类型,此时固定为:text agentid 是 企业应用的id,整型。企业内部开发,可在应用的设置页面查看;第三方服务商,可通过接口 获取企业授权信息 获取该参数值 content 是 消息内容,最长不超过2048个字节,超过将截断(支持id转译) safe 否 表示是否是保密消息,0表示可对外分享,1表示不能分享且内容显示水印,默认为0 enable_id_trans 否 表示是否开启id转译,0表示否,1表示是,默认0。 enable_duplicate_check 否 表示是否开启重复消息检查,0表示否,1表示是,默认0 duplicate_check_interval 否 表示是否重复消息检查的时间间隔,默认1800s,最大不超过4小时

注意:touser、toparty、totag不能同时为空,后面不再强调。

注意:成员ID是企业微信的唯一标识,查看方式如下:

注意:可以进行编辑修改,只有一次机会

修改建议:企业微信成员的企业邮箱作为账号成员ID

样式如下:

4.4.2请求消息类型(文本卡片

{
   \"touser\" : \"UserID1|UserID2|UserID3\",
   \"toparty\" : \"PartyID1 | PartyID2\",
   \"totag\" : \"TagID1 | TagID2\",
   \"msgtype\" : \"textcard\",
   \"agentid\" : 1,
   \"textcard\" : {
            \"title\" : \"领奖通知\",
            \"description\" : \"

2016年9月26日
恭喜你抽中iPhone 7一台,领奖码:xxxx
请于2016年10月10日前联系行政同事领取

\",
            \"url\" : \"URL\",
                        \"btntxt\":\"更多\"
   },
   \"enable_id_trans\": 0,
   \"enable_duplicate_check\": 0,
   \"duplicate_check_interval\": 1800
}

参数 是否必须 说明 touser 否 成员ID列表(消息接收者,多个接收者用‘|’分隔,最多支持1000个)。特殊情况:指定为@all,则向关注该企业应用的全部成员发送 toparty 否 部门ID列表,多个接收者用‘|’分隔,最多支持100个。当touser为@all时忽略本参数 totag 否 标签ID列表,多个接收者用‘|’分隔,最多支持100个。当touser为@all时忽略本参数 msgtype 是 消息类型,此时固定为:textcard agentid 是 企业应用的id,整型。企业内部开发,可在应用的设置页面查看;第三方服务商,可通过接口 获取企业授权信息 获取该参数值 title 是 标题,不超过128个字符,超过会自动截断(支持id转译) description 是 描述,不超过512个字符,超过会自动截断(支持id转译) url 是 点击后跳转的链接。最长2048字节,请确保包含了协议头(http/https) btntxt 否 按钮文字。 默认为“详情”, 不超过4个文字,超过自动截断。 enable_id_trans 否 表示是否开启id转译,0表示否,1表示是,默认0 enable_duplicate_check 否 表示是否开启重复消息检查,0表示否,1表示是,默认0 duplicate_check_interval 否 表示是否重复消息检查的时间间隔,默认1800s,最大不超过4小时

样式如下:

4.5接口工具测试(postman)

4.5.1、带上请求参数param

4.5.2、带上请求体

4.5.3、结果

5、使用springboot整合Spring-WebFlux开发发送消息接口

5.1、maven导入Spring-WebFlux

   org.springframework.boot spring-boot-starter-webflux 

5.2、配置WebClient(我们只需要用到webflux的http客户端)

@Configurationpublic class WebClientConfig { @Bean public WebClient webClient() { return WebClient.builder() .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) .build(); }}

5.3、新增数据传输对象(DTO)

根据第4节的请求示例新建DTO

5.3.1 WeChatMessageDTO
@Datapublic class WeChatMessageDTO { /** * 指定接收消息的成员,成员ID列表(多个接收者用‘|’分隔,最多支持1000个)。 * 特殊情况:指定为\"@all\",则向该企业应用的全部成员发送 */ private String touser; /** * 指定接收消息的部门,部门ID列表,多个接收者用‘|’分隔,最多支持100个。 * 当touser为\"@all\"时忽略本参数 */ private String toparty; /** * 指定接收消息的标签,标签ID列表,多个接收者用‘|’分隔,最多支持100个。 * 当touser为\"@all\"时忽略本参数 */ private String totag; /** * 消息类型,text为返回的文本 */ private String msgtype; /** * 企业应用的id,整型。企业内部开发,可在应用的设置页面查看;第三方服务商,可通过接口 获取企业授权信息 获取该参数值 */ private Integer agentid; /** * 消息内容,最长不超过2048个字节,超过将截断(支持id转译) */ private WeChatTextDTO text; /** * 卡片消息的展现形式非常灵活,支持使用br标签或者空格来进行换行处理,也支持使用div标签来使用不同的字体颜色,目前内置了3种文字颜色:灰色(gray)、高亮(highlight)、默认黑色(normal),将其作为div标签的class属性即可,具体用法请参考上面的示例。 */ private WeChatTextDTO textcard; /** * 表示是否是保密消息,0表示可对外分享,1表示不能分享且内容显示水印,默认为0 */ private Integer safe; /** * 表示是否开启id转译,0表示否,1表示是,默认0。 */ private Integer enable_id_trans; /** * 表示是否开启重复消息检查,0表示否,1表示是,默认0 */ private Integer enable_duplicate_check; /** * 表示是否重复消息检查的时间间隔,默认1800s,最大不超过4小时 */ private Integer duplicate_check_interval; //touser、toparty、totag不能同时为空,后面不再强调。}
5.3.2 WeChatTextDTO
@Datapublic class WeChatTextDTO { //text专用 private String content;//消息内容,最长不超过2048个字节,超过将截断(支持id转译) //textcard专用 private String title;//标题,不超过128个字符,超过会自动截断(支持id转译) private String description;//描述,不超过512个字符,超过会自动截断(支持id转译) private String url;//点击后跳转的链接。最长2048字节,请确保包含了协议头(http/https) private String btntxt;//按钮文字。 默认为“详情”, 不超过4个文字,超过自动截断。 //图文、语音、视频等...}

5.4 WxService

@Servicepublic class WxService { @Autowired private WebClient webClient; @Autowired private RedisCache redisCache; @Autowired private RedissonClient redissonClient; private static final Logger logger = LoggerFactory.getLogger(WxService.class); //企业微信所需凭证 private static final String corpid = \"在第一节的1.2获取到的企业ID\"; private static final String corpsecret = \"在一节的1.1获取到的企业SecretID\"; private String getAccessTokenByApi(){ String url = \"https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=\" + corpid + \"&corpsecret=\" + corpsecret; Map response = webClient.get() .uri(url) .retrieve() .bodyToMono(new ParameterizedTypeReference<Map>() {}) .block(); if (response == null || !response.containsKey(\"access_token\")) { throw new RuntimeException(\"获取 access_token 失败:\" + response); } return response.get(\"access_token\").toString(); } public String getAccessTokenByRedis() { //从redis中获取wx_access_token Object cacheObject = redisCache.getCacheObject(\"wx_access_token\"); //如果存在,直接返回access_token if(cacheObject != null){ return cacheObject.toString(); } //如果不存在,获取分布式锁 RLock lock = redissonClient.getLock(\"wx_access_token_lock\"); //默认未上锁 boolean locked = false; try { //尝试获取锁,最多等待3秒,上锁后10秒自动释放 locked = lock.tryLock(3,10, TimeUnit.SECONDS); //如果获取到锁,再次从redis中获取access_token,防止在上锁期间,其他线程已经获取到锁并更新了access_token。 if (locked) { cacheObject = redisCache.getCacheObject(\"wx_access_token\"); if(cacheObject != null){  return cacheObject.toString(); } String accessToken = getAccessTokenByApi(); //企业微信接口的返回值access_token有效期为7200秒,这里设置为7000秒,防止临界值过期问题。 //将access_token存入redis,有效期7000秒。 redisCache.setCacheObject(\"wx_access_token\", accessToken,7000, TimeUnit.SECONDS); return accessToken; }else{ //未取到锁 throw new RuntimeException(\"获取 access_token 超时,请稍后再试\"); } }catch (Exception e){ throw new RuntimeException(\"Redisson 锁被中断\", e); }finally { if(locked && lock.isHeldByCurrentThread()){ lock.unlock(); } } } public Map pushMessage(WeChatMessageDTO weChatMessageDTO){ String accessToken = getAccessTokenByRedis(); String url = \"https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token=\"+accessToken; return webClient.post() .uri(url) .contentType(MediaType.APPLICATION_JSON) .bodyValue(weChatMessageDTO) .retrieve() .bodyToMono(new ParameterizedTypeReference<Map>() {}) .block(); // 阻塞获取响应(适用于同步调用场景) } /** * 推送企业微信通知 */ public void sendWeChatNotification(String title, String content, String receiverWxId) { try { WeChatTextDTO textDTO = new WeChatTextDTO(); textDTO.setTitle(title); textDTO.setDescription(content); textDTO.setUrl(\"填写你要跳转的URL\"); textDTO.setBtntxt(\"点击查看\"); WeChatMessageDTO messageDTO = new WeChatMessageDTO(); messageDTO.setTouser(receiverWxId); messageDTO.setMsgtype(\"textcard\"); messageDTO.setAgentid(1000002); messageDTO.setTextcard(textDTO); Map result = pushMessage(messageDTO); logger.info(\"企业微信推送结果: {}\", result); } catch (Exception e) { logger.error(\"企业微信推送失败\", e); } }}

5.5 WxTest

使用springboot的单元测试来测试。

@SpringBootTest@DisplayName(\"单元测试案例\")public class WxTest { @Autowired private WxService wxService; @DisplayName(\"获取accessToken\") @Test public void test1(){ //获取accessToken String accessToken = wxService.getAccessTokenByRedis(); System.out.println(\"accessToken:\"+accessToken); } @DisplayName(\"推送信息-文本\") @Test public void test2(){ WeChatTextDTO weChatTextDTO = new WeChatTextDTO(); weChatTextDTO.setContent(\"私聊测试\"); WeChatMessageDTO weChatMessageDTO = new WeChatMessageDTO(); weChatMessageDTO.setTouser(\"ChenPengWei\");//填入企业ID则是私聊 weChatMessageDTO.setMsgtype(\"text\"); weChatMessageDTO.setAgentid(1000002); weChatMessageDTO.setText(weChatTextDTO); Map map = wxService.pushMessage(weChatMessageDTO); System.out.println(map); } @DisplayName(\"推送信息-文本卡片\") @Test public void test3(){ WeChatTextDTO weChatTextDTO = new WeChatTextDTO(); weChatTextDTO.setTitle(\"CRM系统通知\"); weChatTextDTO.setDescription(\"
2025年07月01日
天气不错
在一个阳光明媚的下午....
\"); weChatTextDTO.setUrl(\"https://www.baidu.com\"); weChatTextDTO.setBtntxt(\"点击查看\"); WeChatMessageDTO weChatMessageDTO = new WeChatMessageDTO(); weChatMessageDTO.setTouser(\"@all\");//@all是公告,群广播 weChatMessageDTO.setMsgtype(\"textcard\"); weChatMessageDTO.setAgentid(1000002); weChatMessageDTO.setTextcard(weChatTextDTO);//textcard Map map = wxService.pushMessage(weChatMessageDTO); System.out.println(map); }}

效果图:

小弹窗:

窗口: