建議先簡單了解企業微信開發者中心文檔:開發前必讀 - 文檔 - 企業微信開發者中心
了解一下企業微信調用接口的基礎參數:基本概念介紹 - 文檔 - 企業微信開發者中心
本篇每個步驟都會跟著官網文檔走,都會貼上相關鏈接,看完本篇文章,可以獲得技能:
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_tokenObject cacheObject = redisCache.getCacheObject("wx_access_token");//如果存在,直接返回access_tokenif(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 {@Beanpublic 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 {@Autowiredprivate WebClient webClient;@Autowiredprivate RedisCache redisCache;@Autowiredprivate 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_tokenObject cacheObject = redisCache.getCacheObject("wx_access_token");//如果存在,直接返回access_tokenif(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 {@Autowiredprivate WxService wxService;@DisplayName("獲取accessToken")@Testpublic void test1(){//獲取accessTokenString accessToken = wxService.getAccessTokenByRedis();System.out.println("accessToken:"+accessToken);}@DisplayName("推送信息-文本")@Testpublic 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("推送信息-文本卡片")@Testpublic 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);//textcardMap<String,Object> map = wxService.pushMessage(weChatMessageDTO);System.out.println(map);}}
效果圖:
小彈窗:
窗口: