简介:这套SpringBoot JAVA开源OA协同办公系统,面向企业信息化建设者与Java开发人员,聚焦办公审批、流程管理、权限管控等常见需求,基于JDK8+SpringBoot+MyBatis+Redis+Druid+Beetl+Shiro技术栈构建,自研工作流引擎支持可视化表单与流程设计,并满足分布式部署及国产数据库适配。资源共2000个文件,以java后端源码、js前端交互脚本、html页面模板、css样式、xml配置文件及png图片资源为主,整体约74.98MB,目录结构清晰便于理解前后端分层与模块化开发。目前已有2464人学习使用,适合需要快速搭建企业级OA或研究多人协同审批场景的开发者。包内除了完整可运行源码,还包含权限控制到页面/接口/数据操作的设计思想、多数据库适配方案、JasperReport报表集成示范及丰富的表单流程示例,可供二次开发与源码研读直接参考。
1. 从「要个 OA」到「选个开源 SpringBoot OA」:第一步不是下载代码
很多团队在接到「弄个 OA」的需求时,第一个念头是去下载一套现成的开源项目。这没有错,但真正的坑往往不在下载,而在下载之后:代码跑不起来、权限模型和自家组织架构对不上、审批流改不动,最后又回到从零开发的起点。SpringBoot 生态里确实不缺开源 OA 系统——基于 Java 的、前后端分离的、内置工作流引擎的都有,但「能搜到」和「能落地」是两回事。这篇要讲的,是从选型、架构、核心审批状态机、权限流程到上线前优化的完整思路,目标不是让你背参数,而是让你拿到任何一个开源 SpringBoot OA 项目时,知道先看哪里、先改哪里、先测哪里。适合正在选型的技术负责人,也适合要接手二次开发的 Java 工程师。以下所有方案,都是我在类似项目里会直接采用的落地路径。
2. 先把技术选型定稳:SpringBoot OA 的分层与模块边界
2.1 为什么 OA 适合落在 SpringBoot 而不是微服务
OA 系统的典型特征是:单机部署多、并发峰值不高、逻辑集中在审批流和权限模型上,业务边界远没有电商系统那么清晰。这时候上微服务,等于用分布式的复杂度换一个并不存在的伸缩性需求,往往得不偿失。SpringBoot 的单体应用形态反而更合适——一个可执行 Jar 包、一套数据库、一个 Redis,就能服务几百人。
从工程角度讲,SpringBoot 的开源 OA 项目一般都把模块拆成三个层面:基础设施层(数据库、缓存、消息)、业务领域层(用户、组织、审批、考勤、日程)、接入层(REST API、定时任务、消息消费者)。这种拆分不是微服务,是「模块化单体」,好处是开发时隔离性好,部署时不用处理分布式事务。
我一般会建议在选型时先看三个能力点:有没有现成的 RBAC 权限模型、流程引擎用的是自研状态机还是 Flowable/Activiti、是否支持表单的动态渲染。这三个点决定了二次开发的核心成本,其他功能都是围绕它们长出来的枝叶。
2.2 依赖选择:持久层、流程引擎与权限框架
2.2.1 持久层:MyBatis-Plus 与 JPA 的取舍
开源 OA 项目里,MyBatis-Plus 出现频率明显更高,原因是 OA 的报表和自定义查询多,SQL 需要细粒度控制;MyBatis-Plus 的LambdaQueryWrapper又能在不写 XML 的情况下完成大部分 CRUD。JPA 的优势在关联模型复杂时体现得更好,比如组织架构的多级嵌套,但 OA 里这类查询往往是只读的,用 MyBatis 写个递归 SQL 反而更直观。
如果项目已经用 JPA 做了,没必要推翻重来;如果是新建项目,优先选 MyBatis-Plus。一个可参考的依赖组合是:
<dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.7</version> </dependency> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>这段依赖是典型 OA 后端的底座:MyBatis-Plus 负责 ORM,MySQL 驱动负责存储,Redis 用来做缓存和待办数据的热存储。版本号以你实际拉取的 SpringBoot 版本为准,不要盲目追新,3.x 的 SpringBoot 对应 MyBatis-Plus 3.5.x 即可。
2.2.2 流程引擎:自研状态机与 Flowable 的界限
这是选型里最容易被忽略的一环。不要一上来就引入 Flowable,先问一个问题:你的审批流是「固定路径」还是「动态路由」?如果只是请假、报销、用章申请,走固定层级审批,自研状态机完全够用;如果涉及会签、或签、条件分支、动态加签,那才需要工作流引擎。
Flowable 的学习成本不低,光 BPMN 2.0 的 XML 配置就够团队适应一阵子。很多开源 OA 项目干脆做了一套轻量级状态机,把审批节点存在数据库表里,配合一张流程实例表来驱动流转。这种方案的优势是业务人员能看懂,排查问题不需要打开一堆流程图的 XML。
2.3 一个可落地的六层工程结构
com.company.oa ├── common // 通用工具、常量、异常、枚举 ├── config // SpringBoot 配置类(Security、Redis、MyBatis) ├── framework // 切面、拦截器、注解 ├── module │ ├── system // 用户、角色、菜单、部门 │ ├── process // 审批流程、实例、任务 │ ├── form // 动态表单定义与实例 │ └── business // 具体业务(请假、报销等) ├── quartz // 定时任务(超时提醒、数据统计) └── web // Controller、VO、DTO这个结构的价值在边界清晰:system模块是权限底座,process模块是审批核心,form模块负责把前端动态表单配置映射为可存储的 JSON 结构。业务模块不直接操作状态机,而是调用process模块的 API,这样换流程引擎时只需要动process内部。
3. 用最小命令跑通 SpringBoot OA 服务端
3.1 后端启动前的三项准备
3.1.1 建库与初始化 SQL
先建数据库,字符集选utf8mb4,排序规则选utf8mb4_general_ci。注意不要用utf8——OA 系统里员工的签名、附件文件名、审批意见很可能包含 Emoji 字符,utf8在 MySQL 里存不下四个字节的字符,等出现乱码再改字符集就麻烦了。
CREATE DATABASE oa_system DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;然后导入项目自带的sql/init.sql和sql/data.sql。这一步我踩过坑:很多开源项目把表结构、菜单数据、初始管理员账号拆在三个文件里,顺序错了会报外键错误。稳妥的顺序是:结构 → 基础数据 → 菜单权限数据。
3.1.2 配置文件的必调参数
spring: datasource: url: jdbc:mysql://localhost:3306/oa_system?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: your_password driver-class-name: com.mysql.cj.jdbc.Driver data: redis: host: localhost port: 6379 database: 0 password: mybatis-plus: mapper-locations: classpath*:mapper/**/*.xml configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl map-underscore-to-camel-case: true server: port: 8080参数说明:serverTimezone必须设,否则 MySQL 8.x 连接会报时区错误;map-underscore-to-camel-case让数据库的user_name自动映射到 Java 的userName,这是大多数开源 OA 的约定。log-impl在开发期打开,方便直接看到 SQL 和参数,上线前改成Slf4jImpl或直接移除,避免日志刷屏。
提示:spring.data.redis是 SpringBoot 3.x 的写法,2.x 版本写的是spring.redis。如果启动报RedisConnectionFactory相关错误,先检查这个路径。
3.2 启动与验证第一个接口
mvn clean package -DskipTests java -jar target/oa-system.jar --spring.profiles.active=dev启动日志里看到Started OaApplication in 8.2 seconds后,先不要急着去点页面。验证两个接口:登录接口和当前用户信息接口,确认真实用户体系没有被代码里写死的假用户绕过。
curl -X POST http://localhost:8080/login \ -H "Content-Type: application/json" \ -d '{"username":"admin","password":"admin123"}'返回的 JSON 里应包含token字段,拿到 token 后再请求用户信息接口:
curl http://localhost:8080/system/user/info \ -H "Authorization: Bearer <token>"3.3 启动阶段最常见的三个报错
一是Invalid bound statement,说明 MyBatis 的 Mapper XML 没有被扫描到,检查mapper-locations是否匹配实际路径;二是Table doesn't exist,说明 SQL 初始化没执行;三是Unable to connect to Redis,本地先启动一个 Redis,或者把配置里 Redis 相关依赖暂时注释掉——很多项目的启动类上有@EnableCaching,没有 Redis 会直接启动失败。
4. 核心业务:OA 审批状态机与流程引擎的选择
4.1 审批流本质上是状态机
记一个反直觉的结论:大多数 OA 的审批流不需要 BPMN 引擎。审批的实质就是一张审批单在不同节点间的状态迁移,每个迁移有前置条件和操作者角色。做成状态机,状态是有限集、事件是操作、条件是校验逻辑,模型足够稳定。只有当你需要自由编排节点(比如「先 A 审批,金额大于 5000 再加 B 会签」)时,才需要把路由规则做成可配置的数据。
常见开源 OA 项目里,「勾股 OA」「魔方 OA」这类系统对审批的处理也偏轻量级:核心是BpmDefine(流程定义)、BpmInstance(流程实例)、BpmTask(待办任务)三张表,外加一张用来记录意见和附件的BpmTaskOpinion。这套模型在中小团队内部足够跑顺。
4.2 状态机的表设计
CREATE TABLE `bpm_instance` ( `id` bigint NOT NULL AUTO_INCREMENT, `process_key` varchar(64) NOT NULL COMMENT '流程定义key', `business_key` varchar(64) NOT NULL COMMENT '业务表单id', `status` varchar(32) NOT NULL COMMENT '状态机当前状态', `current_node` varchar(64) NOT NULL, `creator_id` bigint NOT NULL, `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_business_key` (`business_key`) ); CREATE TABLE `bpm_task` ( `id` bigint NOT NULL AUTO_INCREMENT, `instance_id` bigint NOT NULL, `node_key` varchar(64) NOT NULL, `assignee_id` bigint NOT NULL, `status` varchar(16) NOT NULL COMMENT 'WAITING/DONE/CANCEL', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, `finish_time` datetime DEFAULT NULL, PRIMARY KEY (`id`), KEY `idx_assignee_status` (`assignee_id`, `status`) );两张表解释了 OA 审批的核心逻辑:bpm_instance存的是「这条流程现在到哪了」,bpm_task存的是「谁手上还有什么事」。status字段的值建议和 Java 枚举一一对应,不要用没有约束的字符串,否则写十个审批节点后,状态值的拼写错误就会让你崩溃。
4.3 用代码实现状态机流转
4.3.1 状态迁移的统一入口
public class ProcessStateMachine { private static final Map<String, Set<String>> TRANSITIONS = new HashMap<>(); static { // key: 当前状态 + 操作;value: 允许到达的下一状态 TRANSITIONS.put("DRAFT_SUBMIT", Set.of("APPROVING")); TRANSITIONS.put("APPROVING_APPROVE", Set.of("APPROVING", "APPROVED")); TRANSITIONS.put("APPROVING_REJECT", Set.of("REJECTED")); TRANSITIONS.put("APPROVING_WITHDRAW", Set.of("CANCELED")); } public static String nextState(String state, String event) { Set<String> nextStates = TRANSITIONS.get(state + "_" + event); if (nextStates == null) { throw new IllegalStateException("非法的状态迁移: " + state + " -> " + event); } if (nextStates.size() == 1) { return nextStates.iterator().next(); } // 状态有多个去向时,由业务规则决定,例如金额、部门层级 return RouteResolver.resolve(state, event, nextStates); } }逻辑说明:这个类是一个纯函数式的状态迁移校验器,不依赖 Spring 容器,可以单独写单元测试。nextState的入参是当前状态和操作名,返回下一个状态。这里的重点是「不存在合法迁移时直接抛异常」——这样前端无论怎么乱点,后端状态也不会被带偏。RouteResolver是路由决策器,负责在多个可能目标状态中按业务规则选择,比如金额超过阈值进入会签节点。
bpm_instance表里记录的current_node字段,要和状态机的状态解耦。状态是流程的生命周期(草稿、审批中、已通过),节点是当前处在审批链的哪一级(部门经理、总监、HR)。字段和状态机状态分开,才能在审批被驳回时准确知道退回哪个节点。
4.4 什么时候该换 Flowable
在上述状态机里,出现这几个信号就该考虑 Flowable:审批节点数超过五层,且每层还有分支;需要并行会签,会签人数不固定;需要按条件动态决定下一个节点;需要支持流程版本升级,正在跑的流程不受影响。
Flowable 的优势是 BPMN 2.0 标准化,repositoryService部署流程定义、runtimeService启动实例、taskService完成任务,这些 API 是稳定的。但代价是表结构多出几十张,你需要在 SpringBoot 里加flowable-spring-boot-starter并处理好和业务表的事务边界。常见做法是业务表保存流程实例 ID,Flowable 负责状态推进,业务表和引擎表之间不直接外键关联,靠processInstanceId关联。
5. 权限、菜单与动态流程的接入节奏
5.1 RBAC 与数据权限,别只做菜单权限
开源 OA 项目通常做两层权限:粗粒度是「谁能进这个菜单」,细粒度是「谁能看这一行数据」。很多系统第一层做得很好,第二层直接没有。做数据权限的关键,是把它做成框架级能力,而不是每个业务模块自己写判断逻辑。做法是定义数据权限的 SQL 片段,由 MyBatis 拦截器在查询时自动拼入。
常见的数据权限模型有五种:全部、本部门、本部门及以下、仅本人、按岗位。对应到具体语句,由当前登录用户的信息在前端传参或由后端根据 Token 解析,拦截器在 SQL 末尾追加AND dept_id IN (...)。
5.2 Spring Security + JWT 的上下文处理
5.2.1 登录认证的调用链
@Override protected void configure(HttpSecurity http) throws Exception { http.csrf().disable() .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .authorizeRequests() .antMatchers("/login", "/captcha", "/actuator/health").permitAll() .antMatchers("/system/**", "/process/**").authenticated() .anyRequest().authenticated() .and() .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); }这段配置要说清楚三层意思:一是STATELESS,OA 前端是前后端分离的话,后端不做 Session,每次请求带 JWT;二是放行路径,/login、验证码、健康检查必须放行,其他接口全部走认证;三是jwtAuthenticationFilter的位置,放在UsernamePasswordAuthenticationFilter之前,先从 Header 里取 token,解析出用户名和权限,再构造Authentication对象塞进SecurityContextHolder。
权限控制建议用@PreAuthorize("hasAuthority('system:user:add')")注解直接标在 Controller 方法上,并在启动类上加@EnableGlobalMethodSecurity(prePostEnabled = true)。这样可以做到菜单可不显示、接口也不可用的双重校验——只隐藏按钮不安全,接口没做权限才会漏数据。
5.3 表单与流程定义怎么「挂」在一起
动态流程不能把业务字段硬编码在流程代码里,常见做法是表单 JSON Schema 驱动。在bpm_define表里存一个form_schema字段,内容是 JSON,格式形如:
{ "fields": [ { "key": "leaveType", "label": "请假类型", "type": "select", "options": ["事假", "病假", "年假"] }, { "key": "days", "label": "天数", "type": "number" } ], "rules": { "days": { "required": true, "max": 30 } } }前端拿到这份 JSON 动态渲染表单,提交时把数据按 key 存储为form_dataJSON 字段。后端在提交事务里做两件事:保存form_data,创建bpm_instance。这样新增一种审批类型时,只需要在后台维护一份表单定义和一条流程链路的节点配置,不需要写新的业务类,也不需要改动审批的核心代码。
注意区分两个概念:业务权限和流程权限。前者是「你有没有权限提交报销单」,后者是「你提交后流程会走到谁那里」。两者不要混在一起设计,流程节点的审批人先从角色映射关系表里取,取不到再退回申请人并提示配置错误。
5.4 现有系统对接:单点登录与组织同步
很多 OA 项目不会独立存在,接的是企业微信、钉钉、飞书或公司内部的统一认证中心。这时候把认证方式做成可插拔接口,比直接改登录逻辑更稳。定义一个ThirdPartyAuthService接口,实现类里处理不同平台的回调、解码、用户映射,登录成功后走和普通密码登录一样的方式签发 JWT。组织架构同步则用定时任务拉取部门用户数据,和本地sys_user、sys_dept表做差异比对,不要做全量替换,避免把管理员手动维护的岗位信息冲掉。
6. 上线前需要核对的优化点与两个进阶技巧
6.1 限流与审计日志:先防住自己人
OA 是内网系统,但内网也有风险。至少要做两件事:一是登录接口限流,指定时间内连续失败超过 5 次就锁定该账号 15 分钟;二是敏感操作全量审计,谁在什么时候把审批单退回、修改了角色权限,都要落库。Audit 表至少要有operator_id、operation、target_id、detail、create_time五个字段,detail建议存操作前后关键字段的变化,排查扯皮问题时省力很多。
限流可以用 Redis 的INCR加EXPIRE实现,也可以用spring-boot-starter-aop配合一个自定义@RateLimit注解。注意别把限流做成针对单个接口的固定值,不同接口阈值差异很大——登录接口 3 次/分钟,列表查询接口 30 次/秒也没问题。
6.2 缓存策略:待办数是第一优先级
OA 系统最常见的数据库压力来自代办角标的轮询。每个用户一进首页就查一次「我名下的待办数量」,几百人同时操作,数据库就打满了。把待办聚合数放进 Redis,key 设计成oa:pending:count:{userId},过期时间 30 秒,这样数据库查询频率被降到原来的十分之一。
6.2.1 缓存更新时机
在bpm_task的状态变更处,同步删除对应用户的缓存。流程图如下:任务完成时删除当前处理人的缓存,流程流转到下一节点时删除下一处理人的缓存。这里不要用「定时全量刷新」的懒办法,权限变化的实时性不强可以接受,但待办数延迟 30 秒会直接被领导质问。
6.3 两个进阶技巧
6.3.1 技巧一:表单字段按条件显隐
表单 JSON Schema 里加一个visibleIf字段,值为一个字符串表达式,例如leaveType == "病假"。后端不解释这个表达式,下发 JSON 给前端,由前端的表单渲染引擎去解析,需要补充材料时才把对应字段展示出来。后端在提交时校验form_data和form_schema的字段一致性,要特别注意visibleIf没通过的字段,前端即使传了多余字段,后端也要主动忽略,而不是全量入库。
6.3.2 技巧二:流程超时自动提醒
OA 里最常见的抱怨是「审批卡在某人那里好几天」。实现方案是定时任务扫表:每分钟扫一遍bpm_task表,筛选status = 'WAITING'且create_time早于当前时间减去超时阈值的记录,按处理人分组,汇总后发站内信和邮件。注意不是每张待办任务发一条,否则一个积压 20 天任务的人会收到几十封邮件。另外,超时提醒记录表里要标记每次提醒的时间,避免重复发送。
我在代码里会把这个调度任务交给 Spring 的@Scheduled,加一个布尔型开关oa.process-timeout-reminder-enabled,默认关闭。这样实施时可以根据客户的响应速度逐步开启,不会一上线就把所有人的邮箱炸掉。
本文还有配套的精品资源,点击获取