记一次基于云服务开发文档在线编辑系统的开发记录,支持版本记录、可增加批注。
从工作实习的时候我就参与了一个项目叫做“云文档管理系统”,说白了就是文件的上传、下载、预览、分享、回收站等等一些操作。上传下载以及分享都很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 }); } } }
有兴趣的同志可以一起交流。