目錄
1 OA-審批
1.1 場景描述
(1)企業可通過審批應用或自建應用secret換取access_token,用於企業微信審批應用相關接口調用。
(2)首先,可通過“獲取審批模板詳情”接口,了解模板內的控件構成及控件id。
(3)然后,可通過“提交審批申請”,利用模板id和控件id,代員工發起和填寫審批申請,自定義審批流程。
(4)審批前后,可通過“審批申請狀態變化回調通知”,訂閱審批單據流轉的變化,進行各項拓展動作。
(5)此外,還可通過“批量獲取審批編號”、“獲取審批申請詳情”接口,隨時獲取審批申請的內容詳情和流程狀態。
1.2 與審批流程引擎的區別
(1)企業微信審批應用相關接口,是圍繞“審批應用”的開放,數據的寫入、讀取對象都為企業微信“審批應用”。
(2)“審批流程引擎”相關接口,是在“自建應用”或“第三方應用”中增加流程相關功能,使用和作用對象都為“自建應用”或“第三方應用”,不會影響企業微信“審批應用”。
(3)注:如下圖所示指向的"審批"和"自建審批應用"都屬於是企業內部開發,其獲取AccessToken的方式參考“獲取access_token”。而第三方應用獲取AccessToken的方式參考參考“獲取企業憑證”。這里有個概念就行,后面主要講企業內部開發的兩種方式。

2 獲取審批模板詳情
企業可通過審批應用或自建應用Secret調用本接口,獲取企業微信“審批應用”內指定審批模板的詳情。官方文檔鏈接
企業微信官方線上調試:調試工具
請求方式:POST
請求地址:https://qyapi.weixin.qq.com/cgi-bin/oa/gettemplatedetail?access_token=ACCESS_TOKEN
請求示例:
{
"template_id" : "3TmmcVxSXNgmZJM8ZpbrZVuucxadbweg7pdtEmaQ"
}
較早時間創建的模板,id為類似“1910324946027731_1688852032423522_1808577376_15111111111”的數字串。
參數說明:
| 參數 | 必須 | 說明 |
|---|---|---|
| access_token | 是 | 調用接口憑證。必須使用審批應用或企業內自建應用的secret獲取,獲取方式參考:文檔-獲取access_token |
| template_id | 是 | 模板的唯一標識id。可在“獲取審批單據詳情”、“審批狀態變化回調通知”中獲得,也可在審批模板的模板編輯頁面瀏覽器Url鏈接中獲得。 |
1.審批應用的Secret可獲取企業自建模板及第三方服務商添加的模板詳情;自建應用的Secret可獲取企業自建模板的模板詳情。
2.接口調用頻率限制為600次/分鍾。
2.1 線上調試
1、找到"應用管理"->“審批”->“API”->“查看”->“發送”,此時企業微信將會收到該應用的密鑰,自己保存好即可。

2、點擊調試工具,進入官方提供的調試界面,其中corpid指的是企業id(在后台"我的企業"可看到),corpsecret指的是上一步中在企業微信客戶端收到的secret,填寫完后,點擊"獲取access_token"按鈕,即可看到輸出的token,如下圖:

3、找到"應用管理"->“審批”->“請假"模版,最后一個”/“中的字符串為該模版的id,復制即可。

4、將第"3’'步中的id替換掉下圖的"template_id”,點擊"調用接口"按鈕即可。

5、將滾動條拖到底部,可看到如下圖所示輸出該模版的詳情信息。
{
"errcode": 0,
"errmsg": "ok",
"template_names": [
{
"text": "請假",
"lang": "zh_CN"
},
{
"text": "Leave",
"lang": "en"
}
],
"template_content": {
"controls": [
{
"property": {
"control": "Vacation",
"id": "vacation-1563793073898",
"title": [
{
"text": "請假類型",
"lang": "zh_CN"
},
{
"text": "Leave Type",
"lang": "en"
}
],
"placeholder": [
{
"text": "",
"lang": "zh_CN"
}
],
"require": 1,
"un_print": 0
}
},
{
"property": {
"control": "Textarea",
"id": "item-1497581399901",
"title": [
{
"text": "請假事由",
"lang": "zh_CN"
},
{
"text": "Leave Reason",
"lang": "en"
}
],
"placeholder": [
{
"text": "請輸入請假事由",
"lang": "zh_CN"
},
{
"text": "Enter a reason",
"lang": "en"
}
],
"require": 0,
"un_print": 0
}
},
{
"property": {
"control": "File",
"id": "item-1497581426169",
"title": [
{
"text": "說明附件",
"lang": "zh_CN"
},
{
"text": "Attachment",
"lang": "en"
}
],
"placeholder": [
{
"text": "",
"lang": "zh_CN"
}
],
"require": 0,
"un_print": 1
}
}
]
},
"vacation_list": {
"item": [
{
"id": 1,
"name": [
{
"text": "年假",
"lang": "zh_CN"
}
]
},
{
"id": 2,
"name": [
{
"text": "事假",
"lang": "zh_CN"
}
]
},
{
"id": 3,
"name": [
{
"text": "病假",
"lang": "zh_CN"
}
]
},
{
"id": 4,
"name": [
{
"text": "調休假",
"lang": "zh_CN"
}
]
},
{
"id": 5,
"name": [
{
"text": "婚假",
"lang": "zh_CN"
}
]
},
{
"id": 6,
"name": [
{
"text": "產假",
"lang": "zh_CN"
}
]
},
{
"id": 7,
"name": [
{
"text": "陪產假",
"lang": "zh_CN"
}
]
},
{
"id": 8,
"name": [
{
"text": "其他",
"lang": "zh_CN"
}
]
}
]
}
}
2.2 代碼實戰
2.2.1獲取access_token工具類
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HttpUtil;
import com.alibaba.fastjson.JSONObject;
import com.tzwy.lcls.entity.vo.AccessTokenVO;
import com.tzwy.lcls.exception.ApiException;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.client.RestClientException;
import org.springframework.web.client.RestTemplate;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
/** * 企業微信工具 * * @author oldlu * @version 1.0 */
@Order(3)
public class WeComUtil {
// 獲取accessToken地址
private static String GET_TOKEN_URL;
// 獲取企業微信ID
private static String CORPID;
// 獲取應用密匙
private static String CORPSECRET;
// 獲取應用ID
private static String AGENTID;
// 根據code獲取openId
private static String GET_OPEN_ID;
// access_token的失效時間
private static long expiresTime;
// 緩存的access_token
private static String accessToken;
// 靜態塊,保障一次加載獲得數據
static {
GET_TOKEN_URL = YamlConfigurerUtil.getStrYmlVal("wecom.getTokenUrl");
CORPID = YamlConfigurerUtil.getStrYmlVal("wecom.corpid");
CORPSECRET = YamlConfigurerUtil.getStrYmlVal("wecom.corpsecret");
AGENTID = YamlConfigurerUtil.getStrYmlVal("wecom.AgentId");
}
/** * 獲取accessToken * * @return accessToken */
public static AccessTokenVO getAccessToken() throws ApiException {
String errcode;
AccessTokenVO accesstokenVO = new AccessTokenVO();
// 判斷accessToken是否已經過期,如果過期需要重新獲取
if (accessToken == null || expiresTime < new Date().getTime()) {
// 發起請求獲取accessToken
Map<String, Object> map = new HashMap<>();
map.put("corpid", CORPID);
map.put("corpsecret", CORPSECRET);
String ret = HttpUtil.get(GET_TOKEN_URL,map);
JSONObject result = JSONObject.parseObject(ret);
System.out.println(result.toJSONString());
accesstokenVO = JSONObject.parseObject(ret, AccessTokenVO.class);
if (StrUtil.isBlank(result.getString("errcode"))||result.getInteger("errcode")==0) {
// 設置accessToken的失效時間
long expires_in = result.getLong("expires_in");
// 失效時間 = 當前時間 + 有效期(提前一分鍾)
expiresTime = new Date().getTime() + (expires_in - 60) * 1000;
} else {
throw new ApiException("獲取accessToken失敗:" + accesstokenVO.getErrmsg(), HttpStatus.BAD_REQUEST);
}
}
return accesstokenVO;
}
/** * 根據code獲取微信用戶openId * @param code code * @return 微信用戶openId * @throws ApiException 異常信息 */
public static String getOpenId(String appId,String secret,String code) throws ApiException{
String result;
try {
RestTemplate restTemplate = new RestTemplate();
String params = "appid=" + appId +
"&secret=" + secret +
"&js_code=" + code +
"&grant_type=" + "authorization_code";
ResponseEntity<String> responseEntity = restTemplate.getForEntity(GET_OPEN_ID + params, String.class);
result= responseEntity.getBody();
} catch (RestClientException e) {
e.printStackTrace();
throw new ApiException("code獲取微信用戶openId失敗!:", HttpStatus.BAD_REQUEST);
}
return result;
}
//發送應用消息
/** * * @return * @throws ApiException */
public static void appMessage(String messageUrl, String accessToken, String touser, String textcard,String agentid) throws ApiException {
Map<String, Object> map = new HashMap<>();
Map<String, Object> textcardMap = new HashMap<>();
textcardMap.put("title","領獎通知");
textcardMap.put("description","<div class=\\\"gray\\\">2016年9月26日</div> <div class=\\\"normal\\\">恭喜你抽中iPhone 7一台,領獎碼:xxxx</div><div class=\\\"highlight\\\">請於2016年10月10日前聯系行政同事領取</div>");
textcardMap.put("url","url");
textcardMap.put("btntxt","更多");
map.put("touser", touser);
map.put("msgtype", "textcard");
map.put("agentid", agentid);
map.put("textcard", textcardMap);
/* * { "touser" : "UserID1|UserID2|UserID3", "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":"更多" }, } * */
JSONObject jsonObject = EWeChatUtil.postJson(messageUrl, map, accessToken);
}
public static void main(String[] args) {
JSONObject jsonObject=new JSONObject();
WeComUtil.appMessage("https://qyapi.weixin.qq.com/cgi-bin/message/send","M851qF0u3EjSDGMrTeBJW3L7LaCbbM0Khpr3T9Pcq11htS3lopmfl5-Scqo9MTkVTOmCdntRJD5sC6wHe4R93H27cK5qjyYtPTSnHYllmUwSl-Ztu7ShtdfNnLHcmo07q2Sxhig-fNkEgE4OkfkVh5MgOnbBFDUDfr8-oSKHq1AeHHNHVe41jMPrzfXM0gRlo30z8dEHmmgzYxmqa4Xj5g","zgl",
jsonObject.toJSONString(),"1000003");
}
}
2.2.2 錯誤分析
1、此時給請求access_token的corpi(企業id)后面隨意加個字符,會出現如下所示錯誤,意思是企業id有誤,故需排查一下密鑰是否填寫錯誤。
{
"access_token": null,
"expires_in": 0,
"errcode": "40013",
"errmsg": "invalid corpid, hint: [1619103786_194_2ee3fb91c5238be28588ceed7a444d23], from ip: 119.129.123.127, more info at https://open.work.weixin.qq.com/devtool/query?e=40013"
}
2、在請求access_token的corpsecret(應用密鑰)后面隨意加個字符,會出現如下所示錯誤,提示錯誤的憑據,原因是密鑰有誤,故需排查一下密鑰是否填寫錯誤。
{
"access_token": null,
"expires_in": 0,
"errcode": "40001",
"errmsg": "invalid credential, hint: [1619103952_195_a01894928bf0bce72d1467131c09363f], from ip: 119.129.123.127, more info at https://open.work.weixin.qq.com/devtool/query?e=40001"
}
2.2.3 獲取模版詳情

2.2.4 錯誤分析
1、在上述TemplateDetails接口的url后隨意加一個字符(讓token與緩存中的不一致),會出現如下錯誤,提示無效的access_token。
{
"errcode": 40014,
"errmsg": "invalid access_token",
"template_names": []
}
2、在上述TemplateDetails接口的templateid后隨意加一個字符(讓templateid不存在),會出現如下錯誤,提示"提交審批單請求參數錯誤",因為請求體(body)只有一個參數,故此時應排查templateid是否有誤。
{
"errcode": 301025,
"errmsg": "get approval param error, hint: [1619104495_170_e21873150fdc0119da67c4fe98dafe69], from ip: 119.129.123.127, more info at https://open.work.weixin.qq.com/devtool/query?e=301025",
"template_names": []
}
3 提交審批申請
企業可通過審批應用或自建應用Secret調用本接口,代應用可見范圍內員工在企業微信“審批應用”內提交指定類型的審批申請。
企業微信官方線上調試:調試工具
請求方式:POST
請求地址: https://qyapi.weixin.qq.com/cgi-bin/oa/applyevent?access_token=ACCESS_TOKEN
請求示例(請假模版):(參數說明請看官方文檔)
注:json數據是根據請求過的"請假審批申請"構建的,后面會介紹到如何請求審批申請詳情數據。
{
"creator_userid": "發起申請的用戶id",
"template_id": "請假模版id",
"use_template_approver": 0,
"approver": [
{
"attr": 1,
"userid": [ "審批人id" ]
}
],
"notify_type": 1,
"apply_data": {
"contents": [
{
"control": "Vacation",
"id": "vacation-1563793073898",
"title": [
{
"text": "請假類型",
"lang": "zh_CN"
},
{
"text": "Leave Type",
"lang": "en"
}
],
"value": {
"tips": [],
"members": [],
"departments": [],
"files": [],
"children": [],
"stat_field": [],
"vacation": {
"selector": {
"type": "single",
"options": [
{
"key": "1",
"value": [
{
"text": "年假",
"lang": "zh_CN"
}
]
}
]
},
"attendance": {
"date_range": {
"type": "halfday",
"new_begin": 1618848000,
"new_end": 1618891200,
"new_duration": 86400
},
"type": 1,
"slice_info": {
"day_items": [
{
"daytime": 1618848000,
"time_sections": [],
"duration": 28800
}
],
"duration": 28800
}
}
},
"sum_field": [],
"related_approval": [],
"students": [],
"classes": []
}
},
{
"control": "Textarea",
"id": "item-1497581399901",
"title": [
{
"text": "請假事由",
"lang": "zh_CN"
},
{
"text": "Leave Reason",
"lang": "en"
}
],
"value": {
"text": "你猜猜看",
"tips": [],
"members": [],
"departments": [],
"files": [],
"children": [],
"stat_field": [],
"sum_field": [],
"related_approval": [],
"students": [],
"classes": []
}
},
{
"control": "File",
"id": "item-1497581426169",
"title": [
{
"text": "說明附件",
"lang": "zh_CN"
},
{
"text": "Attachment",
"lang": "en"
}
],
"value": {
"tips": [],
"members": [],
"departments": [],
"files": [],
"children": [],
"stat_field": [],
"sum_field": [],
"related_approval": [],
"students": [],
"classes": []
}
}
]
},
"summary_list": [
{
"summary_info": [
{
"text": "請假類型:年假",
"lang": "zh_CN"
}
]
},
{
"summary_info": [
{
"text": "開始時間:2021/4/21 上午",
"lang": "zh_CN"
}
]
},
{
"summary_info": [
{
"text": "結束時間:2021/4/21 下午",
"lang": "zh_CN"
}
]
}
]
}
3.1 代碼實戰
1、用戶id對應的是"通訊錄"->“xxx成員”->“賬號”,如下圖所示:

2、執行上述json之前,請先手動在企業微信客戶端進行"請假"申請的發起,了解具體有哪些控件,會做哪些操作。
3、大致請求代碼如下(其他幫助類存放在代碼的ApplyEventModel中,具體看git倉庫),請思路是A(發起人)發起一個請假申請,B(審批人)對該請假申請進行審批:

4、請求結果如下,errmsg為ok則表示成功,其中sp_no是提交成功后返回的表單編號:

3.2 錯誤分析
1、此時,將用戶id改為當前企業微信不存在的用戶id,則會出現如下錯誤,提示"審批參數有誤,無效的用戶id"
{
"errcode": 301025,
"errmsg": "get approval param error:invalid userid:"
}
2、在上述BuildApplyEventModel方法中的templateid后隨意加一個字符(讓templateid不存在),會出現如下錯誤,提示"提交審批單請求參數錯誤,操作的模版id",故此時應排查templateid是否有誤。
{
"errcode": 301025,
"errmsg": "get approval param error:invalid template_id"
}
3.3 提交審批自定義模板中的json值

地址:https://work.weixin.qq.com/api/doc/90000/90135/91853
樣例:
{
"creator_userid": "zgl",
"template_id": "C4NugmdPcdnjKJyExMM9rXp1xgN48Udq5v4ZZgmas",
"use_template_approver": 0,
"approver": [{
"attr": 1,
"userid": ["zgl", "oyxc"]
}],
"notify_type": 1,
"apply_data": {
"contents": [{
"control": "Text",
"id": "Text-1603769671122",
"title": [{
"text": "文本控件",
"lang": "zh_CN"
}],
"value": {
"text": "6666"
}
}, {
"control": "Text",
"id": "Text-1603769697926",
"title": [{
"text": "文本控件",
"lang": "zh_CN"
}],
"value": {
"text": "6666"
}
}, {
"control": "Text",
"id": "Text-1603769748863",
"title": [{
"text": "文本控件",
"lang": "zh_CN"
}],
"value": {
"text": "6666"
}
}, {
"control": "Text",
"id": "Text-1634277117986",
"title": [{
"text": "文本控件",
"lang": "zh_CN"
}],
"value": {
"text": "6666"
}
}, {
"control": "Text",
"id": "Text-1634277129673",
"title": [{
"text": "文本控件",
"lang": "zh_CN"
}],
"value": {
"text": "6666"
}
}, {
"control": "Text",
"id": "Text-1634260850168",
"title": [{
"text": "文本控件",
"lang": "zh_CN"
}],
"value": {
"text": "6666"
}
}, {
"control": "Text",
"id": "Text-1634260860288",
"title": [{
"text": "文本控件",
"lang": "zh_CN"
}],
"value": {
"text": "6666"
}
}
]
},
"summary_list": [{
"summary_info": [{
"text": "案號:oldlu",
"lang": "zh_CN"
}]
},
{
"summary_info": [{
"text": "案由:oldlu",
"lang": "zh_CN"
}]
},
{
"summary_info": [{
"text": "綁定人:oldlu",
"lang": "zh_CN"
}]
}
]
}
4 測試類
里面的實體Approve是實體json對應的java實體類
/** * @return * @throws ApiException */
@ApiOperation(value = "測試發送企業微信審批消息", notes = "測試發送企業微信審批消息")
@GetMapping("/sendMsg")
public ResponseEntity sendMsg() throws ApiException {
RestTemplate restTemplate = new RestTemplate();
Approve approve = new Approve();
approve.setCreator_userid("zgl");
approve.setTemplate_id("C4NugmdPcdnjKJyExMM9rXp1xgN48Udq5v4ZZgmas");
approve.setNotify_type(1);
approve.setUse_template_approver(0);
/* "approver": [{ "attr": 1, "userid": ["zgl", "oyxc"] }],*/
List<Approver> approvers = new ArrayList<>();
Approver approver = new Approver();
approver.setAttr(1);
List<String> userids = new ArrayList<>();
userids.add("zgl");
userids.add("oyxc");
approver.setUserid(userids);
approvers.add(approver);
approve.setApprover(approvers);
/* "apply_data": { "contents": [{ "control": "Text", "id": "Text-1603769671122", "title": [{ "text": "文本控件", "lang": "zh_CN" }], "value": { "text": "6666" } }*/
Apply_data apply_data = new Apply_data();
List<Contents> contents = new ArrayList<>();
Contents content = new Contents();
/*案號*/
content.setControl("Text");
content.setId("Text-1634277117986");
List<Title> titles = new ArrayList<>();
Title title = new Title();
title.setLang("zh_CN");
title.setText("文本控件");
titles.add(title);
content.setTitle(titles);
Value value = new Value();
value.setText("666666");
content.setValue(value);
contents.add(content);
/*案由*/
Contents content1 = new Contents();
content1.setControl("Text");
content1.setId("Text-1634277129673");
List<Title> titles1 = new ArrayList<>();
Title title1 = new Title();
title1.setLang("zh_CN");
title1.setText("文本控件");
titles.add(title1);
content1.setTitle(titles1);
Value value1 = new Value();
value1.setText("666666");
content1.setValue(value1);
contents.add(content1);
/*承辦人*/
Contents content2 = new Contents();
content2.setControl("Text");
content2.setId("Text-1634260860288");
List<Title> titles2 = new ArrayList<>();
Title title2 = new Title();
title2.setLang("zh_CN");
title2.setText("文本控件");
titles.add(title2);
content2.setTitle(titles2);
Value value2 = new Value();
value2.setText("666666");
content2.setValue(value2);
contents.add(content2);
/*當事人*/
Contents content3 = new Contents();
content3.setControl("Text");
content3.setId("Text-1634260850168");
List<Title> titles3 = new ArrayList<>();
Title title3 = new Title();
title3.setLang("zh_CN");
title3.setText("文本控件");
titles.add(title3);
content3.setTitle(titles3);
Value value3 = new Value();
value3.setText("666666");
content3.setValue(value3);
contents.add(content3);
/*綁定人*/
Contents content4 = new Contents();
content4.setControl("Text");
content4.setId("Text-1603769671122");
List<Title> titles4 = new ArrayList<>();
Title title4 = new Title();
title4.setLang("zh_CN");
title4.setText("文本控件");
titles.add(title4);
content4.setTitle(titles4);
Value value4 = new Value();
value4.setText("666666");
content4.setValue(value4);
contents.add(content4);
/*身份證號*/
Contents content5 = new Contents();
content5.setControl("Text");
content5.setId("Text-1603769697926");
List<Title> titles5 = new ArrayList<>();
Title title5 = new Title();
title5.setLang("zh_CN");
title5.setText("文本控件");
titles.add(title5);
content5.setTitle(titles5);
Value value5 = new Value();
value5.setText("666666");
content5.setValue(value5);
contents.add(content5);
/*手機號*/
Contents content6 = new Contents();
content6.setControl("Text");
content6.setId("Text-1603769748863");
List<Title> titles6 = new ArrayList<>();
Title title6 = new Title();
title6.setLang("zh_CN");
title6.setText("文本控件");
titles.add(title6);
content6.setTitle(titles6);
Value value6 = new Value();
value6.setText("666666");
content6.setValue(value6);
contents.add(content6);
apply_data.setContents(contents);
approve.setApply_data(apply_data);
/* "summary_list": [{ "summary_info": [{ "text": "案號:oldlu", "lang": "zh_CN" }] },*/
List<Summary_list> summary_lists = new ArrayList<>();
Summary_list summary_list = new Summary_list();
List<Summary_info> summary_info = new ArrayList<>();
/*案號*/
Summary_info summary_info1 = new Summary_info();
summary_info1.setLang("zh_CN");
summary_info1.setText("案號:oldlu");
/**/
Summary_info summary_info2 = new Summary_info();
summary_info2.setLang("zh_CN");
summary_info2.setText("案由:oldlu");
summary_info.add(summary_info1);
summary_list.setSummary_info(summary_info);
summary_lists.add(summary_list);
approve.setSummary_list(summary_lists);
String jsonString = JSONObject.toJSONString(approve);
JSONObject jsonObject1 = JSONObject.parseObject(jsonString);
String post = HttpUtil.post("https://qyapi.weixin.qq.com/cgi-bin/oa/applyevent?access_token=QxTJ7dNV8KXNHBAwQaB9U_72JU0HqtFexX91532nJZRhCQVSNO5EawaCFsPGngaPGR1BG045XcgZR4JbGyKuVXR_VePXQB6Hy2ExmZYJp3j6jMXULhpAXKMkJE4AIhIUEOffut_x01k1yuXW1p5oF3PXbp4k2zeSl6g7m2fUtX_LjWVbVsqM1n1bYE0p2H4mXesLLxWipcped3Xz98M6DQ", jsonObject1.toJSONString());
return ResponseEntity.ok(jsonObject1);
}


