
简介基于JSP的企业人事管理系统是一套面向毕业设计场景的Java Web项目完整覆盖员工信息管理、注册登录、考勤记录、薪酬核算、绩效评估、培训安排和统计报表等模块适合正在做课程设计或准备毕业答辩的学生参考。压缩包共有277个文件整体大小仅3.18MB包含104个JSP视图页面、14个Java源文件与28个class编译产物还有CSS样式、JavaScript脚本、jpg/gif图片、XML与MF配置等辅助资源结构清晰导入开发工具后即可查看运行。项目报告以5个doc文档提供需求分析、系统数据库E-R设计、编码实现和测试说明并配有系统截图和使用步骤方便快速复现与二次开发目前已有51人学习/下载适合作为初学JSPServletMySQL的实战范例。通过阅读源码和设计文档可以理清JSP动态页面、Servlet请求处理、数据库交互等关键技术链路为独立完成同类型管理系统打下扎实基础。1. 这套JSP企业人事管理系统为什么值得在毕设季选中它每年这个时候需要毕业设计的计算机类学生都会在选题上磨一阵子。导师要的从来不是想法炫酷而是能演示、有报告、代码被问到能讲出原理。基于JSP的企业人事管理系统恰好卡在这个需求上技术栈经典、功能边界清楚、数据库关系型设计有讲头交付形态通常就是源代码加项目报告打成一个压缩包拿来改改就能跑。系统做的事也很直观管理员登录后维护员工档案、部门、考勤和工资典型的中小型企业人事管理场景。适合两类人一是想在最短时间把毕设落地、把精力省给答辩的学生二是想用老技术快速搭一套内部工具的管理员。这篇笔记按实际动手顺序来写先拆需求建表再写代码链路最后讲部署和避坑。2. 从需求到数据库先拆出5张核心表再谈写代码2.1 功能模块先定边界登录、员工、部门、考勤、工资各自做什么拿到一个压缩包先别急着启动。我一般会花半小时把里头的项目报告目录扫一遍把功能模块画在纸上。人事管理系统看着复杂拆开就五块登录与用户管理、员工档案管理、部门管理、考勤管理、工资管理。权限上做一个两级角色就够了管理员能进所有页面普通用户只能看自己的个人信息展示页面。很多毕设翻车就翻在功能没边界比如试图做考勤打卡对接第三方接口最后卡在外部对接上。JSP系统里考勤全部手工录入迟到早退状态用下拉框选毕业设计的评分点在于增删改查完整、数据表关联正确不在外部集成多高级。模块定清楚之后再画数据关系。员工属于部门考勤和工资都挂在员工名下管理员是一个独立用户实体而不是员工表里的某个人。这样画出来数据库该建几张表就呼之欲出了。下面2.2节就是按这个关系拆出来的表设计。2.2 数据库设计5张表和它们的字段取舍建表是整套系统最不能省的一步。表建错了后面写代码全是补丁。我按上面的关系拆成5张核心表如下表名说明关键字段与其他表的关系user管理员登录表user_id, username, password, role独立存在不挂在员工表下department部门表dept_id, dept_name, manager被employee引用employee员工档案表emp_id, emp_no, emp_name, dept_id, position, gender, phone, email, hire_date, statusdept_id外键关联departmentattendance考勤表att_id, emp_id, att_date, check_in, check_out, statusemp_id外键关联employeesalary工资表sal_id, emp_id, base_salary, bonus, deduct, pay_dateemp_id外键关联employee字段设计有三个细节要说明。第一employee表里的status建议用tinyint1代表在职0代表离职不要直接delete员工记录离职员工的历史考勤和工资还挂在名下删了会对不齐。第二attendance表里最好对(emp_id, att_date)建唯一约束防止同一天给同一个人录两遍。第三salary表的pay_date建议只精确到月用DATE类型配合唯一索引(emp_id, pay_date)保证一个人一个月只出一笔工资。还有一张表很多源码包里会出现但我不建议当核心表就是公告表。公告这个功能适合当锦上添花的加分模块写报告时提一句基于文件或简单表实现了公告发布但不要在它身上花时间做主流程答辩评委很少盯着公告问。提示如果你的数据库里员工和部门关系想做得更规范可以加一张部门负责人关联表但5张表已经能覆盖全部功能外加关联表只会让代码量凭空增加。2.3 初始化SQL脚本建库、建表、灌入测试数据数据库设计落地的第一步是跑初始化脚本。下面这段SQL是整个系统的地基我按从上到下的顺序建表和插入数据-- 建库utf8mb4 是中文不乱码的关键 CREATE DATABASE IF NOT EXISTS hrms DEFAULT CHARACTER SET utf8mb4; USE hrms; -- 1. 管理员表 CREATE TABLE IF NOT EXISTS user ( user_id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL, -- 存MD5值不存明文 role VARCHAR(20) DEFAULT admin ) ENGINEInnoDB; -- 2. 部门表 CREATE TABLE IF NOT EXISTS department ( dept_id INT PRIMARY KEY AUTO_INCREMENT, dept_name VARCHAR(50) NOT NULL UNIQUE, manager VARCHAR(30) ) ENGINEInnoDB; -- 3. 员工表dept_id 外键指向部门 CREATE TABLE IF NOT EXISTS employee ( emp_id INT PRIMARY KEY AUTO_INCREMENT, emp_no VARCHAR(20) NOT NULL UNIQUE, emp_name VARCHAR(30) NOT NULL, dept_id INT, position VARCHAR(30), gender CHAR(1), phone VARCHAR(20), hire_date DATE, status TINYINT DEFAULT 1, CONSTRAINT fk_emp_dept FOREIGN KEY (dept_id) REFERENCES department(dept_id) ) ENGINEInnoDB; -- 4. 考勤表(emp_id, att_date) 联合唯一防重复录入 CREATE TABLE IF NOT EXISTS attendance ( att_id INT PRIMARY KEY AUTO_INCREMENT, emp_id INT NOT NULL, att_date DATE NOT NULL, check_in TIME, check_out TIME, status VARCHAR(10), UNIQUE KEY uk_att (emp_id, att_date), CONSTRAINT fk_att_emp FOREIGN KEY (emp_id) REFERENCES employee(emp_id) ) ENGINEInnoDB; -- 5. 工资表 CREATE TABLE IF NOT EXISTS salary ( sal_id INT PRIMARY KEY AUTO_INCREMENT, emp_id INT NOT NULL, base_salary DECIMAL(10,2), bonus DECIMAL(10,2), deduct DECIMAL(10,2), pay_date DATE, CONSTRAINT fk_sal_emp FOREIGN KEY (emp_id) REFERENCES employee(emp_id) ) ENGINEInnoDB; -- 测试数据一个管理员、两个部门、三名员工 INSERT INTO user(username, password) VALUES (admin, MD5(123456)); INSERT INTO department(dept_name, manager) VALUES (技术部, 张三), (人事部, 李四); INSERT INTO employee(emp_no, emp_name, dept_id, position, gender, phone, hire_date) VALUES (E001, 王五, 1, Java工程师, 男, 13800000001, 2023-03-01), (E002, 赵六, 2, HR专员, 女, 13800000002, 2023-06-15), (E003, 孙七, 1, 前端工程师, 男, 13800000003, 2024-01-10);逻辑说明先建被引用的表再建引用方否则外键约束报错DECIMAL(10,2)保证工资计算不会出现浮点误差联合唯一索引uk_att是防重复录入的兜底。测试数据尽量贴近真实语义后面写列表页、工资汇总页都有内容可展示。参数说明utf8mb4最优先MySQL 5.7及以上都支持它比utf8多了对emoji等四字节字符的支持DECIMAL(10,2)中小数点后两位是金额常规精度实际项目里如果涉及个人所得税建议用DECIMAL(12,2)留够进位空间。user表里存MD5写报告时一定要补一句生产环境请使用加盐哈希这是答辩时容易被追问的安全点。3. 把登录和员工CRUD跑通DAO、Servlet、JSP三层各自要写的代码3.1 源代码管理的起点先用一个清晰的项目结构分层源代码管理这件事在单兵作战的毕设里最容易乱。我见过把几十个jsp文件堆在WebContent根目录下的项目改一个名字要全局搜索半天。JSP项目常见而且稳妥的做法是经典三层jsp负责展示、Servlet负责接收请求和跳转、DAO负责跟数据库打交道。hrms/ ├── src/com/hrms/ │ ├── dao/ # DBUtil.java, EmployeeDao.java, UserDao.java │ ├── model/ # Employee.java, Department.java, User.java │ ├── servlet/ # LoginServlet.java, EmployeeServlet.java, AttendanceServlet.java │ └── filter/ # EncodingFilter.java ├── WebContent/ │ ├── login.jsp │ ├── employee/ │ │ ├── list.jsp # 员工列表 │ │ ├── add.jsp # 新增员工 │ │ └── edit.jsp # 编辑员工 │ └── WEB-INF/ │ ├── web.xml │ └── lib/ # mysql-connector-java.jar └── sql/ └── init.sql这个结构对应源代码管理上的一个原则按职责分包让改页面和改逻辑两个动作互不干扰。model里的类就是数据库表的映射字段名和表列名保持一致少写一堆setter映射代码。毕业设计评委翻代码时第一眼看的就是包结构三层清晰比任何花哨技巧都加分。3.2 DBUtil先把数据库连接写稳再谈业务代码所有DAO的活儿都建立在连接之上。JDBC连接写不好后面全是too many connections的报错。先给一个开发期够用的DBUtilpackage com.hrms.dao; import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; public class DBUtil { // URL 里的参数useUnicodecharacterEncoding 保证中文不丢serverTimezone 是 MySQL 8.x 必填 private static final String URL jdbc:mysql://localhost:3306/hrms?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai; private static final String USER root; private static final String PASSWORD 123456; static { try { // 5.x 驱动用 com.mysql.jdbc.Driver8.x 用 com.mysql.cj.jdbc.Driver Class.forName(com.mysql.jdbc.Driver); } catch (ClassNotFoundException e) { throw new ExceptionInInitializerError(e); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } }逻辑说明静态块里Class.forName把驱动注册一次getConnection每次新建连接。毕业设计阶段数据量不超过几千条新建连接的性能损耗完全可接受反而比引入连接池更省心——少一个jar就少一个部署报错源。如果报告想写生产环境会用连接池自己在答辩时补一句Druid的配置思路就行。参数说明URL里的serverTimezoneAsia/Shanghai是MySQL 8.x必填少了它报时区错误useSSLfalse可以省去SSL握手警告characterEncodingutf8和建库时的utf8mb4要配对单独设一边中文乱码就是必然的。USER和PASSWORD直接写死在类里是毕设的通用做法但要在代码注释或报告里明确仅用于学习环境。3.3 登录模块一次完整的Request到Session到跳转请求链登录是所有功能的前置把它的请求链看懂了后面员工CRUD全是同一套模式。下面是LoginServlet的doPost方法package com.hrms.servlet; import com.hrms.dao.UserDao; import com.hrms.model.User; import javax.servlet.ServletException; import javax.servlet.http.*; import java.io.IOException; public class LoginServlet extends HttpServlet { private UserDao userDao new UserDao(); Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { // 第一步取参数前先设置编码放在 Filter 里更稳妥 req.setCharacterEncoding(UTF-8); String username req.getParameter(username); String password MD5Util.md5(req.getParameter(password)); // 与库里存的 MD5 做比较 User user userDao.findByUsernameAndPassword(username, password); if (user ! null) { HttpSession session req.getSession(); session.setAttribute(loginUser, user); // 登录态放 session不是 request resp.sendRedirect(employee/list); // 重定向防止刷新重复登录 } else { req.setAttribute(error, 用户名或密码错误); req.getRequestDispatcher(login.jsp).forward(req, resp); // 转发保留错误提示 } } }逻辑说明登录成功走sendRedirect、失败走forward这是Servlet/JSP里的经典选择题。转发是服务端把请求转给下一个资源浏览器URL不变刷新会再次提交POST重定向是浏览器重新发起GET请求URL变成列表页刷新操作天然安全。答辩被问为什么用重定向不用转发答案就是这句话。request.setCharacterEncoding(UTF-8)放在每个Servlet里会写到手酸规范做法是放到Filter里全局处理第3.5节给Filter代码。参数说明MD5Util.md5是把用户输入的密码先MD5再查库比对发生在数据库侧而不是读出所有密码再比对。session.setAttribute(loginUser)里存的是User对象不是用户名这种裸字符串后面的JSP页面可以直接用EL表达式取属性。3.4 员工列表与新增PreparedStatement参数化的两个细节列表页是JSP系统的门面考核的重点是联表查询和关键词搜索。下面是用PreparedStatement做模糊查询的DAO方法public ListEmployee list(String keyword) throws SQLException { String sql SELECT e.*, d.dept_name FROM employee e LEFT JOIN department d ON e.dept_id d.dept_id WHERE e.emp_name LIKE ? OR e.emp_no LIKE ?; ListEmployee list new ArrayList(); try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { String like % keyword %; ps.setString(1, like); ps.setString(2, like); try (ResultSet rs ps.executeQuery()) { while (rs.next()) { Employee e new Employee(); e.setEmpId(rs.getInt(emp_id)); e.setEmpNo(rs.getString(emp_no)); e.setEmpName(rs.getString(emp_name)); e.setDeptName(rs.getString(dept_name)); // 从联表结果里取不额外查部门 e.setPosition(rs.getString(position)); e.setStatus(rs.getInt(status)); list.add(e); } } } return list; }逻辑说明LEFT JOIN保证了即使员工dept_id为空员工行依然能出现在结果集里dept_name由数据库端补上代码里少一次按部门循环查询。try-with-resources是JDK 7引入的Connection、PreparedStatement、ResultSet三层全部自动close手工写finally里关资源的话漏一层就是连接泄漏Tomcat跑半天就报too many connections。新增员工走另一条SQL。这里最常见的错误是把参数拼接成VALUES( name )务必一律用占位符String sql INSERT INTO employee(emp_no, emp_name, dept_id, position, phone, hire_date, status) VALUES(?,?,?,?,?,?,1); try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, req.getParameter(empNo)); ps.setString(2, req.getParameter(empName)); ps.setInt(3, Integer.parseInt(req.getParameter(deptId))); // 前端 select 传来的值要转 int ps.setString(4, req.getParameter(position)); ps.setString(5, req.getParameter(phone)); ps.setDate(6, java.sql.Date.valueOf(req.getParameter(hireDate))); // 字符串转 sql.Date int rows ps.executeUpdate(); if (rows 0) { resp.sendRedirect(employee/list); // 保存成功同样走重定向避免重复提交 } }参数说明setInt和setDate这两个类型转换是新人最爱翻车的地方——前端表单里date类型的input传回来是2024-03-01字符串要用java.sql.Date.valueOf转换deptId如果没选req.getParameter返回nullInteger.parseInt直接抛NumberFormatException所以前端select要加一个请选择部门的disabled选项后端也要判空。这两处判空逻辑写进报告里能堵住大半测试用例的设计。JSP页面这边列表页推荐用JSTL的c:forEach循环输出不要用Scriptlet写一坨for循环。下面是一个最小可用的list.jsp片段% page contentTypetext/html;charsetUTF-8 languagejava % % taglib prefixc urihttp://java.sun.com/jsp/jstl/core % table border1 tr th工号/thth姓名/thth部门/thth职位/thth状态/th /tr c:forEach varemp items${empList} tr td${emp.empNo}/td td${emp.empName}/td td${emp.deptName}/td td${emp.position}/td td${emp.status 1 ? 在职 : 离职}/td /tr /c:forEach /table逻辑说明${empList}是Servlet里request.setAttribute(empList, list)传过来的EL表达式通过getter方法取值。如果项目没引入JSTL的jar页面会报无法在库中找到标记库——这是一个非常常见的部署错误检查WebContent/WEB-INF/lib里有没有jstl.jar和standard.jar。3.5 用Filter一次性解决中文乱码别再每个Servlet里setEncoding中文乱码是JSP项目被吐槽最多的点根因是客户端、服务端、数据库三方编码不统一。最省事的方案是写一个EncodingFilter在请求到达Servlet之前统一设置编码package com.hrms.filter; import javax.servlet.*; import javax.servlet.annotation.WebFilter; import java.io.IOException; WebFilter(/*) public class EncodingFilter implements Filter { public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { req.setCharacterEncoding(UTF-8); // 处理 POST 请求体里的中文 resp.setCharacterEncoding(UTF-8); // 处理响应输出里的中文 resp.setContentType(text/html;charsetUTF-8); chain.doFilter(req, resp); } }逻辑说明WebFilter(/*)表示拦截所有请求路径filter把编码设好之后通过chain.doFilter放行后面的Servlet、JSP拿到的请求和响应都已经是UTF-8状态。注意GET请求的编码不归这里管GET参数放在URL上要改Tomcat的server.xml在第4章避坑部分细说。Filter配置有两种方式注解方式Tomcat 7都支持代码少web.xml方式写起来啰嗦但兼容性最好。毕业设计我建议用web.xml配因为很多老教程里的部署流程还在用xml描述符报告里把两种都写一句显得不是只会抄注解。4. 部署运行中的5个坑从Tomcat版本到乱码踩完再答辩4.1 Tomcat 9和10的包名不兼容项目一启动就报错现象源码在Eclipse里编译通过部署到Tomcat后控制台抛出一堆程序包javax.servlet不存在或NoClassDefFoundError。原因Tomcat 10是一个分水岭Servlet规范从javax.迁移到了jakarta.。大量JSP毕业设计和配套教材都基于javax写的直接放进Tomcat 10import全断。解决最稳妥的是装Tomcat 9.0.x这是javax时代的最后一个大版本绝大多数jsp源码包和IDE的服务器配置教程都以它为准。如果非要留在Tomcat 10就得把所有import里javax.servlet批量替换成jakarta.servlet同时检查依赖的第三方jar是否也完成了迁移。这个批量替换看起来简单但JSTL、连接池这些间接依赖很容易漏所以不要跟版本较劲。注意选Tomcat版本前先看一眼源代码里import的是javax还是jakarta这一眼能省下两个小时。4.2 MySQL驱动5.x和8.x混用连接直接翻车现象数据库连接报ClassNotFoundException: com.mysql.jdbc.Driver或者报Unable to load authentication plugin caching_sha2_password。原因两个版本问题叠加。mysql-connector-java 8.x把驱动类改名成com.mysql.cj.jdbc.DriverMySQL 8默认认证插件是caching_sha2_password老驱动5.x不认。代码里写死的是5.x类名库里是MySQL 8两头对不上。解决先看本机MySQL版本。MySQL 5.7配5.1.49的驱动用com.mysql.jdbc.DriverMySQL 8.x配8.0.x的驱动类名用com.mysql.cj.jdbc.DriverURL加serverTimezone。驱动jar放在WebContent/WEB-INF/lib下随项目走这样换一台机器部署不会漏jar。检查驱动是否正确有个笨办法WEB-INF/lib里只留一个mysql的jar如果同时出现两个版本删掉旧的那个。4.3 中文乱码三个环节单独调一处治不好现象登录用户名是中文时页面显示???或者数据库表里中文正常JSP页面却一片乱码再或者插入的记录到了数据库直接变问号。原因乱码是三个环节叠加单点修复永远有漏网之鱼。一是请求体编码没设二是响应编码没设三是JDBC连接串里没声明characterEncoding。解决建议把四处一次性配齐。第一使用第3.5节的EncodingFilter统一请求响应编码第二Tomcat的server.xml里找到Connector节点加上URIEncodingUTF-8解决GET参数乱码第三JDBC URL带characterEncodingutf8第四建库时用utf8mb4。这套四件套配完之后在员工新增页面录一条带张的测试数据就能确认全链路通畅。4.4 表单重复提交刷新一次就多一条数据现象在新增员工页面点保存浏览器再按一下F5列表里出现两条一模一样的记录。原因表单POST提交后如果Servlet里用forward跳转到列表页浏览器的地址栏还停在提交URL上刷新会原样重发POST。这是Servlet/JSP时代最经典的翻车现场很多源码包自己都没改干净。解决第3.3节和第3.4节里我一直强调保存成功后用sendRedirect而不是forward就是这个原因。Post/Redirect/Get模式核心逻辑是POST写入数据重定向到GET请求的列表页。按这个模式改完刷新列表页安全多按几次F5也不会新增记录。如果项目中还出现了jsp页面让加载完后刷新一次的需求注意那是页面级的onload刷新和表单重复提交不是一回事别混淆。4.5 项目报告和源代码对不上答辩现场被问穿现象zip包里的项目报告写的是Eclipse加Tomcat 8加MySQL 5.7打开源代码一看是IDEA的工程文件报告截图里有的功能页面在代码里搜不到对应Servlet。原因很多打包的毕设是报告先写、代码后改或者反过来两份产物各改各的最后打成一个包没人做一致性校验。这属于交付物的质量问题不解决的话答辩评委随手翻一页就能把你问住。解决拿到源码的第一天别急着启动先做一次三核对。第一核对技术栈版本——报告的部署章节和实际web.xml、jar包是否一致第二核对功能清单——报告里每个功能截图对应的JSP页面和Servlet是否存在第三核对运行路径——报告写的访问URL是不是项目实际的context-path。把不一致的地方在报告里改掉或者自己补一个环境差异说明小节主动交代比被问出来体面得多。我自己的血泪经验是当年拿到一个包报告写的是MySQL 5.7本机装的是8.0驱动类名和URL全都要改我花了整整一个晚上排掉之后所有环境差异我都会第一时间写进部署说明。5. 答辩前用一张检查清单跑完所有功能再补两个加分技巧进入答辩周别再改功能了改出新的bug没时间兜底。我习惯把系统从登录到退出完整走一遍每项功能对照下表确认功能项操作预期结果登录正确/错误账号各试一次正确进入主页错误显示提示且不跳转员工新增带中文和数字字符提交列表出现新记录数据库无乱码员工编辑修改职位和部门列表和详情页同步更新员工删除删除一名在职员工记录消失关联考勤记录不被删掉考勤录入同人同日录两次第二次被唯一索引拦截工资查询查看某员工工资详情实发工资等于基本加奖金减扣款退出登录点击退出session失效访问列表页被迫回登录页这个清单的好处是每一条都能对应报告里的一个功能模块截图答辩演示时照着走不会卡壳。两个加分技巧。第一工资列表加一个实发工资合计行实现上在SQL里写SELECT SUM(base_salary bonus - deduct) FROM salary WHERE pay_date?比在JSP循环里累加更显得你懂聚合函数。第二报告部署章节里放一张版本对照表把JDK版本、Tomcat版本、MySQL版本、驱动版本四列列清楚再写一句本说明已在全新环境验证通过这句话的信任度比十页架构图都高。最后说一句我自己的教训做完这套JSP人事管理系统我最大的感受是真正耗时间的不是写代码是环境不一致造成的玄学问题。版本、编码、驱动三个黑匣子每踩一个都要半天起步。希望这一篇能帮你把这些坑提前填平把精力留给答辩本身希望帮到你。本文还有配套的精品资源点击获取