記一次基於雲服務開發文檔在線編輯系統的開發記錄,支持版本記錄、可增加批註。
- 2021 年 3 月 8 日
- 筆記
從工作實習的時候我就參與了一個項目叫做「雲文檔管理系統」,說白了就是文件的上傳、下載、預覽、分享、回收站等等一些操作。上傳下載以及分享都很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 = "//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 = "//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 = "//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 = "//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 = "//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 }); } } }
有興趣的同志可以一起交流。