简介:这是一套基于SSM框架的Java办公管理系统(OA)完整源码,面向Java Web初学者、课程设计学生及需要二次开发的企业开发者,帮助理解企业级办公自动化的实现思路。压缩包共1160个文件,约6.95MB,其中333个java源文件与365个class文件构成核心业务与框架代码,179个js、50个jsp、18个css负责前端交互与页面渲染,另有35个xml、8个properties等配置资源,以及sql脚本、字体与图片素材,结构完整便于导入IDE运行调试。系统涵盖用户管理、工作流审批、文档管理、通知公告、任务与会议管理等模块,并预留了功能扩展、性能优化与接口集成的空间。目前已有2370人学习下载,适合作为SSM框架实战参考与OA二次开发的基础工程。
1. 从一份 JAVA 办公管理系统源码说起:OA 到底能跑通哪些真实业务
很多做企业信息化的朋友,第一次接触办公管理系统(OA)源码时,最关心的不是它用了什么设计模式,而是这套东西能不能直接跑起来、能不能改、改完能不能上线。我手上这份 JAVA 办公管理系统(OA)源码包,技术栈是 Spring 框架打底,配合经典的分层结构,覆盖了日常办公里最高频的几块业务:用户与权限、部门管理、考勤打卡、请假审批、公告通知、文件流转。它不是那种只放几个 CRUD 演示的玩具工程,而是把「一个人从入职到请假到离职」这条主线上的数据流都串了起来。
适合谁看?如果你正在做企业内训项目、课程设计,或者公司想搭一套轻量级内部办公平台,这份源码能省掉大量从零搭架子、设计表结构、写权限拦截的时间。新手可以照着步骤把环境跑通,熟手可以直接跳到权限模型和审批流那两章,看它怎么用 Spring 的拦截器和 AOP 把「谁能看什么、谁能改什么」落到代码里。下面我就按「先跑通、再拆解、最后避坑」的顺序,把这份资源拆开讲透。
2. 环境搭建与数据库初始化:把 Spring 项目从压缩包跑到登录页
拿到一份 JAVA 源码,最怕的就是「代码能看懂,环境跑不起来」。这一章的目标很明确:让你在本地把项目启动,看到登录页,并且能用初始账号登进去。整个过程分三步——依赖准备、数据库导入、配置修改。每一步我都会说清楚为什么这么做,以及参数改哪里。
2.1 依赖版本与目录结构确认
先看项目根目录,常见结构是src/main/java放业务代码,src/main/resources放配置文件和 Mapper XML,src/main/webapp放前端页面和静态资源。这份 OA 源码用的是 Maven 管理依赖,根目录下应该有pom.xml。打开它,重点确认三件事:Spring 的版本、数据库驱动版本、JDK 编译级别。
<!-- pom.xml 关键片段,版本号以实际文件为准 --> <properties> <spring.version>5.x.x</spring.version> <!-- Spring 核心版本 --> <jdk.version>1.8</jdk.version> <!-- 编译级别,建议 JDK8 --> </properties> <dependencies> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.x</version> <!-- 驱动版本要和本地 MySQL 匹配 --> </dependency> <!-- 其他依赖省略 --> </dependencies>逻辑说明:Spring 5.x 配 JDK8 是这套源码的常见组合,如果你本地是 JDK11 或 17,编译时可能遇到模块化相关的报错,最稳妥的做法是装一个 JDK8 专门跑它。MySQL 驱动 8.0 和 5.7 的连接串写法不同,8.0 必须带时区参数,否则启动就报时区错误。参数怎么改:spring.version不要随意升级,源码里的 XML 配置和注解都是按这个版本写的;jdk.version保持 1.8,除非你打算自己重写配置类。
2.2 数据库建库与 SQL 导入
源码包里一般会有一个sql目录,里面是建表语句和初始数据。常见做法是先用命令行或客户端建一个空库,字符集选utf8mb4,然后执行 SQL 文件。
# 登录 MySQL 并创建数据库 mysql -u root -p CREATE DATABASE oa_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE oa_db; # 导入建表与初始数据,路径按实际调整 source /path/to/oa_db.sql;逻辑说明:字符集必须用utf8mb4,因为公告和请假理由里经常出现特殊符号或表情,用utf8会截断。导入完成后,用SHOW TABLES;确认表数量,通常会有用户表、角色表、权限表、部门表、考勤表、请假表、公告表等十几张。参数说明:如果你的 MySQL 是 8.0,导入时可能遇到utf8mb4_0900_ai_ci排序规则不兼容旧 SQL 的情况,解决办法是在 SQL 文件开头把排序规则统一改成utf8mb4_general_ci,或者导入时加--default-character-set=utf8mb4。
2.3 配置文件修改与启动验证
数据库有了,接下来改src/main/resources下的配置文件。常见的是jdbc.properties或applicationContext.xml里直接写连接信息。重点改四个参数:URL、用户名、密码、驱动类名。
# jdbc.properties 示例 jdbc.driver=com.mysql.cj.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/oa_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false jdbc.username=root jdbc.password=你的密码逻辑说明:serverTimezone=Asia/Shanghai是 MySQL 8.0 驱动必须的,不写就报The server time zone value错误;useSSL=false在本地开发时关掉,避免证书警告。改完后,用 Maven 打包或直接在 IDE 里配置 Tomcat 启动。启动日志里看到Root WebApplicationContext: initialization completed并且没有异常堆栈,就说明容器起来了。浏览器访问http://localhost:8080/项目名/,出现登录页,用 SQL 里预置的管理员账号登录,能进主页就说明环境通了。
提示:如果启动时报
ClassNotFoundException,先检查 Maven 依赖是否下载完整,IDE 里执行一次mvn clean install;如果报NoSuchBeanDefinitionException,多半是配置文件没被扫描到,检查web.xml或 Spring 配置里的contextConfigLocation路径。
3. 权限模型拆解:Spring 拦截器 + RBAC 怎么落到表结构上
环境跑通只是第一步,真正决定这套 OA 能不能用在真实场景的,是权限模型。这份源码用的是经典的 RBAC(基于角色的访问控制),但它在 Spring 里的落地方式有几个细节值得细看。这一章我会把表结构、拦截器逻辑、以及前后端如何配合讲清楚,让你不仅能看懂,还能按自己公司的组织架构去改。
3.1 五张核心表与关联关系
RBAC 的核心思想是「用户不直接拥有权限,而是通过角色间接拥有」。这份源码里对应的表通常是:用户表、角色表、权限表、用户角色关联表、角色权限关联表。下面用表格把关键字段列出来,方便你对照 SQL 文件。
| 表名 | 关键字段 | 作用 |
|---|---|---|
| sys_user | id, username, password, dept_id, status | 存用户基本信息,dept_id 关联部门 |
| sys_role | id, role_name, role_key | 角色定义,role_key 用于代码里判断 |
| sys_menu | id, menu_name, perms, parent_id | 权限项,perms 是权限标识字符串 |
| sys_user_role | user_id, role_id | 用户和角色的多对多关系 |
| sys_role_menu | role_id, menu_id | 角色和权限的多对多关系 |
逻辑说明:sys_menu这张表既是菜单也是权限点,perms字段里存的是类似system:user:list、system:user:edit这样的字符串。前端根据菜单树渲染侧边栏,后端根据perms做接口级拦截。参数怎么改:如果你公司部门层级多,sys_dept表里会有parent_id和ancestors字段,用来做数据权限过滤,这个在请假审批里会用到——部门经理只能看本部门及下属部门的请假单。
3.2 拦截器与注解式权限校验
Spring 项目里做权限校验,常见做法是写一个HandlerInterceptor,在preHandle方法里判断当前登录用户是否拥有访问该 URL 所需的权限。这份源码里,权限标识通常通过自定义注解加在 Controller 方法上。
// 自定义权限注解 @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface RequiresPermissions { String value(); // 权限标识,如 "system:user:edit" } // 拦截器中的校验逻辑 public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (handler instanceof HandlerMethod) { HandlerMethod method = (HandlerMethod) handler; RequiresPermissions anno = method.getMethodAnnotation(RequiresPermissions.class); if (anno != null) { String perm = anno.value(); Set<String> userPerms = (Set<String>) request.getSession().getAttribute("perms"); if (userPerms == null || !userPerms.contains(perm)) { response.sendRedirect(request.getContextPath() + "/error/403"); return false; } } } return true; }逻辑说明:用户登录成功后,把该用户所有角色对应的perms集合查出来放进 Session。每次请求进入 Controller 前,拦截器读取方法上的注解,比对 Session 里的权限集合。参数说明:value()里的字符串必须和sys_menu.perms完全一致,大小写敏感。常见坑是注解写了但拦截器没注册,或者拦截器注册了但excludePathPatterns把登录接口也拦了,导致死循环。改的时候,登录、验证码、静态资源这些路径一定要排除。
3.3 前端菜单渲染与按钮级控制
后端权限控制住了接口,前端还要控制「看不到的菜单不显示、没权限的按钮不渲染」。这份源码的前端通常用 JSP 或 Thymeleaf 模板,登录后从 Session 里取菜单树,递归渲染。
<!-- 菜单渲染片段,逻辑示意 --> <c:forEach items="${menus}" var="menu"> <c:if test="${menu.parentId == 0}"> <li> <a href="${menu.url}">${menu.menuName}</a> <c:if test="${not empty menu.children}"> <ul> <c:forEach items="${menu.children}" var="child"> <li><a href="${child.url}">${child.menuName}</a></li> </c:forEach> </ul> </c:if> </li> </c:if> </c:forEach>逻辑说明:菜单树在登录时根据用户角色查出来,只包含该用户有权限访问的菜单。按钮级控制则是在页面里用权限标识做判断,比如<button shiro:hasPermission="system:user:add">新增</button>,如果项目没用 Shiro,就用自定义标签或 JSTL 判断 Session 里的权限集合。参数说明:菜单的url字段要和 Controller 的@RequestMapping对应,否则点了菜单会 404。改的时候注意,新增一个功能要同时加菜单记录、权限标识、Controller 注解、前端按钮判断,缺一个都会导致权限失效。
4. 审批流与考勤模块:从请假单到打卡记录的完整链路
权限是骨架,业务是血肉。这份 OA 源码里,请假审批和考勤打卡是使用频率最高的两个模块,也是最能体现「办公管理系统」价值的模块。这一章我会把请假单的提交、流转、审批、归档这条链路拆开,再把考勤打卡的数据写入和统计逻辑讲清楚。如果你要二次开发,这两个模块的代码最值得细读。
4.1 请假单的状态机与流转逻辑
请假单不是简单的增删改查,它有一个明确的状态流转:草稿 → 待审批 → 审批中 → 通过/驳回 → 归档。这份源码里,状态字段通常叫status,用数字或枚举表示。提交后,根据请假天数和申请人角色,决定审批层级。
// 请假单提交服务层逻辑示意 public void submitLeave(LeaveForm form, User user) { form.setStatus(LeaveStatus.PENDING); // 初始状态:待审批 form.setApplyTime(new Date()); leaveMapper.insert(form); // 根据天数决定审批人 if (form.getDays() <= 1) { // 一天以内,直属主管审批即可 approvalService.createTask(form.getId(), user.getDeptLeaderId()); } else { // 超过一天,主管审批后再由人事审批 approvalService.createTask(form.getId(), user.getDeptLeaderId()); approvalService.createTask(form.getId(), hrUserId); } }逻辑说明:这里的关键是「审批任务」的生成。每张请假单对应一条或多条审批任务,审批人处理完自己的任务后,系统判断是否还有下一级,有就继续流转,没有就更新请假单状态。参数说明:days的计算要排除周末和法定节假日,源码里通常有一个HolidayUtils工具类,读取配置的节假日表。改的时候注意,如果公司审批层级复杂,建议把审批规则抽成配置表,而不是硬编码在 if-else 里,否则每改一次流程就要重新发版。
4.2 考勤打卡的数据写入与防重复
考勤模块的核心是「谁、在什么时间、打了什么卡」。这份源码里,打卡记录表通常有user_id、punch_time、punch_type(上班/下班)、punch_date几个字段。为了防止同一天重复打上班卡,需要在插入前做校验。
// 打卡服务层逻辑示意 public Result punch(PunchType type, User user) { Date now = new Date(); String today = DateUtils.format(now, "yyyy-MM-dd"); // 查询当天该用户该类型的打卡记录 PunchRecord exist = punchMapper.selectByUserAndDateAndType(user.getId(), today, type); if (exist != null) { return Result.fail("今天已经打过" + type.getDesc() + "卡了"); } PunchRecord record = new PunchRecord(); record.setUserId(user.getId()); record.setPunchTime(now); record.setPunchType(type); record.setPunchDate(today); punchMapper.insert(record); return Result.success("打卡成功"); }逻辑说明:用user_id + punch_date + punch_type做唯一性校验,数据库层面也可以加唯一索引兜底。参数说明:punch_date单独存一个日期字段,而不是每次用punch_time去截取,是为了查询统计时走索引更快。改的时候注意,如果公司有外勤打卡,还要加经纬度字段和距离判断,这部分源码里可能没有,需要自己扩展。
4.3 考勤统计与异常判定
打卡数据有了,接下来是统计。常见需求是:每月每人出勤天数、迟到次数、早退次数、缺卡次数。这份源码里通常有一个定时任务或手动触发的统计方法,遍历打卡记录,和标准上下班时间比对。
// 考勤统计逻辑示意 public void statMonth(String month) { List<User> users = userMapper.selectAll(); for (User user : users) { List<PunchRecord> records = punchMapper.selectByUserAndMonth(user.getId(), month); int lateCount = 0, earlyCount = 0, missCount = 0; for (PunchRecord r : records) { if (r.getPunchType() == PunchType.ON) { if (r.getPunchTime().after(standardOnTime)) lateCount++; } else { if (r.getPunchTime().before(standardOffTime)) earlyCount++; } } // 缺卡判定:当天没有上班或下班记录 // 统计结果写入考勤汇总表 } }逻辑说明:标准上下班时间一般从配置表或系统参数里读,不要写死在代码里。参数说明:迟到早退的判定要留一个缓冲时间,比如上班时间 9:00,9:05 之前不算迟到,这个缓冲值也应该是可配置的。改的时候注意,跨天夜班、弹性工作制这些场景,简单的after/before判断会翻车,需要根据实际排班表来算。
5. 避坑与排查:这份 OA 源码最容易翻车的五个地方
源码跑通不难,难的是改完之后不出问题。这一章我整理了五个最常见的翻车场景,每个都按「现象 → 原因 → 解决」来说。这些都是实际拆解和二次开发时踩过的坑,你大概率也会遇到。
5.1 登录后菜单空白或 403
现象:账号密码正确,能进主页,但侧边栏菜单是空的,或者点任何菜单都跳 403 页面。
原因:菜单树查询依赖用户角色关联,如果sys_user_role表里没有给该用户配角色,或者角色对应的sys_role_menu没有记录,菜单就是空的。403 则是因为拦截器比对权限时,Session 里的perms集合为空。
解决:先查sys_user_role确认用户有角色,再查sys_role_menu确认角色有菜单权限。如果是新加的用户,记得在后台管理里分配角色,而不是直接改数据库——直接改容易漏掉缓存刷新。
5.2 中文乱码从数据库到页面
现象:公告内容或请假理由里的中文,存进数据库变成问号,或者页面上显示乱码。
原因:三个环节都可能出问题——数据库字符集、连接串字符集、页面编码。常见的是数据库建库时用了latin1,或者连接串没写characterEncoding=utf8。
解决:建库时强制utf8mb4,连接串加useUnicode=true&characterEncoding=utf8,JSP 页面顶部加<%@ page contentType="text/html;charset=UTF-8" %>。如果已经存了乱码数据,只能清掉重新录入,改字符集救不回来。
5.3 审批流卡在「审批中」不动
现象:请假单提交后,审批人处理了,但状态一直是「审批中」,没有变成「通过」或「驳回」。
原因:审批任务表里有多条任务,审批人只处理了自己那条,系统没有触发「检查是否还有未完成任务」的逻辑。或者审批人处理时更新了任务状态,但没有同步更新请假单主表状态。
解决:在审批处理方法里,处理完当前任务后,必须查一次该请假单下是否还有status=待处理的任务。如果没有,根据最后一条任务的审批结果更新主表状态。这个逻辑要放在事务里,避免并发时状态不一致。
5.4 考勤统计把周末算成缺卡
现象:每月统计出来,缺卡次数特别多,一看全是周末。
原因:统计逻辑遍历了整月的日期,没有排除周末和节假日。源码里可能只做了简单的日期循环,没有读节假日配置。
解决:统计前先加载当月的节假日列表,循环时跳过这些日期。如果公司有调休,还要把调休上班的周末算进去。这个配置建议做成后台可维护的,不要写死在代码里。
5.5 文件上传后找不到路径
现象:公告附件或请假证明上传成功,但下载时 404,或者服务器重启后文件丢失。
原因:上传路径写的是相对路径,或者写在了项目部署目录里,重新部署后目录被覆盖。也可能是上传目录没有写权限。
解决:上传路径配置成绝对路径,比如/data/oa/upload/,并且确保运行项目的用户对该目录有读写权限。数据库里只存文件名或相对路径,下载时再拼绝对路径。不要存完整 URL,换域名或端口后就失效了。
注意:这五个坑里,权限和审批流的问题最隐蔽,因为不报错,只是功能不正常。建议每次改完权限或流程,都用不同角色的账号各走一遍完整流程。
6. 二次开发进阶:把硬编码的审批层级改成可配置的规则引擎
如果你只是拿这份源码做课程设计,前面五章够用了。但如果你打算把它用在真实公司环境,迟早会遇到「审批流程要改」的需求。这一章讲一个具体技巧:把硬编码的 if-else 审批层级,改成基于配置表的规则引擎。这样以后调整审批人,只需要改数据,不用改代码、不用重新发版。
6.1 审批规则表设计
先加一张oa_approval_rule表,核心字段包括:规则 ID、适用部门、请假天数下限、请假天数上限、审批层级、审批人角色。这样一条规则就能表达「技术部,请假 1 到 3 天,先由部门主管审批,再由人事审批」。
| 字段 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| dept_id | bigint | 适用部门,0 表示全部 |
| days_min | int | 天数下限,含 |
| days_max | int | 天数上限,含 |
| level | int | 审批层级,从 1 开始 |
| approver_role | varchar | 审批人角色标识 |
逻辑说明:提交请假单时,根据申请人的部门和请假天数,查出所有匹配的规则,按level排序,依次生成审批任务。参数说明:days_min和days_max要覆盖所有可能的天数,否则会出现没有匹配规则、审批任务生成失败的情况。建议加一条兜底规则,比如days_min=0, days_max=999, level=1, approver_role=dept_leader。
6.2 规则匹配与任务生成代码
// 基于规则的审批任务生成 public void createApprovalTasks(LeaveForm form, User user) { // 查询匹配的规则:部门匹配或全部部门,天数在范围内 List<ApprovalRule> rules = ruleMapper.selectMatching(user.getDeptId(), form.getDays()); if (rules.isEmpty()) { throw new BusinessException("没有匹配的审批规则,请联系管理员"); } // 按层级排序,逐级生成任务 rules.sort(Comparator.comparingInt(ApprovalRule::getLevel)); for (ApprovalRule rule : rules) { Long approverId = userService.findUserByRoleAndDept(rule.getApproverRole(), user.getDeptId()); if (approverId == null) { throw new BusinessException("找不到审批人,角色:" + rule.getApproverRole()); } approvalService.createTask(form.getId(), approverId, rule.getLevel()); } }逻辑说明:selectMatching的 SQL 要同时考虑部门为 0 的全局规则和部门匹配的规则,按天数范围过滤。参数说明:findUserByRoleAndDept如果找到多个审批人,需要决定是任选一个还是全部审批,常见做法是取第一个或配置一个默认审批人。改的时候注意,规则表的数据要有人维护,建议在后台加一个规则管理页面,否则每次改流程都要找 DBA。
6.3 验证方法与回滚策略
改完规则引擎后,怎么验证它是对的?我的习惯是准备三组测试数据:一组一天以内的请假、一组三天的请假、一组跨部门的请假。分别用不同部门的账号提交,然后检查oa_approval_task表里生成的任务数量、审批人、层级顺序是否符合规则表配置。
如果发现任务生成不对,先查规则表的匹配逻辑,再查审批人查找逻辑。回滚策略很简单:规则引擎的代码可以保留,但把createApprovalTasks的调用切回原来的硬编码方法,改一个配置开关就行。这样即使规则引擎出问题,业务也不会中断。
从那以后我每次改审批流程,都强制走一遍「规则表配置 → 提交测试单 → 查任务表 → 走完审批 → 查主表状态」这个完整链路,确认无误才上生产。希望帮到你。
本文还有配套的精品资源,点击获取