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

土倫發表於2021-03-08

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

相關文章