钉钉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公网地址

选择添加应用能力

填写公网域名

同时记得开放权限

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

相关推荐
用户9083246027315 小时前
Spring AI 1.1.2 + Neo4j:用知识图谱增强 RAG 检索(上篇:图谱构建)
java·spring boot
用户8307196840821 天前
Spring Boot 集成 RabbitMQ :8 个最佳实践,杜绝消息丢失与队列阻塞
spring boot·后端·rabbitmq
Java水解1 天前
Spring Boot 视图层与模板引擎
spring boot·后端
Java水解1 天前
一文搞懂 Spring Boot 默认数据库连接池 HikariCP
spring boot·后端
洋洋技术笔记2 天前
Spring Boot Web MVC配置详解
spring boot·后端
初次攀爬者2 天前
Kafka 基础介绍
spring boot·kafka·消息队列
用户8307196840822 天前
spring ai alibaba + nacos +mcp 实现mcp服务负载均衡调用实战
spring boot·spring·mcp
Java水解2 天前
SpringBoot3全栈开发实战:从入门到精通的完整指南
spring boot·后端
初次攀爬者3 天前
RocketMQ在Spring Boot上的基础使用
java·spring boot·rocketmq
花花无缺3 天前
搞懂@Autowired 与@Resuorce
java·spring boot·后端