企业微信开发实战(五、自建应用-审批流程引擎之配置可信任域名、创建审批模版、发起审批)

2023-05-16

文章目录

  • 四、自建应用-审批流程引擎
    • 1.概述
    • 2.创建自建应用审批模板
      • 2.1创建自建审批应用
      • 2.2配置可信任域名
      • 2.3创建审批模版
    • 3.自建应用发起审批
      • 3.1概述
      • 3.2代码实战
        • 3.2.1前端代码
        • 3.2.2后端代码
      • 3.3试错
    • 源码
    • 赞赏

四、自建应用-审批流程引擎

1.概述

1、企业微信向开发者提供审批流程引擎,此特性可将审批流程相关功能嵌入到自建应用中。
2、开发者可在自建应用中直接调用接口发起审批申请,系统根据审批流程自动通知相关人员进行审批操作。
3、提交申请后审批流程的每次状态变化,都会通知开发者,可按需进行拓展开发。

此套接口在自建应用中闭环,与“企业微信审批应用”相关接口无关。

自建应用审批流程引擎示意图

2.创建自建应用审批模板

2.1创建自建审批应用

1、找到"应用管理"->“自建”->点击"创建应用"。

开发者可在“管理后台-自建应用-审批接口”中,创建审批模板。
在这里插入图片描述
2、选择应用logo,应用名称设置为"自建审批应用",可见范围选择某个部门,再点击"创建应用",即可看到出现第"1"步中的带有"柯南"头像的自建审批应用。
在这里插入图片描述

3、点击上图带"柯南"头像的自建审批应用,来到该应用的详情页。

(1)点击"查看"获取应用secret。

(2)点击下图应用主页的"修改",设置应用主页的网址(对应你内网穿透的网址),后续在客户端中点击"自建审批应用"时,就会调到该链接指向的主页。
在这里插入图片描述
在这里插入图片描述

2.2配置可信任域名

1、找到"开发者接口"->“网页授权及JS-SDK”。
在这里插入图片描述

2、如下图所示,将内网穿透的域名添加为可信域名(注:这里需要去掉"http://“或"https://”)
在这里插入图片描述
3、打开企业微信客户端,进行如下图1、2步操作,可看到出现创建的ASP.NET Core Web项目的首页,原因是再"2.1创建自建审批应用"步骤是设置了该内网穿透网址对应的应用程序为应用的主页。
在这里插入图片描述

2.3创建审批模版

1、找到"开发者接口"->“审批接口”。

2、如下图所示,点击"添加模版",创建一个名为"测试模版"的审批模版。
在这里插入图片描述

功能说明:

参数说明
模板ID用于审批申请类型区分。在后续发起审批申请时,将申请和审批流程进行关联。
审批流程审批流程相关配置。后续以此模板ID发起的审批申请,都将按照设置的流程进行通知和流转。

3.自建应用发起审批

3.1概述

通过JS-SDK,可在自建应用中发起审批。查看JS-SDK调用详细说明
具体步骤:
(1)通过config接口注入权限验证配置。查看
(2)通过agentConfig注入应用的权限。查看
(3)调用审批流程引擎JS-API(如下文请求示例)。

注:企业微信客户端2.5.0及以上版本支持。

3.2代码实战

3.2.1前端代码

1、创建一个名为SelfApprovalController的控制器,并在"Pages"文件夹中创建一个"About.cshtml",并在"_Layout.cshtml"中About菜单项的代码做如下更改:

<li class="nav-item"><a href="/About" class="nav-link">About</a></li>

在这里插入图片描述

2、About.cshtml的代码如下(请先阅读"3.1概述"中对应链接的内容后,再看该cshtml中的代码,因为这里的代码主要也是在官方文档中复制的):

@page
@using QiYeWeiXinDev.Models
<h1>显示Json</h1>
<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>

3.2.2后端代码

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
        /// <summary>
        /// 请求企业微信自建审批应用accessToken
        /// </summary>
        /// <returns></returns>
        [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
{
    /// <summary>
    /// token、ticket帮助类
    /// </summary>
    public class TokenTicketHelper
    {
        private MemoryCacheHelper _cahce;
        public TokenTicketHelper(MemoryCacheHelper cahce)
        {
            _cahce = cahce;
        }
         #region 1、获取accessToken
        /// <summary>
        /// 请求企业微信accessToken
        /// </summary>
        /// <param name="accessTokenKey"></param>
        /// <returns></returns>
        private async Task<T> RequireAccessToken<T>(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<T>(await client.GetStringAsync(url));
                }
            }
            catch (Exception ex)
            {
                //后面添加日志记录
                Console.WriteLine(ex);
            }
            return result;
        }

        /// <summary>
        /// 获取缓存中的accesstoken
        /// </summary>
        /// <param name="accessTokenKey"></param>
        /// <returns></returns>
        private AccessTokenModel CacheAccessToken(RequireTokenModel model)
        {
            var accessToken = _cahce.Get<AccessTokenModel>(model.accessTokenKey);
            if (accessToken == null)
            {
                // 往memorycache里面存入数据
                accessToken = RequireAccessToken<AccessTokenModel>(model).Result;
                _cahce.Set(model.accessTokenKey, accessToken, TimeSpan.FromSeconds(PublicParam.sleepTime));
            }
            return accessToken;
        }
        /// <summary>
        /// 获取审批应用accessToken
        /// </summary>
        /// <param name="accessTokenKey"></param>
        /// <returns></returns>
        public AccessTokenModel GetApprovalAccessToken()
        {
            RequireTokenModel model = new RequireTokenModel()
            {
                requireUrl = PublicParam.tokenRequireUrl,
                accessTokenKey = PublicParam.defaultApprovalAccessTokenKey,
                corpi = PublicParam.corpi,
                secret = PublicParam.defaultApprovalSecret
            };
            return CacheAccessToken(model);
        }

        /// <summary>
        /// 获取自建审批应用accessToken
        /// </summary>
        /// <param name="accessTokenKey"></param>
        /// <returns></returns>
        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
        /// <summary>
        /// 请求企业微信JSApiTicket
        /// </summary>
        /// <returns></returns>
        //private async Task<JSApiTicketModel> RequireJSApiTicket()
        private async Task<T> RequireJSApiTicket<T>(string url) where T : class
        {
            T result = null;
            try
            {
                using (HttpClient client = new HttpClient())
                {
                    result = JsonConvert.DeserializeObject<T>(await client.GetStringAsync(url));
                }
            }
            catch (Exception ex)
            {
                //后面添加日志记录
                Console.WriteLine(ex);
            }
            return result;
        }

        /// <summary>
        /// 获取缓存中的jsapi_ticket
        /// </summary>
        /// <param name="accessTokenKey"></param>
        /// <returns></returns>
        private JSApiTicketModel CacheJSApiTicket(string url, string jsApiTicketKey)
        {
            var jsApiTicket = _cahce.Get<JSApiTicketModel>(jsApiTicketKey);
            if (jsApiTicket == null)
            {
                // 往memorycache里面存入数据
                jsApiTicket = RequireJSApiTicket<JSApiTicketModel>(url).Result;
                _cahce.Set(jsApiTicketKey, jsApiTicket, TimeSpan.FromSeconds(PublicParam.sleepTime));
            }
            return jsApiTicket;
        }

        /// <summary>
        /// 获取企业JSApiTicket
        /// </summary>
        /// <returns></returns>
        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);
        }

        /// <summary>
        /// 请求企业微信审批应用JSApiTicket
        /// </summary>
        /// <returns></returns>
        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

        /// <summary>
        /// 基于Sha1的自定义加密字符串方法:输入一个字符串,返回一个由40个字符组成的十六进制的哈希散列(字符串)。
        /// </summary>
        /// <param name="str">要加密的字符串</param>
        /// <returns>加密后的十六进制的哈希散列(字符串)</returns>
        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。
         */
        /// <summary>
        /// 请求企业微信JSApiTicket
        /// </summary>
        /// <returns></returns>
        [Route("CacheTryGetTicket")]
        public IActionResult CacheTryGetTicket()
        {
            return Json(_tokenTicketHelper.GetCorpApprovalJSApiTicket());
        }
        #endregion

3、获取应用的jsapi_ticket

  #region 3、获取应用的jsapi_ticket
        /// <summary>
        /// 请求企业微信审批应用JSApiTicket
        /// </summary>
        /// <returns></returns>
        [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}&timestamp={timestamp}&url={url}";
            //append agent
            string strAgentTicket = $"jsapi_ticket={agent_jsapi_ticket}&noncestr={nonceStr}&timestamp={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模式(注:一定要在企业微信客户端访问,否则会出现问题,待会会在"试错"章节说明)。
在这里插入图片描述

6、选择"审批人",点击"提交"即可完成一次审批申请的发起
在这里插入图片描述
在这里插入图片描述

7、此时在"!!!“用户的企业微信账号上可以接收到该审批申请,点击"同意”,刚刚发起申请的用户就会接收到"你的测试模版已通过"的消息,如下图:
在这里插入图片描述

在这里插入图片描述

8、到这一步,我们就完成了自建应用发起审批的操作了,具体代码可看文末的git仓库地址。

3.3试错

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

赞赏

​ 如果您觉得文章还不错,那就请作者喝杯咖啡吧!
在这里插入图片描述

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

企业微信开发实战(五、自建应用-审批流程引擎之配置可信任域名、创建审批模版、发起审批) 的相关文章

随机推荐