ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

开源SpringBoot OA落地方案:选型、审批状态机与权限设计

2026/9/17 2:32:17 拓冰建站 浏览量
开源SpringBoot OA落地方案:选型、审批状态机与权限设计 简介这套SpringBoot JAVA开源OA协同办公系统面向企业信息化建设者与Java开发人员聚焦办公审批、流程管理、权限管控等常见需求基于JDK8SpringBootMyBatisRedisDruidBeetlShiro技术栈构建自研工作流引擎支持可视化表单与流程设计并满足分布式部署及国产数据库适配。资源共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 groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.7/version /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-redis/artifactId /dependency这段依赖是典型 OA 后端的底座MyBatis-Plus 负责 ORMMySQL 驱动负责存储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?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/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.activedev启动日志里看到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 token3.3 启动阶段最常见的三个报错一是Invalid bound statement说明 MyBatis 的 Mapper XML 没有被扫描到检查mapper-locations是否匹配实际路径二是Table doesnt 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 MapString, SetString 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) { SetString 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并处理好和业务表的事务边界。常见做法是业务表保存流程实例 IDFlowable 负责状态推进业务表和引擎表之间不直接外键关联靠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); }这段配置要说清楚三层意思一是STATELESSOA 前端是前后端分离的话后端不做 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 系统最常见的数据库压力来自代办角标的轮询。每个用户一进首页就查一次「我名下的待办数量」几百人同时操作数据库就打满了。把待办聚合数放进 Rediskey 设计成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默认关闭。这样实施时可以根据客户的响应速度逐步开启不会一上线就把所有人的邮箱炸掉。本文还有配套的精品资源点击获取