资讯动态

Java Web人事信息管理系统设计与实现(Servlet+JSP)

发布时间:2026/10/20 14:03:28 来源:尧图企业网站定制
做这套系统的时候我刚从“能跑就行”的阶段过渡到“别给后人挖坑”的阶段。小型企业的人事信息管理系统听起来不像电商平台或者数据中台那么有排面但它覆盖的场景非常典型人员花名册、部门组织、考勤记录、请假审批、工资台账、登录权限。能用 Java Web 技术把这套东西从零做到能投产、能维护比单纯跑通几个 Demo 有价值得多。这篇文章就围绕这套系统的设计与实现来拆适合正在做毕业设计、刚入行想练手、或者真想给小公司做内部工具的开发者参考。1. 需求拆解先搞明白小企业要的到底是什么1.1 小企业人事管理的三个核心痛点小企业的人事管理跟大厂完全不是一个量级。大厂有专门的人事系统、薪酬系统、考勤系统数据在各系统之间通过接口流转小企业往往是一个行政兼人事的同事用 Excel 管理上百号人的档案和考勤。真实痛点不外乎三个第一数据分散且口径不一致。今天这个人调了部门明天那个人改了岗位Excel 表格改了这版忘了那版跟工资条对不上的情况经常发生。第二权限不清晰。一个小公司老板要看全局部门主管看本部门普通员工只能看自己。Excel 很难做到这种粒度共享文件要么全可见要么干脆都不可见。第三流程回溯困难。请假审批、入离职登记、工资调整这些操作如果没有系统记录出了纠纷根本说不清当时是谁批的、批了多少。这套 Java Web 人事系统的需求就围绕这三个痛点展开。模块上划分成员工档案管理、部门岗位管理、考勤管理、薪资管理、系统用户与权限管理五块外加一个统计首页。注意我没有加绩效考核、社保公积金自动计算这些重功能。小企业人事系统最忌讳一口气吃成胖子真正的需求是把基础数据管清楚把日常流程跑通再谈扩展。1.2 核心业务闭环从入职到发薪的一个完整链路设计系统的时候我习惯先画一条业务闭环。企业招人后第一步是在员工档案里新建一条记录录入姓名、身份证号、岗位、所属部门、入职日期第二步是开通系统账号分配员工或主管角色第三步是日常考勤每天上下班打卡或者由管理员登记第四步是请假、加班审批第五步是每个月根据出勤天数和基本工资计算应发工资再由财务确认发放。这条链路跑通系统就有了被日常使用的机会。如果只做增删改查那跟 Excel 没有任何区别用户不会愿意用。所以我在设计数据库时不是简单堆表而是把员工表、考勤表、请假表、薪资表之间的关联看得非常重。比如考勤记录必然挂在某个员工 ID 上薪资发放记录也必然关联员工和月份。有了这个闭环统计首页才能展示实时的在岗人数、本月请假人数、待发薪人数管理层才真正感受到系统带来的便利。1.3 角色权限模型老板、主管、员工看的是同一个系统里的不同世界权限设计这块纯 JavaScript 隐藏按钮是没用的必须在后端做真实校验。系统采用基于角色的权限控制用户表、角色表、菜单/权限表、用户角色关联表四件套。我定义了四个角色系统管理员拥有全部权限包括用户管理、系统参数配置。人事专员可以操作员工档案、考勤、薪资录入审核。部门主管可以查看本部门员工信息审批本部门的请假申请。普通员工只能查看自己的档案、考勤、工资条。登录成功后后端把当前用户的角色标识放进 session每次请求到达 Servlet 时先经过一个过滤器根据请求路径判断是否具备访问权限。比如 /salary/list 这种路径普通员工角色直接过滤掉连数据都不能查到。这个模型实现成本不高但比单纯靠前端菜单隐藏要靠谱得多。2. 技术选型与架构设计Java Web 项目怎么搭才不容易返工2.1 技术栈选择的几个考量点现在的 Java Web 项目有很多流派纯 JSPS Servlet、SSH 老项目、Spring MVC MyBatis、Spring Boot 全家桶。考虑到这套系统的定位是小型企业、内部使用、代码清晰易维护我最终选择了经典的 Servlet JSP JDBC 作为底层也就是朴素的三层架构。为什么不用 Spring Boot不是说 Spring Boot 不好而是当系统只有这几十张表和几个业务模块时框架本身的复杂度会超过业务复杂度。而且用 Servlet 写一遍控制层转发、请求参数封装、数据库连接管理能让人把 HTTP 请求的生命周期、Session 机制、事务边界这些底层概念彻底吃透再回头用框架时思路会清晰很多。当然如果团队已经习惯了 Spring Boot那我建议用 Spring Boot MyBatis 替换业务层接口不用大改只是控制层的写法从 servlet 换成 Controller。不过本文面向的需求场景是“学习和练兵”所以主要讲 Servlet JSP 的实现思路。技术栈清单如下开发语言Java 8前端JSP JSTL Bootstrap JavaScript图表用 ECharts后端Servlet 3.0 JDBC Druid 连接池或用标准连接池组件数据库关系型数据库SQL 为标准语法构建工具Maven部署环境开源 Servlet 容器2.2 分层设计Controller、Service、Dao 各管各的事系统的包结构是第一道防线。我从一开始就坚持按功能分包而不是把所有类都丢在一个包里。基础结构如下com.company.hrms.controllerServlet 控制层负责接收请求、解析参数、调用 Service、跳转视图。 com.company.hrms.service业务接口层同时提供事务边界。 com.company.hrms.service.impl业务实现层一个类对应一个接口。 com.company.hrms.dao数据访问接口层。 com.company.hrms.dao.implJDBC 实现层。 com.company.hrms.entity实体类对应数据库表。 com.company.hrms.common工具类、常量、过滤器等。 com.company.hrms.filter登录、权限、编码过滤器。这套分层的理由很简单Servlet 里不写 SQLDao 里不写业务判断Service 层只处理业务规则。举个典型场景删除一个部门之前要先判断这个部门下是否有员工如果有员工就不能直接删。这个逻辑放在 Service 层做Controller 只负责告诉 Service “我要删这个部门”Dao 只负责执行 delete 语句。一旦出现问题顺着调用链就能定位不会为了查一个 SQL 拼写错误翻遍整个项目。事务管理是分层之后必须顺势解决的一环。JDBC 写事务很简单con.setAutoCommit(false)执行完业务操作再 con.commit()异常时 con.rollback()。我把连接绑定到每个线程的同一个连接上确保一次请求中所有 Dao 操作共用一个数据库连接。具体做法是用 ThreadLocal 保存 Connection在 Service 层的事务过滤器里统一开启和关闭事务。3. 核心功能模块怎么落地每一个模块都能写出花3.1 员工档案模块不只是增删改查人事系统最重要的数据底座就是员工档案。这个模块看似简单实际踩坑的地方不少。员工表的核心字段我设计为员工编号、姓名、性别、身份证号、手机号、邮箱、最高学历、入职日期、转正日期、部门ID、岗位ID、员工状态试用/正式/离职、备注。需要注意身份证号不能当主键原因很简单员工可能离职后再入职身份信息不变但员工业务身份变了系统应该允许同一个身份证号对应多条历史记录所以主键必须用自增ID。出生日期和性别也不需要用户手动录入可以由身份证号反推。代码里封装一个 IdCardUtils 工具类对输入的身份证进行校验和解析避免脏数据。录入员工信息时头像上传是可选项。文件上传我用 Servlet 的 MultipartConfig 注解配合 Part 对象处理图片保存到服务器磁盘的一个指定目录数据库只存相对路径。这里有个很重要的小细节绝对路径一定不要存进数据库因为项目部署路径会变把项目从开发机换到服务器绝对路径就全废了。存相对路径然后用常量拼出访问 URL才是最稳妥的做法。员工列表页默认展示在职员工搜索条件按姓名、部门、岗位、状态进行模糊查询。分页用 PageBean 包装每页默认十条。导出 Excel 的功能我是用 CSV 格式实现生成一个带 BOM 头的 CSV 文件浏览器下载后可以用 Excel 直接打开避免引入重量级 excel 依赖库。3.2 部门与岗位管理树形结构与数据联动部门管理不能做成平铺列表因为小公司也可能有三四级组织架构比如总公司下面有生产部和销售部生产部下面又有一车间和二车间。数据库设计采用 parent_id 自关联结构字段为部门ID、部门名称、上级部门ID、排序号。前端 JSP 页面用递归算法把部门列表渲染成树形下拉框选择员工所属部门时就能按层级选择。岗位表相对简单岗位ID、岗位名称、所属部门ID。岗位跟部门绑定避免出现“研发部里选了个财务专员”这种不伦不类的情况。这里要重点讲一个容易出错的设计点删除部门时不能只检查部门表有没有子部门还要检查员工表里有没有员工引用这个部门。我的做法是在 Service 层写一个 deleteDepartment 方法先查子部门数量再查该部门及所有子部门下的员工数量都为零才允许删除。事务范围覆盖这两个查询和最终删除避免并发情况下出现数据不一致。3.3 考勤与请假审批状态机思想的具体运用考勤模块是人事系统里最容易写乱的部分。我把它拆成两个子功能每日考勤登记和请假管理。每日考勤记录字段为考勤ID、员工ID、考勤日期、上班时间、下班时间、考勤类型正常/迟到/早退/请假/缺勤、备注。小企业一般不做实时打卡而是由人事专员每天在后台批量登记因此页面提供“按部门批量初始化今日考勤”的功能默认生成所有在职员工的正常出勤记录人事只需要修改异常项。这个设计非常贴近现实使用习惯。请假管理我用了一个简单的状态机提交申请 - 主管审批 - 人事确认。状态字段维护三个值0 待审批、1 已通过、2 已驳回。员工提交请假单后主管审批通过状态变为 1系统自动把该员工在对应日期范围内的考勤记录更新为请假类型。这个过程涉及两张表的业务联动必须放在同一个事务里。Servlet 控制层只接收审批动作Service 层根据当前状态判断哪些流转是被允许的。比如已驳回的单子不允许再次审批已经撤销的请假单也不能被主管重复操作。3.4 薪资管理工资项可配置才不会被抱怨死薪资模块的坑主要在于工资项不是固定的。基本工资、岗位工资、绩效奖金、餐补、全勤奖每个人所在部门不同薪资构成也不同。如果给员工表加上一排固定工资字段后期调整很痛苦。我的做法是设计两张表工资项配置表和员工工资明细表。工资项配置表定义所有可能的工资项名称、类型增项还是减项、默认值员工工资明细表记录某个员工在某个月份每个工资项的具体金额。这样既能灵活适配不同人的薪资结构又能追溯历史调整记录。每月计算工资时系统根据员工基本工资和考勤统计生成带薪天数再根据请假天数扣除对应金额。全勤奖的判断依据是当月迟到、早退、请假天数是否为零。计算完成后生成“待确认”状态的月度工资记录由人事专员审核后确认发放。确认之后该月工资记录不允许修改必须走调薪单流程这是为了防止误操作后账对不上。3.5 登录认证与系统管理简单的 RBAC 也有细节要打磨最后是用户跟权限。用户表字段要有 id、用户名、密码、盐值、员工ID、角色ID、状态。密码绝对不能明文存储。我用的方案是 MD5 加盐随机生成一个长度为 8 的盐值把盐值和密码拼接后做 MD5 摘要数据库里同时保存盐值和摘要值。登录校验时再取盐值重新计算后比较。虽然现在大家谈论加密都更推荐 BCrypt 一类算法但在 Servlet JDBC 的老技术栈里加盐 MD5 作为练习题是合格的如果拿到生产环境建议升级为 BCrypt。登录检查通过过滤器实现。用户第一次访问任何需要登录的页面时过滤器先检查 session 里是否有 loginUser没有就重定向到登录页。已登录用户访问权限之外的路径返回 403 或跳转到权限不足页面。前端菜单按照角色动态渲染后端接口按角色做白名单校验两层配合才敢说权限是真的。3.6 统计首页用数据让管理层愿意天天打开系统首页统计是一个很容易被忽略但很能展示系统价值的地方。我在首页放了几张卡片在职人数、本月新入职、本月离职、本月请假人次下面用折线图展示近六个月入职趋势用柱状图展示各部门人数分布。查询 SQL 主要是 count、group by 加上日期范围筛选。图表用 ECharts 的简单折线图和柱状图后端返回 JSON前端异步加载渲染。我这样做的原因很直接如果系统的首页就是一排菜单老板不会觉得有什么但如果一打开就能看到人员和部门概况他会自然对这个系统产生信任感而这个信任感对争取项目资源、推动系统在企业里落地非常关键。4. 数据库表设计建表之前先想三个问题4.1 一张图看懂核心表的关联关系表结构是整个系统的地基我建议把主要表设计成下面这样再做调整系统用户表 sys_userid、username、password、salt、emp_id、role_id、status角色表 sys_roleid、role_name、role_key员工表 employeeid、emp_no、name、id_card、gender、birth_date、phone、email、education、hire_date、regular_date、dept_id、position_id、status部门表 departmentid、dept_name、parent_id、sort_order岗位表 positionid、pos_name、dept_id考勤表 attendanceid、emp_id、att_date、clock_in、clock_out、att_type、remark请假表 leave_requestid、emp_id、begin_date、end_date、leave_type、reason、status、approver_id、approve_time工资项配置表 salary_itemid、item_name、item_type、default_value员工薪资明细表 employee_salary_detailid、emp_id、salary_month、item_id、amount月度工资确认表 monthly_salaryid、emp_id、salary_month、base_pay、total_pay、status、confirm_time4.2 外键和冗余字段怎么权衡新学数据库时老师要求外键约束必须加但实际开发中我更多用逻辑关联而不用数据库外键约束。原因有两方面一是避免删除部门时被 Employee 表外键卡住业务逻辑已经拦截了非法删除不需要数据库再兜底第二层二是后续要拆表、分库的时候物理外键会成为障碍。冗余字段同样要有取舍。部门名称在员工列表页经常要显示如果完全靠 join 查每次列表页都要关联部门表SQL 更复杂。我的折中方案是 employee 表里冗余一个 dept_name 字段员工调部门时先更新员工部门 ID再同步更新冗余的部门名称。虽然理论上存在数据一致性风险但在这个量级下收益远大于风险。如果你喜欢更规范的做法不在员工表冗余部门名称只在查询 SQL 里 join也完全可行只是性能在数据量增大时会受影响。4.3 日期类型和字符集是最容易踩的两个坑日期处理是 Java Web 老区最容易反复踩坑的地方。MySQL 的 date 类型不带时分秒datetime 带时分秒。员工入职日期用 date 就行考勤打卡时间用 datetime。Java 端接收字符串“2025-04-10”用 SimpleDateFormat 解析后传给 PreparedStatement 的 setDate 方法。查询日期范围时SQL 写法为 attendance_date ? AND attendance_date ?字符串可以用 yyyy-MM-dd 的当天最大值解决边界问题也可以直接用 次日 00:00:00。我建议用“att_date ? and att_date date_add(?, interval 1 day)”这样就不会漏掉当天 23:59 的记录。字符集方面数据库连接 URL 必须加 useUnicodetruecharacterEncodingUTF-8Servlet 层面加 CharacterEncodingFilter 强制设置请求和响应编码。JSP 页面顶部也要声明 pageEncodingUTF-8。这三处缺一不可否则就可能出现插入中文正常、查询中文却是乱码的怪象。5. 关键代码实现直接贴出可以复用的部分5.1 用 BaseServlet 统一处理请求分发如果每个功能都写一个 Servlet那 doGet 和 doPost 里必然充满重复代码。我写了一个 BaseServlet 基类通过反射根据 method 参数调用对应方法类似于 Spring MVC 里 Controller RequestMapping 的效果。public class BaseServlet extends HttpServlet { protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { handleRequest(req, resp); } protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding(UTF-8); handleRequest(req, resp); } private void handleRequest(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String method req.getParameter(method); if (method null || method.isEmpty()) { resp.sendError(HttpServletResponse.SC_BAD_REQUEST, 缺少 method 参数); return; } try { java.lang.reflect.Method action this.getClass().getMethod(method, HttpServletRequest.class, HttpServletResponse.class); action.setAccessible(true); action.invoke(this, req, resp); } catch (NoSuchMethodException e) { resp.sendError(HttpServletResponse.SC_NOT_FOUND, 找不到对应方法); } catch (Exception e) { throw new ServletException(e); } } }比如员工模块的 Servlet 写为public class EmployeeServlet extends BaseServlet { private EmployeeService employeeService new EmployeeServiceImpl(); public void list(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String pageNum req.getParameter(pageNum); String searchName req.getParameter(name); PageBeanEmployee page employeeService.findPage(Integer.parseInt(pageNum), 10, searchName); req.setAttribute(page, page); req.getRequestDispatcher(/WEB-INF/jsp/employee/list.jsp).forward(req, resp); } public void save(HttpServletRequest req, HttpServletResponse resp) throws Exception { Employee emp parseEmployeeFromRequest(req); boolean ok employeeService.saveOrUpdate(emp); resp.sendRedirect(req.getContextPath() /employee?methodlist); } }这样做的好处是新增一个功能只需要继承 BaseServlet 并添加一个方法URL 统一维护成 /employee?methodxxx无需在 web.xml 里配置一长串 Servlet 映射。5.2 数据库连接池与事务管理连接不直接 new、不用 DriverManager.getConnection而是从连接池获取用完归还避免频繁创建销毁连接。工具类大致如下public class DataSourceUtil { private static DruidDataSource dataSource new DruidDataSource(); static { dataSource.setDriverClassName(com.mysql.cj.jdbc.Driver); dataSource.setUrl(jdbc:mysql://localhost:3306/hrms?useUnicodetruecharacterEncodingUTF-8serverTimezoneAsia/Shanghai); dataSource.setUsername(root); dataSource.setPassword(your_password); dataSource.setInitialSize(5); dataSource.setMaxActive(20); } public static Connection getConnection() throws SQLException { return dataSource.getConnection(); } }事务管理的实现思路是每个请求开始时从连接池拿一个连接放到 ThreadLocal 里Service 层操作时统一获取请求结束在 Filter 的 finally 里提交或回滚。这个模式不复杂但请务必注意一个问题Filter 里的事务拦截器必须在权限过滤器之后执行而且 try/catch 要包住整个 filterChain.doFilter 调用否则事务提交发生在视图渲染之前就不可控了。5.3 密码加盐与登录拦截密码加密工具类里面保存加盐逻辑public class PasswordUtil { public static String generateSalt() { String chars abcdefghijklmnopqrstuvwxyz0123456789; StringBuilder sb new StringBuilder(); Random random new SecureRandom(); for (int i 0; i 8; i) { sb.append(chars.charAt(random.nextInt(chars.length()))); } return sb.toString(); } public static String encrypt(String password, String salt) { String src password salt; return DigestUtils.md5DigestAsHex(src.getBytes(StandardCharsets.UTF_8)); } public static boolean verify(String rawPassword, String salt, String encrypted) { return encrypt(rawPassword, salt).equals(encrypted); } }登录过滤器要排除登录页、静态资源其余请求全部检查 sessionpublic class LoginFilter implements Filter { private ListString excludedFields Arrays.asList(/login, /static); public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request (HttpServletRequest) req; HttpServletResponse response (HttpServletResponse) resp; String path request.getRequestURI().substring(request.getContextPath().length()); for (String prefix : excludedFields) { if (path.startsWith(prefix)) { chain.doFilter(req, resp); return; } } Object loginUser request.getSession().getAttribute(loginUser); if (loginUser null) { response.sendRedirect(request.getContextPath() /login.jsp); return; } chain.doFilter(req, resp); } }5.4 避免 SQL 注入所有用户输入都要过 PreparedStatement有朋友写业务代码时喜欢拼 SQL比如 String sql select * from employee where name like % name %这属于底线问题。所有带参数的查询必须用 PreparedStatement 的 ? 占位符以及 like 的写法为 name like ?传入参数 % name %。我在代码评审时看到拼 SQL 都是直接打回。另外数据字典下拉项也要从后端常量或数据库表读取不要硬编码到 JSP 页面里不然加一个学历类型就要改 JSP 重新编译。6. 部署上线与常见问题排查实录6.1 小企业服务器部署怎么操作内部系统部署到一台普通办公电脑或者小云主机即可。步骤可以不复杂Maven 执行 clean package 打出 war 包把 war 包复制到 Servlet 容器的 webapps 目录下启动容器等待自动解压部署。如果有独立数据库服务器就在生产库上执行建表脚本修改项目里数据库连接配置重新打包。如果只是共用一个库那要注意建表前先备份原有测试数据避免覆盖开发库。Linux 服务器上需要注意防火墙和端口问题。数据库端口不要对公网开放应用端口用一台 Nginx 反代并绑定域名。虽然小企业内部工具看起来不重要但最基本的网络安全底线还是要有。6.2 高频报错与排查方式驱动类找不到确认 pom 里数据库驱动依赖是否引入打包后有没有塞进 lib 目录。中文乱码按顺序检查数据库连接地址、数据库建库字符集、JSP 页面编码、Servlet 编码过滤器四个地方统一为 UTF-8。连接池获取连接超时检查数据库是否启动、账号密码、防火墙。页面 404检查 Servlet 映射路径和 JSP 在 WEB-INF 下的路径WEB-INF 下视图必须走 RequestDispatcher 转发不能直接浏览器访问。页面 500绝大多数是空指针或 SQL 错误。先看控制台完整堆栈别只看第一行“NullPointerException”要找到项目自己代码的那一层。文件上传为空MultipartConfig 注解是否加到 Servlet 上表单是否写了 enctypemultipart/form-data。登录后立刻失效session 作用域被误配置为 request或者浏览器开启了禁用 Cookie而容器默认使用 cookie 保存 session id。6.3 常见问题速查表现象可能原因处理建议插入中文变问号数据库连接字符集错误连接URL加 characterEncodingUTF-8JSP 页面中文乱码页面编码不对统一 pageEncodingUTF-8列表页数据库连接超时连接池配置过小调大 initialSize/maxActivefilter 中转发页面死循环登录过滤排除列表不全排除掉 login 和静态资源路径删除部门报错有子部门或员工引用Service 层先查询阻止删除工资计算结果对不上考勤日期边界问题用左闭右开区间查询导出文件中文乱码CSV 未写 BOM文件头加 UTF-8 BOM 字节从项目到经验我还想提的三件事第一件事代码永远比想象中需要更多时间尤其是联调和修改历史数据。人事系统上线最忙的不是开发而是把员工档案、考勤历史录入新系统的阶段。我在开发时顺手写了一个 SQL 初始化脚本把测试数据换成完全脱敏的模拟数据同时预留了 Excel 批量导入员工档案的入口这样上线时就能快速迁移。第二件事权限设计看起来简单但真正考验你的是“改需求时不会推倒重来”。我的角色表里留了 role_key 字段后续如果想增加“部门助理”这种角色只需要插入一行角色数据再在菜单权限表里配置不需要改 Java 代码。同理员工状态、请假类型、工资项类型都尽量用数据字典维护而不是硬编码常量。第三件事也是我踩过的最深一次坑本地测试一切正常部署到服务器后时间查询差八小时。原因出在时区参数连接字符串里加 serverTimezoneAsia/Shanghai 就解决了。这类环境差异问题在交付部署文档时一定要提前写明白。这套系统做下来最大的收获不是学会了某种框架而是理解了怎么把一个模糊的需求翻译成表结构、接口和页面。如果你想练 Java Web 的完整流程人事系统绝对比随便写两个接口有价值得多。先把数据关系理清楚再把事务边界切好最后把页面做顺手等这些基本功扎实了再引入 Spring Boot、MyBatis 或者更复杂的微服务方案会轻松很多。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑