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