一:概述

onlyoffice是做什么用的在这里就不做解释了,能看到这篇博客的我相信都是了解的,本文主要讲解onlyoffice在使用过程中要注意的问题。

使用环境:window10 + VMware Workstation Pro14 + CentOS-7-x86_64-Minimal + Docker 18.03.1-ce + onlyoffice

onlyoffice官网:https://www.onlyoffice.com/
onlyoffice API: https://api.onlyoffice.com/editors/basic

onlyoffice API要着重看下,里面阐述了onlyoffice的运行原理,同时里面有基于多种语言的样例代码,基本理解onlyoffice的运行原理后再结合样例代码验证必能事半功倍。在这首先阐述下API中设计到的几个概念:
1,document manager : 文档管理器,等同于一个界面中的文件列表,该列表就是文档管理器【我们自己编写,不一定需要】。
2,document storage service :文档存储服务,即管理文档存放的模块,很多时候就是我们上传文件然后将其保存在服务器上,网上也有使用nextcloud作为存储的【我们自己编写或使用存储文件的软件】。
3,document editor : 文档编辑器,就是文档编辑窗口【onlyoffice提供的前端页面插件】
4,document editing service : 文档编辑服务,从文档存储服务获取要编辑的文档,转换成Office OpenXML格式后传给文档编辑器,编辑期间文档编辑器与文档编辑服务长期交互【onlyoffice提供的后台服务】

二:实践(基于样例代码)
首先从官网下载基于java开发的样例代码“Java Example.zip”,该代码是使用maven构建的webapp,在webapp目录下有两个文件“index.jsp”和"editor.jsp"; 同时在resources目录下有个setting.properties配置文件,这三个文件是我们首先要着重看的地方。

如果index.jsp能顺利运行起来,那界面应该是这样的:
OnlyOffice基础实践

首先我们可以点击“Choose file”按钮上传一个要编辑的文件,这时会调用 controllers.IndexServlet中的doPost方法。其处理方法为:

protected void proce***equest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException{String action = request.getParameter("type");if(action == null)  {request.getRequestDispatcher("index.jsp").forward(request, response);return;}DocumentManager.Init(request, response);PrintWriter writer = response.getWriter();switch (action.toLowerCase()){case "upload":Upload(request, response, writer);break;case "convert":Convert(request, response, writer);break;case "track":Track(request, response, writer);break;}}
private static void Upload(HttpServletRequest request, HttpServletResponse response, PrintWriter writer){response.setContentType("text/plain");try{Part httpPostedFile = request.getPart("file");String fileName = "";for (String content : httpPostedFile.getHeader("content-disposition").split(";")){if (content.trim().startsWith("filename")){fileName = content.substring(content.indexOf('=') + 1).trim().replace("\"", "");}}long curSize = httpPostedFile.getSize();if (DocumentManager.GetMaxFileSize() < curSize || curSize <= 0){writer.write("{ \"error\": \"File size is incorrect\"}");return;}String curExt = FileUtility.GetFileExtension(fileName);if (!DocumentManager.GetFileExts().contains(curExt)){writer.write("{ \"error\": \"File type is not supported\"}");return;}InputStream fileStream = httpPostedFile.getInputStream();fileName = DocumentManager.GetCorrectName(fileName);//文件保存路径String fileStoragePath = DocumentManager.StoragePath(fileName, null);File file = new File(fileStoragePath);try (FileOutputStream out = new FileOutputStream(file)){int read;final byte[] bytes = new byte[1024];while ((read = fileStream.read(bytes)) != -1){out.write(bytes, 0, read);}out.flush();}writer.write("{ \"filename\": \"" + fileName + "\"}");}catch (IOException | ServletException e){writer.write("{ \"error\": \"" + e.getMessage() + "\"}");}}

深入的不在阐述,经过几番周折后,最终是将文件保存在硬盘上。这个上传操作在实际应用onlyoffice的项目中也应该会有,实现方法和存放的路径根据实际调整即可。

上传过程界面如下:
OnlyOffice基础实践

当点击“Edit”按钮后,将会通过EditorServlet跳转到“editor.jsp页面”,并将需要的参数传递过去。其中关键的代码为

docEditor = new DocsAPI.DocEditor("iframeEditor",{width: "100%",height: "100%",type: "${type}",documentType: "<%= Model.GetDocumentType() %>",document: {title: fileName,url: "<%= Model.GetFileUri() %>",fileType: fileType,key: "<%= Model.GetKey() %>",info: {author: "Me",created: "<%= new SimpleDateFormat("MM/dd/yyyy").format(new Date()) %>",},permissions: {edit: <%= Boolean.toString(DocumentManager.GetEditedExts().contains(FileUtility.GetFileExtension(Model.GetFileName()))).toLowerCase() %>,download: true,}},editorConfig: {mode: "<%= DocumentManager.GetEditedExts().contains(FileUtility.GetFileExtension(Model.GetFileName())) && !"view".equals(request.getAttribute("mode")) ? "edit" : "view" %>",lang: "en",callbackUrl: "<%= Model.GetCallbackUrl() %>",user: {id: "<%= Model.CurUserHostAddress() %>",name: "John Smith",},embedded: {saveUrl: "<%= Model.GetFileUri() %>",embedUrl: "<%= Model.GetFileUri() %>",shareUrl: "<%= Model.GetFileUri() %>",toolbarDocked: "top",},customization: {about: true,feedback: true,goback: {url: "<%= Model.GetServerUrl() %>/IndexServlet",},},},events: {"onReady": onReady,"onDocumentStateChange": onDocumentStateChange,'onRequestEditRights': onRequestEditRights,"onError": onError,"onOutdatedVersion": onOutdatedVersion,}});};

其中 document属性下的url地址 和 editorConfig属性下的callbackUrl地址十分重要,也是在开发中需要重点关注的地方,很多报出的以下错误基本都是地址不对导致的。
OnlyOffice基础实践

重点说说这两个配置的作用
1)document属性下的url配置是onlyoffice的编辑服务用于获取文档的地址,也就是说,我们必须保证在docker中是必须能访问到的地址, 通过wget命令尝试在docker所在的服务器中是否能够访问。

OnlyOffice基础实践

2) editorConfig属性下的callbackUrl配置是onlyoffice的编辑服务回调的,该回调的作用是告知你编辑后文档的下载地址,以便更新原始文件,所以我们也得保证docker能够访问到该地址。我们可以自己编写个用于回调的方法,例如:

public void saveFile(HttpServletRequest request, HttpServletResponse response) {PrintWriter writer = null;System.out.println("===saveeditedfile------------") ;try {writer = response.getWriter();Scanner scanner = new Scanner(request.getInputStream()).useDelimiter("\\A");String body = scanner.hasNext() ? scanner.next() : "";JSONObject jsonObj = (JSONObject) new JSONParser().parse(body);System.out.println("===saveeditedfile:" + jsonObj.get("status")) ;/*0 - no document with the key identifier could be found,1 - document is being edited,2 - document is ready for saving,3 - document saving error has occurred,4 - document is closed with no changes,6 - document is being edited, but the current document state is saved,7 - error has occurred while force saving the document.* */if ((long) jsonObj.get("status") == 2) {/** 当我们关闭编辑窗口后,十秒钟左右onlyoffice会将它存储的我们的编辑后的文件,,此时status = 2,通过request发给我们,我们需要做的就是接收到文件然后回写该文件。* *//** 定义要与文档存储服务保存的编辑文档的链接。当状态值仅等于2或3时,存在链路。* */String downloadUri = (String) jsonObj.get("url");System.out.println("====文档编辑完成,现在开始保存编辑后的文档,其下载地址为:" + downloadUri);//解析得出文件名String fileName = downloadUri.substring(downloadUri.lastIndexOf('/')+1);System.out.println("====下载的文件名:" + fileName);URL url = new URL(downloadUri);java.net.HttpURLConnection connection = (java.net.HttpURLConnection) url.openConnection();InputStream stream = connection.getInputStream();//更换为实际的路径File savedFile = new File("e:\\");try (FileOutputStream out = new FileOutputStream(savedFile)) {int read;final byte[] bytes = new byte[1024];while ((read = stream.read(bytes)) != -1) {out.write(bytes, 0, read);}out.flush();}connection.disconnect();}} catch (IOException e) {// TODO Auto-generated catch blocke.printStackTrace();} catch (ParseException e) {// TODO Auto-generated catch blocke.printStackTrace();}/** status = 1,我们给onlyoffice的服务返回{"error":"0"}的信息,这样onlyoffice会认为回调接口是没问题的,这样就可以在线编辑文档了,否则的话会弹出窗口说明* */writer.write("{\"error\":0}");}

接下来再说说setting.properties配置文件,里面的配置内容如下:

filesize-max=5242880
storage-folder=app_datafiles.docservice.viewed-docs=.pdf|.djvu|.xps
files.docservice.edited-docs=.docx|.xlsx|.csv|.pptx|.ppsx|.txt
files.docservice.convert-docs=.docm|.dotx|.dotm|.dot|.doc|.odt|.fodt|.xlsm|.xltx|.xltm|.xlt|.xls|.ods|.fods|.pptm|.ppt|.ppsm|.pps|.potx|.potm|.pot|.odp|.fodp|.rtf|.mht|.html|.htm|.epub
files.docservice.timeout=120000files.docservice.url.converter=http://192.168.10.129/ConvertService.ashx
files.docservice.url.tempstorage=http://192.168.10.129/ResourceService.ashx
files.docservice.url.api=http://192.168.10.129/web-apps/apps/api/documents/api.js
files.docservice.url.preloader=http://192.168.10.129/web-apps/apps/api/documents/cache-scripts.html

其中的192.168.10.29是访问onlyoffice document server的地址,如果访问到,其结果应该如下:
OnlyOffice基础实践

以上就是使用onlyoffice的两个关键点,文档下载地址和onlyoffice回调地址

三:实践(基于Springboot)
1,首先配置文件存放路径和对应暴露的访问地址

@Configuration
@EnableWebMvc
public class MvcConfig implements WebMvcConfigurer {@Overridepublic void addResourceHandlers(ResourceHandlerRegistry registry) {// addResourceLocations指的是文件放置的目录,addResourceHandler指的是对外暴露的访问路径registry.addResourceHandler("/assets/**").addResourceLocations("classpath:/static/");registry.addResourceHandler("/file/").addResourceLocations("file:D:/uploadfile/");registry.addResourceHandler("swagger-ui.html").addResourceLocations("classpath:/META-INF/resources/");registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/");}/*** 统一处理没啥业务逻辑处理的controller请求,实现代码的简洁* * @param registry*/@Overridepublic void addViewControllers(ViewControllerRegistry registry) {registry.addViewController("/").setViewName("index");registry.addViewController("/index").setViewName("index");}/*** SpringMVC的路径参数如果带“.”的话,“.”后面的值将被忽略 .../pathvar/xx.yy 解析得到:xx*/@Overridepublic void configurePathMatch(PathMatchConfigurer configurer) {// 通过设置为false使其可以接受"."后的但是configurer.setUseSuffixPatternMatch(false);}
}

此处我们将上传文件保存在了“D:/uploadfile/”目录下,该地址最好是在配置文件中配置,例如上面的setting.properties文件中。

2,创建用于编辑文档的Controller

@Controller
public class EditorController {@RequestMapping("/EditorServlet")public ModelAndView index(HttpServletRequest request,HttpServletResponse response,Model model,ModelMap modelMap) throws Exception {String fileName = "";if (request.getParameterMap().containsKey("fileName")) {fileName = request.getParameter("fileName");}String fileExt = null;if (request.getParameterMap().containsKey("fileExt")) {fileExt = request.getParameter("fileExt");}if (fileExt != null) {try {DocumentManager.Init(request, response);fileName = DocumentManager.CreateDemo(fileExt);} catch (Exception ex) {return new ModelAndView(new FastJsonJsonView(),"Error: " + ex.getMessage(), ex) ;}}String mode = "";if (request.getParameterMap().containsKey("mode")){mode = request.getParameter("mode");}Boolean desktopMode = !"embedded".equals(mode);FileModel file = new FileModel();file.SetTypeDesktop(desktopMode);file.SetFileName(fileName);System.out.println("==========EditorController==========");DocumentManager.Init(request, response);//要编辑的文件名model.addAttribute("fileName", fileName) ;//要编辑的文件类型model.addAttribute("fileType", FileUtility.GetFileExtension(fileName).replace(".", "")) ;//要编辑的文档类型model.addAttribute("documentType",FileUtility.GetFileType(fileName).toString().toLowerCase()) ;//要编辑的文档访问urlmodel.addAttribute("fileUri",DocumentManager.GetFileUri(fileName)) ;model.addAttribute("fileKey",ServiceConverter.GenerateRevisionId(DocumentManager.CurUserHostAddress(null) + "/" + fileName)) ;model.addAttribute("callbackUrl", DocumentManager.GetCallback(fileName)) ;model.addAttribute("serverUrl", DocumentManager.GetServerUrl()) ;model.addAttribute("editorMode", DocumentManager.GetEditedExts().contains(FileUtility.GetFileExtension(fileName)) && !"view".equals(request.getAttribute("mode")) ? "edit" : "view") ;model.addAttribute("editorUserId",DocumentManager.CurUserHostAddress(null)) ;model.addAttribute("type", desktopMode ? "desktop" : "embedded");model.addAttribute("docserviceApiUrl", ConfigManager.GetProperty("files.docservice.url.api"));model.addAttribute("docServiceUrlPreloader", ConfigManager.GetProperty("files.docservice.url.preloader")) ;model.addAttribute("currentYear", "2018") ;model.addAttribute("convertExts", String.join(",", DocumentManager.GetConvertExts())) ;model.addAttribute("editedExts", String.join(",", DocumentManager.GetEditedExts())) ;model.addAttribute("documentCreated", new SimpleDateFormat("MM/dd/yyyy").format(new Date())) ;model.addAttribute("permissionsEdit", Boolean.toString(DocumentManager.GetEditedExts().contains(FileUtility.GetFileExtension(fileName))).toLowerCase()) ;return new ModelAndView("editor") ;}
}

3,创建用于保存修改后文件的Controller

/** 用于保存修改后的文件* */
@Controller
@RequestMapping("/savefilectrl")
public class SaveFileController {/*** 文档编辑服务使用JavaScript API通知callbackUrl,向文档存储服务通知文档编辑的状态。文档编辑服务使用具有正文中的信息的POST请求。* https://api.onlyoffice.com/editors/callback* 参数示例:{"actions": [{"type": 0, "userid": "78e1e841"}],"changesurl": "https://documentserver/url-to-changes.zip","history": {"changes": changes,"serverVersion": serverVersion},"key": "Khirz6zTPdfd7","status": 2,"url": "https://documentserver/url-to-edited-document.docx","users": ["6d5a81d0"]}* @throws ParseException */@RequestMapping("/saveeditedfile")public void saveFile(HttpServletRequest request, HttpServletResponse response) {PrintWriter writer = null;System.out.println("===saveeditedfile------------") ;try {writer = response.getWriter();Scanner scanner = new Scanner(request.getInputStream()).useDelimiter("\\A");String body = scanner.hasNext() ? scanner.next() : "";JSONObject jsonObj = (JSONObject) new JSONParser().parse(body);System.out.println("===saveeditedfile:" + jsonObj.get("status")) ;/*0 - no document with the key identifier could be found,1 - document is being edited,2 - document is ready for saving,3 - document saving error has occurred,4 - document is closed with no changes,6 - document is being edited, but the current document state is saved,7 - error has occurred while force saving the document.* */if ((long) jsonObj.get("status") == 2) {/** 当我们关闭编辑窗口后,十秒钟左右onlyoffice会将它存储的我们的编辑后的文件,,此时status = 2,通过request发给我们,我们需要做的就是接收到文件然后回写该文件。* *//** 定义要与文档存储服务保存的编辑文档的链接。当状态值仅等于2或3时,存在链路。* */String downloadUri = (String) jsonObj.get("url");System.out.println("====文档编辑完成,现在开始保存编辑后的文档,其下载地址为:" + downloadUri);//解析得出文件名String fileName = downloadUri.substring(downloadUri.lastIndexOf('/')+1);System.out.println("====下载的文件名:" + fileName);URL url = new URL(downloadUri);java.net.HttpURLConnection connection = (java.net.HttpURLConnection) url.openConnection();InputStream stream = connection.getInputStream();File savedFile = new File("e:\\");try (FileOutputStream out = new FileOutputStream(savedFile)) {int read;final byte[] bytes = new byte[1024];while ((read = stream.read(bytes)) != -1) {out.write(bytes, 0, read);}out.flush();}connection.disconnect();}} catch (IOException e) {// TODO Auto-generated catch blocke.printStackTrace();} catch (ParseException e) {// TODO Auto-generated catch blocke.printStackTrace();}/** status = 1,我们给onlyoffice的服务返回{"error":"0"}的信息,这样onlyoffice会认为回调接口是没问题的,这样就可以在线编辑文档了,否则的话会弹出窗口说明* */writer.write("{\"error\":0}");}
}

4,项目中使用的Freemarker,所以编辑页面修改为

<!DOCTYPE html>
<html><head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8"><title>ONLYOFFICE</title><link rel="icon" href="asstes/favicon.ico" type="image/x-icon" /><link rel="stylesheet" type="text/css" href="assets/css/editor.css" /><script type="text/javascript" src="${docserviceApiUrl}"></script><script type="text/javascript" language="javascript">var docEditor;var fileName = "${fileName}";var fileType = "${fileType}";var innerAlert = function (message) {if (console && console.log)console.log(message);};var onReady = function () {innerAlert("Document editor ready");};var onDocumentStateChange = function (event) {var title = document.title.replace(/\*$/g, "");document.title = title + (event.data ? "*" : "");};var onRequestEditRights = function () {location.href = location.href.replace(RegExp("action=view\&?", "i"), "");};var onError = function (event) {if (event)innerAlert(event.data);};var onOutdatedVersion = function (event) {location.reload(true);};var сonnectEditor = function () {docEditor = new DocsAPI.DocEditor("iframeEditor",{width: "100%",height: "100%",type: "${type}",documentType: "${documentType}",document: {title:"${fileName}", url: "${fileUri}",fileType: "${fileType}",key: "${fileKey}",info: {author: "Me",created: "${documentCreated}",},permissions: {edit: ${permissionsEdit},download: true,}},editorConfig: {mode: "${editorMode}",lang: "en",callbackUrl: "${callbackUrl}",user: {id: "${editorUserId}",name: "John Smith",},embedded: {saveUrl: "${fileUri}",embedUrl: "${fileUri}",shareUrl: "${fileUri}",toolbarDocked: "top",},customization: {about: true,feedback: true,goback: {url: "${serverUrl}/IndexServlet",},},},events: {"onReady": onReady,"onDocumentStateChange": onDocumentStateChange,'onRequestEditRights': onRequestEditRights,"onError": onError,"onOutdatedVersion": onOutdatedVersion,}});};if (window.addEventListener) {window.addEventListener("load", сonnectEditor);} else if (window.attachEvent) {window.attachEvent("load", сonnectEditor);}function getXmlHttp() {var xmlhttp;try {xmlhttp = new ActiveXObject("Msxml2.XMLHTTP");} catch (e) {try {xmlhttp = new ActiveXObject("Microsoft.XMLHTTP");} catch (ex) {xmlhttp = false;}}if (!xmlhttp && typeof XMLHttpRequest !== "undefined") {xmlhttp = new XMLHttpRequest();}return xmlhttp;}</script></head><body><div class="form"><div id="iframeEditor"></div></div></body>
</html>

其Freemarker的配置

#设定ftl文件路径
spring.freemarker.template-loader-path=classpath:/templates/freemarker
spring.freemarker.suffix=.html
spring.freemarker.content-type=text/html
spring.freemarker.templateEncoding=UTF-8

以上就是核心部分的代码,重点是要保证配置准确。

至于docker和onlyoffice安装过程就不赘述了,最后看下docker运行情况
OnlyOffice基础实践

附:onlyoffice安装过程
1,安装docer

yum install docker -y

2,启动docker服务

systemctl start docker

3,拉取onlyoffice

docker pull onlyoffice/documentserver

4,启动Document Server镜像,并映射80端口至本地(前面个80)。

sudo docker run -i -t -d -p 80:80 onlyoffice/documentserver

转载于:https://blog.51cto.com/dengshuangfu/2154826

OnlyOffice基础实践相关推荐

  1. Tensor基础实践

    Tensor基础实践 飞桨(PaddlePaddle,以下简称Paddle)和其他深度学习框架一样,使用Tensor来表示数据,在神经网络中传递的数据均为Tensor. Tensor可以将其理解为多维 ...

  2. 零基础实践深度学习之数学基础

    零基础实践深度学习之数学基础 深度学习常用数学知识 数学基础知识 高等数学 线性代数 行列式 矩阵 向量 线性方程组 矩阵的特征值和特征向量 二次型 概率论和数理统计 随机事件和概率 随机变量及其概率 ...

  3. 零基础实践深度学习之Python基础

    零基础实践深度学习之Python基础 Python数据结构 数字 字符串 列表 元组 字典 Python面向对象 Python JSON Python异常处理 Python文件操作 常见Linux命令 ...

  4. 大学计算机应用技术基础,大学计算机应用技术基础实践教程(Windows7+Office2010/21世纪高等学校计算机规划教材...

    <大学计算机应用技术基础实践教程(Windows7+Office2010/21世纪高等学校计算机规划教材>是<大学计算机应用技术基础教程>的配套实验教材,是对教学内容的必要补充 ...

  5. 20145227鄢曼君《网络对抗》Web安全基础实践

    20145227鄢曼君<网络对抗>Web安全基础实践 实验后回答问题 1.SQL注入攻击原理,如何防御? SQL注入攻击指的是通过构建特殊的输入作为参数传入Web应用程序,而这些输入大都是 ...

  6. 20155202《网络对抗》Exp9 web安全基础实践

    20155202<网络对抗>Exp9 web安全基础实践 实验前回答问题 (1)SQL注入攻击原理,如何防御 SQL注入产生的原因,和栈溢出.XSS等很多其他的攻击方法类似,就是未经检查或 ...

  7. 20145206邹京儒《网络对抗》逆向及Bof基础实践

    20145206邹京儒<网络对抗>逆向及Bof基础实践 1 逆向及Bof基础实践说明 1.1 实践目标 本次实践的对象是一个名为pwn1的linux可执行文件. 该程序正常执行流程是:ma ...

  8. 东北农业大学大学计算机基础作业答案,大学计算机基础实践教学改革的研究

    针对目前大学计算机基础实践教学中存在的问题,提出了教学内容.教学方法.教学模式.考试形式等方面的改革方案,并在实践中不断践行和完善,达到预期的教学效果,为提高实践教学质量提供参考. 第 o第. 卷期 ...

  9. python 从入门到实践 pdf_python入门基础实践课,带你有效的学习python

    你还在枯燥的一个人学python吗?你尝试过python入门基础实践课吗? python入门实践课,带你快速了解python,它与你平时学python,有什么不一样的地方呢: 1.完全免费 第一次学p ...

最新文章

  1. Python游戏开发,Pygame模块,Python从零开始带大家实现魔塔小游戏
  2. mpvue 小程序如何开启下拉刷新,上拉加载?
  3. 汇编中的word ptr
  4. sizeof()使用注意
  5. jsp九大内置对象与servlet中java对象
  6. windows Tracert命令
  7. 潘在亮:给业务开发提供黑科技装备的“测试Q博士”
  8. 复数类--重载运算符3+
  9. redis学习-分布式数据库CAP原理
  10. Apache 2.4.28的安装
  11. BOM offset client scroll
  12. 用把位帮助记忆吉他音阶在指板上的分布
  13. Shell判断路径是否存在
  14. 计算机键盘交替换键是,用了这么久电脑,还不知道键盘上的Ctrl、Tab、Alt是啥英文?快来涨知识!...
  15. 玩转Excel系列-index+match查找函数用法
  16. 如何将py文件转化为exe
  17. 微信公众号开启服务器配置流程及注意事项
  18. 课程设计 --- 黑白棋中的 AI
  19. 外贸ERP系统的操作有什么特点?中小企业适合选择吗?
  20. 知乎热议: Java, Go和Python那个前景好?

热门文章

  1. 斐波拉契(Fibonacci)数列
  2. 开天机型系统镜像及软件资源网盘链接
  3. 王多鱼噩梦:MIT的机器狗学会守门了,拦截率87.5%,比顶尖运动员还高!
  4. %3c?php+eval,callback噩梦:解析著名CMS框架Drupal SQL注入漏洞
  5. css3字体闪烁炫酷,css3 scale动画 字体、输入框等会闪烁,怎么解决?
  6. vue+summernote富文本编辑器
  7. dell灵越笔记本后盖怎么拆_戴尔灵越5000笔记本拆解 内部做工相当出众
  8. 换零钱程序c语言,《SICP》换零钱的递归法与迭代法
  9. AI智慧工地视频分析系统 yolov7
  10. 购买mysql服务器时需要考虑的问题