☰
Java协同办公OA系统源码解析:从技术架构到权限与审批流实战
2026/10/4 1:18:57 网站建设 项目流程

简介:企业级协同办公系统(OA)是软件开发中最常见的业务场景之一,其核心价值在于将组织架构、权限控制、审批流等抽象为可复用的技术模块。从基础的技术选型来看,Spring Boot整合MyBatis与MySQL构成了轻量级后端基石,而权限模块通常落地为基于RBAC模型的角色-菜单-按钮授权体系,配合Shiro安全框架实现认证与访问控制。工作流引擎则通过状态机与任务表串联请假、报销等审批链路,形成完整的业务闭环。理解这些通用原理后,开发者便能高效完成二次开发。本文以一套可运行的Java协同办公OA系统源码为例,拆解其工程结构、核心代码与踩坑细节,帮助读者快速掌握企业级项目从环境搭建到权限审批流定制的关键路径,为实际工程实践提供参考。

1. Java 协同办公 OA 系统源码:拆之前先搞清楚它到底能给你什么

做 Java 开发的人,手里没几套 OA 系统源码都不好意思说自己做过企业级项目。但说实话,市面上的协同办公 OA 源码鱼龙混杂,有的就是几个 CRUD 拼在一起,有的连登录都带 SQL 注入。我今天要拆的这套 Java 协同办公 OA 系统源码,属于那种「结构完整、能直接跑、适合二次开发」的类型。它解决的是最常见的办公诉求:组织架构管理、用户权限控制、审批流程、通知公告、日程安排这些。适合谁呢?一个是刚入行想看懂真实企业项目的 Java 新人,一个是公司需要快速搭 OA 但没有预算买商业产品的工程师。读完后你会发现,OA 系统没那么玄学,关键就几条链路:谁能登录、谁能看什么、流程怎么流转。下面我一条一条拆给你看。

2. 技术选型与工程结构:为什么这套源码能直接跑起来

2.1 常见技术栈三件套:SSM 还是 Spring Boot

这套 Java 协同办公 OA 系统源码,基本沿用了市面上最主流的那套组合。常见的做法是 Spring Boot + MyBatis + MySQL,前几年老一点的源码会用 SSM(Spring MVC + Spring + MyBatis),但新一点的版本基本都迁到 Spring Boot 上了。Spring Boot 的好处不用多说,内嵌 Tomcat、自动装配、不用写一堆 XML 配置,对于 OA 这种典型的管理系统来说,开发效率高很多。

我一般会先看 pom.xml 或者 build.gradle,确认几个关键依赖:spring-boot-starter-web、mybatis-spring-boot-starter、mysql-connector-java。如果出现了 spring-boot-starter-security 或者 shiro-spring,那说明权限这块是专门做了设计的,不是那种纯靠拦截器硬写的半吊子。这套源码用的是 Spring Boot 2.x + MyBatis + MySQL 5.7 的搭配,属于怎么折腾都不会翻车的组合。JDK 建议用 1.8,太新的版本反而容易遇到兼容性问题。

2.2 工程结构怎么组织:按业务分包还是按技术分包

打开源码第一件事,不是急着跑,而是先看目录结构。好的 OA 源码,包结构一定是有讲究的。常见有两种组织方式:一种是按技术分层,controller、service、mapper、entity 各放各的;另一种是按业务域划分,比如 user、dept、leave、notice 各一个包,里面再分 controller、service、mapper。这套源码用的是第二种,按业务域分包。我个人更推荐这种,因为 OA 系统业务边界清晰,按业务切分,后面做二次开发找人找代码都方便。

看结构的时候顺便看一下 resources 目录。mapper XML 是不是和接口对应、application.yml 里的配置是不是分环境(dev/prod/test)、有没有 sql 初始化脚本。这套东西很全,sql 脚本放在doc/sql下面,直接导入就能用。有的源码会把这个目录省略,让你自己在启动时 flyway 初始化,那也合理,但如果是纯手动的 MySQL 脚本,建议你先打开看一眼表结构和注释,别直接闷头导。

2.3 数据库表设计:组织架构与待办事项的根基

OA 系统的核心表离不开这几张:sys_user、sys_role、sys_menu、sys_user_role、sys_role_menu,这是标准的 RBAC 五张表。然后要有业务表,比如 leave_apply(请假申请)、oa_notice(公告)、oa_plan(日程计划)。这套源码的数据字典做得比较用心,每张表都有注释,比如 sys_user 里面的 status 字段,0 表示禁用,1 表示正常,很直观。

表设计这块有个坑,很多源码把流程相关的表单独建,比如 oa_process_instance、oa_process_task,但审批的具体业务数据(比如请假天数、出差地点)却放在业务表里,两者之间靠 business_key 关联。这个设计是合理的,因为流程引擎要通用,而业务数据是多变的。如果你拿到源码发现流程数据和业务数据混在一张表里,那基本可以判断这个工作流是硬编码的,扩展性会很差。这套源码不是,它用 business_key 关联业务表和流程表,这点值得点赞。

3. 核心链路拆解:从登录到审批流的实现思路

3.1 用户认证:Shiro 还是 Spring Security

OA 系统的登录逻辑看起来简单,但细节很多。这套源码用的是 Apache Shiro,原因很简单:Shiro 在传统企业级系统中的普及度高,配置比 Spring Security 简洁,和 Spring Boot 集成也成熟。核心逻辑在AuthController里,调用subject.login(token),成功后把用户信息放入 session,再重定向到首页。失败的话捕获AuthenticationException,返回错误提示。

@PostMapping("/login") public Result doLogin(HttpServletRequest request, String username, String password, String captcha) { if (!captchaService.validate(request.getSession().getId(), captcha)) { return Result.error("验证码错误"); } UsernamePasswordToken token = new UsernamePasswordToken(username, password); Subject subject = SecurityUtils.getSubject(); try { subject.login(token); UserInfo user = (UserInfo) subject.getPrincipal(); request.getSession().setAttribute("user", user); return Result.success(user); } catch (UnknownAccountException e) { return Result.error("用户名不存在"); } catch (IncorrectCredentialsException e) { return Result.error("密码错误"); } catch (LockedAccountException e) { return Result.error("账号已被锁定"); } catch (AuthenticationException e) { return Result.error("认证失败,请联系管理员"); } }

这段代码有几个注意点。第一,验证码校验放在登录逻辑之前,如果验证码不合法,直接返回错误,不继续消耗数据库查询。第二,UsernamePasswordToken是 Shiro 的标准登录令牌,接收前端传过来的账号密码。第三,subject.getPrincipal()拿到的是在doGetAuthenticationInfo里返回的 UserInfo 对象,不是只有用户名,而是完整的用户信息,后面存 session 直接用。第四,多个catch分支对应不同的登录失败原因,这个很重要,很多 OA 系统的登录失败提示永远是「用户名或密码错误」,排查问题的时候就很难受。

3.2 权限模型:RBAC 在源码里怎么落地

权限这块,这套源码用的是标准的 RBAC 模型,前面提到的五张表在这里发挥作用。用户登录后,系统通过 Shiro 的Realm加载用户的角色和权限。看下面的代码:

@Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { UserInfo user = (UserInfo) principals.getPrimaryPrincipal(); SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(); List<Long> roleIds = userRoleMapper.queryRoleIdByUserId(user.getUserId()); List<SysRole> roles = roleMapper.queryByRoleIds(roleIds); List<String> roleNames = roles.stream().map(SysRole::getRoleName).collect(Collectors.toList()); info.addRoles(roleNames); List<Long> menuIds = roleMenuMapper.queryMenuIdByRoleIds(roleIds); List<SysMenu> menus = menuMapper.queryByMenuIds(menuIds); List<String> perms = menus.stream().map(SysMenu::getPerms).collect(Collectors.toList()); info.addStringPermissions(perms); return info; }

这里要注意一个点:Shiro 的doGetAuthorizationInfo是在每一次权限校验时都会调用的,如果每次都去查数据库,压力会比较大。所以这套源码在 Service 层加了@Cacheable注解,用了 Spring Cache 做缓存。你在改权限的时候,要记得清缓存,不然改完权限用户那边还带着旧权限,这种问题排查起来很恶心。常见做法是提供一个「刷新权限缓存」的接口,或者给权限表加一个 version 字段,每次修改 version,缓存 key 里带上 version。

菜单表和权限标识(perms)是关联的,前端通过一个/sys/menu/getUserMenuList接口拿到当前用户可见的菜单树。这个接口返回的是父子嵌套结构,用parentId关联。前端渲染成侧边栏菜单,后端只返回用户有权限的菜单。不少 Excel 外包项目的权限只做到菜单级,按钮级的权限不做,这套源码的菜单表里有一个perms字段,按钮级权限是通过@RequiresPermissions注解控制的,比只做到菜单级细了很多。

3.3 审批流怎么串起来:状态机与任务表

审批流是 OA 系统最核心的东西。这套源码没有引入完整的 Activiti/Flowable 引擎,而是用状态机 + 任务表的方式自己实现了一套轻量级工作流。我觉得这个设计很务实,对于 OA 这种业务来说,完全不需要 BPMN 那种重度流程建模,反而自研的更好理解、更好改。

核心表结构是这样的:

表名字段说明
oa_process_definitionprocess_key / process_name / node_rule_json流程定义,node_rule_json 存流程节点的 JSON 配置
oa_process_instanceinstance_id / business_key / current_node / status流程实例,一次申请一个实例
oa_process_tasktask_id / instance_id / assignee / task_status / comment任务表,每个节点一条任务记录
oa_process_audit_recordrecord_id / instance_id / operator / action / comment审批记录,所有操作留痕

启动流程的代码逻辑大概是这样的:创建流程实例,记录当前节点为「部门经理审批」,然后创建一条待办任务,指定 assignee 为申请人所在部门的经理。审批人点击通过,流程引擎先校验当前操作人是不是任务的 assignee,然后再更新实例状态到下一个节点,同时生成审批记录。

public ApprovalResult completeTask(Long taskId, String action, String comment, String currentUser) { OaProcessTask task = taskMapper.selectById(taskId); if (task == null || !Objects.equals(task.getAssignee(), currentUser)) { return ApprovalResult.fail("当前用户不是该任务的审批人"); } if (!Objects.equals(task.getTaskStatus(), "pending")) { return ApprovalResult.fail("该任务已被处理,请勿重复操作"); } // 记录审批历史 OaProcessAuditRecord record = new OaProcessAuditRecord(); record.setTaskId(taskId); record.setOperator(currentUser); record.setAction(action); record.setComment(comment); auditRecordMapper.insert(record); // 解析流程定义中的节点规则 OaProcessInstance instance = instanceMapper.selectById(task.getInstanceId()); OaProcessDefinition definition = definitionMapper.selectByKey(instance.getProcessKey()); List<NodeConfig> nodes = JSON.parseArray(definition.getNodeRuleJson(), NodeConfig.class); if ("approve".equals(action)) { // 找到当前节点的下一个节点,更新实例状态 NodeConfig nextNode = nodes.stream() .filter(n -> n.getNodeId().equals(instance.getCurrentNode())) .findFirst() .map(n -> n.getNextNode()) .orElse(null); if (nextNode == null) { instance.setStatus("finished"); } else { instance.setCurrentNode(nextNode.getNodeId()); insertNewTask(instance, nextNode); } } else { instance.setStatus("rejected"); } task.setTaskStatus("done"); taskMapper.updateById(task); instanceMapper.updateById(instance); return ApprovalResult.success(); }

这段代码要注意的是:nextNode为 null 代表流程走完,实例状态要改成 finished;如果走 reject 分支,流程直接终止。很多人改这种流程代码时只改了通过的逻辑,忘了拒绝分支,导致流程被拒后卡在 pending 状态,这就是典型的流程状态机没闭合的问题。

4. 把源码跑起来:环境准备、配置修改与启动验证

4.1 环境清单与版本匹配

在启动之前先把环境准备好。这套源码的依赖版本是偏保守的,你不需要用最新的 JDK 去挑战它的兼容性。具体的环境建议是这样的:

组件推荐版本说明
JDK1.8不要用 11 或 17,部分老版本 cglib 会报错
MySQL5.7.x8.x 也能用,但驱动和时区配置要小心
Maven3.6+3.8 以上没问题
Redis5.x如果验证码和 session 用了 Redis 存储则必装
Node.js不涉及如果前端是模板渲染就不需要

拿到源码后先看一眼application.yml,里面对应的是 dev 环境的配置。有spring.redis.host、spring.datasource.url这样几个关键项。如果你机器上的 MySQL 密码不是 root/123456,记得先改配置,不然启动报数据库连接异常会让你误以为源码有 bug。

4.2 配置文件的修改点

配置文件的修改有几个常见位置。第一步改数据源:

spring: datasource: url: jdbc:mysql://127.0.0.1:3306/oa?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai username: root password: root driver-class-name: com.mysql.cj.jdbc.Driver

url 里面的serverTimezone=Asia/Shanghai很关键。MySQL 8.x 默认时区是 UTC,不加这个参数,数据库查出来的时间和本地时间会差 8 个小时,所有时间字段全是乱的。useSSL=false是关掉 SSL 连接,本地开发环境不需要加密连接,不关的话会有大量警告日志。

第二步改 Redis 配置,如果你的验证码和 session 用了 Redis:

spring: redis: host: 127.0.0.1 port: 6379 database: 1 password: # 没有密码就留空

第三步改文件上传路径,OA 系统一般都有头像上传、附件上传功能。配置文件里一般会有一个file.upload.path的配置项,本地开发设为绝对路径,比如/data/oa/upload,注意提前创建目录并且有写权限。Windows 上建议设为D:/oa/upload,不要用相对路径,因为打包成 jar 后相对路径的基准目录不可控,这是最容易踩的坑。

4.3 初始化数据与启动命令

数据初始化分两步。第一步用 MySQL 客户端导入sql目录下的oa_init.sql和oa_data.sql。第二步启动项目让 MyBatis 自动建表?不对,MyBatis 默认没有自动建表功能,所以表结构必须靠 sql 脚本导入。如果你用的脚本是只导表结构不导数据,需要自己手动创建管理员账号。

-- 导入数据库 mysql -uroot -p < doc/sql/oa_init.sql mysql -uroot -p < doc/sql/oa_data.sql -- 或登录 mysql 后执行(二选一) source /path/to/oa_init.sql; source /path/to/oa_data.sql;

导入完成后检查一下数据,看 sys_user 表里有没有默认的 admin 账号,多数源码默认管理员是 admin/admin123 或者 admin/123456。如果登录的时候提示密码错误,去 sys_user 表看一眼密码字段是明文还是密文。如果是密文,常见的是 MD5 加密或 BCrypt 加密,你得用生成器生成一个对应的加密值替换进数据库。

启动的命令也很标准,开发环境用mvn spring-boot:run,生产模拟就用mvn clean package -DskipTests打 jar 包,然后java -jar oa-system.jar。注意-DskipTests是跳过测试编译,很多源码里的测试类依赖测试环境配置,不跳过会报错。

# 开发模式启动 mvn spring-boot:run # 打包并跳过测试 mvn clean package -DskipTests java -jar target/oa-system.jar

启动日志出现Started Application in x.xxx seconds并且没有 ERROR,说明启动成功。这时打开浏览器访问http://127.0.0.1:8080,看到登录页,再用 admin 账号登录进首页,基本就算跑通了。如果 8080 端口被占用,在application.yml里改server.port,但要注意改了端口之后,前端请求的 baseURL 也要对应改掉。

5. OA 系统源码避坑指南:5 条换来的血泪经验

5.1 数据库连接失败的隐藏原因

现象:启动项目,日志报Unable to obtain connection from database,但用 Navicat 连 MySQL 完全正常。

原因:MySQL 用户表的 host 字段限制。本地连接用root@localhost没问题,但 Java 连接通过127.0.0.1走的是 TCP 协议,MySQL 把它当作root@127.0.0.1。如果user表里只有root@localhost,TCP 连接就会被拒绝。

解决:执行一条授权命令,允许指定 IP 连接:

GRANT ALL PRIVILEGES ON *.* TO 'root'@'127.0.0.1' IDENTIFIED BY 'root' WITH GRANT OPTION; FLUSH PRIVILEGES;

或者干脆在配置文件里用localhost代替127.0.0.1,但你要确认 JDBC 驱动能不能正确解析。

5.2 中文乱码问题出在三个地方

现象:页面显示中文正常,但数据库里存的中文是问号乱码;或者页面直接显示???。

原因:三个地方只要有一处不是 utf8,就会乱码。第一个是数据库连接串没带characterEncoding=utf8,第二个是 MySQL 表字段的字符集是 latin1,第三个是前端页面本身不是 utf8 编码。

解决:先改连接串,然后查看表字符集:

SHOW CREATE TABLE sys_user;

看到DEFAULT CHARSET=latin1就说明问题在这,把表转成 utf8mb4:

ALTER TABLE sys_user CONVERT TO CHARACTER SET utf8mb4;

注意 utf8mb4 比 utf8 多支持 emoji 表情,做 OA 系统的话建议一律用 utf8mb4,不然有人在审批意见里填了个 emoji 表情,直接报 Incorrect string value 错误。

5.3 时间总是差 8 小时

现象:前端页面上显示的待办时间比真实时间晚 8 小时,数据库里存的时间也不对。

原因:最常见的是 MySQL 连接串里没有serverTimezone=Asia/Shanghai,其次是 Java 序列化时间到前端时没做时区处理,还有可能是服务器本身的时区设置是 UTC。

解决:第一步改连接串,改成上面 4.2 节那样。第二步在实体类的日期字段上加@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8"),保证 JSON 输出的时候是东八区:

@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") private LocalDateTime createTime;

如果是 LocalDateTime 类型且用了 MyBatis-Plus,一定要检查全局配置里有没有time-zone: GMT+8,否则 MyBatis-Plus 的自动填充功能会把时间写错。

5.4 循环依赖导致 Bean 创建失败

现象:启动直接报The dependencies of some of the beans in the application context form a cycle,比如UserServiceImpl和DeptServiceImpl互相注入。

原因:OA 系统的业务模块天然有强关联,很多开发图省事直接字段注入,导致循环依赖。当 Spring Boot 2.6 以上版本把循环依赖默认禁止后,启动就炸了。

解决:两种思路。第一种改成构造器注入,这是 Spring 官方推荐的方式,但改动量大。第二种在启动类上临时允许循环依赖,快速验证:

@SpringBootApplication public class OaApplication { public static void main(String[] args) { SpringApplication app = new SpringApplication(OaApplication.class); app.setAllowCircularReferences(true); app.run(args); } }

这套源码本身是能正常启动的,如果你修改代码时引入循环依赖,用第二种方式可以应急。但正式环境我还是建议你重构,把公共服务抽取出来,消除循环依赖。

5.5 接口返回的字段是 null,但数据库里有值

现象:数据库的 sys_user 表里mobile字段有值,但前端接口返回的用户信息里 mobile 是 null。

原因:实体类的属性名和数据库字段名对不上。比如数据库字段是mobile_phone,实体类属性是mobile,并且 MyBatis 没开启驼峰映射,或者查询用的 SQL 没有给字段起别名。

解决:开启 MyBatis 的驼峰映射:

mybatis: configuration: map-underscore-to-camel-case: true

如果还是不行,那就检查 mapper XML 里的 resultMap 是不是没配全:

<resultMap type="com.oa.entity.UserInfo" id="UserMap"> <id column="user_id" property="userId"/> <result column="mobile_phone" property="mobilePhone"/> </resultMap>

6. 验证与进阶:把待办模块改成自己业务之前,先过这四关

第一关是回归测试。源码跑通之后,先别急着加功能,把核心链路全部走一遍。我习惯用 Postman 建一套 OA 接口集合,按顺序跑:登录获取 token、获取菜单列表、发起请假申请、审批通过、查看待办列表。每个接口关注 HTTP 状态码、响应结构里的 code 字段、数据条数是否合理。如果登录后拿到的 token 在后续接口请求中丢了,多半是拦截器配置的问题,看你源码里的AuthInterceptor是不是把放行路径写死了。

第二关是日志排查。启动时加上--debug参数或者把logging.level.com.oa=debug写进配置,就能看到 MyBatis 打印的 SQL。看 SQL 时重点盯两件事:有没有select *查全表、有没有 N+1 查询。常见的 OA 模板是把「用户列表 + 每个用户所属部门」用两条 SQL 分开查,部门查了 N 次,这就是 N+1。遇到这种情况,改成一次性 join 查出结果,或者用 MyBatis 的嵌套查询加懒加载。

第三关是慢 SQL 验证。OA 系统的数据量一大,最先扛不住的就是待办列表和审批记录查询。源码里如果索引建得不全,在 MySQL 里手动补上关键索引:

ALTER TABLE oa_process_task ADD INDEX idx_assignee_status(assignee, task_status); ALTER TABLE oa_process_instance ADD INDEX idx_business_key(business_key);

补完索引重新跑一遍待办接口,你会发现响应时间从两秒降到几十毫秒。这个动作价值很高,也是你面试时能讲出来的实战优化点。

第四关是权限缓存清理。权限加了缓存之后,管理员改完角色权限,用户那边如果不刷新缓存,会继续用老权限访问新菜单。我给这套源码加了一个「强制刷新权限」的接口,逻辑就是调用Realm.clearCache()清除授权缓存。从那以后,我每次改完权限模块的代码,都强制自己先跑一遍「创建角色 → 分配权限 → 登录验证 → 改权限 → 刷新缓存 → 重新验证」六步流程。OA 系统这种活,最怕的不是功能复杂,而是权限边界模糊,线上出了越权事故又定位不到原因,那就真的血亏了。希望这篇拆解能帮你在拿到源码后少走几条弯路,把项目跑起来、改顺手,真正变成自己能讲清楚的东西。

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

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

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

立即咨询