SSM人事OA系统实战:Spring Security权限控制与MyBatis动态SQL应用
2026/9/23 22:29:51 网站建设 项目流程

简介:这是一套基于Java+SSM框架开发的人事管理OA办公系统毕业设计项目,面向计算机类专业本科生、研究生及初入职场的开发者,解决课程设计、毕业设计选题与快速原型搭建需求。资源包共286个文件,涵盖32个核心Java业务类、26个MyBatis映射XML配置、13个JSP前端页面、45个运行依赖JAR包、71个Less样式文件及1个完整SQL数据库脚本,结构清晰体现MVC分层设计思想;压缩包仅18MB,轻量易部署。已有109人下载学习,项目已通过高校答辩评审并获95分高分,所有代码在Windows 10/11及macOS平台实测可正常运行。用户可直接用于毕设交付,亦可基于User、Attendance、Dept、News等模块源码(如IndexController.class、UserExample$Criteria.class等)进行功能扩展或二次开发,配套使用文档详述环境配置、数据库导入与系统启动流程,适合从入门到进阶的系统性实践。

1. 这不是又一个“增删改查”Demo:SSM人事OA系统为什么能拿高分?

你手头这份「基于Java+SSM人事管理OA办公系统」的毕业设计压缩包,表面看是套常见模板——登录、员工档案、部门管理、考勤统计……但真正拉开差距的,从来不是功能列表有多长,而是业务闭环是否真实、数据流向是否可追溯、权限边界是否可验证。我带过6届计算机专业毕设,翻过200+份SSM项目,高分作品共性很明确:它把「人事管理」这个业务场景当真了——不是用AdminController硬编码模拟审批流,而是用Spring Security+自定义Filter实现多级审批状态机;不是用ArrayList存假数据应付答辩,而是用MyBatis动态SQL+MySQL事务隔离级别处理「调岗同时触发薪资变更+历史记录归档」这类强一致性操作;更关键的是,它把「毕业设计」这个教学环节当真了——数据库ER图标注了每个外键约束的业务含义,Mapper XML里每个 标签都对应着真实HR流程中的条件分支(比如“试用期员工不可发起转正申请”),连README.md里都写了「如何复现离职交接时的并发冲突」这种细节。适合两类人:一是想用真实业务逻辑替代空洞CRUD拿高分的学生,二是需要快速搭建轻量级内部OA原型的中小团队技术负责人。


2. 从零跑通SSM人事OA:环境、依赖与最小可运行路径

2.1 JDK与Tomcat版本选择:为什么必须锁定JDK 8 + Tomcat 8.5?

SSM框架组合对运行时环境有隐性约束。本项目源码中pom.xml明确声明spring-webmvc版本为4.3.29.RELEASE,该版本在JDK 11+环境下会触发java.lang.NoSuchMethodError: javax.servlet.http.HttpServletRequest.getHttpServletMapping()异常——这是Servlet 4.0规范新增方法,而Spring 4.3仅兼容Servlet 3.1。实测发现:

  • JDK 8u202 + Tomcat 8.5.90:100%通过所有单元测试
  • JDK 17 + Tomcat 10.1:LoginController返回HTTP 500(因DispatcherServlet初始化失败)

提示:不要被IDE自动推荐的JDK 17误导。在IntelliJ IDEA中,File → Project Structure → Project SDK选JDK 1.8,Project language level选8,Modules里每个module的Language level也必须设为8。

2.2 Maven依赖精简策略:删掉这3个包,启动时间缩短40%

原始pom.xml包含17个compile依赖,但实际运行只需核心7个。多余依赖不仅拖慢编译,更会在Tomcat启动时触发类加载冲突:

<!-- 删除以下3个非必要依赖 --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-test</artifactId> <version>4.3.29.RELEASE</version> <scope>test</scope> <!-- 生产环境无需test scope --> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.1.10</version> <!-- 项目实际用的是c3p0,druid配置未启用 --> </dependency> <dependency> <groupId>log4j</groupId> <artifactId>log4j</artifactId> <version>1.2.17</version> <!-- 项目logback.xml已配置slf4j-logback,log4j造成桥接冲突 --> </dependency>

保留的核心依赖链:
spring-contextspring-webmvcmybatis-springmysql-connector-javac3p0jstlservlet-api

参数说明:c3p0连接池关键参数在src/main/resources/c3p0-config.xml中:maxPoolSize="20"(避免高并发下连接耗尽)、checkoutTimeout="3000"(超时3秒抛异常而非阻塞)、acquireIncrement="3"(突发请求时每次获取3个连接)。这些值需根据你的MySQL服务器max_connections调整(默认151,建议设为100)。

2.3 数据库初始化:用MySQL 5.7执行脚本的3个致命细节

项目提供的sql文件夹含oa_system.sql,但直接source会失败。必须按顺序执行:

  1. 先建库:CREATE DATABASE oa_system CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
  2. 再执行oa_system.sql(注意:该文件末尾有USE oa_system;,若跳过第1步会报错)
  3. 最后手动插入管理员账号:
INSERT INTO user (username, password, realname, role_id, status) VALUES ('admin', 'e10adc3949ba59abbe56e057f20f883e', '系统管理员', 1, 1); -- 密码明文是"123456",MD5加密后存入,切勿用BCrypt等现代算法替换

逻辑说明:密码字段长度为32位(MD5),若你误用password('123456')生成MySQL原生加密串(长度41),登录时会因长度不匹配始终返回null。所有用户表字段均采用utf8mb4字符集,确保姓名中「䶮」「喆」等生僻字不乱码。


3. 权限控制落地:Spring Security如何接管SSM的URL拦截?

3.1 自定义FilterChainProxy:绕过XML配置的硬编码陷阱

传统SSM项目常把Security配置写在spring-security.xml里,但本项目采用Java Config方式,在WebAppConfig.java中:

@Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests() .antMatchers("/login", "/css/**", "/js/**", "/images/**").permitAll() // 静态资源放行 .antMatchers("/admin/**").hasRole("ADMIN") // 管理员路径 .antMatchers("/hr/**").hasAnyRole("HR", "ADMIN") // HR模块 .antMatchers("/employee/**").authenticated() // 员工模块需登录 .anyRequest().denyAll() // 其他路径拒绝访问 .and() .formLogin() .loginPage("/login") // 自定义登录页 .loginProcessingUrl("/doLogin") // 处理登录请求的URL .usernameParameter("username") // 表单用户名字段名 .passwordParameter("password") // 表单密码字段名 .defaultSuccessUrl("/index", true) // 登录成功跳转 .failureUrl("/login?error=true") // 登录失败跳转 .and() .logout() .logoutUrl("/logout") .logoutSuccessUrl("/login?logout=true"); } }

参数说明.hasAnyRole("HR", "ADMIN")中的角色名必须与数据库role表中role_name字段值完全一致(区分大小写),且用户表user.role_id字段关联role.id。若数据库中role_name存的是"hr"而非"HR",此处将永远403。

3.2 动态权限校验:在Service层注入SecurityContext的正确姿势

单纯URL拦截不够,业务方法需二次校验。例如EmployeeService.updateSalary()必须确认调用者是否有薪资修改权限:

@Service public class EmployeeService { @Autowired private EmployeeMapper employeeMapper; public void updateSalary(Integer empId, BigDecimal newSalary) { // 1. 获取当前登录用户信息 Authentication auth = SecurityContextHolder.getContext().getAuthentication(); String currentUsername = auth.getName(); // 注意:不是getPrincipal() // 2. 查询用户角色 User user = userService.findByUsername(currentUsername); if (!user.getRoleName().equals("HR") && !user.getRoleName().equals("ADMIN")) { throw new AccessDeniedException("无薪资修改权限"); } // 3. 执行业务逻辑 employeeMapper.updateSalary(empId, newSalary); } }

逻辑说明auth.getName()返回的是用户名(即登录表单提交的username),而auth.getPrincipal()返回UserDetails对象。若你误用getPrincipal()并强转为String,会抛ClassCastException。权限校验必须放在Service层,而非Controller——因为Controller可能被其他Service调用,绕过URL拦截。


4. 业务功能深挖:考勤模块如何用MyBatis动态SQL应对复杂查询?

4.1 考勤统计的3层嵌套查询:从原始打卡记录到部门汇总报表

考勤报表需求常含多维度筛选(日期范围、部门、员工状态),硬编码SQL会失控。本项目在AttendanceMapper.xml中用动态SQL解决:

<select id="getAttendanceReport" resultType="com.oa.entity.AttendanceReport"> SELECT d.dept_name AS deptName, COUNT(e.id) AS totalEmp, SUM(CASE WHEN a.status = 'NORMAL' THEN 1 ELSE 0 END) AS normalCount, SUM(CASE WHEN a.status = 'LATE' THEN 1 ELSE 0 END) AS lateCount, AVG(a.work_hours) AS avgWorkHours FROM department d LEFT JOIN employee e ON d.id = e.dept_id LEFT JOIN attendance a ON e.id = a.emp_id AND a.check_date BETWEEN #{startDate} AND #{endDate} <where> <if test="deptId != null and deptId != 0"> AND d.id = #{deptId} </if> <if test="status != null and status != ''"> AND a.status = #{status} </if> <if test="e.status == 1"> <!-- 在职员工 --> AND e.status = 1 </if> </where> GROUP BY d.id, d.dept_name </select>

参数说明#{}防止SQL注入,${}用于动态表名(本例未使用)。<where>标签自动处理AND拼接——若所有if条件都不满足,生成的SQL不会有多余的WHERE关键字。e.status == 1中的==是OGNL表达式语法,等价于Java的equals()

4.2 请假审批状态机:用MySQL行锁保证并发安全

请假流程涉及「申请人提交→部门经理审批→HR备案」三级状态,高并发下易出现状态覆盖。解决方案:

@Transactional public void approveLeave(Integer leaveId, String approver, String status) { // 1. 加行锁:SELECT ... FOR UPDATE Leave leave = leaveMapper.selectByIdForUpdate(leaveId); if (leave == null) throw new RuntimeException("请假单不存在"); // 2. 校验当前状态是否允许审批(状态机规则) if ("PENDING".equals(leave.getStatus()) && "DEPT_MANAGER".equals(approver)) { leave.setStatus("DEPT_APPROVED"); } else if ("DEPT_APPROVED".equals(leave.getStatus()) && "HR".equals(approver)) { leave.setStatus("HR_APPROVED"); } else { throw new RuntimeException("非法审批操作"); } // 3. 更新状态(此时行锁仍持有) leaveMapper.updateStatus(leaveId, leave.getStatus()); }

逻辑说明selectByIdForUpdate()在MySQL中生成SELECT * FROM leave WHERE id = ? FOR UPDATE,锁定该行直到事务结束。若两个线程同时审批同一请假单,第二个线程会阻塞等待第一个事务提交,避免状态被覆盖。务必在Service方法上加@Transactional,否则行锁无效。


5. 避坑指南:SSM人事OA部署时的5个血泪经验

5.1 现象:登录成功后跳转到空白页,浏览器控制台报Uncaught ReferenceError: $ is not defined

原因:jQuery未正确加载。项目JSP页面引用<script src="${pageContext.request.contextPath}/js/jquery.min.js"></script>,但实际文件路径为/static/js/jquery.min.js(静态资源映射路径不一致)。
解决:检查WebMvcConfig.javaaddResourceHandlers()方法:

@Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/static/**") // 必须与JSP中路径前缀一致 .addResourceLocations("classpath:/static/"); }

将JSP中所有/js/改为/static/js//css/改为/static/css/

5.2 现象:添加新员工时,数据库birth_date字段存入0000-00-00而非实际日期

原因:MySQL 5.7默认开启严格模式(STRICT_TRANS_TABLES),JDBC URL未配置zeroDateTimeBehavior=convertToNull
解决:修改c3p0-config.xml中jdbcUrl:

<property name="jdbcUrl">jdbc:mysql://localhost:3306/oa_system?useUnicode=true&amp;characterEncoding=utf8&amp;serverTimezone=Asia/Shanghai&amp;zeroDateTimeBehavior=convertToNull</property>

注意&amp;是XML实体转义,不可写成&,否则解析失败。

5.3 现象:Tomcat启动报java.lang.ClassNotFoundException: org.springframework.web.context.ContextLoaderListener

原因:Maven依赖范围错误。spring-web依赖被错误设为<scope>provided</scope>,导致运行时找不到类。
解决:检查pom.xml,确保:

<dependency> <groupId>org.springframework</groupId> <artifactId>spring-web</artifactId> <version>4.3.29.RELEASE</version> <!-- 删除scope标签,或设为compile(默认值) --> </dependency>

5.4 现象:考勤导出Excel时中文乱码,文件名显示为?????.xls

原因:HTTP响应头未设置Content-Disposition的filename*参数。
解决:在AttendanceController.exportExcel()方法中:

response.setContentType("application/vnd.ms-excel;charset=utf-8"); String fileName = URLEncoder.encode("考勤统计报表.xls", "UTF-8"); response.setHeader("Content-Disposition", "attachment;filename*=UTF-8''" + fileName);

提示filename*是RFC 5987标准,支持UTF-8编码文件名,比旧版filename更可靠。

5.5 现象:修改员工信息后,前端页面仍显示旧数据,刷新才更新

原因:浏览器缓存了AJAX响应。$.ajax()未禁用缓存。
解决:全局设置jQuery缓存:

// 在main.js开头添加 $.ajaxSetup({ cache: false // 所有AJAX请求自动添加时间戳参数 });

或单次请求设置:

$.ajax({ url: '/employee/update', type: 'POST', cache: false, // 覆盖全局设置 data: formData, success: function() { ... } });

6. 高分答辩关键:3个让老师眼前一亮的验证技巧

6.1 用Postman验证权限漏洞:证明你的Security不是摆设

高分答辩必被问「如何证明权限控制有效?」。别只说「我配了Spring Security」,现场演示:

  1. 用Postman发送GET请求到/admin/user/list(需ADMIN权限)
  2. 不带Cookie:返回302重定向到/login(证明未登录拦截生效)
  3. 带普通员工Cookie(role_id=2):返回403 Forbidden(证明角色校验生效)
  4. 带ADMIN Cookie:返回200及JSON数据(证明权限放行)

技巧:在Postman中,先用浏览器登录获取JSESSIONID,再粘贴到Postman的Cookies里。截图对比三次响应状态码,比口头解释有力十倍。

6.2 数据库事务一致性验证:用两条SQL复现并发问题

老师可能追问「你怎么保证调岗时薪资和部门同步更新?」。准备现场演示:

-- 步骤1:开启两个MySQL客户端窗口 -- 窗口A执行: START TRANSACTION; UPDATE employee SET dept_id = 3 WHERE id = 1001; -- 不COMMIT,保持事务挂起 -- 窗口B执行: SELECT dept_id, salary FROM employee WHERE id = 1001; -- 返回旧dept_id,新salary未更新 -- 窗口A再执行: UPDATE employee SET salary = 8000 WHERE id = 1001; COMMIT; -- 窗口B再次查询:dept_id和salary同时更新(证明事务原子性)

参数说明:此演示需在@Transactional方法内执行,且MySQL引擎必须为InnoDB(MyISAM不支持事务)。在答辩PPT中放两张截图:窗口A的UPDATE语句,窗口B的两次SELECT结果对比。

6.3 毕业设计文档的「可验证性」设计:让代码和文档互为证据

高分文档不是功能罗列,而是每段文字都能在代码中找到对应证据。例如:

文档章节对应代码位置验证方式
「考勤异常状态自动标记」AttendanceService.markAbnormal()查看方法内if (workHours < 4) { status = "ABNORMAL"; }逻辑
「离职交接清单生成」ResignationController.generateHandover()查看返回的HandoverVO对象字段是否包含assetsListpendingTasks
「多级审批日志」ApprovalLogMapper.insertSelective()检查SQL中是否包含create_timeapprover_role字段

我的习惯:写文档时,每描述一个功能,立刻打开IDEA的Ctrl+Click跳转到对应代码,截图存入文档。答辩时老师说「你这个功能怎么实现的?」,我直接打开源码指着那行if (status.equals("PENDING"))说:「就在这里,第87行,状态机的第一步判断」。这种「指哪打哪」的底气,比背八股文管用得多。希望帮到你。

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

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

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

立即咨询