教人做甜品的网站,合肥网站建设服务公司,金融品牌网站设计,积分购物型网站要求实现#xff1a;从SVN地址预览文件并编辑#xff0c;并能上传到SVN的功能 刚开始的实现思路是这样的#xff1a; 进入对应的文件#xff0c;url会携带文件id参数#xff0c;点击文件信息页的按钮#xff0c;发送后端请求#xff0c;后端根据文件id查询数据库、获取…要求实现从SVN地址预览文件并编辑并能上传到SVN的功能刚开始的实现思路是这样的进入对应的文件url会携带文件id参数点击文件信息页的按钮发送后端请求后端根据文件id查询数据库、获取文件SVN地址、checkout文件前端跳转到onlyoffice编辑页面collaborative.html。这里的onlyoffice直接使用的是https://github.com/fernfei/OnlyofficePersonal。使用的这份代码是纯前端库没有部署DocumentServer。这份代码只能在onlyoffice编辑器内部点击“另存为”将文件下载下来之前一直在尝试使用官方文档写的回调函数将文件下载下来但一直没成功后来发现这种方式好像需要DocumentServer、/躺于是采取了笨办法另存为结束之后在这个编辑页面上方点击“上传到SVN”按钮后端把下载的文件覆盖到之前checkout的SVN工作目录。以下是主界面的前端逻辑发送后端请求跳转到onlyoffice编辑页面// --- 交互事件绑定 --- function bindInteractionEvents() { //省略 if (e.target.classList.contains(collaborative-edit-btn)) { e.preventDefault(); e.stopPropagation(); const resourceId e.target.getAttribute(data-id); const resourceName e.target.getAttribute(data-name); const requirementId e.target.getAttribute(data-requirement); const resourceType e.target.getAttribute(data-type); previewResourceUrl(resourceId, resourceName, requirementId, resourceType); } } async function updateResourceDetailActions(resource) { if (canManage) { // 2. 文档协同 (仅设计和测试) if (resource.type design || resource.type test) { const collabBtn document.createElement(button); collabBtn.className btn btn-info; collabBtn.innerHTML 文档协同; collabBtn.onclick function() { //改成从svn下载 previewResourceUrl(resource.id,resource.name,resource.requirement_id,resource.type); //openCollaborativeEditor(resource.id, resource.name, resource.requirement_id, resource.type); }; container.appendChild(collabBtn); } } }// 预览资源URL function previewResourceUrl(resourceId,resourceName, requirementId, resourceType) { // 从URL获取文件并打开 fetch(resource?actionpreviewid${resourceId}type${resourceType}) .then(res res.json()) .then(data { if (data.success) { alert(获取内容成功准备预览); const url collaborative.html?resType${resourceType}resId${resourceId}title${resourceName}; window.open(url, _blank); } else { alert(错误: data.error); } }); }以下是后端获取文件的部分代码SVN锁定这里还有点坑以后再说吧protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { //省略 //预览请求 String action request.getParameter(action); // 获取 action 参数 // 如果 action 是 preview跳转到预览处理逻辑 if (preview.equals(action) idParam ! null) { try { previewResource(out, Integer.parseInt(idParam),type, request); } catch (Exception e) { e.printStackTrace(); out.print({\success\: false, \error\: \预览准备失败: e.getMessage() \}); } return; // 处理完预览后直接返回 } } /**增加预览函数 * 先checkout目录 再update文件 * 考虑未上传多次进入编辑的情况 */ private void previewResource(PrintWriter out, int id, String type, HttpServletRequest request) throws Exception { Connection conn null; try { conn DBUtil.getConnection(); String svnUrl null; String fileName null; String sql SELECT url, name FROM requirement_resources WHERE id ?; PreparedStatement stmt conn.prepareStatement(sql); stmt.setInt(1, id); ResultSet rs stmt.executeQuery(); if (rs.next()) { svnUrl rs.getString(url); fileName rs.getString(name); } rs.close(); stmt.close(); if (svnUrl null || svnUrl.isEmpty()) { out.print({\success\: false, \error\: \无效的 SVN 地址\}); return; } // 2. 设定临时目录 //String tempDir request.getServletContext().getRealPath(/temp_svn_files); String tempDir System.getProperty(user.home)/temp_svn_files; if (tempDir null) { out.print({\success\: false, \error\: \无法获取临时目录\}); return; } File tempDirFile new File(tempDir); if (!tempDirFile.exists()) { tempDirFile.mkdirs(); } // 3. 检查工作目录是否已存在 String workingDirPath tempDir /svn_work_ id; File workingDir new File(workingDirPath); String baseName getFileNameFromUrl(svnUrl); File svnFile null; // 检查工作目录是否已经存在且有文件 if (workingDir.exists() new File(workingDir, .svn).exists()) { System.out.println(SVN工作目录已存在: workingDirPath); // 查找工作目录中的文件 svnFile new File(workingDir, baseName); if (!svnFile.exists()) { File[] files workingDir.listFiles(); if (files ! null) { for (File file : files) { if (!file.isDirectory() !file.getName().startsWith(.) !file.getName().equals(.svn)) { svnFile file; baseName file.getName(); break; } } } } if (svnFile ! null svnFile.exists()) { System.out.println(使用现有工作目录中的文件: svnFile.getAbsolutePath()); System.out.println(文件大小: svnFile.length() bytes); System.out.println(可写: svnFile.canWrite()); // 4. 更新文件到最新版本 try { System.out.println(更新SVN文件到最新版本...); ProcessBuilder updatePb new ProcessBuilder( svn, update, svnFile.getName(), --non-interactive, --trust-server-cert ); updatePb.directory(workingDir); Process updateProcess updatePb.start(); // 读取更新输出 BufferedReader outputReader new BufferedReader(new InputStreamReader(updateProcess.getInputStream())); BufferedReader errorReader new BufferedReader(new InputStreamReader(updateProcess.getErrorStream())); StringBuilder output new StringBuilder(); StringBuilder errorOutput new StringBuilder(); String line; while ((line outputReader.readLine()) ! null) { output.append(line).append(\n); System.out.println(SVN更新输出: line); } while ((line errorReader.readLine()) ! null) { errorOutput.append(line).append(\n); System.err.println(SVN更新错误: line); } int updateExitCode updateProcess.waitFor(); if (updateExitCode 0) { System.out.println(文件更新成功新大小: svnFile.length() bytes); // 检查是否有冲突 if (output.toString().contains(Conflict)) { System.out.println(检测到文件冲突需要解决冲突); // 这里可以添加冲突处理逻辑 } } else { System.err.println(文件更新失败: errorOutput.toString()); // 继续使用现有文件 } } catch (Exception e) { System.err.println(更新文件时出错: e.getMessage()); // 继续使用现有文件 } // 5. 重新锁定文件如果之前已解锁 try { System.out.println(重新锁定文件...); ProcessBuilder lockPb new ProcessBuilder( svn, lock, svnFile.getName(), -m, Web编辑锁定, --non-interactive, --trust-server-cert, --force // 强制锁定即使已被锁定 ); lockPb.directory(workingDir); Process lockProcess lockPb.start(); int lockExitCode lockProcess.waitFor(); if (lockExitCode 0) { System.out.println(文件锁定成功); } else { BufferedReader lockErrorReader new BufferedReader(new InputStreamReader(lockProcess.getErrorStream())); StringBuilder lockError new StringBuilder(); String line; while ((line lockErrorReader.readLine()) ! null) { lockError.append(line); } System.err.println(文件锁定失败: lockError.toString()); } } catch (Exception e) { System.err.println(锁定文件时出错: e.getMessage()); } // 6. 返回成功信息使用更新后的文件 out.print({ \success\: true, \docId\: \res_ id \, \fileName\: \ escapeJson(fileName) \, \workingDir\: \temp_svn_files/svn_work_ id \, \svnFileName\: \ baseName \, \fromCache\: true, \updated\: true }); return; } } // 7. 如果不存在或不是SVN工作副本创建新的工作目录 if (workingDir.exists()) { deleteDirectory(workingDir); } workingDir.mkdirs(); System.out.println(创建新的SVN工作目录: workingDirPath); // 8. 获取父目录URL String parentUrl getParentUrl(svnUrl); System.out.println(父目录URL: parentUrl); // 9. 第一步创建空的工作拷贝目录 ProcessBuilder checkoutPb new ProcessBuilder( svn, checkout, --depth, empty, parentUrl, workingDir.getAbsolutePath(), --non-interactive, --trust-server-cert ); Process checkoutProcess checkoutPb.start(); BufferedReader checkoutErrorReader new BufferedReader(new InputStreamReader(checkoutProcess.getErrorStream())); StringBuilder checkoutError new StringBuilder(); String line; while ((line checkoutErrorReader.readLine()) ! null) { checkoutError.append(line).append(\n); } int checkoutExitCode checkoutProcess.waitFor(); if (checkoutExitCode ! 0) { out.print({\success\: false, \error\: \创建SVN工作目录失败: escapeJson(checkoutError.toString()) \}); return; } System.out.println(空工作目录创建成功); // 10. 第二步单独检出文件 ProcessBuilder updatePb new ProcessBuilder( svn, update, baseName, --non-interactive, --trust-server-cert ); updatePb.directory(workingDir); Process updateProcess updatePb.start(); BufferedReader updateErrorReader new BufferedReader(new InputStreamReader(updateProcess.getErrorStream())); StringBuilder updateError new StringBuilder(); while ((line updateErrorReader.readLine()) ! null) { updateError.append(line).append(\n); } int updateExitCode updateProcess.waitFor(); if (updateExitCode ! 0) { out.print({\success\: false, \error\: \检出SVN文件失败: escapeJson(updateError.toString()) \}); return; } // 11. 检查文件是否成功检出 svnFile new File(workingDir, baseName); if (!svnFile.exists()) { // 查找第一个文件 File[] files workingDir.listFiles(); if (files ! null) { for (File file : files) { if (!file.isDirectory() !file.getName().startsWith(.) !file.getName().equals(.svn)) { svnFile file; baseName file.getName(); break; } } } } if (!svnFile.exists()) { out.print({\success\: false, \error\: \文件检出后未找到\}); return; } System.out.println(文件检出成功: svnFile.getAbsolutePath()); System.out.println(文件大小: svnFile.length() bytes); // 12. 加锁文件 ProcessBuilder lockPb new ProcessBuilder( svn, lock, svnFile.getName(), -m, Web编辑锁定, --non-interactive, --trust-server-cert ); lockPb.directory(workingDir); Process lockProcess lockPb.start(); lockProcess.waitFor(); // 不检查退出码锁定失败也可以继续 // 13. 返回成功信息 out.print({ \success\: true, \docId\: \res_ id \, \fileName\: \ escapeJson(fileName) \, \workingDir\: \temp_svn_files/svn_work_ id \, \svnFileName\: \ baseName \, \fromCache\: false, \updated\: false }); } catch (Exception e) { System.err.println(预览资源时发生异常: e.getMessage()); e.printStackTrace(); out.print({\success\: false, \error\: \预览失败: e.getMessage() \}); } finally { try { if (conn ! null) conn.close(); } catch (Exception e) {} } }之后是跳转到collaborative页面这个页面先向后端获取文档内容再生成Blob URL用于OnlyOffice加载OnlyOffice API就可以用OnlyOffice编辑器编辑了。我只在页面中额外增加了SVN版本历史显示栏和“上传到SVN”按钮。// 加载文档内容 async function loadDocument() { try { showLoading(true); document.getElementById(status).textContent 正在加载文档...; // 从后端获取文档内容 const response await fetch(resource?actiongetContentid${resId}); if (!response.ok) { throw new Error(HTTP错误: ${response.status}); } const data await response.json(); if (!data.success) { throw new Error(data.error || 加载文档失败); } // 创建Blob URL用于OnlyOffice const fileName data.fileName || document.docx; let blobUrl null; if (data.content) { // 处理Base64编码的内容 const binary atob(data.content); const array new Uint8Array(binary.length); for(let i 0; i binary.length; i) { array[i] binary.charCodeAt(i); } const blob new Blob([array.buffer]); blobUrl URL.createObjectURL(blob); } // 配置OnlyOffice编辑器 currentDocConfig { document: { fileType: getFileExtension(fileName), key: res_${resId}_${Date.now()}, // 每次加载生成新key防止缓存 title: fileName, url: blobUrl }, documentType: getDocumentType(fileName), editorConfig: { mode: edit, lang: zh }, height: 100%, width: 100% }; // 初始化OnlyOffice编辑器 initializeOnlyOffice(); // 加载SVN历史 loadSvnHistory(); // 更新状态 document.getElementById(status).textContent 文档已加载; document.getElementById(saveSvnBtn).style.display inline-block; } catch (error) { console.error(加载文档失败:, error); showNotification(加载失败: ${error.message}, error); document.getElementById(status).textContent 加载失败; } finally { showLoading(false); } } // 初始化OnlyOffice编辑器 function initializeOnlyOffice() { if (typeof DocsAPI undefined) { showNotification(OnlyOffice API 未加载请检查文档服务器, error); return; } docEditor new DocsAPI.DocEditor(editor, currentDocConfig); }文件保存下来之后点击“上传到SVN”按钮发送发送POST请求actioncommit_svn。但是远程访问时这个流程会失败因为服务器试图在自己的下载目录中查找下载的文件but文件在客户端机器上。于是又回到了刚开始的问题。没有成功使用OnlyOffice的回调机制callbackUrl来处理保存。挣扎了许久都没有解决最后我打算拦截文件下载直接把文件内容传给后端。尝试了监听OnlyOffice的事件但是加入控制台输出语句发现在“另存为”时没有事件消息只有x2t的消息。x2t_helper.js:396 X2T WASM script loaded successfullyx2t_helper.js:433 X2T module initialized successfully监听 XMLHttpRequest没有效果然后意识到要在iframe内部拦截才行。// // 【新增】iframe 拦截捕获编辑器另存为产生的 blob 下载 // 自动将文件内容 POST 到后端 saveFromEditor写入 SVN 工作目录 // 用户随后可点击上传到SVN按钮调用已有的 commit_svn 完成提交 // /** * 向指定 document 注入 createElement 劫持 * 拦截所有带 download 属性且 href 为 blob: 的 a 标签点击 */ function injectSaveInterceptor(iframeDoc) { if (!iframeDoc || iframeDoc._interceptorInjected) return; iframeDoc._interceptorInjected true; const _orig iframeDoc.createElement.bind(iframeDoc); iframeDoc.createElement function(tag) { const el _orig(tag); if (tag.toLowerCase() a) { el.addEventListener(click, function(e) { if (el.hasAttribute(download) el.href el.href.startsWith(blob:)) { e.preventDefault(); e.stopImmediatePropagation(); const filename el.download || document.docx; console.log([saveFromEditor] 拦截到另存为文件名:, filename); saveEditorFileToServer(el.href, filename); } }); } return el; }; console.log([saveFromEditor] 拦截器已注入); } /** * 递归向所有子 iframe 注入拦截器并监听后续动态创建的 iframe * OnlyOffice 内部存在多层 iframe需要全部覆盖 */ function injectInterceptorRecursive(doc) { if (!doc) return; injectSaveInterceptor(doc); try { Array.from(doc.querySelectorAll(iframe)).forEach(function(f) { try { f.addEventListener(load, function() { try { injectInterceptorRecursive(f.contentDocument); } catch(e) {} }); if (f.contentDocument) injectInterceptorRecursive(f.contentDocument); } catch(e) {} }); } catch(e) {} // 监听后续动态插入的 iframe try { const obs new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType 1 node.tagName IFRAME) { node.addEventListener(load, function() { try { injectInterceptorRecursive(node.contentDocument); } catch(e) {} }); } }); }); }); obs.observe(doc.body || doc.documentElement, { childList: true, subtree: true }); } catch(e) {} } /** * 等待编辑器 iframenameframeEditor出现后启动注入 * DocsAPI 动态创建 iframe需用 MutationObserver 等待 */ function waitForEditorIframe() { const existing document.querySelector(iframe[nameframeEditor]); if (existing) { setupIframeInterceptor(existing); return; } const observer new MutationObserver(function() { const iframe document.querySelector(iframe[nameframeEditor]); if (iframe) { observer.disconnect(); setupIframeInterceptor(iframe); } }); observer.observe(document.body, { childList: true, subtree: true }); } function setupIframeInterceptor(iframe) { const tryInject function() { try { injectInterceptorRecursive(iframe.contentDocument || iframe.contentWindow.document); } catch(e) { console.warn([saveFromEditor] 注入失败:, e); } }; iframe.addEventListener(load, tryInject); // 若 iframe 已加载完成则立即注入 if (iframe.contentDocument iframe.contentDocument.readyState complete) { tryInject(); } } /** * 读取 blob 内容POST 到后端写入 SVN 工作目录 * 后端路径resource?actionsaveFromEditorid{resId} */ async function saveEditorFileToServer(blobUrl, filename) { if (!resId) { showNotification(资源ID未初始化无法保存, error); return; } try { showNotification(正在保存到工作目录..., info); document.getElementById(status).textContent 正在保存...; const fileRes await fetch(blobUrl); const blob await fileRes.blob(); const formData new FormData(); formData.append(file, blob, filename); const response await fetch(resource?actionsaveFromEditorid${resId}, { method: POST, body: formData }); const data await response.json(); if (data.success) { hasUnsavedChanges false; showNotification(已保存到工作目录可点击上传到SVN提交, success); document.getElementById(status).textContent 已保存到工作目录 new Date().toLocaleTimeString(); } else { throw new Error(data.error || 保存失败); } } catch (error) { console.error([saveFromEditor] 失败:, error); showNotification(保存失败: error.message, error); document.getElementById(status).textContent 保存失败; } } // // 初始化OnlyOffice编辑器原有逻辑新增 waitForEditorIframe 调用 // function initializeOnlyOffice() { if (typeof DocsAPI undefined) { showNotification(OnlyOffice API 未加载请检查文档服务器, error); return; } docEditor new DocsAPI.DocEditor(editor, currentDocConfig); // 【新增】编辑器创建后立即启动 iframe 拦截器 waitForEditorIframe(); } //其他代码省略后端在doPost里面添加把文件内容保存到SVN目录的逻辑再把之前提交到svn代码里多余的寻找文件、移动文件等内容删掉就可以了。else if (saveFromEditor.equals(action) idParam ! null) { Connection conn null; try { int id Integer.parseInt(idParam); Part filePart request.getPart(file); if (filePart null || filePart.getSize() 0) { out.print({\success\: false, \error\: \未收到文件内容\}); return; } // 1. 从数据库查出 svn url推算文件名 conn DBUtil.getConnection(); PreparedStatement stmt conn.prepareStatement( SELECT url FROM requirement_resources WHERE id ?); stmt.setInt(1, id); ResultSet rs stmt.executeQuery(); if (!rs.next()) { out.print({\success\: false, \error\: \资源不存在\}); rs.close(); stmt.close(); conn.close(); return; } String svnUrl rs.getString(url); rs.close(); stmt.close(); // 2. 定位工作目录和目标文件与 previewResource/commitToSvn 保持一致 String baseName getFileNameFromUrl(svnUrl); String workingDirPath System.getProperty(user.home) /temp_svn_files/svn_work_ id; File workingDir new File(workingDirPath); File svnFile new File(workingDir, baseName); if (!workingDir.exists() || !svnFile.exists()) { out.print({\success\: false, \error\: \工作目录不存在请先在页面中打开文档再保存\}); conn.close(); return; } // 3. 覆盖写入文件SVN 工作副本中的文件可能是只读的先设可写 if (!svnFile.canWrite()) { svnFile.setWritable(true); } try (InputStream in filePart.getInputStream()) { java.nio.file.Files.copy(in, svnFile.toPath(), java.nio.file.StandardCopyOption.REPLACE_EXISTING); } System.out.println( [saveFromEditor] 文件已写入: svnFile.getAbsolutePath() 大小: svnFile.length() bytes); out.print({\success\: true}); conn.close(); } catch (Exception e) { e.printStackTrace(); if (conn ! null) try { conn.close(); } catch (Exception ignored) {} out.print({\success\: false, \error\: \ escapeJson(e.getMessage()) \}); } return; }