WPS本地镜像化在线文档操作以及样例

一个客户项目有引进在线文档操作需求,让我这边做一个demo调研下,给我的对接文档里有相关方法的说明,照着对接即可。但在真正对接过程中还是踩过不少坑,这儿对之前的对接工作做个记录。

按照习惯先来一个效果:

Demo下载链接:https://download.csdn.net/download/qxyywy/88117444

接入指引

  1. 申请授权证书并进行授权。

  2. 登录系统后台页面

  3. 创建、获取应用信息 access_key (简称ak)、 secret_key (简称 sk )。开发者在接口调用时,使用ak 、 sk 生成WPS-4签名进行鉴权。

  4. 在线编辑、在线预览、格式转换接入按照对应开放能力文档接入。在线编辑、在线预览对接过程中需要设置回调地址。其中,在线编辑可通过配置开启历史版本功能。

  5. 使用过程中需通过证书查询接口关注授权证书状态,若证书即将过期或者不可用,需进行更新证书操作。

  6. 在线编辑或在线预览服务端对接完毕,对接方可使用JSSDK,调用API实现相关需求。

WPS-4签名算法

在对接时,耗费了一定时间在WPS-4签名处,对接文档中有WPS-4的说明和样例,自己在对接转换成NetCore的时候踩了一些坑,签名算法中最主要是:Wps-Docs-Authorization的计算方法

签名格式:WPS-4 {accessKey}:{signature} 注意WPS-4后面有空格。

signature:hmac-sha256(secret_key, Ver + HttpMethod + URI + Content-Type + WpsDocs-Date + sha256(HttpBody))

signature的样例如下:WPS-4POST/callback/path/demoapplication/jsonWed, 20 Apr 2022 01:33:07GMTfc005f51a6e75586d2d5d078b657dxxxdf9c1dfa6a7c0c0ba38c715daeb6ede9

这是文档中对签名算法的解释,对照着格式完成相关算法,具体算法如下:
signature的组装:

cs 复制代码
        /// <summary>
        /// 获取签名
        /// </summary>
        /// <param name="method">请求方法,如:GET,POST</param>
        /// <param name="uri">请求url,带querystring</param>
        /// <param name="body">请求body</param>
        /// <param name="date">日期</param>
        /// <param name="contentType">默认:application/json</param>
        /// <param name="secretKey">应用SK</param>
        /// <returns></returns>
        public static string WPS4Signature(string secretKey,string method,string uri, byte[] body=null,DateTime? date=null,string contentType= "application/json")
        {
            //获取uri路径
            string path = uri;
            //日期格式化
            if (date == null)
                date = DateTime.Now;
            string dateStr = String.Format("{0:r}", date);
            //open不参与签名,做替换处理
            if (path.StartsWith("/open"))
            {
                path = path.Replace("/open", "");
            }

            string sha256body;
            //body为空则为空,否则返回sha256(body)
            if (body != null && body.Length > 0)
            {
                sha256body = Sha256(body);
            }
            else
            {
                sha256body = "";
            }
            String signature = null;
            signature = HmacSHA256Encrypt($"WPS-4{method.ToUpper()}{path}{contentType}{dateStr}{sha256body}", secretKey);

            return signature;
        }

HmacSHA256加密算法:

cs 复制代码
        /// <summary>
        /// HmacSHA256加密
        /// </summary>
        /// <param name="secret"></param>
        /// <param name="signKey"></param>
        /// <returns></returns>
        public static string HmacSHA256Encrypt(string secret, string signKey)
        {
            string signRet = string.Empty;
            using (HMACSHA256 mac = new HMACSHA256(Encoding.UTF8.GetBytes(signKey)))
            {
                byte[] hash = mac.ComputeHash(Encoding.UTF8.GetBytes(secret));
                //signRet = Convert.ToBase64String(hash);
                signRet = ToHexStrFromByte(hash); 
            }
            return signRet;
        }

Sha256转换:

cs 复制代码
        /// <summary>
        /// Sha256转换
        /// </summary>
        /// <param name="input">The input.</param>
        /// <returns>A hash.</returns>
        public static string Sha256(this byte[] input)
        {
            if (input == null)
            {
                return null;
            }
            using (var sha = SHA256.Create())
            {
                var hash = sha.ComputeHash(input);
                return ToHexStrFromByte(hash);
            }
        }

字节数组转16进制字符串:

cs 复制代码
        /// <summary>
        /// 字节数组转16进制字符串:空格分隔
        /// </summary>
        /// <param name="byteDatas"></param>
        /// <returns></returns>
        public static string ToHexStrFromByte(this byte[] byteDatas)
        {
            StringBuilder builder = new StringBuilder();
            for (int i = 0; i < byteDatas.Length; i++)
            {
                builder.Append(string.Format("{0:X2}", byteDatas[i]));
            }
            return builder.ToString().Trim().ToLower();
        }

获取在线预览链接

cs 复制代码
        /// <summary>
        /// 获取在线预览链接
        /// </summary>
        /// <param name="request"></param>
        /// <returns></returns>
        [Route("api/wps/previewgenarate")]
        [HttpPost]
        public Task<GenarateResult> GenarateWPSPreviewUrl(GenarateRequest request)
        {
            return Task.Run(() =>
            {
                string wpsHost = "http://10.4.**.**";
                string uri = $"/api/preview/v1/files/{defaultFileId}/link?type=w&preview_mode=high_definition";
                string fullUrl = $"{wpsHost}/open{uri}";
                Dictionary<string, string> headers = new Dictionary<string, string>();
                DateTime now = DateTime.Now;
                headers.Add("Content-Type", "application/json");
                headers.Add("Wps-Docs-Date", String.Format("{0:r}", now));
                var signature = WPSLocalSIgnatureHelper.WPS4Signature("SKrpaxjdwoetijjv", "get", uri, null, now);
                string docsAuthorization = WPSLocalSIgnatureHelper.WPS4SignAuthorization("UOMYPEVAHWQLTKJF", signature);
                headers.Add("Wps-Docs-Authorization", docsAuthorization);
                HttpHelper httpHelper = new HttpHelper();
                var resultTemp = httpHelper.Get(fullUrl, headers);
                var result = JsonConvert.DeserializeObject<ResponseBaseModel<OnlineEditResultModel>>(resultTemp);
                string url = "";
                if (result != null && result.data != null)
                {
                    url = result.data.link;
                }
                return new GenarateResult { Url = url };
            });
        }

这儿比较坑的地方来了,方法参数都组装好了,通过HttpWebRequest后端发起请求获取WPS中台返回的在线预览地址时,始终提示401报错获取不到数据。迫不得已自己通过APIPost组装了相关请求和参数居然又能获取到相关数据。对照了下请求头,又差异后调整了程序的请求头,保证和apiPost里的完全一致,依然返回401,通过查阅相关资料找到一个处理办法,在发起的请求后手动捕获WebException,然后在WebException里解析数据流获取信息。
HttpWebRequest Get方式访问

cs 复制代码
        /// <summary>
        ///  Get方式访问
        /// </summary>
        /// <param name="url"></param>
        /// <param name="encode"></param>
        /// <param name="referer"></param>
        /// <param name="headers"></param>
        /// <returns></returns>
        public string Get(string url, string encode, string referer, Dictionary<string, string> headers=null)
        {
            int num = _tryTimes;
            HttpWebRequest request = null;
            HttpWebResponse response = null;
            StreamReader reader = null;
            while (num-- >= 0)
            {
                try
                {
                    DelaySomeTime();
                    ServicePointManager.ServerCertificateValidationCallback = new System.Net.Security.RemoteCertificateValidationCallback(CheckValidationResult);//验证服务器证书回调自动验证
                    request = (HttpWebRequest)WebRequest.Create(url);
                    request.Headers.Add("accept", "*/*");
                    request.Headers.Add("accept-encoding", "gzip, deflate, br");
                    request.Headers.Add("accept-language", "zh-CN");
                    request.Headers.Add("connection", "keep-alive");
                    
                    if (headers != null) 
                    {
                        foreach (var item in headers)
                        {
                            request.Headers.Add(item.Key, item.Value);
                        }
                    }
                    //request.UserAgent = reqUserAgent;
                    request.CookieContainer = _cookie;
                    request.Referer = referer;
                    request.Method = "GET";
                    request.Timeout = _timeOut;
                    if (_proxy != null && _proxy.Credentials != null)
                    {
                        request.UseDefaultCredentials = true;
                    }
                    request.Proxy = _proxy;
                    response = (HttpWebResponse)request.GetResponse();
                    reader = new StreamReader(response.GetResponseStream(), Encoding.GetEncoding(encode));
                    return reader.ReadToEnd();
                }
                catch (WebException ex)
                {
                    response = (HttpWebResponse)ex.Response; //解析401等错误返回的有效信息
                    var resultTemp = "";
                    Stream stream = response.GetResponseStream();
                    using (StreamReader readers = new StreamReader(stream, Encoding.UTF8))
                    {
                        resultTemp = readers.ReadToEnd();
                    }
                    return resultTemp;
                }
                catch (Exception ex)
                {
                    _logger.Error(url + "\r\n" + ex.ToString());
                    continue;
                }
                finally
                {
                    if (request != null)
                    {
                        request.Abort();
                    }
                    if (response != null)
                    {
                        response.Close();
                    }
                    if (reader != null)
                    {
                        reader.Close();
                    }
                }
            }
            return string.Empty;
        }

DemoHtml

到上面时后端的处理就基本完成,前端这边因为时临时demo就用了一个html来处理,真实项目中需要用VUE等处理也都类似。

以下为 html的代码:

html 复制代码
<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8" />
        <!-- 建议禁用外框浏览器自带的缩放 -->
        <meta
            name="viewport"
            content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0,user-scalable=no"
        />
        <meta http-equiv="X-UA-Compatible" content="ie=edge" />
        <title>WPS Web Office(iframe)接入指南</title>
        <style>
            * {
                box-sizing: border-box;
            }

            html,
            body {
                display: flex;
                flex-direction: column;
                padding: 0;
                margin: 0;
                height: 100%;
                /* 防止双击缩放 */
                touch-action: manipulation;
            }

            iframe {
                flex: 1;
            }
        </style>
        <!-- cdn引入JQ -->
        <script src="https://ajax.aspnetcdn.com/ajax/jQuery/jquery-3.4.1.min.js"></script>
        <script src="./jwps.js"></script>
        <script type="text/javascript">
            var appHost = "http://10.4.146.19:8890";
            // 支持 HTTPS
            // 注意:如果通过postMessage来设置token,请在url参数加上_w_tokentype=1
            function showWPS(url) {
                // 初始化
                var wps = WPS.config({
                    mount: document.querySelector("#wpsPanl"),
                    // 文字
                    wpsUrl: url,
                    headers: {
                        shareBtn: {
                            tooltip: "分享",
                            subscribe: function() {
                                console.log("click callback");
                            }
                        },
                        otherMenuBtn: {
                            tooltip: "其他按钮",
                            items: [
                                {
                                    // 自定义, type 固定填 'custom'
                                    type: "custom",
                                    icon:
                                        "http://ep.wps.cn/index/images/logo_white2.png",
                                    text: "API 导出 PDF",
                                    subscribe: function(wps) {
                                        if (wps.WpsApplication) {
                                            wps.WpsApplication()
                                                .ActiveDocument.ExportAsFixedFormatAsync()
                                                .then(function(result) {
                                                    console.table(result);
                                                });
                                        }
                                    }
                                },
                                {
                                    // 自定义, type 固定填 'custom'
                                    type: "custom",
                                    icon:
                                        "http://ep.wps.cn/index/images/logo_white2.png",
                                    text: "API 使用",
                                    subscribe: function(wps) {
                                        let result;
                                        if (wps.WpsApplication) {
                                            wps.WpsApplication()
                                                .ActiveDocument.ExportAsFixedFormatAsync()
                                                .then(function(result) {
                                                    console.table(result);
                                                });
                                        }
                                    }
                                }
                            ]
                        }
                    }
                });
                return wps;
            }

            window.onload = function() {
                 $.ajax({
                    url: appHost+"/api/wps/previewgenarate",
                    contentType: "application/json",
                    dataType: "json",
                    data: JSON.stringify({
                        fileId: 'a123',
                        fileName: "test.docx",
                        fileType: 1,
                        userId: 1505340867
                    }),
                    type: "post",
                    success: function(res) {
                        var wpsUrl = res.url;
                        console.log(wpsUrl);
                        var wps = showWPS(wpsUrl);
                    }
                 });

                var fileInput = document.getElementById("bookimg1");
                //选择文件
                fileInput.addEventListener('change', function () {
                    //如果未传入文件则中断
                    if (fileInput.files[0] == undefined) {
                        return;
                    }

                    var file = fileInput.files[0];

                    //FileReader可直接将上传文件转化为二进制流
                    var reader = new FileReader();
                    reader.readAsDataURL(file);//转化二进制流,异步方法
                    reader.onload = function (result) {//完成后this.result为二进制流
                        var base64Str = this.result;
                        $('#uploadImg').attr('src', base64Str);
                        $('#imgPreview').show();
                    } 
                })
            };
            function replaceWps() {
                $('.tdname1').html($('#name1').val());
                $('.tddept1').html($('#dept1').val());
                $('.tdage1').html($('#age1').val());
                $('.tdname2').html($('#name2').val());
                $('.tddept2').html($('#dept2').val());
                $('.tdage2').html($('#age2').val());
                var fileBytes = "";
                var fileName = "";
                if ($('#bookimg1')[0].files[0] != undefined) {
                    var imgFile = $('#bookimg1')[0].files[0];
                    fileName = imgFile.name;
                    //FileReader可直接将上传文件转化为二进制流
                    var reader = new FileReader();
                    reader.readAsDataURL(imgFile);//转化二进制流,异步方法
                    reader.onload = function (result) {//完成后this.result为二进制流
                        var base64Str = this.result;
                        var startNum = base64Str.indexOf("base64,");
                        startNum = startNum * 1 + 7;
                        //去除前部格式信息(如果有需求)
                        var baseStr = base64Str.slice(startNum);
                        fileBytes = baseStr;

                        $.ajax({
                            url: appHost + "/api/wps/wrapheader",
                            contentType: "application/json",
                            dataType: "json",
                            data: JSON.stringify({
                                sample_list: [
                                    {
                                        bookmark: 'bookmark1',
                                        type: 'TEXT',
                                        text: $('#bookmark1').val()
                                    },
                                    {
                                        bookmark: 'bookmark2',
                                        type: 'TEXT',
                                        text: $('#bookmark2').val()
                                    },
                                    {
                                        bookmark: 'bookmark3',
                                        type: 'TEXT',
                                        text: $('#bookmark3').val()
                                    },
                                    {
                                        bookmark: 'bookimg1',
                                        type: 'IMAGE',
                                        /*sample_url:$('#bookimg1').val(),*/
                                        sample_filename: fileName,
                                        text: fileBytes,
                                    },
                                    {
                                        bookmark: 'bookform1',
                                        type: 'TEXT',
                                        text: $('#testForm').prop("outerHTML")
                                    }
                                ],
                            }),
                            type: "post",
                            success: function (res) {
                                var wpsUrl = res.url;;
                                var wps = showWPS(wpsUrl);
                            }
                        });
                    }
                } else {
                    $.ajax({
                        url: "http://10.4.146.19:8890/api/wps/wrapheader",
                        contentType: "application/json",
                        dataType: "json",
                        data: JSON.stringify({
                            sample_list: [
                                {
                                    bookmark: 'bookmark1',
                                    type: 'TEXT',
                                    text: $('#bookmark1').val()
                                },
                                {
                                    bookmark: 'bookmark2',
                                    type: 'TEXT',
                                    text: $('#bookmark2').val()
                                },
                                {
                                    bookmark: 'bookmark3',
                                    type: 'TEXT',
                                    text: $('#bookmark3').val()
                                },
                                {
                                    bookmark: 'bookform1',
                                    type: 'TEXT',
                                    text: $('#testForm').prop("outerHTML")
                                }
                            ],
                        }),
                        type: "post",
                        success: function (res) {
                            var wpsUrl = res.url;;
                            var wps = showWPS(wpsUrl);
                        }
                    });
                }

				
			}
        </script>
    </head>
    <body>
		<div style="width:100%;height:700px;">
			<div id="wpsPanl" style="width:65%;float:left;height:100%;"></div>
            <div id="form" style="width:34%;float:left;height:100%;padding-top:50px;padding-left: 20px;">
                <div><div class="title">课题名称:</div><input class="input" type="text" id="bookmark1" placeholder="请填写课题名称"></div>
                <dl style="clear:both;"></dl>
                <div><div class="title">课题申报单位:</div><input class="input" type="text" id="bookmark2" placeholder="请填写课题申报单位"></div>
                <dl style="clear:both;"></dl>
                <div><div class="title">课题负责人:</div><input class="input" type="text" id="bookmark3" placeholder="请填写课题负责人"></div>
                <dl style="clear:both;"></dl>
                <div style="height:140px">
                    <div class="title">课题成员:</div>
                    <table>
                        <thead><tr><td>姓名</td><td>所属部门</td><td>年龄</td></tr></thead>
                        <tr><td><input class="forminput" type="text" id="name1" placeholder="请填写姓名"></td><td><input class="forminput" type="text" id="dept1" placeholder="请填写所属部门"></td><td><input class="forminput" type="text" id="age1" placeholder="请填写年龄"></td></tr>
                        <tr><td><input class="forminput" type="text" id="name2" placeholder="请填写姓名"></td><td><input class="forminput" type="text" id="dept2" placeholder="请填写所属部门"></td><td><input class="forminput" type="text" id="age2" placeholder="请填写年龄"></td></tr>
                    </table>
                    <div style="width:100%;display:none;">
                        <table border="1" cellspacing="0" style="width:90%;" id="testForm">
                            <thead><tr><td>姓名</td><td>所属部门</td><td>年龄</td></tr></thead>
                            <tr><td class="tdname1"></td><td class="tddept1"></td><td class="tdage1"></td></tr>
                            <tr><td class="tdname2"></td><td class="tddept2"></td><td class="tdage2"></td></tr>
                        </table>
                    </div>
                </div>
                <dl style="clear:both;"></dl>
                <div style="height:20px;margin:0;"><div class="title"></div><span style="color:red">表格替换在第6页</span></div>
                <dl style="clear:both;"></dl>
                <div><div class="title">图片上传:</div><input style="padding:8px 0;" type="file" id="bookimg1"></div>
                <div style="display:none;" id="imgPreview"><img src="" id="uploadImg" width="100" /></div>
                <dl style="clear:both;"></dl>
                <div style="height:20px;margin:0;"><div class="title"></div><span style="color:red">图片替换在第7页</span></div>
                <dl style="clear:both;"></dl>
                <div style="text-align:center;">
                    <input style="
    margin: 0 150px;
    height: 40px;
    background: rgba(2,128,204,1);
    border-radius: 2px;
    display: block;
    width: 80px;
    border: none;
    cursor: pointer;
    font-size: 14px;
    font-weight: 600;
    color: rgba(255,255,255,1);
    " type="button" value="替换" onclick="replaceWps()" id="submit">
                </div>
            </div>
		<div>
	</body>
	<style>
		#wpsPanl iframe{width:99%;height:850px;}
		#form div{
			margin: 10px 0;
			height: 40px;
			color: rgba(51,51,51,1);
			float: left;
		}
		#form div .input{
			height: 35px;
			margin: 2px 0;
			width: 358px;
			border: solid 1px rgba(193,193,193,.35);
		}
        #form div .forminput {
            height: 35px;
            margin: 2px 0;
            width: 94px;
            border: solid 1px rgba(193,193,193,.35);
        }
		#form div .title{width:120px;text-align: right;}
        td {
            border: 1px solid rgba(193,193,193,.35);
            padding: 8px 12px;
        }
        tr {
            background-color: inherit;
            font-size: 14px;
            border-top: 1px solid var(--vp-c-divider);
            transition: background-color .5s;
        }
        table {
            /*width: 100%;*/
            display: table;
            border-collapse: collapse;
            /*margin: 20px 0;*/
            overflow-x: auto;
        }
        table thead {
            font-weight: bold;
        }
	</style>
</html>

个人总结:需要在线编辑等相关操作,其实也可以用免费的组件组合,比如可以用onlyoffice+Aspose.Words去操作,仅个人见解。

相关推荐
Fairy_sevenseven13 小时前
【二十八】【QT开发应用】模拟WPS Tab
开发语言·qt·wps
<s>LoCloudy</s>3 天前
WPS(金山文档)与金蝶云星空通过HTTP实现连接
wps·金蝶
beyond谚语5 天前
WPS在表格中填写材料时,内容过多导致表格不换页,其余内容无法正常显示 以及 内容过多,导致表格换页——解决方法
wps
RZer6 天前
WPS使用越来越卡顿
运维·wps·uos
Tony11548 天前
WPS中让两列数据合并的方法
wps
gan7777778 天前
只装了WPS,DOC文档无法打开
经验分享·wps
醉颜凉8 天前
银河麒麟桌面操作系统如何添加WPS字体
运维·服务器·kylin·wps·麒麟·wps添加字体
薰珞婷紫小亭子9 天前
解决Mac 默认设置 wps不能双面打印的问题
wps
国中之林9 天前
【qt】一个WPS项目了解qt界面设计的基本套路
c++·qt·学习·wps·ui设计
StrokeAce9 天前
linux打开桌面软件(wps)、获取已打开的文件名(wps)
linux·wps·c/c++·slackware