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); }