审批控件中的外部选项 - 文档 - 企业微信开发者中心 (qq.com)

相关文档需要仔细阅读。

注意事项:

(1)signature时,url 的问题:如果设置的是明细中的选项时,企微自动加上的参数中key的值会有 中括号 [] , 我们要将这两个符号进行下转换,[ 转换成 %5B 、] 转换成 %5D,之后使用转换后的url 进行 signature

(2)签名按照官方文档

(3)access_token 、 ticket 要做本地缓存保存

(4)应用要设置可信域名、可信IP

1. JS-SDK 中 wx.config 、wx.agentConfig 

wx.config({
    beta: true,// 必须这么写,否则wx.invoke调用形式的jsapi会有问题
    debug: false, // 开启调试模式,调用的所有api的返回值会在客户端alert出来,若要查看传入的参数,可以在pc端打开,参数信息会通过log打出,仅在pc端时才会打印。
    appId: resConfig.appId, // 必填,企业微信的corpID,必须是本企业的corpID,不允许跨企业使用
    timestamp: resConfig.timestamp, // 必填,生成签名的时间戳
    nonceStr: resConfig.noncestr, // 必填,生成签名的随机串
    signature: resConfig.signature,// 必填,签名,见 附录-JS-SDK使用权限签名算法
    jsApiList: ['selectExternalContact', 'saveApprovalSelectedItems', 'getApprovalSelectedItems', 'agentConfig'] // 必填,需要使用的JS接口列表,凡是要调用的接口都需要传进来
});
wx.agentConfig({
    debug: false, // 开启调试模式,调用的所有api的返回值会在客户端alert出来,若要查看传入的参数,可以在pc端打开,参数信息会通过log打出,仅在pc端时才会打印。
    corpid: resConfigAgent.corpid, // 必填,企业微信的corpid,必须与当前登录的企业一致
    agentid: resConfigAgent.agentid, // 必填,企业微信的应用id (e.g. 1000247)
    timestamp: resConfigAgent.timestamp, // 必填,生成签名的时间戳
    nonceStr: resConfigAgent.noncestr, // 必填,生成签名的随机串
    signature: resConfigAgent.signature,// 必填,签名,见附录-JS-SDK使用权限签名算法
    jsApiList: ['selectExternalContact', 'saveApprovalSelectedItems', 'getApprovalSelectedItems'], //必填,传入需要使用的接口名称
    success: function (res) {
        console.log("agentConfig调用成功。" + JSON.stringify(res));
    },
    fail: function (res) {
        if (res.errMsg.indexOf('function not exist') > -1) {
            alert('版本过低请升级')
        }
    }
 });

2. access_token 、 ticket 这写暂不做详细说明,可以自行参考API

3.采用 .NET MVC 开发 相关代码如下:

后端代码:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;

namespace Web.Controllers
{
    public class ExternalOptionsController : Controller
    {
        public const string corpId = "";  //企业微信的企业ID
        public const string agentId = ""; //企业微信的应用的AgentId
        public const string corpSecret = "";  //企业微信的应用的Secret

        /// <summary>
        /// GET: ExternalOptions
        /// selectorType 、 key 参数是企业微信那边自动添加上的
        /// <summary>
        /// <param name="dataType">调用什么数据,如采购单:Purchase、供应商:Supplier</param>
        /// <param name="selectorType">表示该选择控件是单选还是多选,单选 - single,多选 - multi。请注意根据此参数限制用户的选择行为:单选时,只能选择1个,选择多个将报错;多选时,最多选择 300 个,超过将报错。</param>
        /// <param name="key">调用下述接口时需要原值传入</param>
        /// <returns></returns>
        public ActionResult Index(string dataType, string selectorType, string key)
        {
            if (dataType == "Purchase")
            {
                ViewData["Title"] = "采购单列表";
            }
            else if (dataType == "Supplier")
            {
                ViewData["Title"] = "供应商列表";
            }
            //将要展示的选项数据放到list里面
            List<selectedDataItem> list = new List<selectedDataItem>();
            if (dataType == "Purchase")
            {
                list.Add(new selectedDataItem() { key = "1", value = "CG20240618151234563001" });
                list.Add(new selectedDataItem() { key = "2", value = "CG20240618151234563002" });
                list.Add(new selectedDataItem() { key = "3", value = "CG20240618151234563003" });
            }
            else if (dataType == "Supplier")
            {
                list.Add(new selectedDataItem() { key = "1", value = "上海****公司" });
                list.Add(new selectedDataItem() { key = "2", value = "广东****公司" });
                list.Add(new selectedDataItem() { key = "3", value = "海南****公司" });
            }
            ViewBag.ItemData = list;
            ViewBag.selectorType = selectorType;

            return View();
        }

        [HttpGet]
        public JsonResult GetConfigSignature(string url)
        {
            string timestamp = GenerateTimeStamp();
            string noncestr = GenerateNonceStr();

            string access_token = GetToken(corpId, corpSecret);
            string jsapi_ticket = GetTicket(access_token, agentId);

            //采用排序的Dictionary的好处是方便对数据包进行签名,不用再签名之前再做一次排序
            SortedDictionary<string, object> m_values_config = new SortedDictionary<string, object>();
            m_values_config["jsapi_ticket"] = jsapi_ticket;
            m_values_config["noncestr"] = noncestr;
            m_values_config["timestamp"] = timestamp;
            //企微自动传过来的参数key的值要进行转换 [] 
            url = url.Replace("[", "%5B").Replace("]", "%5D");
            m_values_config["url"] = url;
            string signContent = GetSignContent(m_values_config);
            string signature = CalSHA1(signContent);

            object data = new
            {
                appId = appId,
                timestamp = timestamp,
                noncestr = noncestr,
                jsapi_ticket = jsapi_ticket,
                url = url,
                signature = signature
            };

            return Json(data, JsonRequestBehavior.AllowGet);
        }

        [HttpGet]
        public JsonResult GetConfigAgentSignature(string url)
        {
            long timestamp= Convert.ToInt64(GenerateTimeStamp());
            string nonceStrAgent = GenerateNonceStr();

            string access_token = GetToken(corpId, corpSecret);
            string jsapi_ticket = GetTicket(access_token, agentId, "agent_config");

            //采用排序的Dictionary的好处是方便对数据包进行签名,不用再签名之前再做一次排序
            SortedDictionary<string, object> m_values_agentConfig = new SortedDictionary<string, object>();
            m_values_agentConfig["jsapi_ticket"] = jsapi_ticket;
            m_values_agentConfig["noncestr"] = nonceStrAgent;
            m_values_agentConfig["timestamp"] = timestampAgent;
            //企微自动传过来的参数key的值要进行转换 [] 
            url = url.Replace("[", "%5B").Replace("]", "%5D");
            m_values_agentConfig["url"] = url;
            string signContent = GetSignContent(m_values_agentConfig);
            string signature = CalSHA1(signContent);

            object data = new
            {
                corpid = appId,
                agentid = agentId,
                timestamp = timestampAgent,
                noncestr = nonceStrAgent,
                jsapi_ticket = jsapi_ticket,
                url = url,
                signature = signature
            };

            return Json(data, JsonRequestBehavior.AllowGet);
        }

        /// <summary>
        /// Get Sign Content
        /// </summary>
        /// <param name="parameters"></param>
        /// <returns></returns>
        public string GetSignContent(SortedDictionary<string, object> parameters)
        {
            System.Text.StringBuilder query = new System.Text.StringBuilder();
            foreach (KeyValuePair<string, object> pair in parameters)
            {
                if (!string.IsNullOrEmpty(pair.Key))
                {
                    if (pair.Value != null)
                    {
                        query.Append(pair.Key).Append("=").Append(pair.Value.ToString()).Append("&");
                    }
                    else
                    {
                        query.Append(pair.Key).Append("=").Append("").Append("&");
                    }
                }
            }
            return query.ToString().TrimEnd('&');
        }

        /// <summary>
        /// SHA签名算法
        /// </summary>
        /// <param name="str"></param>
        /// <returns></returns>
        public string CalSHA1(string str)
        {
            System.Security.Cryptography.SHA1 sha1 = new System.Security.Cryptography.SHA1CryptoServiceProvider();
            byte[] bytesIn = System.Text.Encoding.UTF8.GetBytes(str);
            byte[] bytesOut = sha1.ComputeHash(bytesIn);
            sha1.Dispose();
            string result = BitConverter.ToString(bytesOut);
            result = result.Replace("-", "");
            return result;
        }

        /// <summary>
        /// 获取access_token是调用企业微信API接口的第一步,相当于创建了一个登录凭证,其它的业务API接口,都需要依赖于access_token来鉴权调用者身份。
        /// 因此开发者,在使用业务接口前,要明确access_token的颁发来源,使用正确的access_token。
        /// </summary>
        /// <param name="corpId">企业微信的企业ID</param>
        /// <param name="corpSecret">企业微信应用的Secret</param>
        /// <returns></returns>
        public string GetToken(string corpId, string corpSecret)
        {
            string access_token = "";
            try
            {
                string reqUrl = string.Format("https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid={0}&corpsecret={1}", corpId, corpSecret);
                string resData = HttpGet(reqUrl);
                dynamic result = JsonConvert.DeserializeObject<dynamic>(resData);
                if (result.error == 0)
                {
                    access_token = result.access_token;
                }
            }
            catch (Exception ex)
            {
                throw ex;
            }
            return access_token;
        }

        /// <summary>
        /// 获取调用微信JS接口的临时票据(企业微信)
        /// (JS-SDK使用权限签名算法 企业的jsapi_ticket、应用的jsapi_ticket)
        /// </summary>
        /// <param name="accessToken">接口调用凭证</param>
        /// <param name="agent">企业微信对应应用AgentId</param>
        /// <param name="type">默认为空时获取企业的jsapi_ticket,为 agent_config 时获取应用的jsapi_ticket</param>
        /// <returns></returns>
        public string GetTicket(string accessToken, string agent, string type = "")
        {
            string ticket = "";
            try
            {
                string tempUrl = "";
                if (!string.IsNullOrWhiteSpace(type))
                {
                    tempUrl = "ticket/get";
                }
                else
                {
                    tempUrl = "get_jsapi_ticket";
                }
                string reqUrl = string.Format("https://qyapi.weixin.qq.com/cgi-bin/{0}?access_token={1}", tempUrl, accessToken);
                if (!string.IsNullOrWhiteSpace(type))
                {
                    reqUrl += string.Format("&type={0}", type);
                }
                string resData = HttpGet(reqUrl);
                dynamic result = JsonConvert.DeserializeObject<dynamic>(resData);
                if (result.error == 0)
                {
                    ticket = result.ticket;
                }
            }
            catch (Exception ex)
            {
                throw ex;
            }
            return ticket;
        }
        
        /// <summary>
        /// 发送 Get 请求
        /// </summary>
        /// <param name="url">请求的地址</param>
        /// <param name="timeout">超时时间 默认为30秒</param>
        /// <returns></returns>
        public string HttpGet(string url, int timeout = 30000)
        {
            string result = "";
            System.Net.HttpWebRequest request = null;
            try
            {
                request = (System.Net.HttpWebRequest)System.Net.WebRequest.Create(url);
                request.Timeout = timeout;
                request.Method = "GET";
                request.ContentType = "application/json;charset=UTF-8";

                using (System.Net.HttpWebResponse response = (System.Net.HttpWebResponse)request.GetResponse())
                {
                    using (System.IO.Stream stream = response.GetResponseStream())
                    {
                        //获取响应内容
                        using (System.IO.StreamReader reader = new System.IO.StreamReader(stream, System.Text.Encoding.UTF8))
                        {
                            result = reader.ReadToEnd();
                        }
                    }
                }
            }
            catch (Exception ex)
            {
                throw ex;
            }
            finally
            {
                if (request != null)
                {
                    request.Abort();
                }
            }
            return result;
        }
        /// <summary>
        /// 生成时间戳,标准北京时间,时区为东八区,自1970年1月1日 0点0分0秒以来的秒数
        /// </summary>
        /// <returns>时间戳</returns>
        public string GenerateTimeStamp()
        {
            TimeSpan ts = DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, 0);
            return Convert.ToInt64(ts.TotalSeconds).ToString();
        }
        /// <summary>
        /// 生成随机串,随机串包含字母或数字
        /// </summary>
        /// <returns>随机串</returns>
        public string GenerateNonceStr()
        {
            System.Security.Cryptography.RNGCryptoServiceProvider csp = new System.Security.Cryptography.RNGCryptoServiceProvider();
            byte[] buffer = new byte[sizeof(uint)];
            csp.GetBytes(buffer);
            return BitConverter.ToUInt32(buffer, 0).ToString();
        }
    }

    public class selectedDataItem
    {
        public string key { set; get; }
        public string value { set; get; }
    }
}

前端代码:

@{
    Layout = null;
}
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
    <meta name="viewport" content="width=device-width,initial-scale=1" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <title>@ViewBag.Title</title>
    <link rel="stylesheet" type="text/css" href="https://res.wx.qq.com/open/libs/weui/1.1.2/weui-for-work.min.css" />
</head>
<body data-color-mode="dark">
    <div class="page">
        <div class="page__bd">
            <div class="weui-cells">
                <div class="weui-cell">
                    <div class="weui-cell__bd">
                        <input class="weui-input" type="text" placeholder="请输入搜索内容" id="searchInput" />
                    </div>
                </div>
            </div>
            <div class="weui-cells weui-cells_radio" id="item_list">
                @foreach (var item in ViewBag.ItemData)
                {
                    if (@ViewBag.selectorType == "single")
                    {
                        <label class="weui-cell weui-check__label" for="@item.value">
                            <div class="weui-cell__bd">
                                <p>@item.value</p>
                            </div>
                            <div class="weui-cell__ft">
                                <input type="radio" class="weui-check" name="radio1" id="@item.value" value="@item.key" onclick="SaveSelectedData()" />
                                <span class="weui-icon-checked"></span>
                            </div>
                        </label>
                    }
                    else if (@ViewBag.selectorType == "multi")
                    {
                        <label class="weui-cell weui-check__label" for="@item.value">
                            <div class="weui-cell__hd">
                                <input type="checkbox" class="weui-check" name="checkbox1" id="@item.value" value="@item.key" onclick="SaveSelectedData()" />
                                <i class="weui-icon-checked"></i>
                            </div>
                            <div class="weui-cell__bd">
                                <p>@item.value</p>
                            </div>
                        </label>
                    }
                }
            </div>
        </div>
    </div>
    <script src="https://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 src="https://code.jquery.com/jquery-3.3.1.min.js"></script>
    <script type="text/javascript">
        var dataType = GetQueryString('dataType');
        var selectorType = GetQueryString('selectorType');
        var itemData = [];
        var oldSelectedData = [];

        var url = location.href.split('#')[0];
        //console.log(encodeURIComponent(url));
        
        $(document).ready(function () {

            $('#searchInput').on("input", function (e) {
                let search = $(this).val();
                // 处理输入事件
                console.log('输入的内容:', search);
                //alert(search);
                ShowItem(search);
            });

            GetConfigSignature(url).then(
                function (resConfig) {
                    console.log("wx.config", resConfig);
                    wx.config({
                        beta: true,// 必须这么写,否则wx.invoke调用形式的jsapi会有问题
                        debug: false, // 开启调试模式,调用的所有api的返回值会在客户端alert出来,若要查看传入的参数,可以在pc端打开,参数信息会通过log打出,仅在pc端时才会打印。
                        appId: resConfig.appId, // 必填,企业微信的corpID,必须是本企业的corpID,不允许跨企业使用
                        timestamp: resConfig.timestamp, // 必填,生成签名的时间戳
                        nonceStr: resConfig.noncestr, // 必填,生成签名的随机串
                        signature: resConfig.signature,// 必填,签名,见 附录-JS-SDK使用权限签名算法
                        jsApiList: ['selectExternalContact', 'saveApprovalSelectedItems', 'getApprovalSelectedItems', 'agentConfig'] // 必填,需要使用的JS接口列表,凡是要调用的接口都需要传进来
                    });

                    wx.ready(function () {
                        GetConfigAgentSignature(url).then(
                            function (resConfigAgent) {
                                console.log("wx.agentConfig", resConfigAgent);
                                // config信息验证后会执行ready方法,所有接口调用都必须在config接口获得结果之后,config是一个客户端的异步操作,所以如果需要在页面加载时就调用相关接口,则须把相关接口放在ready函数中调用来确保正确执行。对于用户触发时才调用的接口,则可以直接调用,不需要放在ready函数中。
                                wx.agentConfig({
                                    debug: false, // 开启调试模式,调用的所有api的返回值会在客户端alert出来,若要查看传入的参数,可以在pc端打开,参数信息会通过log打出,仅在pc端时才会打印。
                                    corpid: resConfigAgent.corpid, // 必填,企业微信的corpid,必须与当前登录的企业一致
                                    agentid: resConfigAgent.agentid, // 必填,企业微信的应用id (e.g. 1000247)
                                    timestamp: resConfigAgent.timestamp, // 必填,生成签名的时间戳
                                    nonceStr: resConfigAgent.noncestr, // 必填,生成签名的随机串
                                    signature: resConfigAgent.signature,// 必填,签名,见附录-JS-SDK使用权限签名算法
                                    jsApiList: ['selectExternalContact', 'saveApprovalSelectedItems', 'getApprovalSelectedItems'], //必填,传入需要使用的接口名称
                                    success: function (res) {
                                        console.log("agentConfig调用成功。" + JSON.stringify(res));
                                        GetSelectedData().then(
                                            function (res) {
                                                if (res) {
                                                    oldSelectedData = JSON.parse(res);
                                                    var inputType = "";
                                                    if (selectorType == "single") {
                                                        inputType = "radio";
                                                    }
                                                    else if (selectorType == "multi") {
                                                        inputType = "checkbox";
                                                    }

                                                    oldSelectedData.forEach((item, index) => {
                                                        $('input[type="' + inputType + '"][name="' + inputType + '1"][id="' + item.value + '"]').attr('checked', true);
                                                    });
                                                }
                                            },
                                            function () {
                                                alert('获取 getApprovalSelectedItems 出错了');
                                            }
                                        );
                                    },
                                    fail: function (res) {
                                        if (res.errMsg.indexOf('function not exist') > -1) {
                                            alert('版本过低请升级')
                                        }
                                    }
                                });
                            },
                            function (err) {
                                alert('获取 wx.agentConfig 出错了');
                            }
                        );
                    });
                },
                function (err) {
                    alert('获取 wx.config 出错了');
                }
            );
            
            wx.error(function (res) {
                // config信息验证失败会执行error函数,如签名过期导致验证失败,具体错误信息可以打开config的debug模式查看,也可以在返回的res参数中查看,对于SPA可以在这里更新签名。
                alert(JSON.stringify(res));
            });
        });

        function ShowItem(search) {
            if (typeof (search) !== "undefined" && search !== null && search !== "")
            {
                var inputType = "";
                if (selectorType == "single") {
                    inputType = "radio";
                }
                else if (selectorType == "multi") {
                    inputType = "checkbox";
                }
                
                $('input[type="' + inputType + '"][name="' + inputType + '1"]').each(function () {
                    var value = $(this).attr("id");
                    // 已选中的 增加 checked  没选中的 移除 checked
                    let isExistsKey = ExistsKey(value);
                    if (isExistsKey) {
                        $(this).attr('checked', true);
                    }
                    else {
                        $(this).removeAttr('checked');
                    }
                    // 匹配到的显示 未匹配到的隐藏
                    if (value.indexOf(search) == -1) {
                        $(this).parent().parent().hide();
                    }
                    else {
                        $(this).parent().parent().show();
                    }
                });
            }
        }
        
        function GetSelectedData() {
            return new Promise((resolve, reject) => {
                wx.invoke('getApprovalSelectedItems', {
                    "key": GetQueryString('key'), // 字符串,从 URL 中获取到的 key
                }, (res) => {
                    console.log("原始选项值" + JSON.stringify(res));
                    if (res.err_msg === "getApprovalSelectedItems:ok") {
                        // 获取成功,res.selectedData 为获取到的已选中选项的 JSON 字符串,注意可能为空。格式见下文。
                        resolve(res.selectedData);
                    }
                    else {
                        reject(res)
                    }
                });
            });
        }

        function SaveSelectedData() {
            var selectedData = GetRadioValue();
            wx.invoke('saveApprovalSelectedItems', {
                "key": GetQueryString('key'), // 字符串,从 URL 中获取到的 key
                "selectedData": selectedData // 字符串,选中的选项格式化为 JSON 字符串,格式见下文
            }, (res) => {
                console.log("选项保存成功。" + JSON.stringify(res) + " " + JSON.stringify(selectedData));
                if (res.err_msg === 'saveApprovalSelectedItems:ok') {
                    // 保存成功

                }
            });
        }

        function GetRadioValue() {;
            var inputType = "";
            if (selectorType == "single") {
                inputType = "radio";
            }
            else if (selectorType == "multi") {
                inputType = "checkbox";
            }
            var selectData = [];
            $('input[type="' + inputType + '"][name="' + inputType + '1"]:checked').each(function () {
                let key = $(this).val();
                let value = $(this).attr("id");
                if (typeof (key) !== "undefined" && key !== null && key !== "") {
                    selectData.push({
                        key: key, value: value
                    });
                }
            });
            return selectData;
        }

        function ExistsKey(key)
        {
            oldSelectedData.forEach((item, index) => {
                if (item.key == key) {
                    return true;
                }
            });
            return false;
        }

        function GetQueryString(name) {
            var reg = new RegExp("(^|&)" + name + "=([^&]*)(&|$)", "i");
            var r = window.location.search.substr(1).match(reg); //获取url中"?"符后的字符串并正则匹配
            var context = "";
            if (r != null)
                context = decodeURIComponent(r[2]);
            reg = null;
            r = null;
            return context == null || context == "" || context == "undefined" ? "" : context;
        }
        
        function GetConfigSignature(url) {
            return new Promise((resolve, reject) => {
                // 发起 Ajax 请求
                $.ajax({
                    url: '@Url.Action("GetConfigSignature", "ExternalOptions")', // 控制器和动作的路径
                    type: 'GET',
                    data: { url: url },
                    dataType: 'json',
                    success: function (res) {
                        //console.log(res);
                        resolve(res);
                    },
                    error: function (err) {
                        // 错误处理
                        //console.log(err)
                        reject(err);
                    }
                });
            });
        }

        function GetConfigAgentSignature(url) {
            return new Promise((resolve, reject) => {
                // 发起 Ajax 请求
                $.ajax({
                    url: '@Url.Action("GetConfigAgentSignature", "ExternalOptions")', // 控制器和动作的路径
                    type: 'GET',
                    data: { url: url },
                    dataType: 'json',
                    success: function (res) {
                        //console.log(res);
                        resolve(res);
                    },
                    error: function (err) {
                        // 错误处理
                        //console.log(err)
                        reject(err);
                    }
                });
            });
        }
    </script>
</body>
</html>

审批控件中的外部选项  页面地址 :https://域名/ExternalOptions?dataType=Purchase  就可以获取采购单列表数据  https://域名/ExternalOptions?dataType=Supplier  就可以获取供应商列表数据。

以上基本可以成功。

另外发现一个问题是:PC端企业微信Windows和苹果手机端IOS,选择选项后确定按钮还是灰色;安卓手机测试可以正常。

posted on 2024-06-18 15:45  £冷☆月№  阅读(255)  评论(0编辑  收藏  举报