记一次基于云服务开发文档在线编辑系统的开发记录,支持版本记录、可增加批注。

  从工作实习的时候我就参与了一个项目叫做“云文档管理系统”,说白了就是文件的上传、下载、预览、分享、回收站等等一些操作。上传下载以及分享都很Easy,复杂的就在文档预览上,图片、视频、音频都有现成的插件可以使用,Office文档的在线预览相对来说还是比较复杂的,当时也是看好多把Office文档转换成html进行预览的,也有转换成Pdf预览的,即使都实现预览效果又怎样。客户提出一个需求叫做“文档版本修改历史留存、可增加批注”,当时这个需求简直让人头大,我不知道如何下手。我记得我当时的主管对这个需求也是很无助啊,过了几天他就告诉我他找到一个插件,不过只能在IE浏览器上使用Active X控件才能实现,而且调试起来超级麻烦。我记得当时这个功能还是废弃了。

  时隔五年,我偶然间发现了一个文档在线预览的服务,大家可以参考我的另一篇博客《如何实现文档在线预览》,这里我就不再过多赘述了。即使到目前我也只是把文档在线预览功能找到了解决方案,可是文档在线编辑一直是我的一个心结。2021年开年到现在,每天工作都很繁忙,午休的时间累积在一起我写了一个基于云服务的文档在线编辑系统(基础功能基本已经实现),如果有需要的小伙伴可以参照我下面介绍的步骤来体验一下:

开通开发者权限

我们进入云服务官网,申请加入开发者,跟着导航一步一步走就OK了,等待审核通过,你会得到appId和appKey,这俩参数在调用接口时候会用到。

验签方法封装

  验签方法,就是对你调用接口的参数进行签名,被调用方拿到你的参数要进行校验,校验通过才算是有效调用。

/// <summary>
/// 生成验签数据 sign
/// </summary>
public class Signclient
{
    public static string generateSign(string secret, Dictionary<string, string[]> paramMap)
    {
        string fullParamStr = uniqSortParams(paramMap);
        return HmacSHA256(fullParamStr, secret);
    }
    public static string uniqSortParams(Dictionary<string, string[]> paramMap)
    {
        paramMap.Remove("sign");
        paramMap = paramMap.OrderBy(o => o.Key).ToDictionary(o => o.Key.ToString(), p => p.Value);
        StringBuilder strB = new StringBuilder();
        foreach (KeyValuePair<string, string[]> kvp in paramMap)
        {
            string key = kvp.Key;
            string[] value = kvp.Value;
            if (value.Length > 0)
            {
                Array.Sort(value);
                foreach (string temp in value)
                {
                    strB.Append(key).Append("=").Append(temp);
                }
            }
            else
            {
                strB.Append(key).Append("=");
            }
        }
        return strB.ToString();
    }
    public static string HmacSHA256(string data, string key)
    {
        string signRet = string.Empty;
        using (HMACSHA256 mac = new HMACSHA256(Encoding.UTF8.GetBytes(key)))
        {
            byte[] hash = mac.ComputeHash(Encoding.UTF8.GetBytes(data));
            signRet = ToHexString(hash); ;
        }
        return signRet;
    }
    public static string ToHexString(byte[] bytes)
    {
        string hexString = string.Empty;
        if (bytes != null)
        {
            StringBuilder strB = new StringBuilder();
            foreach (byte b in bytes)
            {
                strB.AppendFormat("{0:X2}", b);
            }
            hexString = strB.ToString();
        }
        return hexString;
    }
}

记住,这个sign很重要,因为我刚开始把appKey当做sign传入参数进行调用,总是报错,后来才知道是我签名传了个寂寞。

 

接口调用

  准备工作已经准备完毕了,下面就要开始接口调用了,API提供了新建文档、本地文档上传、文件删除、文件版本删除等等,我这里不一一调用了,只做了几个我项目中用到的来罗列一下,我界面做的比较丑,凑合看。。

 本地文档上传,文档上传成功之后返回的结果如下,包含第一个文件版本Id和文件的Id,这样我的文档就上传到云服务了,我们拿着文件版本ID,就可以进行在线编辑了。

我们拿过来刚才的文件版本ID,进行在线编辑功能测试,我这里直接做了个跳转,跳转到的页面就是在线编辑页面,如果你在Postman调用的话,会得到一大串HTML代码,就算你粘贴过来,也是缺少css和js的,因为打开的方式就不对。我们来看一下效果:

 在线编辑效果:整体效果非常好,而且可以进行批注

回调函数

  在线编辑是可以了,但是还没完。因为你用之前的版本ID再次打开会发现,什么也没更改,这是为什么呢?因为我们修改的内容已经作为新版本进行保存了,因为我是在本机进行测试,没有发布到服务器,所以我也不知道保存后的文档版本ID是多少,我根据文件版本名字发现了规律,那就是从0开始依次累加,那我直接在文件ID后加下划线 _1进行测试,果然打开了我上次修改并保存的那个文档。于是我又进入API文档发现,这个在线编辑是实时本地保存的,一旦你离开在线编辑,它就会回调给你的接口,这里我们先配置一下接口:

这里乍一看是个外网地址,其实是我映射内网的地址,我搭建了内网映射服务,这样我就可以在外网调试的时候,映射到我本机电脑进行调试了,于是我编辑完文档,并返回,这是回调地址就起了作用了,值得注意的是,路由地址要按照接口给出的3rd/edit/callBack进行配置,否则你的接口接收不到任何东西。

 

 好了,我们接收到了云服务给我们回调的数据,这样我们就可以根据这些数据进行数据操作了。

能力有限,只会C#这一编程语言,仅供参考。

namespace WebApplication.Controllers
{
    /// <summary>
    /// 基于WebUploader插件的图片上传实例
    /// </summary>
    public class UploadController : Controller
    {
        public static readonly string appId = "yozojqut3Leq7916";
        public static readonly string appKey = "5f83670ada246fc8e0d1********";

        #region 文件上传
        /// <summary>
        /// 文件上传
        /// </summary>
        /// <returns></returns>
        public ActionResult FileUpload()
        {
            return View();
        }
        /// <summary>
        /// 上传文件方法
        /// </summary>
        /// <param name="form"></param>
        /// <param name="file"></param>
        /// <returns></returns>
        [HttpPost]
        public ActionResult UploadFile(FormCollection form, HttpPostedFileBase file)
        {
            Dictionary<string, string[]> dic = new Dictionary<string, string[]>();
            dic.Add("appId", new string[] { appId });
            string sign = Signclient.generateSign(appKey, dic);
            try
            {
                if (Request.Files.Count == 0)
                {
                    throw new Exception("请选择上传文件!");
                }
                using (HttpClient client = new HttpClient())
                {
                    var postContent = new MultipartFormDataContent();
                    HttpContent fileStreamContent = new StreamContent(file.InputStream);
                    postContent.Add(fileStreamContent, "file", file.FileName);
                    var requestUri = "http://dmc.yozocloud.cn/api/file/upload?appId=" + appId + "&sign=" + sign + "";
                    var response = client.PostAsync(requestUri, postContent).Result;
                    Task<string> t = response.Content.ReadAsStringAsync();
                    return Json(new
                    {
                        Status = response.StatusCode.GetHashCode(),
                        Message = response.StatusCode.GetHashCode() == 200 ? "上传文件成功" : "上传文件失败",
                        Data = t.Result
                    });
                }
            }
            catch (Exception ex)
            {
                //扔出异常
                throw;
            }
        }
        #endregion
        #region 文件删除
        /// <summary>
        /// 删除文件
        /// </summary>
        /// <returns></returns>
        public ActionResult DelFile()
        {
            return View();
        }
        /// <summary>
        /// 删除文件版本
        /// </summary>
        /// <returns></returns>
        public ActionResult DelFileVersion()
        {
            return View();
        }

        [HttpGet]
        public ActionResult FileDelete(string fileId)
        {
            Dictionary<string, string[]> dic = new Dictionary<string, string[]>();
            dic.Add("fileId", new string[] { fileId });
            dic.Add("appId", new string[] { appId });
            string sign = Signclient.generateSign(appKey, dic);
            using (HttpClient client = new HttpClient())
            {
                var requestUri = "http://dmc.yozocloud.cn/api/file/delete/file?fileId=" + fileId + "&appId=" + appId + "&sign=" + sign + "";
                var response = client.GetAsync(requestUri).Result;
                Task<string> t = response.Content.ReadAsStringAsync();
                return Json(new
                {
                    Status = response.StatusCode.GetHashCode(),
                    Message = response.StatusCode.GetHashCode() == 200 ? "请求成功" : "请求失败",
                    Data = t.Result
                },JsonRequestBehavior.AllowGet);
            }
        }
        [HttpGet]
        public ActionResult FileVersionDelete(string fileVersionId)
        {
            Dictionary<string, string[]> dic = new Dictionary<string, string[]>();
            dic.Add("fileVersionId", new string[] { fileVersionId });
            dic.Add("appId", new string[] { appId });
            string sign = Signclient.generateSign(appKey, dic);
            using (HttpClient client = new HttpClient())
            {
                var requestUri = "http://dmc.yozocloud.cn/api/file/delete/version?fileVersionId=" + fileVersionId + "&appId=" + appId + "&sign=" + sign + "";
                var response = client.GetAsync(requestUri).Result;
                Task<string> t = response.Content.ReadAsStringAsync();
                return Json(new
                {
                    Status = response.StatusCode.GetHashCode(),
                    Message = response.StatusCode.GetHashCode() == 200 ? "请求成功" : "请求失败",
                    Data = t.Result
                }, JsonRequestBehavior.AllowGet);
            }
        }
        #endregion
        #region 新建文档
        /// <summary>
        /// 文档类型,文件名
        /// </summary>
        /// <param name="templateType"></param>
        /// <param name="fileName"></param>
        /// <returns></returns>
        public ActionResult NewDoc(string templateType, string fileName)
        {
            Dictionary<string, string[]> dic = new Dictionary<string, string[]>();
            dic.Add("templateType", new string[] { templateType });
            dic.Add("fileName", new string[] { fileName });
            dic.Add("appId", new string[] { appId });
            string sign = Signclient.generateSign(appKey, dic);
            using (HttpClient client = new HttpClient())
            {
                var requestUri = "http://dmc.yozocloud.cn/api/file/template?templateType=" + templateType + "&fileName=" + fileName + "&appId=" + appId + "&sign=" + sign + "";
                var response = client.GetAsync(requestUri).Result;
                Task<string> t = response.Content.ReadAsStringAsync();
                return Json(new
                {
                    Status = response.StatusCode.GetHashCode(),
                    Message = response.StatusCode.GetHashCode() == 200 ? "删除文件版本成功" : "删除文件版本失败",
                    Data = t.Result
                });
            }
        }
        #endregion
        /// <summary>
        /// 在线编辑
        /// </summary>
        /// <returns></returns>
        public ActionResult FileEdit()
        {
            return View();
        }
        [HttpGet]
        public ActionResult GetFileEdit(string fileversionId)
        {
            Dictionary<string, string[]> dic = new Dictionary<string, string[]>();
            dic.Add("fileVersionId", new string[] { fileversionId });
            dic.Add("appId", new string[] { appId });
            string sign = Signclient.generateSign(appKey, dic);
            string ret = "http://eic.yozocloud.cn/api/edit/file?fileVersionId=" + fileversionId + "&appId=" + appId + "&sign=" + sign + "";
            return Redirect(ret);
        }
        [HttpPost]
        [Route("3rd/edit/callBack")]
        public ActionResult EditCallBack(string oldFileId, string newFileId, string message, int errorCode)
        {
            //文件ID
            //575716913322135553
            //文件版本 依次累加 0 1 2 3 4
            //575716913322135553_0 、 7
            return Json(new
            {
                oldFileId = oldFileId,
                newFileId = newFileId,
                message = message,
                errorCode = errorCode
            });
        }
    }
}

有兴趣的同志可以一起交流。

Github已开源此Demo

posted @ 2021-03-08 17:31  土伦  阅读(609)  评论(0编辑  收藏  举报