☰
Java OA办公审批系统源码实战:从部署到二次开发指南
2026/10/10 7:30:16 网站建设 项目流程

简介:这是一套基于Java开发的OA办公审批系统完整源码,配有详细项目说明,适合计算机相关专业在校学生、教师、企业员工用于毕业设计、课程设计或项目初期立项参考。系统管理端采用SpringBoot、MyBatisPlus、SpringSecurity、Redis、Activiti与MySQL,员工端基于微信公众号实现授权登录、审批与消息推送,前端使用vue-admin-template搭建,涵盖权限管理、审批管理、公众号菜单管理等模块,便于掌握完整业务链路。压缩包共一百零八个文件,以九十一个Java源码文件为主,附十二个XML配置、两个YAML配置和项目说明文档,整体大小仅一百零六KB,按公共模块、实体类模块、系统服务模块划分,结构清晰。目前已有七百三十四人学习下载。除经过运行验证的代码外,资源还提供项目概述、技术栈清单与接口汇总,能帮助读者快速掌握SpringSecurity权限控制、Activiti工作流集成及前后端分离开发思路,便于在此基础上升级和二次扩展。

1. 基于Java开发的OA办公审批系统:为什么这套源码值得上手拆一遍

如果你在一家小公司做后端,突然被通知要上线一套请假、报销、用印审批系统,既要交付快又要稳定,那个感受我太熟了。拆一套基于Java开发的OA办公审批系统源码来改,是比从零造轮子靠谱得多的选择。这类源码通常自带请假、报销、用章等常见审批流程,后端用Spring Boot做底座,MyBatis管数据库,审批流程靠数据表和状态字段驱动,一个人一天就能部署起来。它适合两类人:刚入行的Java工程师想搞懂一套真实业务系统的代码结构,以及需要快速给团队落地内部审批平台的工程师。照着这篇笔记往下走,你既能部署,也能把二次开发的落点理清楚。

2. 技术选型与源码结构:Spring Boot + MyBatis为什么是OA系统的主流答案

2.1 技术栈选择的理由:不引入工作流引擎,状态机够用

翻这类OA源码,我第一件事永远是打开pom.xml。常见的技术栈组合绕不开三件套:Spring Boot 2.x做Web框架,MyBatis做持久层,MySQL存数据。前端的话,老一点的源码用JSP或Thymeleaf做服务端渲染,新一点的会把前端拆成Vue项目单独维护。这套基于Java的OA办公审批系统通常不会引入Activiti或Flowable这类重量级工作流引擎,审批流程是用自研状态机加几张流程表实现的。

很多新人会问:为什么放着现成的工作流引擎不用?原因就是量级不匹配。OA的审批流大多是请假三天以内的单线流转、报销金额超五千走两级审批这种简单分支,犯不上引入完整BPMN规范。自研状态机的优势是轻、直观,出问题一条SQL就能定位;劣势是会签、条件网关这类需求要自己写。落地中,超过八成OA需求用自研状态机都能撑住,而Activiti的学习和调试成本在项目初期特别拖节奏。

选型判断我一般这么给:

  • 审批流只有“提交—审批—通过/驳回”,直接数据表驱动;
  • 需要多级会签、动态路由、时限提醒,再考虑Flowable;
  • 纯Java栈,数据库用MySQL,避免引入太重或偏门的中间件。

2.2 从pom.xml入手:先看清依赖边界

打开源码先看pom.xml,能快速判断项目版本和依赖面。典型依赖大概是:

<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.7.18</version> <relativePath/> </parent> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.3.1</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> </dependencies>

这里有几个信息量很大的点。spring-boot-starter-web决定了这是一个内嵌Tomcat的Web项目,部署时不用单独装容器;mysql-connector-java在Spring Boot 2.7里通常不带版本号,由parent统一管理,但如果你换到Spring Boot 3.x,这个坐标要改成com.mysql:mysql-connector-j,这是容易踩的坑。Lombok依赖意味着源码里大量使用@Data注解,编译时自动生成getter/setter,看到实体类都是空壳别慌。

如果pom里出现的是mybatis-plus而不是mybatis-spring-boot-starter,Mapper的写法要按MyBatis-Plus的BaseMapper来读,SQL细节被框架封装了,翻源码时要区分。还有一点,看pom也顺手确认项目是不是多模块。很多OA源码用单工程,偶尔拆成oa-common、oa-system、oa-admin。多模块时根pom只做依赖管理,业务模块之间必须显式引用,否则IDEA编译会报找不到类,那说明模块依赖没配对。

2.3 目录结构解读:Maven工程该从哪个类开始读

一套典型的Spring Boot OA工程,目录结构大致是这样:

oa-system/ ├── pom.xml ├── sql/ │ └── oa_init.sql └── src/main/ ├── java/com/example/oa/ │ ├── OaApplication.java │ ├── config/ │ │ └── MybatisConfig.java │ ├── controller/ │ │ ├── AuthController.java │ │ ├── LeaveController.java │ │ └── ApprovalController.java │ ├── service/ │ │ ├── LeaveService.java │ │ └── WorkflowService.java │ ├── mapper/ │ │ ├── ApplyMapper.java │ │ └── ApprovalRecordMapper.java │ └── entity/ │ ├── Apply.java │ └── ApprovalRecord.java └── resources/ ├── application.yml └── mapper/ ├── ApplyMapper.xml └── ApprovalRecordMapper.xml

我一般先看启动类OaApplication.java,确认@SpringBootApplication和@MapperScan。@MapperScan("com.example.oa.mapper")是MyBatis扫描Mapper接口的入口,没配的话启动直接报找不到Bean。接着看application.yml里的数据源配置和mybatis配置,再顺着一个最薄的流程走一遍,整个项目的脉络就通了。

目录结构本身也是判断代码质量的风向标。controller包很薄、只有几行转发,说明业务在service层,这种结构适合二次开发;反过来,如果controller里堆满了业务逻辑,改起来就头痛,每个接口都可能直接操作SqlSession。

2.4 核心模块划分:一个OA系统到底拆成几块

从功能模块看,一套OA办公审批系统内部通常按以下方式组织:

模块主要实体职责
用户与组织oa_user、oa_dept登录认证、部门与职位管理
菜单权限oa_menu、oa_role控制能打开哪些页面和按钮
审批中心oa_apply、oa_approval_record提交审批单、记录每条审批意见
流程配置oa_process_type配置请假、报销等类型和默认审批规则
消息通知oa_message站内信提醒审批人有待办

模块划分决定了你后面改什么不动什么。最常见的需求是新增一个审批类型,通常只需要动“审批中心”和“流程配置”两层,其他模块不用碰。这样二次开发的边界可控,也不容易把用户模块改崩。

3. 本地跑通最小路径:数据库初始化、配置文件与启动命令

先把结论放在前面:一套Spring Boot + MyBatis的OA源码,从解压到登录首页,正常30分钟。前提是环境干净,别在JDK版本上翻车。下面几小节把完整路径拆开讲。

3.1 环境准备:JDK、MySQL、Maven的版本搭配

环境版本是这类项目最常翻车的地方,先给一个实测下来最稳妥的组合:

  • JDK:1.8及以上,建议就用JDK 8。很多OA源码基于Spring Boot 2.x,跑在JDK 8上最稳。
  • MySQL:5.7或8.0都行。5.7最常见,8.0要特别注意认证插件兼容性。
  • Maven:3.6以上,别用2.x。

提示:如果本机装了多个JDK,启动脚本里显式指定JAVA_HOME,别依赖环境变量,这是降低环境差异最有效的一步。

JDK版本的核心矛盾在Spring Boot 2.7.x:它在JDK 8下编译运行没有任何问题,换到JDK 11以上时部分反射逻辑要加--add-opens参数,很多人栽在这。MySQL这边,如果用8.0,建库时建议指定字符集,避免后面中文乱码:

CREATE DATABASE oa_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;

utf8mb4是必须的,因为有些审批意见里会放emoji,utf8存不下会直接变问号。这一步做对,后面很多坑直接绕开。

3.2 导入SQL脚本:初始化数据库

解压源码后,一般会在根目录或sql目录下看到一个或多个.sql文件,名称通常是oa_init.sql或oa_db.sql。导入命令很简单:

mysql -uroot -p oa_db < sql/oa_init.sql

导入完成后,用下面这条命令验证表是否建全:

mysql -uroot -p -e "USE oa_db; SHOW TABLES;"

正常情况下能看到oa_user、oa_apply、oa_process_instance、oa_approval_record、oa_process_type这一组表。如果表数量很少或表名对不上,先检查导入日志有没有报错。

命令说明:<重定向是把文件内容传入mysql客户端执行;如果遇到报错,可以在mysql交互环境里用source sql/oa_init.sql命令,输出信息更直观,定位到哪一行报错也更容易。导入报错最常见的场景是SQL文件里带了CREATE DATABASE语句,而你当前账号权限不足,这种情况手动执行建库语句或者用source方式都能解决。

3.3 修改application.yml:数据库连接、端口、日志

这是几乎每次都要改的文件。打开src/main/resources/application.yml,找到spring.datasource段,按本机环境改:

server: port: 8080 servlet: context-path: / spring: datasource: url: jdbc:mysql://localhost:3306/oa_db?useUnicode=true&characterEncoding=utf8mb4&serverTimezone=Asia/Shanghai username: root password: your_password driver-class-name: com.mysql.cj.jdbc.Driver mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.oa.entity

改动点有三个。url里的serverTimezone=Asia/Shanghai必须写,MySQL高版本驱动不带时区参数会直接拒绝连接;password改成你的本机密码;driver-class-name在MySQL 8.0连接器里建议显式写com.mysql.cj.jdbc.Driver。

context-path值得多讲一句。如果这里配了/api,那么所有接口路径都会带上前缀,登录页请求也要跟着变。二次开发时前端请求路径和后端controller的RequestMapping必须对齐,否则页面打不开。

端口号要不要改,看实际需要。8080被占用就改成8090再启动,但注意前端页面里如果有写死的localhost:8080,也要一起改。我遇到过前端js里硬编码端口的情况,后端改了端口,整个页面接口全部失败。

3.4 启动与验证:mvn spring-boot:run和日志里的三行关键信息

配置改好,接下来在项目根目录执行:

mvn spring-boot:run

第一次启动会下载依赖,耗时取决于网络。启动成功后,控制台里会有三条关键信息值得关注:

第一条是“Tomcat started on port(s): 8080”,说明Web容器起来了。第二条是“Started OaApplication in x.xxx seconds”,说明Spring上下文完整加载;如果这行没出现,说明启动失败,异常堆栈才是排查的重点。第三条是MyBatis的初始化日志,mapper-locations配置好之后,启动阶段就会加载所有Mapper.xml,XML里的SQL语法错误会在启动时暴露,不用等跑接口才炸。

启动完成后,用curl确认服务在监听:

curl -i http://localhost:8080/

返回200或302(跳转登录页)都算正常。浏览器打开http://localhost:8080/,看到登录页后,用SQL脚本里预置的管理员账号密码登录。很多源码的管理员账号密码是明文存在SQL脚本里的,登录后第一件事就是改密码。

登录后按“提交一笔请假申请”的路径走一遍:新建申请、选择请假类型、填写时长和原因、提交,然后切到审批账号,去待办列表点通过。这条链路通了,核心功能就验证完了。

4. 审批流程是怎么建模的:核心表设计加上五态状态机

从这一章开始进入源码的业务核心。OA办公审批系统价值最高的部分不是页面,而是审批流程的数据模型。把它看懂了,后续所有二次开发都有抓手。

4.1 核心表结构:oa_apply、oa_process_instance、oa_approval_record

一套自研状态机的OA源码,核心表通常就三张。每个审批类型对应oa_apply里的一条记录,每次提交生成一条流程实例,每个人的审批动作都记录在审批记录表里。把三张表放一起看,审批流的“从哪来到哪去”一目了然。

常见建表语句长这样:

CREATE TABLE oa_apply ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '审批单ID', apply_no VARCHAR(32) NOT NULL UNIQUE COMMENT '申请编号,业务流水号', apply_type VARCHAR(20) NOT NULL COMMENT '审批类型:leave/expense/seal', title VARCHAR(100) NOT NULL COMMENT '申请标题', content TEXT COMMENT '申请内容', applicant_id BIGINT NOT NULL COMMENT '申请人ID', status TINYINT NOT NULL DEFAULT 0 COMMENT '0草稿 1待审批 2已通过 3已驳回 4已撤销', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, KEY idx_applicant (applicant_id), KEY idx_status (status) ) ENGINE=InnoDB COMMENT '审批申请表'; CREATE TABLE oa_process_instance ( id BIGINT PRIMARY KEY AUTO_INCREMENT, apply_id BIGINT NOT NULL COMMENT '关联审批单ID', current_node INT NOT NULL DEFAULT 1 COMMENT '当前节点序号', approver_id BIGINT COMMENT '当前审批人ID', state TINYINT NOT NULL DEFAULT 1 COMMENT '1审批中 2已完成', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_apply (apply_id) ) ENGINE=InnoDB COMMENT '流程实例表'; CREATE TABLE oa_approval_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT, apply_id BIGINT NOT NULL, process_instance_id BIGINT NOT NULL, approver_id BIGINT NOT NULL COMMENT '审批人ID', action TINYINT NOT NULL COMMENT '1通过 2驳回', comment VARCHAR(500) COMMENT '审批意见', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_apply (apply_id) ) ENGINE=InnoDB COMMENT '审批记录表';

字段设计里有几个关键点。apply_no建了唯一索引,是业务编号,不是主键,别混用。status字段放在oa_apply上是冗余的,但它是查询列表的核心筛选条件,“我的待办”和“我的已办”都靠它,冗余反而合理。approver_id直接放在流程实例表而不是单独建节点配置表,说明流程是固定审批链路,不适合动态会签,这个问题要知道。

4.2 五态流转模型:一张表看懂审批状态

整条审批流程的推进,本质就是oa_apply.status在五个状态之间跳动。状态定义整理成表格,排查审批问题时先对这张表:

状态值含义触发动作
0草稿新建申请保存
1待审批提交申请,生成流程实例
2已通过最后一个节点审批通过
3已驳回任意节点审批驳回
4已撤销待审批状态下申请人主动撤回

这个模型里最关键的分支是驳回之后怎么办。很多OA的规则是“驳回返回到发起人,修改后再提交,状态回到1”;也有更严格的“驳回直接终止流程,必须重新建单”。二次开发时先确认源码是哪种规则,因为两种规则对应的代码分支完全不同,改错一段代码会让审批数据错乱。

状态流转的事务边界也值得注意。提交申请时,insert oa_apply和insert oa_process_instance必须在同一个数据库事务里,否则会出现单子建了但流程没起来的脏数据。这一步在Service类里通常用@Transactional(rollbackFor = Exception.class)实现,看到这个注解就知道事务已经交代好了。

4.3 一条审批单从提交到办结的完整路径

理解表之后再看代码,会顺很多。提交审批的Service层方法通常是这样的:

@Service public class LeaveService { @Autowired private ApplyMapper applyMapper; @Autowired private ProcessInstanceMapper processInstanceMapper; @Transactional(rollbackFor = Exception.class) public Long submitLeave(LeaveForm form) { // 1. 保存审批单,状态置为待审批 Apply apply = new Apply(); apply.setApplyNo(generateApplyNo()); apply.setApplyType("leave"); apply.setContent(form.getContent()); apply.setApplicantId(form.getApplicantId()); apply.setStatus(1); applyMapper.insert(apply); // 2. 创建流程实例,指定当前审批人 ProcessInstance pi = new ProcessInstance(); pi.setApplyId(apply.getId()); pi.setCurrentNode(1); pi.setApproverId(findApproverByDept(form.getApplicantId())); pi.setState(1); processInstanceMapper.insert(pi); return apply.getId(); } }

提交方法的核心逻辑分两步:先插审批单,再插流程实例。approverId由findApproverByDept方法决定,常见规则是取申请人的部门负责人。这行是整段代码里最容易被改的业务点——改成按角色找、按金额阈值找、还是按汇报链逐级找,都只动这一行的逻辑。generateApplyNo也要看,常见格式是日期加序列号,底层如果用数据库自增ID而非独立序列表,并发高时会撞唯一索引。

审批动作的核心是更新流程实例节点和写审批记录:

@Transactional(rollbackFor = Exception.class) public void approve(Long applyId, Long approverId, boolean pass, String comment) { // 1. 写审批记录 ApprovalRecord record = new ApprovalRecord(); record.setApplyId(applyId); record.setApproverId(approverId); record.setAction(pass ? 1 : 2); record.setComment(comment); approvalRecordMapper.insert(record); // 2. 更新流程实例和审批单状态 ProcessInstance pi = processInstanceMapper.findByApplyId(applyId); if (pass && pi.getCurrentNode() >= MAX_NODE) { // 最后一个节点通过,整单完成 applyMapper.updateStatus(applyId, 2); processInstanceMapper.updateState(pi.getId(), 2); } else if (pass) { // 继续流转到下一个审批人 processInstanceMapper.updateNode(pi.getId(), pi.getCurrentNode() + 1); } else { // 驳回 applyMapper.updateStatus(applyId, 3); processInstanceMapper.updateState(pi.getId(), 2); } }

这段代码里MAX_NODE是流程节点总数,定义在常量类或配置类里。写死带来的限制是:不同部门的请假流程不能有不同审批层级,除非把节点数做成oa_process_type表里的配置字段,按审批类型读取。这才是二次开发里真正有价值的改动点。

5. 部署与二次开发避坑:四个最常翻车的点与排查路径

5.1 数据库连接失败:时区和字符集是最隐蔽的元凶

现象:启动报Error creating bean with name 'dataSource',或者系统能起来但保存中文全变问号。

原因:九成是JDBC连接串里没带时区参数。MySQL 8.0以上驱动默认要求serverTimezone,缺了直接抛异常;字符集问题则多半是因为建库时没指定utf8mb4,用的是默认latin1。

解决:先改url,再加一条修复表字符集的命令:

ALTER DATABASE oa_db CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; ALTER TABLE oa_apply CONVERT TO CHARACTER SET utf8mb4;

改完重启应用。排查时看启动日志里有没有Communications link failure字样,有就先ping数据库地址和端口,别急着怀疑账号密码。这个问题是部署期第一大坑,90%的源码跑不起来都从这开始。

5.2 审批人配置不生效:流程实例的approver_id没被正确赋值

现象:申请单提交成功,待办列表查不到数据,流程卡住没人处理。

原因:常见两种。一是流程实例创建时findApproverByDept返回null,申请人没有关联部门,或部门负责人没维护;二是待办列表的SQL关联条件拼错,审批人看不到自己的单子。

解决:先查流程实例表里approver_id的实际值:

SELECT apply_id, current_node, approver_id, state FROM oa_process_instance WHERE apply_id = 12345;

如果approver_id是0或NULL,去oa_user表和oa_dept表对一下申请人有没有正确的部门ID和负责人ID。这是数据初始化不完整的问题,不是代码Bug,把组织架构字段补全就会恢复。

5.3 静态资源加载404:页面没样式没图片

现象:打开登录页只有文字没有样式,浏览器F12里一片css/js文件404。

原因:最大可能是application.yml设置了context-path,但页面里静态资源的引用路径没有同步带上前缀。其次是自定义拦截器没放行静态资源目录,把classpath:/static/下的请求挡了。

解决:页面里用相对路径或模板引擎的表达式引用资源,别写死绝对路径。如果是拦截器问题,把/static/、/css/、/js/**加入拦截器排除名单。这个问题的排查顺序是:先看URL路径里有没有带context-path,再看拦截器配置。

5.4 MyBatis报BindingException找不到Mapper

现象:启动或调用接口时报Invalid bound statement (not found): com.example.oa.mapper.ApplyMapper.findByStatus。

原因:Mapper接口有了,但对应的XML文件没被扫描到。常见是mapper-locations写的是classpath:mapper/*.xml,XML实际却没放在resources/mapper下;或者XML里的namespace和接口全限定名对不上。

解决:打开ApplyMapper.xml,检查namespace是否等于接口的全限定名,同时确认mapper-locations路径和resources下的目录层级一致。再看每个select标签的id和接口方法签名是否完全一致,少一个id就会在首次调用时爆出同样的错。这个排查顺序很固定:先看报错的是接口还是statement,再查namespace和路径,最后才查SQL本身。

5.5 审批时间差八小时:serverTimezone埋下的深坑

现象:审批记录里的create_time比本地时间早8小时,导出报表也对不上号。

原因:连接串里用了serverTimezone=UTC。UTC是零时区,北京时间是UTC+8,MySQL把UTC时间写进DATETIME字段,查出来自然差了8小时。

解决:统一改成Asia/Shanghai,并确认MySQL服务器本身的时区一致:

SHOW VARIABLES LIKE '%time_zone%';

如果服务器时区也不对,就在my.cnf里设default-time-zone='+08:00'。注意不要在连接串里写serverTimezone又在数据库层面设置CST,CST在不同语境代表完全不同的时区,这个坑我踩过一次就记住了。

6. 二次开发进阶:新增一个审批类型要动哪几个文件

部署和核心模型讲完了,落到一个高频需求:在公司现成的OA系统里新增一种审批类型。比如突然要上“加班审批”,那不是简单加个表单的事,更不是从零另起炉灶。

6.1 改动路径:先标出必须动的四层

以这套源码常见的分层结构,新增加班审批的改动范围如下:

层次改动内容影响范围
数据库在oa_process_type表插入一条类型配置,必要时新建oa_overtime表存加班专项字段低,只加表和数据
页面新增提交加班申请的JSP或Vue页面中,前端工作量
Controller新增OvertimeController,复用审批中心的接口低
Service新增OvertimeService,核心逻辑复制LeaveService而来中

要改动最少,做法是复制Leave模块的一套文件,后缀改成Overtime,再删掉不需要的代码。别一上来就抽公共抽象类,做一个新流程就重构一次,很容易把状态机改崩。

6.2 参数与配置:审批节点数的扩展

加班审批如果只有一层审批,复制代码后一个节点级别的改动量最小。但公司规定加班累计超过20小时要部门总监二次审批,那就得把配置字段化。给oa_process_type表加一个max_node字段:

ALTER TABLE oa_process_type ADD COLUMN max_node INT NOT NULL DEFAULT 1 COMMENT '最大审批节点数';

然后把之前Service里写成常量的MAX_NODE改成从这张表读取。这一步做完,就实现了按审批类型配置审批层级的能力,后续所有类型都能复用。验证方法就是提交一条加班申请,把节点数改成2,看审批记录是否依次落在两位审批人身上。

6.3 验证与收尾

改完以后,不要只测“提交—通过—办结”这条快乐路径,必测三条旁路:驳回后编辑再提交的状态流转、草稿保存后再编辑、待审批状态下撤销。这三个分支最容易留Bug。

最后讲一个我的习惯:改完源码后,在项目目录下建一个CHANGELOG.md,记录改了哪些表、哪些接口、状态定义有没有变化。这样两个月后回头看,不会对着diff文件发呆。这套OA系统虽然不大,但表结构和状态流转都是值得照着拆一遍的样本,把新需求加进去完整跑通一遍,Spring Boot + MyBatis + 状态机这套组合的边界也就摸透了。希望帮到你。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询