接高校人事系统项目久了会发现,真正难的不是功能多,而是“看起来简单”的流程细节特别磨人。最近和朋友一起复盘了一个springboot-java高校人事教师请假工资管理系统,它属于典型的高校内网业务平台,核心覆盖三块:教师基础人事信息、请假流程审批、工资核算与发放。听上去不复杂,但实际上手做起来,光状态流转和权限边界就能绕晕一批新手。
这篇文章我打算把整个系统的思路、结构、核心业务流程、数据库设计、技术难点和部署经验全拆开讲一遍。讲的时候不会刻意端着“框架教程”的架子,更多的像是一线干活的人聊项目:为什么这么设计,哪里容易踩坑,怎么排查问题。如果你正在做类似的人事OA、请假审批、薪资管理类系统,或者打算拿springboot+java做毕业设计、转行练手项目,这篇可以直接当参考底稿。
1. 项目整体设计与模块架构思路
1.1 高校人事系统的核心痛点与功能需求
高校和普通企业的人事系统最大的差异在于编制、岗位、职称、系部归属这些维度比企业复杂得多。教师请假不只涉及到调课,还牵动着课时费结算、考勤统计、工资核算,所以这三个模块不能做成彼此孤立的功能堆叠,要把数据流串起来。
这个系统的需求可以拆成四个大块:
- 系统管理:用户登录、角色权限、部门(院系)管理、菜单管理、操作日志。
- 人事档案:教师基本信息、入职状态、职称信息、所属院系、联系方式、异动记录。
- 请假管理:请假申请、流程审批(多级审批)、销假、请假记录查询、按月度统计汇总。
- 工资管理:基础工资设置、课时费/补贴核算、扣款(请假扣款、考勤扣款)、月度工资单生成、历史工资查询与导出。
模块之间的关联逻辑要很清楚:教师请假获批后,考勤模块才能拿到请假天数,工资模块再根据请假类型(病假、事假、公出)来决定是否扣款、扣多少。如果后期加了调课管理,还需要更新课表状态,这里就体现出一个后台管理系统的数据联动设计思路。
用SpringBoot做这类项目,我个人觉得比用传统的SSH或纯粹Servlet要省心得多。SpringBoot自带Tomcat、自动配置、Starter依赖管理,开发阶段可以快速跑起来,部署阶段打jar包也简单。再加上Java在企业级应用里的稳定生态,学校信息中心一般都有技术积累,维护起来不会出现“人走技术断层”的情况。
1.2 技术栈选型:为什么是SpringBoot+Java+前端分离
后台框架用SpringBoot,前端这块我建议直接上Vue,做成前后端分离。如果你的项目要以最稳妥的方式部署到学校机房服务器,更省事的是用Thymeleaf做服务端渲染。考虑到这个系统涉及审批、表单、数据报表,交互复杂度不算低,前后端分离的体验好很多。
技术栈清单大概是:
| 层级 | 选型 |
|---|---|
| 后端框架 | Spring Boot 2.x / 3.x,Java 8/11 |
| 权限认证 | Spring Security + JWT |
| ORM | MyBatis-Plus(或 Spring Data JPA) |
| 数据库 | MySQL 5.7 / 8.0 |
| 定时任务 | Quartz 或 Spring Task |
| 消息队列 | ActiveMQ 可选,用于异步通知 |
| 前端 | Vue 2/3 + Element UI / Element Plus |
| 部署 | Docker Desktop / 云服务器,jar 包部署 |
这里要特别说一下版本选择。最近很多人问“SpringBoot版本太高是不是坑”,尤其是用JDK8的老项目升级时,经常踩到 javax 变 jakarta 的兼容性问题。如果你还在用JDK8,SpringBoot老老实实选 2.7.x 系列就好;新项目若用JDK17,可以上SpringBoot 3.x,但要注意MyBatis-Plus、一些老工具包对SpringBoot 3的适配进度。
1.3 项目目录结构与代码组织规范
实际编码之前,先把包结构定好。很多新手一上来就把所有Controller挤在一个包,写到后面找代码全靠全局搜索。我常用的分层结构是:
code复制com.school.hrms
├── common // 通用结果返回、异常处理、工具类
├── config // 配置类:Security、JWT、Swagger、Quartz
├── controller // 接口层
├── service // 业务层
│ └── impl
├── mapper // MyBatis-Plus Mapper接口
├── entity // 数据库实体
├── dto // 请求/响应对象
├── vo // 视图对象,如工资单VO、请假审批VO
└── job // 定时任务
Controller层只做参数接收和结果封装,不写业务逻辑;Service层承载业务规则;Mapper层只做数据访问。有人觉得“Service还要写个接口再写个实现类”很啰嗦,但放到协作开发场景里,这个分层能减少很多冲突,而且便于后面写单元测试时做mock。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 数据库设计与核心表结构
2.1 核心数据表概览
我建表的时候坚持一个原则:同一个业务主体的数据尽量聚合在一张主表+若干扩展表里,不要为了省事把所有字段扔进一张大宽表。这个系统的核心表有这些:
| 表名 | 说明 |
|---|---|
| sys_user | 系统用户表(登录账号、密码、角色) |
| sys_role | 角色表 |
| sys_menu | 菜单权限表 |
| teacher_info | 教师人事信息表 |
| dept_info | 院系/部门表 |
| leave_apply | 请假申请表 |
| leave_approval | 请假审批流水表 |
| salary_config | 工资项目配置表 |
| salary_record | 工资月度记录表 |
教师人事信息和登录用户我拆成了两张表,原因是登录用户只是身份凭证,教师信息里有大量业务字段,如果合在一张表里,权限控制会变复杂,而且未来接入统一身份认证时不好解耦。
2.2 请假审批的状态流转是怎么设计的
请假申请最重要的字段有两个:current_status(当前状态)和approval_step(当前审批层级)。不能用单一状态字段去代替审批层级,因为高校老师请假往往需要两级审批:系主任初审、院领导终审,甚至还有教务处备案环节。
我设计的状态值是:
code复制0-待提交,1-系部审批中,2-院级审批中,3-已通过,4-已驳回,5-已撤销,6-已销假
要特别注意“已销假”和“已通过”的区别。请假通过后,如果老师提前回来,要把状态改成“已销假”,同时更新实际请假天数。工资核算的时候只能用实际销假天数,不能用申请天数。
leave_apply表的关键字段:
| 字段 | 类型 | 说明 |
|---|---|---|
| apply_no | varchar(32) | 请假单号,可生成如 JQ20250115001 |
| teacher_id | bigint | 教师ID |
| leave_type | tinyint | 1-事假,2-病假,3-公出,4-婚假,5-产假 |
| start_time | datetime | 开始时间 |
| end_time | datetime | 结束时间 |
| leave_days | decimal(4,1) | 请假天数,精确到0.5天 |
| current_status | tinyint | 当前状态 |
| approval_step | tinyint | 当前审批层级 |
| actual_days | decimal(4,1) | 实际请假天数 |
2.3 工资记录表需要留足扩展字段
工资表比请假表更容易设计失误。初学者容易把每个工资项设成单独字段,比如basic_salary、position_salary、performance_salary。这种做法在工资项固定的情况下够用,但高校的工资发放规则经常调整,比如加一笔“疫情补贴”或“专项奖励”,这种设计就要改表结构。
更推荐的方案是用 salary_record(月度汇总)+ salary_detail(工资明细项)两表。salary_detail里存每个工资项目名称、金额、是否扣款项。这样无论将来怎么调整工资项,都不用动表结构,只需往明细表插数据就行。
salary_record表里需要留一个record_month字段,用varchar(6)存储格式如 202501,不要用date类型,因为工资总是按整月查询,字符串比较效率不差,而且避免时区、格式带来的坑。状态字段status也建议设计:0-草稿,1-已确认,2-已发放,3-已作废。
3. 核心功能落地与代码实现细节
3.1 基于JWT的登录认证与权限控制
学校系统不比其他互联网应用,用户角色很清晰:教师、系部管理员、院级管理员、系统管理员。我选的是Spring Security + JWT这套组合。核心点在于:
- 登录成功后签发JWT,返回token到前端。
- 前端请求头携带
Authorization: Bearer token。 - 后端配置
OncePerRequestFilter做token校验。 - 接口上使用
@PreAuthorize("hasRole('ADMIN')")做方法级权限控制。
Swagger放行这一点每次都会被问。JWT拦截器要把/swagger-ui/**、/v3/api-docs/**(SpringBoot 2.x是/v2/api-docs)这些路径排除掉,不然在线调试接口永远401。我习惯在SecurityConfig里定义一个白名单数组,把登录接口、swagger路径、静态资源路径都放进去。代码类似于:
java复制@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers("/auth/login", "/swagger-ui/**", "/v3/api-docs/**", "/doc.html").permitAll()
.anyRequest().authenticated()
.and()
.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
return http.build();
}
3.2 请假业务的完整闭环
请假模块是系统的核心,实现一个完整的业务闭环需要这样一套逻辑:
第一步:提交申请
教师在前端选择请假类型、开始结束时间,系统自动计算出 leave_days,默认按自然日计算,但支持0.5天粒度。如果请假天数超过5天,需要强制填写“课程安排说明”,这是一个典型的业务校验规则。
第二步:逐级审批
审批人登录后只看到待自己审批的单子。规则是:
- 请假天数 ≤ 3天:系主任审批通过就完结。
- 3天 < 天数 ≤ 7天:系主任初审通过后,院级领导审批,通过即完结。
- 天数 > 7天:系主任、院级领导都通过后,需要教务处备案。
代码里我用了一个简单的工厂+策略模式处理等级审批逻辑。每次审批通过后,更新approval_step并重新判断是否走完流程;如果被驳回,直接把整个申请单状态置为驳回,流程终止。审批记录必须落表到leave_approval,保留每一步的操作人、操作时间、意见。
第三步:销假
销假操作只有教师本人能操作,系统自动计算实际请假天数,写入actual_days字段。如果销假时发现实际请假天数大于审批天数,提醒教师重新走审批流程。这个细节特别重要,不然“多请了”没法统计扣款。
3.3 工资核算的定时任务与报表导出
工资核算我用了策略模式来处理不同扣款项的计算规则,避免写一堆if-else。默认扣款规则是:
- 事假:扣日工资
- 病假:扣50%日工资
- 公出/婚假/产假:不扣款
日工资 = 月工资基数 / 21.75(这是按国家规定月计薪天数算的)。用decimal类型存金额和天数,避免浮点误差。
定时任务用Quartz,每月25号自动生成下个月工资草稿单:
java复制@Component
public class SalaryGenerateJob implements Job {
@Override
public void execute(JobExecutionContext context) {
// 1. 查询所有在职教师
// 2. 获取上月请假统计数据
// 3. 按工资规则核算各项金额
// 4. 写入salary_record和salary_detail
// 5. 发送站内通知,提醒人事科确认
}
}
这里要踩的坑是:Quartz的cron表达式和Spring Task的cron表达式规则略有不同,复制配置时容易出错。我用的是MySQL锁表的方式防止定时任务重复执行,简单说就是在一个sys_task_lock表里记录任务执行状态,执行前先update状态为“执行中”,如果影响行数为0,说明有其他节点在执行,直接跳过。
工资导出用EasyExcel或POI生成Excel报表。EasyExcel对大数据量的导出内存控制好很多,并且提供了@ExcelProperty注解直接映射VO类,代码量比POI少一半。
4. 开发中的高发问题与排查实录
4.1 SpringBoot版本与JDK版本匹配的坑
最近看到很多人搜索“springboot版本太高”“源发行版17需要目标发行版17”这类问题。我自己的建议是:如果不是要用SpringBoot 3的新特性(比如GraalVM原生镜像),用JDK8+SpringBoot 2.7.x是最稳的组合。
有一个很常见的场景:IDEA里编译一切正常,但maven打包时报“java: 警告: 源发行版 17 需要目标发行版 17”。本质是maven的compiler插件指定的Java版本和项目里的JDK版本不匹配。在pom.xml里显式指定:
xml复制<properties>
<java.version>1.8</java.version>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
</properties>
如果你用SpringBoot 3.x,默认要求JDK17,这种情况下pom里的java.version要改成17,然后确保IDEA的Project SDK和Maven的JRE都指向JDK17。很多“版本太高”的问题不是框架本身不兼容,而是本地装了多个JDK,IDEA和Maven用到了不同版本。
4.2 循环依赖问题怎么解决
SpringBoot 2.6版本开始,默认禁止循环依赖。项目里如果出现类似“Constructor循环依赖”的报错,并且你不方便升级代码,有两种办法:一是在application.yml里设置spring.main.allow-circular-references=true,但这种方式只是掩耳盗铃,不推荐;另一种是重构代码,把互相依赖的Service拆开,抽出第三个Service来管理公共逻辑。
实际项目中,最常见的循环依赖是A Service调用B Service,B Service又调回A Service。我在工资模块里就遇到过:SalaryService要调用TeacherService获取教师信息,TeacherService又要调用SalaryService查询教师历史工资用于职级评定。解决方式是新增一个TeacherSalaryQueryService,专门负责跨模块查询,两个原Service都只依赖这个新Service,问题就消除了。
4.3 ActiveMQ消息异步处理
这个系统里我加了ActiveMQ做异步通知。比如请假审批通过后,系统要通知老师、通知工资核算模块做预扣款计算。这些操作不需要同步返回,直接丢到队列里异步处理就行。
整合ActiveMQ时最容易遇到的问题就是SpringBoot 2.x的ActiveMQ客户端和ActiveMQ服务端版本不一致,导致连接报错。如果在本地用Docker跑ActiveMQ:
bash复制docker run -d --name activemq -p 61616:61616 -p 8161:8161 rmohr/activemq
项目配置:
yaml复制spring:
activemq:
broker-url: tcp://localhost:61616
user: admin
password: admin
确认8161端口(Web控制台)能打开,然后再排查代码问题。我之前碰到过“连接被拒绝”其实是broker-url里写成了vm://localhost,这个默认是在JVM内存里跑一个broker,不连外部MQ。
4.4 资源映射和文件上传下载
高校系统里有一个高频需求:上传课程表、上传请假附件、下载工资条。SpringBoot默认静态资源路径是classpath:/static/,但上传文件要存在可持久化的目录里,不能放在jar包内部,否则重启就丢。
我的做法是配置自定义资源映射:
java复制@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
// 把 /files/** 映射到服务器绝对路径
registry.addResourceHandler("/files/**")
.addResourceLocations("file:/opt/hrms/upload/");
}
}
上传大文件时会遇到内存溢出,尤其当tomcat默认限制1MB时,前端传个大文件直接413或报错。在application.yml里调大限制:
yaml复制spring:
servlet:
multipart:
max-file-size: 100MB
max-request-size: 200MB
JVM内存也要跟着调整。用“java: outofmemoryerror: insufficient memory”这样的报错,多半是启动jar包时没指定内存参数。部署时用:
bash复制java -Xms512m -Xmx1024m -jar hrms-system.jar
4.5 单元测试与集成测试的边界
说到测试,很多人会把单元测试写成“启动整个Spring容器”的集成测试。比如用@SpringBootTest去测一个Service方法,执行一次要加载所有配置,非常慢,而且依赖数据库。我建议的测试分层是:
- 用Mockito测试Service层的业务规则,不依赖数据库。
- 用
@MybatisPlusTest(或@DataJpaTest)测试Mapper层时用H2内存数据库。 - Controller层用MockMvc做接口级测试,重点测JWT拦截器。
请假扣款规则测试可以这样写:
java复制@Test
void testCalculateLeaveDeduction() {
when(teacherMapper.selectById(1L)).thenReturn(teacher);
SalaryCalculator calculator = new SalaryCalculator();
BigDecimal deduction = calculator.calculateDeduction(
new LeaveStat(3.0, LeaveType.SHI_JIA), new BigDecimal(8000));
assertEquals(0, deduction.compareTo(new BigDecimal("1103.45")));
}
4.6 SpringBoot自动装配原理与常见困惑
很多人在调试时会疑惑:为什么引入一个starter依赖,相关的功能就自动生效了?这背后的机制是@EnableAutoConfiguration。SpringBoot利用spring.factories或AutoConfiguration.imports文件加载所有候选的自动配置类,再通过@ConditionalOnClass、@ConditionalOnMissingBean等条件注解按需生效。
理解这个原理对排查问题特别重要。比如加了一个Redis的依赖,但Redis配置不正确,启动不报错,运行时报空指针,原因就是RedisAutoConfiguration被加载,但连接没配上。排查时看启动日志里的“AutoConfigurationReport”能帮你快速定位哪些配置类生效了。
如果你不想引入某些自动配置,可以直接排除:
java复制@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
4.7 Docker Desktop打包JDK8镜像
最后说部署。学校服务器环境一般不统一,用Docker部署最省心。我项目里用的Dockerfile:
dockerfile复制FROM openjdk:8-jdk-alpine
LABEL maintainer="hrms-team"
COPY target/hrms-system.jar /app/hrms-system.jar
WORKDIR /app
EXPOSE 8080
ENV JAVA_OPTS="-Xms512m -Xmx1024m -Dfile.encoding=UTF-8"
ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar hrms-system.jar"]
在Docker Desktop上跑的话,要留意文件挂载路径。Windows环境建议:
yaml复制volumes:
- D:/hrms/upload:/app/upload
- D:/hrms/logs:/app/logs
5. 基于个人经验的优化建议与扩展方向
系统做完以后,如果时间允许,可以从这几个方向继续打磨:
数据权限精细化。目前是角色控制接口权限,但同一个角色下,系部管理员只能看本系部的数据。这种数据权限不能只靠SQL里加WHERE dept_id = ?,建议用一个自定义的数据权限拦截器,根据当前登录用户解析出可见院系范围,动态拼到SQL上。
工资条电子签收。工资查询不能只做到“查看”,可以加一个确认签收环节,教师确认后记录时间戳。这在人事审计时有很大帮助,也能减少薪酬争议。
审批流程可配置化。当前审批层级是写死在代码里的。如果换成Flowable或Activiti工作流引擎,就能在管理界面上自定义审批流程。但坏处是学习成本和系统复杂度会明显上升。小规模人事系统用状态机就足够了,没必要为了“高大上”引入工作流引擎。
考勤和课表联动。如果学校已经有课表系统,请假通过后应该自动标记调课信息;如果暂时没有接口,可以在请假审批通过时给教务管理员发送待办提醒,人工确认调课安排。
数据备份和日志审计。学校系统对数据完整性要求很高。建议每天凌晨自动备份MySQL数据,同时用AOP记录关键操作日志(谁在什么时间改了什么审批状态)。这个日志不仅要记录到数据库表,还要同步滚动到文件,方便追溯。
我在实际做这个项目时,有两点体会很深。第一,业务规则远比技术框架复杂,写代码前把请假和工资的边界条件梳理清楚,比选什么技术栈重要得多。第二,不要一开始就把功能铺得太满,先跑通“请假提交-审批-工资计算”这条主干流程,再接权限、报表、邮件通知这些外围功能,这样每个阶段都有可演示的成果,团队协作时也不容易乱。
最后再分享一个小技巧:如果你准备拿这个项目去面试,不要只讲CRUD。可以重点谈谈状态机设计、策略模式在工资核算中的应用、Quartz集群任务防重复执行以及JWT无状态登录的优缺点,这些小点才是面试官真正愿意听的内容。希望这个项目拆解能帮到正准备做类似系统的朋友,祝你们少踩坑、早交付。
