资讯详情 Java轻量OA系统:基于Activiti的可配置审批流实战
📅 2026/10/23 20:16:36
简介这是一套基于SSM框架SpringSpringMVCMyBatis开发的Java办公自动化系统OA面向Java初学者与中级开发者适用于课程设计、毕业项目及中小企业轻量级OA定制需求。系统覆盖用户管理、工作流审批、文档共享、通知公告、任务与会议管理等核心办公模块具备良好的可扩展性与二次开发基础。资源包共1160个文件含333个Java源码、365个编译后class文件支撑完整MVC分层结构另有179个JS脚本实现前端交互、50个JSP页面构成视图层、35个XML配置文件管理映射与参数辅以CSS、JSON、SQL等配套资源整体压缩包仅6.95MB轻量易部署。目前已有2371人学习下载读者可直接运行调试、理解SSM整合原理、掌握OA典型业务流程实现逻辑并基于现有结构快速拓展CRM、HRM等功能模块。1. 这不是又一个“登录后显示欢迎页”的OA Demo它跑在真实局域网里支撑过37人行政人事IT协同流程审批流能回退、抄送可分级、附件带版本号——适合想拿源码改出自己公司第一版轻量OA的Java后端开发者我去年帮某高校实验室搭内部事务系统时翻过二十多个标着“Java OA”的开源项目八成点开就是Spring Boot Thymeleaf写个用户管理公告列表连请假单字段都硬编码在HTML里。这个项目不一样它用Spring MVC分层清晰到能直接拎出approval-service模块做微服务拆分数据库设计里sys_workflow_node表存的是可配置的节点跳转规则不是if-else写死的“领导A批完→领导B批”更关键的是它把Office文档在线预览封装成独立doc-viewer-starter连Word里的修订痕迹都能渲染出来。如果你正卡在“怎么让审批人看到上一环节的批注”或者“怎么把Excel导入的员工数据自动同步到组织架构树”这份代码就是你该下载的那一个——它不教你怎么配Spring Boot启动类但会告诉你WorkflowEngine.execute()里第17行那个context.put(currentApprover, resolveNextApprover())是怎么动态算出下一审批人的。2. 项目结构与技术栈从pom.xml看懂它为什么能扛住并发审批压测2.1 目录结构五个核心模块撑起完整业务闭环项目采用标准Maven多模块结构根目录下pom.xml声明了6个子模块但真正承载业务的是以下5个oa-common为工具包不计入业务模块oa-parent/ ├── oa-core/ # Spring MVC控制器层 全局异常处理 拦截器链 ├── oa-workflow/ # 审批引擎核心流程定义解析、节点执行、驳回重走逻辑 ├── oa-doc/ # 文档服务Office文件解析Apache POI、在线预览LibreOffice headless、版本快照存储 ├── oa-report/ # 报表中心JasperReports集成支持导出PDF/Excel的审批统计看板 └── oa-admin/ # 管理后台组织架构树维护、角色权限分配RBAC、流程模板可视化配置提示oa-workflow模块的src/main/resources/workflow-definitions/目录下存放XML格式的流程定义文件如leave-process-v2.xml这是整个审批流的“源代码”修改它比改Java代码更安全——后面会详解怎么热更新。2.2 技术选型深度解析为什么用MyBatis而不是JPA项目放弃Spring Data JPA坚持用MyBatis-Plus 3.4.3原因很实际动态SQL能力审批流中“根据部门负责人是否在职动态跳过节点”需要运行时拼接WHERE条件MyBatis的choose标签比JPA的Criteria API写起来快3倍SQL审计友好oa-core模块的SqlAuditInterceptor能直接拦截Mapper.xml里的SQL而JPA的Query注解会让审计日志变成一堆问号参数分库分表平滑oa-report模块未来要接入ShardingSphereMyBatis-Plus的TableNameHandler接口比JPA的Table注解更容易注入分片逻辑。依赖清单关键片段pom.xml!-- 审批引擎强依赖 -- dependency groupIdorg.activiti/groupId artifactIdactiviti-spring-boot-starter-basic/artifactId version7.1.0.M6/version !-- 注意非官方最新版此版本修复了Activiti 7.1.0.M5的并发锁bug -- /dependency !-- 文档解析避坑选择 -- dependency groupIdorg.apache.poi/groupId artifactIdpoi-ooxml/artifactId version4.1.2/version !-- 低于4.1.2无法正确读取.xlsx中的条件格式 -- /dependency dependency groupIdnet.sf.jasperreports/groupId artifactIdjasperreports/artifactId version6.17.0/version /dependency2.3 数据库设计精髓一张sys_workflow_instance表如何承载所有流程状态oa-workflow模块的数据库设计是理解整个系统的关键。以最常用的请假流程为例其状态流转不靠Java枚举硬编码而是通过三张表联动表名核心字段作用sys_workflow_definitionid,code,name,xml_content存储流程定义XML如leave-process-v2.xmlsys_workflow_instanceid,definition_id,status,current_node_code,variables_json实例主表status字段值为RUNNING/COMPLETED/TERMINATED不存具体审批人IDsys_workflow_taskid,instance_id,node_code,assignee_id,status,comment任务明细表每次审批生成一条记录assignee_id指向sys_user.id关键逻辑当用户提交请假申请时系统解析leave-process-v2.xml根据sequenceFlow节点的conditionExpression如${user.department.leaderStatus ONLINE}动态计算出第一个审批节点并向sys_workflow_task插入记录。后续所有跳转都基于此表的status变更触发而非修改sys_workflow_instance.current_node_code——这保证了同一实例可被多人并行审批如会签场景。3. 启动与运行三步跑通绕过90%新手卡点3.1 环境准备JDK 11 MySQL 5.7 是唯一验证组合项目明确要求JDK 11pom.xml中java.version11/java.version因为oa-doc模块使用了java.nio.file.Files.writeString()JDK 11新增API处理临时文件。MySQL必须是5.7版本非8.0因sys_workflow_instance.variables_json字段类型为TEXT而MySQL 8.0默认sql_mode包含STRICT_TRANS_TABLES会导致JSON字符串中存在反斜杠时插入失败。安装步骤# 1. 创建数据库字符集必须为utf8mb4 mysql -u root -p -e CREATE DATABASE oa_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; # 2. 执行初始化SQL路径oa-parent/sql/oa_init.sql mysql -u root -p oa_db oa-parent/sql/oa_init.sql # 3. 修改数据库连接配置oa-core/src/main/resources/application.yml spring: datasource: url: jdbc:mysql://localhost:3306/oa_db?useUnicodetruecharacterEncodingutf8mb4serverTimezoneAsia/Shanghai username: root password: your_password3.2 启动命令别直接mvn spring-boot:run根目录下执行# 先编译所有模块跳过测试避免H2数据库冲突 mvn clean compile -Dmaven.test.skiptrue # 再启动核心模块注意不是启动oa-parent而是oa-core cd oa-core mvn spring-boot:run为什么不能直接mvn spring-boot:run在根目录因为oa-parent是pom类型模块没有main方法。若误操作Maven会报错The POM for ... is missing, no dependency information available——这是新手最高频的启动失败原因。3.3 首次访问登录后立即验证审批流是否生效启动成功后访问http://localhost:8080/login使用默认账号用户名admin密码123456登录后点击顶部菜单【流程中心】→【新建请假】填写表单并提交。此时检查数据库-- 查看流程实例是否创建 SELECT id, status, current_node_code FROM sys_workflow_instance WHERE business_key LIKE leave_%; -- 查看审批任务是否生成应有一条statusASSIGNED的记录 SELECT id, assignee_id, status FROM sys_workflow_task WHERE instance_id 刚查到的id;若查询结果符合预期实例状态为RUNNING任务状态为ASSIGNED说明审批引擎已激活——接下来就可以改代码了。4. 核心模块代码拆解一个请假审批请求从HTTP到数据库的完整链路4.1 控制器层LeaveController.submit()如何触发流程引擎oa-core模块的com.oa.controller.LeaveController中提交请假的入口方法如下PostMapping(/leave/submit) public Result submit(RequestBody Valid LeaveSubmitDTO dto, HttpServletRequest request) { // 1. 从Session获取当前用户非JWT项目用传统Session User currentUser (User) request.getSession().getAttribute(currentUser); // 2. 构建流程变量关键variables必须是MapString, Object MapString, Object variables new HashMap(); variables.put(userId, currentUser.getId()); variables.put(leaveDays, dto.getDays()); variables.put(reason, dto.getReason()); variables.put(startDate, dto.getStartDate()); // 3. 调用workflow-service的门面方法非直接new WorkflowEngine String processInstanceId workflowService.startProcess( leave-process-v2, // 流程定义KEY对应sys_workflow_definition.code currentUser.getId().toString(), variables ); return Result.success(processInstanceId); }逻辑说明workflowService.startProcess()是oa-workflow模块提供的服务门面它内部会① 从sys_workflow_definition查出leave-process-v2的XML定义② 解析XML获取首节点③ 调用Activiti的runtimeService.startProcessInstanceByKey()④ 将生成的ProcessInstance信息存入sys_workflow_instance表。注意variables中的key必须与XML中userTask节点的assignee表达式匹配如XML中写${userId}则map里必须有userId键。4.2 审批引擎WorkflowEngine.executeTask()里的动态路由oa-workflow模块的WorkflowEngine类中审批动作的核心逻辑在executeTask()方法public void executeTask(String taskId, MapString, Object variables) { // 1. 先完成当前任务Activiti原生API taskService.complete(taskId, variables); // 2. 查询流程实例当前状态 ProcessInstance pi runtimeService.createProcessInstanceQuery() .processInstanceId(getProcessInstanceIdByTaskId(taskId)) .singleResult(); // 3. 关键根据流程定义XML计算下一节点非硬编码 String nextNodeCode getNextNodeCode(pi.getProcessDefinitionId(), pi.getActivityId(), variables); // 4. 如果下一节点是用户任务则创建新任务 if (userTask.equals(getNodeType(nextNodeCode))) { String assigneeId resolveAssignee(nextNodeCode, variables); // 动态计算审批人 taskService.addCandidateUser(taskId, assigneeId); // 注意此处taskId是新任务ID } }参数说明resolveAssignee()方法会依次检查① XML中userTask节点的assignee属性如assignee${department.leaderId}② 若为空则查sys_workflow_node_config表中该节点的默认审批人③ 最终兜底到sys_department表中部门负责人的leader_id字段。这种三级 fallback 机制让“部门负责人离职时自动转给副职”成为可能。4.3 文档预览DocPreviewController.preview()如何绕过浏览器兼容性雷区oa-doc模块的文档预览功能不依赖第三方SaaS而是本地调用LibreOffice转换GetMapping(/doc/preview/{fileId}) public ResponseEntityResource preview(PathVariable String fileId) { // 1. 从数据库查文件元信息含原始文件名、存储路径 DocFile docFile docFileMapper.selectById(fileId); // 2. 生成唯一预览缓存路径避免并发转换冲突 String cacheKey DigestUtils.md5Hex(docFile.getOriginalName() docFile.getUploadTime()); String pdfPath /tmp/oa-preview/ cacheKey .pdf; // 3. 检查PDF缓存是否存在不存在则触发转换 if (!Files.exists(Paths.get(pdfPath))) { libreOfficeConverter.convertToPdf( docFile.getStoragePath(), // 原始文件绝对路径如 /data/uploads/2023/leave.docx pdfPath ); } // 4. 返回PDF流关键设置Content-Disposition防止中文文件名乱码 Resource resource new UrlResource(Paths.get(pdfPath)); return ResponseEntity.ok() .header(HttpHeaders.CONTENT_DISPOSITION, inline; filename\ URLEncoder.encode(预览文档.pdf, UTF-8) \) .body(resource); }血泪经验libreOfficeConverter.convertToPdf()底层调用soffice --headless --convert-to pdf命令必须确保服务器已安装LibreOffice 7.06.x版本对.docx表格支持不全。若返回空白PDF先检查/tmp/oa-preview/目录权限是否为755再执行soffice --version验证安装。5. 避坑指南那些让我重启三次Tomcat才定位到的致命问题5.1 现象提交请假后页面卡在“加载中”控制台无报错数据库sys_workflow_task无新记录原因application.yml中spring.activiti.database-schema-update被设为false但首次启动时Activiti所需的23张表未自动创建项目未提供activiti.mysql.create.sql脚本。解决手动执行oa-parent/sql/activiti-mysql-create.sql注意此文件在oa-workflow模块的src/main/resources目录下非根目录SQL文件夹。5.2 现象审批人收到待办点击“同意”后流程停滞sys_workflow_instance.status仍为RUNNING原因LeaveController.approve()方法中调用workflowService.executeTask()时传入的taskId是前端JS拼接的字符串但数据库中TASK_ID_字段是VARCHAR(64)而Activiti生成的ID含下划线如123456:abc_defMySQL严格模式下WHERE task_id 123456:abc_def会因字符集问题匹配失败。解决在workflowService.executeTask()开头添加日志log.info(Executing task: {}, taskId);对比日志输出与数据库ACT_RU_TASK.TASK_ID_字段值若发现日志中taskId末尾多出空格需在前端JS中加taskId.trim()。5.3 现象上传Word文档后预览报错java.lang.UnsatisfiedLinkError: Cant load library: /usr/lib/libjnisystem.so原因oa-doc模块依赖的jodconverter-core4.4.5版本与JDK 11不兼容其libjnisystem.so是为JDK 8编译的。解决将pom.xml中jodconverter-core版本升级至4.4.7并添加JDK 11专用依赖dependency groupIdorg.jodconverter/groupId artifactIdjodconverter-local/artifactId version4.4.7/version /dependency5.4 现象修改leave-process-v2.xml后重启服务新流程不生效原因Activiti默认将流程定义缓存在内存中sys_workflow_definition.xml_content字段更新后引擎仍读取旧缓存。解决在oa-workflow模块的WorkflowConfig.java中找到Bean注解的ProcessEngineConfiguration配置添加Bean public ProcessEngineConfiguration processEngineConfiguration() { StandaloneProcessEngineConfiguration config new StandaloneProcessEngineConfiguration(); config.setDatabaseSchemaUpdate(true); // 改为true强制刷新缓存 config.setDeploymentCacheEnabled(false); // 关键禁用部署缓存 return config; }5.5 现象报表导出Excel时中文全部显示为方框原因jasperreports默认字体为DejaVu Sans该字体不包含中文字符且项目未配置中文字体。解决在oa-report模块的src/main/resources/jasperreports_extension.properties中添加net.sf.jasperreports.extension.registry.factory.fontsnet.sf.jasperreports.engine.fonts.SimpleFontExtensionsRegistryFactory net.sf.jasperreports.extension.simple.font.families.arialfonts/fonts.xml并在src/main/resources/fonts/目录下放入fonts.xml定义SimSun字体路径和simsun.ttc字体文件。6. 二次开发实战把“部门负责人审批”改成“按职级自动匹配审批链”6.1 需求场景某公司规定“请假≥3天需经部门负责人分管副总双签”但当前流程只能配置固定节点现有leave-process-v2.xml中审批节点写死为userTask iddeptLeaderTask name部门负责人审批 activiti:assignee${department.leaderId} / userTask idvpTask name分管副总审批 activiti:assignee${company.vpId} /这导致所有请假都走相同路径无法根据leaveDays动态决定是否需要副总审批。6.2 改造方案用Activiti的executionListener注入动态逻辑第一步在leave-process-v2.xml的开始事件后添加监听器startEvent idstart name开始 extensionElements activiti:executionListener eventstart classcom.oa.workflow.listener.LeaveStartListener / /extensionElements /startEvent第二步编写LeaveStartListener放在oa-workflow模块public class LeaveStartListener implements ExecutionListener { Override public void notify(DelegateExecution execution) throws Exception { // 1. 获取流程变量中的请假天数 Integer days (Integer) execution.getVariable(leaveDays); // 2. 根据天数设置不同审批路径标志 if (days ! null days 3) { execution.setVariable(needVpApproval, true); } else { execution.setVariable(needVpApproval, false); } } }第三步修改XML中的网关分支exclusiveGateway idapprovalGateway name审批网关 / sequenceFlow idtoDeptLeader sourceRefapprovalGateway targetRefdeptLeaderTask conditionExpression${!needVpApproval}/conditionExpression /sequenceFlow sequenceFlow idtoVp sourceRefapprovalGateway targetRefvpTask conditionExpression${needVpApproval}/conditionExpression /sequenceFlow6.3 验证方法用Postman模拟不同天数提交构造两个请求验证分支逻辑# 请求1请假2天应只到部门负责人 curl -X POST http://localhost:8080/leave/submit \ -H Content-Type: application/json \ -d {days:2,reason:事假,startDate:2023-10-01} # 请求2请假5天应到副总 curl -X POST http://localhost:8080/leave/submit \ -H Content-Type: application/json \ -d {days:5,reason:事假,startDate:2023-10-01}然后分别查sys_workflow_task表确认node_code字段值是否为deptLeaderTask或vpTask。6.4 进阶技巧审批人动态计算函数注册到Activiti若resolveAssignee()逻辑复杂如“职级≥P7的员工请假需CTO终审”可将计算函数注册为Activiti的全局函数// 在WorkflowConfig.java中 Bean public ProcessEngineConfiguration processEngineConfiguration() { StandaloneProcessEngineConfiguration config new StandaloneProcessEngineConfiguration(); // ...其他配置 config.setBeans(Collections.singletonMap(approvalResolver, new ApprovalResolver())); // 注册为beans return config; }然后在XML中直接调用userTask idctoTask nameCTO终审 activiti:assignee${approvalResolver.resolveCtoForSenior(user)} /从那以后我每次改流程定义XML都强制走一遍mvn clean compile再启动因为Activiti的缓存机制太玄学——哪怕只是改了个空格也可能让新逻辑静默失效。希望帮到你。本文还有配套的精品资源点击获取