☰
JSP企业人事管理系统毕设实战:从数据库设计到权限控制全流程
2026/10/8 21:33:59 网站建设 项目流程

简介:这份资源是面向高校计算机相关专业毕业生的人事管理系统毕业设计完整资料包,包含可运行的JSP源代码与配套论文,适合正在准备毕设、需要参考企业级信息管理项目实现的学生及Java Web初学者。压缩包共140个文件,约2.21MB,以52个jsp页面文件、22张jpg与20张gif界面素材、14个class与7个java源码文件为主,另含8个css样式、4份doc文档及xml、js等配置脚本,覆盖前端页面、业务逻辑与数据库设计各层。论文部分从人事管理信息系统初步调查入手,详细阐述需求分析、数据流程分析、系统总体结构设计、数据结构与数据库设计及输入输出设计,源代码则实现了登录、用户表单、管理后台等核心模块。目前已有82人学习下载,读者可借此获得一套结构完整的毕设参考方案,用于理解JSP开发流程、梳理论文写作框架并对照改进自身项目。

1. 企业人事管理系统:JSP 毕设里最容易被低估的硬骨头

很多同学拿到「基于JSP的企业人事管理系统设计」这个题目时,第一反应是“老掉牙的技术栈,随便糊弄一下就能过”。但真正动手做过的都知道,这套系统里藏着不少能让你卡上两三天甚至一周的细节——从 JSP 页面里表单数据的乱码处理,到员工照片上传后的路径映射,再到不同角色登录后菜单权限的动态渲染,每一个点单拎出来都不算难,凑在一起就是一台“翻车制造机”。这篇文章面向的是正在做计算机毕业设计、选了 JSP 技术路线的同学,也适合想用一套完整业务系统练手 Java Web 基础的开发者。我会按实际开发顺序,把企业人事管理系统从数据库设计到核心模块实现再到部署排错的完整路径拆开讲,代码可以直接抄改,参数会说明为什么这么设。读完你至少能拿到一套能跑起来、能答辩、能写进论文的系统骨架。

2. 先把数据库和项目骨架定下来:别急着写 JSP

2.1 企业人事管理系统的数据表怎么切分才不返工

很多同学一上来就打开 Eclipse 或 IDEA 新建 JSP 文件,写到一半发现部门表和员工表的关系没理清,又回头改数据库,改完数据库发现实体类对不上,实体类改完发现 JSP 页面里取的字段名全错了。这种返工链条的根源就是数据库设计没前置。

企业人事管理系统的核心实体其实不多,但关系要理清楚。我一般会切成这几张表:部门表(department)、职位表(position)、员工表(employee)、用户表(user)、考勤记录表(attendance)、薪资表(salary)。其中员工表和部门表是多对一,员工表和职位表也是多对一,用户表和员工表是一对一(用于登录),考勤和薪资都是员工的一对多。

建表时有两个容易忽略的点。第一,员工表里不要直接存部门名称和职位名称,只存 department_id 和 position_id,展示的时候用 JOIN 查出来。第二,用户表要单独建,不要把登录密码塞进员工表,否则后面做权限控制时你会后悔。下面是我常用的建表 SQL,以 MySQL 为例:

-- 部门表:只存最基础的层级信息 CREATE TABLE department ( dept_id INT PRIMARY KEY AUTO_INCREMENT, dept_name VARCHAR(50) NOT NULL, dept_desc VARCHAR(200), create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 职位表:和部门弱关联,一个部门可以有多个职位 CREATE TABLE position ( pos_id INT PRIMARY KEY AUTO_INCREMENT, pos_name VARCHAR(50) NOT NULL, dept_id INT, base_salary DECIMAL(10,2), FOREIGN KEY (dept_id) REFERENCES department(dept_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 员工表:核心表,注意字段长度和默认值 CREATE TABLE employee ( emp_id INT PRIMARY KEY AUTO_INCREMENT, emp_name VARCHAR(30) NOT NULL, gender CHAR(2) DEFAULT '男', birth_date DATE, phone VARCHAR(20), email VARCHAR(50), dept_id INT, pos_id INT, hire_date DATE, photo_path VARCHAR(200), status TINYINT DEFAULT 1 COMMENT '1在职 0离职', FOREIGN KEY (dept_id) REFERENCES department(dept_id), FOREIGN KEY (pos_id) REFERENCES position(pos_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 用户表:登录用,和员工表一对一 CREATE TABLE user ( user_id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(30) UNIQUE NOT NULL, password VARCHAR(64) NOT NULL, role VARCHAR(20) DEFAULT 'employee' COMMENT 'admin/hr/employee', emp_id INT, FOREIGN KEY (emp_id) REFERENCES employee(emp_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

字符集统一用 utf8mb4,不要用 utf8,否则员工姓名里如果有生僻字会存不进去。密码字段给 64 位是留了余地,后面如果做 MD5 或 SHA-256 加密,长度够用。status 字段用 TINYINT 而不是 BOOLEAN,方便后面扩展更多状态(比如试用期、停薪留职)。

2.2 JSP + Servlet + JDBC 的分层结构怎么搭

数据库定好之后,项目结构要按分层来。我见过太多毕设项目把所有 Java 代码堆在 src 根目录下,JSP 页面散落在 WebContent 各个角落,最后自己都找不到文件。推荐的结构是这样的:

src/ com.hr.entity/ -- 实体类,和数据库表一一对应 com.hr.dao/ -- 数据访问层,只做增删改查 com.hr.service/ -- 业务逻辑层,处理事务和复杂判断 com.hr.servlet/ -- 控制层,接收请求、调 service、跳转页面 com.hr.util/ -- 工具类,数据库连接、编码过滤、分页等 WebContent/ css/ js/ images/ WEB-INF/ web.xml lib/ -- 放 mysql-connector-java.jar 等依赖 admin/ -- 管理员页面 employee/ -- 普通员工页面 login.jsp index.jsp

这个结构的好处是:改数据库字段时只需要动 entity 和 dao,JSP 页面里用的属性名通过 EL 表达式取值,不会因为表结构微调就大面积崩。Servlet 里不要写 SQL,DAO 里不要写 request.getParameter,各层职责分明,答辩的时候老师问起来也能说清楚。

数据库连接用 JDBC 直接连就行,毕设不需要上连接池(虽然 Druid 也不复杂,但多一个配置文件就多一个出错点)。写一个 DBUtil 类,把驱动加载、连接获取、资源关闭封装好:

public class DBUtil { private static final String URL = "jdbc:mysql://localhost:3306/hr_system?useUnicode=true&characterEncoding=utf8mb4&serverTimezone=Asia/Shanghai"; private static final String USER = "root"; private static final String PASSWORD = "你的密码"; static { try { Class.forName("com.mysql.cj.jdbc.Driver"); } catch (ClassNotFoundException e) { e.printStackTrace(); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } public static void close(Connection conn, Statement stmt, ResultSet rs) { // 依次关闭,每个都 try-catch,不要图省事只写一个 } }

URL 里的 characterEncoding 要写 utf8mb4,serverTimezone 必须加,否则 MySQL 8.x 会报时区错误。这个坑每年都有大量同学踩,现象是“明明数据库里有数据,Java 查出来就是空”,其实就是时区没配对导致连接失败但异常被吞了。

3. 核心模块逐个击破:登录、员工 CRUD、权限控制

3.1 登录模块:从 JSP 表单到 Session 的完整链路

登录是企业人事管理系统的入口,也是答辩时老师必看的功能。很多同学写登录只写一个 login.jsp 和一个 LoginServlet,验证完密码就跳转,结果后面每个页面都要重新验证一遍用户有没有登录,代码重复得让人头皮发麻。

正确的做法是用 Filter 做统一拦截。先写登录的 Servlet:

@WebServlet("/login") public class LoginServlet extends HttpServlet { protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding("UTF-8"); String username = req.getParameter("username"); String password = req.getParameter("password"); UserDao userDao = new UserDao(); User user = userDao.findByUsername(username); if (user == null) { req.setAttribute("msg", "用户名不存在"); req.getRequestDispatcher("/login.jsp").forward(req, resp); return; } // 实际项目里密码要加密比对,毕设至少做个 MD5 if (!user.getPassword().equals(MD5Util.encode(password))) { req.setAttribute("msg", "密码错误"); req.getRequestDispatcher("/login.jsp").forward(req, resp); return; } HttpSession session = req.getSession(); session.setAttribute("currentUser", user); session.setMaxInactiveInterval(30 * 60); // 30分钟超时 // 按角色跳转到不同首页 if ("admin".equals(user.getRole())) { resp.sendRedirect(req.getContextPath() + "/admin/index.jsp"); } else { resp.sendRedirect(req.getContextPath() + "/employee/index.jsp"); } } }

这里有几个参数要说明。setMaxInactiveInterval 设 1800 秒是毕设的合理值,太短了演示时容易掉线,太长了不安全。密码比对用 MD5 是为了演示加密流程,实际生产要用 BCrypt,但毕设用 MD5 足够,论文里可以写“采用 MD5 摘要算法存储密码,避免明文泄露”。

然后是登录拦截 Filter:

@WebFilter("/*") public class LoginFilter implements Filter { public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) resp; String uri = request.getRequestURI(); // 放行登录页、登录接口、静态资源 if (uri.endsWith("login.jsp") || uri.endsWith("/login") || uri.contains("/css/") || uri.contains("/js/") || uri.contains("/images/")) { chain.doFilter(req, resp); return; } HttpSession session = request.getSession(false); if (session == null || session.getAttribute("currentUser") == null) { response.sendRedirect(request.getContextPath() + "/login.jsp"); return; } chain.doFilter(req, resp); } }

Filter 的 urlPattern 写 /* 表示拦截所有请求,然后在代码里手动放行白名单。注意 getSession(false) 不要传 true,否则每个未登录请求都会创建一个新 Session,白白消耗内存。

3.2 员工信息 CRUD:分页查询和文件上传的落地细节

员工管理模块是企业人事管理系统里代码量最大的部分,包含列表展示、新增、编辑、删除、搜索、分页。其中分页和文件上传是两个最容易出问题的地方。

先看分页查询。不要用 SELECT * FROM employee 一次性查出来再在 Java 里 subList,数据量一大就内存溢出。正确做法是用 LIMIT 在数据库层面分页:

public List<Employee> findByPage(int pageNum, int pageSize, String keyword) { List<Employee> list = new ArrayList<>(); StringBuilder sql = new StringBuilder( "SELECT e.*, d.dept_name, p.pos_name FROM employee e " + "LEFT JOIN department d ON e.dept_id = d.dept_id " + "LEFT JOIN position p ON e.pos_id = p.pos_id WHERE 1=1 "); List<Object> params = new ArrayList<>(); if (keyword != null && !keyword.trim().isEmpty()) { sql.append("AND e.emp_name LIKE ? "); params.add("%" + keyword.trim() + "%"); } sql.append("ORDER BY e.emp_id DESC LIMIT ?, ?"); params.add((pageNum - 1) * pageSize); params.add(pageSize); // 执行查询,封装 ResultSet 到 Employee 对象 // ... return list; }

pageNum 从 1 开始,pageSize 一般设 10 或 15。LIMIT 的第一个参数是偏移量,计算方式是 (pageNum - 1) * pageSize,这个公式写错的话第一页会跳过数据或者最后一页出现空记录。总记录数要单独写一个 count 方法,SQL 是 SELECT COUNT(*) FROM employee WHERE 同样的条件,不要用 list.size() 代替。

文件上传(员工照片)是另一个高频翻车点。JSP 表单必须加 enctype="multipart/form-data",Servlet 里不能用 request.getParameter 直接取普通字段,要用 Servlet 3.0 的 Part API:

@WebServlet("/employee/add") @MultipartConfig(maxFileSize = 2 * 1024 * 1024, maxRequestSize = 5 * 1024 * 1024) public class EmployeeAddServlet extends HttpServlet { protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding("UTF-8"); String empName = req.getParameter("empName"); String phone = req.getParameter("phone"); // ... 其他字段 Part photoPart = req.getPart("photo"); String photoPath = null; if (photoPart != null && photoPart.getSize() > 0) { String fileName = System.currentTimeMillis() + "_" + photoPart.getSubmittedFileName(); String saveDir = getServletContext().getRealPath("/upload/photo"); File dir = new File(saveDir); if (!dir.exists()) dir.mkdirs(); photoPart.write(saveDir + File.separator + fileName); photoPath = "upload/photo/" + fileName; } Employee emp = new Employee(); emp.setEmpName(empName); emp.setPhotoPath(photoPath); // ... 调 service 保存 } }

@MultipartConfig 的 maxFileSize 设 2MB 是合理的,员工照片不需要太大。getSubmittedFileName 在 Servlet 3.1 才有,如果用的 Tomcat 7 要换成 part.getHeader("content-disposition") 手动解析。保存路径用 getRealPath 拿到的是项目部署目录,重启 Tomcat 后上传的文件可能会丢,毕设演示时要注意这一点,或者把文件存到项目外的固定目录。

3.3 角色权限控制:菜单动态渲染和 URL 级拦截

企业人事管理系统一般有三种角色:管理员(admin)、人事专员(hr)、普通员工(employee)。管理员什么都能看,人事专员能管员工和考勤但不能管用户,普通员工只能看自己的信息和考勤。

权限控制分两层。第一层是菜单动态渲染,在 JSP 里根据 Session 中的角色决定显示哪些菜单项:

<%@ page contentType="text/html;charset=UTF-8" language="java" %> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <% User currentUser = (User) session.getAttribute("currentUser"); String role = currentUser != null ? currentUser.getRole() : ""; %> <ul class="nav"> <li><a href="index.jsp">首页</a></li> <c:if test="<%= \"admin\".equals(role) || \"hr\".equals(role) %>"> <li><a href="employee/list">员工管理</a></li> <li><a href="attendance/list">考勤管理</a></li> </c:if> <c:if test="<%= \"admin\".equals(role) %>"> <li><a href="user/list">用户管理</a></li> <li><a href="dept/list">部门管理</a></li> </c:if> <li><a href="employee/myInfo">个人信息</a></li> </ul>

注意 JSTL 的 c:if 里嵌 Java 变量需要用 <%= %> 输出,写法比较别扭但能用。更干净的方式是在 Servlet 里把权限布尔值放进 request 域,JSP 里直接取。

第二层是 URL 级拦截。光隐藏菜单不够,普通员工如果直接在浏览器地址栏输入 /admin/user/list 照样能访问。需要在 Filter 里加角色判断:

// 在 LoginFilter 的登录校验之后追加 String role = (String) session.getAttribute("currentUser") != null ? ((User) session.getAttribute("currentUser")).getRole() : null; if (uri.contains("/admin/") && !"admin".equals(role)) { response.sendRedirect(request.getContextPath() + "/noPermission.jsp"); return; } if (uri.contains("/hr/") && !("admin".equals(role) || "hr".equals(role))) { response.sendRedirect(request.getContextPath() + "/noPermission.jsp"); return; }

这种基于 URL 路径的权限判断虽然粗糙,但毕设够用,而且逻辑清晰,论文里可以画一张角色-资源访问矩阵表来说明。

4. 避坑与排查:JSP 毕设里那些让人抓狂的瞬间

4.1 中文乱码:三个位置必须同时设对

现象:表单提交后数据库里存的是问号,或者页面显示的是乱码。

原因:JSP 页面的 pageEncoding、Servlet 的 request/response 编码、数据库连接的 characterEncoding 三者不一致。

解决:JSP 页面头部写 <%@ page contentType="text/html;charset=UTF-8" pageEncoding="UTF-8" %>;Servlet 里 doPost 第一行写 req.setCharacterEncoding("UTF-8"),doGet 如果也要处理中文,得手动 new String(req.getParameter("x").getBytes("ISO-8859-1"), "UTF-8");数据库 URL 里加 characterEncoding=utf8mb4。三处缺一处都会出问题,这是血泪经验。

4.2 数据库连不上但异常被吞了

现象:页面一直转圈或者报 500,但控制台没有明显的 SQLException 堆栈。

原因:DBUtil 的 close 方法里把异常 catch 了但没打印,或者 getConnection 返回 null 后直接使用导致 NullPointerException,真正的连接失败信息被掩盖。

解决:在 getConnection 的 catch 块里必须 e.printStackTrace(),不要写空 catch。另外可以在获取连接后加一句 if (conn == null) throw new SQLException("数据库连接为空"),让问题尽早暴露。

4.3 上传文件后重启 Tomcat 图片就没了

现象:上传员工照片时能看到,重启 Tomcat 后图片变成裂图。

原因:文件保存在 getRealPath("/upload") 返回的目录里,这个目录在 Tomcat 重启或重新部署时会被清空。

解决:把上传目录配到项目外部,比如 D:/hr_upload/,数据库里存相对路径,JSP 里用访问。或者在 Tomcat 的 server.xml 里配一个 Context 指向外部目录。毕设演示时如果只在一次会话内展示,用 getRealPath 也能凑合,但论文里最好写清楚生产环境的做法。

4.4 JSTL 标签不解析直接原样输出

现象:JSP 页面里写了 <c:forEach> 但浏览器里显示的就是这行代码本身。

原因:缺少 jstl.jar 和 standard.jar,或者 web.xml 的版本太低不支持 JSTL。

解决:在 WEB-INF/lib 下放入 jstl-1.2.jar(一个 jar 就够,不需要 standard.jar),并且在 JSP 头部加上 <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>。如果用的是 Maven 项目,加 javax.servlet:jstl:1.2 依赖。

4.5 分页查询最后一页显示空数据

现象:列表总共 23 条,每页 10 条,点第三页时显示空白。

原因:LIMIT 偏移量算错了,或者总页数计算时用了整数除法丢精度。

解决:总页数用 int totalPages = (totalCount + pageSize - 1) / pageSize; 这个公式保证向上取整。偏移量用 (pageNum - 1) * pageSize,pageNum 从 1 开始。另外在 Servlet 里要判断 pageNum 不能小于 1,也不能大于 totalPages,否则直接重定向到第一页或最后一页。

5. 论文与源码的收尾技巧:让答辩老师挑不出硬伤

走到这一步,系统功能基本跑通了,但毕设还有一半分数在论文上。很多同学系统做得不错,论文却写得像说明书,老师一看就皱眉头。这里说几个我反复验证过的收尾技巧。

第一,论文里的系统架构图不要用网上随便找的模板。用 Visio 或 draw.io 画一张三层架构图,标注清楚 JSP 负责表现层、Servlet 负责控制层、DAO 负责数据访问层,每一层之间用箭头标出数据流向。这张图放在第三章,老师一眼就能看出你理解了自己的代码结构。

第二,数据库设计章节要放 E-R 图和表结构说明。E-R 图用矩形表示实体、菱形表示关系、椭圆表示属性,不要直接用数据库表截图代替。表结构说明用表格列出字段名、类型、约束、说明,下面这张表可以直接参考:

字段名类型约束说明
emp_idINT主键,自增员工编号
emp_nameVARCHAR(30)非空员工姓名
genderCHAR(2)默认'男'性别
dept_idINT外键所属部门
statusTINYINT默认11在职 0离职

第三,核心代码不要整段贴进论文。选两三段有代表性的,比如登录的密码加密比对、分页查询的 LIMIT 拼接、Filter 的权限拦截,每段代码配一段文字说明“这段代码解决了什么问题、关键参数为什么这么设”。贴太多代码会被查重,贴太少又显得工作量不够,三到五段刚好。

第四,测试章节要写具体的测试用例和结果。不要只写“系统运行正常”,要列出:输入正确的用户名密码能登录成功、输入错误的密码提示“密码错误”、未登录直接访问员工列表会跳转到登录页、上传超过 2MB 的照片会提示文件过大。每条用例写清楚操作步骤、预期结果、实际结果,用表格呈现。

最后说一个我自己的习惯:答辩前把系统在另一台电脑上完整部署一遍。从安装 JDK、配置 Tomcat、导入数据库、改 DBUtil 里的密码,到启动后走一遍所有功能。这个过程能暴露 90% 的环境问题,比如 JDK 版本不兼容、MySQL 驱动包没放对位置、端口被占用。我见过太多同学在自己电脑上跑得好好的,一到答辩教室就各种报错,提前在陌生环境跑一遍能省下很多后悔药。希望帮到你。

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

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

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

立即咨询