简介这是一套基于JSP与MySQL开发的人事管理系统毕业设计项目面向计算机专业本科生及Java Web初学者聚焦企业人力资源信息的信息化集成管理适用于课程设计、毕设选题与Servlet/JSP技术实践。资源包共108个文件包含12个JSP页面实现用户交互逻辑、11个Java源码与11个Class字节码涵盖AdminService、StaffInfoAdd等核心业务类、10个HTML静态页、6个CSS样式文件、1个SQL数据库脚本及27张界面截图JPG整体压缩后仅2.04MB轻量易部署。已有1447人学习下载资源结构完整含lib目录下的数据库配置与DAO层实现如BaseDao、AdminDaoImpl并体现典型MVC分层设计思想。读者可直接导入运行快速掌握登录校验、员工信息增删改查、管理员权限控制等关键功能模块的代码组织与数据库联动逻辑。1. 这不是又一个“登录增删改查”的毕设模板它真能跑通员工异动审批流连离职交接单都自动生成PDF去年帮某高校计算机系三位同学调试毕设翻了二十多个标着“人事管理系统JSPMySQL”的压缩包八成卡在登录页404、五成连Tomcat都起不来、剩下两个倒是能进首页——但点“部门管理”就500查“员工档案”直接空指针。直到打开这个项目它没用Spring Boot偷懒老老实实写Servlet跳转没把MySQL建表语句藏在Word里而是放在sql/目录下带完整注释最关键是——它把“员工调岗申请→直属领导审批→HR备案→系统自动更新岗位信息生成交接清单”这条真实业务链路用纯JSPServletJDBC串成了闭环。适合Java Web零基础但肯啃源码的本科生也适合想快速搭个轻量级内部管理后台的中小团队技术负责人。它不炫技但每行代码都在解决一个具体问题比如离职流程里如何让“最后工作日”字段自动触发考勤停算、社保停缴提醒、门禁权限回收三个动作。2. 环境搭建与运行验证从解压到看到首页我只用了17分钟2.1 JDKTomcatMySQL三件套的版本选择玄学这项目没写pom.xml是纯传统Web应用所以版本兼容性比Maven项目更敏感。我试过JDK 17配Tomcat 10——直接报javax.servlet找不到因为Tomcat 10已全面切到Jakarta EE 9命名空间jakarta.servlet。血泪经验必须用JDK 8u202 Tomcat 8.5.90 MySQL 5.7。为什么不是最新版看web.xml头声明?xml version1.0 encodingUTF-8? web-app xmlnshttp://xmlns.jcp.org/xml/ns/javaee xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttp://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd version3.1version3.1明确指向Servlet 3.1规范而Tomcat 8.5是最后一个完整支持Servlet 3.1且不强制要求Jakarta命名空间的稳定版。MySQL 5.7则是因为建表SQL里用了datetime DEFAULT CURRENT_TIMESTAMP这是5.6才支持的语法但5.8又开始限制DEFAULT对datetime的使用方式5.7最稳妥。提示JDK 8安装后务必配置JAVA_HOMETomcat解压后修改bin/catalina.batWindows或bin/catalina.shLinux里的JAVA_HOME路径别信网上说的“自动识别”。2.2 数据库初始化三步走别跳过第二步项目根目录下的sql/hr_system.sql是唯一建库脚本但它不是“一键执行就能用”的类型。我拆解成三步第一步手动创建数据库CREATE DATABASE hr_system CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;注意用utf8mb4而非utf8——项目里员工姓名字段有生僻字如“䶮”、“堃”utf8在MySQL 5.7中实际只支持3字节UTF-8存不进去会变问号。第二步执行SQL前的关键替换打开hr_system.sql找到第12行INSERT INTO user (username, password, role) VALUES (admin, e10adc3949ba59abbe56e057f20f883e, admin);这个密码是123456的MD5e10adc3949ba59abbe56e057f20f883e但项目登录逻辑在LoginServlet.java里是明文比对// LoginServlet.java 第45行 if (123456.equals(password)) { // 注意这里硬编码了密码 session.setAttribute(user, user); response.sendRedirect(main.jsp); }所以你得把SQL里的密码值换成123456否则数据库插进去了登录还是失败。这是典型“开发写死密码测试忘了改”的坑。第三步导入并验证mysql -u root -p hr_system hr_system.sql然后连上数据库执行SELECT COUNT(*) FROM employee; -- 应该返回10默认插入10条测试员工数据 SELECT * FROM user WHERE usernameadmin; -- 确认密码字段是1234562.3 Tomcat部署别把整个项目文件夹拖进去很多同学解压后直接把人事管理系统文件夹整个拖进webapps/结果访问http://localhost:8080/人事管理系统/报404。原因Tomcat要求Web应用根目录必须是标准结构而这个项目解压后是人事管理系统/ ├── src/ ← Java源码 ├── WebContent/ ← 实际Web根目录含WEB-INF、jsp、css等 ├── sql/ └── README.txt正确操作是只把WebContent/目录下的全部内容包括WEB-INF文件夹复制到webapps/hr_system/然后启动Tomcat。最终目录结构应为webapps/hr_system/ ├── WEB-INF/ │ ├── web.xml │ └── classes/ ← 编译后的.class文件放这里见2.4节 ├── login.jsp ├── main.jsp └── css/访问地址就是http://localhost:8080/hr_system/login.jsp。2.4 编译Class文件手动生成比IDE自动编译更可控项目没提供.class文件必须自己编译。别急着开Eclipse——先用命令行确认路径# 进入项目src目录 cd 人事管理系统/src # 编译所有Servlet注意-cp参数必须包含servlet-api.jar javac -cp D:\apache-tomcat-8.5.90\lib\servlet-api.jar \ -d ../WebContent/WEB-INF/classes \ *.java关键点-cp指定servlet-api.jar路径否则import javax.servlet.*报错-d参数必须指向WEB-INF/classes这是Tomcat加载类的默认路径编译后检查classes/目录下是否有LoginServlet.class、EmployeeServlet.class等文件一个都不能少。注意如果提示package com.hr.util does not exist说明src/com/hr/util/下的工具类没编译。此时要递归编译javac -cp D:\tomcat\lib\servlet-api.jar -d ../WebContent/WEB-INF/classes com/hr/util/*.java3. 核心模块拆解一个调岗申请从提交到生效的完整链路3.1 前端交互层JSP里的隐藏逻辑比想象中多项目没用任何前端框架但transfer_apply.jsp调岗申请页藏着三个关键设计第一动态部门下拉框的异步加载页面底部有这段JavaScript// transfer_apply.jsp 第188行 function loadDepartments() { var xhr new XMLHttpRequest(); xhr.open(GET, DepartmentServlet?actionlist, true); xhr.onreadystatechange function() { if (xhr.readyState 4 xhr.status 200) { var departments JSON.parse(xhr.responseText); var select document.getElementById(toDept); select.innerHTML ; // 清空原有选项 departments.forEach(function(dept) { var option document.createElement(option); option.value dept.id; option.text dept.name; select.appendChild(option); }); } }; xhr.send(); }它通过DepartmentServlet?actionlist获取JSON格式部门列表。这个Servlet在doGet()里调用DepartmentDAO.getAllDepartments()再用Gson转成JSON项目lib/目录下有gson-2.8.6.jar。注意这里没做CSRF防护生产环境必须加token校验。第二表单提交前的双重校验// transfer_apply.jsp 第215行 function validateForm() { var reason document.getElementById(reason).value.trim(); var date document.getElementById(effectiveDate).value; if (!reason || reason.length 10) { alert(调岗原因至少10个字); return false; } if (new Date(date) new Date()) { alert(生效日期不能早于今天); return false; } return true; // 通过校验才提交 }前端校验只是用户体验优化真正的业务规则在TransferApplyServlet.java的doPost()里二次校验比如检查“目标部门是否已满编”。第三PDF生成按钮的静默触发页面右上角有个“生成交接单”按钮点击后执行document.getElementById(pdfBtn).onclick function() { window.open(TransferPDFServlet?applyId applyId, _blank); };TransferPDFServlet收到请求后调用com.hr.pdf.PDFGenerator.createTransferPDF(applyId)用iText 5.5.13生成PDF。生成的PDF文件名是transfer_20240515_001.pdf年月日序号存放在WebContent/pdfs/目录下浏览器直接下载。3.2 后端控制层Servlet如何协调DAO与业务逻辑以TransferApplyServlet.java为例它处理调岗申请的全生命周期第一步参数解析与封装// TransferApplyServlet.java 第32行 String empId request.getParameter(empId); String toDeptId request.getParameter(toDept); String reason request.getParameter(reason); String effectiveDate request.getParameter(effectiveDate); TransferApply apply new TransferApply(); apply.setEmpId(Integer.parseInt(empId)); apply.setToDeptId(Integer.parseInt(toDeptId)); apply.setReason(reason); apply.setEffectiveDate(new SimpleDateFormat(yyyy-MM-dd).parse(effectiveDate)); apply.setStatus(pending); // 初始状态为待审批这里用SimpleDateFormat解析日期但没做异常捕获——如果用户手动改了HTML表单的日期格式会直接500。生产环境必须用try-catch包裹并返回友好错误页。第二步事务控制与状态流转// TransferApplyServlet.java 第65行 Connection conn null; try { conn DBUtil.getConnection(); // 从DBUtil获取连接 conn.setAutoCommit(false); // 开启事务 // 1. 插入调岗申请记录 TransferApplyDAO.insert(apply, conn); // 2. 更新员工当前部门临时标记为“调岗中” EmployeeDAO.updateDeptStatus(apply.getEmpId(), transferring, conn); conn.commit(); request.setAttribute(msg, 调岗申请已提交请等待审批); } catch (SQLException e) { if (conn ! null) conn.rollback(); request.setAttribute(msg, 系统繁忙请重试); } finally { if (conn ! null) conn.close(); }关键点DBUtil.getConnection()用的是DriverManager.getConnection()没用连接池。这意味着高并发时可能耗尽数据库连接。如果要上线必须替换为Druid或HikariCP。第三步审批状态变更的回调机制当HR在approve_transfer.jsp点击“同意”触发TransferApproveServlet// TransferApproveServlet.java 第42行 if (agree.equals(action)) { TransferApplyDAO.updateStatus(applyId, approved, conn); // 关键同步更新员工主表部门ID EmployeeDAO.updateDepartment(empId, toDeptId, conn); // 触发交接单生成异步避免阻塞响应 new Thread(() - PDFGenerator.createTransferPDF(applyId)).start(); }这里用Thread.start()异步生成PDF防止用户等太久。但没做线程池管理大量并发时可能OOM。建议改用Executors.newFixedThreadPool(3)。3.3 数据访问层DAO里的SQL拼接与防注入实践EmployeeDAO.java的searchByCondition()方法展示了传统JDBC如何安全拼接动态SQL// EmployeeDAO.java 第89行 public ListEmployee searchByCondition(String name, String dept, String status) { ListEmployee list new ArrayList(); StringBuilder sql new StringBuilder(SELECT * FROM employee WHERE 11); ListString params new ArrayList(); // 存储参数值 if (name ! null !name.trim().isEmpty()) { sql.append( AND name LIKE ?); params.add(% name.trim() %); } if (dept ! null !dept.trim().isEmpty()) { sql.append( AND dept_id ?); params.add(dept.trim()); } if (status ! null !status.trim().isEmpty()) { sql.append( AND status ?); params.add(status.trim()); } try (PreparedStatement ps conn.prepareStatement(sql.toString())) { for (int i 0; i params.size(); i) { ps.setString(i 1, params.get(i)); // 参数索引从1开始 } ResultSet rs ps.executeQuery(); while (rs.next()) { list.add(mapToEmployee(rs)); } } return list; }这种写法比字符串拼接name LIKE % name %安全得多完全规避SQL注入。但注意params列表和ps.setString()的索引必须严格对应否则会ArrayIndexOutOfBoundsException。4. 避坑指南那些让我重启三次Tomcat的致命细节4.1 现象登录成功后跳转到main.jsp但页面顶部显示“欢迎null”原因LoginServlet.java里设置session属性的key写错了。原文第48行session.setAttribute(usernmae, user); // 注意这里是usernmae少了个e而main.jsp第12行读取时写的是% session.getAttribute(username) %key不一致导致取不到值。解决统一改为username并在所有JSP和Servlet中全局搜索替换。4.2 现象添加新员工时出生日期选了1995-05-20保存后数据库里变成0000-00-00原因MySQL的date类型对格式极其敏感1995-05-20是合法格式但EmployeeDAO.insert()方法里用的是ps.setDate()而传入的java.util.Date对象没经过java.sql.Date转换。原文第62行ps.setDate(4, (java.sql.Date) birthDate); // 强制转换会失败java.util.Date不能直接强转java.sql.Date。解决改为ps.setDate(4, new java.sql.Date(birthDate.getTime()));4.3 现象导出PDF时浏览器下载的文件打不开提示“文件已损坏”原因TransferPDFServlet.java里设置了错误的响应头。原文第35行response.setContentType(application/pdf); response.setHeader(Content-Disposition, attachment; filenametransfer.pdf);但没设置Content-Length且PDF二进制流写入后没调用response.getOutputStream().flush()。解决补全响应头并刷新流File pdfFile new File(getServletContext().getRealPath(/pdfs/) fileName); response.setContentLength((int) pdfFile.length()); response.setHeader(Content-Disposition, attachment; filename fileName); Files.copy(pdfFile.toPath(), response.getOutputStream()); response.getOutputStream().flush(); // 必须刷新4.4 现象修改员工信息后页面显示更新成功但数据库里没变化原因EmployeeServlet.java的update分支里UPDATE语句的WHERE条件写成了id?但参数却传了empId员工编号而数据库主键是id自增整数。原文第102行ps.setInt(8, employee.getEmpId()); // 错应该用employee.getId()解决Employee实体类里必须有id字段主键getEmpId()是员工工号getId()才是数据库主键。更新时用setId()设置主键值。4.5 现象Tomcat启动时报错java.lang.ClassNotFoundException: com.mysql.jdbc.Driver原因MySQL 5.7驱动类名已从com.mysql.jdbc.Driver改为com.mysql.cj.jdbc.Driver而DBUtil.java第22行还写着旧类名。原文Class.forName(com.mysql.jdbc.Driver); // MySQL 5.1-5.6写法解决改为Class.forName(com.mysql.cj.jdbc.Driver); // MySQL 5.7写法同时确保lib/目录下是mysql-connector-java-8.0.28.jar不是5.x版本。5. 二次开发实战给离职流程加个“社保停缴倒计时”提醒5.1 需求分析为什么倒计时比静态日期更有价值某公司HR反馈员工提交离职申请后系统只显示“最后工作日2024-06-30”但没人记得那天是几号星期几更不知道社保缴纳截止日是当月15号。结果常出现“人走了社保还在交”的财务损失。所以我们要在resign_detail.jsp里加一个动态倒计时实时显示“距离社保停缴还有X天”且当倒计时≤3天时文字变红色并弹窗提醒。5.2 前端改造JSP里嵌入JavaScript倒计时在resign_detail.jsp的离职信息区块下方插入!-- resign_detail.jsp 第142行 -- div idcountdown stylefont-size:18px; font-weight:bold; margin-top:10px; span idcountdown-text加载中.../span /div script function startCountdown() { // 从JSP获取最后工作日格式2024-06-30 var lastWorkDateStr % request.getAttribute(lastWorkDate) %; if (!lastWorkDateStr || lastWorkDateStr null) return; var lastWorkDate new Date(lastWorkDateStr); // 社保停缴日 最后工作日所在月的15号 var cutoffDate new Date(lastWorkDate.getFullYear(), lastWorkDate.getMonth(), 15); // 如果15号已过则顺延到下月15号 if (cutoffDate new Date()) { cutoffDate new Date(lastWorkDate.getFullYear(), lastWorkDate.getMonth() 1, 15); } function updateCountdown() { var now new Date(); var diffTime cutoffDate - now; var daysLeft Math.ceil(diffTime / (1000 * 60 * 60 * 24)); var element document.getElementById(countdown-text); if (daysLeft 0) { element.innerHTML 社保已停缴; element.style.color green; } else if (daysLeft 3) { element.innerHTML ⚠️ 距离社保停缴还有 daysLeft 天; element.style.color red; // 弹窗提醒仅首次进入时 if (!sessionStorage.getItem(countdownAlerted)) { alert(重要提醒社保停缴日临近请确认停缴操作); sessionStorage.setItem(countdownAlerted, true); } } else { element.innerHTML 距离社保停缴还有 daysLeft 天; element.style.color black; } } updateCountdown(); setInterval(updateCountdown, 60000); // 每分钟更新一次 } window.onload startCountdown; /script关键点sessionStorage保证弹窗只触发一次避免用户反复刷新页面被骚扰cutoffDate计算逻辑覆盖了“15号已过则顺延”的边界情况setInterval设为60秒而非1秒减少浏览器性能消耗。5.3 后端增强在ResignServlet里注入倒计时所需数据ResignServlet.java的doGet()方法需要把lastWorkDate传给JSP// ResignServlet.java 第58行 String sql SELECT * FROM resignation WHERE id ?; try (PreparedStatement ps conn.prepareStatement(sql)) { ps.setInt(1, Integer.parseInt(id)); ResultSet rs ps.executeQuery(); if (rs.next()) { Resignation resign new Resignation(); resign.setId(rs.getInt(id)); resign.setEmpId(rs.getInt(emp_id)); resign.setLastWorkDate(rs.getDate(last_work_date)); // 数据库字段 // 新增把日期格式化为字符串传给JSP request.setAttribute(lastWorkDate, new SimpleDateFormat(yyyy-MM-dd).format(rs.getDate(last_work_date))); request.setAttribute(resign, resign); } }注意rs.getDate(last_work_date)返回java.sql.Date必须用SimpleDateFormat转成字符串否则JSP里% ... %会输出toString()的乱码。5.4 验证与上线 checklist改造完成后按顺序验证功能验证新建离职申请设置最后工作日为2024-05-20打开详情页确认倒计时显示“距离社保停缴还有 X 天”边界验证把最后工作日设为2024-05-10刷新页面确认倒计时变为红色并弹窗性能验证打开浏览器开发者工具Network标签下确认resign_detail.jsp加载时间500ms无额外HTTP请求兼容验证在Chrome/Firefox/Edge中分别测试确认倒计时正常工作安全验证手动修改URL中的lastWorkDate参数为scriptalert(1)/script确认JSP输出被HTML转义不执行JS。从那以后我每次改JSP里的JavaScript都强制走一遍“修改→编译→重启Tomcat→清浏览器缓存→F5刷新”全流程。因为JSP的热加载在Tomcat 8.5里经常失效缓存的旧JS会让你以为代码没生效其实只是浏览器在耍赖。希望帮到你本文还有配套的精品资源点击获取