建议先简单了解企业微信开发者中心文档:开发前必读 - 文档 - 企业微信开发者中心
了解一下企业微信调用接口的基础参数:基本概念介绍 - 文档 - 企业微信开发者中心
本篇每个步骤都会跟着官网文档走,都会贴上相关链接,看完本篇文章,可以获得技能:
1、学会查看各应用的开发文档
2、学会调用接口,对接口的处理
3、学会redis和redisson的使用
4、学会封装数据传输对象(DTO)来调用接口和获取返回值
5、学会使用Spring WebFlux的非阻塞HTTP客户端的使用
实现所需以下步骤:
1、获取企业的ID和SecretID
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出发前可查看<a href=\"https://work.weixin.qq.com\">邮件中心视频实况</a>,聪明避开排队。"
},
"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" : "<div class=\"gray\">2016年9月26日</div> <div class=\"normal\">恭喜你抽中iPhone 7一台,领奖码:xxxx</div><div class=\"highlight\">请于2016年10月10日前联系行政同事领取</div>",
"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
<!-- webflux-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
5.2、配置WebClient(我们只需要用到webflux的http客户端)
@Configuration
public 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
@Data
public 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
@Data
public 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
@Service
public 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<String, Object> response = webClient.get()
.uri(url)
.retrieve()
.bodyToMono(new ParameterizedTypeReference<Map<String, Object>>() {})
.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<String,Object> 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<String, Object>>() {})
.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<String, Object> 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<String,Object> map = wxService.pushMessage(weChatMessageDTO);
System.out.println(map);
}
@DisplayName("推送信息-文本卡片")
@Test
public void test3(){
WeChatTextDTO weChatTextDTO = new WeChatTextDTO();
weChatTextDTO.setTitle("CRM系统通知");
weChatTextDTO.setDescription("<div class=\"gray\">2025年07月01日</div> <div class=\"normal\">天气不错</div><div class=\"highlight\">在一个阳光明媚的下午....</div>");
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<String,Object> map = wxService.pushMessage(weChatMessageDTO);
System.out.println(map);
}
}
效果图:
小弹窗:
窗口: