PageOffice实现文档并发控制

问题背景

在B/S架构下的办公系统中,用户访问请求都是并发的,也就是说经常会出现同时N个用户对一个服务器页面发出请求,这就有可能出现同一个文档被多个用户同时打开进行编辑的情况,那么,保存时文件就可能出现互相覆盖的问题。为什么会出现互相覆盖呢?举个简单例子,例如A用户先访问页面打开了一个文档开始编辑,这时B用户访问相同的页面打开了同一个文档也开始编辑,B用户可能很快就完成了文档修改工作并且保存到服务器。随后A用户也完成了工作并保存文档到服务器。这时,服务器上的这个文档已经变成了A用户修改的版本,B用户的修改被A用户的保存操作覆盖从而消失了。

两种解决方案

  • 引入工作流模块。文档流转到哪个环节,就由哪个环节的用户去操作,其他用户无法打开此文档,或无法以编辑模式打开此文档。(互联网上有专门的工作流产品,因此本文不对此方案进行介绍。)
  • 采用锁机制实现文档并发控制。在服务器端对文档进行加锁,只有被锁的用户才能打开此文档进行编辑,其他用户无法打开此文档,或无法以编辑模式打开此文档。

PageOffice V5及以前的版本自带了文档并发控制功能,设置PageOfficeCtrl对象的TimeSlice属性就可以保证同一时间同一篇文档只能由一个人打开,但是此功能仅限于单体Web项目且部署在一台服务器上。由于现在越来越多的项目使用了微服务架构或集群部署,因此就需要开发人员实现自定义的文档并发控制功能。下面我们以一个最简单的文档并发控制方案为例,介绍一下实现自定义的文档并发控制功能的主要步骤:

  1. 数据库设计调整,增加锁状态字段。在文档记录表中增加一个Editor字段,用于记录当前文档是否正被某用户编辑。如果该字段为空,表示文档未被任何用户编辑;如果非空,则字段值应为当前编辑者的用户名。
  2. 打开文件之前发送检查请求。当用户尝试打开文档时,前端应首先向后端发送一个请求,检查文档的Editor字段。
    • 如果Editor字段为空,说明文档当前未被任何用户编辑,可以安全地允许当前用户打开文档,并将当前用户的用户名写入文档记录表中的Editor字段,以此标记文档当前处于被编辑状态。
    • 如果Editor字段非空,即已经有其他用户正在编辑文档,则向用户显示提示信息,告知文档正在被他人编辑,建议稍后再试,或者以只读预览模式打开文档。
  3. 用户完成文档编辑并关闭文档时,前端应当通过Ajax请求通知后端,后端需将文档记录表中对应的Editor字段清空,从而释放文档的编辑锁。

后端代码

后端验证文档编辑状态的接口(比如:detectCurrentEditor.jsp),代码如下:

  1. String id = request.getParameter("id");
  2. String editor = "";
  3. Class.forName("org.sqlite.JDBC");
  4. String strUrl = "jdbc:sqlite:" + this.getServletContext().getRealPath("BingFa/BingFa.db");
  5. Connection conn=DriverManager.getConnection(strUrl);
  6. Statement stmt = conn.createStatement();
  7. ResultSet rs = stmt.executeQuery("select * from doc where id="+id);
  8. if(rs.next()){
  9. editor = rs.getString("Editor");
  10. }
  11. rs.close();
  12. stmt.close();
  13. conn.close();
  14. response.setContentType("application/json");
  15. out.print("{\"editor\":\""+ editor +"\"}"); //返回当前文档的编辑者

后端释放文档编辑锁的接口(比如:clearCurrentEditor.jsp),代码如下:

  1. String id = request.getParameter("id");
  2. Class.forName("org.sqlite.JDBC");
  3. String strUrl = "jdbc:sqlite:" + this.getServletContext().getRealPath("BingFa/BingFa.db");
  4. Connection conn=DriverManager.getConnection(strUrl);
  5. Statement stmt2 = conn.createStatement();
  6. stmt2.executeUpdate("Update doc set Editor='' where id="+id); //清空文档的编辑者
  7. stmt2.close();
  8. conn.close();
  9. response.setContentType("application/json");
  10. out.print("{\"msg\":\"ok\"}");

前端代码

打开文件之前发送ajax请求到后端接口(比如:detectCurrentEditor.jsp)验证,检查当前文档是否有用户正在编辑。

  1. // 编辑文件
  2. function editFile(id){
  3. var user = loginName; // 假设loginName为当前登录用户的用户名
  4. // 检查当前文档是否有用户正在编辑
  5. $.ajax({
  6. url: 'detectCurrentEditor.jsp?id=' + id,
  7. type: 'GET',
  8. dataType: 'json',
  9. success: function(data) {
  10. if(data.editor == user || data.editor == ''){
  11. POBrowser.openWindow('word.jsp?id='+id+'&user='+encodeURIComponent(user) , 'width=1200px;height=800px;');
  12. }else{
  13. alert('用户“'+data.editor+'”正在编辑此文档,请稍后重试,或点击“查看”只读打开。');
  14. }
  15. },
  16. error: function(xhr, status, error) {
  17. console.error('请求失败:', error);
  18. }
  19. });
  20. }

 关闭文件时,通过PageOffice的OnBeforeBrowserClosed()事件函数,发送ajax请求到后端接口(比如:clearCurrentEditor.jsp),将文档的Editor字段清空,释放编辑锁。

  1. // 通知服务器端,用户已停止编辑文档
  2. function SendCloseMsg(){
  3. $.ajax({
  4. url: 'clearCurrentEditor.jsp?id=<%=id%>',
  5. type: 'GET',
  6. dataType: 'json',
  7. success: function(data) {
  8. console.log('请求成功:', data);
  9. },
  10. error: function(xhr, status, error) {
  11. console.error('请求失败:', error);
  12. }
  13. });
  14. }
  15. function OnBeforeBrowserClosed() {
  16. // 此处可以执行窗口关闭前需要执行的业务逻辑代码
  17. if(pageofficectrl.IsDirty){
  18. if (confirm("提示:文档已被修改,是否继续关闭放弃保存 ?")) {
  19. SendCloseMsg();
  20. pageofficectrl.CloseWindow(true);//必须。否则窗口不会关闭。
  21. }
  22. }else{
  23. SendCloseMsg();
  24. pageofficectrl.CloseWindow(true);//必须。否则窗口不会关闭。
  25. }
  26. }

原文档:自定义文档并发控制 | PageOffice 开发者中心

posted @ 2025-08-08 14:06  爱吃苹果皮  阅读(11)  评论(0)    收藏  举报