審批控件中的外部選項(xiàng) - 文檔 - 企業(yè)微信開(kāi)發(fā)者中心 (qq.com)
相關(guān)文檔需要仔細(xì)閱讀。
注意事項(xiàng):
(1)signature時(shí),url 的問(wèn)題:如果設(shè)置的是明細(xì)中的選項(xiàng)時(shí),企微自動(dòng)加上的參數(shù)中key的值會(huì)有 中括號(hào) [] , 我們要將這兩個(gè)符號(hào)進(jìn)行下轉(zhuǎn)換,[ 轉(zhuǎn)換成 %5B 、] 轉(zhuǎn)換成 %5D,之后使用轉(zhuǎn)換后的url 進(jìn)行 signature
(2)簽名按照官方文檔
(3)access_token 、 ticket 要做本地緩存保存
(4)應(yīng)用要設(shè)置可信域名、可信IP
1. JS-SDK 中 wx.config 、wx.agentConfig
wx.config({ beta: true,// 必須這么寫(xiě),否則wx.invoke調(diào)用形式的jsapi會(huì)有問(wèn)題 debug: false, // 開(kāi)啟調(diào)試模式,調(diào)用的所有api的返回值會(huì)在客戶端alert出來(lái),若要查看傳入的參數(shù),可以在pc端打開(kāi),參數(shù)信息會(huì)通過(guò)log打出,僅在pc端時(shí)才會(huì)打印。 appId: resConfig.appId, // 必填,企業(yè)微信的corpID,必須是本企業(yè)的corpID,不允許跨企業(yè)使用 timestamp: resConfig.timestamp, // 必填,生成簽名的時(shí)間戳 nonceStr: resConfig.noncestr, // 必填,生成簽名的隨機(jī)串 signature: resConfig.signature,// 必填,簽名,見(jiàn) 附錄-JS-SDK使用權(quán)限簽名算法 jsApiList: ['selectExternalContact', 'saveApprovalSelectedItems', 'getApprovalSelectedItems', 'agentConfig'] // 必填,需要使用的JS接口列表,凡是要調(diào)用的接口都需要傳進(jìn)來(lái) });
wx.agentConfig({ debug: false, // 開(kāi)啟調(diào)試模式,調(diào)用的所有api的返回值會(huì)在客戶端alert出來(lái),若要查看傳入的參數(shù),可以在pc端打開(kāi),參數(shù)信息會(huì)通過(guò)log打出,僅在pc端時(shí)才會(huì)打印。 corpid: resConfigAgent.corpid, // 必填,企業(yè)微信的corpid,必須與當(dāng)前登錄的企業(yè)一致 agentid: resConfigAgent.agentid, // 必填,企業(yè)微信的應(yīng)用id (e.g. 1000247) timestamp: resConfigAgent.timestamp, // 必填,生成簽名的時(shí)間戳 nonceStr: resConfigAgent.noncestr, // 必填,生成簽名的隨機(jī)串 signature: resConfigAgent.signature,// 必填,簽名,見(jiàn)附錄-JS-SDK使用權(quán)限簽名算法 jsApiList: ['selectExternalContact', 'saveApprovalSelectedItems', 'getApprovalSelectedItems'], //必填,傳入需要使用的接口名稱 success: function (res) { console.log("agentConfig調(diào)用成功。" + JSON.stringify(res)); }, fail: function (res) { if (res.errMsg.indexOf('function not exist') > -1) { alert('版本過(guò)低請(qǐng)升級(jí)') } } });
2. access_token 、 ticket 這寫(xiě)暫不做詳細(xì)說(shuō)明,可以自行參考API
3.采用 .NET MVC 開(kāi)發(fā) 相關(guān)代碼如下:
后端代碼:
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 = ""; //企業(yè)微信的企業(yè)ID public const string agentId = ""; //企業(yè)微信的應(yīng)用的AgentId public const string corpSecret = ""; //企業(yè)微信的應(yīng)用的Secret /// <summary> /// GET: ExternalOptions /// selectorType 、 key 參數(shù)是企業(yè)微信那邊自動(dòng)添加上的 /// <summary> /// <param name="dataType">調(diào)用什么數(shù)據(jù),如采購(gòu)單:Purchase、供應(yīng)商:Supplier</param> /// <param name="selectorType">表示該選擇控件是單選還是多選,單選 - single,多選 - multi。請(qǐng)注意根據(jù)此參數(shù)限制用戶的選擇行為:?jiǎn)芜x時(shí),只能選擇1個(gè),選擇多個(gè)將報(bào)錯(cuò);多選時(shí),最多選擇 300 個(gè),超過(guò)將報(bào)錯(cuò)。</param> /// <param name="key">調(diào)用下述接口時(shí)需要原值傳入</param> /// <returns></returns> public ActionResult Index(string dataType, string selectorType, string key) { if (dataType == "Purchase") { ViewData["Title"] = "采購(gòu)單列表"; } else if (dataType == "Supplier") { ViewData["Title"] = "供應(yīng)商列表"; } //將要展示的選項(xiàng)數(shù)據(jù)放到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的好處是方便對(duì)數(shù)據(jù)包進(jìn)行簽名,不用再簽名之前再做一次排序 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; //企微自動(dòng)傳過(guò)來(lái)的參數(shù)key的值要進(jìn)行轉(zhuǎn)換 [] 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的好處是方便對(duì)數(shù)據(jù)包進(jìn)行簽名,不用再簽名之前再做一次排序 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; //企微自動(dòng)傳過(guò)來(lái)的參數(shù)key的值要進(jìn)行轉(zhuǎn)換 [] 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是調(diào)用企業(yè)微信API接口的第一步,相當(dāng)于創(chuàng)建了一個(gè)登錄憑證,其它的業(yè)務(wù)API接口,都需要依賴于access_token來(lái)鑒權(quán)調(diào)用者身份。 /// 因此開(kāi)發(fā)者,在使用業(yè)務(wù)接口前,要明確access_token的頒發(fā)來(lái)源,使用正確的access_token。 /// </summary> /// <param name="corpId">企業(yè)微信的企業(yè)ID</param> /// <param name="corpSecret">企業(yè)微信應(yīng)用的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> /// 獲取調(diào)用微信JS接口的臨時(shí)票據(jù)(企業(yè)微信) /// (JS-SDK使用權(quán)限簽名算法 企業(yè)的jsapi_ticket、應(yīng)用的jsapi_ticket) /// </summary> /// <param name="accessToken">接口調(diào)用憑證</param> /// <param name="agent">企業(yè)微信對(duì)應(yīng)應(yīng)用AgentId</param> /// <param name="type">默認(rèn)為空時(shí)獲取企業(yè)的jsapi_ticket,為 agent_config 時(shí)獲取應(yīng)用的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> /// 發(fā)送 Get 請(qǐng)求 /// </summary> /// <param name="url">請(qǐng)求的地址</param> /// <param name="timeout">超時(shí)時(shí)間 默認(rèn)為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()) { //獲取響應(yīng)內(nèi)容 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> /// 生成時(shí)間戳,標(biāo)準(zhǔn)北京時(shí)間,時(shí)區(qū)為東八區(qū),自1970年1月1日 0點(diǎn)0分0秒以來(lái)的秒數(shù) /// </summary> /// <returns>時(shí)間戳</returns> public string GenerateTimeStamp() { TimeSpan ts = DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, 0); return Convert.ToInt64(ts.TotalSeconds).ToString(); } /// <summary> /// 生成隨機(jī)串,隨機(jī)串包含字母或數(shù)字 /// </summary> /// <returns>隨機(jī)串</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="請(qǐng)輸入搜索內(nèi)容" 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('輸入的內(nèi)容:', search); //alert(search); ShowItem(search); }); GetConfigSignature(url).then( function (resConfig) { console.log("wx.config", resConfig); wx.config({ beta: true,// 必須這么寫(xiě),否則wx.invoke調(diào)用形式的jsapi會(huì)有問(wèn)題 debug: false, // 開(kāi)啟調(diào)試模式,調(diào)用的所有api的返回值會(huì)在客戶端alert出來(lái),若要查看傳入的參數(shù),可以在pc端打開(kāi),參數(shù)信息會(huì)通過(guò)log打出,僅在pc端時(shí)才會(huì)打印。 appId: resConfig.appId, // 必填,企業(yè)微信的corpID,必須是本企業(yè)的corpID,不允許跨企業(yè)使用 timestamp: resConfig.timestamp, // 必填,生成簽名的時(shí)間戳 nonceStr: resConfig.noncestr, // 必填,生成簽名的隨機(jī)串 signature: resConfig.signature,// 必填,簽名,見(jiàn) 附錄-JS-SDK使用權(quán)限簽名算法 jsApiList: ['selectExternalContact', 'saveApprovalSelectedItems', 'getApprovalSelectedItems', 'agentConfig'] // 必填,需要使用的JS接口列表,凡是要調(diào)用的接口都需要傳進(jìn)來(lái) }); wx.ready(function () { GetConfigAgentSignature(url).then( function (resConfigAgent) { console.log("wx.agentConfig", resConfigAgent); // config信息驗(yàn)證后會(huì)執(zhí)行ready方法,所有接口調(diào)用都必須在config接口獲得結(jié)果之后,config是一個(gè)客戶端的異步操作,所以如果需要在頁(yè)面加載時(shí)就調(diào)用相關(guān)接口,則須把相關(guān)接口放在ready函數(shù)中調(diào)用來(lái)確保正確執(zhí)行。對(duì)于用戶觸發(fā)時(shí)才調(diào)用的接口,則可以直接調(diào)用,不需要放在ready函數(shù)中。 wx.agentConfig({ debug: false, // 開(kāi)啟調(diào)試模式,調(diào)用的所有api的返回值會(huì)在客戶端alert出來(lái),若要查看傳入的參數(shù),可以在pc端打開(kāi),參數(shù)信息會(huì)通過(guò)log打出,僅在pc端時(shí)才會(huì)打印。 corpid: resConfigAgent.corpid, // 必填,企業(yè)微信的corpid,必須與當(dāng)前登錄的企業(yè)一致 agentid: resConfigAgent.agentid, // 必填,企業(yè)微信的應(yīng)用id (e.g. 1000247) timestamp: resConfigAgent.timestamp, // 必填,生成簽名的時(shí)間戳 nonceStr: resConfigAgent.noncestr, // 必填,生成簽名的隨機(jī)串 signature: resConfigAgent.signature,// 必填,簽名,見(jiàn)附錄-JS-SDK使用權(quán)限簽名算法 jsApiList: ['selectExternalContact', 'saveApprovalSelectedItems', 'getApprovalSelectedItems'], //必填,傳入需要使用的接口名稱 success: function (res) { console.log("agentConfig調(diào)用成功。" + 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 出錯(cuò)了'); } ); }, fail: function (res) { if (res.errMsg.indexOf('function not exist') > -1) { alert('版本過(guò)低請(qǐng)升級(jí)') } } }); }, function (err) { alert('獲取 wx.agentConfig 出錯(cuò)了'); } ); }); }, function (err) { alert('獲取 wx.config 出錯(cuò)了'); } ); wx.error(function (res) { // config信息驗(yàn)證失敗會(huì)執(zhí)行error函數(shù),如簽名過(guò)期導(dǎo)致驗(yàn)證失敗,具體錯(cuò)誤信息可以打開(kāi)config的debug模式查看,也可以在返回的res參數(shù)中查看,對(duì)于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 沒(méi)選中的 移除 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("原始選項(xiàng)值" + JSON.stringify(res)); if (res.err_msg === "getApprovalSelectedItems:ok") { // 獲取成功,res.selectedData 為獲取到的已選中選項(xiàng)的 JSON 字符串,注意可能為空。格式見(jiàn)下文。 resolve(res.selectedData); } else { reject(res) } }); }); } function SaveSelectedData() { var selectedData = GetRadioValue(); wx.invoke('saveApprovalSelectedItems', { "key": GetQueryString('key'), // 字符串,從 URL 中獲取到的 key "selectedData": selectedData // 字符串,選中的選項(xiàng)格式化為 JSON 字符串,格式見(jiàn)下文 }, (res) => { console.log("選項(xiàng)保存成功。" + 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) => { // 發(fā)起 Ajax 請(qǐng)求 $.ajax({ url: '@Url.Action("GetConfigSignature", "ExternalOptions")', // 控制器和動(dòng)作的路徑 type: 'GET', data: { url: url }, dataType: 'json', success: function (res) { //console.log(res); resolve(res); }, error: function (err) { // 錯(cuò)誤處理 //console.log(err) reject(err); } }); }); } function GetConfigAgentSignature(url) { return new Promise((resolve, reject) => { // 發(fā)起 Ajax 請(qǐng)求 $.ajax({ url: '@Url.Action("GetConfigAgentSignature", "ExternalOptions")', // 控制器和動(dòng)作的路徑 type: 'GET', data: { url: url }, dataType: 'json', success: function (res) { //console.log(res); resolve(res); }, error: function (err) { // 錯(cuò)誤處理 //console.log(err) reject(err); } }); }); } </script> </body> </html>
審批控件中的外部選項(xiàng) 頁(yè)面地址 :https://域名/ExternalOptions?dataType=Purchase 就可以獲取采購(gòu)單列表數(shù)據(jù) https://域名/ExternalOptions?dataType=Supplier 就可以獲取供應(yīng)商列表數(shù)據(jù)。
以上基本可以成功。
另外發(fā)現(xiàn)一個(gè)問(wèn)題是:PC端企業(yè)微信Windows和蘋(píng)果手機(jī)端IOS,選擇選項(xiàng)后確定按鈕還是灰色;安卓手機(jī)測(cè)試可以正常。
浙公網(wǎng)安備 33010602011771號(hào)