­

記一次基於雲服務開發文檔在線編輯系統的開發記錄,支持版本記錄、可增加批註。

  從工作實習的時候我就參與了一個項目叫做「雲文檔管理系統」,說白了就是文件的上傳、下載、預覽、分享、回收站等等一些操作。上傳下載以及分享都很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
            });
        }
    }
}

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

Github已開源此Demo