利用钉钉云盘实现业务系统需要的附件上传、下载和预览

您所在的位置:网站首页 钉盘如何上传文件打印 利用钉钉云盘实现业务系统需要的附件上传、下载和预览

利用钉钉云盘实现业务系统需要的附件上传、下载和预览

2024-07-09 05:15| 来源: 网络整理| 查看: 265

本文主要记录自己在工作学习中遇到的坑和解决思路,仅供大家参考

目录

前言

一、钉盘是什么?

二、为什么要使用钉盘?

三、JSAPI鉴权

1.鉴权的时机

2.鉴权的时效

3.鉴权的代码

3.1.获取access_token

3.2.获取jsapi_ticket

3.3.计算签名参数

3.4.引入JS

3.5.JSAPI鉴权

四、钉盘空间

1.添加空间

2.空间授权

五、上传和预览

1.上传

2.预览

六、问题与解决

1.问题

2.解决思路

七、总结

前言

       在工作中,经常会被要求提供附件的上传、下载和预览等功能,上传和下载实现起来较为简单,但是预览功能在尝试过多种实现方式后都不尽如人意。此时想到了网上是否有现成的第三方程序可以直接使用,鉴于一些不可描述的原因(就是领导不舍得花钱,就是抠~~~),最终选定了免费的钉钉云盘来实现预览功能。以下是我在实现过程中的一些心得和遇到的问题及其解决方式。

一、钉盘是什么?

        钉盘,顾名思义,就是钉钉自带的云盘,可以给注册企业开辟免费100G云空间(想扩容得加钱)用于文件的存储。

二、为什么要使用钉盘?

        钉钉提供了很多自带的API给广大的开发者,而调用这些API则可以实现我们的需求(主要原因还是免费)。

三、JSAPI鉴权

        钉钉不管后端提供了各种API,前端也提供了大量的JSAPI供大家使用,比如我们即将使用的上传和预览,都是前端可以直接用JS调用,但是由于不同客户端的区分,部分JSAPI需要在使用之前先要鉴权(比如钉钉的获取登录用户信息就不用鉴权,上传和预览则需要),具体哪些JSAPI需要鉴权可参考:https://open.dingtalk.com/document/isvapp/jsapi-overview

       鉴权的方式有很多种,这里我介绍一种我这次使用的鉴权方式

1.鉴权的时机

         上面我们说了为什么要鉴权,但为什么鉴权还有时机呢?那是因为鉴权是针对某一个页面URL的,比如我们新增页面要用到上传功能,这时候我们需要在用上传的JSAPI前先对当前新增页面进行鉴权,成功后才能在新增页面使用上传的JSAPI。那么问题来了,我一个应用有N个需要用到此类JSAPI的页面,那每个页面都需要单独做鉴权吗?那岂不是很麻烦?

        参考钉钉的开发者文档,加和钉钉开发人员你的沟通后,总结出鉴权只要在主页面鉴权一次,后续路由变动不影响鉴权的有效性。什么意思呢?举个例子,我通过登录页进入到首页后,只要在首页初始化时鉴权一次(比如对url:http://xxx.xxx.xxx.xxx:8080/index鉴权),后面通过变动路由实现跳转的页面将全部都有鉴权效果。

2.鉴权的时效

        鉴权一次后,是不是只要URL不变,鉴权是不是一直都有效呢?当然不是,当你关闭浏览器后重新通过浏览器登录系统后,需要重新鉴权,所以建议在登录时跟着登录的方法或进入首页时,调用鉴权。

3.鉴权的代码 3.1.获取access_token

        根据创建的H5微应用的AppKey和AppSecret获取,基操,做过钉钉应用开发的应该都会,新人可以参考:https://open.dingtalk.com/document/orgapp/obtain-orgapp-token#

3.2.获取jsapi_ticket

        调用钉钉API获取jsapi_ticket,jsapi_ticket有2小时的时效性(不要在意这些,因为我们获取后马上就会用上,不可能会给他过期的机会)

public static String getJsapiTicket(String accessToken) throws Exception { DingTalkClient client = new DefaultDingTalkClient("https://oapi.dingtalk.com/get_jsapi_ticket"); OapiGetJsapiTicketRequest req = new OapiGetJsapiTicketRequest(); req.setHttpMethod("GET"); OapiGetJsapiTicketResponse rsp = client.execute(req, accessToken); System.out.println(rsp.getBody()); return rsp.getTicket(); } 3.3.计算签名参数

        参数说明:

                jsticket:通过3.2获取;

                nonceStr:自定义标识,可以随意取;

                timeStamp:时间戳,一般用当前时间即可,需要注意的是需要保存下来,后面一起传递到前端使用,前后端的时间戳要保持一致

                url:需要鉴权的URL,上面提到的首页地址即可

public static String sign(String jsticket, String nonceStr, long timeStamp, String url) throws Exception { String plain = "jsapi_ticket=" + jsticket + "&noncestr=" + nonceStr + "×tamp=" + String.valueOf(timeStamp) + "&url=" + decodeUrl(url); MessageDigest sha1 = MessageDigest.getInstance("SHA-256"); sha1.reset(); sha1.update(plain.getBytes("UTF-8")); return byteToHex(sha1.digest()); } // 字节数组转化成十六进制字符串 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(); } 3.4.引入JS

        前端引入JS

3.5.JSAPI鉴权

        获取上面获取的鉴权信息,填入后即可鉴权成功。

ding.config({ agentId: XXXXXXXX, // 必填,微应用ID corpId: 'dingXXXXXXXXXX', // 必填,企业ID timeStamp: 123456789, // 必填,生成签名的时间戳 nonceStr: 'ceshi', // 必填,自定义固定字符串。 signature: '987654321', // 必填,签名 type: 0, // 选填。0表示微应用的jsapi,1表示服务窗的jsapi;不填默认为0。该参数从dingtalk.js的0.8.3版本开始支持 jsApiList: ['biz.cspace.preview'] // 必填,需要使用的jsapi列表,注意:不要带dd。 }) ding.error(function(err) { console.log('dd error: ' + JSON.stringify(err)) }) // 该方法必须带上,用来捕获鉴权出现的异常信息,否则不方便排查出现的问题 四、钉盘空间

        通过第三步的一顿操作我们终于把最复杂(恶心,yue~~~)的鉴权准备工作做好了,后续我们就可以调用上传和预览的JSAPI了,但在此之前,我们需要指定一个空间用来存放上传的附件,这里我们就需要在钉盘上添加一个特定的空间(可以理解为是一个文件夹),用于存储文件。

1.添加空间

        这个比较简单,话不多说,直接上代码。

Client client = createClient(); AddSpaceHeaders addSpaceHeaders = new AddSpaceHeaders(); addSpaceHeaders.xAcsDingtalkAccessToken = accessToken; AddSpaceRequest.AddSpaceRequestOptionCapabilities optionCapabilities = new AddSpaceRequest.AddSpaceRequestOptionCapabilities() .setCanSearch(true) // 是否支持搜索,默认否 .setCanRename(true) // 是否支持重命名空间名称,默认否 .setCanRecordRecentFile(true); // 是否支持被列入最近使用列表,默认否 AddSpaceRequest.AddSpaceRequestOption option = new AddSpaceRequest.AddSpaceRequestOption() .setName("测试") // 空间名称 // .setQuota(1024L) // 空间大小,不指定则不限制 .setCapabilities(optionCapabilities) .setScene("ceshi") // 空间场景 .setSceneId("001") // 空间场景Id,scene和sceneId两者一起确定一个空间,可以理解为联合主键 .setOwnerType("USER"); // owner类型,填USER即可 AddSpaceRequest addSpaceRequest = new AddSpaceRequest() .setUnionId("123456789") // 钉钉用户自带的unionId,可以通过钉钉API获取 .setOption(option); try { AddSpaceResponse addSpaceResponse = client.addSpaceWithOptions(addSpaceRequest, addSpaceHeaders, new RuntimeOptions()); System.out.println(addSpaceResponse.getBody().getSpace()); // 获取空间的ID } 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 属性,可帮助开发定位问题 } }

         注意:新创建的空间是无法通过客户端(比如手机钉钉或钉钉PC端)看到的,可以理解为创建的文件夹是不可见的,但确实真实存在的。正好用于我们保存上传的附件,因为我们只是想让用户可以把附件上传上来,后续不能通过客户端查看。

2.空间授权

        新创建的空间对于所有普通用户(非管理员)是没有权限操作的,企业的员工还无法上传和预览,我们需要对企业员工授权。

        参数说明:

                members.type:授权对象类型,ORG为企业;DEPT为部门;USER为用户,常用这仨

                members.id:ORG对应企业corpId,DEPT对应部门deptId,USER对应用户unionId

                roleId:OWNER为拥有者,MANAGER为管理者,EDITOR为编辑者,DOWNLOADER为下载者,READER为查看者,权限按顺利从高至低,没人只能保有一个roleId,即给用户添加了高级权限后,再添加低级权限也没用,反之则高权限覆盖低权限。如需要从高权限降至低权限则需要调用修改权限的API,参考:https://open.dingtalk.com/document/orgapp/modify-storage-permissions

String accessToken = accessToken; com.aliyun.dingtalkstorage_1_0.Client client = createClient(); AddPermissionHeaders addPermissionHeaders = new AddPermissionHeaders(); addPermissionHeaders.xAcsDingtalkAccessToken = accessToken; AddPermissionRequest.AddPermissionRequestOption option = new AddPermissionRequest.AddPermissionRequestOption(); AddPermissionRequest.AddPermissionRequestMembers members0 = new AddPermissionRequest.AddPermissionRequestMembers() .setType("ORG") // 授权对象类型 .setId("dingXXXXXXXXXXXX") .setCorpId("dingXXXXXXXXXXXX"); AddPermissionRequest addPermissionRequest = new AddPermissionRequest() .setUnionId("123456789") // 授权管理员的unionId .setRoleId("DOWNLOADER") // 赋予的权限 .setMembers(java.util.Arrays.asList( members0 )) .setOption(option); try { AddPermissionResponse addPermissionResponse = client.addPermissionWithOptions("123123123", "0", addPermissionRequest, addPermissionHeaders, new RuntimeOptions()); System.out.println(addPermissionResponse.getBody().getSuccess()); } catch (TeaException err) { if (!com.aliyun.teautil.Common.empty(err.code) && !com.aliyun.teautil.Common.empty(err.message)) { // err 中含有 code 和 message 属性,可帮助开发定位问题 System.out.println(err.code); System.out.println(err.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 属性,可帮助开发定位问题 System.out.println(err.code); System.out.println(err.message); } } 五、上传和预览

        OK,准备工作已经就绪,现在就可以上传和预览了,直接上代码

1.上传

        参数说明:

                compress:是否压缩,默认true

                spaceId:上面创建的空间ID

                folderId:空间中存放附件的文件夹ID,没有可以不传

                types:上传的方式

dd.biz.util.uploadAttachment({ image:{multiple:true,compress:false,max:9,spaceId: "12345",folderId:"123"}, space:{corpId:"xxx3020",spaceId:"12345",folderId:"123",isCopy:1 , max:9}, file:{spaceId:"12345",folderId:"123",max:1}, types:["photo","camera","file","space"],//PC端支持["photo","file","space"] onSuccess : function(result) { //onSuccess将在文件上传成功之后调用 /* { type:'', // 用户选择了哪种文件类型 ,image(图片)、file(手机文件)、space(钉盘文件) data: [ { spaceId: "232323", fileId: "DzzzzzzNqZY", fileName: "审批流程.docx", fileSize: 1024, fileType: "docx" }, { spaceId: "232323", fileId: "DzzzzzzNqZY", fileName: "审批流程1.pdf", fileSize: 1024, fileType: "pdf" }, { spaceId: "232323", fileId: "DzzzzzzNqZY", fileName: "审批流程3.pptx", fileSize: 1024, fileType: "pptx" } ] } */ }, onFail : function(err) {} }); 2.预览

        参数说明:都很好理解,友情提醒fileSize和fileType可以不填,不影响预览

dd.biz.cspace.preview({ corpId: "dingf8b3xxxxx", spaceId: "13557022", fileId: "11452819", fileName: "钉盘快速入门", fileSize: 1024, fileType: "pdf", onSuccess : function(res) { }, onFail : function(err) { } }); 六、问题与解决

        到这里是不是小伙伴们觉得已经完成了?我只能说你们还是太年轻了,下面我将我遇到的问题和解决思路分享出来,有更好的解决方法可以分享出来讨论。

1.问题

        为了每个用户都要可以上传附件,所以我们将会给每个企业用户设置钉盘空间的权限为EDITOR。那么问题来了,有了编辑者的权限后,所有用户都可以对空间内的附件进行修改,毕竟领导可不希望上传到服务器的附件今天显示的报销金额是1000,明天就变成了10000。这时候就会有小伙伴说了,不是新建的空间是客户端不可见的吗,我看不到我肯定也改不了啊。但事你们忽略了一个问题,我们还有预览功能。

        预览里面居然可以编辑,而且问过钉钉开发人员后得知还不能隐藏,为什么就不能在预览的JSAPI调用参数里添加一个参数来控制是否允许在预览时进行修改呢?当我知道无法通过代码层面来解决这个问题时,我的内心是崩溃的,连我这么菜的人都能想到的需求为啥钉钉开发人员没有想到。

2.解决思路

        崩溃归崩溃,但还是要实现甲方爸爸的需求(毕竟关系到了我的口袋)。既然钉钉无法实现我们的需求,我们就要换一个思路。经过翻开了大量钉钉API后我找到了一种办法:

        现在的问题是,预览只需要用户有READER或者DOWNLOADER权限即可,可以用户能上传,就必须要有EDITOR的权限,那么我们是否可以将两者分开呢?基于这个思路,我将原来的A空间不变,还是赋予员工EDITOR权限。然后新增B钉盘空间,赋予员工DOWNLOADER权限。在用户上传附件至A空间后,立即触发移动文件的API,将刚上传的附件从A空间移动到B空间。之后就简单了,前端的预览功能统一访问B空间的文件。这样就做到了权限分离,上传和预览分开,顺利解决。上代码:

        参数说明:

                option.conflictStrategy:

                        auto_rename:自动重命名,默认值

                        overwrite:覆盖

                        return_dentry_if_exists:返回已存在

                        return_error_if_exists:报错

                unionId:操作人的unionId,需要注意的是由于目标空间普通用户没有编辑权限,这里需要写死管理员的unionId

public void moveFile(@RequestParam(value = "file_id", required = false) String fileId) throws Exception { String accessToken = accessToken; com.aliyun.dingtalkstorage_1_0.Client client = createClient(); MoveDentryHeaders moveDentryHeaders = new MoveDentryHeaders(); moveDentryHeaders.xAcsDingtalkAccessToken = accessToken; MoveDentryRequest.MoveDentryRequestOption option = new MoveDentryRequest.MoveDentryRequestOption() .setConflictStrategy("AUTO_RENAME") // 文件和文件夹的名称冲突策略 .setPresevePermissions(true); // 移动后,是否保留权限,默认否 MoveDentryRequest moveDentryRequest = new MoveDentryRequest() .setUnionId("1234567") // 操作人的钉钉unionId .setTargetSpaceId("目标空间ID") // 目标空间ID .setTargetFolderId("0") // 目标空间文件夹ID,根目录传0 .setOption(option); try { MoveDentryResponse moveDentryResponse = client.moveDentryWithOptions("原空间ID", fileId, moveDentryRequest, moveDentryHeaders, new RuntimeOptions()); System.out.println(moveDentryResponse.getBody().getAsync()); } 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 属性,可帮助开发定位问题 } } } 七、总结

        至此顺利完成功能,虽然其中的原理不难,都是调用钉钉现成的API,但当第三方无法实现自身的需求时,变通思路是非常有必要的,特此记录作为学习笔记。如大家有更好的解决方式望留言分享。



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3