PageOffice实现文档并发控制
问题背景
在B/S架构下的办公系统中,用户访问请求都是并发的,也就是说经常会出现同时N个用户对一个服务器页面发出请求,这就有可能出现同一个文档被多个用户同时打开进行编辑的情况,那么,保存时文件就可能出现互相覆盖的问题。为什么会出现互相覆盖呢?举个简单例子,例如A用户先访问页面打开了一个文档开始编辑,这时B用户访问相同的页面打开了同一个文档也开始编辑,B用户可能很快就完成了文档修改工作并且保存到服务器。随后A用户也完成了工作并保存文档到服务器。这时,服务器上的这个文档已经变成了A用户修改的版本,B用户的修改被A用户的保存操作覆盖从而消失了。
两种解决方案
- 引入工作流模块。文档流转到哪个环节,就由哪个环节的用户去操作,其他用户无法打开此文档,或无法以编辑模式打开此文档。(互联网上有专门的工作流产品,因此本文不对此方案进行介绍。)
- 采用锁机制实现文档并发控制。在服务器端对文档进行加锁,只有被锁的用户才能打开此文档进行编辑,其他用户无法打开此文档,或无法以编辑模式打开此文档。
PageOffice V5及以前的版本自带了文档并发控制功能,设置PageOfficeCtrl对象的TimeSlice属性就可以保证同一时间同一篇文档只能由一个人打开,但是此功能仅限于单体Web项目且部署在一台服务器上。由于现在越来越多的项目使用了微服务架构或集群部署,因此就需要开发人员实现自定义的文档并发控制功能。下面我们以一个最简单的文档并发控制方案为例,介绍一下实现自定义的文档并发控制功能的主要步骤:
- 数据库设计调整,增加锁状态字段。在文档记录表中增加一个Editor字段,用于记录当前文档是否正被某用户编辑。如果该字段为空,表示文档未被任何用户编辑;如果非空,则字段值应为当前编辑者的用户名。
- 打开文件之前发送检查请求。当用户尝试打开文档时,前端应首先向后端发送一个请求,检查文档的Editor字段。
- 如果Editor字段为空,说明文档当前未被任何用户编辑,可以安全地允许当前用户打开文档,并将当前用户的用户名写入文档记录表中的Editor字段,以此标记文档当前处于被编辑状态。
- 如果Editor字段非空,即已经有其他用户正在编辑文档,则向用户显示提示信息,告知文档正在被他人编辑,建议稍后再试,或者以只读预览模式打开文档。
- 用户完成文档编辑并关闭文档时,前端应当通过Ajax请求通知后端,后端需将文档记录表中对应的Editor字段清空,从而释放文档的编辑锁。
后端代码
后端验证文档编辑状态的接口(比如:detectCurrentEditor.jsp),代码如下:
String id = request.getParameter("id");
String editor = "";Class.forName("org.sqlite.JDBC");
String strUrl = "jdbc:sqlite:" + this.getServletContext().getRealPath("BingFa/BingFa.db");
Connection conn=DriverManager.getConnection(strUrl);
Statement stmt = conn.createStatement(); ResultSet rs = stmt.executeQuery("select * from doc where id="+id);
if(rs.next()){editor = rs.getString("Editor");
}
rs.close();
stmt.close();
conn.close();response.setContentType("application/json");
out.print("{\"editor\":\""+ editor +"\"}"); //返回当前文档的编辑者
后端释放文档编辑锁的接口(比如:clearCurrentEditor.jsp),代码如下:
String id = request.getParameter("id");Class.forName("org.sqlite.JDBC");
String strUrl = "jdbc:sqlite:" + this.getServletContext().getRealPath("BingFa/BingFa.db");
Connection conn=DriverManager.getConnection(strUrl);
Statement stmt2 = conn.createStatement();
stmt2.executeUpdate("Update doc set Editor='' where id="+id); //清空文档的编辑者
stmt2.close();
conn.close();response.setContentType("application/json");
out.print("{\"msg\":\"ok\"}");
前端代码
打开文件之前发送ajax请求到后端接口(比如:detectCurrentEditor.jsp)验证,检查当前文档是否有用户正在编辑。
// 编辑文件
function editFile(id){var user = loginName; // 假设loginName为当前登录用户的用户名// 检查当前文档是否有用户正在编辑$.ajax({url: 'detectCurrentEditor.jsp?id=' + id, type: 'GET', dataType: 'json', success: function(data) {if(data.editor == user || data.editor == ''){POBrowser.openWindow('word.jsp?id='+id+'&user='+encodeURIComponent(user) , 'width=1200px;height=800px;');}else{alert('用户“'+data.editor+'”正在编辑此文档,请稍后重试,或点击“查看”只读打开。');}},error: function(xhr, status, error) {console.error('请求失败:', error);}});
}
关闭文件时,通过PageOffice的OnBeforeBrowserClosed()
事件函数,发送ajax请求到后端接口(比如:clearCurrentEditor.jsp),将文档的Editor字段清空,释放编辑锁。
// 通知服务器端,用户已停止编辑文档
function SendCloseMsg(){$.ajax({url: 'clearCurrentEditor.jsp?id=<%=id%>', type: 'GET', dataType: 'json', success: function(data) {console.log('请求成功:', data);},error: function(xhr, status, error) {console.error('请求失败:', error);}});
}function OnBeforeBrowserClosed() {// 此处可以执行窗口关闭前需要执行的业务逻辑代码if(pageofficectrl.IsDirty){if (confirm("提示:文档已被修改,是否继续关闭放弃保存 ?")) {SendCloseMsg();pageofficectrl.CloseWindow(true);//必须。否则窗口不会关闭。} }else{SendCloseMsg();pageofficectrl.CloseWindow(true);//必须。否则窗口不会关闭。}}
原文档:自定义文档并发控制 | PageOffice 开发者中心