1、企业微信向开发者提供审批流程引擎,此特性可将审批流程相关功能嵌入到自建应用中。
2、开发者可在自建应用中直接调用接口发起审批申请,系统根据审批流程自动通知相关人员进行审批操作。
3、提交申请后审批流程的每次状态变化,都会通知开发者,可按需进行拓展开发。
此套接口在自建应用中闭环,与“企业微信审批应用”相关接口无关。
1、找到"应用管理"->“自建”->点击"创建应用"。
开发者可在“管理后台-自建应用-审批接口”中,创建审批模板。
2、选择应用logo,应用名称设置为"自建审批应用",可见范围选择某个部门,再点击"创建应用",即可看到出现第"1"步中的带有"柯南"头像的自建审批应用。
3、点击上图带"柯南"头像的自建审批应用,来到该应用的详情页。
(1)点击"查看"获取应用secret。
(2)点击下图应用主页的"修改",设置应用主页的网址(对应你内网穿透的网址),后续在客户端中点击"自建审批应用"时,就会调到该链接指向的主页。
2、如下图所示,将内网穿透的域名添加为可信域名(注:这里需要去掉"http://“或"https://”)
3、打开企业微信客户端,进行如下图1、2步操作,可看到出现创建的ASP.NET Core Web项目的首页,原因是再"2.1创建自建审批应用"步骤是设置了该内网穿透网址对应的应用程序为应用的主页。
1、找到"开发者接口"->“审批接口”。
2、如下图所示,点击"添加模版",创建一个名为"测试模版"的审批模版。
功能说明:
参数 | 说明 |
---|---|
模板ID | 用于审批申请类型区分。在后续发起审批申请时,将申请和审批流程进行关联。 |
审批流程 | 审批流程相关配置。后续以此模板ID发起的审批申请,都将按照设置的流程进行通知和流转。 |
通过JS-SDK,可在自建应用中发起审批。查看JS-SDK调用详细说明
具体步骤:
(1)通过config接口注入权限验证配置。查看
(2)通过agentConfig注入应用的权限。查看
(3)调用审批流程引擎JS-API(如下文请求示例)。
注:企业微信客户端2.5.0及以上版本支持。
1、创建一个名为SelfApprovalController
的控制器,并在"Pages"文件夹中创建一个"About.cshtml",并在"_Layout.cshtml"中About菜单项的代码做如下更改:
<li class="nav-item"><a href="/About" class="nav-link">Abouta>li>
2、About.cshtml的代码如下(请先阅读"3.1概述"中对应链接的内容后,再看该cshtml中的代码,因为这里的代码主要也是在官方文档中复制的):
@page
@using QiYeWeiXinDev.Models
<h1>显示Jsonh1>
<pre id="showJson">pre>
@(Html.DevExtreme().Button()
.Text("测试")
.Type(ButtonType.Default)
.Width(100)
.OnClick("TestFun")
)
@*引入使用JS-SDK需要用到的js*@
<script src="//res.wx.qq.com/open/js/jweixin-1.2.0.js">script>
<script src="https://open.work.weixin.qq.com/wwopen/js/jwxwork-1.0.0.js">script>
<script type="text/javascript">
function TestFun() {
$.ajax({
type: "GET",
url: "api/SelfApproval/OutputJsApiTicketConfig",//请求后端生成好的配置
success: function (res) {
console.log(res);
wx.config({
beta: true,// 必须这么写,否则wx.invoke调用形式的jsapi会有问题
debug: true, // 开启调试模式,调用的所有api的返回值会在客户端alert出来,若要查看传入的参数,可以在pc端打开,参数信息会通过log打出,仅在pc端时才会打印。
appId: res.corpi, // 必填,企业微信的corpID
timestamp: res.timestamp, // 必填,生成签名的时间戳
nonceStr: res.nonceStr, // 必填,生成签名的随机串
signature: res.signature,// 必填,签名,见 附录-JS-SDK使用权限签名算法
jsApiList: ['agentConfig', 'openUserProfile', 'thirdPartyOpenPage', 'selectExternalContact'] // 必填,需要使用的JS接口列表,凡是要调用的接口都需要传进来
});
wx.ready(function () {
// config信息验证后会执行ready方法,所有接口调用都必须在config接口获得结果之后,config是一个客户端的异步操作,所以如果需要在页面加载时就调用相关接口,则须把相关接口放在ready函数中调用来确保正确执行。对于用户触发时才调用的接口,则可以直接调用,不需要放在ready函数中。
console.log(" wx.ready");
wx.checkJsApi({
jsApiList: ['thirdPartyOpenPage'], // 需要检测的JS接口列表,所有JS接口列表见附录2,
success: function (res) {
console.log(res)
// 以键值对的形式返回,可用的api值true,不可用为false
// 如:{"checkResult":{"chooseImage":true},"errMsg":"checkJsApi:ok"}
},
fail: function (res) {
console.log(res);
}
});
wx.agentConfig({
corpid: res.corpi, // 必填,企业微信的corpid,必须与当前登录的企业一致
agentid: res.agentId, // 必填,企业微信的应用id (e.g. 1000247)
timestamp: res.timestamp, // 必填,生成签名的时间戳
nonceStr: res.nonceStr, // 必填,生成签名的随机串
signature: res.agentSignature,// 必填,签名,见附录-JS-SDK使用权限签名算法
jsApiList: ['agentConfig', 'openUserProfile', 'thirdPartyOpenPage', 'selectExternalContact'], //必填,传入需要使用的接口名称
success: function (res) {
console.log("agentConfig");
console.log(res);
// 回调
wx.invoke('thirdPartyOpenPage', {
"oaType": "10001",// 操作类型,目前支持:10001-发起审批;10002-查看审批详情
"templateId": "1aa556e88991520f9b2477e8f1d3a6e8_852697916",// 发起审批的模板ID,在自建应用-审批接口中创建模板可获取。
"thirdNo": "thirdNo" + new Date().getTime(),// 审批单号,由开发者自行定义,不可重复。
"extData": {
'fieldList': [{
'title': '采购类型',
'type': 'text',
'value': '市场活动',
},
{
'title': '订单链接',
'type': 'link',
'value': 'https://work.weixin.qq.com',
}],
}
},
function (res) {
// 输出接口的回调信息
console.log("回调的信息");
console.log(res);
});
},
fail: function (res) {
console.log(res);
if (res.errMsg.indexOf('function not exist') > -1) {
alert('版本过低请升级')
}
}
});
});
wx.error(function (res) {
// config信息验证失败会执行error函数,如签名过期导致验证失败,具体错误信息可以打开config的debug模式查看,也可以在返回的res参数中查看,对于SPA可以在这里更新签名。
console.log(res)
});
}
})
}
/*
agentConfig的作用
config注入的是企业的身份与权限,而agentConfig注入的是应用的身份与权限。尤其是当调用者为第三方服务商时,通过config无法准确区分出调用者是哪个第三方应用,而在部分场景下,又必须严谨区分出第三方应用的身份,此时即需要通过agentConfig来注入应用的身份信息。
解释:
企业的身份与权限:用于帮助企业微信客户端了解当前是 哪个企业 正在申请接口调用权限
应用的身份与权限:用于帮助企业微信客户端了解当前是 哪个第三方应用 正在申请接口调用权限
调用agentConfig的注意事项
agentConfig与config的签名算法完全一样,但是jsapi_ticket的获取方法不一样,请特别注意,查看”获取应用身份的ticket“.
调用wx.agentConfig之前,必须确保先成功调用wx.config. 注意:从企业微信3.0.24及以后版本(可通过企业微信UA判断版本号),无须先调用wx.config,可直接wx.agentConfig.
当前页面url中的域名必须是在该应用中设置的可信域名。
agentConfig仅在企业微信2.5.0及以后版本支持,微信客户端不支持(微信开发者工具也不支持)
仅部分接口才需要调用agentConfig,需注意每个接口的说明
*/
script>
1、获取自建应用的accessToken
(1)控制器中的代码如下:
private MemoryCacheHelper _cahce;
private TokenTicketHelper _tokenTicketHelper;
private HttpUtils _httpUtils;
private JsonXmlHelper _jsonXmlHelper;
public SelfApprovalController(MemoryCacheHelper cahce,
TokenTicketHelper tokenTicketHelper,
HttpUtils httpUtils, JsonXmlHelper jsonXmlHelper)
{
_cahce = cahce;
_tokenTicketHelper = tokenTicketHelper;
_httpUtils = httpUtils;
_jsonXmlHelper = jsonXmlHelper;
}
#region 1、获取accesstoken
///
/// 请求企业微信自建审批应用accessToken
///
///
[Route("CacheTryGetAppToken")]
public IActionResult CacheTryGetAppToken()
{
return Json(_tokenTicketHelper.GetSelfApprovalAccessToken());
}
#endregion
(2)TokenTicketHelper
的代码如下:
using Newtonsoft.Json;
using QiYeWeiXinDev.Models;
using QiYeWeiXinDev.Models.QiYeWX;
using QiYeWeiXinDev.Models.QiYeWX.AccessToken;
using QiYeWeiXinDev.Utils.Cache;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
namespace QiYeWeiXinDev.Utils
{
///
/// token、ticket帮助类
///
public class TokenTicketHelper
{
private MemoryCacheHelper _cahce;
public TokenTicketHelper(MemoryCacheHelper cahce)
{
_cahce = cahce;
}
#region 1、获取accessToken
///
/// 请求企业微信accessToken
///
///
///
private async Task RequireAccessToken(RequireTokenModel model) where T : class
{
T result = null;
try
{
//1、构建请求token的url
string url = string.Format(model.requireUrl, model.corpi, model.secret);
//2、请求
using (HttpClient client = new HttpClient())
{
//3、反序列化为T对应的模型类
result = JsonConvert.DeserializeObject(await client.GetStringAsync(url));
}
}
catch (Exception ex)
{
//后面添加日志记录
Console.WriteLine(ex);
}
return result;
}
///
/// 获取缓存中的accesstoken
///
///
///
private AccessTokenModel CacheAccessToken(RequireTokenModel model)
{
var accessToken = _cahce.Get(model.accessTokenKey);
if (accessToken == null)
{
// 往memorycache里面存入数据
accessToken = RequireAccessToken(model).Result;
_cahce.Set(model.accessTokenKey, accessToken, TimeSpan.FromSeconds(PublicParam.sleepTime));
}
return accessToken;
}
///
/// 获取审批应用accessToken
///
///
///
public AccessTokenModel GetApprovalAccessToken()
{
RequireTokenModel model = new RequireTokenModel()
{
requireUrl = PublicParam.tokenRequireUrl,
accessTokenKey = PublicParam.defaultApprovalAccessTokenKey,
corpi = PublicParam.corpi,
secret = PublicParam.defaultApprovalSecret
};
return CacheAccessToken(model);
}
///
/// 获取自建审批应用accessToken
///
///
///
public AccessTokenModel GetSelfApprovalAccessToken()
{
RequireTokenModel model = new RequireTokenModel()
{
requireUrl = PublicParam.tokenRequireUrl,
accessTokenKey = PublicParam.selfApprovalAccessTokenKey,
corpi = PublicParam.corpi,
secret = PublicParam.selfApprovalAgentSecret
};
return CacheAccessToken(model);
}
#endregion
#region 2、获取JSApiTicket
///
/// 请求企业微信JSApiTicket
///
///
//private async Task RequireJSApiTicket()
private async Task RequireJSApiTicket(string url) where T : class
{
T result = null;
try
{
using (HttpClient client = new HttpClient())
{
result = JsonConvert.DeserializeObject(await client.GetStringAsync(url));
}
}
catch (Exception ex)
{
//后面添加日志记录
Console.WriteLine(ex);
}
return result;
}
///
/// 获取缓存中的jsapi_ticket
///
///
///
private JSApiTicketModel CacheJSApiTicket(string url, string jsApiTicketKey)
{
var jsApiTicket = _cahce.Get(jsApiTicketKey);
if (jsApiTicket == null)
{
// 往memorycache里面存入数据
jsApiTicket = RequireJSApiTicket(url).Result;
_cahce.Set(jsApiTicketKey, jsApiTicket, TimeSpan.FromSeconds(PublicParam.sleepTime));
}
return jsApiTicket;
}
///
/// 获取企业JSApiTicket
///
///
public JSApiTicketModel GetCorpApprovalJSApiTicket()
{
var tokenModel = GetSelfApprovalAccessToken();
string accessToken = tokenModel == null ? null : tokenModel.access_token;
//构建 获取企业JSApiTicket请求url
string url = string.Format(PublicParam.jsapiTicketUrl,accessToken);
return CacheJSApiTicket(url, PublicParam.jsApiTicketKey);
}
///
/// 请求企业微信审批应用JSApiTicket
///
///
public JSApiTicketModel GetSelfAgentApprovalJSApiTicket()
{
var tokenModel = GetSelfApprovalAccessToken();
string accessToken = tokenModel == null ? null : tokenModel.access_token;
//构建 获取企业JSApiTicket请求url
string url = string.Format(PublicParam.jsapiAgentTicketUrl, accessToken);
return CacheJSApiTicket(url, PublicParam.selfAgentJsApiTicket);
}
#endregion
}
}
#region SHA1
///
/// 基于Sha1的自定义加密字符串方法:输入一个字符串,返回一个由40个字符组成的十六进制的哈希散列(字符串)。
///
/// 要加密的字符串
/// 加密后的十六进制的哈希散列(字符串)
private string SHA1Encode(string str)
{
var buffer = Encoding.UTF8.GetBytes(str);
var data = SHA1.Create().ComputeHash(buffer);
var sb = new StringBuilder();
foreach (var t in data)
{
sb.Append(t.ToString("X2"));
}
return sb.ToString().ToLower();
}
#endregion
2、获取企业的jsapi_ticket
#region 2、获取企业的jsapi_ticket
/*jsapi_ticket是H5应用调用企业微信JS接口的临时票据。正常情况下,
* jsapi_ticket的有效期为7200秒,通过access_token来获取。
* 由于获取jsapi_ticket的api调用次数非常有限(一小时内,一个企业最多可获取400次,且单个应用不能超过100次),
* 频繁刷新jsapi_ticket会导致api调用受限,影响自身业务,开发者必须在自己的服务全局缓存jsapi_ticket。
*/
///
/// 请求企业微信JSApiTicket
///
///
[Route("CacheTryGetTicket")]
public IActionResult CacheTryGetTicket()
{
return Json(_tokenTicketHelper.GetCorpApprovalJSApiTicket());
}
#endregion
3、获取应用的jsapi_ticket
#region 3、获取应用的jsapi_ticket
///
/// 请求企业微信审批应用JSApiTicket
///
///
[Route("CacheTryGetAgentTicket")]
public IActionResult CacheTryGetAgentTicket()
{
return Json(_tokenTicketHelper.GetSelfAgentApprovalJSApiTicket());
}
#endregion
4、创建JsApiTicketConfig
接口供About.cshtml
调用
#region 4、输出JsApiTicketConfig给About.cshtml
[Route("OutputJsApiTicketConfig")]
public IActionResult OutputJsApiTicketConfig()
{
string nonceStr = "Wm3WZYTPz0wzccnW";//生成签名的随机字符串
int timestamp = 1619056870;//时间戳
string jsapi_ticket = "", agent_jsapi_ticket = "";
string url = PublicParam.jsapiAccessUrl;// url(当前网页的URL, 不包含#及其后面部分)
//get ticket
var ticketModel = _tokenTicketHelper.GetCorpApprovalJSApiTicket();
var agentTicketModel = _tokenTicketHelper.GetSelfAgentApprovalJSApiTicket();
if (ticketModel != null)
jsapi_ticket = ticketModel.ticket;
if (agentTicketModel != null)
agent_jsapi_ticket = agentTicketModel.ticket;
//append string
string strTicket = $"jsapi_ticket={jsapi_ticket}&noncestr={nonceStr}×tamp={timestamp}&url={url}";
//append agent
string strAgentTicket = $"jsapi_ticket={agent_jsapi_ticket}&noncestr={nonceStr}×tamp={timestamp}&url={url}";
//output sha1 code
string signature = SHA1Encode(strTicket);
string agentSignature = SHA1Encode(strAgentTicket);
//output config dto
JsApiTicketConfigDTO dto = new JsApiTicketConfigDTO()
{
corpi = PublicParam.corpi,
agentId = PublicParam.selfApprovalAgentId,
nonceStr = nonceStr,
timestamp = timestamp,
signature = signature,
agentSignature = agentSignature
};
return Json(dto);
}
#endregion
4、运行代码,在客户端中打开"自建审批应用"->“About”。
5、点击该页面的"测试"按钮,会出现一系列弹窗,一直点击"OK"即可,出现弹窗的原因是我们开启在前端代码中开启的debug模式(注:一定要在企业微信客户端访问,否则会出现问题,待会会在"试错"章节说明)。
7、此时在"!!!“用户的企业微信账号上可以接收到该审批申请,点击"同意”,刚刚发起申请的用户就会接收到"你的测试模版已通过"的消息,如下图:
8、到这一步,我们就完成了自建应用发起审批的操作了,具体代码可看文末的git仓库地址。
1、在浏览器中打开"About.cshtml",并点击"测试"按钮,会出现 "agentConfig:fail"的错误,这是因为企业微信jssdk运行环境是有限制的,需要在企业微信环境中进行访问(企业微信pc端调试工具)。
{err_Info: "fail", errMsg: "agentConfig:fail", hint: "Hint: '0424164822:yZI2hABGJwWrT7DoZ6xwew:40093'. M…s://open.work.weixin.qq.com/devtool/query?e=40093"}
errMsg: "agentConfig:fail"
err_Info: "fail"
hint: "Hint: '0424164822:yZI2hABGJwWrT7DoZ6xwew:40093'. More info at https://open.work.weixin.qq.com/devtool/query?e=40093"
__proto__: Object
2、将代码agentSignature=SHA1Encode(strAgentTicket)
更改为agentSignature=SHA1Encode(strTicket)
,会出现如下错误,原因是我们配置agentConfig的signature需要使用"自建应用jsApiTicket"去构建,而不是"企业的jsApiTicket去构建"。
//string agentSignature = SHA1Encode(strAgentTicket);
string agentSignature = SHA1Encode(strTicket);
可将模版id设置为不存在的模版id、设置错误的应用id等情况进行代码的配置,看看会出现什么错误提示。
参数说明:
参数 | 必须 | 说明 |
---|---|---|
oaType | 是 | 操作类型,目前支持:10001-发起审批;10002-查看审批详情。 |
templateId | 是 | 发起审批的模板ID,在自建应用-审批接口中创建模板可获取。 |
thirdNo | 是 | 审批单号,由开发者自行定义,不可重复。 |
extData | 是 | 详情数据,Json格式,用于审批详情页信息展示。 |
extData数据说明:
extData在发起时由开发者传入,其中数据将全部展示在审批申请中:
1.开发者可利用此特性,在发起审批时,传入需要申请人、审批人、抄送人看到的信息;
2.若需用户填写数据,可在自行使用表单收集,并传入extData中,用于展示。
{
"extData": {
'fieldList': [
{
'title': '采购类型',
'type': 'text',
'value': '市场活动',
},
{
'title': '采购说明',
'type': 'text',
'value': '购买个人办公电脑',
},
{
'title': '采购金额',
'type': 'text',
'value': '4839.00元',
},
{
'title': '申请时间',
'type': 'text',
'value': '2018/06/20',
},
{
'title': '订单链接',
'type': 'link', // link类型,用于在审批详情页展示第三方订单跳转地址
'value': 'https://www.qq.com',
},
],
},
}
参数说明:
参数 | 必须 | 说明 |
---|---|---|
title | 否 | 字段标题,将会在审批详情页中展示。 |
type | 否 | 字段类型,目前支持:text-文本;link:链接。link仅展示在审批详情页。 |
value | 否 | 字段值,将会在审批详情页中展示。 |
错误说明:
错误提示 | 说明 |
---|---|
已存在相同的审批编号 | oaType为10001时,传入的thirdNo已经被其他审批单占用。 |
审批申请不存在 | oaType为10002时,在历史记录中,传入的thirdNo对应的审批单不存在。 |
审批模板ID不正确 | 调用接口时传入了错误的templateId |
应用ID不正确 | 使用了错误的 agentId |
地址:https://gitee.com/wusuoweixgy/QiYeWeiXinCode
克隆:git clone https://gitee.com/wusuoweixgy/QiYeWeiXinCode.git