钉钉H5微应用Springboot+Vue开发分享

文章目录

说明

由于钉钉开发文档的内容特别多,虽然介绍已经非常仔细了,当对于那些第一次看这个文档的时候,会有些疑惑。为了避免少走很多弯路,故写下该文章进行技术分享

  • 本文主要功能:1、钉钉免登录获取用户信息 2、钉钉获取当前的定位

简单来说,就是在钉钉里面,展示我们编写的手机格式大小的网页页面

技术路线

VUE作为前端开发框架,后端为Springboot项目

可以直接通过npm运行项目或者nginx运行项目

为了方便(只需要部署一个项目),我把vue打包成为静态文件,放置到Springboot的 static 文件中。

注意

1、钉钉开发文档,有时候叫 开发H5微应用,有时候叫 开发网页应用,注意分辨
2、开发过程中,有时候会用到小程序开发者工具,注意看说明书。jsapi接口有时候这个工具用不了,得实际放到钉钉dingtalk才有用
3、目前该分享,只是涉及到网页应用,不涉及小程序应用。要注意分辨

操作步骤

1、获取钉钉的应用(corpId/agentId/appKey/appSecret)。开发环境可以自己注册企业,自己创建钉钉应用(注意配置免密的权限)
2、创建java项目,pom引入钉钉的sdk
3、创建vue项目(或uniapp项目),npm引入sdk的依赖
4、拥有公网域名端口。开发环境可以使用(贝锐花生壳等工具)
5、打开钉钉开发者平台,配置钉钉应用的h5公网地址
6、打开手机钉钉,即可看到开发的页面

思路图

获取免登录

jsapi鉴权获取定位坐标(只有安卓端 或 苹果端有用)

一、创建钉钉应用

注册钉钉企业,打开钉钉开发者平台

https://open-dev.dingtalk.com/

记录下 corpId

创建应用

记录下 agentId、appKey、appSecret

二、创建java项目

POM引入依赖,因为钉钉的接口分为新的接口和旧的接口,目前最新的版本,新接口和旧接口都是可以使用的。所以两个接口的依赖同时引入

参考我上传到 gitee的后端代码

https://gitee.com/chencanzhan/cancan-java-share/tree/master/dingtalk-demo

核心pom文件

xml 复制代码
        <!-- 新的接口 -->
        <dependency>
            <groupId>com.aliyun</groupId>
            <artifactId>dingtalk</artifactId>
            <version>2.1.21</version>
        </dependency>

        <!-- 旧的接口 -->
        <dependency>
            <groupId>com.aliyun</groupId>
            <artifactId>alibaba-dingtalk-service-sdk</artifactId>
            <version>2.0.0</version>
        </dependency>

核心代码

java 复制代码
@Service
public class DingH5Service {

    @Value("${dingtalk.appKey}")
    private String appKey;

    @Value("${dingtalk.appSecret}")
    private String accessKeySecret;

    public DingUserInfo getUserByCode(String code) {
        DingTalkClient client = new DefaultDingTalkClient("https://oapi.dingtalk.com/topapi/v2/user/getuserinfo");
        OapiV2UserGetuserinfoRequest req = new OapiV2UserGetuserinfoRequest();
        req.setCode(code);
        OapiV2UserGetuserinfoResponse rsp = null;
        try {
            rsp = client.execute(req, getAccessToken());
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        cn.hutool.json.JSONObject entries = JSONUtil.parseObj(rsp.getBody());
        Integer errcode = entries.getInt("errcode");
        if(errcode == 0){
            cn.hutool.json.JSONObject result = entries.getJSONObject("result");
            DingUserInfo dingUserInfo = new DingUserInfo();
            dingUserInfo.setAssociatedUnionid(result.getStr("associated_unionid"));
            String unionid = result.getStr("unionid");
            dingUserInfo.setUnionid(unionid);
            String deviceId = result.getStr("device_id");
            dingUserInfo.setDeviceId(deviceId);
            dingUserInfo.setSysLevel(result.getInt("sys_level"));
            String name = result.getStr("name");
            dingUserInfo.setName(name);
            dingUserInfo.setSys(result.getBool("sys"));
            String userid = result.getStr("userid");
            dingUserInfo.setUserid(userid);

            return dingUserInfo;
        }
        return null;
    }

    public String getJsapiTicket() {
        com.aliyun.dingtalkoauth2_1_0.Client client = null;
        try {
            client = createClient();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        try {
            com.aliyun.dingtalkoauth2_1_0.models.CreateJsapiTicketHeaders createJsapiTicketHeaders = new com.aliyun.dingtalkoauth2_1_0.models.CreateJsapiTicketHeaders();
            createJsapiTicketHeaders.xAcsDingtalkAccessToken = getAccessToken();
            CreateJsapiTicketResponse jsapiTicketWithOptions = client.createJsapiTicketWithOptions(createJsapiTicketHeaders, new RuntimeOptions());
            CreateJsapiTicketResponseBody body = jsapiTicketWithOptions.getBody();
            return body.getJsapiTicket();
        } catch (TeaException err) {
            if (!com.aliyun.teautil.Common.empty(err.code) && !com.aliyun.teautil.Common.empty(err.message)) {
                // err 中含有 code 和 message 属性,可帮助开发定位问题
            }

        } catch (Exception _err) {
            TeaException err = new TeaException(_err.getMessage(), _err);
            if (!com.aliyun.teautil.Common.empty(err.code) && !com.aliyun.teautil.Common.empty(err.message)) {
                // err 中含有 code 和 message 属性,可帮助开发定位问题
            }

        }
        return null;
    }

    /**
     * 创建钉钉客户端
     * @return
     * @throws Exception
     */
    public static com.aliyun.dingtalkoauth2_1_0.Client createClient() throws Exception {
        com.aliyun.teaopenapi.models.Config config = new com.aliyun.teaopenapi.models.Config();
        config.protocol = "https";
        config.regionId = "central";
        return new com.aliyun.dingtalkoauth2_1_0.Client(config);
    }

    public String getAccessToken() throws Exception {
        com.aliyun.teaopenapi.models.Config config = new com.aliyun.teaopenapi.models.Config();
        config.protocol = "https";
        config.regionId = "central";
        com.aliyun.dingtalkoauth2_1_0.Client client = new com.aliyun.dingtalkoauth2_1_0.Client(config);
        com.aliyun.dingtalkoauth2_1_0.models.GetAccessTokenRequest getAccessTokenRequest = new com.aliyun.dingtalkoauth2_1_0.models.GetAccessTokenRequest()
                .setAppKey(appKey)
                .setAppSecret(accessKeySecret);
        try {
            return client.getAccessToken(getAccessTokenRequest).getBody().getAccessToken();
        } catch (TeaException err) {
            if (!com.aliyun.teautil.Common.empty(err.code) && !com.aliyun.teautil.Common.empty(err.message)) {
                // err 中含有 code 和 message 属性,可帮助开发定位问题
            }
        } catch (Exception _err) {
            TeaException err = new TeaException(_err.getMessage(), _err);
            if (!com.aliyun.teautil.Common.empty(err.code) && !com.aliyun.teautil.Common.empty(err.message)) {
                // err 中含有 code 和 message 属性,可帮助开发定位问题
            }
        }
        return null;
    }

}
java 复制代码
@RestController
@RequestMapping("/ding-h5")
public class DingH5Controller {

    @Value("${dingtalk.agentId}")
    private String agentId;

    @Value("${dingtalk.corpId}")
    private String corpId;

    @Value("${dingtalk.appKey}")
    private String appKey;

    @Value("${dingtalk.urlPath}")
    private String urlPath;

    @Autowired
    private DingH5Service dingH5Service;

    /**
     * 获取签名
     * @param dingConfigSignVo
     * @param request
     * @return
     */
    @PostMapping("/signAll")
    public ResponseEntity<Object> signAll(@RequestBody DingConfigSignVo dingConfigSignVo, HttpServletRequest request){
        String sign = null;
        String signedUrl = urlPath;
        String jticket =  dingH5Service.getJsapiTicket();
        dingConfigSignVo.setJsticket(jticket);
        Map<String, Object> jMap = new HashMap<>();
        try {
            sign = DdConfigSign.sign(dingConfigSignVo.getJsticket(),dingConfigSignVo.getNonceStr(),dingConfigSignVo.getTimeStamp(),signedUrl);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        jMap.put("agentId",agentId);
        jMap.put("corpId",corpId);
        jMap.put("appKey",appKey);
        jMap.put("sign",sign);
        return new ResponseEntity<>(jMap, HttpStatus.OK);
    }

    /**
     * 根据code获取用户信息
     * @param code
     * @return
     */
    @GetMapping("/getUserByCode")
    public ResponseEntity<Object> getUserByCode(String code){
        DingUserInfo userByCode = dingH5Service.getUserByCode(code);
        return new ResponseEntity<>(userByCode, HttpStatus.OK);
    }

}
java 复制代码
/**
 * 计算dd.config的签名参数
 **/
public class DdConfigSign {

    /**
     * 计算dd.config的签名参数
     *
     * @param jsticket  通过微应用appKey获取的jsticket
     * @param nonceStr  自定义固定字符串
     * @param timeStamp 当前时间戳
     * @param url       调用dd.config的当前页面URL
     * @return
     * @throws Exception
     */
    public static String sign(String jsticket, String nonceStr, long timeStamp, String url) throws Exception {
        String plain = "jsapi_ticket=" + jsticket + "&noncestr=" + nonceStr + "&timestamp=" + String.valueOf(timeStamp)
            + "&url=" + decodeUrl(url);
        try {
            MessageDigest sha1 = MessageDigest.getInstance("SHA-256");
            sha1.reset();
            sha1.update(plain.getBytes("UTF-8"));
            return byteToHex(sha1.digest());
        } catch (Exception e) {
            throw new Exception(e.getMessage());
        }
    }

    // 字节数组转化成十六进制字符串
    private static String byteToHex(final byte[] hash) {
        Formatter formatter = new Formatter();
        for (byte b : hash) {
            formatter.format("%02x", b);
        }
        String result = formatter.toString();
        formatter.close();
        return result;
    }

    /**
     * 因为ios端上传递的url是encode过的,android是原始的url。开发者使用的也是原始url,
     * 所以需要把参数进行一般urlDecode
     *
     * @param url
     * @return
     * @throws Exception
     */
    private static String decodeUrl(String url) throws Exception {
        URL urler = new URL(url);
        StringBuilder urlBuffer = new StringBuilder();
        urlBuffer.append(urler.getProtocol());
        urlBuffer.append(":");
        if (urler.getAuthority() != null && urler.getAuthority().length() > 0) {
            urlBuffer.append("//");
            urlBuffer.append(urler.getAuthority());
        }
        if (urler.getPath() != null) {
            urlBuffer.append(urler.getPath());
        }
        if (urler.getQuery() != null) {
            urlBuffer.append('?');
            urlBuffer.append(URLDecoder.decode(urler.getQuery(), "utf-8"));
        }
        return urlBuffer.toString();
    }

    public static String getRandomStr(int count) {
        String base = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
        Random random = new Random();
        StringBuffer sb = new StringBuffer();
        for (int i = 0; i < count; i++) {
            int number = random.nextInt(base.length());
            sb.append(base.charAt(number));
        }
        return sb.toString();
    }
}

三、创建vue项目(或uniapp项目),npm引入sdk的依赖

1、使用npm安装。
npm install dingtalk-jsapi --save

2、加载 dingtalk-jsapi
import * as dd from 'dingtalk-jsapi'; // 此方式为整体加载,也可按需进行加载

完整的代码

vue 复制代码
<template>
	<el-main>
		<div>
			用户名:{{name}}
		</div>

		<div>
			当前位置:{{rrsss.address}}
		</div>


		<button @click="handlegetSignAll">测试</button>



	</el-main>
</template>

<script>

import api from '@/api';
import * as dd from 'dingtalk-jsapi'; // 此方式为整体加载,也可按需进行加载

export default {
	data() {
		return {
			t1: 0,
			name: '',
			agentId: '',
			appKey: '',
			corpId: '',
			sign: '',
			rrsss: {},
		}
	},
	mounted() {
		this.handlegetSignAll();
	},
	methods: {
		handlegetSignAll() {
			this.t1 = Date.now()
			let params = {
				nonceStr: 'a',
				timeStamp: this.t1
			}
			api.getSignAll(params).then(res => {
				if (res && res.status === 200) {
					this.agentId = res.data.agentId
					this.appKey = res.data.appKey
					this.corpId = res.data.corpId
					this.sign = res.data.sign
					this.setDDConfig();
					this.getAuthCode();
				}
			})
		},
		setDDConfig() {
			/**钉钉鉴权 */
			dd.config({
				agentId: this.agentId, // 必填,微应用ID
				corpId: this.corpId,//必填,企业ID
				timeStamp: this.t1, // 必填,生成签名的时间戳
				nonceStr: 'a', // 必填,自定义固定字符串。
				signature: this.sign, // 必填,签名
				type: 0,   //选填。0表示微应用的jsapi,1表示服务窗的jsapi;不填默认为0。该参数从dingtalk.js的0.8.3版本开始支持
				jsApiList: [
					'device.geolocation.get'
				] // 必填,需要使用的jsapi列表,注意:不要带dd。
			})
			this.getGeolocation();
			//该方法必须带上,用来捕获鉴权出现的异常信息,否则不方便排查出现的问题
			dd.error(function () {
				console.log("钉钉鉴权失败,无法定位等,请联系管理员,或重新尝试!");
			})
		},
		getGeolocation() {
			dd.ready(() => {
			dd.device.geolocation.get({
				targetAccuracy: 200,
				coordinate: 1,
				withReGeocode: true,
				useCache: false,
				onSuccess: function (res) {
					// 调用成功时回调
					console.log(res)
					this.rrsss = res
				},
				onFail: function (err) {
					// 调用失败时回调
					console.log(err)
				}
			});
		})
		},
		getAuthCode() {
			dd.requestAuthCode({
				corpId: this.corpId,
				clientId: this.appKey,
				onSuccess: (result) => {
					api.getUserInfo({code:result.code}).then(res => {
					if (res && res.status === 200) {
						this.name = res.data.name
					}
				})
				},
				onFail: function () { },
			});
		}	
	}
}
</script>
<style scoped>
</style>

四、拥有公网域名端口。开发环境可以使用(贝锐花生壳等工具)

这自己百度,映射到本地端口

可以直接通过npm运行项目或者nginx运行项目
为了方便(只需要部署一个项目),我把vue打包成为静态文件,放置到Springboot的 static 文件中。

五、打开钉钉开发者平台,配置钉钉应用的h5公网地址

选择添加应用能力

填写公网域名

同时记得开放权限

六、打开手机钉钉,即可看到开发的页面

相关推荐
码上一元2 小时前
SpringBoot自动装配原理解析
java·spring boot·后端
计算机-秋大田2 小时前
基于微信小程序的养老院管理系统的设计与实现,LW+源码+讲解
java·spring boot·微信小程序·小程序·vue
魔道不误砍柴功4 小时前
简单叙述 Spring Boot 启动过程
java·数据库·spring boot
枫叶_v4 小时前
【SpringBoot】22 Txt、Csv文件的读取和写入
java·spring boot·后端
路在脚下@4 小时前
Springboot 的Servlet Web 应用、响应式 Web 应用(Reactive)以及非 Web 应用(None)的特点和适用场景
java·spring boot·servlet
尘浮生6 小时前
Java项目实战II基于微信小程序的移动学习平台的设计与实现(开发文档+数据库+源码)
java·开发语言·数据库·spring boot·学习·微信小程序·小程序
2401_857610036 小时前
Spring Boot框架:电商系统的技术优势
java·spring boot·后端
java—大象8 小时前
基于java+springboot+layui的流浪动物交流信息平台设计实现
java·开发语言·spring boot·layui·课程设计
ApiHug8 小时前
ApiSmart x Qwen2.5-Coder 开源旗舰编程模型媲美 GPT-4o, ApiSmart 实测!
人工智能·spring boot·spring·ai编程·apihug
魔道不误砍柴功8 小时前
探秘Spring Boot中的@Conditional注解
数据库·spring boot·oracle