刚开始查看钉钉文档的时候,可能是由于自身了解相关文档较少原因,阅读起来特别费劲,导致在搞清楚哪部分接口适合开发H5调用的环节都浪费了特别多的时间。所以我先将按照自己的理解简单的划分,我这里在使用上将钉钉的接口分为两部分,便于自己清晰钉钉文档,第一部分是前端接口,调用来用手机功能的接口,如弹框、获取当前位置;第二部分是后端调用服务端API接口对业务集成,如免登录。
需要用到的工具及资料
钉钉开发文档:创建H5微应用(前端接口,优先查看)。服务端接口
调试工具:开发必不可少调试,需要下载开发版钉钉,钉钉官网下载,在官网可以下载到安卓端或者PC端允许调试的开发版钉钉,这个开发版钉钉基于你的代码去调试;还有就是在线调试工具API Exploere。经过使用,我觉得
- 开发版钉钉。
PC端:可以用来调试一些不需要jsapi_ticket就可以调用的接口。jsapi_ticket:调用手机部分功能需要先获取票据
Android:有部分接口需要在手机才可以测试的功能。如:获取当前位置、获取手机基础信息
- 在线调试工具:这个工具我觉得是可以用来验证自己的参数是否正确比较合适,这个工具也只能调试部分接口;这个调试工具可以在你写代码时拷贝上面的代码,它会根据你的参数自动生成代码。
钉钉开发者后台:后台。用来查看创建的应用基本信息、开发管理、设置开发者权限等等。
对接钉钉流程
其实跟着文档章节的先后顺序来开发就可以,我这里列一下流程
1、获取AccessToken
2、获取JsApiTicket
3、生成签名
4、JsApi鉴权(这里指:dd.config())
5、到这里已经可以调用JsApi了
6、获取授权码。在dd.ready()中调用dd.runtime.permission.requestAuthCode()获取授权码,将授权码传到后端
7、免登。
第1、2、3、7步需要在后端实现,剩下的则是需要在前端实现。
然后就可以调用钉钉所有的接口了,无论是否需要鉴权。
前端代码
1 @{ 2 ViewData["Title"] = "Home Page"; 3 } 4 5 <script src="https://g.alicdn.com/dingding/dingtalk-jsapi/2.10.3/dingtalk.open.js"></script> 6 <script src="~/lib/jquery/dist/jquery.js"></script> 7 <script> 8 //获取鉴权需要用到的参数 9 var configModel; 10 $.ajaxSettings.async = false; 11 //Config方法包含第一、二、三步 12 $.get("/Home/Config", 13 { url: "当前网页的URL,鉴权使用" }, 14 function (data) { 15 console.log(data); 16 configModel = JSON.parse(data); 17 console.log(configModel); 18 //window.location.href = "/Home/Index"; 19 }); 20 dd.error(function (error) { 21 /** 22 { 23 errorMessage:"错误信息",// errorMessage 信息会展示出钉钉服务端生成签名使用的参数,请和您生成签名的参数作对比,找出错误的参数 24 errorCode: "错误码" 25 } 26 **/ 27 console.log('dd error: ' + JSON.stringify(error)); 28 alert('dd error: ' + JSON.stringify(error)); 29 }); 30 //第四步:鉴权 31 dd.config({ 32 agentId: configModel.AgentId, // 必填,微应用ID 33 corpId: configModel.CorpId,//必填,企业ID 34 timeStamp: configModel.TimeStamp, // 必填,生成签名的时间戳 35 nonceStr: configModel.NonceStr, // 必填,生成签名的随机串 36 signature: configModel.Signature, // 必填,签名 37 type: 0, // 1, //选填。0表示微应用的jsapi,1表示服务窗的jsapi;不填默认为0。该参数从dingtalk.js的0.8.3版本开始支持 38 jsApiList: [ 39 'runtime.info', 40 'biz.contact.choose', 41 'device.notification.confirm', 42 'device.notification.alert', 43 'device.notification.prompt', 44 'biz.ding.post', 45 'biz.util.openLink', 46 'device.geolocation.get', 47 'biz.map.locate', 48 'biz.map.view', 49 'biz.telephone.showCallMenu' 50 ] // 必填,需要使用的jsapi列表,注意:不要带dd。 51 }); 52 //第五步:dd.ready参数为回调函数,在环境准备就绪时触发,jsapi的调用需要保证在该回调函数触发后调用,否则无效。 53 dd.ready(function () { 54 //第六步:获取授权码 55 dd.runtime.permission.requestAuthCode({ 56 corpId: configModel.CorpId, 57 onSuccess: function (res) { 58 //AuthLogin是第七步 59 $.get("/Home/AuthLogin", 60 { authCode: res.code }, 61 function (data) { 62 DingAlert("获取用户信息成功!"); 63 //window.location.href = "/Home/Index"; 64 }); 65 }, 66 onFail: function (err) { 67 alert('dd error: ' + JSON.stringify(err)); 68 } 69 70 }); 71 72 73 }); 74 </script> 75 <div class="text-center"> 76 <h1 class="display-4">Hello Word!</h1> 77 </div> 78 <div class="text-center"> 79 <label id="locationLabel">locationLabel</label> 80 <button onclick="ShowLocation()">获取当前位置</button> 81 </div> 82 <div class="text-center"> 83 <button onclick="Locate()">定位</button> 84 </div> 85 <div class="text-center"> 86 <input type="text" id="phoneNumber" /> 87 <button onclick="ShowCallMenu()">拨打电话</button> 88 </div> 89 <script> 90 function ShowLocation() { 91 GetLocation(); 92 } 93 //获取当前地理位置信息(单次定位) 94 function GetLocation() { 95 dd.device.geolocation.get({ 96 targetAccuracy: 200, 97 coordinate: 1, 98 withReGeocode: false, 99 useCache: true, //默认是true,如果需要频繁获取地理位置,请设置false 100 onSuccess: function (result) { 101 /* 高德坐标 result 结构 102 { 103 longitude : Number, 104 latitude : Number, 105 accuracy : Number, 106 address : String, 107 province : String, 108 city : String, 109 district : String, 110 road : String, 111 netType : String, 112 operatorType : String, 113 errorMessage : String, 114 errorCode : Number, 115 isWifiEnabled : Boolean, 116 isGpsEnabled : Boolean, 117 isFromMock : Boolean, 118 provider : wifi|lbs|gps, 119 isMobileEnabled : Boolean 120 } 121 */ 122 ShowMap(result.longitude, result.latitude, "我的位置"); 123 }, 124 onFail: function (err) { 125 DingAlert(JSON.stringify(err)); 126 } 127 }); 128 } 129 130 function GetPhoneInfo() { 131 dd.device.base.getPhoneInfo({ 132 onSuccess: function (data) { 133 /* 134 { 135 screenWidth: 1080, // 手机屏幕宽度 136 screenHeight: 1920, // 手机屏幕高度 137 brand:'Mi', // 手机品牌 138 model:'Note4', // 手机型号 139 version:'7.0', // 版本 140 netInfo:'wifi', // 网络类型 wifi/4g/3g 141 operatorType:'xx' // 运营商信息 142 } 143 */ 144 DingAlert(JSON.stringify(data)); 145 }, 146 onFail: function (err) { 147 DingAlert(JSON.stringify(err)); 148 } 149 }); 150 } 151 152 //展示位置 153 function ShowMap(latitude, longitude, title) { 154 dd.biz.map.view({ 155 latitude: latitude, // 纬度 156 longitude: longitude, // 经度 157 title: title // 地址/POI名称 158 }); 159 } 160 //定位 161 function Locate(latitude, longitude) { 162 dd.biz.map.locate({ 163 latitude: latitude ? latitude : null, // 纬度,非必须 164 longitude: longitude ? longitude : null, // 经度,非必须 165 scope: 500, // 限制搜索POI的范围;设备位置为中心,scope为搜索半径 166 onSuccess: function (result) { 167 /* result 结构 168 { 169 province: 'xxx', // POI所在省会,可能为空 170 provinceCode: 'xxx', // POI所在省会编码,可能为空 171 city: 'xxx', // POI所在城市,可能为空 172 cityCode: 'xxx', // POI所在城市编码,可能为空 173 adName: 'xxx', // POI所在区名称,可能为空 174 adCode: 'xxx', // POI所在区编码,可能为空 175 distance: 'xxx', // POI与设备位置的距离 176 postCode: 'xxx', // POI的邮编,可能为空 177 snippet: 'xxx', // POI的街道地址,可能为空 178 title: 'xxx', // POI的名称 179 latitude: 39.903578, // POI的纬度 180 longitude: 116.473565, // POI的经度 181 } 182 */ 183 }, 184 onFail: function (err) { 185 } 186 }); 187 } 188 //拨打电话 189 function ShowCallMenu() { 190 var phoneNumber = $("#phoneNumber").val(); 191 dd.biz.telephone.showCallMenu({ 192 phoneNumber: phoneNumber, // 期望拨打的电话号码 193 code: '+86', // 国家代号,中国是+86 194 showDingCall: true, // 是否显示钉钉电话 195 onSuccess: function () { 196 console.log("ShowCallMenu:success"); 197 }, 198 onFail: function () { 199 console.log("ShowCallMenu:fail"); 200 } 201 }); 202 } 203 //弹窗 204 function DingAlert(mes) { 205 dd.device.notification.alert({ 206 message: mes, 207 title: "提示", 208 buttonName: "确认", 209 onSuccess: function (res) { 210 // 调用成功时回调 211 console.log(JSON.stringify(res)); 212 }, 213 onFail: function (err) { 214 // 调用失败时回调 215 console.log(JSON.stringify(err)); 216 } 217 }); 218 } 219 </script>View Code
后端代码
1 using DingTalk.Api; 2 using DingTalk.Api.Request; 3 using DingTalk.Api.Response; 4 using HJMinimally.Log; 5 using HJMinimally.Utility.Common; 6 using HJMinimally.Utility.Extensions; 7 using HJMinimally.Utility.Strings; 8 using Microsoft.AspNetCore.Mvc; 9 using System; 10 using System.Diagnostics; 11 using TestDingTalk.Models; 12 13 namespace TestDingTalk.Controllers 14 { 15 public class HomeController : Controller 16 { 17 public IActionResult Index() 18 { 19 return View(); 20 } 21 22 public IActionResult Privacy() 23 { 24 return View(); 25 } 26 27 [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] 28 public IActionResult Error() 29 { 30 return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier }); 31 } 32 33 private static string AccessToken = string.Empty; 34 /// <summary> 35 /// //第七步:免登 36 /// </summary> 37 /// <param name="authCode"></param> 38 /// <returns></returns> 39 [HttpGet] 40 public IActionResult AuthLogin(string authCode) 41 { 42 Logger.Debug($"authCode:{authCode}\r\n"); 43 var token = GetAccessToken(); 44 #region 服务端获取授权码 45 46 ////根据sns临时授权码获取用户在当前开放应用所属企业的唯一标识unionid。 47 //client = new DefaultDingTalkClient("https://oapi.dingtalk.com/sns/getuserinfo_bycode"); 48 //OapiSnsGetuserinfoBycodeRequest req = new OapiSnsGetuserinfoBycodeRequest(); 49 //req.TmpAuthCode = authCode; 50 //OapiSnsGetuserinfoBycodeResponse getuserinfoBycodeResponse = client.Execute(req, "dingoacofrn2pfp8gfj2bk", "w2NFFD6SQ_hJpn0dBRwKE_wwUhXOaYm72dWytNXoIU-DNCNIE49IyiE5vaGqegzl"); 51 //Logger.Debug($"getuserinfoBycodeResponse:{getuserinfoBycodeResponse.ToJson()}\r\n"); 52 //var userInfo = getuserinfoBycodeResponse.UserInfo; 53 54 ////根据unionid获取userid 55 //client = new DefaultDingTalkClient("https://oapi.dingtalk.com/topapi/user/getbyunionid"); 56 //OapiUserGetbyunionidRequest getbyunionidRequest = new OapiUserGetbyunionidRequest(); 57 //getbyunionidRequest.Unionid = userInfo.Unionid; 58 //OapiUserGetbyunionidResponse getbyunionidResponse = client.Execute(getbyunionidRequest, token); 59 //Logger.Debug($"getbyunionidResponse:{getbyunionidResponse.ToJson()}\r\n"); 60 //var userId = getbyunionidResponse.Result.Userid; 61 62 63 #endregion 64 DefaultDingTalkClient client = new DefaultDingTalkClient("https://oapi.dingtalk.com/user/getuserinfo"); 65 OapiUserGetuserinfoRequest getuserinfoRequest = new OapiUserGetuserinfoRequest(); 66 getuserinfoRequest.Code = authCode; 67 getuserinfoRequest.SetHttpMethod("GET"); 68 OapiUserGetuserinfoResponse getuserinfoResponse = client.Execute(getuserinfoRequest, token); 69 Logger.Debug($"getuserinfoResponse:{getuserinfoResponse.ToJson()}\r\n"); 70 var userId = getuserinfoResponse.Userid; 71 72 //根据userid获取用户详情 73 client = new DefaultDingTalkClient("https://oapi.dingtalk.com/topapi/v2/user/get"); 74 OapiV2UserGetRequest userGetRequest = new OapiV2UserGetRequest(); 75 userGetRequest.Userid = userId; 76 userGetRequest.Language = "zh_CN"; 77 OapiV2UserGetResponse userGetResponse = client.Execute(userGetRequest, token); 78 Logger.Debug($"userGetResponse:{userGetResponse.ToJson()}\r\n"); 79 return Content(userGetResponse.ToJson()); 80 } 81 82 [HttpGet] 83 public IActionResult Config(string url) 84 { 85 var configModel = new ConfigModel(); 86 // 87 /* 第二步:获取jsapi_ticket 88 *(1)当jsapi_ticket未过期时,再次调用get_jsapi_ticket会获取到一个全新的jsapi_ticket(和旧的jsapi_ticket值不同),这个全新的jsapi_ticket的过期时间是2小时。 89 *(2)jsapi_ticket是一个appKey对应一个,所以在使用的时候需要将jsapi_ticket以appKey为维度进行缓存下来(设置缓存过期时间2小时),并不需要每次都通过接口拉取。 90 */ 91 DefaultDingTalkClient client = new DefaultDingTalkClient("https://oapi.dingtalk.com/get_jsapi_ticket"); 92 OapiGetJsapiTicketRequest getJsapiTicketRequest = new OapiGetJsapiTicketRequest(); 93 getJsapiTicketRequest.SetHttpMethod("GET"); 94 var token = GetAccessToken(); 95 OapiGetJsapiTicketResponse getJsapiTicketResponse = client.Execute(getJsapiTicketRequest, token); 96 Logger.Debug($"getJsapiTicketResponse:{getJsapiTicketResponse.ToJson()}\r\n"); 97 configModel.JsTicket = getJsapiTicketResponse.Ticket; 98 #region 响应示例 99 100 /* 101 *{ 102 "errcode": 0, 103 "errmsg": "ok", 104 "ticket": "dsf8sdf87sd7f87sd8v8ds0vs09dvu09sd8vy87dsv87", //用于JSAPI的临时票据 105 "expires_in": 7200 //票据过期时间 106 } 107 */ 108 109 #endregion 110 //Str.Unique(); 111 configModel.NonceStr = Str.Unique(); 112 configModel.AgentId = "1050978221"; 113 configModel.CorpId = "ding69e4ec80d575281f"; 114 configModel.TimeStamp = ConvertTimestamp(DateTime.Now); 115 //第三步: 116 string signature = ""; 117 var res = DingTalkAuth.GenSigurate(configModel.NonceStr, configModel.TimeStamp.ToString(), configModel.JsTicket, url, ref signature); 118 Logger.Debug($"DingTalkAuth.GenSigurate:{(res == 0 ? signature : "失败")}\r\n"); 119 configModel.Signature = signature; 120 return Content(configModel.ToJson()); 121 } 122 /// <summary> 123 /// DateTime转换为Unix时间戳 124 /// </summary> 125 /// <param name="time"></param> 126 /// <returns></returns> 127 private long ConvertTimestamp(DateTime time) 128 { 129 double intResult = 0; 130 System.DateTime startTime = TimeZone.CurrentTimeZone.ToLocalTime(new System.DateTime(1970, 1, 1)); 131 intResult = (time - startTime).TotalMilliseconds; 132 return Converts.ToLong(intResult); 133 } 134 /// <summary> 135 /// 第一步:获取token 136 /// </summary> 137 /// <returns></returns> 138 private string GetAccessToken() 139 { 140 if (!string.IsNullOrEmpty(AccessToken)) 141 { 142 return AccessToken; 143 } 144 //获取API调用凭证access_token 145 DefaultDingTalkClient client = new DefaultDingTalkClient("https://oapi.dingtalk.com/gettoken"); 146 OapiGettokenRequest gettokenRequest = new OapiGettokenRequest(); 147 gettokenRequest.Appkey = "dingknomdr5kypvlbo2t"; 148 gettokenRequest.Appsecret = "AX1uZXvrtky8HNhkfYDdF_zlRP1oDK3HDMmI8DFMfh-SdMLmBk1vRLRl0HewtS8s"; 149 gettokenRequest.SetHttpMethod("GET"); 150 OapiGettokenResponse gettokenResponse = client.Execute(gettokenRequest); 151 Logger.Debug($"gettokenResponse:{gettokenResponse.ToJson()}\r\n"); 152 var token = gettokenResponse.AccessToken; 153 AccessToken = token; 154 return token; 155 } 156 157 private class ConfigModel 158 { 159 /// <summary> 160 /// 应用的标识 161 /// </summary> 162 public string AgentId { get; set; } 163 /// <summary> 164 /// CorpId 165 /// </summary> 166 public string CorpId { get; set; } 167 /// <summary> 168 /// JsAPI的临时票据 169 /// </summary> 170 public string JsTicket { get; set; } 171 /// <summary> 172 /// 随机串 173 /// </summary> 174 public string NonceStr { get; set; } 175 /// <summary> 176 /// 时间戳 177 /// </summary> 178 public long TimeStamp { get; set; } 179 /// <summary> 180 /// 签名 181 /// </summary> 182 public string Signature { get; set; } 183 } 184 } 185 }View Code
生成签名帮助类
1 using System; 2 using System.Security.Cryptography; 3 using System.Text; 4 5 namespace TestDingTalk 6 { 7 public static class DingTalkAuth 8 { 9 /// <summary> 10 /// </summary> 11 /// <param name="noncestr">随机字符串,自己随便填写即可</param> 12 /// <param name="sTimeStamp">当前时间戳</param> 13 /// <param name="jsapi_ticket">获取的jsapi_ticket</param> 14 /// <param name="url">当前网页的URL,不包含#及其后面部分</param> 15 /// <param name="signature">生成的签名</param> 16 /// <returns>0 成功,2 失败</returns> 17 public static int GenSigurate(string noncestr, string sTimeStamp, string jsapi_ticket, string url, ref string signature) 18 { 19 string assemble = string.Format("jsapi_ticket={0}&noncestr={1}×tamp={2}&url={3}", jsapi_ticket, noncestr, sTimeStamp, url); 20 SHA1 sha; 21 ASCIIEncoding enc; 22 string hash = ""; 23 try 24 { 25 sha = new SHA1CryptoServiceProvider(); 26 enc = new ASCIIEncoding(); 27 byte[] dataToHash = enc.GetBytes(assemble); 28 byte[] dataHashed = sha.ComputeHash(dataToHash); 29 hash = BitConverter.ToString(dataHashed).Replace("-", ""); 30 hash = hash.ToLower(); 31 } 32 catch (Exception) 33 { 34 return 2; 35 } 36 signature = hash; 37 return 0; 38 39 } 40 } 41 }View Code