使用.NetCore在钉钉中内嵌H5微应用的开发记录

刚开始查看钉钉文档的时候,可能是由于自身了解相关文档较少原因,阅读起来特别费劲,导致在搞清楚哪部分接口适合开发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步需要在后端实现,剩下的则是需要在前端实现。

 使用.NetCore在钉钉中内嵌H5微应用的开发记录

然后就可以调用钉钉所有的接口了,无论是否需要鉴权。

前端代码

使用.NetCore在钉钉中内嵌H5微应用的开发记录
  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

后端代码

使用.NetCore在钉钉中内嵌H5微应用的开发记录
  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

生成签名帮助类

使用.NetCore在钉钉中内嵌H5微应用的开发记录
 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}&timestamp={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
上一篇:订单支付流程梳理


下一篇:技术干货 | jsAPI 方式下的导航栏的动态化修改