简介:基于Java开发的OA办公审批系统源码包,内含项目详细说明,适合计算机相关专业学生用于毕业设计、课程设计,也可作为Java初学者或企业开发人员的项目参考。系统覆盖管理端与员工端,包含权限管理、审批管理、公众号菜单管理以及微信授权登录、消息推送等完整功能,后端采用SpringBoot、MyBatisPlus、SpringSecurity、Redis、Activiti与MySQL,前端使用vue-admin-template、Vue、ElementUI、Axios,代码按oa-parent、common、model、service-oa等模块清晰组织。压缩包共108个文件,主要由91个Java源码文件、12个XML文件(配置/映射)、2个YML配置文件、1个Markdown说明文档和1个架构示意图组成,整包仅106KB,便于快速下载部署查看。已有734人学习下载,适合在现有基础上扩展二次开发,能帮助理解OA系统全流程与主流技术栈的整合方式。
1. 基于 Java 的 OA 办公审批系统源码:为什么值得拆,拆完能带走什么
很多刚学完 Spring Boot 的人,简历上写“做过电商系统”写到没感觉了,却不知道企业中真正高频、高价值、面试也爱问的实战项目,恰恰是 OA 办公审批系统。把线下纸质审批搬上线,让请假单、报销单、用章申请都按既定的流程走——这就是 OA 办公审批系统源码 zip 里最核心的东西。它不是花架子:里面有流程引擎、权限模型、待办列表、审批记录,几乎把企业 Web 开发的常见难点都压进了一个项目里。适合两类人:一类是想找第一个完整项目的 Java 新人,另一类是想在企业内部实现流程数字化的 IT 运维。拆完这套源码,Spring Boot、MyBatis、MySQL、Redis 怎么配合,心里基本就有底了。
2. 审批系统的灵魂是流程引擎:先把六张表的血缘关系理清
2.1 别急着看代码,先盯着表结构看十分钟
拿到任何一份 OA 源码,我习惯先不看 controller 和 service,先把数据库脚本从头到尾过一遍。审批系统的表再多,核心逃不开这六张:流程定义表、流程节点表、审批单表、审批记录表、用户表、角色表。
流程定义表(通常叫process_def)存的是“审批流长什么样”:请假流程有几个节点、报销流程谁先审谁后审。流程节点表(process_node)定义每个节点的具体信息:节点名称、审批人角色、节点类型是单人审批还是会签。审批单表(approval_order)是业务数据,一条记录对应一张请假单或报销单。审批记录表(approval_record)则是操作日志,谁在什么时间点了同意还是驳回。
这三张表的关系是:流程定义是模板,审批单是实例,审批记录是轨迹。结构上就是两条外键链:
-- 流程定义表 CREATE TABLE process_def ( id BIGINT PRIMARY KEY AUTO_INCREMENT, def_key VARCHAR(64) NOT NULL COMMENT '流程唯一标识,如 leave', def_name VARCHAR(128) NOT NULL COMMENT '流程名称', version INT NOT NULL DEFAULT 1, node_json TEXT COMMENT '节点配置JSON,见process_node', enabled TINYINT DEFAULT 1 ); -- 流程节点表 CREATE TABLE process_node ( id BIGINT PRIMARY KEY AUTO_INCREMENT, def_id BIGINT NOT NULL, node_key VARCHAR(64) NOT NULL COMMENT '节点key,如 start/leader/hr', node_name VARCHAR(64) NOT NULL, node_type TINYINT COMMENT '1单人审批 2会签 3或签', approver_role VARCHAR(64) COMMENT '审批人角色', next_node_key VARCHAR(64) COMMENT '下一节点key', timeout_hours INT DEFAULT 48 COMMENT '限时审批小时数' );这里有个参数值得多说一句:next_node_key不是简单存一个字符串,而是你整个路由逻辑的地基。很多半成品源码把它硬编码在 Java 代码里,改一条流程要动代码重新部署;正规一点的会把路由表单独拆出来,但一般中小型项目用next_node_key这种链式字段就够用了。看源码时优先看这个字段是怎么维护的,能快速判断作者水平。
2.2 审批单的状态机:一个 status 字段的学问
审批单表里最重要的一列是status。看似只是一个 int,其实背后是一个状态机:待提交、审批中、已通过、已驳回、已撤回、已作废。很多人改 OA 源码第一件事就是加状态,结果把状态加到十个以上,后面所有查询的 where 条件全乱掉。
常见做法是只存主状态,把明细动作放审批记录表里。主状态的意义是给列表页和待办页用的,不是给审计用的。
public enum ApprovalStatus { DRAFT(0, "待提交"), RUNNING(1, "审批中"), APPROVED(2, "已通过"), REJECTED(3, "已驳回"), REVOKED(4, "已撤回"); private final int code; private final String desc; ApprovalStatus(int code, String desc) { this.code = code; this.desc = desc; } }状态流转的规则就三条:发起人提交时DRAFT → RUNNING;审批人同意且还有下一节点时,单子保持RUNNING,只是current_node变了;最后一个人同意才置为APPROVED,任何一个人驳回就置为REJECTED。写代码时最容易漏的是“撤回”只在DRAFT或第一个审批人尚未处理时允许,这个业务规则比状态本身难写。
2.3 最小流程引擎的路由:算下一个节点该找谁
流程引擎听起来高深,落到最小实现就是三步:找到当前节点 -> 判断当前节点是否处理完 -> 找到下一个节点。处理完的判断标准和节点类型绑定,单人节点是“这条审批记录存在且通过”,会签节点是“所有审批人都通过”,或签节点是“任意一个人通过”。
public NodeActionResult moveToNextNode(ApprovalOrder order) { ProcessNode curNode = getCurrentNode(order); // 当前节点 if (curNode.getNodeType() == NodeType.SIGN) { // 会签节点:需要所有审批人都同意 if (!isAllApproved(order, curNode)) { return NodeActionResult.WAIT; // 还没会签完,继续等人 } } // 其他类型省略 ProcessNode nextNode = getByKey(curNode.getNextNodeKey()); if (nextNode == null) { order.setStatus(ApprovalStatus.APPROVED); return NodeActionResult.FINISH; } order.setCurrentNodeKey(nextNode.getNodeKey()); return NodeActionResult.CONTINUE; }这段代码的逻辑说明:getCurrentNode根据审批单当前的current_node_key查出节点配置;会签节点必须等所有审批人都操作完,只要有一个没点,就返回WAIT,审批单停留在原节点;nextNode为空说明流程走到底,直接把整单置为已通过。这个“返回等待”的设计特别关键,少了它,会签就会在第一个审批人同意后直接跳到下一节点,这是新手改源码最常犯的错。
3. 把 OA 源码跑通本地:MySQL 初始化、配置修改和一张请假单的完整路径
3.1 环境准备:JDK、Maven 与 MySQL 的版本搭配
看源码之前先看 pom.xml 里 Spring Boot 的版本。如果这个 OA 项目用的 Spring Boot 2.x,JDK 最好配 8 或 11,不要配最新的 JDK 17 或 21——不是不能用,而是很多老项目里的 cglib 代理、JSP 编译会和新版 JDK 起冲突,报一些看不懂的InaccessibleObjectException,纯属给自己找麻烦。
环境变量这里我踩过坑:JDK 配好后在命令行用java -version验证一下,而不仅仅是看 IDEA 里能跑。IDEA 会自动识别,但终端里 mvn 命令用的是哪套 JDK,又另说。常见做法是配好JAVA_HOME和PATH,然后在 cmd 里执行:
java -version mvn -version输出里 Java 版本和 Maven 版本对得上,再往后走。MySQL 用 5.7 或 8.0 都行,但 8.0 要注意驱动版本,pom.xml 里默认的com.mysql.jdbc.Driver在 8.0 下必须换成com.mysql.cj.jdbc.Driver,同时连接 URL 要加时区参数,否则启动直接包Server time zone错误。这一步卡住过不少人。
3.2 数据库初始化:source 命令导入 SQL 脚本
拿到源码包,先找数据库脚本。规范的项目会放一个sql/目录,里面可能是init.sql或带日期的备份文件。老手做事比较稳,一般不直接用 Navicat 的运行 SQL 文件,因为文件太大平台会卡,更稳的是 MySQL 命令行执行。
mysql -u root -p --default-character-set=utf8 source D:/oa/sql/init.sql;source命令执行完会输出一堆Query OK,这时候不妨数一数有没有ERROR关键字。项目说明文档里如果提到要手动创建数据库,第一行应该是CREATE DATABASE oa之类的语句;如果脚本里没有,先建好库再 source:
CREATE DATABASE oa DEFAULT CHARACTER SET utf8mb4;utf8mb4 值得单独说:如果项目里有人点评论、签名这类字段,用老的 utf8 字符集存不了这些特殊字符。等着你半夜排查Incorrect string value报错,不如一开始就统一utf8mb4,表和库都指定,排序规则选utf8mb4_general_ci就够用了。
3.3 启动前检查:application.yml 里改这几个参数
数据库导入后,打开src/main/resources/application.yml改配置。OA 项目配置文件里通常有三个高频修改点:数据库连接串、Redis 地址、文件上传路径。缺一不可,很多人只改了数据库就启动项目,结果报Unable to connect to Redis时还以为源码有问题。
spring: datasource: url: jdbc:mysql://localhost:3306/oa?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8mb4 username: root password: yourpassword driver-class-name: com.mysql.cj.jdbc.Driver redis: host: localhost port: 6379 database: 0这段配置有三个坑要说明。一,serverTimezone必须写,MySQL 8.x 不写时区直接启动失败。二,useSSL=false是本地环境最优解,线上再看需要不需要开 TLS,本地因为 SSL 握手超时连不上库的大有人在。三,Redis 不装也可以先启动吗?很多 OA 项目把 session 或验证码放 Redis,没装的话启动时一般不会挂,但登录验证码会一直报错。建议本地用 Redis 的 Windows 版或 Docker 起一个,一劳永逸。
改完后启动主类,日志出现Tomcat started on port 8080就算起来了。这时候别急着登录,先用浏览器打开http://localhost:8080看一眼登录页是否正常渲染,如果出现白屏或 404,多半是静态资源路径配错了,这个后面避坑章节会提。
3.4 串联一条完整链路:发起请假、提交、审批通过
系统跑起来后,按源码附带的账号表登录。通常会有 admin 和一个普通员工账号,项目说明里一般有写到初始化 SQL 的sys_user表里。
操作链路一般是这样:先用普通员工账号登录,进“我的申请”发起一个请假单,填写开始时间和请假天数,提交后状态变“审批中”;退出登录,换成 admin 账号,进“待办审批”里能看到这单子,点同意,审批流走完,再用员工账号回去看状态变“已通过”。
这条链路走通,才说明这个 OA 源码基本健康。如果卡在某个环节,比如提交后待办没出现在 admin 账号下,优先排查两个地方:一是流程定义里approver_role配的是不是 admin 这个角色。二是审批人权限,admin 用户是否有这个角色关联。这两个表对不上,流程怎么走都不会推到你头上。
4. 把 OA 改造成自己的项目:会签节点、限时审批和行级权限改造
4.1 会签节点:让三个审批人同时看到同一条待办
很多 OA 源码默认只有单人审批链:部门经理审批完到 HR。但真实企业场景里经常有“副总经理、财务总监、法务都要点头”的情况,这就是会签节点。改造思路不是在业务代码里写死循环,而是在节点表里加一个节点类型node_type=2,然后以节点配置驱动同时生成多条待办。
以 MyBatis 为例,这个逻辑拆成两步。第一步新增节点时配置会签:
INSERT INTO process_node (def_id, node_key, node_name, node_type, timeout_hours) VALUES (1, 'legal_approval', '法务会签', 2, 48); INSERT INTO process_node_approver (node_id, user_id) VALUES (last_insert_id(), 101); INSERT INTO process_node_approver (node_id, user_id) VALUES (last_insert_id(), 102);第二步是推进流程时同时生成多个待办任务:
if (node.getNodeType() == NodeType.SIGN) { List<Long> approverIds = nodeApproverMapper.findByNodeId(node.getId()); for (Long userId : approverIds) { todoMapper.insert(order.getId(), node.getId(), userId, TodoStatus.PENDING); } order.setStatus(ApprovalStatus.RUNNING); }这段改造说明两点:会签节点的“待办生成”动作必须发生在流程推进到该节点的那一刻,而不是预先给所有人一次性生成;修改在推进逻辑里时要注意别影响后面的流程处理,会签节点整个节点未结束前不能把current_node_key往下移。所以一个配套的改动是加一张process_node_approver表,把节点和审批人的多对多关系拆出来,不然一条节点记录里没法优雅地存 3 个审批人。
4.2 限时审批:设置 48 小时未处理自动提醒
很多 OA 源码里的审批没有时间概念,单子挂在那没人管也没人知道。加限时审批,本质上就是给每个节点配一个timeout_hours,再用一个定时任务扫描超时未处理的待办,推送提醒。
推荐用 Spring 自带的@Scheduled定时任务做扫描,比引入 Quartz 轻量得多。核心实现思路:
@Scheduled(fixedDelay = 60000) // 每分钟扫一次 public void checkTimeoutTodos() { List<ApprovalOrder> orders = approvalOrderMapper.findRunningOverdue(); for (ApprovalOrder order : orders) { ProcessNode curNode = getCurrentNode(order); DateTime dueTime = order.getArriveNodeTime() .plusHours(curNode.getTimeoutHours()); if (dueTime.isBefore(DateTime.now())) { notifyService.sendRemind(order, curNode); // 标记提醒过,避免每分钟重复发 todoMapper.markReminded(order.getId(), curNode.getId()); } } }这段代码有两个关键设计。第一,findRunningOverdue只查状态为审批中且当前节点所需审批数未满足的单子,条件里必须带上arrive_node_time is not null。第二,很多节点审批人可能不止一个,所以要用isBefore(DateTime.now())判断,一旦发现到期没处理就发提醒。同时要有一列记录“是否已提醒过”,否则定时任务每分钟跑一次,短信接口等着被打爆。实际项目中提醒渠道可以很朴素:内部邮件或者企业微信机器人 webhook,先跑通提醒链路,再考虑升级成短信。
定时任务配置别忘了在主类上加@EnableScheduling,否则这个@Scheduled完全不会执行,好多人改了代码却不加注解,等了半天没消息还以为逻辑写错了。
4.3 行级权限:为什么部门经理只能看到本部门的申请单
OA 源码里最常见的权限模型是 RBAC:用户 -> 角色 -> 菜单按钮。单靠这个解决不了数据隔离问题:同为部门经理角色,A 部门经理不应该看到 B 部门的报销单。这就需要行级权限,本质是在 SQL 查询时动态拼接过滤条件。
常见做法是给approval_order表加一列dept_id,然后拦截器里把数据权限拼进 MyBatis 查询。实现上常用 MyBatis 的拦截器,拦截select语句时自动追加条件:
@Intercepts({ @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}) }) public class DataScopeInterceptor implements Interceptor { @Override public Object intercept(Invocation invocation) throws Throwable { // 省略解析SQL的代码,核心是在WHERE后追加 dept_id = 当前人部门 return invocation.proceed(); } }这个拦截器在生产项目里有风险点:一旦 SQL 里有子查询或union,字符串拼条件很容易拼错位置。更稳妥的做法是显式在 mapper 里传deptId参数,而不是做全局拦截:
<select id="selectApprovalOrderPage" resultType="ApprovalOrder"> SELECT * FROM approval_order WHERE 1=1 <if test="deptId != null"> AND dept_id = #{deptId} </if> ORDER BY create_time DESC </select>显式传参的好处是每个查询的权限逻辑都能人工审查,代码里也容易看出来你有没有漏掉某张表。等到以后数据量大了,再考虑引入更重的 mybatis-plus 数据权限插件,或者干脆把数据权限做进 Redis 缓存里。行级权限是 OA 系统最容易出事故的地方,宁可写笨一点,不要写炫。
5. 避坑/常见问题:OA 源码部署中反复出现的 6 个坑
5.1 建表顺序错误导致外键失败
现象:导入 SQL 脚本时 MySQL 报Cannot add foreign key constraint,后面的建表语句全部中断。
原因:脚本里先建了approval_order表,它引用了sys_user的主键,但sys_user的建表语句排在后面,外键找不着父表。
解决:用文本编辑器打开 SQL 脚本,把所有CREATE TABLE按依赖关系重排:用户角色表在业务表之前,审批单表在审批记录表之前。或者更省事的做法是删除脚本里的所有FOREIGN KEY约束,业务层面用代码保证一致性。生产环境不用外键的团队不少,本地跑源码更应该先把流程跑通,没必要被外键卡死。
5.2 审批单卡在“审批中”,后台数据库连接池耗尽
现象:服务没有崩溃,但登录页转圈、审批列表一直加载不出来,日志里大量Connection is not available, request timed out。
原因:OA 里有个定时任务或消息消费者频繁获取连接,某个查询死锁,或者接口异常后连接没归还。最常见的是事务里做了 RPC 调用,外部接口一直不返回,事务不提交,连接就被占着不放。
解决:给数据源配置合理的最⼤连接数和等待超时,并开启连接泄露检测。HikariCP 下设置:
spring: datasource: hikari: maximum-pool-size: 20 connection-timeout: 30000 leak-detection-threshold: 60000leak-detection-threshold值得解释:连接被占用超过这个毫秒数就打印告警日志,能直接看出是哪个方法占着连接不还。本地跑源码时连接池不用开太大,10 到 20 足够,和并发数匹配即可。
5.3 待办列表出现了别人的审批单
现象:员工 A 登录后,待办列表里能看到员工 B 发起的单子,或者能看到其他部门的单子。
原因:待办查询 SQL 只联了process_node_approver表,没过滤当前登录人的 ID,而是查了该节点下所有审批人的待办。由于 OA 里经常用join恢复出审批单全字段,如果忘了WHERE approver_id = 当前用户,数据就全部泄露了。
解决:写待办查询时把这个过滤条件写死在 SQL 里,而不是靠前端传参:
SELECT o.* FROM approval_order o INNER JOIN todo t ON o.id = t.order_id WHERE t.approver_id = #{currentUserId} AND t.status = 0 ORDER BY t.create_time DESC这个坑反复出现的根源,是很多源码里把currentUserId放在 service 层拼条件,而不是在 mapper 层固化。改造成在 mapper 接口上直接声明参数,每个涉及待办的查询都要带上,代码评审时一眼就能检查出来。
5.4 修改了角色权限但登录后没生效
现象:给员工加了一个审批角色,重新登录后发现仍然看不到审批菜单,甚至功能按钮消失。
原因:OA 项目里菜单权限通常缓存在 Redis 或者用户的 session 里,加角色只改了数据库,缓存还是旧数据。
解决:在权限修改接口里主动清掉该用户的缓存。如果是 Redis 存储,删除对应 key;如果是 Session 存储,让用户退出重新登录即可。更彻底的做法是给角色分配接口加一个“更新角色时清理关联用户缓存”的逻辑,避免每次该权限都要手动清缓存。别问我为什么知道的——在项目里有同事直接改了数据库权限就跑到客户现场演示,结果权限半天没生效,场面很尴尬。
5.5 时间字段在页面显示差了 8 个小时
现象:审批记录的创建时间是早上 9 点,前端页面显示的是凌晨 1 点。
原因:Java 服务端用LocalDateTime存的是本地时区,JSON 序列化时Jackson默认格式没带时区,前端拿到字符串后按浏览器本地时区解析,或者 MySQL 连接串没指定serverTimezone,驱动把CST本地时间又转了一次。
解决:第一,把 MySQL 连接参数里的serverTimezone明确写成Asia/Shanghai;第二,Jackson 全局配置统一时间格式:
spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8改动后重启服务,重新发起一条审批单看时间是否对齐。这个坑不影响功能,但影响对系统的信任感。如果前端展示还是有问题,可以检查前端的时区解析逻辑,很多 Vue 项目没有对后端返回的时间做格式化。
5.6 启动时端口被占用
现象:启动日志显示Port 8080 was already in use。
原因:本地起过其他项目没关干净,或者上次 IDE 挂着没退出。
解决:找到占用进程处理一下即可:
netstat -ano | findstr :8080 taskkill /f /pid 进程号这个就纯粹是日常操作了,不值得花时间深究。
6. 把审批流做成“事找人”:消息驱动改造和最终建议
6.1 待办提醒的前置设计:把“人找事”变成“事找人”
审批系统用得越久,越会发现一个尴尬点:单子卡在某个审批人那里没人看。与其靠定时任务扫描,不如在审批状态变更的当下主动触发消息推送。常见做法是:在审批记录插入成功后,调用一个MessageService,发给下一节点的审批人。在测试环境可以先接一个企业微信机器人或 WebHook,用 HTTP 请求推送即可,先把链路跑通再考虑接入正式的通知平台。
6.2 幂等与追溯:同一张单子不能重复推进
最后想给一个习惯建议:给审批核心操作加上防重逻辑,利用数据库唯一索引而不是代码判断。一个审批人如果在同一秒点两次“同意”,前端的按钮已经禁用了,但移动端和 PC 端同时打开就可能重复提交。增加一条唯一索引是:
ALTER TABLE approval_record ADD UNIQUE KEY uk_order_node_approver (order_id, current_node_key, approver_id);这样即使代码里漏了判断,数据库也不会产生两条相同节点的审批记录。这个习惯是从数据血泪教训里总结出来的,一次生产数据被覆盖后就再也不相信纯代码层面的幂等判断了。
6.3 验证:拿这份源码做一次自己的性能测试
如果这套 OA 源码你已经跑通并完成了会签、限时提醒这类改造,下一步值得做的是 Jmeter 压测。造 1000 个用户,并发发起 100 张审批单,看数据库连接池和 CPU 的表现。这一步做完,你对“企业应用的真实负载”会建立起比较直觉的感受。当然也不必过度设计——中小型公司内部 OA 日均几千单,单机加 MySQL 稳稳够用了,不必一开始就往微服务方向演进。
把 OA 源码当玩具跑一遍不够,要能动手改、压一遍、知道自己写的代码在什么量级会翻车,才算真正消化了它。如果你能把这套流程在本地完整改造一遍,这个过程中的成长会远超过背十道面试题。希望这些经验和踩坑记录能帮到正要拆这份源码的你。
本文还有配套的精品资源,点击获取