ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

JAVA办公管理系统实战:Spring Boot与Flowable流程引擎落地指南

JAVA办公管理系统实战:Spring Boot与Flowable流程引擎落地指南

简介:这是一套基于SSM框架的Java办公管理系统(OA)完整源码,面向具备Java Web基础、希望学习企业级项目架构或进行二次开发的开发者。系统涵盖用户管理、工作流审批、文档管理、通知公告、任务与会议管理等核心模块,采用Spring、SpringMVC与MyBatis组合,实现控制反转、MVC分层与SQL解耦,适合作为课程设计、毕业设计或企业信息化改造的参考蓝本。压缩包共1160个文件,约6.95MB,其中333个java源文件与365个class文件构成业务与框架主体,179个js、50个jsp及18个css负责前端交互与页面渲染,另有35个xml、8个properties等配置资源,结构完整便于按模块研读。目前已有2370人学习下载。借助清晰的目录结构与分层设计,读者可快速理解SSM整合思路、掌握审批流程与权限控制的实现方式,并在此基础上扩展CRM、HRM等模块或进行性能调优与界面定制。

1. 从一张请假单说起:JAVA办公管理系统到底在管什么

很多团队第一次动 OA 的念头,都是从一张请假单开始的。纸质单子要跑三个楼层签字,审批人出差就卡住,月底行政翻箱倒柜找存根。JAVA办公管理系统(OA)要解决的正是这类"流程在纸上、状态在人脑里"的问题:把请假、报销、用章、采购这些日常审批搬到线上,让每一步流转有记录、有提醒、可追溯。它适合两类人——一类是中小团队里被行政流程拖住的技术负责人,想自己搭一套贴合业务的系统;另一类是刚接触企业级开发的工程师,想找一个覆盖权限、流程、消息、报表的完整练手项目。这套系统通常基于 Spring Boot 起步,前端配 Vue 或 Thymeleaf,数据库用 MySQL,核心难点不在增删改查,而在流程引擎和权限模型的落地。

2. 技术选型与模块拆分:为什么这套组合最稳

2.1 后端为什么优先 Spring Boot 而不是传统 SSH

十年前做 OA,主流是 Struts2 + Spring + Hibernate 这套 SSH 组合,配置全写在 XML 里,一个applicationContext.xml能写到上千行。现在再上手,我一般直接推 Spring Boot,理由很实在:内嵌 Tomcat 省掉单独部署容器的步骤,starter 依赖把版本冲突挡在门外,application.yml里几行就能配好数据源和连接池。对 OA 这种模块多、迭代频繁的系统,自动装配带来的启动速度优势很明显。

选型时还要考虑团队熟悉度。如果组里有人只会写 Servlet,硬上 Spring Cloud 微服务就是给自己挖坑。OA 的并发量通常不高,单体应用加模块化分包完全够用,等真到了需要拆分的规模再拆也不迟。常见做法是按system(用户、角色、菜单)、workflow(流程定义、实例、任务)、oa(请假、报销、公告)三个包分层,每个包内部再分 controller、service、mapper。

2.2 权限模型:RBAC 三张表怎么落地

OA 的权限不是简单的"登录就能看",而是"谁能看哪个菜单、谁能审哪个节点"。标准做法是 RBAC(基于角色的访问控制),核心三张表:用户表、角色表、用户角色关联表,再配菜单表和角色菜单关联表。

-- 用户表 CREATE TABLE sys_user ( user_id BIGINT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(32) NOT NULL UNIQUE COMMENT '登录名', password VARCHAR(100) NOT NULL COMMENT 'BCrypt加密后的密码', dept_id BIGINT COMMENT '所属部门', status TINYINT DEFAULT 1 COMMENT '1正常 0停用' ); -- 角色表 CREATE TABLE sys_role ( role_id BIGINT PRIMARY KEY AUTO_INCREMENT, role_key VARCHAR(50) NOT NULL UNIQUE COMMENT '如 oa:leave:approve', role_name VARCHAR(50) NOT NULL ); -- 用户角色关联 CREATE TABLE sys_user_role ( user_id BIGINT NOT NULL, role_id BIGINT NOT NULL, PRIMARY KEY (user_id, role_id) );

这里有个容易忽略的点:role_key建议用冒号分隔的权限标识,而不是中文角色名。因为前端按钮的显隐、后端接口的拦截都靠这个字符串匹配,中文一旦改名,所有引用点全崩。密码字段务必用 BCrypt 存哈希,别图省事存 MD5,彩虹表一撞就出来。

2.3 流程引擎:自研状态机还是上 Activiti/Flowable

这是 OA 选型里分歧最大的地方。我的经验是分两种情况:如果流程固定、节点不超过五个(比如请假就"提交→主管审→HR备案"),自研一个状态机反而更轻,用一张leave_apply表加status字段就能跑,改起来也快。但如果流程要支持会签、驳回重提、动态加签、条件分支,那就别硬扛,直接上 Flowable。

Flowable 的核心概念是 BPMN 2.0 流程图,用 XML 描述节点和连线。下面是一个最简请假流程的定义片段:

<process id="leaveProcess" name="请假流程"> <startEvent id="start"/> <userTask id="deptApprove" name="部门主管审批" flowable:assignee="${deptLeader}"/> <exclusiveGateway id="gw"/> <sequenceFlow sourceRef="deptApprove" targetRef="gw"/> <sequenceFlow sourceRef="gw" targetRef="hrRecord"> <conditionExpression xsi:type="tFormalExpression"> ${approved == true} </conditionExpression> </sequenceFlow> <userTask id="hrRecord" name="HR备案"/> <endEvent id="end"/> </process>

assignee用表达式${deptLeader}而不是写死人名,这样流程定义和具体人员解耦,换人不用改流程图。conditionExpression里的approved是任务完成时传进去的流程变量。要注意 Flowable 默认会建二十多张ACT_开头的表,生产环境记得单独建库,别和业务表混在一起,否则备份和迁移会很痛苦。

3. 从零跑通一个请假审批:代码与配置

3.1 环境准备与依赖引入

先把地基打好。JDK 用 17(Spring Boot 3.x 的最低要求),MySQL 8.0,Maven 3.8 以上。pom.xml里除了spring-boot-starter-web,还要加 MyBatis-Plus、Flowable 和连接池:

<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.5</version> </dependency> <dependency> <groupId>org.flowable</groupId> <artifactId>flowable-spring-boot-starter</artifactId> <version>7.0.1</version> </dependency> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency> </dependencies>

版本号这里要留个心眼:Flowable 7.x 要求 Spring Boot 3.x,如果你项目还在 Spring Boot 2.7,就得降到 Flowable 6.8。两者 API 有差异,混用会报NoSuchMethodError,这种错排查起来很费时间。

3.2 数据源与流程引擎配置

application.yml里把数据源和 Flowable 的建表策略配好:

spring: datasource: url: jdbc:mysql://localhost:3306/oa_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: oa_user password: your_password driver-class-name: com.mysql.cj.jdbc.Driver flowable: database-schema-update: true # 首次启动自动建表,生产建议改 false async-executor-activate: false # 小团队关掉异步,省线程 history-level: full # 记录完整历史,便于追溯

database-schema-update: true只在开发环境用,它会在启动时检查并创建缺失的表。生产环境一定要改成false,否则某次升级可能悄悄改表结构。history-level设成full会多存历史明细,磁盘占用大一些,但 OA 场景经常要查"谁在什么时候改了什么",这个代价值得。

3.3 提交请假单的接口实现

核心逻辑分两步:先落业务表,再启动流程实例。

@RestController @RequestMapping("/oa/leave") public class LeaveController { @Autowired private LeaveService leaveService; @PostMapping("/submit") public Result submit(@RequestBody LeaveForm form) { // 1. 校验请假天数与起止时间是否匹配 if (form.getDays() <= 0) { return Result.fail("请假天数必须大于0"); } // 2. 保存业务数据,拿到主键 LeaveApply apply = leaveService.saveApply(form); // 3. 启动流程,把业务主键塞进流程变量 leaveService.startProcess(apply.getId(), form.getDeptLeader()); return Result.ok(apply.getId()); } }

Service 层启动流程的关键代码:

public void startProcess(Long applyId, String deptLeader) { Map<String, Object> vars = new HashMap<>(); vars.put("deptLeader", deptLeader); // 对应 BPMN 里的 ${deptLeader} vars.put("applyId", applyId); // 用业务主键作为 businessKey,方便反查 runtimeService.startProcessInstanceByKey( "leaveProcess", String.valueOf(applyId), vars); }

businessKey传业务主键是血泪经验。早期我没传,后来要查"这张单子走到哪了",只能靠流程变量里的applyId去ACT_RU_VARIABLE表里捞,SQL 又长又慢。传了businessKey之后,一句runtimeService.createProcessInstanceQuery().processInstanceBusinessKey(id)就搞定。

3.4 审批任务的查询与完成

审批人登录后要看到待办列表,处理时完成任务并推进流程:

// 查询某人的待办 List<Task> tasks = taskService.createTaskQuery() .taskAssignee(currentUser) .orderByTaskCreateTime().desc() .list(); // 完成审批,approved 决定流程走向 Map<String, Object> vars = new HashMap<>(); vars.put("approved", true); vars.put("comment", "同意"); taskService.complete(taskId, vars);

taskAssignee要和流程定义里的assignee表达式对得上。如果流程里写的是${deptLeader},而启动时没传这个变量,任务会创建失败并抛FlowableException。排查时先看ACT_RU_TASK表里有没有这条任务,没有就是变量没传进去。

4. 避坑与排查:五个真实踩过的坑

4.1 流程变量传了对象,序列化直接报错

现象:启动流程时报Cannot serialize object,堆栈指向SerializableType。原因是往流程变量里塞了一个没实现Serializable接口的自定义对象。Flowable 默认把变量序列化后存进ACT_RU_VARIABLE的TEXT_字段。解决办法是只传基本类型和字符串,复杂数据存业务表,流程里只放主键。我现在的习惯是流程变量里永远只有 Long、String、Boolean 三种。

4.2 待办列表查不到,其实是候选人没配

现象:任务创建成功,但审批人登录后待办为空。原因多半是流程里用了candidateUsers而不是assignee,而查询时用的是taskAssignee。候选人任务要用taskCandidateUser(user)查,两者不能混。解决方式是统一规范:单人审批用assignee,多人抢单用candidateUsers,查询接口按类型分开写。

4.3 中文乱码,从数据库一路查到连接串

现象:请假事由存进去变成问号。排查顺序是:先看数据库字符集是不是utf8mb4,再看连接串有没有characterEncoding=utf8,最后看表字段是不是utf8mb4_general_ci。三处有一处不对就会乱码。我一般建库时直接CREATE DATABASE oa_db DEFAULT CHARACTER SET utf8mb4,省得后面改。

4.4 权限拦截失效,接口裸奔

现象:菜单藏起来了,但直接调接口还能拿到数据。原因是只做了前端按钮控制,后端没加校验。解决是在 Spring Security 或拦截器里按role_key校验,比如@PreAuthorize("hasAuthority('oa:leave:approve')")。前端隐藏只是体验优化,后端拦截才是安全底线,两者都要做。

4.5 流程版本升级后,老实例走不动

现象:改了 BPMN 文件重新部署,正在运行的老流程实例报节点找不到。原因是 Flowable 默认按 key 部署新版本,老实例还绑在旧版本定义上。解决是部署时用deploymentBuilder.key()显式指定,或者对老实例做迁移。生产环境改流程前,一定要先确认没有在途实例,或者准备好迁移脚本。

5. 进阶技巧:让 OA 从能用变成好用

系统跑起来只是及格线,真正拉开差距的是细节。分享几个我反复用到的技巧。

第一个是审批意见的留痕。别只存一个"同意/驳回",把comment连同操作人、时间、任务节点一起写进业务日志表。OA 的价值一半在流程,一半在追溯,出了纠纷能翻出完整链路,这才是它比纸质单强的地方。

第二个是消息提醒的降噪。早期我做了邮件加站内信双通道,结果审批人一天收几十封,直接屏蔽。后来改成:普通节点只发站内信,超时未处理才补一封邮件,紧急流程才发短信。判断"紧急"用流程变量里的优先级字段,而不是无差别群发。

第三个是待办列表的分页与索引。ACT_RU_TASK表在任务量大时会成为瓶颈,查询待办务必带taskAssignee条件并加索引。我一般会在ASSIGNEE_字段上建索引,同时限制单次查询返回条数,前端做滚动加载而不是一次拉全量。

第四个是流程定义的版本管理。每次改 BPMN 都提交到 Git,部署时记录版本号和变更说明。有次线上流程卡住,靠对比两个版本的 XML 差异,十分钟定位到是某个连线条件写反了。没有版本记录的话,这种问题能查一整天。

最后说个验证方法:写一个集成测试,用 Flowable 的内存引擎跑完整流程,从提交到归档断言每一步的任务名和状态。这样每次改流程定义,跑一遍测试就知道有没有破坏原有逻辑,比手工点页面靠谱得多。

我自己踩过最深的坑,是早期图省事把流程变量当数据库用,什么都往里塞,结果一次序列化失败导致整个流程实例卡死,只能手工改表恢复。从那以后我定了个规矩:流程变量只放主键和开关,业务数据一律进业务表。这个习惯帮我省下了无数次深夜排查。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进