1. 项目背景与核心需求
在大学校园管理场景中,学生请假审批流程的数字化一直是教务管理的痛点。传统纸质请假条存在易丢失、审批周期长、数据难追溯等问题。我去年为某高校开发这套系统时,教务主任反映高峰期每天要处理200多张纸质假条,经常出现审批积压和记录混乱的情况。
这个基于SpringBoot的请销假系统主要解决三个核心问题:
- 审批流程线上化(缩短处理时间从平均2天到2小时)
- 多级审批权限配置(辅导员→院系主任→教务处三级联动)
- 请假数据可视化统计(自动生成月度考勤报表)
系统上线后,该高校的请假审批效率提升近80%,特别在疫情期间突发请假场景中展现出明显优势。下面我会从技术选型到具体实现,完整还原这个项目的开发过程。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 技术栈选型分析
2.1 为什么选择SpringBoot
作为基础框架,SpringBoot 2.7.3版本提供了几个关键优势:
- 内嵌Tomcat服务器:简化部署流程,打包成jar即可运行
- 自动配置:通过spring-boot-starter-web快速构建RESTful API
- 监控管理:集成Actuator端点方便后期运维
对比传统SSM框架,开发效率提升约40%。实测在4核8G服务器上,单个实例可支撑500+并发请假申请。
2.2 MySQL数据库设计考量
使用MySQL 8.0而非5.7版本,主要考虑:
- JSON字段支持:存储动态审批意见
- 窗口函数:方便生成请假统计报表
- 性能提升:查询速度比5.7快2倍以上
配置建议:
sql复制# 创建专用数据库时建议配置
CREATE DATABASE leave_management
DEFAULT CHARACTER SET utf8mb4
COLLATE utf8mb4_unicode_ci;
2.3 前端技术选型
虽然标题未提及,但实际采用Vue3+Element Plus实现管理后台,主要因为:
- 组件化开发效率高
- 与SpringBoot天然适配(通过axios通信)
- 丰富的图表库支持数据可视化
3. 核心功能模块实现
3.1 请假申请模块
关键实体设计:
java复制@Entity
public class LeaveApplication {
@Id @GeneratedValue(strategy=GenerationType.IDENTITY)
private Long id;
@ManyToOne
private Student student;
private LocalDateTime startTime;
private LocalDateTime endTime;
private String reason;
@Enumerated(EnumType.STRING)
private LeaveType type; // 病假/事假/公假
@OneToMany(mappedBy="application", cascade=CascadeType.ALL)
private List<ApprovalRecord> records;
}
踩坑提醒:时间字段必须使用LocalDateTime而非Date,否则前端序列化会出问题
3.2 多级审批流程实现
采用状态机模式设计审批流程:
mermaid复制stateDiagram
[*] --> PENDING
PENDING --> APPROVED_BY_TUTOR: 辅导员审批
PENDING --> REJECTED: 拒绝
APPROVED_BY_TUTOR --> APPROVED_BY_DEPARTMENT: 院系审批
APPROVED_BY_DEPARTMENT --> APPROVED_BY_OFFICE: 教务处审批
APPROVED_BY_OFFICE --> [*]
对应Spring状态机配置:
java复制@Configuration
@EnableStateMachineFactory
public class StateMachineConfig extends EnumStateMachineConfigurerAdapter<LeaveState, LeaveEvent> {
@Override
public void configure(StateMachineStateConfigurer<LeaveState, LeaveEvent> states) throws Exception {
states.withStates()
.initial(LeaveState.PENDING)
.states(EnumSet.allOf(LeaveState.class));
}
@Override
public void configure(StateMachineTransitionConfigurer<LeaveState, LeaveEvent> transitions) throws Exception {
transitions
.withExternal()
.source(LeaveState.PENDING)
.target(LeaveState.APPROVED_BY_TUTOR)
.event(LeaveEvent.TUTOR_APPROVE)
.and()
// 其他状态转换...
}
}
3.3 销假与统计模块
销假逻辑的特殊处理:
java复制@Transactional
public void confirmReturn(Long applicationId) {
LeaveApplication app = repository.findById(applicationId)
.orElseThrow(() -> new BusinessException("请假记录不存在"));
if(app.getActualEndTime() == null) {
app.setActualEndTime(LocalDateTime.now());
// 自动计算是否逾期
if(app.getActualEndTime().isAfter(app.getEndTime())) {
app.setStatus(LeaveStatus.OVERDUE);
warningService.notifyStudent(app.getStudent());
}
repository.save(app);
}
}
统计查询使用JPA Specification实现动态查询:
java复制public Page<LeaveStatsVO> getStats(LeaveQuery query, Pageable pageable) {
return repository.findAll((root, cq, cb) -> {
List<Predicate> predicates = new ArrayList<>();
if(query.getStudentId() != null) {
predicates.add(cb.equal(root.get("student").get("id"), query.getStudentId()));
}
if(query.getMonth() != null) {
predicates.add(cb.between(root.get("startTime"),
query.getMonth().atDay(1).atStartOfDay(),
query.getMonth().atEndOfMonth().atTime(23, 59, 59)));
}
return cb.and(predicates.toArray(new Predicate[0]));
}, pageable).map(this::convertToStatsVO);
}
4. 关键问题解决方案
4.1 高并发请假申请处理
测试发现当并发量超过300时,数据库出现死锁。解决方案:
- 添加乐观锁版本控制:
java复制@Entity
public class LeaveApplication {
@Version
private Integer version;
//...
}
- 采用Redis缓存热门数据:
java复制@Cacheable(value = "applications", key = "#id")
public LeaveApplication getById(Long id) {
return repository.findById(id).orElse(null);
}
- 消息队列削峰:
java复制@KafkaListener(topics = "leave-apply")
public void handleApply(LeaveApplyDTO dto) {
// 异步处理核心业务逻辑
}
4.2 审批通知实时推送
采用WebSocket实现:
java复制@RestController
@RequiredArgsConstructor
public class NotificationController {
private final SimpMessagingTemplate template;
@PostMapping("/approve")
public void approve(@RequestBody ApproveDTO dto) {
// 审批逻辑...
template.convertAndSendToUser(
dto.getStudentId().toString(),
"/queue/notifications",
new Notification("您的请假已审批"));
}
}
前端配合SockJS建立连接:
javascript复制const socket = new SockJS('/ws');
const stompClient = Stomp.over(socket);
stompClient.connect({}, () => {
stompClient.subscribe(`/user/${userId}/queue/notifications`,
(message) => showToast(message.body));
});
4.3 数据安全防护
- SQL注入防护:
java复制@Repository
public interface LeaveRepository extends JpaRepository<LeaveApplication, Long> {
// 使用参数化查询
@Query("SELECT l FROM LeaveApplication l WHERE l.student.name LIKE %:name%")
List<LeaveApplication> findByStudentName(@Param("name") String name);
}
- XSS过滤:
java复制public class XssFilter implements Filter {
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
chain.doFilter(new XssRequestWrapper((HttpServletRequest) req), res);
}
}
// 包装器中使用Jsoup.clean()处理输入
5. 部署与性能优化
5.1 生产环境配置
推荐服务器配置:
- 4核8G内存(支持2000+学生规模)
- CentOS 7.6+
- OpenJDK 17
- MySQL 8.0配置建议:
ini复制[mysqld]
innodb_buffer_pool_size = 2G
innodb_log_file_size = 256M
max_connections = 500
5.2 性能调优实战
- JVM参数优化:
bash复制java -jar -Xms2g -Xmx2g -XX:MaxMetaspaceSize=512m \
-XX:+UseG1GC -XX:MaxGCPauseMillis=200 \
-Dspring.profiles.active=prod \
leave-system.jar
- 慢查询优化案例:
sql复制-- 优化前(执行时间1.8s)
EXPLAIN SELECT * FROM leave_application
WHERE student_id IN (SELECT id FROM student WHERE class_id = 101);
-- 优化后(0.2s)
CREATE INDEX idx_student_class ON student(class_id);
SELECT la.* FROM leave_application la
JOIN student s ON la.student_id = s.id
WHERE s.class_id = 101;
5.3 监控方案
集成Prometheus + Grafana监控:
yaml复制# application.yml
management:
endpoints:
web:
exposure:
include: health,info,metrics,prometheus
metrics:
export:
prometheus:
enabled: true
关键监控指标:
- 平均审批耗时(99线应<500ms)
- 活跃会话数(预警阈值>800)
- MySQL连接池使用率(>80%需扩容)
6. 项目扩展方向
在实际运行中,我们陆续增加了以下功能:
- 微信小程序端:学生可通过微信提交请假
- 智能审批:对病假自动验证医院证明真伪
- 疫情关联:与健康码系统对接,自动核验风险地区
一个特别实用的扩展是课表冲突检测:
java复制public void checkScheduleConflict(LeaveApplication app) {
List<CourseSchedule> courses = scheduleService
.getByStudentAndTime(app.getStudent(), app.getStartTime(), app.getEndTime());
if(!courses.isEmpty()) {
throw new BusinessException("请假时间与以下课程冲突:" +
courses.stream().map(CourseSchedule::getCourseName)
.collect(Collectors.joining(",")));
}
}
这个系统从第一版开发到目前稳定运行,经历了三次大的架构调整。最大的体会是:前期良好的领域模型设计比技术选型更重要。比如我们早期将"审批流程"硬编码在业务逻辑中,后来改用状态机模式后才真正实现灵活的流程配置。
